WebSocket check registration token if Charging Station is new + Enhanced logs
MODIFY
src/client/websocket/WSClient.ts
src/client/websocket/WSClient.ts
@@ -5,7 +5,6 @@ import Logging from '../../utils/Logging';
import { ServerAction } from '../../types/Server';
import Utils from '../../utils/Utils';
import WebSocket from 'ws';
-import axiosRetry from 'axios-retry';
const MODULE_NAME = 'WSClient';
@@ -20,9 +19,6 @@ export default class WSClient {
private options: WSClientOptions;
private callbacks: { [key: string]: (...args: any[]) => void };
private dbLogging: boolean;
- private autoReconnectRetryCount: number;
- private autoReconnectMaxRetries: number;
- private autoReconnectTimeout: number;
private logTenantID: string;
private ws: WebSocket;
@@ -38,9 +34,6 @@ export default class WSClient {
'onmaximum': () => { }
};
this.dbLogging = dbLogging;
- this.autoReconnectRetryCount = 0;
- this.autoReconnectMaxRetries = options.autoReconnectMaxRetries; // -1 for unlimited retries
- this.autoReconnectTimeout = options.autoReconnectTimeout * 1000; // Ms, 0 to disable
this.logTenantID = options.logTenantID ? options.logTenantID : Constants.DEFAULT_TENANT;
this.open();
}
@@ -121,12 +114,6 @@ export default class WSClient {
this.ws.pong(data, mask, callback);
}
- /**
- * Forcibly close the connection.
- *
- * @returns {void}
- * @public
- */
public terminate(): void {
return this.ws.terminate();
}
@@ -135,46 +122,7 @@ export default class WSClient {
return this.ws?.readyState === WebSocket.OPEN;
}
- private reconnect(error: Error): void {
- if (this.autoReconnectTimeout !== Constants.WS_RECONNECT_DISABLED &&
- (this.autoReconnectRetryCount < this.autoReconnectMaxRetries || this.autoReconnectMaxRetries === Constants.WS_RECONNECT_UNLIMITED)) {
- this.autoReconnectRetryCount++;
- Utils.sleep(axiosRetry.exponentialDelay(this.autoReconnectRetryCount)).catch(() => { });
- setTimeout(() => {
- // Informational message
- if (this.dbLogging) {
- Logging.logInfo({
- tenantID: this.logTenantID,
- module: MODULE_NAME, method: 'reconnect',
- action: ServerAction.WS_CLIENT_INFO,
- message: `Reconnection try #${this.autoReconnectRetryCount} to '${this.url}' with timeout ${this.autoReconnectTimeout} ms`
- });
- } else {
- // eslint-disable-next-line no-console
- (Utils.isDevelopmentEnv() || Utils.isTestEnv()) && console.log(`WSClient reconnection try #${this.autoReconnectRetryCount} to '${this.url}' with timeout ${this.autoReconnectTimeout} ms`);
- }
- this.onreconnect(error);
- this.open();
- }, this.autoReconnectTimeout);
- } else if (this.autoReconnectTimeout !== Constants.WS_RECONNECT_DISABLED || this.autoReconnectMaxRetries !== Constants.WS_RECONNECT_UNLIMITED) {
- // Informational message
- if (this.dbLogging) {
- Logging.logInfo({
- tenantID: this.logTenantID,
- module: MODULE_NAME, method: 'reconnect',
- action: ServerAction.WS_CLIENT_INFO,
- message: `Reconnection maximum retries reached (${this.autoReconnectRetryCount}) or disabled (${this.autoReconnectTimeout}) to '${this.url}'`
- });
- } else {
- // eslint-disable-next-line no-console
- (Utils.isDevelopmentEnv() || Utils.isTestEnv()) && console.log(`WSClient reconnection maximum retries reached (${this.autoReconnectRetryCount}) or disabled (${this.autoReconnectTimeout}) to '${this.url}'`);
- }
- this.onmaximum(error);
- }
- }
-
private onOpen() {
- this.autoReconnectRetryCount = 0;
}
private reinstantiateCbs() {
@@ -190,7 +138,7 @@ export default class WSClient {
case 'ECONNREFUSED':
// Error message
if (this.dbLogging) {
- Logging.logError({
+ void Logging.logError({
tenantID: this.logTenantID,
module: MODULE_NAME, method: 'onError',
action: ServerAction.WS_CLIENT_ERROR,
@@ -201,12 +149,11 @@ export default class WSClient {
// eslint-disable-next-line no-console
(Utils.isDevelopmentEnv() || Utils.isTestEnv()) && console.error(`WSClient connection refused to '${this.url}':`, error);
}
- // pragma this.reconnect(error);
break;
default:
// Error message
if (this.dbLogging) {
- Logging.logError({
+ void Logging.logError({
tenantID: this.logTenantID,
module: MODULE_NAME, method: 'onError',
action: ServerAction.WS_CLIENT_ERROR,
@@ -227,7 +174,7 @@ export default class WSClient {
case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
// Informational message
if (this.dbLogging) {
- Logging.logInfo({
+ void Logging.logInfo({
tenantID: this.logTenantID,
module: MODULE_NAME, method: 'onClose',
action: ServerAction.WS_CLIENT_INFO,
@@ -237,12 +184,12 @@ export default class WSClient {
// eslint-disable-next-line no-console
(Utils.isDevelopmentEnv() || Utils.isTestEnv()) && console.log(`WSClient connection closing to '${this.url}', Reason: '${reason ? reason : 'No reason given'}', Message: '${Utils.getWebSocketCloseEventStatusString(code)}', Code: '${code}'`);
}
- this.autoReconnectRetryCount = 0;
break;
- default: // Abnormal close
+ // Abnormal close
+ default:
// Error message
if (this.dbLogging) {
- Logging.logError({
+ void Logging.logError({
tenantID: this.logTenantID,
module: MODULE_NAME, method: 'onClose',
action: ServerAction.WS_CLIENT_ERROR,
@@ -252,16 +199,11 @@ export default class WSClient {
// eslint-disable-next-line no-console
(Utils.isDevelopmentEnv() || Utils.isTestEnv()) && console.error(`WSClient Connection closing error to '${this.url}', Reason: '${reason ? reason : 'No reason given'}', Message: '${Utils.getWebSocketCloseEventStatusString(code)}', Code: '${code}'`);
}
- this.reconnect(new Error(`Connection has been closed abnormally, Reason: '${reason ? reason : 'No reason given'}', Message: '${Utils.getWebSocketCloseEventStatusString(code)}', Code: '${code}'`));
break;
}
}
}
-/**
- * Add the `onopen`, `onerror`, `onclose`, `onmessage`, `onreconnect`
- * and `onmaximum` attributes.
- */
for (const method of ['onopen', 'onerror', 'onclose', 'onmessage']) {
Object.defineProperty(WSClient.prototype, method, {
configurable: true,
@@ -276,6 +218,7 @@ for (const method of ['onopen', 'onerror', 'onclose', 'onmessage']) {
}
});
}
+
for (const method of ['onreconnect', 'onmaximum']) {
Object.defineProperty(WSClient.prototype, method, {
configurable: true,
@@ -289,9 +232,6 @@ for (const method of ['onreconnect', 'onmaximum']) {
});
}
-/**
- * Add some ws properties
- */
for (const property of ['binaryType', 'bufferedAmount', 'extensions', 'protocol', 'readyState']) {
Object.defineProperty(WSClient.prototype, property, {
enumerable: true,

MODIFY
src/server/ocpp/json/JsonCentralSystemServer.ts
src/server/ocpp/json/JsonCentralSystemServer.ts
@@ -10,6 +10,7 @@ import JsonWSConnection from './JsonWSConnection';
import Logging from '../../../utils/Logging';
import { OCPPVersion } from '../../../types/ocpp/OCPPServer';
import Utils from '../../../utils/Utils';
+import WSConnection from './WSConnection';
import WSServer from './WSServer';
import WebSocket from 'ws';
import { WebSocketCloseEventStatusCode } from '../../../types/WebSocket';
@@ -136,28 +137,30 @@ export default class JsonCentralSystemServer extends CentralSystemServer {
this.wsServer = new WSServer(this.centralSystemConfig, this.serverName, verifyClient, handleProtocols);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.wsServer.on('connection', async (ws: WebSocket, req: http.IncomingMessage) => {
+ let wsConnection: WSConnection;
try {
// Check Rest calls
if (req.url.startsWith('/REST')) {
// Create a Rest WebSocket connection object
- const wsConnection = new JsonRestWSConnection(ws, req, this);
+ wsConnection = new JsonRestWSConnection(ws, req, this);
// Init
await wsConnection.initialize();
// Add
- this.addRestConnection(wsConnection);
+ this.addRestConnection(wsConnection as JsonRestWSConnection);
} else if (req.url.startsWith(`/${Utils.getOCPPServerVersionURLPath(OCPPVersion.VERSION_16)}`)) {
// Create a Json WebSocket connection object
- const wsConnection = new JsonWSConnection(ws, req, this);
+ wsConnection = new JsonWSConnection(ws, req, this);
// Init
await wsConnection.initialize();
// Add
- this.addJsonConnection(wsConnection);
+ this.addJsonConnection(wsConnection as JsonWSConnection);
} else {
throw Error('Wrong WebSocket client connection URI path');
}
} catch (error) {
- await Logging.logException(error, ServerAction.WS_CONNECTION, '', MODULE_NAME, 'connection', Constants.DEFAULT_TENANT);
ws.close(WebSocketCloseEventStatusCode.CLOSE_UNSUPPORTED, error.message);
+ await Logging.logException(error, ServerAction.WS_CONNECTION, '', MODULE_NAME, 'connection',
+ wsConnection?.getTenantID() ? wsConnection.getTenantID() : Constants.DEFAULT_TENANT);
}
});
// Keep alive WebSocket connection

MODIFY
src/server/ocpp/json/JsonWSConnection.ts
src/server/ocpp/json/JsonWSConnection.ts
@@ -14,6 +14,7 @@ import JsonChargingStationService from './services/JsonChargingStationService';
import Logging from '../../../utils/Logging';
import OCPPError from '../../../exception/OcppError';
import { OCPPHeader } from '../../../types/ocpp/OCPPHeader';
+import OCPPUtils from '../utils/OCPPUtils';
import Utils from '../../../utils/Utils';
import WSConnection from './WSConnection';
import http from 'http';
@@ -61,18 +62,6 @@ export default class JsonWSConnection extends WSConnection {
if (!this.initialized) {
// Call super class
await super.initialize();
- // Update the Charging Station
- const chargingStation = await ChargingStationStorage.getChargingStation(this.getTenantID(), this.getChargingStationID(), {}, ['id']);
- if (chargingStation) {
- // Update Last Seen
- await ChargingStationStorage.saveChargingStationLastSeen(this.getTenantID(),
- chargingStation.id, { lastSeen: new Date() });
- // Update CF Instance
- if (Configuration.isCloudFoundry()) {
- await ChargingStationStorage.saveChargingStationCFApplicationIDAndInstanceIndex(
- this.getTenantID(), chargingStation.id, Configuration.getCFApplicationIDAndInstanceIndex());
- }
- }
// Initialize the default Headers
this.headers = {
chargeBoxIdentity: this.getChargingStationID(),
@@ -85,6 +74,23 @@ export default class JsonWSConnection extends WSConnection {
Address: this.getClientIP()
}
};
+ // Update the Charging Station
+ const chargingStation = await ChargingStationStorage.getChargingStation(this.getTenantID(), this.getChargingStationID(), {}, ['id']);
+ if (chargingStation) {
+ // Update Last Seen
+ await ChargingStationStorage.saveChargingStationLastSeen(this.getTenantID(),
+ chargingStation.id, { lastSeen: new Date() });
+ // Update CF Instance
+ if (Configuration.isCloudFoundry()) {
+ await ChargingStationStorage.saveChargingStationCFApplicationIDAndInstanceIndex(
+ this.getTenantID(), chargingStation.id, Configuration.getCFApplicationIDAndInstanceIndex());
+ }
+ // Must have a valid Token
+ } else {
+ // Check connection Token
+ await OCPPUtils.checkChargingStationConnectionToken(
+ ServerAction.BOOT_NOTIFICATION, this.getTenant(), this.getChargingStationID(), this.getToken(), { headers: this.headers });
+ }
this.initialized = true;
await Logging.logInfo({
tenantID: this.getTenantID(),

MODIFY
src/server/ocpp/json/WSConnection.ts
src/server/ocpp/json/WSConnection.ts
@@ -2,9 +2,7 @@ import { OCPPErrorType, OCPPIncomingRequest, OCPPMessageType, OCPPRequest } from
import WebSocket, { CLOSED, CLOSING, CONNECTING, CloseEvent, ErrorEvent, MessageEvent, OPEN } from 'ws';
import BackendError from '../../../exception/BackendError';
-import ChargingStationStorage from '../../../storage/mongodb/ChargingStationStorage';
import { Command } from '../../../types/ChargingStation';
-import Configuration from '../../../utils/Configuration';
import Constants from '../../../utils/Constants';
import DatabaseUtils from '../../../storage/mongodb/DatabaseUtils';
import JsonCentralSystemServer from './JsonCentralSystemServer';
@@ -12,6 +10,7 @@ import Logging from '../../../utils/Logging';
import OCPPError from '../../../exception/OcppError';
import { OCPPVersion } from '../../../types/ocpp/OCPPServer';
import { ServerAction } from '../../../types/Server';
+import Tenant from '../../../types/Tenant';
import TenantStorage from '../../../storage/mongodb/TenantStorage';
import Utils from '../../../utils/Utils';
import http from 'http';
@@ -21,12 +20,13 @@ const MODULE_NAME = 'WSConnection';
export default abstract class WSConnection {
protected initialized: boolean;
protected wsServer: JsonCentralSystemServer;
- private readonly chargingStationID: string;
- private readonly tenantID: string;
- private readonly token: string;
- private readonly url: string;
- private readonly clientIP: string | string[];
- private readonly wsConnection: WebSocket;
+ private chargingStationID: string;
+ private tenantID: string;
+ private tenant: Tenant;
+ private token: string;
+ private url: string;
+ private clientIP: string | string[];
+ private wsConnection: WebSocket;
private requests: { [id: string]: OCPPRequest };
private validTenant: boolean;
@@ -76,7 +76,7 @@ export default abstract class WSConnection {
});
}
let logMsg = `Unknown type WS connection attempts with URL: '${req.url}'`;
- let action: ServerAction = ServerAction.WS_CONNECTION_OPENED;
+ let action = ServerAction.WS_CONNECTION_OPENED;
if (req.url.startsWith('/REST')) {
logMsg = `REST service connection attempts to Charging Station with URL: '${req.url}'`;
action = ServerAction.WS_REST_CONNECTION_OPENED;
@@ -119,7 +119,7 @@ export default abstract class WSConnection {
public async initialize(): Promise<void> {
try {
// Check Tenant?
- await DatabaseUtils.checkTenant(this.tenantID);
+ this.tenant = await DatabaseUtils.checkTenant(this.tenantID);
this.validTenant = true;
} catch (error) {
// Custom Error
@@ -335,6 +335,10 @@ export default abstract class WSConnection {
return Constants.DEFAULT_TENANT;
}
+ public getTenant(): Tenant {
+ return this.tenant;
+ }
+
public getToken(): string {
return this.token;
}

MODIFY
src/server/ocpp/services/OCPPService.ts
src/server/ocpp/services/OCPPService.ts
@@ -1732,35 +1732,9 @@ export default class OCPPService {
}
private async checkAndCreateChargingStation(tenant: Tenant, bootNotification: OCPPBootNotificationRequestExtended, headers: OCPPHeader): Promise<ChargingStation> {
- // Check Token
- if (!headers.token) {
- throw new BackendError({
- source: headers.chargeBoxIdentity,
- action: ServerAction.BOOT_NOTIFICATION,
- module: MODULE_NAME, method: 'checkAndRegisterNewChargingStation',
- message: `Registration rejected: Token is required for: '${headers.chargeBoxIdentity}' on ip '${headers.currentIPAddress.toString()}'`,
- detailedMessages: { headers, bootNotification }
- });
- }
- const token = await RegistrationTokenStorage.getRegistrationToken(tenant.id, headers.token);
- if (!token || !token.expirationDate || moment().isAfter(token.expirationDate)) {
- throw new BackendError({
- source: headers.chargeBoxIdentity,
- action: ServerAction.BOOT_NOTIFICATION,
- module: MODULE_NAME, method: 'checkAndRegisterNewChargingStation',
- message: `Registration rejected: Token '${headers.token}' is invalid or expired for: '${headers.chargeBoxIdentity}' on ip '${headers.currentIPAddress.toString()}'`,
- detailedMessages: { headers, bootNotification }
- });
- }
- if (token.revocationDate || moment().isAfter(token.revocationDate)) {
- throw new BackendError({
- source: headers.chargeBoxIdentity,
- action: ServerAction.BOOT_NOTIFICATION,
- module: MODULE_NAME, method: 'checkAndRegisterNewChargingStation',
- message: `Registration rejected: Token '${headers.token}' is revoked for: '${headers.chargeBoxIdentity}' on ip '${headers.currentIPAddress.toString()}'`,
- detailedMessages: { headers, bootNotification }
- });
- }
+ // Check connection Token
+ const token = await OCPPUtils.checkChargingStationConnectionToken(
+ ServerAction.BOOT_NOTIFICATION, tenant, headers.chargeBoxIdentity, headers.token, { headers, bootNotification });
// New Charging Station: Create
const newChargingStation = {} as ChargingStation;
for (const key in bootNotification) {

MODIFY
src/server/ocpp/utils/OCPPUtils.ts
src/server/ocpp/utils/OCPPUtils.ts
@@ -33,6 +33,8 @@ import OICPUtils from '../../oicp/OICPUtils';
import { PricedConsumption } from '../../../types/Pricing';
import PricingFactory from '../../../integration/pricing/PricingFactory';
import { PricingSettingsType } from '../../../types/Setting';
+import RegistrationToken from '../../../types/RegistrationToken';
+import RegistrationTokenStorage from '../../../storage/mongodb/RegistrationTokenStorage';
import { ServerAction } from '../../../types/Server';
import SiteArea from '../../../types/SiteArea';
import SiteAreaStorage from '../../../storage/mongodb/SiteAreaStorage';
@@ -50,6 +52,41 @@ import url from 'url';
const MODULE_NAME = 'OCPPUtils';
export default class OCPPUtils {
+ public static async checkChargingStationConnectionToken(action: ServerAction, tenant: Tenant, chargingStationID: string,
+ tokenID: string, detailedMessages?: any): Promise<RegistrationToken> {
+ // Check Token
+ if (!tokenID) {
+ throw new BackendError({
+ source: chargingStationID,
+ action: ServerAction.BOOT_NOTIFICATION,
+ module: MODULE_NAME, method: 'checkChargingStationConnectionToken',
+ message: 'Charging Station Token is required, connection refused',
+ detailedMessages
+ });
+ }
+ // Get the Token
+ const token = await RegistrationTokenStorage.getRegistrationToken(tenant.id, tokenID);
+ if (!token) {
+ throw new BackendError({
+ source: chargingStationID,
+ action,
+ module: MODULE_NAME, method: 'checkChargingStationConnectionToken',
+ message: `Charging Station Token ID '${tokenID}' has not been found, connection refused`,
+ detailedMessages
+ });
+ }
+ if (!token.expirationDate || moment().isAfter(token.expirationDate)) {
+ throw new BackendError({
+ source: chargingStationID,
+ action,
+ module: MODULE_NAME, method: 'checkChargingStationConnectionToken',
+ message: `Charging Station Token ID '${tokenID}' is expired, connection refused`,
+ detailedMessages
+ });
+ }
+ return token;
+ }
+
public static async processTransactionRoaming(tenant: Tenant, transaction: Transaction,
chargingStation: ChargingStation, transactionAction: TransactionAction): Promise<void> {
try {

MODIFY
src/storage/mongodb/DatabaseUtils.ts
src/storage/mongodb/DatabaseUtils.ts
@@ -5,10 +5,10 @@ import DbLookup from '../../types/database/DbLookup';
import { OCPPFirmwareStatus } from '../../types/ocpp/OCPPServer';
import { ObjectId } from 'mongodb';
import Tenant from '../../types/Tenant';
+import TenantStorage from './TenantStorage';
import User from '../../types/User';
import UserToken from '../../types/UserToken';
import Utils from '../../utils/Utils';
-import global from '../../types/GlobalType';
const FIXED_COLLECTIONS: string[] = ['tenants', 'migrations'];
@@ -393,7 +393,7 @@ export default class DatabaseUtils {
return userID;
}
- public static async checkTenant(tenantID: string): Promise<void> {
+ public static async checkTenant(tenantID: string): Promise<Tenant> {
if (!tenantID) {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
@@ -413,9 +413,7 @@ export default class DatabaseUtils {
});
}
// Get the Tenant
- const tenant = await global.database.getCollection<any>(Constants.DEFAULT_TENANT, 'tenants').findOne({
- '_id': DatabaseUtils.convertToObjectID(tenantID)
- });
+ const tenant = await TenantStorage.getTenant(tenantID);
if (!tenant) {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
@@ -424,6 +422,7 @@ export default class DatabaseUtils {
message: `Invalid Tenant ID '${tenantID}'`
});
}
+ return tenant;
}
}

MODIFY
test/api/OCPPCommonTests.ts
test/api/OCPPCommonTests.ts
@@ -343,9 +343,12 @@ export default class OCPPCommonTests {
}
public async testChargingStationRegistrationWithInvalidToken() {
- const response = await this.chargingStationContext.sendBootNotification();
- expect(response).not.to.be.null;
- expect(response.status).eq(OCPPStatus.REJECTED);
+ try {
+ await this.chargingStationContext.sendBootNotification();
+ fail('BootNotification should fail');
+ } catch (error) {
+ expect(error).to.be.not.null;
+ }
}
public async testChargingStationRegistrationWithInvalidIdentifier() {

MODIFY
test/api/context/ContextProvider.ts
test/api/context/ContextProvider.ts
import chai, { expect } from 'chai';
-
-import CentralServerService from '../client/CentralServerService';
+import chaiSubset from 'chai-subset';
+import { ObjectId } from 'mongodb';
+import { OCPPVersion } from '../../../src/types/ocpp/OCPPServer';
+import TenantComponents from '../../../src/types/TenantComponents';
import Constants from '../../../src/utils/Constants';
-import ContextDefinition from './ContextDefinition';
+import config from '../../config';
import Factory from '../../factories/Factory';
-import { OCPPVersion } from '../../../src/types/ocpp/OCPPServer';
+import CentralServerService from '../client/CentralServerService';
+import ContextDefinition from './ContextDefinition';
import SiteContext from './SiteContext';
-import TenantComponents from '../../../src/types/TenantComponents';
import TenantContext from './TenantContext';
-import chaiSubset from 'chai-subset';
-import config from '../../config';
-import faker from 'faker';
+
chai.use(chaiSubset);
@@ -135,7 +135,7 @@ export default class ContextProvider {
await newTenantContext.addChargingStation(chargingStation);
}
- const registrationToken = faker.random.alphaNumeric(10);
+ const registrationToken = new ObjectId().toHexString();
const unregisteredChargingStation15 = await Factory.chargingStation.build({ id: ContextDefinition.CHARGING_STATION_CONTEXTS.UNREGISTERED_OCPP15, ocppVersion: OCPPVersion.VERSION_15 });
await newTenantContext.addChargingStation(unregisteredChargingStation15, registrationToken);
const unregisteredChargingStation16 = await Factory.chargingStation.build({ id: ContextDefinition.CHARGING_STATION_CONTEXTS.UNREGISTERED_OCPP16, ocppVersion: OCPPVersion.VERSION_16 });

MODIFY
test/config.js
test/config.js
@@ -187,7 +187,7 @@ const config = convict({
autoReconnectMaxRetries: {
doc: 'WebSocket client re-connection max retries.',
format: 'int',
- default: 10,
+ default: 0,
},
autoReconnectTimeout: {
doc: 'WebSocket client re-connection timeout.',

WebSocket check registration token if Charging Station is new + Enhanced logs