Requests PDU validation (#139)
[e-mobility-charging-stations-simulator.git] / src / charging-station / ocpp / 1.6 / OCPP16IncomingRequestService.ts
index f51cc65526ecc684a99fd8045989cb6c982ea196..3238722568d9cc92bae51cd1476a1e90a10d0b77 100644 (file)
@@ -1,5 +1,30 @@
 // Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
 
+import fs from 'fs';
+import path from 'path';
+import { URL, fileURLToPath } from 'url';
+
+import { JSONSchemaType } from 'ajv';
+import { Client, FTPResponse } from 'basic-ftp';
+import tar from 'tar';
+
+import OCPPError from '../../../exception/OCPPError';
+import { JsonObject, JsonType } from '../../../types/JsonType';
+import { OCPP16ChargePointErrorCode } from '../../../types/ocpp/1.6/ChargePointErrorCode';
+import { OCPP16ChargePointStatus } from '../../../types/ocpp/1.6/ChargePointStatus';
+import {
+  ChargingProfilePurposeType,
+  OCPP16ChargingProfile,
+} from '../../../types/ocpp/1.6/ChargingProfile';
+import {
+  OCPP16StandardParametersKey,
+  OCPP16SupportedFeatureProfiles,
+} from '../../../types/ocpp/1.6/Configuration';
+import { OCPP16DiagnosticsStatus } from '../../../types/ocpp/1.6/DiagnosticsStatus';
+import {
+  OCPP16MeterValuesRequest,
+  OCPP16MeterValuesResponse,
+} from '../../../types/ocpp/1.6/MeterValues';
 import {
   ChangeAvailabilityRequest,
   ChangeConfigurationRequest,
@@ -10,6 +35,7 @@ import {
   MessageTrigger,
   OCPP16AvailabilityType,
   OCPP16BootNotificationRequest,
+  OCPP16ClearCacheRequest,
   OCPP16HeartbeatRequest,
   OCPP16IncomingRequestCommand,
   OCPP16RequestCommand,
@@ -35,11 +61,6 @@ import {
   SetChargingProfileResponse,
   UnlockConnectorResponse,
 } from '../../../types/ocpp/1.6/Responses';
-import {
-  ChargingProfilePurposeType,
-  OCPP16ChargingProfile,
-} from '../../../types/ocpp/1.6/ChargingProfile';
-import { Client, FTPResponse } from 'basic-ftp';
 import {
   OCPP16AuthorizationStatus,
   OCPP16AuthorizeRequest,
@@ -50,40 +71,24 @@ import {
   OCPP16StopTransactionRequest,
   OCPP16StopTransactionResponse,
 } from '../../../types/ocpp/1.6/Transaction';
-import {
-  OCPP16MeterValuesRequest,
-  OCPP16MeterValuesResponse,
-} from '../../../types/ocpp/1.6/MeterValues';
-import {
-  OCPP16StandardParametersKey,
-  OCPP16SupportedFeatureProfiles,
-} from '../../../types/ocpp/1.6/Configuration';
-
-import type ChargingStation from '../../ChargingStation';
-import { ChargingStationConfigurationUtils } from '../../ChargingStationConfigurationUtils';
-import Constants from '../../../utils/Constants';
-import { DefaultResponse } from '../../../types/ocpp/Responses';
+import { OCPPConfigurationKey } from '../../../types/ocpp/Configuration';
 import { ErrorType } from '../../../types/ocpp/ErrorType';
 import { IncomingRequestHandler } from '../../../types/ocpp/Requests';
-import { JsonType } from '../../../types/JsonType';
-import { OCPP16ChargePointErrorCode } from '../../../types/ocpp/1.6/ChargePointErrorCode';
-import { OCPP16ChargePointStatus } from '../../../types/ocpp/1.6/ChargePointStatus';
-import { OCPP16DiagnosticsStatus } from '../../../types/ocpp/1.6/DiagnosticsStatus';
-import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
-import { OCPPConfigurationKey } from '../../../types/ocpp/Configuration';
-import OCPPError from '../../../exception/OCPPError';
-import OCPPIncomingRequestService from '../OCPPIncomingRequestService';
-import { URL } from 'url';
-import Utils from '../../../utils/Utils';
-import fs from 'fs';
+import { DefaultResponse } from '../../../types/ocpp/Responses';
+import Constants from '../../../utils/Constants';
 import logger from '../../../utils/Logger';
-import path from 'path';
-import tar from 'tar';
+import Utils from '../../../utils/Utils';
+import type ChargingStation from '../../ChargingStation';
+import { ChargingStationConfigurationUtils } from '../../ChargingStationConfigurationUtils';
+import { ChargingStationUtils } from '../../ChargingStationUtils';
+import OCPPIncomingRequestService from '../OCPPIncomingRequestService';
+import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
 
 const moduleName = 'OCPP16IncomingRequestService';
 
 export default class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
   private incomingRequestHandlers: Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>;
+  private jsonSchemas: Map<OCPP16IncomingRequestCommand, JSONSchemaType<JsonObject>>;
 
   public constructor() {
     if (new.target?.name === moduleName) {
@@ -125,6 +130,152 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
       [OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, this.handleRequestGetDiagnostics.bind(this)],
       [OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, this.handleRequestTriggerMessage.bind(this)],
     ]);
+    this.jsonSchemas = new Map<OCPP16IncomingRequestCommand, JSONSchemaType<JsonObject>>([
+      [
+        OCPP16IncomingRequestCommand.RESET,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/Reset.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<ResetRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.CLEAR_CACHE,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/ClearCache.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16ClearCacheRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/UnlockConnector.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<UnlockConnectorRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.GET_CONFIGURATION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/GetConfiguration.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<GetConfigurationRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/ChangeConfiguration.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<ChangeConfigurationRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.GET_DIAGNOSTICS,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/GetDiagnostics.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<GetDiagnosticsRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/SetChargingProfile.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<SetChargingProfileRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/ClearChargingProfile.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<ClearChargingProfileRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/ChangeAvailability.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<ChangeAvailabilityRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/RemoteStartTransaction.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<RemoteStartTransactionRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/RemoteStopTransaction.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<RemoteStopTransactionRequest>,
+      ],
+      [
+        OCPP16IncomingRequestCommand.TRIGGER_MESSAGE,
+        JSON.parse(
+          fs.readFileSync(
+            path.resolve(
+              path.dirname(fileURLToPath(import.meta.url)),
+              '../../../assets/json-schemas/ocpp/1.6/TriggerMessage.json'
+            ),
+            'utf8'
+          )
+        ) as JSONSchemaType<OCPP16TriggerMessageRequest>,
+      ],
+    ]);
   }
 
   public async incomingRequestHandler(
@@ -142,20 +293,36 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
     ) {
       throw new OCPPError(
         ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle request payload ${JSON.stringify(
+        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
           commandPayload,
           null,
           2
         )} while the charging station is in pending state on the central server`,
-        commandName
+        commandName,
+        commandPayload
       );
     }
     if (
       chargingStation.isRegistered() ||
       (!chargingStation.getOcppStrictCompliance() && chargingStation.isInUnknownState())
     ) {
-      if (this.incomingRequestHandlers.has(commandName)) {
+      if (
+        this.incomingRequestHandlers.has(commandName) &&
+        ChargingStationUtils.isIncomingRequestCommandSupported(commandName, chargingStation)
+      ) {
         try {
+          if (this.jsonSchemas.has(commandName)) {
+            this.validateIncomingRequestPayload(
+              chargingStation,
+              commandName,
+              this.jsonSchemas.get(commandName),
+              commandPayload
+            );
+          } else {
+            logger.warn(
+              `${chargingStation.logPrefix()} ${moduleName}.incomingRequestHandler: No JSON schema found for command ${commandName} PDU validation`
+            );
+          }
           // Call the method to build the response
           response = await this.incomingRequestHandlers.get(commandName)(
             chargingStation,
@@ -170,23 +337,25 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
         // Throw exception
         throw new OCPPError(
           ErrorType.NOT_IMPLEMENTED,
-          `${commandName} is not implemented to handle request payload ${JSON.stringify(
+          `${commandName} is not implemented to handle request PDU ${JSON.stringify(
             commandPayload,
             null,
             2
           )}`,
-          commandName
+          commandName,
+          commandPayload
         );
       }
     } else {
       throw new OCPPError(
         ErrorType.SECURITY_ERROR,
-        `${commandName} cannot be issued to handle request payload ${JSON.stringify(
+        `${commandName} cannot be issued to handle request PDU ${JSON.stringify(
           commandPayload,
           null,
           2
         )} while the charging station is not registered on the central server.`,
-        commandName
+        commandName,
+        commandPayload
       );
     }
     // Send the built response
@@ -251,7 +420,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
         >(chargingStation, OCPP16RequestCommand.METER_VALUES, {
           connectorId,
           transactionId,
-          meterValue: transactionEndMeterValue,
+          meterValue: [transactionEndMeterValue],
         });
       }
       const stopResponse = await chargingStation.ocppRequestService.requestHandler<
@@ -333,23 +502,6 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
     chargingStation: ChargingStation,
     commandPayload: ChangeConfigurationRequest
   ): ChangeConfigurationResponse {
-    // JSON request fields type sanity check
-    if (!Utils.isString(commandPayload.key)) {
-      logger.error(
-        `${chargingStation.logPrefix()} ${
-          OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION
-        } request key field is not a string:`,
-        commandPayload
-      );
-    }
-    if (!Utils.isString(commandPayload.value)) {
-      logger.error(
-        `${chargingStation.logPrefix()} ${
-          OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION
-        } request value field is not a string:`,
-        commandPayload
-      );
-    }
     const keyToChange = ChargingStationConfigurationUtils.getConfigurationKey(
       chargingStation,
       commandPayload.key,
@@ -511,7 +663,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
                 clearCurrentCP = true;
               }
               if (clearCurrentCP) {
-                connectorStatus.chargingProfiles[index] = {} as OCPP16ChargingProfile;
+                connectorStatus.chargingProfiles.splice(index, 1);
                 logger.debug(
                   `${chargingStation.logPrefix()} Matching charging profile(s) cleared on connector id ${
                     commandPayload.connectorId
@@ -614,7 +766,11 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
           if (
             chargingStation.getLocalAuthListEnabled() &&
             chargingStation.hasAuthorizedTags() &&
-            chargingStation.authorizedTags.find((value) => value === commandPayload.idTag)
+            chargingStation.authorizedTagsCache
+              .getAuthorizedTags(
+                ChargingStationUtils.getAuthorizationFile(chargingStation.stationInfo)
+              )
+              .find((value) => value === commandPayload.idTag)
           ) {
             connectorStatus.localAuthorizeIdTag = commandPayload.idTag;
             connectorStatus.idTagLocalAuthorized = true;
@@ -834,7 +990,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
           >(chargingStation, OCPP16RequestCommand.METER_VALUES, {
             connectorId,
             transactionId,
-            meterValue: transactionEndMeterValue,
+            meterValue: [transactionEndMeterValue],
           });
         }
         await chargingStation.ocppRequestService.requestHandler<
@@ -848,7 +1004,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
         return Constants.OCPP_RESPONSE_ACCEPTED;
       }
     }
-    logger.info(
+    logger.warn(
       chargingStation.logPrefix() +
         ' Trying to remote stop a non existing transaction ' +
         transactionId.toString()
@@ -881,7 +1037,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
       let ftpClient: Client;
       try {
         const logFiles = fs
-          .readdirSync(path.resolve(__dirname, '../../../../'))
+          .readdirSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../'))
           .filter((file) => file.endsWith('.log'))
           .map((file) => path.join('./', file));
         const diagnosticsArchive = chargingStation.stationInfo.chargingStationId + '_logs.tar.gz';
@@ -910,7 +1066,10 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
             });
           });
           uploadResponse = await ftpClient.uploadFrom(
-            path.join(path.resolve(__dirname, '../../../../'), diagnosticsArchive),
+            path.join(
+              path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../'),
+              diagnosticsArchive
+            ),
             uri.pathname + diagnosticsArchive
           );
           if (uploadResponse.code === 226) {