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