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