Improve payload type checking in OCPP, UI and Broadcast Channel
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / ui-services / AbstractUIService.ts
1 import { RawData } from 'ws';
2
3 import BaseError from '../../../exception/BaseError';
4 import { JsonType } from '../../../types/JsonType';
5 import {
6 ProcedureName,
7 ProtocolRequest,
8 ProtocolRequestHandler,
9 ProtocolResponse,
10 ProtocolVersion,
11 RequestPayload,
12 ResponsePayload,
13 ResponseStatus,
14 } from '../../../types/UIProtocol';
15 import logger from '../../../utils/Logger';
16 import Bootstrap from '../../Bootstrap';
17 import UIServiceWorkerBroadcastChannel from '../../UIServiceWorkerBroadcastChannel';
18 import type { AbstractUIServer } from '../AbstractUIServer';
19
20 const moduleName = 'AbstractUIService';
21
22 export default abstract class AbstractUIService {
23 protected readonly version: ProtocolVersion;
24 protected readonly uiServer: AbstractUIServer;
25 protected readonly requestHandlers: Map<ProcedureName, ProtocolRequestHandler>;
26 protected uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel;
27
28 constructor(uiServer: AbstractUIServer, version: ProtocolVersion) {
29 this.version = version;
30 this.uiServer = uiServer;
31 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
32 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
33 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
34 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)],
35 ]);
36 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this);
37 }
38
39 public async requestHandler(request: RawData): Promise<void> {
40 let messageId: string;
41 let command: ProcedureName;
42 let requestPayload: RequestPayload | undefined;
43 let responsePayload: ResponsePayload;
44 try {
45 [messageId, command, requestPayload] = this.requestValidation(request);
46
47 if (this.requestHandlers.has(command) === false) {
48 throw new BaseError(
49 `${command} is not implemented to handle message payload ${JSON.stringify(
50 requestPayload,
51 null,
52 2
53 )}`
54 );
55 }
56
57 // Call the request handler to build the response payload
58 responsePayload = await this.requestHandlers.get(command)(messageId, requestPayload);
59 } catch (error) {
60 // Log
61 logger.error(
62 `${this.uiServer.logPrefix(moduleName, 'messageHandler')} Handle request error:`,
63 error
64 );
65 responsePayload = {
66 status: ResponseStatus.FAILURE,
67 command,
68 requestPayload,
69 responsePayload,
70 errorMessage: (error as Error).message,
71 errorStack: (error as Error).stack,
72 };
73 }
74
75 if (responsePayload !== undefined) {
76 // Send the response
77 this.uiServer.sendResponse(this.buildProtocolResponse(messageId ?? 'error', responsePayload));
78 }
79 }
80
81 public sendRequest(
82 messageId: string,
83 procedureName: ProcedureName,
84 requestPayload: RequestPayload
85 ): void {
86 this.uiServer.sendRequest(this.buildProtocolRequest(messageId, procedureName, requestPayload));
87 }
88
89 public sendResponse(messageId: string, responsePayload: ResponsePayload): void {
90 this.uiServer.sendResponse(this.buildProtocolResponse(messageId, responsePayload));
91 }
92
93 public logPrefix(modName: string, methodName: string): string {
94 return this.uiServer.logPrefix(modName, methodName);
95 }
96
97 private buildProtocolRequest(
98 messageId: string,
99 procedureName: ProcedureName,
100 requestPayload: RequestPayload
101 ): string {
102 return JSON.stringify([messageId, procedureName, requestPayload] as ProtocolRequest);
103 }
104
105 private buildProtocolResponse(messageId: string, responsePayload: ResponsePayload): string {
106 return JSON.stringify([messageId, responsePayload] as ProtocolResponse);
107 }
108
109 // Validate the raw data received from the WebSocket
110 // TODO: should probably be moved to the ws verify clients callback
111 private requestValidation(rawData: RawData): ProtocolRequest {
112 // logger.debug(
113 // `${this.uiServer.logPrefix(
114 // moduleName,
115 // 'dataValidation'
116 // )} Raw data received: ${rawData.toString()}`
117 // );
118
119 const data = JSON.parse(rawData.toString()) as JsonType[];
120
121 if (Array.isArray(data) === false) {
122 throw new BaseError('UI protocol request is not an array');
123 }
124
125 if (data.length !== 3) {
126 throw new BaseError('UI protocol request is malformed');
127 }
128
129 return data as ProtocolRequest;
130 }
131
132 private handleListChargingStations(): ResponsePayload {
133 // TODO: remove cast to unknown
134 return {
135 status: ResponseStatus.SUCCESS,
136 ...Array.from(this.uiServer.chargingStations.values()),
137 } as unknown as ResponsePayload;
138 }
139
140 private async handleStartSimulator(): Promise<ResponsePayload> {
141 await Bootstrap.getInstance().start();
142 return { status: ResponseStatus.SUCCESS };
143 }
144
145 private async handleStopSimulator(): Promise<ResponsePayload> {
146 await Bootstrap.getInstance().stop();
147 return { status: ResponseStatus.SUCCESS };
148 }
149 }