| logFile | | combined.log | string | log file relative path |
| logErrorFile | | error.log | string | error log file relative path |
| worker | | {<br />"processType": "workerSet",<br />"startDelay": 500,<br />"elementStartDelay": 0,<br />"elementsPerWorker": 1,<br />"poolMinSize": 4,<br />"poolMaxSize": 16,<br />"poolStrategy": "ROUND_ROBBIN"<br />} | {<br />processType: WorkerProcessType;<br />startDelay: number;<br />elementStartDelay: number;<br />elementsPerWorker: number;<br />poolMinSize: number;<br />poolMaxSize: number;<br />poolStrategy: WorkerChoiceStrategy;<br />} | Worker configuration section:<br />- processType: worker threads process type (workerSet/staticPool/dynamicPool)<br />- startDelay: milliseconds to wait at worker threads startup (only for workerSet threads process type)<br />- elementStartDelay: milliseconds to wait at charging station startup<br />- elementsPerWorker: number of charging stations per worker threads for the `workerSet` process type<br />- poolMinSize: worker threads pool minimum number of threads</br >- poolMaxSize: worker threads pool maximum number of threads<br />- poolStrategy: worker threads pool [poolifier](https://github.com/poolifier/poolifier) worker choice strategy |
-| uiServer | | {<br />"enabled": true,<br />"type": "ws",<br />"options": {<br />"host: "localhost",<br />"port": 8080<br />}<br />} | {<br />enabled: boolean;<br />type: ApplicationProtocol;<br />options: ServerOptions;<br />} | UI server configuration section |
+| uiServer | | {<br />"enabled": true,<br />"type": "ws",<br />"options": {<br />"host: "localhost",<br />"port": 8080<br />}<br />} | {<br />enabled: boolean;<br />type: ApplicationProtocol;<br />options: ServerOptions;<br />authentication: {<br />enabled: boolean;<br />type: AuthenticationType;<br />username: string;<br />password: string;<br />}<br />} | UI server configuration section |
| performanceStorage | | {<br />"enabled": false,<br />"type": "jsonfile",<br />"file:///performanceRecords.json"<br />} | {<br />enabled: boolean;<br />type: string;<br />URI: string;<br />}<br />where type can be 'jsonfile' or 'mongodb' | performance storage configuration section |
| stationTemplateUrls | | {}[] | {<br />file: string;<br />numberOfStations: number;<br />}[] | array of charging station configuration templates URIs configuration section (charging station configuration template file name and number of stations) |
import WorkerFactory from '../worker/WorkerFactory';
import { ChargingStationUtils } from './ChargingStationUtils';
import type { AbstractUIServer } from './ui-server/AbstractUIServer';
-import { UIServiceUtils } from './ui-server/ui-services/UIServiceUtils';
import UIServerFactory from './ui-server/UIServerFactory';
const moduleName = 'Bootstrap';
'ChargingStationWorker' + path.extname(fileURLToPath(import.meta.url))
);
this.initialize();
- Configuration.getUIServer().enabled &&
- (this.uiServer = UIServerFactory.getUIServerImplementation(Configuration.getUIServer().type, {
- ...Configuration.getUIServer().options,
- handleProtocols: UIServiceUtils.handleProtocols,
- }));
- Configuration.getPerformanceStorage().enabled &&
+ if (Configuration.getUIServer().enabled === true) {
+ this.uiServer = UIServerFactory.getUIServerImplementation(
+ Configuration.getUIServer().type,
+ Configuration.getUIServer()
+ );
+ }
+ Configuration.getPerformanceStorage().enabled === true &&
(this.storage = StorageFactory.getStorage(
Configuration.getPerformanceStorage().type,
Configuration.getPerformanceStorage().uri,
-import type { Server as HttpServer } from 'http';
-
-import type WebSocket from 'ws';
+import type { IncomingMessage, Server } from 'http';
import type { ChargingStationData } from '../../types/ChargingStationWorker';
-import type {
+import type { UIServerConfiguration } from '../../types/ConfigurationData';
+import {
+ AuthenticationType,
ProcedureName,
ProtocolRequest,
ProtocolResponse,
export abstract class AbstractUIServer {
public readonly chargingStations: Map<string, ChargingStationData>;
- protected server: WebSocket.Server | HttpServer;
+ protected httpServer: Server;
protected readonly uiServices: Map<ProtocolVersion, AbstractUIService>;
- public constructor() {
+ public constructor(protected readonly uiServerConfiguration: UIServerConfiguration) {
this.chargingStations = new Map<string, ChargingStationData>();
this.uiServices = new Map<ProtocolVersion, AbstractUIService>();
}
return [id, responsePayload];
}
+ protected isBasicAuthEnabled(): boolean {
+ return (
+ this.uiServerConfiguration.authentication?.enabled === true &&
+ this.uiServerConfiguration.authentication?.type === AuthenticationType.BASIC_AUTH
+ );
+ }
+
+ protected 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(':');
+ return (
+ this.uiServerConfiguration.authentication?.username === username &&
+ this.uiServerConfiguration.authentication?.password === password
+ );
+ }
+
public abstract start(): void;
public abstract stop(): void;
public abstract sendRequest(request: ProtocolRequest): void;
import { StatusCodes } from 'http-status-codes';
import BaseError from '../../exception/BaseError';
-import type { ServerOptions } from '../../types/ConfigurationData';
+import type { UIServerConfiguration } from '../../types/ConfigurationData';
import {
ProcedureName,
Protocol,
RequestPayload,
ResponseStatus,
} from '../../types/UIProtocol';
-import Configuration from '../../utils/Configuration';
import logger from '../../utils/Logger';
import Utils from '../../utils/Utils';
import { AbstractUIServer } from './AbstractUIServer';
export default class UIHttpServer extends AbstractUIServer {
private readonly responseHandlers: Map<string, responseHandler>;
- public constructor(private options?: ServerOptions) {
- super();
- this.server = new Server(this.requestListener.bind(this) as RequestListener);
+ public constructor(protected readonly uiServerConfiguration: UIServerConfiguration) {
+ super(uiServerConfiguration);
+ this.httpServer = new Server(this.requestListener.bind(this) as RequestListener);
this.responseHandlers = new Map<string, responseHandler>();
}
public start(): void {
- if ((this.server as Server).listening === false) {
- (this.server as Server).listen(this.options ?? Configuration.getUIServer().options);
+ if (this.httpServer.listening === false) {
+ this.httpServer.listen(this.uiServerConfiguration.options);
}
}
}
private requestListener(req: IncomingMessage, res: ServerResponse): void {
+ if (this.isBasicAuthEnabled() === true && this.isValidBasicAuth(req) === false) {
+ res.setHeader('Content-Type', 'text/plain');
+ res.setHeader('WWW-Authenticate', 'Basic realm=users');
+ res.writeHead(StatusCodes.UNAUTHORIZED);
+ res.end(`${StatusCodes.UNAUTHORIZED} Unauthorized`);
+ return;
+ }
// Expected request URL pathname: /ui/:version/:procedureName
const [protocol, version, procedureName] = req.url?.split('/').slice(1) as [
Protocol,
import chalk from 'chalk';
-import type { ServerOptions } from '../../types/ConfigurationData';
+import type { UIServerConfiguration } from '../../types/ConfigurationData';
import { ApplicationProtocol } from '../../types/UIProtocol';
import Configuration from '../../utils/Configuration';
import type { AbstractUIServer } from './AbstractUIServer';
public static getUIServerImplementation(
applicationProtocol: ApplicationProtocol,
- options?: ServerOptions
+ uiServerConfiguration?: UIServerConfiguration
): AbstractUIServer | null {
- if (!UIServiceUtils.isLoopback(options?.host)) {
+ if (UIServiceUtils.isLoopback(uiServerConfiguration.options?.host) === false) {
console.warn(
chalk.magenta(
'Loopback address not detected in UI server configuration. This is not recommended.'
}
switch (applicationProtocol) {
case ApplicationProtocol.WS:
- return new UIWebSocketServer(options ?? Configuration.getUIServer().options);
+ return new UIWebSocketServer(uiServerConfiguration ?? Configuration.getUIServer());
case ApplicationProtocol.HTTP:
- return new UIHttpServer(options ?? Configuration.getUIServer().options);
+ return new UIHttpServer(uiServerConfiguration ?? Configuration.getUIServer());
default:
return null;
}
-import type { IncomingMessage } from 'http';
+import { IncomingMessage, createServer } from 'http';
+import type internal from 'stream';
-import WebSocket, { RawData } from 'ws';
+import { StatusCodes } from 'http-status-codes';
+import WebSocket, { RawData, WebSocketServer } from 'ws';
import BaseError from '../../exception/BaseError';
-import type { ServerOptions } from '../../types/ConfigurationData';
+import type { UIServerConfiguration } from '../../types/ConfigurationData';
import type { ProtocolRequest, ProtocolResponse } from '../../types/UIProtocol';
import { WebSocketCloseEventStatusCode } from '../../types/WebSocket';
-import Configuration from '../../utils/Configuration';
import logger from '../../utils/Logger';
import Utils from '../../utils/Utils';
import { AbstractUIServer } from './AbstractUIServer';
const moduleName = 'UIWebSocketServer';
export default class UIWebSocketServer extends AbstractUIServer {
- public constructor(options?: ServerOptions) {
- super();
- this.server = new WebSocket.Server(options ?? Configuration.getUIServer().options);
+ private readonly webSocketServer: WebSocketServer;
+
+ public constructor(protected readonly uiServerConfiguration: UIServerConfiguration) {
+ super(uiServerConfiguration);
+ this.httpServer = createServer();
+ this.webSocketServer = new WebSocketServer({
+ handleProtocols: UIServiceUtils.handleProtocols,
+ noServer: true,
+ });
}
public start(): void {
- this.server.on('connection', (ws: WebSocket, request: IncomingMessage): void => {
+ this.webSocketServer.on('connection', (ws: WebSocket, req: IncomingMessage): void => {
const [protocol, version] = UIServiceUtils.getProtocolAndVersion(ws.protocol);
if (UIServiceUtils.isProtocolAndVersionSupported(protocol, version) === false) {
logger.error(
);
});
});
+ this.httpServer.on(
+ 'upgrade',
+ (req: IncomingMessage, socket: internal.Duplex, head: Buffer): void => {
+ this.authenticate(req, (err) => {
+ if (err) {
+ socket.write(`HTTP/1.1 ${StatusCodes.UNAUTHORIZED} Unauthorized\r\n\r\n`);
+ socket.destroy();
+ return;
+ }
+ this.webSocketServer.handleUpgrade(req, socket, head, (ws: WebSocket) => {
+ this.webSocketServer.emit('connection', ws, req);
+ });
+ });
+ }
+ );
+ if (this.httpServer.listening === false) {
+ this.httpServer.listen(this.uiServerConfiguration.options);
+ }
}
public stop(): void {
}
private broadcastToClients(message: string): void {
- for (const client of (this.server as WebSocket.Server).clients) {
+ for (const client of this.webSocketServer.clients) {
if (client?.readyState === WebSocket.OPEN) {
client.send(message);
}
}
}
+ private authenticate(req: IncomingMessage, next: (err: Error) => void): void {
+ if (this.isBasicAuthEnabled() === true) {
+ if (this.isValidBasicAuth(req) === false) {
+ next(new Error('Unauthorized'));
+ } else {
+ next(undefined);
+ }
+ } else {
+ next(undefined);
+ }
+ }
+
private validateRawDataRequest(rawData: RawData): ProtocolRequest {
// logger.debug(
// `${this.logPrefix(
import type { ListenOptions } from 'net';
import type { WorkerChoiceStrategy } from 'poolifier';
-import type { ServerOptions as WSServerOptions } from 'ws';
import type { StorageType } from './Storage';
-import type { ApplicationProtocol } from './UIProtocol';
+import type { ApplicationProtocol, AuthenticationType } from './UIProtocol';
import type { WorkerProcessType } from './Worker';
-export type ServerOptions = WSServerOptions & ListenOptions;
+export type ServerOptions = ListenOptions;
export enum SupervisionUrlDistribution {
ROUND_ROBIN = 'round-robin',
enabled?: boolean;
type?: ApplicationProtocol;
options?: ServerOptions;
+ authentication?: {
+ enabled: boolean;
+ type: AuthenticationType;
+ username?: string;
+ password?: string;
+ };
}
export interface StorageConfiguration {
WS = 'ws',
}
+export enum AuthenticationType {
+ BASIC_AUTH = 'basic-auth',
+}
+
export enum ProtocolVersion {
'0.0.1' = '0.0.1',
}
host: string;
port: number;
protocol: string;
+ username?: string;
+ password?: string;
}