From 844e496b3482e49145467af3f74df54811e91cb6 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Thu, 18 Aug 2022 15:06:43 +0200 Subject: [PATCH] Validate response PDU (#137) --- .../ocpp/1.6/AuthorizeResponse.json | 30 ++++ .../ocpp/1.6/BootNotificationResponse.json | 22 +++ .../ocpp/1.6/HeartbeatResponse.json | 14 ++ .../ocpp/1.6/MeterValuesResponse.json | 8 + .../ocpp/1.6/StartTransactionResponse.json | 33 +++++ .../ocpp/1.6/StatusNotificationResponse.json | 8 + .../ocpp/1.6/StopTransactionResponse.json | 29 ++++ .../ocpp/1.6/OCPP16ResponseService.ts | 140 +++++++++++++++++- .../ocpp/OCPPResponseService.ts | 36 ++++- src/performance/PerformanceStatistics.ts | 6 +- src/types/ocpp/1.6/MeterValues.ts | 2 +- 11 files changed, 319 insertions(+), 9 deletions(-) create mode 100644 src/assets/json-schemas/ocpp/1.6/AuthorizeResponse.json create mode 100644 src/assets/json-schemas/ocpp/1.6/BootNotificationResponse.json create mode 100644 src/assets/json-schemas/ocpp/1.6/HeartbeatResponse.json create mode 100644 src/assets/json-schemas/ocpp/1.6/MeterValuesResponse.json create mode 100644 src/assets/json-schemas/ocpp/1.6/StartTransactionResponse.json create mode 100644 src/assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json create mode 100644 src/assets/json-schemas/ocpp/1.6/StopTransactionResponse.json diff --git a/src/assets/json-schemas/ocpp/1.6/AuthorizeResponse.json b/src/assets/json-schemas/ocpp/1.6/AuthorizeResponse.json new file mode 100644 index 00000000..1d07daf5 --- /dev/null +++ b/src/assets/json-schemas/ocpp/1.6/AuthorizeResponse.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "urn:OCPP:1.6:2019:12:AuthorizeResponse", + "title": "AuthorizeResponse", + "type": "object", + "properties": { + "idTagInfo": { + "type": "object", + "properties": { + "expiryDate": { + "type": "string", + "format": "date-time" + }, + "parentIdTag": { + "type": "string", + "maxLength": 20 + }, + "status": { + "type": "string", + "additionalProperties": false, + "enum": ["Accepted", "Blocked", "Expired", "Invalid", "ConcurrentTx"] + } + }, + "additionalProperties": false, + "required": ["status"] + } + }, + "additionalProperties": false, + "required": ["idTagInfo"] +} diff --git a/src/assets/json-schemas/ocpp/1.6/BootNotificationResponse.json b/src/assets/json-schemas/ocpp/1.6/BootNotificationResponse.json new file mode 100644 index 00000000..dae5400f --- /dev/null +++ b/src/assets/json-schemas/ocpp/1.6/BootNotificationResponse.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "urn:OCPP:1.6:2019:12:BootNotificationResponse", + "title": "BootNotificationResponse", + "type": "object", + "properties": { + "status": { + "type": "string", + "additionalProperties": false, + "enum": ["Accepted", "Pending", "Rejected"] + }, + "currentTime": { + "type": "string", + "format": "date-time" + }, + "interval": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": ["status", "currentTime", "interval"] +} diff --git a/src/assets/json-schemas/ocpp/1.6/HeartbeatResponse.json b/src/assets/json-schemas/ocpp/1.6/HeartbeatResponse.json new file mode 100644 index 00000000..ac679c63 --- /dev/null +++ b/src/assets/json-schemas/ocpp/1.6/HeartbeatResponse.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "urn:OCPP:1.6:2019:12:HeartbeatResponse", + "title": "HeartbeatResponse", + "type": "object", + "properties": { + "currentTime": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false, + "required": ["currentTime"] +} diff --git a/src/assets/json-schemas/ocpp/1.6/MeterValuesResponse.json b/src/assets/json-schemas/ocpp/1.6/MeterValuesResponse.json new file mode 100644 index 00000000..5780cdd2 --- /dev/null +++ b/src/assets/json-schemas/ocpp/1.6/MeterValuesResponse.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "urn:OCPP:1.6:2019:12:MeterValuesResponse", + "title": "MeterValuesResponse", + "type": "object", + "properties": {}, + "additionalProperties": false +} diff --git a/src/assets/json-schemas/ocpp/1.6/StartTransactionResponse.json b/src/assets/json-schemas/ocpp/1.6/StartTransactionResponse.json new file mode 100644 index 00000000..0ff1b460 --- /dev/null +++ b/src/assets/json-schemas/ocpp/1.6/StartTransactionResponse.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "urn:OCPP:1.6:2019:12:StartTransactionResponse", + "title": "StartTransactionResponse", + "type": "object", + "properties": { + "idTagInfo": { + "type": "object", + "properties": { + "expiryDate": { + "type": "string", + "format": "date-time" + }, + "parentIdTag": { + "type": "string", + "maxLength": 20 + }, + "status": { + "type": "string", + "additionalProperties": false, + "enum": ["Accepted", "Blocked", "Expired", "Invalid", "ConcurrentTx"] + } + }, + "additionalProperties": false, + "required": ["status"] + }, + "transactionId": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": ["idTagInfo", "transactionId"] +} diff --git a/src/assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json b/src/assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json new file mode 100644 index 00000000..72b7dfbf --- /dev/null +++ b/src/assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "urn:OCPP:1.6:2019:12:StatusNotificationResponse", + "title": "StatusNotificationResponse", + "type": "object", + "properties": {}, + "additionalProperties": false +} diff --git a/src/assets/json-schemas/ocpp/1.6/StopTransactionResponse.json b/src/assets/json-schemas/ocpp/1.6/StopTransactionResponse.json new file mode 100644 index 00000000..fda9ade0 --- /dev/null +++ b/src/assets/json-schemas/ocpp/1.6/StopTransactionResponse.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "urn:OCPP:1.6:2019:12:StopTransactionResponse", + "title": "StopTransactionResponse", + "type": "object", + "properties": { + "idTagInfo": { + "type": "object", + "properties": { + "expiryDate": { + "type": "string", + "format": "date-time" + }, + "parentIdTag": { + "type": "string", + "maxLength": 20 + }, + "status": { + "type": "string", + "additionalProperties": false, + "enum": ["Accepted", "Blocked", "Expired", "Invalid", "ConcurrentTx"] + } + }, + "additionalProperties": false, + "required": ["status"] + } + }, + "additionalProperties": false +} diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 7eef0270..3372e103 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -1,5 +1,11 @@ // Partial Copyright Jerome Benoit. 2021. All Rights Reserved. +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +import { JSONSchemaType } from 'ajv'; + import OCPPError from '../../../exception/OCPPError'; import { JsonType } from '../../../types/JsonType'; import { OCPP16ChargePointErrorCode } from '../../../types/ocpp/1.6/ChargePointErrorCode'; @@ -16,6 +22,7 @@ import { } from '../../../types/ocpp/1.6/Requests'; import { OCPP16BootNotificationResponse, + OCPP16HeartbeatResponse, OCPP16RegistrationStatus, OCPP16StatusNotificationResponse, } from '../../../types/ocpp/1.6/Responses'; @@ -42,6 +49,13 @@ const moduleName = 'OCPP16ResponseService'; export default class OCPP16ResponseService extends OCPPResponseService { private responseHandlers: Map; + private bootNotificationResponseJsonSchema: JSONSchemaType; + private heartbeatResponseJsonSchema: JSONSchemaType; + private authorizeResponseJsonSchema: JSONSchemaType; + private startTransactionResponseJsonSchema: JSONSchemaType; + private stopTransactionResponseJsonSchema: JSONSchemaType; + private statusNotificationResponseJsonSchema: JSONSchemaType; + private meterValuesResponseJsonSchema: JSONSchemaType; public constructor() { if (new.target?.name === moduleName) { @@ -57,6 +71,69 @@ export default class OCPP16ResponseService extends OCPPResponseService { [OCPP16RequestCommand.STATUS_NOTIFICATION, this.handleResponseStatusNotification.bind(this)], [OCPP16RequestCommand.METER_VALUES, this.handleResponseMeterValues.bind(this)], ]); + this.bootNotificationResponseJsonSchema = JSON.parse( + fs.readFileSync( + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../assets/json-schemas/ocpp/1.6/BootNotificationResponse.json' + ), + 'utf8' + ) + ) as JSONSchemaType; + this.heartbeatResponseJsonSchema = JSON.parse( + fs.readFileSync( + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../assets/json-schemas/ocpp/1.6/HeartbeatResponse.json' + ), + 'utf8' + ) + ) as JSONSchemaType; + this.authorizeResponseJsonSchema = JSON.parse( + fs.readFileSync( + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../assets/json-schemas/ocpp/1.6/AuthorizeResponse.json' + ), + 'utf8' + ) + ) as JSONSchemaType; + this.startTransactionResponseJsonSchema = JSON.parse( + fs.readFileSync( + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../assets/json-schemas/ocpp/1.6/StartTransactionResponse.json' + ), + 'utf8' + ) + ) as JSONSchemaType; + this.stopTransactionResponseJsonSchema = JSON.parse( + fs.readFileSync( + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../assets/json-schemas/ocpp/1.6/StopTransactionResponse.json' + ), + 'utf8' + ) + ) as JSONSchemaType; + this.statusNotificationResponseJsonSchema = JSON.parse( + fs.readFileSync( + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json' + ), + 'utf8' + ) + ) as JSONSchemaType; + this.meterValuesResponseJsonSchema = JSON.parse( + fs.readFileSync( + path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '../../../assets/json-schemas/ocpp/1.6/MeterValuesResponse.json' + ), + 'utf8' + ) + ) as JSONSchemaType; } public async responseHandler( @@ -107,6 +184,12 @@ export default class OCPP16ResponseService extends OCPPResponseService { chargingStation: ChargingStation, payload: OCPP16BootNotificationResponse ): void { + this.validateResponsePayload( + chargingStation, + OCPP16RequestCommand.BOOT_NOTIFICATION, + this.bootNotificationResponseJsonSchema, + payload + ); if (payload.status === OCPP16RegistrationStatus.ACCEPTED) { ChargingStationConfigurationUtils.addConfigurationKey( chargingStation, @@ -142,14 +225,29 @@ export default class OCPP16ResponseService extends OCPPResponseService { } } - // eslint-disable-next-line @typescript-eslint/no-empty-function - private handleResponseHeartbeat(): void {} + private handleResponseHeartbeat( + chargingStation: ChargingStation, + payload: OCPP16HeartbeatResponse + ): void { + this.validateResponsePayload( + chargingStation, + OCPP16RequestCommand.HEARTBEAT, + this.heartbeatResponseJsonSchema, + payload + ); + } private handleResponseAuthorize( chargingStation: ChargingStation, payload: OCPP16AuthorizeResponse, requestPayload: OCPP16AuthorizeRequest ): void { + this.validateResponsePayload( + chargingStation, + OCPP16RequestCommand.AUTHORIZE, + this.authorizeResponseJsonSchema, + payload + ); let authorizeConnectorId: number; for (const connectorId of chargingStation.connectors.keys()) { if ( @@ -183,6 +281,12 @@ export default class OCPP16ResponseService extends OCPPResponseService { payload: OCPP16StartTransactionResponse, requestPayload: OCPP16StartTransactionRequest ): Promise { + this.validateResponsePayload( + chargingStation, + OCPP16RequestCommand.START_TRANSACTION, + this.startTransactionResponseJsonSchema, + payload + ); const connectorId = requestPayload.connectorId; let transactionConnectorId: number; @@ -392,6 +496,12 @@ export default class OCPP16ResponseService extends OCPPResponseService { payload: OCPP16StopTransactionResponse, requestPayload: OCPP16StopTransactionRequest ): Promise { + this.validateResponsePayload( + chargingStation, + OCPP16RequestCommand.STOP_TRANSACTION, + this.stopTransactionResponseJsonSchema, + payload + ); const transactionConnectorId = chargingStation.getConnectorIdByTransactionId( requestPayload.transactionId ); @@ -472,9 +582,27 @@ export default class OCPP16ResponseService extends OCPPResponseService { } } - // eslint-disable-next-line @typescript-eslint/no-empty-function - private handleResponseStatusNotification(): void {} + private handleResponseStatusNotification( + chargingStation: ChargingStation, + payload: OCPP16StatusNotificationResponse + ): void { + this.validateResponsePayload( + chargingStation, + OCPP16RequestCommand.STATUS_NOTIFICATION, + this.statusNotificationResponseJsonSchema, + payload + ); + } - // eslint-disable-next-line @typescript-eslint/no-empty-function - private handleResponseMeterValues(): void {} + private handleResponseMeterValues( + chargingStation: ChargingStation, + payload: OCPP16MeterValuesResponse + ): void { + this.validateResponsePayload( + chargingStation, + OCPP16RequestCommand.METER_VALUES, + this.meterValuesResponseJsonSchema, + payload + ); + } } diff --git a/src/charging-station/ocpp/OCPPResponseService.ts b/src/charging-station/ocpp/OCPPResponseService.ts index e2cad645..905d972a 100644 --- a/src/charging-station/ocpp/OCPPResponseService.ts +++ b/src/charging-station/ocpp/OCPPResponseService.ts @@ -1,14 +1,23 @@ +import { JSONSchemaType } from 'ajv'; +import Ajv from 'ajv-draft-04'; +import ajvFormats from 'ajv-formats'; + +import OCPPError from '../../exception/OCPPError'; import { JsonType } from '../../types/JsonType'; +import { ErrorType } from '../../types/ocpp/ErrorType'; import { RequestCommand } from '../../types/ocpp/Requests'; +import logger from '../../utils/Logger'; import type ChargingStation from '../ChargingStation'; const moduleName = 'OCPPResponseService'; export default abstract class OCPPResponseService { private static instance: OCPPResponseService | null = null; + private ajv: Ajv; protected constructor() { - // This is intentional + this.ajv = new Ajv(); + ajvFormats(this.ajv); } public static getInstance(this: new () => T): T { @@ -18,6 +27,31 @@ export default abstract class OCPPResponseService { return OCPPResponseService.instance as T; } + protected validateResponsePayload( + chargingStation: ChargingStation, + commandName: RequestCommand, + schema: JSONSchemaType, + payload: T + ): boolean { + if (!chargingStation.getPayloadSchemaValidation()) { + return true; + } + const validate = this.ajv.compile(schema); + if (validate(payload)) { + return true; + } + logger.error( + `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: Response PDU is invalid: %j`, + validate.errors + ); + throw new OCPPError( + ErrorType.FORMATION_VIOLATION, + 'Response PDU is invalid', + commandName, + JSON.stringify(validate.errors, null, 2) + ); + } + public abstract responseHandler( chargingStation: ChargingStation, commandName: RequestCommand, diff --git a/src/performance/PerformanceStatistics.ts b/src/performance/PerformanceStatistics.ts index 8dda0253..8423fe44 100644 --- a/src/performance/PerformanceStatistics.ts +++ b/src/performance/PerformanceStatistics.ts @@ -38,7 +38,11 @@ export default class PerformanceStatistics { }; } - public static getInstance(objId: string, objName: string, uri: URL): PerformanceStatistics { + public static getInstance( + objId: string, + objName: string, + uri: URL + ): PerformanceStatistics | undefined { if (!PerformanceStatistics.instances.has(objId)) { PerformanceStatistics.instances.set(objId, new PerformanceStatistics(objId, objName, uri)); } diff --git a/src/types/ocpp/1.6/MeterValues.ts b/src/types/ocpp/1.6/MeterValues.ts index f8137c36..0d9d3263 100644 --- a/src/types/ocpp/1.6/MeterValues.ts +++ b/src/types/ocpp/1.6/MeterValues.ts @@ -83,7 +83,7 @@ export enum MeterValueFormat { } export interface OCPP16SampledValue extends JsonObject { - value?: string; + value: string; unit?: MeterValueUnit; context?: MeterValueContext; measurand?: OCPP16MeterValueMeasurand; -- 2.34.1