UNPKG

node-opcua-transport

Version:

pure nodejs OPCUA SDK - module transport

469 lines (467 loc) 19 kB
"use strict"; /* eslint-disable @typescript-eslint/ban-types */ /** * @module node-opcua-transport */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _a; Object.defineProperty(exports, "__esModule", { value: true }); exports.TCP_transport = void 0; exports.setFakeTransport = setFakeTransport; exports.getFakeTransport = getFakeTransport; const node_events_1 = require("node:events"); const chalk_1 = __importDefault(require("chalk")); const node_opcua_assert_1 = require("node-opcua-assert"); const node_opcua_debug_1 = require("node-opcua-debug"); const node_opcua_object_registry_1 = require("node-opcua-object-registry"); const node_opcua_packet_assembler_1 = require("node-opcua-packet-assembler"); const message_builder_base_1 = require("./message_builder_base"); const status_codes_1 = require("./status_codes"); const TCPErrorMessage_1 = require("./TCPErrorMessage"); const tools_1 = require("./tools"); const utils_1 = require("./utils"); const debugLog = (0, node_opcua_debug_1.make_debugLog)("TRANSPORT"); const doDebug = (0, node_opcua_debug_1.checkDebugFlag)("TRANSPORT"); const errorLog = (0, node_opcua_debug_1.make_errorLog)("TRANSPORT"); const warningLog = (0, node_opcua_debug_1.make_warningLog)("TRANSPORT"); const doDebugFlow = false; const defaultFakeSocket = { invalid: true, destroyed: false, destroy(_err) { this.destroyed = true; errorLog("MockSocket.destroy"); }, end() { errorLog("MockSocket.end"); }, write(_data, callback) { /** */ if (callback) { callback(); } }, setKeepAlive(_enable, _initialDelay) { return this; }, setNoDelay(_noDelay) { return this; }, setTimeout(_timeout, _callback) { return this; } }; let fakeSocket = defaultFakeSocket; function setFakeTransport(mockSocket) { fakeSocket = mockSocket; } function getFakeTransport() { if (fakeSocket.invalid) { throw new Error("getFakeTransport: setFakeTransport must be called first - BadProtocolVersionUnsupported"); } process.nextTick(() => fakeSocket.emit("connect")); return fakeSocket; } let counter = 0; class TCP_transport extends node_events_1.EventEmitter { static registry = new node_opcua_object_registry_1.ObjectRegistry(); /** * the size of the header in bytes * @default 8 */ static headerSize = 8; /** * indicates the version number of the OPCUA protocol used * @default 0 */ protocolVersion; maxMessageSize; maxChunkCount; sendBufferSize; receiveBufferSize; bytesWritten; bytesRead; chunkWrittenCount; chunkReadCount; name; _socket; #_closedEmitted = undefined; #_timerId; #_theCallback; #_on_error_during_one_time_message_receiver; #packetAssembler; #_timeout; #_isDisconnecting = false; _theCloseError = null; constructor() { super(); this.name = this.constructor.name + counter; counter += 1; this._socket = null; this.#_timerId = null; this.#_timeout = 5000; // 5 seconds timeout this.#_theCallback = undefined; this.maxMessageSize = 0; this.maxChunkCount = 0; this.receiveBufferSize = 0; this.sendBufferSize = 0; this.protocolVersion = 0; this.bytesWritten = 0; this.bytesRead = 0; this.chunkWrittenCount = 0; this.chunkReadCount = 0; _a.registry.register(this); } toString() { let str = "\n"; str += ` name.............. = ${this.name}\n`; str += ` protocolVersion... = ${this.protocolVersion}\n`; str += ` maxMessageSize.... = ${this.maxMessageSize}\n`; str += ` maxChunkCount..... = ${this.maxChunkCount}\n`; str += ` receiveBufferSize. = ${this.receiveBufferSize}\n`; str += ` sendBufferSize.... = ${this.sendBufferSize}\n`; str += ` bytesRead......... = ${this.bytesRead}\n`; str += ` bytesWritten...... = ${this.bytesWritten}\n`; str += ` chunkWrittenCount. = ${this.chunkWrittenCount}\n`; str += ` chunkReadCount.... = ${this.chunkReadCount}\n`; str += ` closeEmitted ? ....= ${this.#_closedEmitted}\n`; return str; } setLimits({ receiveBufferSize, sendBufferSize, maxMessageSize, maxChunkCount }) { this.receiveBufferSize = receiveBufferSize; this.sendBufferSize = sendBufferSize; this.maxMessageSize = maxMessageSize; this.maxChunkCount = maxChunkCount; if (maxChunkCount !== 0) { if (maxMessageSize / sendBufferSize > maxChunkCount) { const expectedMaxChunkCount = Math.ceil(maxMessageSize / sendBufferSize); warningLog(`Warning: maxChunkCount is not big enough : maxMessageSize / sendBufferSize ${expectedMaxChunkCount} > maxChunkCount ${maxChunkCount}`); } if (maxMessageSize / receiveBufferSize > maxChunkCount) { const expectedMaxChunkCount = Math.ceil(maxMessageSize / receiveBufferSize); warningLog(`Warning: maxChunkCount is not big enough :maxMessageSize / sendBufferSize ${expectedMaxChunkCount} > maxChunkCount ${maxChunkCount}`); } } // reinstall packetAssembler with correct limits this.#_install_packetAssembler(); } get timeout() { return this.#_timeout; } set timeout(value) { (0, node_opcua_assert_1.assert)(!this.#_timerId); doDebug && debugLog(`Setting socket ${this.name} timeout = ${value}`); this.#_timeout = value; } dispose() { this._cleanup_timers(); (0, node_opcua_assert_1.assert)(!this.#_timerId); if (this._socket) { const gracefully = false; if (gracefully) { // close the connection gracefully this._socket.end(); } else { // close the connection forcefully this._socket.destroy(new Error("ClientTCP_transport disposed")); } // this._socket.removeAllListeners(); this._socket = null; } _a.registry.unregister(this); } /** * write the message_chunk on the socket. * @param messageChunk */ write(messageChunk, callback) { const header = (0, message_builder_base_1.readRawMessageHeader)(messageChunk); (0, node_opcua_assert_1.assert)(header.length === messageChunk.length); const c = header.messageHeader.isFinal; (0, node_opcua_assert_1.assert)(c === "F" || c === "C" || c === "A"); this._write_chunk(messageChunk, (err) => { callback?.(err); }); } isDisconnecting() { return !this._socket || this.#_isDisconnecting; } /** * disconnect the TCP layer and close the underlying socket. * The ```"close"``` event will be emitted to the observers with err=null. * */ disconnect(callback) { if (!this._socket || this.#_isDisconnecting) { if (!this.#_isDisconnecting) { this.#_isDisconnecting = true; this.dispose(); } callback(); return; } this.#_isDisconnecting = true; this._cleanup_timers(); this._socket.prependOnceListener("close", () => { this._emitClose(null); setImmediate(() => { callback(); }); }); this._socket?.destroy(new Error("ClientTCP_transport disconnected")); this._socket = null; } isValid() { return this._socket !== null && !this._socket.destroyed; } _write_chunk(messageChunk, callback) { if (this._socket !== null) { this.bytesWritten += messageChunk.length; this.chunkWrittenCount++; this._socket.write(messageChunk, callback); } else { callback?.(); } } #_install_packetAssembler() { if (this.#packetAssembler) { this.#packetAssembler.removeAllListeners(); this.#packetAssembler = undefined; } // install packet assembler ... this.#packetAssembler = new node_opcua_packet_assembler_1.PacketAssembler({ readChunkFunc: message_builder_base_1.readRawMessageHeader, minimumSizeInBytes: _a.headerSize, maxChunkSize: this.receiveBufferSize //Math.max(this.receiveBufferSize, this.sendBufferSize) }); this.#packetAssembler.on("chunk", (chunk) => this._on_message_chunk_received(chunk)); this.#packetAssembler.on("error", (err, code) => { let statusCode = status_codes_1.StatusCodes2.BadTcpMessageTooLarge; switch (code) { case node_opcua_packet_assembler_1.PacketAssemblerErrorCode.ChunkSizeExceeded: statusCode = status_codes_1.StatusCodes2.BadTcpMessageTooLarge; break; default: statusCode = status_codes_1.StatusCodes2.BadTcpInternalError; } this.sendErrorMessage(statusCode, err.message); this.prematureTerminate(new Error(`Packet Assembler : ${err.message}`), statusCode); }); } _install_socket(socket) { // note: it is possible that a transport may be recycled and re-used again after a connection break (0, node_opcua_assert_1.assert)(socket); (0, node_opcua_assert_1.assert)(!this._socket, "already have a socket"); this._socket = socket; this.#_closedEmitted = undefined; this._theCloseError = null; (0, node_opcua_assert_1.assert)(this.#_closedEmitted === undefined, "TCP Transport has already been closed !"); this._socket.setKeepAlive(true); // Setting true for noDelay will immediately fire off data each time socket.write() is called. this._socket.setNoDelay(true); // set socket timeout debugLog(` TCP_transport#install => setting ${this.name} _socket.setTimeout to `, this.timeout); // let use a large timeout here to make sure that we not conflict with our internal timeout this._socket.setTimeout(this.timeout, () => { }); // c8 ignore next doDebug && debugLog(" TCP_transport#_install_socket ", this.name); this.#_install_packetAssembler(); this._socket .on("data", (data) => this._on_socket_data(data)) .on("close", (hadError) => this._on_socket_close(hadError)) .on("end", () => this._on_socket_end()) .on("error", (err) => this._on_socket_error(err)) .on("timeout", () => this._on_socket_timeout()); } sendErrorMessage(statusCode, extraErrorDescription) { // When the Client receives an Error Message it reports the error to the application and closes the TransportConnection gracefully. // If a Client encounters a fatal error, it shall report the error to the application and send a CloseSecureChannel Message. /* c8 ignore next*/ if (doDebug) { debugLog(chalk_1.default.red(" sendErrorMessage ") + chalk_1.default.cyan(statusCode.toString())); debugLog(chalk_1.default.red(" extraErrorDescription ") + chalk_1.default.cyan(extraErrorDescription)); } const reason = `${statusCode.toString()}:${extraErrorDescription || ""} `; const errorResponse = new TCPErrorMessage_1.TCPErrorMessage({ statusCode, reason }); const messageChunk = (0, tools_1.packTcpMessage)("ERR", errorResponse); this.write(messageChunk); } prematureTerminate(err, statusCode) { // https://reference.opcfoundation.org/v104/Core/docs/Part6/6.7.3/ debugLog("prematureTerminate", err ? err.message : "", statusCode.toString(), "has socket = ", !!this._socket); doDebugFlow && errorLog("prematureTerminate from", "has socket = ", !!this._socket, new Error().stack); if (this._socket) { err.message = `premature socket termination ${err.message} `; // we consider this as an error const _s = this._socket; this._socket = null; _s.destroy(err); this.dispose(); } } forceConnectionBreak() { const socket = this._socket; if (!socket) return; socket.emit("error", new Error("ECONNRESET")); socket.destroy(new Error("ECONNRESET")); } /** * * install a one time message receiver callback * * Rules: * * TCP_transport will not emit the ```message``` event, while the "one time message receiver" is in operation. * * the TCP_transport will wait for the next complete message chunk and call the provided callback func * ```callback(null, messageChunk); ``` * * if a messageChunk is not received within ```TCP_transport.timeout``` or if the underlying socket reports * an error, the callback function will be called with an Error. * */ _install_one_time_message_receiver(callback) { (0, node_opcua_assert_1.assert)(!this.#_theCallback, "callback already set"); (0, node_opcua_assert_1.assert)(typeof callback === "function"); this._start_one_time_message_receiver(callback); } _fulfill_pending_promises(err, data) { if (!this.#_theCallback) return false; doDebugFlow && errorLog("_fulfill_pending_promises from", new Error().stack); const callback = this.#_theCallback; this.#_theCallback = undefined; callback(err, data); return true; } _on_message_chunk_received(messageChunk) { if (utils_1.doTraceIncomingChunk) { warningLog("Transport", this.name); warningLog((0, node_opcua_debug_1.hexDump)(messageChunk)); } const hadCallback = this._fulfill_pending_promises(null, messageChunk); this.chunkReadCount++; if (!hadCallback) { this.emit("chunk", messageChunk); } } _cleanup_timers() { if (this.#_timerId) { clearTimeout(this.#_timerId); this.#_timerId = null; } } _start_one_time_message_receiver(callback) { (0, node_opcua_assert_1.assert)(!this.#_timerId && !this.#_on_error_during_one_time_message_receiver, "timer already started"); const _cleanUp = () => { this._cleanup_timers(); if (this.#_on_error_during_one_time_message_receiver) { this._socket?.removeListener("close", this.#_on_error_during_one_time_message_receiver); this.#_on_error_during_one_time_message_receiver = undefined; } }; const onTimeout = () => { _cleanUp(); this._fulfill_pending_promises(new Error(`Timeout(A) in waiting for data on socket(timeout was = ${this.timeout} ms)`)); this.dispose(); }; // Setup timeout detection timer .... this.#_timerId = setTimeout(() => { this.#_timerId = null; onTimeout(); }, this.timeout); // also monitored if (this._socket) { // to do = intercept socket error as well this.#_on_error_during_one_time_message_receiver = (hadError) => { const err = new Error(`ERROR in waiting for data on socket(timeout was = ${this.timeout} ms) hadError${hadError} `); this._emitClose(err); this._fulfill_pending_promises(err); }; this._socket.prependOnceListener("close", this.#_on_error_during_one_time_message_receiver); } const _callback = callback; this.#_theCallback = (err, data) => { _cleanUp(); this.#_theCallback = undefined; _callback(err ?? null, data); }; } _on_socket_data(data) { // c8 ignore next if (!this.#packetAssembler) { throw new Error("internal Error"); } this.bytesRead += data.length; if (data.length > 0) { this.#packetAssembler.feed(data); } } _on_socket_close(hadError) { // c8 ignore next if (doDebug) { debugLog(chalk_1.default.red(` SOCKET CLOSE ${this.name}: `), chalk_1.default.yellow("had_error ="), chalk_1.default.cyan(hadError.toString())); } this.dispose(); if (this.#_theCallback) return; // if (hadError) { // if (this._socket) { // this._socket.destroy(); // } // } this._emitClose(); } _emitClose(err) { err = err || this._theCloseError; doDebugFlow && warningLog("_emitClose ", err?.message || "", "from", new Error().stack); if (!this.#_closedEmitted) { this.#_closedEmitted = err || "noError"; this.emit("close", err || null); // if (this._theCallback) { // const callback = this._theCallback; // this._theCallback = undefined; // callback(err || null); // } } else { const prevClose = this.#_closedEmitted instanceof Error ? this.#_closedEmitted.message : this.#_closedEmitted; debugLog("Already emitted close event", prevClose); debugLog("err = ", err?.message, err); } } _on_socket_end() { // c8 ignore next doDebug && debugLog(chalk_1.default.red(` SOCKET END: ${this.name} `), "is disconnecting ", this.isDisconnecting()); if (this.isDisconnecting()) { return; } // debugLog(`${chalk_1.default.red(" Transport Connection ended")} ${this.name} `); const err = new Error(`${this.name}: socket has been disconnected by third party`); debugLog(" bytesRead = ", this.bytesRead); debugLog(" bytesWritten = ", this.bytesWritten); this._theCloseError = err; this._fulfill_pending_promises(new Error(`Connection aborted - ended by server: ${err ? err.message : ""} `)); } _on_socket_error(err) { // c8 ignore next debugLog(chalk_1.default.red(` _on_socket_error: ${this.name} `), chalk_1.default.yellow(err.message)); // node The "close" event will be called directly following this event. // this._emitClose(err); } _on_socket_timeout() { const msg = `socket timeout: timeout = ${this.timeout} ${this.name} `; doDebug && debugLog(msg); this.prematureTerminate(new Error(msg), status_codes_1.StatusCodes2.BadTimeout); } } exports.TCP_transport = TCP_transport; _a = TCP_transport; //# sourceMappingURL=tcp_transport.js.map