Fixes to OCPP command payload validation:
authorJérôme Benoit <jerome.benoit@sap.com>
Sat, 7 Jan 2023 16:12:43 +0000 (17:12 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sat, 7 Jan 2023 16:12:43 +0000 (17:12 +0100)
+ Fix multipleOf precision
+ Convert to ISO 8601 string date object in payload before validation

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
src/charging-station/ChargingStation.ts
src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts
src/charging-station/ocpp/1.6/OCPP16RequestService.ts
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts
src/charging-station/ocpp/2.0/OCPP20RequestService.ts
src/charging-station/ocpp/2.0/OCPP20ResponseService.ts
src/charging-station/ocpp/OCPPIncomingRequestService.ts
src/charging-station/ocpp/OCPPRequestService.ts
src/charging-station/ocpp/OCPPResponseService.ts

index c3c984f85ae464029ad2bb89a1e904c02c1dd752..587ca02ff143d391c222b07ca11a9f81fdf7cb70 100644 (file)
@@ -1780,7 +1780,7 @@ export default class ChargingStation {
           logger.error(
             `${this.logPrefix()} Charging profile id ${
               matchingChargingProfile.chargingProfileId
-            } limit is greater than connector id ${connectorId} maximum, dump charging profiles' stack: %j`,
+            } limit ${limit} is greater than connector id ${connectorId} maximum ${connectorMaximumPower}, dump charging profiles' stack: %j`,
             this.getConnectorStatus(connectorId).chargingProfiles
           );
           limit = connectorMaximumPower;
index 377405f41faf3fbcdce410104d22eebd579b098c..92bd399c4b6f05bb6f3dd56878bbaa1ace75f6d6 100644 (file)
@@ -382,7 +382,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
     commandName: OCPP16IncomingRequestCommand,
     commandPayload: JsonType
   ): boolean {
-    if (this.jsonSchemas.has(commandName)) {
+    if (this.jsonSchemas.has(commandName) === true) {
       return this.validateIncomingRequestPayload(
         chargingStation,
         commandName,
index f28f5f9476502ecfd58736d58a6a5083c4b9185d..658664092d71451b5e088bd3e3390fba6deb69c8 100644 (file)
@@ -154,7 +154,6 @@ export default class OCPP16RequestService extends OCPPRequestService {
       ],
     ]);
     this.buildRequestPayload.bind(this);
-    this.validatePayload.bind(this);
   }
 
   public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
@@ -169,7 +168,6 @@ export default class OCPP16RequestService extends OCPPRequestService {
         commandName,
         commandParams
       );
-      this.validatePayload(chargingStation, commandName, requestPayload);
       return (await this.sendMessage(
         chargingStation,
         Utils.generateUUID(),
@@ -187,6 +185,19 @@ export default class OCPP16RequestService extends OCPPRequestService {
     );
   }
 
+  protected getRequestPayloadValidationSchema(
+    chargingStation: ChargingStation,
+    commandName: OCPP16RequestCommand
+  ): JSONSchemaType<JsonObject> | false {
+    if (this.jsonSchemas.has(commandName) === true) {
+      return this.jsonSchemas.get(commandName);
+    }
+    logger.warn(
+      `${chargingStation.logPrefix()} ${moduleName}.getPayloadValidationSchema: No JSON schema found for command ${commandName} PDU validation`
+    );
+    return false;
+  }
+
   private buildRequestPayload<Request extends JsonType>(
     chargingStation: ChargingStation,
     commandName: OCPP16RequestCommand,
@@ -295,23 +306,4 @@ export default class OCPP16RequestService extends OCPPRequestService {
         );
     }
   }
-
-  private validatePayload<Request extends JsonType>(
-    chargingStation: ChargingStation,
-    commandName: OCPP16RequestCommand,
-    requestPayload: Request
-  ): boolean {
-    if (this.jsonSchemas.has(commandName)) {
-      return this.validateRequestPayload(
-        chargingStation,
-        commandName,
-        this.jsonSchemas.get(commandName),
-        requestPayload
-      );
-    }
-    logger.warn(
-      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
-    );
-    return false;
-  }
 }
index 7898ce6e1448d5ae4692839fb9488e737ba07516..65cbefd3e40bcd63e9c9f2fe1e4221ef8c5cc61d 100644 (file)
@@ -238,7 +238,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
     commandName: OCPP16RequestCommand,
     payload: JsonType
   ): boolean {
-    if (this.jsonSchemas.has(commandName)) {
+    if (this.jsonSchemas.has(commandName) === true) {
       return this.validateResponsePayload(
         chargingStation,
         commandName,
index 890d28d1c51141a05f022b106fa70425daa33e7e..96a66f1ad01d39b9fa8c72d161f0c3ed2c23c6b6 100644 (file)
@@ -141,7 +141,7 @@ export default class OCPP20IncomingRequestService extends OCPPIncomingRequestSer
     commandName: OCPP20IncomingRequestCommand,
     commandPayload: JsonType
   ): boolean {
-    if (this.jsonSchemas.has(commandName)) {
+    if (this.jsonSchemas.has(commandName) === true) {
       return this.validateIncomingRequestPayload(
         chargingStation,
         commandName,
index e23b1e76fcc3e1fbf9b64f2e61f3d44f0fdb98d2..eb98bd154ffc11068e9f2a86343cbb93aa9e1beb 100644 (file)
@@ -47,7 +47,6 @@ export default class OCPP20RequestService extends OCPPRequestService {
       ],
     ]);
     this.buildRequestPayload.bind(this);
-    this.validatePayload.bind(this);
   }
 
   public async requestHandler<RequestType extends JsonType, ResponseType extends JsonType>(
@@ -62,7 +61,6 @@ export default class OCPP20RequestService extends OCPPRequestService {
         commandName,
         commandParams
       );
-      this.validatePayload(chargingStation, commandName, requestPayload);
       return (await this.sendMessage(
         chargingStation,
         Utils.generateUUID(),
@@ -80,6 +78,19 @@ export default class OCPP20RequestService extends OCPPRequestService {
     );
   }
 
+  protected getRequestPayloadValidationSchema(
+    chargingStation: ChargingStation,
+    commandName: OCPP20RequestCommand
+  ): JSONSchemaType<JsonObject> | false {
+    if (this.jsonSchemas.has(commandName) === true) {
+      return this.jsonSchemas.get(commandName);
+    }
+    logger.warn(
+      `${chargingStation.logPrefix()} ${moduleName}.getPayloadValidationSchema: No JSON schema found for command ${commandName} PDU validation`
+    );
+    return false;
+  }
+
   private buildRequestPayload<Request extends JsonType>(
     chargingStation: ChargingStation,
     commandName: OCPP20RequestCommand,
@@ -121,23 +132,4 @@ export default class OCPP20RequestService extends OCPPRequestService {
         );
     }
   }
-
-  private validatePayload<Request extends JsonType>(
-    chargingStation: ChargingStation,
-    commandName: OCPP20RequestCommand,
-    requestPayload: Request
-  ): boolean {
-    if (this.jsonSchemas.has(commandName)) {
-      return this.validateRequestPayload(
-        chargingStation,
-        commandName,
-        this.jsonSchemas.get(commandName),
-        requestPayload
-      );
-    }
-    logger.warn(
-      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
-    );
-    return false;
-  }
 }
index d4636f6f3ca25319ef5ad020bd4b29425733e0d8..38b8c24791056293b58c85c8cae0fdd8bbdc632c 100644 (file)
@@ -105,7 +105,7 @@ export default class OCPP20ResponseService extends OCPPResponseService {
     commandName: OCPP20RequestCommand,
     payload: JsonType
   ): boolean {
-    if (this.jsonSchemas.has(commandName)) {
+    if (this.jsonSchemas.has(commandName) === true) {
       return this.validateResponsePayload(
         chargingStation,
         commandName,
index 6015d4d9b8037956e1434ae23b934fd4f98bad6e..952e740e7f472c7484a36b8f0efb45175cbc5e30 100644 (file)
@@ -22,7 +22,9 @@ export default abstract class OCPPIncomingRequestService {
 
   protected constructor(version: OCPPVersion) {
     this.version = version;
-    this.ajv = new Ajv();
+    this.ajv = new Ajv({
+      multipleOfPrecision: 2,
+    });
     ajvFormats(this.ajv);
     this.asyncResource = new AsyncResource(moduleName);
     this.incomingRequestHandler.bind(this);
@@ -71,7 +73,7 @@ export default abstract class OCPPIncomingRequestService {
       return true;
     }
     logger.error(
-      `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestPayload: Incoming request PDU is invalid: %j`,
+      `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestPayload: Command '${commandName}' incoming request PDU is invalid: %j`,
       validate.errors
     );
     throw new OCPPError(
index c089c86a1af4092dd57e1deb281dab48c9d7769a..6b58e2adba0b0b06fbb514a2960d3d6eb80c3fe6 100644 (file)
@@ -37,7 +37,9 @@ export default abstract class OCPPRequestService {
 
   protected constructor(version: OCPPVersion, ocppResponseService: OCPPResponseService) {
     this.version = version;
-    this.ajv = new Ajv();
+    this.ajv = new Ajv({
+      multipleOfPrecision: 2,
+    });
     ajvFormats(this.ajv);
     this.ocppResponseService = ocppResponseService;
     this.requestHandler.bind(this);
@@ -127,19 +129,23 @@ export default abstract class OCPPRequestService {
 
   protected validateRequestPayload<T extends JsonType>(
     chargingStation: ChargingStation,
-    commandName: RequestCommand,
-    schema: JSONSchemaType<T>,
+    commandName: RequestCommand | IncomingRequestCommand,
     payload: T
   ): boolean {
     if (chargingStation.getPayloadSchemaValidation() === false) {
       return true;
     }
+    const schema = this.getRequestPayloadValidationSchema(chargingStation, commandName);
+    if (schema === false) {
+      return true;
+    }
     const validate = this.ajv.compile(schema);
+    this.convertDateToISOString<T>(payload);
     if (validate(payload)) {
       return true;
     }
     logger.error(
-      `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Request PDU is invalid: %j`,
+      `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`,
       validate.errors
     );
     // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
@@ -327,6 +333,7 @@ export default abstract class OCPPRequestService {
           commandName,
           messagePayload as JsonType,
         ]);
+        this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonType);
         messageToSend = JSON.stringify([
           messageType,
           messageId,
@@ -337,6 +344,7 @@ export default abstract class OCPPRequestService {
       // Response
       case MessageType.CALL_RESULT_MESSAGE:
         // Build response
+        // FIXME: Validate response payload
         messageToSend = JSON.stringify([messageType, messageId, messagePayload] as Response);
         break;
       // Error Message
@@ -377,6 +385,16 @@ export default abstract class OCPPRequestService {
     }
   }
 
+  private convertDateToISOString<T extends JsonType>(obj: T): void {
+    for (const k in obj) {
+      if (obj[k] instanceof Date) {
+        (obj as JsonObject)[k] = (obj[k] as Date).toISOString();
+      } else if (obj[k] !== null && typeof obj[k] === 'object') {
+        this.convertDateToISOString<T>(obj[k] as T);
+      }
+    }
+  }
+
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   public abstract requestHandler<ReqType extends JsonType, ResType extends JsonType>(
     chargingStation: ChargingStation,
@@ -384,4 +402,9 @@ export default abstract class OCPPRequestService {
     commandParams?: JsonType,
     params?: RequestParams
   ): Promise<ResType>;
+
+  protected abstract getRequestPayloadValidationSchema(
+    chargingStation: ChargingStation,
+    commandName: RequestCommand | IncomingRequestCommand
+  ): JSONSchemaType<JsonObject> | false;
 }
index 414eb8a8ac6ade9c7c7fafa4d0032b9a2d9ea0f9..adcdacaeeef0e0a6d128211dad74514e196215f2 100644 (file)
@@ -18,7 +18,9 @@ export default abstract class OCPPResponseService {
 
   protected constructor(version: OCPPVersion) {
     this.version = version;
-    this.ajv = new Ajv();
+    this.ajv = new Ajv({
+      multipleOfPrecision: 2,
+    });
     ajvFormats(this.ajv);
     this.responseHandler.bind(this);
     this.validateResponsePayload.bind(this);
@@ -45,7 +47,7 @@ export default abstract class OCPPResponseService {
       return true;
     }
     logger.error(
-      `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: Response PDU is invalid: %j`,
+      `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: Command '${commandName}' response PDU is invalid: %j`,
       validate.errors
     );
     throw new OCPPError(