Validate response PDU (#137)
authorJérôme Benoit <jerome.benoit@sap.com>
Thu, 18 Aug 2022 13:06:43 +0000 (15:06 +0200)
committerGitHub <noreply@github.com>
Thu, 18 Aug 2022 13:06:43 +0000 (15:06 +0200)
src/assets/json-schemas/ocpp/1.6/AuthorizeResponse.json [new file with mode: 0644]
src/assets/json-schemas/ocpp/1.6/BootNotificationResponse.json [new file with mode: 0644]
src/assets/json-schemas/ocpp/1.6/HeartbeatResponse.json [new file with mode: 0644]
src/assets/json-schemas/ocpp/1.6/MeterValuesResponse.json [new file with mode: 0644]
src/assets/json-schemas/ocpp/1.6/StartTransactionResponse.json [new file with mode: 0644]
src/assets/json-schemas/ocpp/1.6/StatusNotificationResponse.json [new file with mode: 0644]
src/assets/json-schemas/ocpp/1.6/StopTransactionResponse.json [new file with mode: 0644]
src/charging-station/ocpp/1.6/OCPP16ResponseService.ts
src/charging-station/ocpp/OCPPResponseService.ts
src/performance/PerformanceStatistics.ts
src/types/ocpp/1.6/MeterValues.ts

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 (file)
index 0000000..1d07daf
--- /dev/null
@@ -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 (file)
index 0000000..dae5400
--- /dev/null
@@ -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 (file)
index 0000000..ac679c6
--- /dev/null
@@ -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 (file)
index 0000000..5780cdd
--- /dev/null
@@ -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 (file)
index 0000000..0ff1b46
--- /dev/null
@@ -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 (file)
index 0000000..72b7dfb
--- /dev/null
@@ -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 (file)
index 0000000..fda9ade
--- /dev/null
@@ -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
+}
index 7eef0270865e752ed836f45051b97b3a8999f423..3372e1036c1ce464e7e1700fb87cca114177170a 100644 (file)
@@ -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<OCPP16RequestCommand, ResponseHandler>;
+  private bootNotificationResponseJsonSchema: JSONSchemaType<OCPP16BootNotificationResponse>;
+  private heartbeatResponseJsonSchema: JSONSchemaType<OCPP16HeartbeatResponse>;
+  private authorizeResponseJsonSchema: JSONSchemaType<OCPP16AuthorizeResponse>;
+  private startTransactionResponseJsonSchema: JSONSchemaType<OCPP16StartTransactionResponse>;
+  private stopTransactionResponseJsonSchema: JSONSchemaType<OCPP16StopTransactionResponse>;
+  private statusNotificationResponseJsonSchema: JSONSchemaType<OCPP16StatusNotificationResponse>;
+  private meterValuesResponseJsonSchema: JSONSchemaType<OCPP16MeterValuesResponse>;
 
   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<OCPP16BootNotificationResponse>;
+    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<OCPP16HeartbeatResponse>;
+    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<OCPP16AuthorizeResponse>;
+    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<OCPP16StartTransactionResponse>;
+    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<OCPP16StopTransactionResponse>;
+    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<OCPP16StatusNotificationResponse>;
+    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<OCPP16MeterValuesResponse>;
   }
 
   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<void> {
+    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<void> {
+    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
+    );
+  }
 }
index e2cad6451d1c08959e19fb9d33a6e62e3347cd87..905d972a3bc2348eee28bc6d9d347e4e8ec9e6b7 100644 (file)
@@ -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<T extends OCPPResponseService>(this: new () => T): T {
@@ -18,6 +27,31 @@ export default abstract class OCPPResponseService {
     return OCPPResponseService.instance as T;
   }
 
+  protected validateResponsePayload<T extends JsonType>(
+    chargingStation: ChargingStation,
+    commandName: RequestCommand,
+    schema: JSONSchemaType<T>,
+    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,
index 8dda0253198e30f69de4fa925aff324ffb02a065..8423fe4422602facfd17129fd4aecefb1c7af3f3 100644 (file)
@@ -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));
     }
index f8137c36782b2f704c0ea5ab20200e163d392ac1..0d9d3263f755d184639bef69d041b10142eca758 100644 (file)
@@ -83,7 +83,7 @@ export enum MeterValueFormat {
 }
 
 export interface OCPP16SampledValue extends JsonObject {
-  value?: string;
+  value: string;
   unit?: MeterValueUnit;
   context?: MeterValueContext;
   measurand?: OCPP16MeterValueMeasurand;