]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/blame - src/charging-station/ui-server/AbstractUIServer.ts
build(deps-dev): bump ruff in /tests/ocpp-server in the regular group (#1457)
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / AbstractUIServer.ts
CommitLineData
0749233f
JB
1import type { WebSocket } from 'ws'
2
66a7748d 3import { type IncomingMessage, Server, type ServerResponse } from 'node:http'
4c3f6c20 4import { createServer, type Http2Server } from 'node:http2'
daa6505e 5
0749233f 6import type { AbstractUIService } from './ui-services/AbstractUIService.js'
fe94fce0 7
66a7748d 8import { BaseError } from '../../exception/index.js'
eb3abc4f 9import {
a6080904 10 ApplicationProtocolVersion,
eb3abc4f 11 AuthenticationType,
268a74bb 12 type ChargingStationData,
ae61fa2f 13 ConfigurationSection,
e0b0ee21
JB
14 type ProcedureName,
15 type ProtocolRequest,
16 type ProtocolResponse,
f130b8e6 17 ProtocolVersion,
e0b0ee21
JB
18 type RequestPayload,
19 type ResponsePayload,
d1f5bfd8 20 type UIServerConfiguration,
66a7748d 21} from '../../types/index.js'
776cdee3 22import { logger } from '../../utils/index.js'
4c3f6c20
JB
23import { UIServiceFactory } from './ui-services/UIServiceFactory.js'
24import { getUsernameAndPasswordFromAuthorizationToken } from './UIServerUtils.js'
776cdee3
JB
25
26const moduleName = 'AbstractUIServer'
8114d10e 27
fe94fce0 28export abstract class AbstractUIServer {
c4a89082
JB
29 public readonly chargingStations: Map<string, ChargingStationData>
30 public readonly chargingStationTemplates: Set<string>
31
0749233f 32 protected readonly httpServer: Http2Server | Server
2c5c7443
JB
33 protected readonly responseHandlers: Map<
34 `${string}-${string}-${string}-${string}-${string}`,
d1f5bfd8 35 ServerResponse | WebSocket
2c5c7443
JB
36 >
37
66a7748d 38 protected readonly uiServices: Map<ProtocolVersion, AbstractUIService>
fe94fce0 39
66a7748d
JB
40 public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) {
41 this.chargingStations = new Map<string, ChargingStationData>()
2f989136 42 this.chargingStationTemplates = new Set<string>()
a6080904
JB
43 switch (this.uiServerConfiguration.version) {
44 case ApplicationProtocolVersion.VERSION_11:
66a7748d
JB
45 this.httpServer = new Server()
46 break
a6080904 47 case ApplicationProtocolVersion.VERSION_20:
66a7748d
JB
48 this.httpServer = createServer()
49 break
a6080904
JB
50 default:
51 throw new BaseError(
d1f5bfd8 52 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
ae61fa2f 53 `Unsupported application protocol version ${this.uiServerConfiguration.version} in '${ConfigurationSection.uiServer}' configuration section`
66a7748d 54 )
a6080904 55 }
2c5c7443
JB
56 this.responseHandlers = new Map<
57 `${string}-${string}-${string}-${string}-${string}`,
d1f5bfd8 58 ServerResponse | WebSocket
2c5c7443 59 >()
66a7748d 60 this.uiServices = new Map<ProtocolVersion, AbstractUIService>()
fe94fce0
JB
61 }
62
c4a89082
JB
63 public buildProtocolRequest (
64 uuid: `${string}-${string}-${string}-${string}-${string}`,
65 procedureName: ProcedureName,
66 requestPayload: RequestPayload
67 ): ProtocolRequest {
68 return [uuid, procedureName, requestPayload]
69 }
70
71 public buildProtocolResponse (
72 uuid: `${string}-${string}-${string}-${string}-${string}`,
73 responsePayload: ResponsePayload
74 ): ProtocolResponse {
75 return [uuid, responsePayload]
76 }
77
78 public clearCaches (): void {
79 this.chargingStations.clear()
80 this.chargingStationTemplates.clear()
81 }
82
83 public hasResponseHandler (uuid: `${string}-${string}-${string}-${string}-${string}`): boolean {
84 return this.responseHandlers.has(uuid)
85 }
86
87 public abstract logPrefix (moduleName?: string, methodName?: string, prefixSuffix?: string): string
88
89 public async sendInternalRequest (request: ProtocolRequest): Promise<ProtocolResponse> {
90 const protocolVersion = ProtocolVersion['0.0.1']
91 this.registerProtocolVersionUIService(protocolVersion)
92 return await (this.uiServices
93 .get(protocolVersion)
94 ?.requestHandler(request) as Promise<ProtocolResponse>)
95 }
96
97 public abstract sendRequest (request: ProtocolRequest): void
98
99 public abstract sendResponse (response: ProtocolResponse): void
100
101 public abstract start (): void
102
103 public stop (): void {
104 this.stopHttpServer()
105 for (const uiService of this.uiServices.values()) {
106 uiService.stop()
107 }
108 this.clearCaches()
109 }
110
66a7748d 111 protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void {
329eab0e 112 const authorizationError = new BaseError('Unauthorized')
66a7748d 113 if (this.isBasicAuthEnabled()) {
329eab0e
JB
114 if (!this.isValidBasicAuth(req, next)) {
115 next(authorizationError)
976d11ec 116 }
66a7748d 117 next()
329eab0e
JB
118 } else if (this.isProtocolBasicAuthEnabled()) {
119 if (!this.isValidProtocolBasicAuth(req, next)) {
120 next(authorizationError)
121 }
122 next()
123 } else if (this.uiServerConfiguration.authentication?.enabled === true) {
124 next(authorizationError)
976d11ec 125 }
66a7748d 126 next()
976d11ec
JB
127 }
128
0749233f
JB
129 protected registerProtocolVersionUIService (version: ProtocolVersion): void {
130 if (!this.uiServices.has(version)) {
131 this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this))
132 }
133 }
134
135 protected startHttpServer (): void {
136 this.httpServer.on('error', error => {
137 logger.error(
138 `${this.logPrefix(moduleName, 'start.httpServer.on.error')} HTTP server error:`,
139 error
140 )
141 })
142 if (!this.httpServer.listening) {
143 this.httpServer.listen(this.uiServerConfiguration.options)
36adaf06
JB
144 }
145 }
146
66a7748d 147 private isBasicAuthEnabled (): boolean {
eb3abc4f
JB
148 return (
149 this.uiServerConfiguration.authentication?.enabled === true &&
5199f9fd 150 this.uiServerConfiguration.authentication.type === AuthenticationType.BASIC_AUTH
66a7748d 151 )
eb3abc4f
JB
152 }
153
329eab0e
JB
154 private isProtocolBasicAuthEnabled (): boolean {
155 return (
156 this.uiServerConfiguration.authentication?.enabled === true &&
157 this.uiServerConfiguration.authentication.type === AuthenticationType.PROTOCOL_BASIC_AUTH
158 )
159 }
160
161 private isValidBasicAuth (req: IncomingMessage, next: (err?: Error) => void): boolean {
75adc3d8 162 const [username, password] = getUsernameAndPasswordFromAuthorizationToken(
329eab0e
JB
163 req.headers.authorization?.split(/\s+/).pop() ?? '',
164 next
165 )
166 return this.isValidUsernameAndPassword(username, password)
167 }
168
169 private isValidProtocolBasicAuth (req: IncomingMessage, next: (err?: Error) => void): boolean {
b35a06e0 170 const authorizationProtocol = req.headers['sec-websocket-protocol']?.split(/,\s+/).pop()
75adc3d8 171 const [username, password] = getUsernameAndPasswordFromAuthorizationToken(
d1f5bfd8 172 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/restrict-template-expressions
48847bc0
JB
173 `${authorizationProtocol}${Array(((4 - (authorizationProtocol!.length % 4)) % 4) + 1).join(
174 '='
175 )}`
329eab0e
JB
176 .split('.')
177 .pop() ?? '',
178 next
179 )
180 return this.isValidUsernameAndPassword(username, password)
181 }
182
329eab0e 183 private isValidUsernameAndPassword (username: string, password: string): boolean {
eb3abc4f
JB
184 return (
185 this.uiServerConfiguration.authentication?.username === username &&
329eab0e 186 this.uiServerConfiguration.authentication.password === password
66a7748d 187 )
eb3abc4f
JB
188 }
189
0749233f
JB
190 private stopHttpServer (): void {
191 if (this.httpServer.listening) {
192 this.httpServer.close()
193 this.httpServer.removeAllListeners()
194 }
195 }
fe94fce0 196}