refactor: strong type UUID usage in UI protocol
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / AbstractUIServer.ts
1 import { type IncomingMessage, Server, type ServerResponse } from 'node:http'
2 import { createServer, type Http2Server } from 'node:http2'
3
4 import type { WebSocket } from 'ws'
5
6 import { BaseError } from '../../exception/index.js'
7 import {
8 ApplicationProtocolVersion,
9 AuthenticationType,
10 type ChargingStationData,
11 type ProcedureName,
12 type ProtocolRequest,
13 type ProtocolResponse,
14 ProtocolVersion,
15 type RequestPayload,
16 type ResponsePayload,
17 type UIServerConfiguration
18 } from '../../types/index.js'
19 import { logger } from '../../utils/index.js'
20 import type { AbstractUIService } from './ui-services/AbstractUIService.js'
21 import { UIServiceFactory } from './ui-services/UIServiceFactory.js'
22 import { getUsernameAndPasswordFromAuthorizationToken } from './UIServerUtils.js'
23
24 const moduleName = 'AbstractUIServer'
25
26 export abstract class AbstractUIServer {
27 public readonly chargingStations: Map<string, ChargingStationData>
28 public readonly chargingStationTemplates: Set<string>
29 protected readonly httpServer: Server | Http2Server
30 protected readonly responseHandlers: Map<
31 `${string}-${string}-${string}-${string}-${string}`,
32 ServerResponse | WebSocket
33 >
34
35 protected readonly uiServices: Map<ProtocolVersion, AbstractUIService>
36
37 public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) {
38 this.chargingStations = new Map<string, ChargingStationData>()
39 this.chargingStationTemplates = new Set<string>()
40 switch (this.uiServerConfiguration.version) {
41 case ApplicationProtocolVersion.VERSION_11:
42 this.httpServer = new Server()
43 break
44 case ApplicationProtocolVersion.VERSION_20:
45 this.httpServer = createServer()
46 break
47 default:
48 throw new BaseError(
49 `Unsupported application protocol version ${this.uiServerConfiguration.version}`
50 )
51 }
52 this.responseHandlers = new Map<
53 `${string}-${string}-${string}-${string}-${string}`,
54 ServerResponse | WebSocket
55 >()
56 this.uiServices = new Map<ProtocolVersion, AbstractUIService>()
57 }
58
59 public buildProtocolRequest (
60 uuid: `${string}-${string}-${string}-${string}-${string}`,
61 procedureName: ProcedureName,
62 requestPayload: RequestPayload
63 ): ProtocolRequest {
64 return [uuid, procedureName, requestPayload]
65 }
66
67 public buildProtocolResponse (
68 uuid: `${string}-${string}-${string}-${string}-${string}`,
69 responsePayload: ResponsePayload
70 ): ProtocolResponse {
71 return [uuid, responsePayload]
72 }
73
74 public stop (): void {
75 this.stopHttpServer()
76 for (const uiService of this.uiServices.values()) {
77 uiService.stop()
78 }
79 this.clearCaches()
80 }
81
82 public clearCaches (): void {
83 this.chargingStations.clear()
84 this.chargingStationTemplates.clear()
85 }
86
87 public async sendInternalRequest (request: ProtocolRequest): Promise<ProtocolResponse> {
88 const protocolVersion = ProtocolVersion['0.0.1']
89 this.registerProtocolVersionUIService(protocolVersion)
90 return await (this.uiServices
91 .get(protocolVersion)
92 ?.requestHandler(request) as Promise<ProtocolResponse>)
93 }
94
95 public hasResponseHandler (uuid: `${string}-${string}-${string}-${string}-${string}`): boolean {
96 return this.responseHandlers.has(uuid)
97 }
98
99 protected startHttpServer (): void {
100 this.httpServer.on('error', error => {
101 logger.error(
102 `${this.logPrefix(moduleName, 'start.httpServer.on.error')} HTTP server error:`,
103 error
104 )
105 })
106 if (!this.httpServer.listening) {
107 this.httpServer.listen(this.uiServerConfiguration.options)
108 }
109 }
110
111 protected registerProtocolVersionUIService (version: ProtocolVersion): void {
112 if (!this.uiServices.has(version)) {
113 this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this))
114 }
115 }
116
117 protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void {
118 const authorizationError = new BaseError('Unauthorized')
119 if (this.isBasicAuthEnabled()) {
120 if (!this.isValidBasicAuth(req, next)) {
121 next(authorizationError)
122 }
123 next()
124 } else if (this.isProtocolBasicAuthEnabled()) {
125 if (!this.isValidProtocolBasicAuth(req, next)) {
126 next(authorizationError)
127 }
128 next()
129 } else if (this.uiServerConfiguration.authentication?.enabled === true) {
130 next(authorizationError)
131 }
132 next()
133 }
134
135 private stopHttpServer (): void {
136 if (this.httpServer.listening) {
137 this.httpServer.close()
138 this.httpServer.removeAllListeners()
139 }
140 }
141
142 private isBasicAuthEnabled (): boolean {
143 return (
144 this.uiServerConfiguration.authentication?.enabled === true &&
145 this.uiServerConfiguration.authentication.type === AuthenticationType.BASIC_AUTH
146 )
147 }
148
149 private isProtocolBasicAuthEnabled (): boolean {
150 return (
151 this.uiServerConfiguration.authentication?.enabled === true &&
152 this.uiServerConfiguration.authentication.type === AuthenticationType.PROTOCOL_BASIC_AUTH
153 )
154 }
155
156 private isValidBasicAuth (req: IncomingMessage, next: (err?: Error) => void): boolean {
157 const [username, password] = getUsernameAndPasswordFromAuthorizationToken(
158 req.headers.authorization?.split(/\s+/).pop() ?? '',
159 next
160 )
161 return this.isValidUsernameAndPassword(username, password)
162 }
163
164 private isValidProtocolBasicAuth (req: IncomingMessage, next: (err?: Error) => void): boolean {
165 const authorizationProtocol = req.headers['sec-websocket-protocol']?.split(/,\s+/).pop()
166 const [username, password] = getUsernameAndPasswordFromAuthorizationToken(
167 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
168 `${authorizationProtocol}${Array(((4 - (authorizationProtocol!.length % 4)) % 4) + 1).join('=')}`
169 .split('.')
170 .pop() ?? '',
171 next
172 )
173 return this.isValidUsernameAndPassword(username, password)
174 }
175
176 private isValidUsernameAndPassword (username: string, password: string): boolean {
177 return (
178 this.uiServerConfiguration.authentication?.username === username &&
179 this.uiServerConfiguration.authentication.password === password
180 )
181 }
182
183 public abstract start (): void
184 public abstract sendRequest (request: ProtocolRequest): void
185 public abstract sendResponse (response: ProtocolResponse): void
186 public abstract logPrefix (moduleName?: string, methodName?: string, prefixSuffix?: string): string
187 }