@evpower/ocpp-ts
Version:
OCPP 1.6: Open Charge Point Protocol
150 lines (128 loc) • 4.76 kB
text/typescript
import EventEmitter from 'events';
import WebSocket, { WebSocketServer, CLOSING } from 'ws';
import { SecureContextOptions } from 'tls';
import { createServer as createHttpsServer } from 'https';
import { createServer as createHttpServer, IncomingMessage, Server as httpServer, STATUS_CODES } from 'http';
import stream from 'node:stream';
import { OCPP_PROTOCOL_1_6 } from './schemas';
import { Client } from './Client';
import { OcppClientConnection } from '../OcppClientConnection';
import { Protocol } from './Protocol';
import StatusCode from "status-code-enum";
const DEFAULT_PING_INTERVAL = 30; // seconds
export class Server extends EventEmitter {
private server: httpServer | undefined;
private clients: Array<Client> = [];
private pingInterval: number = DEFAULT_PING_INTERVAL; // seconds
private protocolTimeout: number = 30000; // milliseconds
public setPingInterval(pingInterval: number) {
this.pingInterval = pingInterval;
}
public setProtocolTimeout(timeout: number) {
this.protocolTimeout = timeout;
}
protected listen(port = 9220, options?: SecureContextOptions) {
if (options) {
this.server = createHttpsServer(options || {});
} else {
this.server = createHttpServer();
}
const wss = new WebSocketServer({
noServer: true,
handleProtocols: (protocols: Set<string>) => {
if (protocols.has(OCPP_PROTOCOL_1_6)) {
return OCPP_PROTOCOL_1_6;
}
return false;
},
});
wss.on('connection', (ws, req) => this.onNewConnection(ws, req));
this.server.on('upgrade', (req: IncomingMessage, socket: stream.Duplex, head: Buffer) => {
const cpId = Server.getCpIdFromUrl(req.url);
if (!cpId) {
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
socket.destroy();
} else if (this.listenerCount('authorization')) {
this.emit('authorization', cpId, req, (status?: StatusCode) => {
if (status && status !== StatusCode.SuccessOK) {
socket.write(`HTTP/1.1 ${status} ${STATUS_CODES[status]}\r\n\r\n`);
socket.destroy();
} else {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
}
});
} else {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
}
});
this.server.listen(port);
}
private onNewConnection(socket: WebSocket, req: IncomingMessage) {
const cpId = Server.getCpIdFromUrl(req.url);
if (!socket.protocol || !cpId) {
// From Spec: If the Central System does not agree to using one of the subprotocols offered
// by the client, it MUST complete the WebSocket handshake with a response without a
// Sec-WebSocket-Protocol header and then immediately close the WebSocket connection.
// console.info('Closed connection due to unsupported protocol');
socket.close();
return;
}
const client = new OcppClientConnection(cpId);
client.setHeaders(req.headers);
client.setConnection(new Protocol(client, socket, this.protocolTimeout));
let isAlive = true;
socket.on('pong', () => {
// console.error('received pong from client', cpId);
isAlive = true;
});
// console.error(`ping interval set to ${this.pingInterval} seconds for ${cpId}`);
const pingTimerInterval = setInterval(() => {
if (isAlive === false) {
// console.error(`did not get pong, terminating connection in under ${this.pingInterval} seconds`, cpId);
socket.terminate();
}
else if (socket.readyState < CLOSING) {
isAlive = false;
socket.ping(cpId, false, (err) => {
if (err) {
// console.info('error on ping', err.message);
socket.terminate();
}
});
}
}, this.pingInterval * 1000);
socket.on('error', (err) => {
client.emit('error', err);
});
socket.on('close', (code: number, reason: Buffer) => {
clearInterval(pingTimerInterval);
const index = this.clients.indexOf(client);
this.clients.splice(index, 1);
client.emit('close', code, reason);
});
this.clients.push(client);
this.emit('connection', client);
}
protected close() {
this.server?.close();
this.clients.forEach((client) => client.close());
}
static getCpIdFromUrl(url: string | undefined): string | undefined {
try {
if (url) {
const encodedCpId = url.split('/')
.pop();
if (encodedCpId) {
return decodeURI(encodedCpId.split('?')[0]);
}
}
} catch (e) {
console.error(e);
}
return undefined;
}
}