Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 82 additions & 99 deletions 00_Base/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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(),
Expand All @@ -50,16 +69,16 @@ 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({
host: z.string().default('localhost').optional(),
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(),
}),
Expand Down Expand Up @@ -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<typeof systemConfigInputSchema>;

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(),
Expand All @@ -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(),
Expand Down Expand Up @@ -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<string>();
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<typeof systemConfigSchema>;

function checkForHostPortDuplicates(websocketServers: { port: number; host: string;}[]): unknown {
const uniqueCombinations = new Set<string>();
for (const item of websocketServers) {
const combo = `${item.host}:${item.port}`;

if (uniqueCombinations.has(combo)) {
return false; // Duplicate found
}

uniqueCombinations.add(combo);
}

return true;
}
export type WebsocketServerConfig = z.infer<typeof websocketServerSchema>;
export type SystemConfig = z.infer<typeof systemConfigSchema>;
2 changes: 1 addition & 1 deletion 00_Base/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions 00_Base/src/interfaces/modules/AbstractModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -105,7 +105,7 @@ export abstract class AbstractModule implements IModule {
protected _initLogger(baseLogger?: Logger<ILogObj>): Logger<ILogObj> {
return baseLogger ? baseLogger.getSubLogger({ name: this.constructor.name }) : new Logger<ILogObj>({
name: this.constructor.name,
minLevel: this._config.server.logLevel,
minLevel: this._config.logLevel,
hideLogPositionForProduction: this._config.env === "production"
});
}
Expand Down Expand Up @@ -133,7 +133,7 @@ export abstract class AbstractModule implements IModule {
async handle(message: IMessage<OcppRequest | OcppResponse>, props?: HandlerProperties): Promise<void> {
if (message.state === MessageState.Response) {
this.handleMessageApiCallback(message as IMessage<OcppResponse>);
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<IHandlerDefinition>).filter((h) => h.action === message.action).pop();
Expand Down Expand Up @@ -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<ClientConnection>(identifier, CacheNamespace.Connections, () => ClientConnection).then((connection) => {
Expand Down
Loading