Fixes to OCPP commands PDU validation code:
authorJérôme Benoit <jerome.benoit@sap.com>
Sun, 8 Jan 2023 08:33:18 +0000 (09:33 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Sun, 8 Jan 2023 08:33:18 +0000 (09:33 +0100)
+ Validate helper for sent response
+ Simplify JSON schemas handling

Signed-off-by: Jérôme Benoit <jerome.benoit@sap.com>
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 93c59ba29cfcedaa63f334270f4f2ca37766372f..8b19902eb609ea7594ff471e22aaf95b00910d80 100644 (file)
@@ -89,8 +89,8 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
 const moduleName = 'OCPP16IncomingRequestService';
 
 export default class OCPP16IncomingRequestService extends OCPPIncomingRequestService {
+  protected jsonSchemas: Map<OCPP16IncomingRequestCommand, JSONSchemaType<JsonObject>>;
   private incomingRequestHandlers: Map<OCPP16IncomingRequestCommand, IncomingRequestHandler>;
-  private jsonSchemas: Map<OCPP16IncomingRequestCommand, JSONSchemaType<JsonObject>>;
 
   public constructor() {
     if (new.target?.name === moduleName) {
@@ -403,7 +403,7 @@ export default class OCPP16IncomingRequestService extends OCPPIncomingRequestSer
       );
     }
     logger.warn(
-      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
+      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command '${commandName}' PDU validation`
     );
     return false;
   }
index 658664092d71451b5e088bd3e3390fba6deb69c8..955dd08510add96008d3d87bf5f0fa25922cb132 100644 (file)
@@ -26,7 +26,6 @@ import { ErrorType } from '../../../types/ocpp/ErrorType';
 import { OCPPVersion } from '../../../types/ocpp/OCPPVersion';
 import type { RequestParams } from '../../../types/ocpp/Requests';
 import Constants from '../../../utils/Constants';
-import logger from '../../../utils/Logger';
 import Utils from '../../../utils/Utils';
 import type ChargingStation from '../../ChargingStation';
 import OCPPRequestService from '../OCPPRequestService';
@@ -36,7 +35,7 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
 const moduleName = 'OCPP16RequestService';
 
 export default class OCPP16RequestService extends OCPPRequestService {
-  private jsonSchemas: Map<OCPP16RequestCommand, JSONSchemaType<JsonObject>>;
+  protected jsonSchemas: Map<OCPP16RequestCommand, JSONSchemaType<JsonObject>>;
 
   public constructor(ocppResponseService: OCPPResponseService) {
     if (new.target?.name === moduleName) {
@@ -185,19 +184,6 @@ 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,
index 65cbefd3e40bcd63e9c9f2fe1e4221ef8c5cc61d..061466e8df4b8e4ce5df5440e793836021f18f71 100644 (file)
@@ -17,6 +17,7 @@ import type {
 } from '../../../types/ocpp/1.6/MeterValues';
 import {
   type OCPP16BootNotificationRequest,
+  OCPP16IncomingRequestCommand,
   OCPP16RequestCommand,
   type OCPP16StatusNotificationRequest,
 } from '../../../types/ocpp/1.6/Requests';
@@ -50,6 +51,11 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils';
 const moduleName = 'OCPP16ResponseService';
 
 export default class OCPP16ResponseService extends OCPPResponseService {
+  public jsonIncomingRequestResponseSchemas: Map<
+    OCPP16IncomingRequestCommand,
+    JSONSchemaType<JsonObject>
+  >;
+
   private responseHandlers: Map<OCPP16RequestCommand, ResponseHandler>;
   private jsonSchemas: Map<OCPP16RequestCommand, JSONSchemaType<JsonObject>>;
 
@@ -179,6 +185,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
         ) as JSONSchemaType<OCPP16DataTransferResponse>,
       ],
     ]);
+    this.jsonIncomingRequestResponseSchemas = new Map();
     this.validatePayload.bind(this);
   }
 
@@ -247,7 +254,7 @@ export default class OCPP16ResponseService extends OCPPResponseService {
       );
     }
     logger.warn(
-      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
+      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command '${commandName}' PDU validation`
     );
     return false;
   }
index 96a66f1ad01d39b9fa8c72d161f0c3ed2c23c6b6..c3a241d4874b170addeb60dc9d992c4eda5f5817 100644 (file)
@@ -26,8 +26,8 @@ import { OCPP20ServiceUtils } from './OCPP20ServiceUtils';
 const moduleName = 'OCPP20IncomingRequestService';
 
 export default class OCPP20IncomingRequestService extends OCPPIncomingRequestService {
+  protected jsonSchemas: Map<OCPP20IncomingRequestCommand, JSONSchemaType<JsonObject>>;
   private incomingRequestHandlers: Map<OCPP20IncomingRequestCommand, IncomingRequestHandler>;
-  private jsonSchemas: Map<OCPP20IncomingRequestCommand, JSONSchemaType<JsonObject>>;
 
   public constructor() {
     if (new.target?.name === moduleName) {
@@ -150,7 +150,7 @@ export default class OCPP20IncomingRequestService extends OCPPIncomingRequestSer
       );
     }
     logger.warn(
-      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
+      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command '${commandName}' PDU validation`
     );
     return false;
   }
index 336327c9cfde468287e60eb9b4c579bccb83caf2..bc056bef13541e3b00f3cde9af16371ab6140765 100644 (file)
@@ -15,7 +15,6 @@ import {
 import { ErrorType } from '../../../types/ocpp/ErrorType';
 import { OCPPVersion } from '../../../types/ocpp/OCPPVersion';
 import type { RequestParams } from '../../../types/ocpp/Requests';
-import logger from '../../../utils/Logger';
 import Utils from '../../../utils/Utils';
 import type ChargingStation from '../../ChargingStation';
 import OCPPRequestService from '../OCPPRequestService';
@@ -25,7 +24,7 @@ import { OCPP20ServiceUtils } from './OCPP20ServiceUtils';
 const moduleName = 'OCPP20RequestService';
 
 export default class OCPP20RequestService extends OCPPRequestService {
-  private jsonSchemas: Map<OCPP20RequestCommand, JSONSchemaType<JsonObject>>;
+  protected jsonSchemas: Map<OCPP20RequestCommand, JSONSchemaType<JsonObject>>;
 
   public constructor(ocppResponseService: OCPPResponseService) {
     if (new.target?.name === moduleName) {
@@ -78,19 +77,6 @@ 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,
index 38b8c24791056293b58c85c8cae0fdd8bbdc632c..acef3c9778774b4baea79609f6ca7506c0be4a8e 100644 (file)
@@ -8,7 +8,10 @@ import type { JSONSchemaType } from 'ajv';
 
 import OCPPError from '../../../exception/OCPPError';
 import type { JsonObject, JsonType } from '../../../types/JsonType';
-import { OCPP20RequestCommand } from '../../../types/ocpp/2.0/Requests';
+import {
+  OCPP20IncomingRequestCommand,
+  OCPP20RequestCommand,
+} from '../../../types/ocpp/2.0/Requests';
 import type { OCPP20BootNotificationResponse } from '../../../types/ocpp/2.0/Responses';
 import { ErrorType } from '../../../types/ocpp/ErrorType';
 import { OCPPVersion } from '../../../types/ocpp/OCPPVersion';
@@ -21,6 +24,11 @@ import { OCPP20ServiceUtils } from './OCPP20ServiceUtils';
 const moduleName = 'OCPP20ResponseService';
 
 export default class OCPP20ResponseService extends OCPPResponseService {
+  public jsonIncomingRequestResponseSchemas: Map<
+    OCPP20IncomingRequestCommand,
+    JSONSchemaType<JsonObject>
+  >;
+
   private responseHandlers: Map<OCPP20RequestCommand, ResponseHandler>;
   private jsonSchemas: Map<OCPP20RequestCommand, JSONSchemaType<JsonObject>>;
 
@@ -46,6 +54,7 @@ export default class OCPP20ResponseService extends OCPPResponseService {
         ) as JSONSchemaType<OCPP20BootNotificationResponse>,
       ],
     ]);
+    this.jsonIncomingRequestResponseSchemas = new Map();
     this.validatePayload.bind(this);
   }
 
@@ -114,7 +123,7 @@ export default class OCPP20ResponseService extends OCPPResponseService {
       );
     }
     logger.warn(
-      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command ${commandName} PDU validation`
+      `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema found for command '${commandName}' PDU validation`
     );
     return false;
   }
index 90c06d22393f048865e55c1018015ff875a98595..4b8c40f806b6c5482cb418ec71368c125df3c2d2 100644 (file)
@@ -5,7 +5,7 @@ import ajvFormats from 'ajv-formats';
 
 import OCPPError from '../../exception/OCPPError';
 import type { HandleErrorParams } from '../../types/Error';
-import type { JsonType } from '../../types/JsonType';
+import type { JsonObject, JsonType } from '../../types/JsonType';
 import type { OCPPVersion } from '../../types/ocpp/OCPPVersion';
 import type { IncomingRequestCommand } from '../../types/ocpp/Requests';
 import logger from '../../utils/Logger';
@@ -19,6 +19,7 @@ export default abstract class OCPPIncomingRequestService {
   protected asyncResource: AsyncResource;
   private readonly version: OCPPVersion;
   private readonly ajv: Ajv;
+  protected abstract jsonSchemas: Map<IncomingRequestCommand, JSONSchemaType<JsonObject>>;
 
   protected constructor(version: OCPPVersion) {
     this.version = version;
index e4890a91d8a6a88e2c8d282fcd603c2b2c31baef..6c0e7b0db4c571064ffb4ceacf474841df0df526 100644 (file)
@@ -32,8 +32,8 @@ export default abstract class OCPPRequestService {
   private static instance: OCPPRequestService | null = null;
   private readonly version: OCPPVersion;
   private readonly ajv: Ajv;
-
   private readonly ocppResponseService: OCPPResponseService;
+  protected abstract jsonSchemas: Map<RequestCommand, JSONSchemaType<JsonObject>>;
 
   protected constructor(version: OCPPVersion, ocppResponseService: OCPPResponseService) {
     this.version = version;
@@ -128,7 +128,7 @@ export default abstract class OCPPRequestService {
     }
   }
 
-  protected validateRequestPayload<T extends JsonType>(
+  protected validateRequestPayload<T extends JsonObject>(
     chargingStation: ChargingStation,
     commandName: RequestCommand | IncomingRequestCommand,
     payload: T
@@ -136,11 +136,14 @@ export default abstract class OCPPRequestService {
     if (chargingStation.getPayloadSchemaValidation() === false) {
       return true;
     }
-    const schema = this.getRequestPayloadValidationSchema(chargingStation, commandName);
-    if (schema === false) {
+    if (this.jsonSchemas.has(commandName as RequestCommand) === false) {
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema found for command '${commandName}' PDU validation`
+      );
       return true;
     }
-    const validate = this.ajv.compile(schema);
+    const validate = this.ajv.compile(this.jsonSchemas.get(commandName as RequestCommand));
+    payload = Utils.cloneObject<T>(payload);
     OCPPServiceUtils.convertDateToISOString<T>(payload);
     if (validate(payload)) {
       return true;
@@ -158,6 +161,47 @@ export default abstract class OCPPRequestService {
     );
   }
 
+  protected validateResponsePayload<T extends JsonObject>(
+    chargingStation: ChargingStation,
+    commandName: RequestCommand | IncomingRequestCommand,
+    payload: T
+  ): boolean {
+    if (chargingStation.getPayloadSchemaValidation() === false) {
+      return true;
+    }
+    if (
+      this.ocppResponseService.jsonIncomingRequestResponseSchemas.has(
+        commandName as IncomingRequestCommand
+      ) === false
+    ) {
+      logger.warn(
+        `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: No JSON schema found for command '${commandName}' PDU validation`
+      );
+      return true;
+    }
+    const validate = this.ajv.compile(
+      this.ocppResponseService.jsonIncomingRequestResponseSchemas.get(
+        commandName as IncomingRequestCommand
+      )
+    );
+    payload = Utils.cloneObject<T>(payload);
+    OCPPServiceUtils.convertDateToISOString<T>(payload);
+    if (validate(payload)) {
+      return true;
+    }
+    logger.error(
+      `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: Command '${commandName}' reponse PDU is invalid: %j`,
+      validate.errors
+    );
+    // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError().
+    throw new OCPPError(
+      OCPPServiceUtils.ajvErrorsToErrorType(validate.errors),
+      'Response PDU is invalid',
+      commandName,
+      JSON.stringify(validate.errors, null, 2)
+    );
+  }
+
   private async internalSendMessage(
     chargingStation: ChargingStation,
     messageId: string,
@@ -334,7 +378,7 @@ export default abstract class OCPPRequestService {
           commandName,
           messagePayload as JsonType,
         ]);
-        this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonType);
+        this.validateRequestPayload(chargingStation, commandName, messagePayload as JsonObject);
         messageToSend = JSON.stringify([
           messageType,
           messageId,
@@ -345,7 +389,7 @@ export default abstract class OCPPRequestService {
       // Response
       case MessageType.CALL_RESULT_MESSAGE:
         // Build response
-        // FIXME: Validate response payload
+        this.validateResponsePayload(chargingStation, commandName, messagePayload as JsonObject);
         messageToSend = JSON.stringify([messageType, messageId, messagePayload] as Response);
         break;
       // Error Message
@@ -393,9 +437,4 @@ export default abstract class OCPPRequestService {
     commandParams?: JsonType,
     params?: RequestParams
   ): Promise<ResType>;
-
-  protected abstract getRequestPayloadValidationSchema(
-    chargingStation: ChargingStation,
-    commandName: RequestCommand | IncomingRequestCommand
-  ): JSONSchemaType<JsonObject> | false;
 }
index 97a130341efe1f669ef2a242325f5d76cdc2205e..7cedf888ed367399fa242a221f18384b8f11b00f 100644 (file)
@@ -2,9 +2,9 @@ import Ajv, { type JSONSchemaType } from 'ajv';
 import ajvFormats from 'ajv-formats';
 
 import OCPPError from '../../exception/OCPPError';
-import type { JsonType } from '../../types/JsonType';
+import type { JsonObject, JsonType } from '../../types/JsonType';
 import type { OCPPVersion } from '../../types/ocpp/OCPPVersion';
-import type { RequestCommand } from '../../types/ocpp/Requests';
+import type { IncomingRequestCommand, RequestCommand } from '../../types/ocpp/Requests';
 import logger from '../../utils/Logger';
 import type ChargingStation from '../ChargingStation';
 import { OCPPServiceUtils } from './OCPPServiceUtils';
@@ -15,6 +15,10 @@ export default abstract class OCPPResponseService {
   private static instance: OCPPResponseService | null = null;
   private readonly version: OCPPVersion;
   private readonly ajv: Ajv;
+  public abstract jsonIncomingRequestResponseSchemas: Map<
+    IncomingRequestCommand,
+    JSONSchemaType<JsonObject>
+  >;
 
   protected constructor(version: OCPPVersion) {
     this.version = version;