refactor: switch eslint configuration to strict type checking
[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) => {
5199f9fd 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 117 public sendResponse (response: ProtocolResponse): void {
5199f9fd 118 const responseId = response[0]
976d11ec 119 try {
1ca4a038 120 if (this.hasResponseHandler(responseId)) {
66a7748d 121 const ws = this.responseHandlers.get(responseId) as WebSocket
5199f9fd 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 128 'sendResponse'
5199f9fd
JB
129 )} Error at sending response id '${responseId}', WebSocket is not open: ${
130 ws.readyState
131 }`
66a7748d 132 )
976d11ec 133 }
976d11ec
JB
134 } else {
135 logger.error(
136 `${this.logPrefix(
137 moduleName,
66a7748d
JB
138 'sendResponse'
139 )} Response for unknown request id: ${responseId}`
140 )
94dc3080 141 }
976d11ec 142 } catch (error) {
94dc3080
JB
143 logger.error(
144 `${this.logPrefix(
145 moduleName,
66a7748d 146 'sendResponse'
976d11ec 147 )} Error at sending response id '${responseId}':`,
66a7748d
JB
148 error
149 )
e2c77f10 150 } finally {
66a7748d 151 this.responseHandlers.delete(responseId)
94dc3080 152 }
178ac666
JB
153 }
154
8b7072dc 155 public logPrefix = (modName?: string, methodName?: string, prefixSuffix?: string): string => {
66a7748d
JB
156 const logMsgPrefix =
157 prefixSuffix != null ? `UI WebSocket Server ${prefixSuffix}` : 'UI WebSocket Server'
32de5a57 158 const logMsg =
9bf0ef23 159 isNotEmptyString(modName) && isNotEmptyString(methodName)
1b271a54 160 ? ` ${logMsgPrefix} | ${modName}.${methodName}:`
66a7748d
JB
161 : ` ${logMsgPrefix} |`
162 return logPrefix(logMsg)
163 }
178ac666 164
66a7748d 165 private broadcastToClients (message: string): void {
eb3abc4f 166 for (const client of this.webSocketServer.clients) {
5199f9fd 167 if (client.readyState === WebSocket.OPEN) {
66a7748d 168 client.send(message)
178ac666
JB
169 }
170 }
171 }
5e3cb728 172
66a7748d 173 private validateRawDataRequest (rawData: RawData): ProtocolRequest | false {
5e3cb728
JB
174 // logger.debug(
175 // `${this.logPrefix(
176 // moduleName,
66a7748d 177 // 'validateRawDataRequest'
e1d9a0f4 178 // // eslint-disable-next-line @typescript-eslint/no-base-to-string
66a7748d
JB
179 // )} Raw data received in string format: ${rawData.toString()}`
180 // )
5e3cb728 181
e1d9a0f4 182 // eslint-disable-next-line @typescript-eslint/no-base-to-string
66a7748d 183 const request = JSON.parse(rawData.toString()) as ProtocolRequest
5e3cb728 184
66a7748d 185 if (!Array.isArray(request)) {
5dea4c94
JB
186 logger.error(
187 `${this.logPrefix(
188 moduleName,
66a7748d 189 'validateRawDataRequest'
5dea4c94 190 )} UI protocol request is not an array:`,
66a7748d
JB
191 request
192 )
193 return false
5e3cb728
JB
194 }
195
5199f9fd 196 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
5e3cb728 197 if (request.length !== 3) {
5dea4c94
JB
198 logger.error(
199 `${this.logPrefix(moduleName, 'validateRawDataRequest')} UI protocol request is malformed:`,
66a7748d
JB
200 request
201 )
202 return false
5dea4c94
JB
203 }
204
5199f9fd 205 if (!validateUUID(request[0])) {
5dea4c94
JB
206 logger.error(
207 `${this.logPrefix(
208 moduleName,
66a7748d 209 'validateRawDataRequest'
5dea4c94 210 )} UI protocol request UUID field is invalid:`,
66a7748d
JB
211 request
212 )
213 return false
5e3cb728
JB
214 }
215
66a7748d 216 return request
5e3cb728 217 }
4198ad5c 218}