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