refactor: refine UI Server configuration checks
[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'
8import { BaseError } from '../../exception/index.js'
eb3abc4f 9import {
a6080904 10 ApplicationProtocolVersion,
eb3abc4f 11 AuthenticationType,
268a74bb 12 type ChargingStationData,
e0b0ee21
JB
13 type ProcedureName,
14 type ProtocolRequest,
15 type ProtocolResponse,
f130b8e6 16 ProtocolVersion,
e0b0ee21
JB
17 type RequestPayload,
18 type ResponsePayload,
66a7748d
JB
19 type UIServerConfiguration
20} from '../../types/index.js'
8114d10e 21
fe94fce0 22export abstract class AbstractUIServer {
66a7748d 23 public readonly chargingStations: Map<string, ChargingStationData>
2f989136 24 public readonly chargingStationTemplates: Set<string>
66a7748d
JB
25 protected readonly httpServer: Server | Http2Server
26 protected readonly responseHandlers: Map<string, ServerResponse | WebSocket>
27 protected readonly uiServices: Map<ProtocolVersion, AbstractUIService>
fe94fce0 28
66a7748d
JB
29 public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) {
30 this.chargingStations = new Map<string, ChargingStationData>()
2f989136 31 this.chargingStationTemplates = new Set<string>()
a6080904
JB
32 switch (this.uiServerConfiguration.version) {
33 case ApplicationProtocolVersion.VERSION_11:
66a7748d
JB
34 this.httpServer = new Server()
35 break
a6080904 36 case ApplicationProtocolVersion.VERSION_20:
66a7748d
JB
37 this.httpServer = createServer()
38 break
a6080904
JB
39 default:
40 throw new BaseError(
66a7748d
JB
41 `Unsupported application protocol version ${this.uiServerConfiguration.version}`
42 )
a6080904 43 }
66a7748d
JB
44 this.responseHandlers = new Map<string, ServerResponse | WebSocket>()
45 this.uiServices = new Map<ProtocolVersion, AbstractUIService>()
fe94fce0
JB
46 }
47
66a7748d 48 public buildProtocolRequest (
852a4c5f
JB
49 id: string,
50 procedureName: ProcedureName,
66a7748d 51 requestPayload: RequestPayload
5e3cb728 52 ): ProtocolRequest {
66a7748d 53 return [id, procedureName, requestPayload]
852a4c5f
JB
54 }
55
66a7748d
JB
56 public buildProtocolResponse (id: string, responsePayload: ResponsePayload): ProtocolResponse {
57 return [id, responsePayload]
852a4c5f
JB
58 }
59
66a7748d
JB
60 public stop (): void {
61 this.stopHttpServer()
09e5a7a8
JB
62 for (const uiService of this.uiServices.values()) {
63 uiService.stop()
64 }
66a7748d 65 this.chargingStations.clear()
2f989136 66 this.chargingStationTemplates.clear()
daa6505e
JB
67 }
68
66a7748d
JB
69 public async sendInternalRequest (request: ProtocolRequest): Promise<ProtocolResponse> {
70 const protocolVersion = ProtocolVersion['0.0.1']
71 this.registerProtocolVersionUIService(protocolVersion)
72 return await (this.uiServices
e1d9a0f4 73 .get(protocolVersion)
66a7748d 74 ?.requestHandler(request) as Promise<ProtocolResponse>)
6bd808fd
JB
75 }
76
66a7748d
JB
77 public hasResponseHandler (id: string): boolean {
78 return this.responseHandlers.has(id)
1ca4a038
JB
79 }
80
66a7748d
JB
81 protected startHttpServer (): void {
82 if (!this.httpServer.listening) {
83 this.httpServer.listen(this.uiServerConfiguration.options)
a307349b
JB
84 }
85 }
86
66a7748d
JB
87 protected registerProtocolVersionUIService (version: ProtocolVersion): void {
88 if (!this.uiServices.has(version)) {
89 this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this))
143498c8
JB
90 }
91 }
92
66a7748d 93 protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void {
329eab0e 94 const authorizationError = new BaseError('Unauthorized')
66a7748d 95 if (this.isBasicAuthEnabled()) {
329eab0e
JB
96 if (!this.isValidBasicAuth(req, next)) {
97 next(authorizationError)
976d11ec 98 }
66a7748d 99 next()
329eab0e
JB
100 } else if (this.isProtocolBasicAuthEnabled()) {
101 if (!this.isValidProtocolBasicAuth(req, next)) {
102 next(authorizationError)
103 }
104 next()
105 } else if (this.uiServerConfiguration.authentication?.enabled === true) {
106 next(authorizationError)
976d11ec 107 }
66a7748d 108 next()
976d11ec
JB
109 }
110
66a7748d
JB
111 private stopHttpServer (): void {
112 if (this.httpServer.listening) {
113 this.httpServer.close()
36adaf06
JB
114 }
115 }
116
66a7748d 117 private isBasicAuthEnabled (): boolean {
eb3abc4f
JB
118 return (
119 this.uiServerConfiguration.authentication?.enabled === true &&
5199f9fd 120 this.uiServerConfiguration.authentication.type === AuthenticationType.BASIC_AUTH
66a7748d 121 )
eb3abc4f
JB
122 }
123
329eab0e
JB
124 private isProtocolBasicAuthEnabled (): boolean {
125 return (
126 this.uiServerConfiguration.authentication?.enabled === true &&
127 this.uiServerConfiguration.authentication.type === AuthenticationType.PROTOCOL_BASIC_AUTH
128 )
129 }
130
131 private isValidBasicAuth (req: IncomingMessage, next: (err?: Error) => void): boolean {
132 const [username, password] = this.getUsernameAndPasswordFromAuthorizationToken(
133 req.headers.authorization?.split(/\s+/).pop() ?? '',
134 next
135 )
136 return this.isValidUsernameAndPassword(username, password)
137 }
138
139 private isValidProtocolBasicAuth (req: IncomingMessage, next: (err?: Error) => void): boolean {
b35a06e0 140 const authorizationProtocol = req.headers['sec-websocket-protocol']?.split(/,\s+/).pop()
329eab0e
JB
141 const [username, password] = this.getUsernameAndPasswordFromAuthorizationToken(
142 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
143 `${authorizationProtocol}${Array(((4 - (authorizationProtocol!.length % 4)) % 4) + 1).join('=')}`
144 .split('.')
145 .pop() ?? '',
146 next
147 )
148 return this.isValidUsernameAndPassword(username, password)
149 }
150
151 private getUsernameAndPasswordFromAuthorizationToken (
152 authorizationToken: string,
153 next: (err?: Error) => void
154 ): [string, string] {
155 if (
156 !/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/.test(authorizationToken)
157 ) {
158 next(new BaseError('Invalid basic authentication token format'))
159 }
66a7748d
JB
160 const authentication = Buffer.from(authorizationToken, 'base64').toString()
161 const authenticationParts = authentication.split(/:/)
329eab0e
JB
162 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
163 return [authenticationParts.shift()!, authenticationParts.join(':')]
164 }
165
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}