refactor: strong type UUID usage in UI protocol
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / AbstractUIServer.ts
CommitLineData
66a7748d 1import { type IncomingMessage, Server, type ServerResponse } from 'node:http'
4c3f6c20 2import { createServer, type Http2Server } from 'node:http2'
daa6505e 3
66a7748d 4import type { WebSocket } from 'ws'
fe94fce0 5
66a7748d 6import { BaseError } from '../../exception/index.js'
eb3abc4f 7import {
a6080904 8 ApplicationProtocolVersion,
eb3abc4f 9 AuthenticationType,
268a74bb 10 type ChargingStationData,
e0b0ee21
JB
11 type ProcedureName,
12 type ProtocolRequest,
13 type ProtocolResponse,
f130b8e6 14 ProtocolVersion,
e0b0ee21
JB
15 type RequestPayload,
16 type ResponsePayload,
66a7748d
JB
17 type UIServerConfiguration
18} from '../../types/index.js'
776cdee3 19import { logger } from '../../utils/index.js'
4c3f6c20
JB
20import type { AbstractUIService } from './ui-services/AbstractUIService.js'
21import { UIServiceFactory } from './ui-services/UIServiceFactory.js'
22import { getUsernameAndPasswordFromAuthorizationToken } from './UIServerUtils.js'
776cdee3
JB
23
24const moduleName = 'AbstractUIServer'
8114d10e 25
fe94fce0 26export abstract class AbstractUIServer {
66a7748d 27 public readonly chargingStations: Map<string, ChargingStationData>
2f989136 28 public readonly chargingStationTemplates: Set<string>
66a7748d 29 protected readonly httpServer: Server | Http2Server
2c5c7443
JB
30 protected readonly responseHandlers: Map<
31 `${string}-${string}-${string}-${string}-${string}`,
32 ServerResponse | WebSocket
33 >
34
66a7748d 35 protected readonly uiServices: Map<ProtocolVersion, AbstractUIService>
fe94fce0 36
66a7748d
JB
37 public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) {
38 this.chargingStations = new Map<string, ChargingStationData>()
2f989136 39 this.chargingStationTemplates = new Set<string>()
a6080904
JB
40 switch (this.uiServerConfiguration.version) {
41 case ApplicationProtocolVersion.VERSION_11:
66a7748d
JB
42 this.httpServer = new Server()
43 break
a6080904 44 case ApplicationProtocolVersion.VERSION_20:
66a7748d
JB
45 this.httpServer = createServer()
46 break
a6080904
JB
47 default:
48 throw new BaseError(
66a7748d
JB
49 `Unsupported application protocol version ${this.uiServerConfiguration.version}`
50 )
a6080904 51 }
2c5c7443
JB
52 this.responseHandlers = new Map<
53 `${string}-${string}-${string}-${string}-${string}`,
54 ServerResponse | WebSocket
55 >()
66a7748d 56 this.uiServices = new Map<ProtocolVersion, AbstractUIService>()
fe94fce0
JB
57 }
58
66a7748d 59 public buildProtocolRequest (
2c5c7443 60 uuid: `${string}-${string}-${string}-${string}-${string}`,
852a4c5f 61 procedureName: ProcedureName,
66a7748d 62 requestPayload: RequestPayload
5e3cb728 63 ): ProtocolRequest {
2c5c7443 64 return [uuid, procedureName, requestPayload]
852a4c5f
JB
65 }
66
2c5c7443
JB
67 public buildProtocolResponse (
68 uuid: `${string}-${string}-${string}-${string}-${string}`,
69 responsePayload: ResponsePayload
70 ): ProtocolResponse {
71 return [uuid, responsePayload]
852a4c5f
JB
72 }
73
66a7748d
JB
74 public stop (): void {
75 this.stopHttpServer()
09e5a7a8
JB
76 for (const uiService of this.uiServices.values()) {
77 uiService.stop()
78 }
5c0e9352
JB
79 this.clearCaches()
80 }
81
a1cfaa16 82 public clearCaches (): void {
66a7748d 83 this.chargingStations.clear()
2f989136 84 this.chargingStationTemplates.clear()
daa6505e
JB
85 }
86
66a7748d
JB
87 public async sendInternalRequest (request: ProtocolRequest): Promise<ProtocolResponse> {
88 const protocolVersion = ProtocolVersion['0.0.1']
89 this.registerProtocolVersionUIService(protocolVersion)
90 return await (this.uiServices
e1d9a0f4 91 .get(protocolVersion)
66a7748d 92 ?.requestHandler(request) as Promise<ProtocolResponse>)
6bd808fd
JB
93 }
94
2c5c7443
JB
95 public hasResponseHandler (uuid: `${string}-${string}-${string}-${string}-${string}`): boolean {
96 return this.responseHandlers.has(uuid)
1ca4a038
JB
97 }
98
66a7748d 99 protected startHttpServer (): void {
776cdee3
JB
100 this.httpServer.on('error', error => {
101 logger.error(
102 `${this.logPrefix(moduleName, 'start.httpServer.on.error')} HTTP server error:`,
103 error
104 )
105 })
66a7748d
JB
106 if (!this.httpServer.listening) {
107 this.httpServer.listen(this.uiServerConfiguration.options)
a307349b
JB
108 }
109 }
110
66a7748d
JB
111 protected registerProtocolVersionUIService (version: ProtocolVersion): void {
112 if (!this.uiServices.has(version)) {
113 this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this))
143498c8
JB
114 }
115 }
116
66a7748d 117 protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void {
329eab0e 118 const authorizationError = new BaseError('Unauthorized')
66a7748d 119 if (this.isBasicAuthEnabled()) {
329eab0e
JB
120 if (!this.isValidBasicAuth(req, next)) {
121 next(authorizationError)
976d11ec 122 }
66a7748d 123 next()
329eab0e
JB
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)
976d11ec 131 }
66a7748d 132 next()
976d11ec
JB
133 }
134
66a7748d
JB
135 private stopHttpServer (): void {
136 if (this.httpServer.listening) {
137 this.httpServer.close()
776cdee3 138 this.httpServer.removeAllListeners()
36adaf06
JB
139 }
140 }
141
66a7748d 142 private isBasicAuthEnabled (): boolean {
eb3abc4f
JB
143 return (
144 this.uiServerConfiguration.authentication?.enabled === true &&
5199f9fd 145 this.uiServerConfiguration.authentication.type === AuthenticationType.BASIC_AUTH
66a7748d 146 )
eb3abc4f
JB
147 }
148
329eab0e
JB
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 {
75adc3d8 157 const [username, password] = getUsernameAndPasswordFromAuthorizationToken(
329eab0e
JB
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 {
b35a06e0 165 const authorizationProtocol = req.headers['sec-websocket-protocol']?.split(/,\s+/).pop()
75adc3d8 166 const [username, password] = getUsernameAndPasswordFromAuthorizationToken(
329eab0e
JB
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
329eab0e 176 private isValidUsernameAndPassword (username: string, password: string): boolean {
eb3abc4f
JB
177 return (
178 this.uiServerConfiguration.authentication?.username === username &&
329eab0e 179 this.uiServerConfiguration.authentication.password === password
66a7748d 180 )
eb3abc4f
JB
181 }
182
66a7748d
JB
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
fe94fce0 187}