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