From b3fc3ff5bc50c2dbe20eb3ac1e681c00a022b4ee Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Sun, 8 Jan 2023 09:33:18 +0100 Subject: [PATCH] Fixes to OCPP commands PDU validation code: MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit + Validate helper for sent response + Simplify JSON schemas handling Signed-off-by: Jérôme Benoit --- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 4 +- .../ocpp/1.6/OCPP16RequestService.ts | 16 +---- .../ocpp/1.6/OCPP16ResponseService.ts | 9 ++- .../ocpp/2.0/OCPP20IncomingRequestService.ts | 4 +- .../ocpp/2.0/OCPP20RequestService.ts | 16 +---- .../ocpp/2.0/OCPP20ResponseService.ts | 13 +++- .../ocpp/OCPPIncomingRequestService.ts | 3 +- .../ocpp/OCPPRequestService.ts | 63 +++++++++++++++---- .../ocpp/OCPPResponseService.ts | 8 ++- 9 files changed, 84 insertions(+), 52 deletions(-) diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 93c59ba2..8b19902e 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -89,8 +89,8 @@ import { OCPP16ServiceUtils } from './OCPP16ServiceUtils'; const moduleName = 'OCPP16IncomingRequestService'; export default class OCPP16IncomingRequestService extends OCPPIncomingRequestService { + protected jsonSchemas: Map>; private incomingRequestHandlers: Map; - private jsonSchemas: Map>; 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; } diff --git a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts index 65866409..955dd085 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -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>; + protected jsonSchemas: Map>; 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 | 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( chargingStation: ChargingStation, commandName: OCPP16RequestCommand, diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 65cbefd3..061466e8 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -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 + >; + private responseHandlers: Map; private jsonSchemas: Map>; @@ -179,6 +185,7 @@ export default class OCPP16ResponseService extends OCPPResponseService { ) as JSONSchemaType, ], ]); + 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; } diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 96a66f1a..c3a241d4 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -26,8 +26,8 @@ import { OCPP20ServiceUtils } from './OCPP20ServiceUtils'; const moduleName = 'OCPP20IncomingRequestService'; export default class OCPP20IncomingRequestService extends OCPPIncomingRequestService { + protected jsonSchemas: Map>; private incomingRequestHandlers: Map; - private jsonSchemas: Map>; 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; } diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index 336327c9..bc056bef 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -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>; + protected jsonSchemas: Map>; 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 | 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( chargingStation: ChargingStation, commandName: OCPP20RequestCommand, diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts index 38b8c247..acef3c97 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -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 + >; + private responseHandlers: Map; private jsonSchemas: Map>; @@ -46,6 +54,7 @@ export default class OCPP20ResponseService extends OCPPResponseService { ) as JSONSchemaType, ], ]); + 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; } diff --git a/src/charging-station/ocpp/OCPPIncomingRequestService.ts b/src/charging-station/ocpp/OCPPIncomingRequestService.ts index 90c06d22..4b8c40f8 100644 --- a/src/charging-station/ocpp/OCPPIncomingRequestService.ts +++ b/src/charging-station/ocpp/OCPPIncomingRequestService.ts @@ -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>; protected constructor(version: OCPPVersion) { this.version = version; diff --git a/src/charging-station/ocpp/OCPPRequestService.ts b/src/charging-station/ocpp/OCPPRequestService.ts index e4890a91..6c0e7b0d 100644 --- a/src/charging-station/ocpp/OCPPRequestService.ts +++ b/src/charging-station/ocpp/OCPPRequestService.ts @@ -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>; protected constructor(version: OCPPVersion, ocppResponseService: OCPPResponseService) { this.version = version; @@ -128,7 +128,7 @@ export default abstract class OCPPRequestService { } } - protected validateRequestPayload( + protected validateRequestPayload( 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(payload); OCPPServiceUtils.convertDateToISOString(payload); if (validate(payload)) { return true; @@ -158,6 +161,47 @@ export default abstract class OCPPRequestService { ); } + protected validateResponsePayload( + 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(payload); + OCPPServiceUtils.convertDateToISOString(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; - - protected abstract getRequestPayloadValidationSchema( - chargingStation: ChargingStation, - commandName: RequestCommand | IncomingRequestCommand - ): JSONSchemaType | false; } diff --git a/src/charging-station/ocpp/OCPPResponseService.ts b/src/charging-station/ocpp/OCPPResponseService.ts index 97a13034..7cedf888 100644 --- a/src/charging-station/ocpp/OCPPResponseService.ts +++ b/src/charging-station/ocpp/OCPPResponseService.ts @@ -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 + >; protected constructor(version: OCPPVersion) { this.version = version; -- 2.34.1