X-Git-Url: https://git.piment-noir.org/?a=blobdiff_plain;f=src%2Fcharging-station%2Fui-server%2FAbstractUIServer.ts;h=a786fbbddc2ed43623316f297a7f5ac94503d474;hb=48847bc0d5e594c3e4c0b81c580ac6df48052668;hp=152749e5ffc14ac3114e2dd081d457fff37ab2df;hpb=32de5a575189d226213641f5ee36004f8454cb50;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 152749e5..a786fbbd 100644 --- a/src/charging-station/ui-server/AbstractUIServer.ts +++ b/src/charging-station/ui-server/AbstractUIServer.ts @@ -1,23 +1,190 @@ -import { Server as HttpServer } from 'http'; +import { type IncomingMessage, Server, type ServerResponse } from 'node:http' +import { createServer, type Http2Server } from 'node:http2' -import WebSocket from 'ws'; +import type { WebSocket } from 'ws' -import { ChargingStationData } from '../../types/ChargingStationWorker'; -import { ProtocolVersion } from '../../types/UIProtocol'; -import AbstractUIService from './ui-services/AbstractUIService'; +import { BaseError } from '../../exception/index.js' +import { + ApplicationProtocolVersion, + AuthenticationType, + type ChargingStationData, + ConfigurationSection, + type ProcedureName, + type ProtocolRequest, + type ProtocolResponse, + ProtocolVersion, + type RequestPayload, + 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; - protected readonly uiServices: Map; - protected server: WebSocket.Server | HttpServer; + public readonly chargingStations: Map + public readonly chargingStationTemplates: Set + protected readonly httpServer: Server | Http2Server + 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() + break + case ApplicationProtocolVersion.VERSION_20: + this.httpServer = createServer() + break + default: + throw new BaseError( + `Unsupported application protocol version ${this.uiServerConfiguration.version} in '${ConfigurationSection.uiServer}' configuration section` + ) + } + this.responseHandlers = new Map< + `${string}-${string}-${string}-${string}-${string}`, + ServerResponse | WebSocket + >() + this.uiServices = new Map() + } + + public buildProtocolRequest ( + uuid: `${string}-${string}-${string}-${string}-${string}`, + procedureName: ProcedureName, + requestPayload: RequestPayload + ): ProtocolRequest { + return [uuid, procedureName, requestPayload] + } + + 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 { + const protocolVersion = ProtocolVersion['0.0.1'] + this.registerProtocolVersionUIService(protocolVersion) + return await (this.uiServices + .get(protocolVersion) + ?.requestHandler(request) as Promise) + } + + 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) + } + } + + protected registerProtocolVersionUIService (version: ProtocolVersion): void { + if (!this.uiServices.has(version)) { + this.uiServices.set(version, UIServiceFactory.getUIServiceImplementation(version, this)) + } + } + + protected authenticate (req: IncomingMessage, next: (err?: Error) => void): void { + const authorizationError = new BaseError('Unauthorized') + if (this.isBasicAuthEnabled()) { + 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() + } + + private stopHttpServer (): void { + if (this.httpServer.listening) { + this.httpServer.close() + this.httpServer.removeAllListeners() + } + } + + private isBasicAuthEnabled (): boolean { + return ( + this.uiServerConfiguration.authentication?.enabled === true && + this.uiServerConfiguration.authentication.type === AuthenticationType.BASIC_AUTH + ) + } + + 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) + } - public constructor() { - this.chargingStations = new Map(); - this.uiServices = new Map(); + private isValidUsernameAndPassword (username: string, password: string): boolean { + return ( + this.uiServerConfiguration.authentication?.username === username && + this.uiServerConfiguration.authentication.password === password + ) } - public abstract start(): void; - public abstract stop(): void; - public abstract sendResponse(message: string): void; - public abstract logPrefix(modName?: string, methodName?: string): string; + public abstract start (): void + public abstract sendRequest (request: ProtocolRequest): void + public abstract sendResponse (response: ProtocolResponse): void + public abstract logPrefix (moduleName?: string, methodName?: string, prefixSuffix?: string): string }