From 0c8357f86de845c860f6ff1cb30e1cfaa8e5643a Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Thu, 12 Mar 2026 23:31:30 +0100 Subject: [PATCH] refactor(ocpp): harmonize JSON schema payload validation across OCPP stacks - Absorb missing-validator null guard into base class methods, eliminating 4 identical private validatePayload wrappers and their bind() calls - Remove dead code (unreachable isValid branches in ResponseService wrappers) - Make parseJsonSchemaFile throw on failure instead of silently returning {}, preventing validators that accept everything as a security bypass - Add dual-path schema resolution for production (esbuild) and test (tsx) - Align OCPP 1.6 schema map construction with OCPP 2.0 registry pattern, replacing ~200 lines of verbose inline entries with declarative registries - Collapse 4 identical payload options factory methods into 1 per version --- .../ocpp/1.6/OCPP16IncomingRequestService.ts | 28 +- .../ocpp/1.6/OCPP16RequestService.ts | 2 +- .../ocpp/1.6/OCPP16ResponseService.ts | 43 +-- .../ocpp/1.6/OCPP16ServiceUtils.ts | 322 ++++-------------- .../ocpp/2.0/OCPP20IncomingRequestService.ts | 28 +- .../ocpp/2.0/OCPP20RequestService.ts | 2 +- .../ocpp/2.0/OCPP20ResponseService.ts | 43 +-- .../ocpp/2.0/OCPP20ServiceUtils.ts | 51 +-- .../ocpp/OCPPIncomingRequestService.ts | 14 +- .../ocpp/OCPPRequestService.ts | 32 +- .../ocpp/OCPPResponseService.ts | 14 +- src/charging-station/ocpp/OCPPServiceUtils.ts | 38 ++- 12 files changed, 136 insertions(+), 481 deletions(-) diff --git a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts index 9fe6c662..d2d3a3b9 100644 --- a/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16IncomingRequestService.ts @@ -149,7 +149,7 @@ const moduleName = 'OCPP16IncomingRequestService' * 3. Request routed to appropriate handler method * 4. Business logic executed with charging station state management * 5. Response payload validated and sent back to Central System - * @see {@link validatePayload} Request payload validation method + * @see {@link validateIncomingRequestPayload} Request payload validation method * @see {@link handleRequestRemoteStartTransaction} Example request handler */ @@ -235,7 +235,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { ]) this.payloadValidatorFunctions = OCPP16ServiceUtils.createPayloadValidatorMap( OCPP16ServiceUtils.createIncomingRequestPayloadConfigs(), - OCPP16ServiceUtils.createIncomingRequestPayloadOptions(moduleName, 'constructor'), + OCPP16ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv ) // Handle incoming request events @@ -429,7 +429,6 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { } } ) - this.validatePayload = this.validatePayload.bind(this) } // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters @@ -468,7 +467,7 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { OCPP16ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName) ) { try { - this.validatePayload(chargingStation, commandName, commandPayload) + this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload) // Call the method to build the response // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)! @@ -1660,25 +1659,4 @@ export class OCPP16IncomingRequestService extends OCPPIncomingRequestService { await chargingStation.reset(OCPP16StopTransactionReason.REBOOT) } } - - /** - * Validates incoming OCPP 1.6 request payload against JSON schema - * @param chargingStation - The charging station instance processing the request - * @param commandName - OCPP 1.6 command name to validate against - * @param commandPayload - JSON payload to validate - * @returns True if payload validation succeeds, false otherwise - */ - private validatePayload ( - chargingStation: ChargingStation, - commandName: OCPP16IncomingRequestCommand, - commandPayload: JsonType - ): boolean { - if (this.payloadValidatorFunctions.has(commandName)) { - return this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload) - } - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function 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 b0c7bc0a..5fe55dc8 100644 --- a/src/charging-station/ocpp/1.6/OCPP16RequestService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16RequestService.ts @@ -55,7 +55,7 @@ export class OCPP16RequestService extends OCPPRequestService { super(OCPPVersion.VERSION_16, ocppResponseService) this.payloadValidatorFunctions = OCPP16ServiceUtils.createPayloadValidatorMap( OCPP16ServiceUtils.createRequestPayloadConfigs(), - OCPP16ServiceUtils.createRequestPayloadOptions(moduleName, 'constructor'), + OCPP16ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv ) this.buildRequestPayload = this.buildRequestPayload.bind(this) diff --git a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts index 104057c4..fce62a82 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ResponseService.ts @@ -70,7 +70,7 @@ const moduleName = 'OCPP16ResponseService' * 3. Response routed to appropriate handler based on original request type * 4. Charging station state updated based on response content * 5. Any follow-up actions triggered (transactions, status changes, etc.) - * @see {@link validatePayload} Response payload validation method + * @see {@link validateResponsePayload} Response payload validation method * @see {@link handleResponse} Response processing methods */ @@ -114,16 +114,15 @@ export class OCPP16ResponseService extends OCPPResponseService { ]) this.payloadValidatorFunctions = OCPP16ServiceUtils.createPayloadValidatorMap( OCPP16ServiceUtils.createResponsePayloadConfigs(), - OCPP16ServiceUtils.createResponsePayloadOptions(moduleName, 'constructor'), + OCPP16ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv ) this.incomingRequestResponsePayloadValidateFunctions = OCPP16ServiceUtils.createPayloadValidatorMap( OCPP16ServiceUtils.createIncomingRequestResponsePayloadConfigs(), - OCPP16ServiceUtils.createIncomingRequestResponsePayloadOptions(moduleName, 'constructor'), + OCPP16ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajvIncomingRequest ) - this.validatePayload = this.validatePayload.bind(this) } // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters @@ -145,7 +144,7 @@ export class OCPP16ResponseService extends OCPPResponseService { OCPP16ServiceUtils.isRequestCommandSupported(chargingStation, commandName) ) { try { - this.validatePayload(chargingStation, commandName, payload) + this.validateResponsePayload(chargingStation, commandName, payload) logger.debug( `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handling '${commandName}' response` ) @@ -602,38 +601,4 @@ export class OCPP16ResponseService extends OCPPResponseService { resetConnectorStatus(connectorStatus) await OCPP16ServiceUtils.restoreConnectorStatus(chargingStation, connectorId, connectorStatus) } - - /** - * Validates incoming OCPP 1.6 response payload against JSON schema - * @param chargingStation - The charging station instance receiving the response - * @param commandName - OCPP 1.6 command name to validate against - * @param payload - JSON response payload to validate - * @returns True if payload validation succeeds, false otherwise - */ - private validatePayload ( - chargingStation: ChargingStation, - commandName: OCPP16RequestCommand, - payload: JsonType - ): boolean { - if (this.payloadValidatorFunctions.has(commandName)) { - logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: Validating '${commandName}' response payload` - ) - const isValid = this.validateResponsePayload(chargingStation, commandName, payload) - if (!isValid) { - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: '${commandName}' response payload validation failed` - ) - } else { - logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: '${commandName}' response payload validation successful` - ) - } - return isValid - } - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation` - ) - return false - } } diff --git a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts index 7cde8008..996c2f45 100644 --- a/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts +++ b/src/charging-station/ocpp/1.6/OCPP16ServiceUtils.ts @@ -44,6 +44,42 @@ import { OCPP16Constants } from './OCPP16Constants.js' const moduleName = 'OCPP16ServiceUtils' export class OCPP16ServiceUtils extends OCPPServiceUtils { + private static readonly incomingRequestSchemaNames: readonly [ + OCPP16IncomingRequestCommand, + string + ][] = [ + [OCPP16IncomingRequestCommand.CANCEL_RESERVATION, 'CancelReservation'], + [OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY, 'ChangeAvailability'], + [OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION, 'ChangeConfiguration'], + [OCPP16IncomingRequestCommand.CLEAR_CACHE, 'ClearCache'], + [OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE, 'ClearChargingProfile'], + [OCPP16IncomingRequestCommand.DATA_TRANSFER, 'DataTransfer'], + [OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE, 'GetCompositeSchedule'], + [OCPP16IncomingRequestCommand.GET_CONFIGURATION, 'GetConfiguration'], + [OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, 'GetDiagnostics'], + [OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION, 'RemoteStartTransaction'], + [OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION, 'RemoteStopTransaction'], + [OCPP16IncomingRequestCommand.RESERVE_NOW, 'ReserveNow'], + [OCPP16IncomingRequestCommand.RESET, 'Reset'], + [OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE, 'SetChargingProfile'], + [OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, 'TriggerMessage'], + [OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, 'UnlockConnector'], + [OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, 'UpdateFirmware'], + ] + + private static readonly outgoingRequestSchemaNames: readonly [OCPP16RequestCommand, string][] = [ + [OCPP16RequestCommand.AUTHORIZE, 'Authorize'], + [OCPP16RequestCommand.BOOT_NOTIFICATION, 'BootNotification'], + [OCPP16RequestCommand.DATA_TRANSFER, 'DataTransfer'], + [OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, 'DiagnosticsStatusNotification'], + [OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, 'FirmwareStatusNotification'], + [OCPP16RequestCommand.HEARTBEAT, 'Heartbeat'], + [OCPP16RequestCommand.METER_VALUES, 'MeterValues'], + [OCPP16RequestCommand.START_TRANSACTION, 'StartTransaction'], + [OCPP16RequestCommand.STATUS_NOTIFICATION, 'StatusNotification'], + [OCPP16RequestCommand.STOP_TRANSACTION, 'StopTransaction'], + ] + public static buildTransactionBeginMeterValue ( chargingStation: ChargingStation, connectorId: number, @@ -386,87 +422,11 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { public static createIncomingRequestPayloadConfigs = (): [ OCPP16IncomingRequestCommand, { schemaPath: string } - ][] => [ - [ - OCPP16IncomingRequestCommand.CANCEL_RESERVATION, - OCPP16ServiceUtils.PayloadValidatorConfig('CancelReservation.json'), - ], - [ - OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY, - OCPP16ServiceUtils.PayloadValidatorConfig('ChangeAvailability.json'), - ], - [ - OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION, - OCPP16ServiceUtils.PayloadValidatorConfig('ChangeConfiguration.json'), - ], - [ - OCPP16IncomingRequestCommand.CLEAR_CACHE, - OCPP16ServiceUtils.PayloadValidatorConfig('ClearCache.json'), - ], - [ - OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE, - OCPP16ServiceUtils.PayloadValidatorConfig('ClearChargingProfile.json'), - ], - [ - OCPP16IncomingRequestCommand.DATA_TRANSFER, - OCPP16ServiceUtils.PayloadValidatorConfig('DataTransfer.json'), - ], - [ - OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE, - OCPP16ServiceUtils.PayloadValidatorConfig('GetCompositeSchedule.json'), - ], - [ - OCPP16IncomingRequestCommand.GET_CONFIGURATION, - OCPP16ServiceUtils.PayloadValidatorConfig('GetConfiguration.json'), - ], - [ - OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, - OCPP16ServiceUtils.PayloadValidatorConfig('GetDiagnostics.json'), - ], - [ - OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION, - OCPP16ServiceUtils.PayloadValidatorConfig('RemoteStartTransaction.json'), - ], - [ - OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION, - OCPP16ServiceUtils.PayloadValidatorConfig('RemoteStopTransaction.json'), - ], - [ - OCPP16IncomingRequestCommand.RESERVE_NOW, - OCPP16ServiceUtils.PayloadValidatorConfig('ReserveNow.json'), - ], - [OCPP16IncomingRequestCommand.RESET, OCPP16ServiceUtils.PayloadValidatorConfig('Reset.json')], - [ - OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE, - OCPP16ServiceUtils.PayloadValidatorConfig('SetChargingProfile.json'), - ], - [ - OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, - OCPP16ServiceUtils.PayloadValidatorConfig('TriggerMessage.json'), - ], - [ - OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, - OCPP16ServiceUtils.PayloadValidatorConfig('UnlockConnector.json'), - ], - [ - OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, - OCPP16ServiceUtils.PayloadValidatorConfig('UpdateFirmware.json'), - ], - ] - - /** - * Factory options for OCPP 1.6 Incoming Request Service - * @param moduleName - Name of the OCPP module - * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 1.6 incoming request validators - */ - public static createIncomingRequestPayloadOptions = (moduleName: string, methodName: string) => - OCPP16ServiceUtils.PayloadValidatorOptions( - OCPPVersion.VERSION_16, - 'assets/json-schemas/ocpp/1.6', - moduleName, - methodName - ) + ][] => + OCPP16ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [ + command, + OCPP16ServiceUtils.PayloadValidatorConfig(`${schemaBase}.json`), + ]) /** * OCPP 1.6 Incoming Request Response Service validator configurations @@ -475,87 +435,19 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { public static createIncomingRequestResponsePayloadConfigs = (): [ OCPP16IncomingRequestCommand, { schemaPath: string } - ][] => [ - [ - OCPP16IncomingRequestCommand.CANCEL_RESERVATION, - OCPP16ServiceUtils.PayloadValidatorConfig('CancelReservationResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.CHANGE_AVAILABILITY, - OCPP16ServiceUtils.PayloadValidatorConfig('ChangeAvailabilityResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.CHANGE_CONFIGURATION, - OCPP16ServiceUtils.PayloadValidatorConfig('ChangeConfigurationResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.CLEAR_CACHE, - OCPP16ServiceUtils.PayloadValidatorConfig('ClearCacheResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.CLEAR_CHARGING_PROFILE, - OCPP16ServiceUtils.PayloadValidatorConfig('ClearChargingProfileResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.DATA_TRANSFER, - OCPP16ServiceUtils.PayloadValidatorConfig('DataTransferResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.GET_COMPOSITE_SCHEDULE, - OCPP16ServiceUtils.PayloadValidatorConfig('GetCompositeScheduleResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.GET_CONFIGURATION, - OCPP16ServiceUtils.PayloadValidatorConfig('GetConfigurationResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.GET_DIAGNOSTICS, - OCPP16ServiceUtils.PayloadValidatorConfig('GetDiagnosticsResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.REMOTE_START_TRANSACTION, - OCPP16ServiceUtils.PayloadValidatorConfig('RemoteStartTransactionResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.REMOTE_STOP_TRANSACTION, - OCPP16ServiceUtils.PayloadValidatorConfig('RemoteStopTransactionResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.RESERVE_NOW, - OCPP16ServiceUtils.PayloadValidatorConfig('ReserveNowResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.RESET, - OCPP16ServiceUtils.PayloadValidatorConfig('ResetResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.SET_CHARGING_PROFILE, - OCPP16ServiceUtils.PayloadValidatorConfig('SetChargingProfileResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.TRIGGER_MESSAGE, - OCPP16ServiceUtils.PayloadValidatorConfig('TriggerMessageResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.UNLOCK_CONNECTOR, - OCPP16ServiceUtils.PayloadValidatorConfig('UnlockConnectorResponse.json'), - ], - [ - OCPP16IncomingRequestCommand.UPDATE_FIRMWARE, - OCPP16ServiceUtils.PayloadValidatorConfig('UpdateFirmwareResponse.json'), - ], - ] + ][] => + OCPP16ServiceUtils.incomingRequestSchemaNames.map(([command, schemaBase]) => [ + command, + OCPP16ServiceUtils.PayloadValidatorConfig(`${schemaBase}Response.json`), + ]) /** - * Factory options for OCPP 1.6 Incoming Request Response Service + * Factory options for OCPP 1.6 payload validators * @param moduleName - Name of the OCPP module * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 1.6 incoming request response validators + * @returns Factory options object for OCPP 1.6 validators */ - public static createIncomingRequestResponsePayloadOptions = ( - moduleName: string, - methodName: string - ) => + public static createPayloadOptions = (moduleName: string, methodName: string) => OCPP16ServiceUtils.PayloadValidatorOptions( OCPPVersion.VERSION_16, 'assets/json-schemas/ocpp/1.6', @@ -570,56 +462,11 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { public static createRequestPayloadConfigs = (): [ OCPP16RequestCommand, { schemaPath: string } - ][] => [ - [OCPP16RequestCommand.AUTHORIZE, OCPP16ServiceUtils.PayloadValidatorConfig('Authorize.json')], - [ - OCPP16RequestCommand.BOOT_NOTIFICATION, - OCPP16ServiceUtils.PayloadValidatorConfig('BootNotification.json'), - ], - [ - OCPP16RequestCommand.DATA_TRANSFER, - OCPP16ServiceUtils.PayloadValidatorConfig('DataTransfer.json'), - ], - [ - OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, - OCPP16ServiceUtils.PayloadValidatorConfig('DiagnosticsStatusNotification.json'), - ], - [ - OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, - OCPP16ServiceUtils.PayloadValidatorConfig('FirmwareStatusNotification.json'), - ], - [OCPP16RequestCommand.HEARTBEAT, OCPP16ServiceUtils.PayloadValidatorConfig('Heartbeat.json')], - [ - OCPP16RequestCommand.METER_VALUES, - OCPP16ServiceUtils.PayloadValidatorConfig('MeterValues.json'), - ], - [ - OCPP16RequestCommand.START_TRANSACTION, - OCPP16ServiceUtils.PayloadValidatorConfig('StartTransaction.json'), - ], - [ - OCPP16RequestCommand.STATUS_NOTIFICATION, - OCPP16ServiceUtils.PayloadValidatorConfig('StatusNotification.json'), - ], - [ - OCPP16RequestCommand.STOP_TRANSACTION, - OCPP16ServiceUtils.PayloadValidatorConfig('StopTransaction.json'), - ], - ] - - /** - * Factory options for OCPP 1.6 Request Service - * @param moduleName - Name of the OCPP module - * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 1.6 validators - */ - public static createRequestPayloadOptions = (moduleName: string, methodName: string) => - OCPP16ServiceUtils.PayloadValidatorOptions( - OCPPVersion.VERSION_16, - 'assets/json-schemas/ocpp/1.6', - moduleName, - methodName - ) + ][] => + OCPP16ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [ + command, + OCPP16ServiceUtils.PayloadValidatorConfig(`${schemaBase}.json`), + ]) /** * OCPP 1.6 Response Service validator configurations @@ -628,62 +475,11 @@ export class OCPP16ServiceUtils extends OCPPServiceUtils { public static createResponsePayloadConfigs = (): [ OCPP16RequestCommand, { schemaPath: string } - ][] => [ - [ - OCPP16RequestCommand.AUTHORIZE, - OCPP16ServiceUtils.PayloadValidatorConfig('AuthorizeResponse.json'), - ], - [ - OCPP16RequestCommand.BOOT_NOTIFICATION, - OCPP16ServiceUtils.PayloadValidatorConfig('BootNotificationResponse.json'), - ], - [ - OCPP16RequestCommand.DATA_TRANSFER, - OCPP16ServiceUtils.PayloadValidatorConfig('DataTransferResponse.json'), - ], - [ - OCPP16RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION, - OCPP16ServiceUtils.PayloadValidatorConfig('DiagnosticsStatusNotificationResponse.json'), - ], - [ - OCPP16RequestCommand.FIRMWARE_STATUS_NOTIFICATION, - OCPP16ServiceUtils.PayloadValidatorConfig('FirmwareStatusNotificationResponse.json'), - ], - [ - OCPP16RequestCommand.HEARTBEAT, - OCPP16ServiceUtils.PayloadValidatorConfig('HeartbeatResponse.json'), - ], - [ - OCPP16RequestCommand.METER_VALUES, - OCPP16ServiceUtils.PayloadValidatorConfig('MeterValuesResponse.json'), - ], - [ - OCPP16RequestCommand.START_TRANSACTION, - OCPP16ServiceUtils.PayloadValidatorConfig('StartTransactionResponse.json'), - ], - [ - OCPP16RequestCommand.STATUS_NOTIFICATION, - OCPP16ServiceUtils.PayloadValidatorConfig('StatusNotificationResponse.json'), - ], - [ - OCPP16RequestCommand.STOP_TRANSACTION, - OCPP16ServiceUtils.PayloadValidatorConfig('StopTransactionResponse.json'), - ], - ] - - /** - * Factory options for OCPP 1.6 Response Service - * @param moduleName - Name of the OCPP module - * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 1.6 response validators - */ - public static createResponsePayloadOptions = (moduleName: string, methodName: string) => - OCPP16ServiceUtils.PayloadValidatorOptions( - OCPPVersion.VERSION_16, - 'assets/json-schemas/ocpp/1.6', - moduleName, - methodName - ) + ][] => + OCPP16ServiceUtils.outgoingRequestSchemaNames.map(([command, schemaBase]) => [ + command, + OCPP16ServiceUtils.PayloadValidatorConfig(`${schemaBase}Response.json`), + ]) public static hasReservation = ( chargingStation: ChargingStation, diff --git a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts index 89bc7e84..6d179205 100644 --- a/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.ts @@ -243,7 +243,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { ]) this.payloadValidatorFunctions = OCPP20ServiceUtils.createPayloadValidatorMap( OCPP20ServiceUtils.createIncomingRequestPayloadConfigs(), - OCPP20ServiceUtils.createIncomingRequestPayloadOptions(moduleName, 'constructor'), + OCPP20ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv ) // Handle incoming request events @@ -407,7 +407,6 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { } } ) - this.validatePayload = this.validatePayload.bind(this) } public handleRequestGetVariables ( @@ -586,7 +585,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { OCPP20ServiceUtils.isIncomingRequestCommandSupported(chargingStation, commandName) ) { try { - this.validatePayload(chargingStation, commandName, commandPayload) + this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const incomingRequestHandler = this.incomingRequestHandlers.get(commandName)! if (isAsyncFunction(incomingRequestHandler)) { @@ -3374,27 +3373,6 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { ) return true } - - /** - * Validates incoming OCPP 2.0 request payload against JSON schema - * @param chargingStation - The charging station instance processing the request - * @param commandName - OCPP 2.0 command name to validate against - * @param commandPayload - JSON payload to validate - * @returns True if payload validation succeeds, false otherwise - */ - private validatePayload ( - chargingStation: ChargingStation, - commandName: OCPP20IncomingRequestCommand, - commandPayload: JsonType - ): boolean { - if (this.payloadValidatorFunctions.has(commandName)) { - return this.validateIncomingRequestPayload(chargingStation, commandName, commandPayload) - } - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation` - ) - return false - } } /** @@ -3436,7 +3414,7 @@ export class OCPP20IncomingRequestService extends OCPPIncomingRequestService { * 3. Request routed to appropriate handler method * 4. Business logic executed with variable model integration * 5. Response payload validated and sent back to CSMS - * @see {@link validatePayload} Request payload validation method + * @see {@link validateIncomingRequestPayload} Request payload validation method * @see {@link handleRequestStartTransaction} Example OCPP 2.0+ request handler * @see {@link OCPP20VariableManager} Variable management integration */ diff --git a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts index af2be2aa..1dae156f 100644 --- a/src/charging-station/ocpp/2.0/OCPP20RequestService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20RequestService.ts @@ -80,7 +80,7 @@ export class OCPP20RequestService extends OCPPRequestService { super(OCPPVersion.VERSION_201, ocppResponseService) this.payloadValidatorFunctions = OCPP20ServiceUtils.createPayloadValidatorMap( OCPP20ServiceUtils.createRequestPayloadConfigs(), - OCPP20ServiceUtils.createRequestPayloadOptions(moduleName, 'constructor'), + OCPP20ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv ) this.buildRequestPayload = this.buildRequestPayload.bind(this) diff --git a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts index e64ca67d..134163a8 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ResponseService.ts @@ -68,7 +68,7 @@ const moduleName = 'OCPP20ResponseService' * 3. Response routed to appropriate handler based on original request type * 4. Charging station state and variable model updated based on response content * 5. Enhanced follow-up actions triggered based on OCPP 2.0+ capabilities - * @see {@link validatePayload} Response payload validation method + * @see {@link validateResponsePayload} Response payload validation method * @see {@link handleResponse} Response processing methods * @see {@link OCPP20VariableManager} Variable management integration */ @@ -125,16 +125,15 @@ export class OCPP20ResponseService extends OCPPResponseService { ]) this.payloadValidatorFunctions = OCPP20ServiceUtils.createPayloadValidatorMap( OCPP20ServiceUtils.createResponsePayloadConfigs(), - OCPP20ServiceUtils.createResponsePayloadOptions(moduleName, 'constructor'), + OCPP20ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajv ) this.incomingRequestResponsePayloadValidateFunctions = OCPP20ServiceUtils.createPayloadValidatorMap( OCPP20ServiceUtils.createIncomingRequestResponsePayloadConfigs(), - OCPP20ServiceUtils.createIncomingRequestResponsePayloadOptions(moduleName, 'constructor'), + OCPP20ServiceUtils.createPayloadOptions(moduleName, 'constructor'), this.ajvIncomingRequest ) - this.validatePayload = this.validatePayload.bind(this) } // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters @@ -156,7 +155,7 @@ export class OCPP20ResponseService extends OCPPResponseService { OCPP20ServiceUtils.isRequestCommandSupported(chargingStation, commandName) ) { try { - this.validatePayload(chargingStation, commandName, payload) + this.validateResponsePayload(chargingStation, commandName, payload) logger.debug( `${chargingStation.logPrefix()} ${moduleName}.responseHandler: Handling '${commandName}' response` ) @@ -407,38 +406,4 @@ export class OCPP20ResponseService extends OCPPResponseService { ) } } - - /** - * Validates incoming OCPP 2.0 response payload against JSON schema - * @param chargingStation - The charging station instance receiving the response - * @param commandName - OCPP 2.0 command name to validate against - * @param payload - JSON response payload to validate - * @returns True if payload validation succeeds, false otherwise - */ - private validatePayload ( - chargingStation: ChargingStation, - commandName: OCPP20RequestCommand, - payload: JsonType - ): boolean { - if (this.payloadValidatorFunctions.has(commandName)) { - logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: Validating '${commandName}' response payload` - ) - const isValid = this.validateResponsePayload(chargingStation, commandName, payload) - if (!isValid) { - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: '${commandName}' response payload validation failed` - ) - } else { - logger.debug( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: '${commandName}' response payload validation successful` - ) - } - return isValid - } - logger.warn( - `${chargingStation.logPrefix()} ${moduleName}.validatePayload: No JSON schema validation function found for command '${commandName}' PDU validation` - ) - return false - } } diff --git a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts index fba639bd..5bcc1268 100644 --- a/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts +++ b/src/charging-station/ocpp/2.0/OCPP20ServiceUtils.ts @@ -254,20 +254,6 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Request.json`), ]) - /** - * Factory options for OCPP 2.0 Incoming Request Service - * @param moduleName - Name of the OCPP module - * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 2.0 incoming request validators - */ - public static createIncomingRequestPayloadOptions = (moduleName: string, methodName: string) => - OCPP20ServiceUtils.PayloadValidatorOptions( - OCPPVersion.VERSION_201, - 'assets/json-schemas/ocpp/2.0', - moduleName, - methodName - ) - /** * Configuration for OCPP 2.0 Incoming Request Response validators * @returns Array of validator configuration tuples @@ -282,15 +268,12 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { ]) /** - * Factory options for OCPP 2.0 Incoming Request Response Service + * Factory options for OCPP 2.0 payload validators * @param moduleName - Name of the OCPP module * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 2.0 incoming request response validators + * @returns Factory options object for OCPP 2.0 validators */ - public static createIncomingRequestResponsePayloadOptions = ( - moduleName: string, - methodName: string - ) => + public static createPayloadOptions = (moduleName: string, methodName: string) => OCPP20ServiceUtils.PayloadValidatorOptions( OCPPVersion.VERSION_201, 'assets/json-schemas/ocpp/2.0', @@ -311,20 +294,6 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Request.json`), ]) - /** - * Factory options for OCPP 2.0 Request Service - * @param moduleName - Name of the OCPP module - * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 2.0 validators - */ - public static createRequestPayloadOptions = (moduleName: string, methodName: string) => - OCPP20ServiceUtils.PayloadValidatorOptions( - OCPPVersion.VERSION_201, - 'assets/json-schemas/ocpp/2.0', - moduleName, - methodName - ) - /** * OCPP 2.0 Response Service validator configurations * @returns Array of validator configuration tuples @@ -338,20 +307,6 @@ export class OCPP20ServiceUtils extends OCPPServiceUtils { OCPP20ServiceUtils.PayloadValidatorConfig(`${schemaBase}Response.json`), ]) - /** - * Factory options for OCPP 2.0 Response Service - * @param moduleName - Name of the OCPP module - * @param methodName - Name of the method/command - * @returns Factory options object for OCPP 2.0 response validators - */ - public static createResponsePayloadOptions = (moduleName: string, methodName: string) => - OCPP20ServiceUtils.PayloadValidatorOptions( - OCPPVersion.VERSION_201, - 'assets/json-schemas/ocpp/2.0', - moduleName, - methodName - ) - public static enforceMessageLimits< T extends { attributeType?: unknown; component: unknown; variable: unknown } >( diff --git a/src/charging-station/ocpp/OCPPIncomingRequestService.ts b/src/charging-station/ocpp/OCPPIncomingRequestService.ts index 3d99275e..351aef63 100644 --- a/src/charging-station/ocpp/OCPPIncomingRequestService.ts +++ b/src/charging-station/ocpp/OCPPIncomingRequestService.ts @@ -69,18 +69,24 @@ export abstract class OCPPIncomingRequestService extends EventEmitter { return true } const validate = this.payloadValidatorFunctions.get(commandName) - if (validate?.(payload) === true) { + if (validate == null) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestPayload: No JSON schema validation function found for command '${commandName}' PDU validation` + ) + return false + } + if (validate(payload)) { return true } logger.error( `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestPayload: Command '${commandName}' incoming request PDU is invalid: %j`, - validate?.errors + validate.errors ) throw new OCPPError( - ajvErrorsToErrorType(validate?.errors), + ajvErrorsToErrorType(validate.errors), 'Incoming request PDU is invalid', commandName, - JSON.stringify(validate?.errors, undefined, 2) + JSON.stringify(validate.errors, undefined, 2) ) } } diff --git a/src/charging-station/ocpp/OCPPRequestService.ts b/src/charging-station/ocpp/OCPPRequestService.ts index ed3a56d5..c9762612 100644 --- a/src/charging-station/ocpp/OCPPRequestService.ts +++ b/src/charging-station/ocpp/OCPPRequestService.ts @@ -448,34 +448,30 @@ export abstract class OCPPRequestService { if (chargingStation.stationInfo?.ocppStrictCompliance === false) { return true } - if ( - !this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.has( - commandName as IncomingRequestCommand - ) - ) { + const validate = this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get( + commandName as IncomingRequestCommand + ) + if (validate == null) { logger.warn( `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation` ) return true } - const validate = this.ocppResponseService.incomingRequestResponsePayloadValidateFunctions.get( - commandName as IncomingRequestCommand - ) payload = clone(payload) convertDateToISOString(payload) - if (validate?.(payload) === true) { + if (validate(payload)) { return true } logger.error( `${chargingStation.logPrefix()} ${moduleName}.validateIncomingRequestResponsePayload: Command '${commandName}' incoming request response PDU is invalid: %j`, - validate?.errors + validate.errors ) // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). throw new OCPPError( - ajvErrorsToErrorType(validate?.errors), + ajvErrorsToErrorType(validate.errors), 'Incoming request response PDU is invalid', commandName, - JSON.stringify(validate?.errors, undefined, 2) + JSON.stringify(validate.errors, undefined, 2) ) } @@ -495,28 +491,28 @@ export abstract class OCPPRequestService { if (chargingStation.stationInfo?.ocppStrictCompliance === false) { return true } - if (!this.payloadValidatorFunctions.has(commandName as RequestCommand)) { + const validate = this.payloadValidatorFunctions.get(commandName as RequestCommand) + if (validate == null) { logger.warn( `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: No JSON schema validation function found for command '${commandName}' PDU validation` ) return true } - const validate = this.payloadValidatorFunctions.get(commandName as RequestCommand) payload = clone(payload) convertDateToISOString(payload) - if (validate?.(payload) === true) { + if (validate(payload)) { return true } logger.error( `${chargingStation.logPrefix()} ${moduleName}.validateRequestPayload: Command '${commandName}' request PDU is invalid: %j`, - validate?.errors + validate.errors ) // OCPPError usage here is debatable: it's an error in the OCPP stack but not targeted to sendError(). throw new OCPPError( - ajvErrorsToErrorType(validate?.errors), + ajvErrorsToErrorType(validate.errors), 'Request PDU is invalid', commandName, - JSON.stringify(validate?.errors, undefined, 2) + JSON.stringify(validate.errors, undefined, 2) ) } } diff --git a/src/charging-station/ocpp/OCPPResponseService.ts b/src/charging-station/ocpp/OCPPResponseService.ts index 3c01181c..d6f122e1 100644 --- a/src/charging-station/ocpp/OCPPResponseService.ts +++ b/src/charging-station/ocpp/OCPPResponseService.ts @@ -79,18 +79,24 @@ export abstract class OCPPResponseService { return true } const validate = this.payloadValidatorFunctions.get(commandName) - if (validate?.(payload) === true) { + if (validate == null) { + logger.warn( + `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: No JSON schema validation function found for command '${commandName}' PDU validation` + ) + return false + } + if (validate(payload)) { return true } logger.error( `${chargingStation.logPrefix()} ${moduleName}.validateResponsePayload: Command '${commandName}' response PDU is invalid: %j`, - validate?.errors + validate.errors ) throw new OCPPError( - ajvErrorsToErrorType(validate?.errors), + ajvErrorsToErrorType(validate.errors), 'Response PDU is invalid', commandName, - JSON.stringify(validate?.errors, undefined, 2) + JSON.stringify(validate.errors, undefined, 2) ) } } diff --git a/src/charging-station/ocpp/OCPPServiceUtils.ts b/src/charging-station/ocpp/OCPPServiceUtils.ts index f6cc126f..0de76ab2 100644 --- a/src/charging-station/ocpp/OCPPServiceUtils.ts +++ b/src/charging-station/ocpp/OCPPServiceUtils.ts @@ -1984,7 +1984,8 @@ const getMeasurandDefaultUnit = ( * response services) to perform common operations and validation. It acts as a shared * utility layer that prevents code duplication across OCPP version-specific implementations. * @see {@link parseJsonSchemaFile} Core JSON schema parsing functionality - * @see {@link validatePayload} Payload validation methods in service classes + * @see {@link validateIncomingRequestPayload} Payload validation methods in service classes + * @see {@link validateResponsePayload} Payload validation methods in service classes */ // eslint-disable-next-line @typescript-eslint/no-extraneous-class @@ -2145,12 +2146,13 @@ export class OCPPServiceUtils { /** * Parses and loads a JSON schema file for OCPP payload validation. - * Handles file reading, JSON parsing, and error recovery for schema validation. + * Handles file reading and JSON parsing for schema validation. * @param relativePath Path to the schema file relative to the OCPP utils directory * @param ocppVersion The OCPP version for error logging context * @param moduleName Optional module name for error logging * @param methodName Optional method name for error logging - * @returns Parsed JSON schema object or empty object if parsing fails + * @returns Parsed JSON schema object + * @throws {NodeJS.ErrnoException} If the schema file cannot be read or parsed */ protected static parseJsonSchemaFile( relativePath: string, @@ -2158,18 +2160,26 @@ export class OCPPServiceUtils { moduleName?: string, methodName?: string ): JSONSchemaType { - const filePath = join(dirname(fileURLToPath(import.meta.url)), relativePath) + const baseDir = dirname(fileURLToPath(import.meta.url)) + // Primary: resolve from file directory (production esbuild bundle) + const primaryPath = join(baseDir, relativePath) try { - return JSON.parse(readFileSync(filePath, 'utf8')) as JSONSchemaType - } catch (error) { - handleFileException( - filePath, - FileType.JsonSchema, - error as NodeJS.ErrnoException, - OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName), - { throwError: false } - ) - return {} as JSONSchemaType + return JSON.parse(readFileSync(primaryPath, 'utf8')) as JSONSchemaType + } catch (primaryError) { + // Fallback: resolve from source root (development/test with tsx) + const fallbackPath = join(baseDir, '..', '..', relativePath) + try { + return JSON.parse(readFileSync(fallbackPath, 'utf8')) as JSONSchemaType + } catch { + handleFileException( + primaryPath, + FileType.JsonSchema, + primaryError as NodeJS.ErrnoException, + OCPPServiceUtils.logPrefix(ocppVersion, moduleName, methodName) + ) + // handleFileException throws by default; this satisfies the compiler + throw primaryError + } } } -- 2.53.0