diff --git a/00_Base/src/config/types.ts b/00_Base/src/config/types.ts index f45f74d7b..e61fda784 100644 --- a/00_Base/src/config/types.ts +++ b/00_Base/src/config/types.ts @@ -7,8 +7,27 @@ import { z } from "zod"; import { RegistrationStatusEnumType } from "../ocpp/model/enums"; import { EventGroup } from ".."; +// TODO: Refactor other objects out of system config, such as certificatesModuleInputSchema etc. +export const websocketServerInputSchema = z.object({ + // TODO: Add support for tenant ids on server level for tenant-specific behavior + id: z.string().optional(), + host: z.string().default('localhost').optional(), + port: z.number().int().positive().default(8080).optional(), + pingInterval: z.number().int().positive().default(60).optional(), + protocol: z.string().default('ocpp2.0.1').optional(), + securityProfile: z.number().int().min(0).max(3).default(0).optional(), + tlsKeysFilepath: z.string().optional(), + tlsCertificateChainFilepath: z.string().optional(), + mtlsCertificateAuthorityRootsFilepath: z.string().optional(), + mtlsCertificateAuthorityKeysFilepath: z.string().optional() +}); + export const systemConfigInputSchema = z.object({ env: z.enum(["development", "production"]), + centralSystem: z.object({ + host: z.string().default("localhost").optional(), + port: z.number().int().positive().default(8081).optional(), + }), modules: z.object({ certificates: z.object({ endpointPrefix: z.string().default(EventGroup.Certificates).optional(), @@ -25,7 +44,7 @@ export const systemConfigInputSchema = z.object({ endpointPrefix: z.string().default(EventGroup.Configuration).optional(), host: z.string().default("localhost").optional(), port: z.number().int().positive().default(8081).optional(), - }), // Configuration module is required + }), evdriver: z.object({ endpointPrefix: z.string().default(EventGroup.EVDriver).optional(), host: z.string().default("localhost").optional(), @@ -50,7 +69,7 @@ export const systemConfigInputSchema = z.object({ endpointPrefix: z.string().default(EventGroup.Transactions).optional(), host: z.string().default("localhost").optional(), port: z.number().int().positive().default(8081).optional(), - }), // Transactions module is required + }) }), data: z.object({ sequelize: z.object({ @@ -58,8 +77,8 @@ export const systemConfigInputSchema = z.object({ port: z.number().int().positive().default(5432).optional(), database: z.string().default('csms').optional(), dialect: z.any().default('sqlite').optional(), - username: z.string().optional(), - password: z.string().optional(), + username: z.string().optional(), + password: z.string().optional(), storage: z.string().default('csms.sqlite').optional(), sync: z.boolean().default(false).optional(), }), @@ -102,40 +121,50 @@ export const systemConfigInputSchema = z.object({ logoPath: z.string(), exposeData: z.boolean().default(true).optional(), exposeMessage: z.boolean().default(true).optional(), - }).optional() - }), - server: z.object({ - logLevel: z.number().min(0).max(6).default(0).optional(), - host: z.string().default("localhost").optional(), - port: z.number().int().positive().default(8081).optional() - }), - websocket: z.object({ - pingInterval: z.number().int().positive().default(60).optional(), - maxCallLengthSeconds: z.number().int().positive().default(5).optional(), - maxCachingSeconds: z.number().int().positive().default(10).optional() + }).optional(), + networkConnection: z.object({ + websocketServers: z.array(websocketServerInputSchema.optional()) + }) }), - websocketSecurity: z.object({ - // TODO: Add support for each websocketServer/tenant to have its own certificates - // Such as when different tenants use different certificate roots for additional security - tlsKeysFilepath: z.string().optional(), - tlsCertificateChainFilepath: z.string().optional(), - mtlsCertificateAuthorityRootsFilepath: z.string().optional(), - mtlsCertificateAuthorityKeysFilepath: z.string().optional() - }).optional(), - websocketServer: z.array(z.object({ - // This allows multiple servers, ideally for different security profile levels - // TODO: Add support for tenant ids on server level for tenant-specific behavior - securityProfile: z.number().int().min(0).max(3).default(0).optional(), - port: z.number().int().positive().default(8080).optional(), - host: z.string().default('localhost').optional(), - protocol: z.string().default('ocpp2.0.1').optional(), - })) + logLevel: z.number().min(0).max(6).default(0).optional(), + maxCallLengthSeconds: z.number().int().positive().default(5).optional(), + maxCachingSeconds: z.number().int().positive().default(10).optional() }); export type SystemConfigInput = z.infer; +export const websocketServerSchema = z.object({ + // TODO: Add support for tenant ids on server level for tenant-specific behavior + id: z.string(), + host: z.string(), + port: z.number().int().positive(), + pingInterval: z.number().int().positive(), + protocol: z.string(), + securityProfile: z.number().int().min(0).max(3), + tlsKeysFilepath: z.string().optional(), + tlsCertificateChainFilepath: z.string().optional(), + mtlsCertificateAuthorityRootsFilepath: z.string().optional(), + mtlsCertificateAuthorityKeysFilepath: z.string().optional() +}).refine(obj => { + switch (obj.securityProfile) { + case 0: // No security + case 1: // Basic Auth + return true; + case 2: // Basic Auth + TLS + return obj.tlsKeysFilepath && obj.tlsCertificateChainFilepath; + case 3: // mTLS + return obj.mtlsCertificateAuthorityRootsFilepath && obj.mtlsCertificateAuthorityKeysFilepath; + default: + return false; + } +}); + export const systemConfigSchema = z.object({ env: z.enum(["development", "production"]), + centralSystem: z.object({ + host: z.string(), + port: z.number().int().positive() + }), modules: z.object({ certificates: z.object({ endpointPrefix: z.string(), @@ -156,7 +185,7 @@ export const systemConfigSchema = z.object({ /** * If false, only data endpoint can update boot status to accepted */ - autoAccept: z.boolean(), + autoAccept: z.boolean(), endpointPrefix: z.string(), host: z.string().optional(), port: z.number().int().positive().optional(), @@ -232,73 +261,27 @@ export const systemConfigSchema = z.object({ logoPath: z.string(), exposeData: z.boolean(), exposeMessage: z.boolean(), - }).optional() - }), - server: z.object({ - logLevel: z.number().min(0).max(6), - host: z.string(), - port: z.number().int().positive() - }), - websocket: z.object({ - pingInterval: z.number().int().positive(), - maxCallLengthSeconds: z.number().int().positive(), - maxCachingSeconds: z.number().int().positive() - }).refine(websocketServer => websocketServer.maxCachingSeconds >= websocketServer.maxCallLengthSeconds, { - message: 'maxCachingSeconds cannot be less than maxCallLengthSeconds' + }).optional(), + networkConnection: z.object({ + websocketServers: z.array(websocketServerSchema).refine(array => { + const idsSeen = new Set(); + return array.filter(obj => { + if (idsSeen.has(obj.id)) { + return false; + } else { + idsSeen.add(obj.id); + return true; + } + }); + }) + }) }), - websocketSecurity: z.object({ - // TODO: Add support for each websocketServer/tenant to have its own certificates - // Such as when different tenants use different certificate roots for additional security - tlsKeysFilepath: z.string().optional(), - tlsCertificateChainFilepath: z.string().optional(), - mtlsCertificateAuthorityRootsFilepath: z.string().optional(), - mtlsCertificateAuthorityKeysFilepath: z.string().optional() - }).optional(), - websocketServer: z.array(z.object({ - // This allows multiple servers, ideally for different security profile levels - // TODO: Add support for tenant ids on server level for tenant-specific behavior - securityProfile: z.number().int().min(0).max(3), - port: z.number().int().positive(), - host: z.string(), - protocol: z.string(), - })).refine(websocketServers => checkForHostPortDuplicates(websocketServers), { - message: 'host and port must be unique' - }) -}).refine((data) => { - const wsSecurity = data.websocketSecurity; - - const requiresTls = data.websocketServer.some(server => server.securityProfile >= 2); - const tlsFieldsFilled = wsSecurity?.tlsKeysFilepath && wsSecurity?.tlsCertificateChainFilepath; - - const requiresMtls = data.websocketServer.some(server => server.securityProfile >= 3); - const mtlsFieldsFilled = wsSecurity?.mtlsCertificateAuthorityRootsFilepath && wsSecurity?.mtlsCertificateAuthorityKeysFilepath; - - if (requiresTls && !tlsFieldsFilled) { - return false; - } - - if (requiresMtls && !mtlsFieldsFilled) { - return false; - } - - return true; -}, { - message: "TLS and/or mTLS fields must be filled based on the security profile of the websocket server." + logLevel: z.number().min(0).max(6), + maxCallLengthSeconds: z.number().int().positive(), + maxCachingSeconds: z.number().int().positive() +}).refine(obj => obj.maxCachingSeconds >= obj.maxCallLengthSeconds, { + message: 'maxCachingSeconds cannot be less than maxCallLengthSeconds' }); -export type SystemConfig = z.infer; - -function checkForHostPortDuplicates(websocketServers: { port: number; host: string;}[]): unknown { - const uniqueCombinations = new Set(); - for (const item of websocketServers) { - const combo = `${item.host}:${item.port}`; - - if (uniqueCombinations.has(combo)) { - return false; // Duplicate found - } - - uniqueCombinations.add(combo); - } - - return true; -} \ No newline at end of file +export type WebsocketServerConfig = z.infer; +export type SystemConfig = z.infer; \ No newline at end of file diff --git a/00_Base/src/index.ts b/00_Base/src/index.ts index b0c28d0eb..5c0858e45 100644 --- a/00_Base/src/index.ts +++ b/00_Base/src/index.ts @@ -21,7 +21,7 @@ export * from "./ocpp/persistence"; export { BootConfig, BOOT_STATUS } from "./config/BootConfig"; export { defineConfig } from "./config/defineConfig"; -export { SystemConfig } from "./config/types"; +export { SystemConfig, WebsocketServerConfig } from "./config/types"; // Utils diff --git a/00_Base/src/interfaces/modules/AbstractModule.ts b/00_Base/src/interfaces/modules/AbstractModule.ts index c6d942a9d..2d991a8d0 100644 --- a/00_Base/src/interfaces/modules/AbstractModule.ts +++ b/00_Base/src/interfaces/modules/AbstractModule.ts @@ -66,7 +66,7 @@ export abstract class AbstractModule implements IModule { this._config = config; // Update all necessary settings for hot reload this._logger.info(`Updating system configuration for ${this._eventGroup} module...`); - this._logger.settings.minLevel = this._config.server.logLevel; + this._logger.settings.minLevel = this._config.logLevel; } get config(): SystemConfig { @@ -105,7 +105,7 @@ export abstract class AbstractModule implements IModule { protected _initLogger(baseLogger?: Logger): Logger { return baseLogger ? baseLogger.getSubLogger({ name: this.constructor.name }) : new Logger({ name: this.constructor.name, - minLevel: this._config.server.logLevel, + minLevel: this._config.logLevel, hideLogPositionForProduction: this._config.env === "production" }); } @@ -133,7 +133,7 @@ export abstract class AbstractModule implements IModule { async handle(message: IMessage, props?: HandlerProperties): Promise { if (message.state === MessageState.Response) { this.handleMessageApiCallback(message as IMessage); - this._cache.set(message.context.correlationId, JSON.stringify(message.payload), message.context.stationId, this._config.websocket.maxCachingSeconds); + this._cache.set(message.context.correlationId, JSON.stringify(message.payload), message.context.stationId, this._config.maxCachingSeconds); } try { const handlerDefinition = (Reflect.getMetadata(AS_HANDLER_METADATA, this.constructor) as Array).filter((h) => h.action === message.action).pop(); @@ -202,7 +202,7 @@ export abstract class AbstractModule implements IModule { if (callbackUrl) { // TODO: Handle callErrors, failure to send to charger, timeout from charger, with different responses to callback this._cache.set(_correlationId, callbackUrl, this.CALLBACK_URL_CACHE_PREFIX + identifier, - this._config.websocket.maxCachingSeconds); + this._config.maxCachingSeconds); } // TODO: Future - Compound key with tenantId return this._cache.get(identifier, CacheNamespace.Connections, () => ClientConnection).then((connection) => { diff --git a/02_Util/src/server/example/server.ts b/02_Util/src/server/example/server.ts index 4f71afeeb..3a86802af 100644 --- a/02_Util/src/server/example/server.ts +++ b/02_Util/src/server/example/server.ts @@ -64,22 +64,22 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral this._deviceModelRepository = deviceModelRepository || new DeviceModelRepository(this._config, this._logger); this._httpServers = []; - this._config.websocketServer.forEach(wsServer => { + this._config.util.networkConnection.websocketServers.forEach(wsServer => { let _httpServer; switch (wsServer.securityProfile) { case 3: // mTLS _httpServer = https.createServer({ - key: fs.readFileSync(this._config.websocketSecurity?.tlsKeysFilepath as string), - cert: fs.readFileSync(this._config.websocketSecurity?.tlsCertificateChainFilepath as string), - ca: fs.readFileSync(this._config.websocketSecurity?.mtlsCertificateAuthorityRootsFilepath as string), + key: fs.readFileSync(wsServer.tlsKeysFilepath as string), + cert: fs.readFileSync(wsServer.tlsCertificateChainFilepath as string), + ca: fs.readFileSync(wsServer.mtlsCertificateAuthorityRootsFilepath as string), requestCert: true, rejectUnauthorized: true }, this._onHttpRequest.bind(this)); break; case 2: // TLS _httpServer = https.createServer({ - key: fs.readFileSync(this._config.websocketSecurity?.tlsKeysFilepath as string), - cert: fs.readFileSync(this._config.websocketSecurity?.tlsCertificateChainFilepath as string) + key: fs.readFileSync(wsServer.tlsKeysFilepath as string), + cert: fs.readFileSync(wsServer.tlsCertificateChainFilepath as string) }, this._onHttpRequest.bind(this)); break; case 1: @@ -95,7 +95,7 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral clientTracking: false }); - _socketServer.on('connection', (ws: WebSocket, req: http.IncomingMessage) => this._onConnection(ws, req)); + _socketServer.on('connection', (ws: WebSocket, req: http.IncomingMessage) => this._onConnection(ws, wsServer.pingInterval, req)); _socketServer.on('error', (wss: WebSocketServer, error: Error) => this._onError(wss, error)); _socketServer.on('close', (wss: WebSocketServer) => this._onClose(wss)); @@ -147,7 +147,7 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral throw new OcppError(messageId, ErrorCode.FormatViolation, "Invalid message format", { errors: errors }); } // Ensure only one call is processed at a time - return this._cache.setIfNotExist(connection.identifier, `${action}:${messageId}`, CacheNamespace.Transactions, this._config.websocket.maxCallLengthSeconds); + return this._cache.setIfNotExist(connection.identifier, `${action}:${messageId}`, CacheNamespace.Transactions, this._config.maxCallLengthSeconds); }).catch(error => { if (error instanceof OcppError) { this.sendCallError(connection.identifier, error.asCallError()); @@ -187,7 +187,7 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral .then(cachedActionMessageId => { this._cache.remove(connection.identifier, CacheNamespace.Transactions); // Always remove pending call transaction if (!cachedActionMessageId) { - throw new OcppError(messageId, ErrorCode.InternalError, "MessageId not found, call may have timed out", { "maxCallLengthSeconds": this._config.websocket.maxCallLengthSeconds }); + throw new OcppError(messageId, ErrorCode.InternalError, "MessageId not found, call may have timed out", { "maxCallLengthSeconds": this._config.maxCallLengthSeconds }); } const [actionString, cachedMessageId] = cachedActionMessageId.split(/:(.*)/); // Returns all characters after first ':' in case ':' is used in messageId if (messageId !== cachedMessageId) { @@ -228,7 +228,7 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral .then(cachedActionMessageId => { this._cache.remove(connection.identifier, CacheNamespace.Transactions); // Always remove pending call transaction if (!cachedActionMessageId) { - throw new OcppError(messageId, ErrorCode.InternalError, "MessageId not found, call may have timed out", { "maxCallLengthSeconds": this._config.websocket.maxCallLengthSeconds }); + throw new OcppError(messageId, ErrorCode.InternalError, "MessageId not found, call may have timed out", { "maxCallLengthSeconds": this._config.maxCallLengthSeconds }); } const [actionString, cachedMessageId] = cachedActionMessageId.split(/:(.*)/); // Returns all characters after first ':' in case ':' is used in messageId if (messageId !== cachedMessageId) { @@ -258,7 +258,7 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral const action = message[2] as CallAction; if (await this._sendCallIsAllowed(identifier, message)) { if (await this._cache.setIfNotExist(identifier, `${action}:${messageId}`, - CacheNamespace.Transactions, this._config.websocket.maxCallLengthSeconds)) { + CacheNamespace.Transactions, this._config.maxCallLengthSeconds)) { // Intentionally removing NULL values from object for OCPP conformity const rawMessage = JSON.stringify(message, (k, v) => v ?? undefined); return this._sendMessage(identifier, rawMessage); @@ -494,7 +494,7 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral * @param {IncomingMessage} req - The request object associated with the connection. * @return {void} */ - private _onConnection(ws: WebSocket, req: http.IncomingMessage): void { + private _onConnection(ws: WebSocket, pingInterval: number, req: http.IncomingMessage): void { const identifier = this._getClientIdFromUrl(req.url as string); this._connections.set(identifier, ws); @@ -512,7 +512,7 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral this._logger.info("Successfully connected new charging station.", identifier); // Register all websocket events - this._registerWebsocketEvents(identifier, ws); + this._registerWebsocketEvents(identifier, ws, pingInterval); // Resume the WebSocket event emitter after events have been subscribed to ws.resume(); @@ -531,7 +531,7 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral * @param {WebSocket} ws - The WebSocket object representing the connection. * @return {void} This function does not return anything. */ - private _registerWebsocketEvents(identifier: string, ws: WebSocket): void { + private _registerWebsocketEvents(identifier: string, ws: WebSocket, pingInterval: number): void { ws.onerror = (event: ErrorEvent) => { this._logger.error("Connection error encountered for", identifier, event.error, event.message, event.type); @@ -566,13 +566,13 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral if (clientConnection) { clientConnection.isAlive = true; this._cache.set(clientConnection.identifier, JSON.stringify(instanceToPlain(clientConnection)), CacheNamespace.Connections).then(() => { - this._ping(clientConnection.identifier, ws); + this._ping(clientConnection.identifier, ws, pingInterval); }); } }); }); - this._ping(identifier, ws); + this._ping(identifier, ws, pingInterval); } /** @@ -658,9 +658,10 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral * * @param {string} identifier - The identifier of the client connection. * @param {WebSocket} ws - The WebSocket connection to ping. + * @param {number} pingInterval - The ping interval in milliseconds. * @return {void} This function does not return anything. */ - private _ping(identifier: string, ws: WebSocket): void { + private _ping(identifier: string, ws: WebSocket, pingInterval: number): void { setTimeout(() => { this._getClientConnection(identifier).then(clientConnection => { if (clientConnection && clientConnection.isAlive) { @@ -674,7 +675,7 @@ export class CentralSystemImpl extends AbstractCentralSystem implements ICentral ws.close(1011, "Client is not alive"); } }); - }, this._config.websocket.pingInterval * 1000); + }, pingInterval * 1000); } /** * diff --git a/03_Modules/Certificates/src/module/module.ts b/03_Modules/Certificates/src/module/module.ts index 71bed1c64..6aa8198cc 100644 --- a/03_Modules/Certificates/src/module/module.ts +++ b/03_Modules/Certificates/src/module/module.ts @@ -33,6 +33,7 @@ import deasyncPromise from "deasync-promise"; import * as forge from "node-forge"; import fs from "fs"; import { ILogObj, Logger } from 'tslog'; +import { CacheNamespace } from "@citrineos/base"; /** * Component that handles provisioning related messages. @@ -57,9 +58,9 @@ export class CertificatesModule extends AbstractModule { ]; protected _deviceModelRepository: IDeviceModelRepository; - private _securityCaCert?: forge.pki.Certificate; - private _securityCaPrivateKey?: forge.pki.rsa.PrivateKey; - + private _securityCaCerts: Map = new Map(); + private _securityCaPrivateKeys: Map = new Map(); + /** * Constructor */ @@ -84,12 +85,12 @@ export class CertificatesModule extends AbstractModule { constructor( config: SystemConfig, cache: ICache, - sender?: IMessageSender, - handler?: IMessageHandler, + sender: IMessageSender, + handler: IMessageHandler, logger?: Logger, deviceModelRepository?: IDeviceModelRepository ) { - super(config, cache, handler || new RabbitMqReceiver(config, logger, cache), sender || new RabbitMqSender(config, logger), EventGroup.Certificates, logger); + super(config, cache, handler || new RabbitMqReceiver(config, logger), sender || new RabbitMqSender(config, logger), EventGroup.Certificates, logger); const timer = new Timer(); this._logger.info(`Initializing...`); @@ -100,8 +101,18 @@ export class CertificatesModule extends AbstractModule { this._deviceModelRepository = deviceModelRepository || new sequelize.DeviceModelRepository(config, logger); - this._securityCaCert = this._config.websocketSecurity?.mtlsCertificateAuthorityRootsFilepath ? forge.pki.certificateFromPem(fs.readFileSync(this._config.websocketSecurity.mtlsCertificateAuthorityRootsFilepath as string, 'utf8')) : undefined; - this._securityCaPrivateKey = this._config.websocketSecurity?.mtlsCertificateAuthorityRootsFilepath ? forge.pki.privateKeyFromPem(fs.readFileSync(this._config.websocketSecurity.mtlsCertificateAuthorityKeysFilepath as string, 'utf8')) : undefined; + + this._config.util.networkConnection.websocketServers.forEach(server => { + if (server.securityProfile == 3) { + try { + this._securityCaCerts.set(server.id, forge.pki.certificateFromPem(fs.readFileSync(server.mtlsCertificateAuthorityRootsFilepath as string, 'utf8'))); + this._securityCaPrivateKeys.set(server.id, forge.pki.privateKeyFromPem(fs.readFileSync(server.mtlsCertificateAuthorityKeysFilepath as string, 'utf8'))); + } catch (error) { + this._logger.error("Unable to start Certificates module due to invalid security certificates for {}: {}", server, error); + throw error; + } + } + }); this._logger.info(`Initialized in ${timer.end()}ms...`); } @@ -118,7 +129,7 @@ export class CertificatesModule extends AbstractModule { this._logger.debug("Get15118EVCertificate received:", message, props); - this._logger.error("Get15118EVCertificate not implemented"); + this._logger.error("Get15118EVCertificate not implemented"); } @AsHandler(CallAction.GetCertificateStatus) @@ -129,7 +140,7 @@ export class CertificatesModule extends AbstractModule { this._logger.debug("GetCertificateStatus received:", message, props); - this._logger.error("GetCertificateStatus not implemented"); + this._logger.error("GetCertificateStatus not implemented"); } @@ -157,13 +168,6 @@ export class CertificatesModule extends AbstractModule { const certificateType = message.payload.certificateType; switch (certificateType) { case CertificateSigningUseEnumType.ChargingStationCertificate: - if (!this._securityCaCert || !this._securityCaPrivateKey) { - // this.sendCallResultWithMessage(message, { status: GenericStatusEnumType.Rejected, statusInfo: { reasonCode: 'SERVER_SECURITY_CA_NOT_CONFIGURED' } } as SignCertificateResponse); - this._logger.error("Security CA not configured"); - return; - } - caCert = this._securityCaCert; - caPrivateKey = this._securityCaPrivateKey; // Verify CSR... // @ts-ignore: Unreachable code error if (!(csr as any).verify() || !this.verifyChargingStationCertificateCSR(csr, message.context.stationId)) { @@ -172,15 +176,18 @@ export class CertificatesModule extends AbstractModule { this._logger.error("Invalid CSR: {}", message.payload.csr); return; } + const clientConnection: string = await this._cache.get(message.context.stationId, CacheNamespace.Connections) as string; + caCert = this._securityCaCerts.get(clientConnection) as forge.pki.Certificate; + caPrivateKey = this._securityCaPrivateKeys.get(clientConnection) as forge.pki.rsa.PrivateKey; break; default: this.sendCallResultWithMessage(message, { status: GenericStatusEnumType.Rejected, statusInfo: { reasonCode: 'SERVER_NOT_IMPLEMENTED', additionalInfo: certificateType } } as SignCertificateResponse); this._logger.error("Unimplemented certificate type {}", certificateType); return; } - + // this.sendCallResultWithMessage(message, { status: GenericStatusEnumType.Accepted } as SignCertificateResponse); - + // Create a new certificate const cert = forge.pki.createCertificate(); cert.publicKey = csr.publicKey as forge.pki.rsa.PublicKey; diff --git a/03_Modules/Configuration/src/module/module.ts b/03_Modules/Configuration/src/module/module.ts index 10781fc3e..c6104f2ab 100644 --- a/03_Modules/Configuration/src/module/module.ts +++ b/03_Modules/Configuration/src/module/module.ts @@ -383,16 +383,16 @@ export class ConfigurationModule extends AbstractModule { // Commenting out this line, using requestId == 0 until fixed (10/26/2023) // const requestId = Math.floor(Math.random() * ConfigurationModule.GET_BASE_REPORT_REQUEST_ID_MAX); const requestId = 0; - this._cache.set(requestId.toString(), 'ongoing', stationId, this.config.websocket.maxCachingSeconds); + this._cache.set(requestId.toString(), 'ongoing', stationId, this.config.maxCachingSeconds); const getBaseReportMessageConfirmation: IMessageConfirmation = await this.sendCall(stationId, tenantId, CallAction.GetBaseReport, { requestId: requestId, reportBase: ReportBaseEnumType.FullInventory } as GetBaseReportRequest); if (getBaseReportMessageConfirmation.success) { this._logger.debug("GetBaseReport successfully sent to charger: ", getBaseReportMessageConfirmation); // Wait for GetBaseReport to complete - let getBaseReportCacheValue = await this._cache.onChange(requestId.toString(), this.config.websocket.maxCachingSeconds, stationId); + let getBaseReportCacheValue = await this._cache.onChange(requestId.toString(), this.config.maxCachingSeconds, stationId); while (getBaseReportCacheValue == 'ongoing') { - getBaseReportCacheValue = await this._cache.onChange(requestId.toString(), this.config.websocket.maxCachingSeconds, stationId); + getBaseReportCacheValue = await this._cache.onChange(requestId.toString(), this.config.maxCachingSeconds, stationId); } if (getBaseReportCacheValue == 'complete') { @@ -423,7 +423,7 @@ export class ConfigurationModule extends AbstractModule { while (setVariableData.length > 0) { // Below pattern is preferred way of receiving CallResults in an async mannner. const correlationId = uuidv4(); - const cacheCallbackPromise: Promise = this._cache.onChange(correlationId, this.config.websocket.maxCachingSeconds, stationId); // x2 fudge factor for any network lag + const cacheCallbackPromise: Promise = this._cache.onChange(correlationId, this.config.maxCachingSeconds, stationId); // x2 fudge factor for any network lag this.sendCall(stationId, tenantId, CallAction.SetVariables, { setVariableData: setVariableData.slice(0, itemsPerMessageSetVariables) } as SetVariablesRequest, undefined, correlationId); setVariableData = setVariableData.slice(itemsPerMessageSetVariables); diff --git a/03_Modules/Reporting/src/module/module.ts b/03_Modules/Reporting/src/module/module.ts index b59c62c5a..00ca1a9be 100644 --- a/03_Modules/Reporting/src/module/module.ts +++ b/03_Modules/Reporting/src/module/module.ts @@ -173,7 +173,7 @@ export class ReportingModule extends AbstractModule { this._logger.info("Completed", success, message.payload.requestId); } else { // tbc (to be continued) is true // Continue to set get base report ongoing. Will extend the timeout. - const success = await this._cache.set(message.payload.requestId.toString(), ReportingModule.GET_BASE_REPORT_ONGOING_CACHE_VALUE, message.context.stationId, this.config.websocket.maxCachingSeconds); + const success = await this._cache.set(message.payload.requestId.toString(), ReportingModule.GET_BASE_REPORT_ONGOING_CACHE_VALUE, message.context.stationId, this.config.maxCachingSeconds); this._logger.info("Ongoing", success, message.payload.requestId); } diff --git a/Server/src/config/envs/docker.ts b/Server/src/config/envs/docker.ts index 052da1d4a..3183e451d 100644 --- a/Server/src/config/envs/docker.ts +++ b/Server/src/config/envs/docker.ts @@ -7,6 +7,10 @@ import { RegistrationStatusEnumType, defineConfig } from "@citrineos/base"; export function createDockerConfig() { return defineConfig({ env: "development", + centralSystem: { + host: "0.0.0.0", + port: 8080 + }, modules: { certificates: { endpointPrefix: "/certificates" @@ -63,28 +67,27 @@ export function createDockerConfig() { logoPath: "/usr/server/src/assets/logo.png", exposeData: true, exposeMessage: true + }, + networkConnection: { + websocketServers: [{ + id: "0", + securityProfile: 0, + pingInterval: 60, + host: "0.0.0.0", + port: 8081, + protocol: "ocpp2.0.1" + }, { + id: "1", + securityProfile: 1, + pingInterval: 60, + host: "0.0.0.0", + port: 8082, + protocol: "ocpp2.0.1" + }] } }, - server: { - logLevel: 2, // debug - host: "0.0.0.0", - port: 8080 - }, - websocket: { - pingInterval: 60, - maxCallLengthSeconds: 5, - maxCachingSeconds: 10 - }, - websocketServer: [{ - securityProfile: 0, - host: "0.0.0.0", - port: 8081, - protocol: "ocpp2.0.1" - },{ - securityProfile: 1, - host: "0.0.0.0", - port: 8082, - protocol: "ocpp2.0.1" - }] + logLevel: 2, // debug + maxCallLengthSeconds: 5, + maxCachingSeconds: 10 }); } \ No newline at end of file diff --git a/Server/src/config/envs/local.ts b/Server/src/config/envs/local.ts index c7a008ec1..921196de0 100644 --- a/Server/src/config/envs/local.ts +++ b/Server/src/config/envs/local.ts @@ -7,6 +7,10 @@ import { RegistrationStatusEnumType, defineConfig } from "@citrineos/base"; export function createLocalConfig() { return defineConfig({ env: "development", + centralSystem: { + host: "0.0.0.0", + port: 8080 + }, modules: { certificates: { endpointPrefix: "/certificates" @@ -60,31 +64,30 @@ export function createLocalConfig() { }, swagger: { path: "/docs", - logoPath: "./src/assets/logo.png", + logoPath: "/usr/server/src/assets/logo.png", exposeData: true, exposeMessage: true + }, + networkConnection: { + websocketServers: [{ + id: "0", + securityProfile: 0, + pingInterval: 60, + host: "0.0.0.0", + port: 8081, + protocol: "ocpp2.0.1" + }, { + id: "1", + securityProfile: 1, + pingInterval: 60, + host: "0.0.0.0", + port: 8082, + protocol: "ocpp2.0.1" + }] } }, - server: { - logLevel: 2, // debug - host: "127.0.0.1", - port: 8080 - }, - websocket: { - pingInterval: 60, - maxCallLengthSeconds: 5, - maxCachingSeconds: 10 - }, - websocketServer: [{ - securityProfile: 0, - host: "0.0.0.0", - port: 8081, - protocol: "ocpp2.0.1" - }, { - securityProfile: 1, - host: "0.0.0.0", - port: 8082, - protocol: "ocpp2.0.1" - }] + logLevel: 2, // debug + maxCallLengthSeconds: 5, + maxCachingSeconds: 10 }); } \ No newline at end of file diff --git a/Server/src/index.ts b/Server/src/index.ts index 7ed75931a..c426fc28a 100644 --- a/Server/src/index.ts +++ b/Server/src/index.ts @@ -64,7 +64,7 @@ class CitrineOSServer { // Initialize parent logger this._logger = new Logger({ name: "CitrineOS Logger", - minLevel: systemConfig.server.logLevel, + minLevel: systemConfig.logLevel, hideLogPositionForProduction: systemConfig.env === "production" }); @@ -151,8 +151,8 @@ class CitrineOSServer { run(): Promise { try { return this._server.listen({ - port: this._config.server.port, - host: this._config.server.host + port: this._config.centralSystem.port, + host: this._config.centralSystem.host }).then(address => { this._logger.info(`Server listening at ${address}`); }).catch(error => { diff --git a/Swarm/src/config/envs/docker.ts b/Swarm/src/config/envs/docker.ts index 6e9957a3d..cb93f71d9 100644 --- a/Swarm/src/config/envs/docker.ts +++ b/Swarm/src/config/envs/docker.ts @@ -7,6 +7,10 @@ import { RegistrationStatusEnumType, defineConfig } from "@citrineos/base"; export function createDockerConfig() { return defineConfig({ env: "development", + centralSystem: { + host: "0.0.0.0", + port: 8080 + }, modules: { certificates: { endpointPrefix: "certificates", @@ -80,28 +84,27 @@ export function createDockerConfig() { logoPath: "/usr/server/src/assets/logo.png", exposeData: true, exposeMessage: true + }, + networkConnection: { + websocketServers: [{ + id: "0", + securityProfile: 0, + pingInterval: 60, + host: "0.0.0.0", + port: 8081, + protocol: "ocpp2.0.1" + }, { + id: "1", + securityProfile: 1, + pingInterval: 60, + host: "0.0.0.0", + port: 8082, + protocol: "ocpp2.0.1" + }] } }, - server: { - logLevel: 2, // debug - host: "0.0.0.0", - port: 8080 - }, - websocket: { - pingInterval: 60, - maxCallLengthSeconds: 5, - maxCachingSeconds: 10 - }, - websocketServer: [{ - securityProfile: 0, - host: "0.0.0.0", - port: 8081, - protocol: "ocpp2.0.1" - },{ - securityProfile: 1, - host: "0.0.0.0", - port: 8082, - protocol: "ocpp2.0.1" - }] + logLevel: 2, // debug + maxCallLengthSeconds: 5, + maxCachingSeconds: 10 }); } \ No newline at end of file diff --git a/Swarm/src/config/envs/local.ts b/Swarm/src/config/envs/local.ts index 7bd390e0b..909065fbd 100644 --- a/Swarm/src/config/envs/local.ts +++ b/Swarm/src/config/envs/local.ts @@ -7,6 +7,10 @@ import { RegistrationStatusEnumType, defineConfig } from "@citrineos/base"; export function createLocalConfig() { return defineConfig({ env: "development", + centralSystem: { + host: "0.0.0.0", + port: 8080 + }, modules: { certificates: { endpointPrefix: "/certificates", @@ -80,28 +84,27 @@ export function createLocalConfig() { logoPath: "/usr/server/src/assets/logo.png", exposeData: true, exposeMessage: true + }, + networkConnection: { + websocketServers: [{ + id: "0", + securityProfile: 0, + pingInterval: 60, + host: "0.0.0.0", + port: 8081, + protocol: "ocpp2.0.1" + }, { + id: "1", + securityProfile: 1, + pingInterval: 60, + host: "0.0.0.0", + port: 8082, + protocol: "ocpp2.0.1" + }] } }, - server: { - logLevel: 2, // debug - host: "0.0.0.0", - port: 8080 - }, - websocket: { - pingInterval: 60, - maxCallLengthSeconds: 5, - maxCachingSeconds: 10 - }, - websocketServer: [{ - securityProfile: 0, - host: "0.0.0.0", - port: 8081, - protocol: "ocpp2.0.1" - },{ - securityProfile: 1, - host: "0.0.0.0", - port: 8082, - protocol: "ocpp2.0.1" - }] + logLevel: 2, // debug + maxCallLengthSeconds: 5, + maxCachingSeconds: 10 }); } \ No newline at end of file diff --git a/Swarm/src/index.ts b/Swarm/src/index.ts index b8e8b6b1f..2daa1a1f1 100644 --- a/Swarm/src/index.ts +++ b/Swarm/src/index.ts @@ -62,7 +62,7 @@ class CitrineOSServer { // Initialize parent logger this._logger = new Logger({ name: "CitrineOS Logger", - minLevel: systemConfig.server.logLevel, + minLevel: systemConfig.logLevel, hideLogPositionForProduction: systemConfig.env === "production" }); @@ -114,8 +114,8 @@ class CitrineOSServer { run(): Promise { try { return this._server.listen({ - port: this._config.server.port, - host: this._config.server.host + port: this._config.centralSystem.port, + host: this._config.centralSystem.host }).then(address => { this._logger.info(`Server listening at ${address}`); }).catch(error => { @@ -174,7 +174,7 @@ class ModuleService { // Initialize parent logger this._logger = new Logger({ name: "CitrineOS Logger", - minLevel: systemConfig.server.logLevel, + minLevel: systemConfig.logLevel, hideLogPositionForProduction: systemConfig.env === "production" });