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