e24a5769956cb40163aa29b2d05f727694029bed
[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
23 export abstract class AbstractUIServer {
24 public readonly chargingStations: Map<string, ChargingStationData>
25 public readonly chargingStationTemplates: Set<string>
26 protected readonly httpServer: Server | Http2Server
27 protected readonly responseHandlers: Map<string, ServerResponse | WebSocket>
28 protected readonly uiServices: Map<ProtocolVersion, AbstractUIService>
29
30 public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) {
31 this.chargingStations = new Map<string, ChargingStationData>()
32 this.chargingStationTemplates = new Set<string>()
33 switch (this.uiServerConfiguration.version) {
34 case ApplicationProtocolVersion.VERSION_11:
35 this.httpServer = new Server()
36 break
37 case ApplicationProtocolVersion.VERSION_20:
38 this.httpServer = createServer()
39 break
40 default:
41 throw new BaseError(
42 `Unsupported application protocol version ${this.uiServerConfiguration.version}`
43 )
44 }
45 this.responseHandlers = new Map<string, ServerResponse | WebSocket>()
46 this.uiServices = new Map<ProtocolVersion, AbstractUIService>()
47 }
48
49 public buildProtocolRequest (
50 id: string,
51 procedureName: ProcedureName,
52 requestPayload: RequestPayload
53 ): ProtocolRequest {
54 return [id, procedureName, requestPayload]
55 }
56
57 public buildProtocolResponse (id: string, responsePayload: ResponsePayload): ProtocolResponse {
58 return [id, responsePayload]
59 }
60
61 public stop (): void {
62 this.stopHttpServer()
63 for (const uiService of this.uiServices.values()) {
64 uiService.stop()
65 }
66 this.clearCaches()
67 }
68
69 private clearCaches (): void {
70 this.chargingStations.clear()
71 this.chargingStationTemplates.clear()
72 }
73
74 public async sendInternalRequest (request: ProtocolRequest): Promise<ProtocolResponse> {
75 const protocolVersion = ProtocolVersion['0.0.1']
76 this.registerProtocolVersionUIService(protocolVersion)
77 return await (this.uiServices
78 .get(protocolVersion)
79 ?.requestHandler(request) as Promise<ProtocolResponse>)
80 }
81
82 public hasResponseHandler (id: string): boolean {
83 return this.responseHandlers.has(id)
84 }
85
86 protected startHttpServer (): void {
87 if (!this.httpServer.listening) {
88 this.httpServer.listen(this.uiServerConfiguration.options)
89 }
90 }
91
92 protected registerProtocolVersionUIService (version: ProtocolVersion): void {
93 if (!this.uiServices.has(version)) {
94 this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this))
95 }
96 }
97
98 protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void {
99 const authorizationError = new BaseError('Unauthorized')
100 if (this.isBasicAuthEnabled()) {
101 if (!this.isValidBasicAuth(req, next)) {
102 next(authorizationError)
103 }
104 next()
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)
112 }
113 next()
114 }
115
116 private stopHttpServer (): void {
117 if (this.httpServer.listening) {
118 this.httpServer.close()
119 }
120 }
121
122 private isBasicAuthEnabled (): boolean {
123 return (
124 this.uiServerConfiguration.authentication?.enabled === true &&
125 this.uiServerConfiguration.authentication.type === AuthenticationType.BASIC_AUTH
126 )
127 }
128
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 {
137 const [username, password] = getUsernameAndPasswordFromAuthorizationToken(
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 {
145 const authorizationProtocol = req.headers['sec-websocket-protocol']?.split(/,\s+/).pop()
146 const [username, password] = getUsernameAndPasswordFromAuthorizationToken(
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
156 private isValidUsernameAndPassword (username: string, password: string): boolean {
157 return (
158 this.uiServerConfiguration.authentication?.username === username &&
159 this.uiServerConfiguration.authentication.password === password
160 )
161 }
162
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
167 }