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