@openevstack/ocpp-rpc
Version:
⚡ A lightweight, production-ready RPC server built with Express and WebSocket for handling OCPP-based EV charger communication. Part of the OpenEVStack ecosystem.
297 lines (296 loc) • 13.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RpcServer = void 0;
const events_1 = __importStar(require("events"));
const ws_1 = require("ws");
const http_1 = require("http");
const rpcError_1 = require("./utils/rpcError");
const errors_1 = require("./errors");
const standard_validators_1 = __importDefault(require("./utils/standard-validators"));
const ws_utils_1 = require("./utils/ws-utils");
const server_client_1 = require("./server-client");
class RpcServer extends events_1.default {
constructor(options) {
super();
this._httpServerAbortControllers = new Set();
this._state = ws_1.OPEN;
this._clients = new Set();
this._pendingUpgrades = new WeakMap();
this._options = {
wssOptions: {},
protocols: [],
callTimeoutMs: 1000 * 30,
pingIntervalMs: 1000 * 30,
deferPingsOnActivity: false,
respondWithDetailedErrors: false,
callConcurrency: 1,
maxBadMessages: Infinity,
strictMode: false,
strictModeValidators: [],
};
this.reconfigure(options || {});
this._wss = new ws_1.WebSocketServer({
...this._options.wssOptions,
noServer: true,
handleProtocols: (_, request) => {
const upgrade = this._pendingUpgrades.get(request);
return upgrade === null || upgrade === void 0 ? void 0 : upgrade.protocol;
},
});
this._wss.on("headers", (h) => h.push(`Server: ${(0, rpcError_1.getPackageIdent)()}`));
this._wss.on("error", (err) => this.emit("error", err));
this._wss.on("connection", this._onConnection.bind(this));
}
reconfigure(options) {
var _a, _b;
const newOpts = Object.assign({}, this._options, options);
if (newOpts.strictMode && !((_a = newOpts.protocols) === null || _a === void 0 ? void 0 : _a.length)) {
throw new Error(`strictMode requires at least one subprotocol`);
}
const strictValidators = [...standard_validators_1.default];
if (newOpts.strictModeValidators) {
strictValidators.push(...newOpts.strictModeValidators);
}
this._strictValidators = strictValidators.reduce((svs, v) => {
svs.set(v.subprotocol, v);
return svs;
}, new Map());
let strictProtocols = [];
if (Array.isArray(newOpts.strictMode)) {
strictProtocols = newOpts.strictMode;
}
else if (newOpts.strictMode) {
strictProtocols = (_b = newOpts.protocols) !== null && _b !== void 0 ? _b : [];
}
const missingValidator = strictProtocols.find((protocol) => this._strictValidators.has(protocol));
if (missingValidator) {
throw new Error(`Missing strictMode validator for subprotocol '${missingValidator}'`);
}
this._options = newOpts;
}
get handleUpgrade() {
return async (request, socket, head) => {
var _a, _b, _c;
let resolved = false;
const ac = new AbortController();
const { signal } = ac;
const url = new URL("http://localhost" + (request.url || "/"));
const pathParts = url.pathname.split("/");
const identity = decodeURIComponent(pathParts.pop() || "");
const endpoint = pathParts.join("/") || "/";
const abortUpgrade = (error) => {
resolved = true;
const code = error instanceof errors_1.WebsocketUpgradeError &&
error.code >= 1000 &&
error.code <= 4999
? error.code
: 1002;
const reason = typeof (error === null || error === void 0 ? void 0 : error.message) === "string"
? error.message.slice(0, 123)
: "Protocol error";
(0, ws_utils_1.abortHandshake)(socket, code, reason);
if (!signal.aborted) {
ac.abort(error);
this.emit("upgradeAborted", {
error,
socket,
request,
identity,
});
}
};
socket.on("error", (err) => {
abortUpgrade(err);
});
try {
if (this._state !== ws_1.OPEN) {
throw new errors_1.WebsocketUpgradeError(1013, "Server not open");
}
const headers = request.headers;
if (((_a = headers.upgrade) === null || _a === void 0 ? void 0 : _a.toLowerCase()) !== "websocket") {
throw new errors_1.WebsocketUpgradeError(1002, "Can only upgrade websocket upgrade requests");
}
const remoteAddress = request.socket.remoteAddress;
const protocols = "sec-websocket-protocol" in headers
? (0, ws_utils_1.parseSubprotocols)(headers["sec-websocket-protocol"])
: new Set();
let password;
if (headers.authorization) {
try {
const b64up = (_c = (_b = headers.authorization.match(/^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/)) === null || _b === void 0 ? void 0 : _b[1]) !== null && _c !== void 0 ? _c : "";
const userPassBuffer = Buffer.from(b64up, "base64");
const identityBuf = Buffer.from(identity + ":");
if (userPassBuffer.compare(identityBuf, 0, identityBuf.length) === 0) {
password = userPassBuffer.subarray(identityBuf.length);
}
}
catch {
// ignore
}
}
const handshake = {
remoteAddress,
headers,
protocols,
endpoint,
identity,
query: url.searchParams,
request,
password,
};
const accept = (session, protocol) => {
var _a;
if (resolved)
return;
resolved = true;
try {
if (socket.readyState !== "open") {
throw new errors_1.WebsocketUpgradeError(1002, `Client readyState = '${socket.readyState}'`);
}
if (protocol === undefined) {
protocol = ((_a = this._options.protocols) !== null && _a !== void 0 ? _a : []).find((p) => protocols.has(p));
}
else if (!protocols.has(protocol)) {
throw new errors_1.WebsocketUpgradeError(1002, `Client doesn't support expected subprotocol`);
}
this._pendingUpgrades.set(request, {
session: session !== null && session !== void 0 ? session : {},
protocol,
handshake,
});
this._wss.handleUpgrade(request, socket, head, (ws) => {
this._wss.emit("connection", ws, request);
});
}
catch (err) {
abortUpgrade(err);
}
};
const reject = (code = 1002, message = "Not found") => {
if (resolved)
return;
resolved = true;
abortUpgrade(new errors_1.WebsocketUpgradeError(code, message));
};
socket.once("end", () => {
reject(1002, `Client connection closed before upgrade complete`);
});
socket.once("close", () => {
reject(1002, `Client connection closed before upgrade complete`);
});
if (this.authCallback) {
await this.authCallback(accept, reject, handshake, signal);
}
else {
accept();
}
}
catch (err) {
abortUpgrade(err);
}
};
}
async _onConnection(websocket, request) {
var _a;
try {
if (this._state !== ws_1.OPEN) {
throw new Error("Server is no longer open");
}
const { handshake, session } = (_a = this._pendingUpgrades.get(request)) !== null && _a !== void 0 ? _a : {};
const client = new server_client_1.RPCServerClient({
identity: handshake.identity,
reconnect: false,
callTimeoutMs: this._options.callTimeoutMs,
pingIntervalMs: this._options.pingIntervalMs,
deferPingsOnActivity: this._options.deferPingsOnActivity,
respondWithDetailedErrors: this._options.respondWithDetailedErrors,
callConcurrency: this._options.callConcurrency,
strictMode: this._options.strictMode,
strictModeValidators: this._options.strictModeValidators,
maxBadMessages: this._options.maxBadMessages,
protocols: this._options.protocols,
}, {
ws: websocket,
session,
handshake,
});
this._clients.add(client);
client.once("close", () => this._clients.delete(client));
this.emit("client", client);
}
catch (err) {
const code = (err === null || err === void 0 ? void 0 : err.statusCode) >= 1000 && (err === null || err === void 0 ? void 0 : err.statusCode) <= 4999
? err.statusCode
: 1011;
const reason = ((err === null || err === void 0 ? void 0 : err.message) || "Internal error").slice(0, 123);
websocket.close(code, reason);
}
}
auth(cb) {
this.authCallback = cb;
}
async listen(port, host, options) {
const ac = new AbortController();
this._httpServerAbortControllers.add(ac);
if (options.signal) {
(0, events_1.once)(options.signal, "abort").then(() => {
var _a;
ac.abort((_a = options.signal) === null || _a === void 0 ? void 0 : _a.reason);
});
}
const httpServer = (0, http_1.createServer)({
noDelay: true,
}, (_, res) => {
res.setHeader("Server", (0, rpcError_1.getPackageIdent)());
res.statusCode = 404;
res.end();
});
httpServer.on("upgrade", this.handleUpgrade);
httpServer.once("close", () => this._httpServerAbortControllers.delete(ac));
await new Promise((resolve, reject) => {
httpServer.listen({ port, host, signal: ac.signal }, (err) => err ? reject(err) : resolve());
});
return httpServer;
}
async close({ code, reason, awaitPending, force }) {
if (this._state === ws_1.OPEN) {
this._state = ws_1.CLOSING;
this.emit("closing");
const closeCode = code !== null && code !== void 0 ? code : 1001;
await Promise.all(Array.from(this._clients).map((cli) => cli.close({ code: closeCode, reason, awaitPending, force })));
await new Promise((resolve, reject) => {
this._wss.close((err) => (err ? reject(err) : resolve()));
this._httpServerAbortControllers.forEach((ac) => ac.abort("Closing"));
});
this._state = ws_1.CLOSED;
this.emit("close");
}
}
}
exports.RpcServer = RpcServer;