X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Fui-server%2FAbstractUIServer.ts;h=a786fbbddc2ed43623316f297a7f5ac94503d474;hb=4c6f35659fb67d395adc035ef80c566eb6eef79e;hp=cd6167389cf159d44c3b6118e20d3446473d01d3;hpb=5199f9fdf202eb534948f165a0994e1993675aa8;p=e-mobility-charging-stations-simulator.git diff --git a/src/charging-station/ui-server/AbstractUIServer.ts b/src/charging-station/ui-server/AbstractUIServer.ts index cd616738..a786fbbd 100644 --- a/src/charging-station/ui-server/AbstractUIServer.ts +++ b/src/charging-station/ui-server/AbstractUIServer.ts @@ -1,15 +1,14 @@ import { type IncomingMessage, Server, type ServerResponse } from 'node:http' -import { type Http2Server, createServer } from 'node:http2' +import { createServer, type Http2Server } from 'node:http2' import type { WebSocket } from 'ws' -import type { AbstractUIService } from './ui-services/AbstractUIService.js' -import { UIServiceFactory } from './ui-services/UIServiceFactory.js' import { BaseError } from '../../exception/index.js' import { ApplicationProtocolVersion, AuthenticationType, type ChargingStationData, + ConfigurationSection, type ProcedureName, type ProtocolRequest, type ProtocolResponse, @@ -18,15 +17,27 @@ import { type ResponsePayload, type UIServerConfiguration } from '../../types/index.js' +import { logger } from '../../utils/index.js' +import type { AbstractUIService } from './ui-services/AbstractUIService.js' +import { UIServiceFactory } from './ui-services/UIServiceFactory.js' +import { getUsernameAndPasswordFromAuthorizationToken } from './UIServerUtils.js' + +const moduleName = 'AbstractUIServer' export abstract class AbstractUIServer { public readonly chargingStations: Map + public readonly chargingStationTemplates: Set protected readonly httpServer: Server | Http2Server - protected readonly responseHandlers: Map + protected readonly responseHandlers: Map< + `${string}-${string}-${string}-${string}-${string}`, + ServerResponse | WebSocket + > + protected readonly uiServices: Map public constructor (protected readonly uiServerConfiguration: UIServerConfiguration) { this.chargingStations = new Map() + this.chargingStationTemplates = new Set() switch (this.uiServerConfiguration.version) { case ApplicationProtocolVersion.VERSION_11: this.httpServer = new Server() @@ -36,28 +47,42 @@ export abstract class AbstractUIServer { break default: throw new BaseError( - `Unsupported application protocol version ${this.uiServerConfiguration.version}` + `Unsupported application protocol version ${this.uiServerConfiguration.version} in '${ConfigurationSection.uiServer}' configuration section` ) } - this.responseHandlers = new Map() + this.responseHandlers = new Map< + `${string}-${string}-${string}-${string}-${string}`, + ServerResponse | WebSocket + >() this.uiServices = new Map() } public buildProtocolRequest ( - id: string, + uuid: `${string}-${string}-${string}-${string}-${string}`, procedureName: ProcedureName, requestPayload: RequestPayload ): ProtocolRequest { - return [id, procedureName, requestPayload] + return [uuid, procedureName, requestPayload] } - public buildProtocolResponse (id: string, responsePayload: ResponsePayload): ProtocolResponse { - return [id, responsePayload] + public buildProtocolResponse ( + uuid: `${string}-${string}-${string}-${string}-${string}`, + responsePayload: ResponsePayload + ): ProtocolResponse { + return [uuid, responsePayload] } public stop (): void { this.stopHttpServer() + for (const uiService of this.uiServices.values()) { + uiService.stop() + } + this.clearCaches() + } + + public clearCaches (): void { this.chargingStations.clear() + this.chargingStationTemplates.clear() } public async sendInternalRequest (request: ProtocolRequest): Promise { @@ -68,11 +93,17 @@ export abstract class AbstractUIServer { ?.requestHandler(request) as Promise) } - public hasResponseHandler (id: string): boolean { - return this.responseHandlers.has(id) + public hasResponseHandler (uuid: `${string}-${string}-${string}-${string}-${string}`): boolean { + return this.responseHandlers.has(uuid) } protected startHttpServer (): void { + this.httpServer.on('error', error => { + logger.error( + `${this.logPrefix(moduleName, 'start.httpServer.on.error')} HTTP server error:`, + error + ) + }) if (!this.httpServer.listening) { this.httpServer.listen(this.uiServerConfiguration.options) } @@ -85,11 +116,19 @@ export abstract class AbstractUIServer { } protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void { + const authorizationError = new BaseError('Unauthorized') if (this.isBasicAuthEnabled()) { - if (!this.isValidBasicAuth(req)) { - next(new BaseError('Unauthorized')) + if (!this.isValidBasicAuth(req, next)) { + next(authorizationError) + } + next() + } else if (this.isProtocolBasicAuthEnabled()) { + if (!this.isValidProtocolBasicAuth(req, next)) { + next(authorizationError) } next() + } else if (this.uiServerConfiguration.authentication?.enabled === true) { + next(authorizationError) } next() } @@ -97,27 +136,50 @@ export abstract class AbstractUIServer { private stopHttpServer (): void { if (this.httpServer.listening) { this.httpServer.close() + this.httpServer.removeAllListeners() } } private isBasicAuthEnabled (): boolean { return ( this.uiServerConfiguration.authentication?.enabled === true && - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition this.uiServerConfiguration.authentication.type === AuthenticationType.BASIC_AUTH ) } - private isValidBasicAuth (req: IncomingMessage): boolean { - const authorizationHeader = req.headers.authorization ?? '' - const authorizationToken = authorizationHeader.split(/\s+/).pop() ?? '' - const authentication = Buffer.from(authorizationToken, 'base64').toString() - const authenticationParts = authentication.split(/:/) - const username = authenticationParts.shift() - const password = authenticationParts.join(':') + private isProtocolBasicAuthEnabled (): boolean { + return ( + this.uiServerConfiguration.authentication?.enabled === true && + this.uiServerConfiguration.authentication.type === AuthenticationType.PROTOCOL_BASIC_AUTH + ) + } + + private isValidBasicAuth (req: IncomingMessage, next: (err?: Error) => void): boolean { + const [username, password] = getUsernameAndPasswordFromAuthorizationToken( + req.headers.authorization?.split(/\s+/).pop() ?? '', + next + ) + return this.isValidUsernameAndPassword(username, password) + } + + private isValidProtocolBasicAuth (req: IncomingMessage, next: (err?: Error) => void): boolean { + const authorizationProtocol = req.headers['sec-websocket-protocol']?.split(/,\s+/).pop() + const [username, password] = getUsernameAndPasswordFromAuthorizationToken( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + `${authorizationProtocol}${Array(((4 - (authorizationProtocol!.length % 4)) % 4) + 1).join( + '=' + )}` + .split('.') + .pop() ?? '', + next + ) + return this.isValidUsernameAndPassword(username, password) + } + + private isValidUsernameAndPassword (username: string, password: string): boolean { return ( this.uiServerConfiguration.authentication?.username === username && - this.uiServerConfiguration.authentication?.password === password + this.uiServerConfiguration.authentication.password === password ) }