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