UNPKG

@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
"use strict"; 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;