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