refactor: factor out UI Server helpers into arrow functions
[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.chargingStations.clear()
67 this.chargingStationTemplates.clear()
68 }
69
70 public async sendInternalRequest (request: ProtocolRequest): Promise<ProtocolResponse> {
71 const protocolVersion = ProtocolVersion['0.0.1']
72 this.registerProtocolVersionUIService(protocolVersion)
73 return await (this.uiServices
74 .get(protocolVersion)
75 ?.requestHandler(request) as Promise<ProtocolResponse>)
76 }
77
78 public hasResponseHandler (id: string): boolean {
79 return this.responseHandlers.has(id)
80 }
81
82 protected startHttpServer (): void {
83 if (!this.httpServer.listening) {
84 this.httpServer.listen(this.uiServerConfiguration.options)
85 }
86 }
87
88 protected registerProtocolVersionUIService (version: ProtocolVersion): void {
89 if (!this.uiServices.has(version)) {
90 this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this))
91 }
92 }
93
94 protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void {
95 const authorizationError = new BaseError('Unauthorized')
96 if (this.isBasicAuthEnabled()) {
97 if (!this.isValidBasicAuth(req, next)) {
98 next(authorizationError)
99 }
100 next()
101 } else if (this.isProtocolBasicAuthEnabled()) {
102 if (!this.isValidProtocolBasicAuth(req, next)) {
103 next(authorizationError)
104 }
105 next()
106 } else if (this.uiServerConfiguration.authentication?.enabled === true) {
107 next(authorizationError)
108 }
109 next()
110 }
111
112 private stopHttpServer (): void {
113 if (this.httpServer.listening) {
114 this.httpServer.close()
115 }
116 }
117
118 private isBasicAuthEnabled (): boolean {
119 return (
120 this.uiServerConfiguration.authentication?.enabled === true &&
121 this.uiServerConfiguration.authentication.type === AuthenticationType.BASIC_AUTH
122 )
123 }
124
125 private isProtocolBasicAuthEnabled (): boolean {
126 return (
127 this.uiServerConfiguration.authentication?.enabled === true &&
128 this.uiServerConfiguration.authentication.type === AuthenticationType.PROTOCOL_BASIC_AUTH
129 )
130 }
131
132 private isValidBasicAuth (req: IncomingMessage, next: (err?: Error) => void): boolean {
133 const [username, password] = getUsernameAndPasswordFromAuthorizationToken(
134 req.headers.authorization?.split(/\s+/).pop() ?? '',
135 next
136 )
137 return this.isValidUsernameAndPassword(username, password)
138 }
139
140 private isValidProtocolBasicAuth (req: IncomingMessage, next: (err?: Error) => void): boolean {
141 const authorizationProtocol = req.headers['sec-websocket-protocol']?.split(/,\s+/).pop()
142 const [username, password] = getUsernameAndPasswordFromAuthorizationToken(
143 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
144 `${authorizationProtocol}${Array(((4 - (authorizationProtocol!.length % 4)) % 4) + 1).join('=')}`
145 .split('.')
146 .pop() ?? '',
147 next
148 )
149 return this.isValidUsernameAndPassword(username, password)
150 }
151
152 private isValidUsernameAndPassword (username: string, password: string): boolean {
153 return (
154 this.uiServerConfiguration.authentication?.username === username &&
155 this.uiServerConfiguration.authentication.password === password
156 )
157 }
158
159 public abstract start (): void
160 public abstract sendRequest (request: ProtocolRequest): void
161 public abstract sendResponse (response: ProtocolResponse): void
162 public abstract logPrefix (moduleName?: string, methodName?: string, prefixSuffix?: string): string
163 }