ba557644735d0dd8278369dacbddf717fddacf91
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / UIWebSocketServer.ts
1 import type { IncomingMessage } from 'node:http'
2 import type { Duplex } from 'node:stream'
3
4 import { StatusCodes } from 'http-status-codes'
5 import { type RawData, WebSocket, WebSocketServer } from 'ws'
6
7 import {
8 MapStringifyFormat,
9 type ProtocolRequest,
10 type ProtocolResponse,
11 type UIServerConfiguration,
12 WebSocketCloseEventStatusCode,
13 } from '../../types/index.js'
14 import {
15 Constants,
16 getWebSocketCloseEventStatusString,
17 isNotEmptyString,
18 JSONStringify,
19 logger,
20 logPrefix,
21 validateUUID,
22 } from '../../utils/index.js'
23 import { AbstractUIServer } from './AbstractUIServer.js'
24 import {
25 getProtocolAndVersion,
26 handleProtocols,
27 isProtocolAndVersionSupported,
28 } from './UIServerUtils.js'
29
30 const moduleName = 'UIWebSocketServer'
31
32 export class UIWebSocketServer extends AbstractUIServer {
33 private readonly webSocketServer: WebSocketServer
34
35 public logPrefix = (modName?: string, methodName?: string, prefixSuffix?: string): string => {
36 const logMsgPrefix =
37 prefixSuffix != null ? `UI WebSocket Server ${prefixSuffix}` : 'UI WebSocket Server'
38 const logMsg =
39 isNotEmptyString(modName) && isNotEmptyString(methodName)
40 ? ` ${logMsgPrefix} | ${modName}.${methodName}:`
41 : ` ${logMsgPrefix} |`
42 return logPrefix(logMsg)
43 }
44
45 public constructor (protected override readonly uiServerConfiguration: UIServerConfiguration) {
46 super(uiServerConfiguration)
47 this.webSocketServer = new WebSocketServer({
48 handleProtocols,
49 noServer: true,
50 })
51 }
52
53 private broadcastToClients (message: string): void {
54 for (const client of this.webSocketServer.clients) {
55 if (client.readyState === WebSocket.OPEN) {
56 client.send(message)
57 }
58 }
59 }
60
61 private validateRawDataRequest (rawData: RawData): false | ProtocolRequest {
62 // logger.debug(
63 // `${this.logPrefix(
64 // moduleName,
65 // 'validateRawDataRequest'
66 // // eslint-disable-next-line @typescript-eslint/no-base-to-string
67 // )} Raw data received in string format: ${rawData.toString()}`
68 // )
69
70 let request: ProtocolRequest
71 try {
72 // eslint-disable-next-line @typescript-eslint/no-base-to-string
73 request = JSON.parse(rawData.toString()) as ProtocolRequest
74 } catch (error) {
75 logger.error(
76 `${this.logPrefix(
77 moduleName,
78 'validateRawDataRequest'
79 // eslint-disable-next-line @typescript-eslint/no-base-to-string
80 )} UI protocol request is not valid JSON: ${rawData.toString()}`
81 )
82 return false
83 }
84
85 if (!Array.isArray(request)) {
86 logger.error(
87 `${this.logPrefix(
88 moduleName,
89 'validateRawDataRequest'
90 )} UI protocol request is not an array:`,
91 request
92 )
93 return false
94 }
95
96 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
97 if (request.length !== 3) {
98 logger.error(
99 `${this.logPrefix(moduleName, 'validateRawDataRequest')} UI protocol request is malformed:`,
100 request
101 )
102 return false
103 }
104
105 if (!validateUUID(request[0])) {
106 logger.error(
107 `${this.logPrefix(
108 moduleName,
109 'validateRawDataRequest'
110 )} UI protocol request UUID field is invalid:`,
111 request
112 )
113 return false
114 }
115
116 return request
117 }
118
119 public sendRequest (request: ProtocolRequest): void {
120 this.broadcastToClients(JSON.stringify(request))
121 }
122
123 public sendResponse (response: ProtocolResponse): void {
124 const responseId = response[0]
125 try {
126 if (this.hasResponseHandler(responseId)) {
127 const ws = this.responseHandlers.get(responseId) as WebSocket
128 if (ws.readyState === WebSocket.OPEN) {
129 ws.send(JSONStringify(response, undefined, MapStringifyFormat.object))
130 } else {
131 logger.error(
132 `${this.logPrefix(
133 moduleName,
134 'sendResponse'
135 )} Error at sending response id '${responseId}', WebSocket is not open: ${ws.readyState.toString()}`
136 )
137 }
138 } else {
139 logger.error(
140 `${this.logPrefix(
141 moduleName,
142 'sendResponse'
143 )} Response for unknown request id: ${responseId}`
144 )
145 }
146 } catch (error) {
147 logger.error(
148 `${this.logPrefix(
149 moduleName,
150 'sendResponse'
151 )} Error at sending response id '${responseId}':`,
152 error
153 )
154 } finally {
155 this.responseHandlers.delete(responseId)
156 }
157 }
158
159 public start (): void {
160 this.webSocketServer.on('connection', (ws: WebSocket, _req: IncomingMessage): void => {
161 if (!isProtocolAndVersionSupported(ws.protocol)) {
162 logger.error(
163 `${this.logPrefix(
164 moduleName,
165 'start.server.onconnection'
166 )} Unsupported UI protocol version: '${ws.protocol}'`
167 )
168 ws.close(WebSocketCloseEventStatusCode.CLOSE_PROTOCOL_ERROR)
169 }
170 const [, version] = getProtocolAndVersion(ws.protocol)
171 this.registerProtocolVersionUIService(version)
172 ws.on('message', rawData => {
173 const request = this.validateRawDataRequest(rawData)
174 if (request === false) {
175 ws.close(WebSocketCloseEventStatusCode.CLOSE_INVALID_PAYLOAD)
176 return
177 }
178 const [requestId] = request
179 this.responseHandlers.set(requestId, ws)
180 this.uiServices
181 .get(version)
182 ?.requestHandler(request)
183 .then((protocolResponse?: ProtocolResponse) => {
184 if (protocolResponse != null) {
185 this.sendResponse(protocolResponse)
186 }
187 return undefined
188 })
189 .catch(Constants.EMPTY_FUNCTION)
190 })
191 ws.on('error', error => {
192 logger.error(`${this.logPrefix(moduleName, 'start.ws.onerror')} WebSocket error:`, error)
193 })
194 ws.on('close', (code, reason) => {
195 logger.debug(
196 `${this.logPrefix(
197 moduleName,
198 'start.ws.onclose'
199 )} WebSocket closed: '${getWebSocketCloseEventStatusString(
200 code
201 )}' - '${reason.toString()}'`
202 )
203 })
204 })
205 this.httpServer.on('connect', (req: IncomingMessage, socket: Duplex, _head: Buffer) => {
206 if (req.headers.connection !== 'Upgrade' || req.headers.upgrade !== 'websocket') {
207 socket.write(`HTTP/1.1 ${StatusCodes.BAD_REQUEST.toString()} Bad Request\r\n\r\n`)
208 socket.destroy()
209 }
210 })
211 this.httpServer.on('upgrade', (req: IncomingMessage, socket: Duplex, head: Buffer): void => {
212 const onSocketError = (error: Error): void => {
213 logger.error(
214 `${this.logPrefix(
215 moduleName,
216 'start.httpServer.on.upgrade'
217 )} Socket error at connection upgrade event handling:`,
218 error
219 )
220 }
221 socket.on('error', onSocketError)
222 this.authenticate(req, err => {
223 if (err != null) {
224 socket.write(`HTTP/1.1 ${StatusCodes.UNAUTHORIZED.toString()} Unauthorized\r\n\r\n`)
225 socket.destroy()
226 return
227 }
228 try {
229 this.webSocketServer.handleUpgrade(req, socket, head, (ws: WebSocket) => {
230 this.webSocketServer.emit('connection', ws, req)
231 })
232 } catch (error) {
233 logger.error(
234 `${this.logPrefix(
235 moduleName,
236 'start.httpServer.on.upgrade'
237 )} Error at connection upgrade event handling:`,
238 error
239 )
240 }
241 })
242 socket.removeListener('error', onSocketError)
243 })
244 this.startHttpServer()
245 }
246 }