UNPKG

node-opcua-transport

Version:

pure nodejs OPCUA SDK - module transport

345 lines (288 loc) 12.1 kB
/** * @module node-opcua-transport */ import { EventEmitter } from "node:events"; import { assert } from "node-opcua-assert"; import { decodeStatusCode, decodeString, decodeUInt32 } from "node-opcua-basic-types"; import { BinaryStream } from "node-opcua-binary-stream"; import { readMessageHeader, SequenceHeader } from "node-opcua-chunkmanager"; import { hexDump, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug"; import { type MessageHeader, PacketAssembler, type PacketInfo } from "node-opcua-packet-assembler"; import type { StatusCode } from "node-opcua-status-code"; import { get_clock_tick } from "node-opcua-utils"; import { StatusCodes2 } from "./status_codes"; const doPerfMonitoring = process.env.NODEOPCUADEBUG && process.env.NODEOPCUADEBUG.indexOf("PERF") >= 0; const errorLog = make_errorLog("MessageBuilder"); const _debugLog = make_debugLog("MessageBuilder"); const warningLog = make_warningLog("MessageBuilder"); export function readRawMessageHeader(data: Buffer): PacketInfo { const messageHeader = readMessageHeader(new BinaryStream(data)); return { extra: "", length: messageHeader.length, messageHeader }; } export interface MessageBuilderBaseOptions { signatureLength?: number; maxMessageSize?: number; maxChunkCount?: number; maxChunkSize?: number; } export interface MessageBuilderBaseEvents { /** * notify the observers that a new message is being built * @event startChunk */ startChunk: [info: PacketInfo, data: Buffer]; /** * notify the observers that new message chunk has been received * @event chunk */ chunk: [chunk: Buffer]; /** * notify the observers that an error has occurred * @event error */ error: [err: Error, statusCode: StatusCode, requestId: number | null]; /** * notify the observers that a full message has been received * @event full_message_body */ full_message_body: [fullMessageBody: Buffer]; /** * notify the observers that a request has been abandoned * @event abandon */ abandon: [requestId: number]; } /** * */ export class MessageBuilderBase extends EventEmitter { public static defaultMaxChunkCount = 1000; public static defaultMaxMessageSize = 1024 * 64 * 1024; // 64Mo public static defaultMaxChunkSize = 1024 * 8; public readonly signatureLength: number; public readonly maxMessageSize: number; public readonly maxChunkCount: number; public readonly maxChunkSize: number; public readonly options: MessageBuilderBaseOptions; public channelId: number; public totalMessageSize: number; public sequenceHeader: SequenceHeader | null; public _tick0: number; public _tick1: number; protected id: string; protected totalBodySize: number; protected messageChunks: Buffer[]; protected messageHeader?: MessageHeader; readonly #_packetAssembler: PacketAssembler; #_securityDefeated: boolean; #_hasReceivedError: boolean; #blocks: Buffer[]; readonly #_expectedChannelId: number; #offsetBodyStart: number; constructor(options?: MessageBuilderBaseOptions) { super(); this.id = ""; this._tick0 = 0; this._tick1 = 0; this.#_hasReceivedError = false; this.#blocks = []; this.messageChunks = []; this.#_expectedChannelId = 0; options = options || { maxMessageSize: 0, maxChunkCount: 0, maxChunkSize: 0 }; this.signatureLength = options.signatureLength || 0; this.maxMessageSize = options.maxMessageSize || MessageBuilderBase.defaultMaxMessageSize; this.maxChunkCount = options.maxChunkCount || MessageBuilderBase.defaultMaxChunkCount; this.maxChunkSize = options.maxChunkSize || MessageBuilderBase.defaultMaxChunkSize; this.options = options; this.#_packetAssembler = new PacketAssembler({ minimumSizeInBytes: 8, maxChunkSize: this.maxChunkSize, readChunkFunc: readRawMessageHeader }); this.#_packetAssembler.on("chunk", (messageChunk) => this.#_feed_messageChunk(messageChunk)); this.#_packetAssembler.on("startChunk", (info, data) => { if (doPerfMonitoring) { // record tick 0: when the first data is received this._tick0 = get_clock_tick(); } this.emit("startChunk", info, data); }); this.#_packetAssembler.on("error", (err) => { warningLog("packet assembler ", err.message); this._report_error(StatusCodes2.BadTcpMessageTooLarge, `packet assembler: ${err.message}`); }); this.#_securityDefeated = false; this.totalBodySize = 0; this.totalMessageSize = 0; this.channelId = 0; this.#offsetBodyStart = 0; this.sequenceHeader = null; this.#_init_new(); } public dispose(): void { this.removeAllListeners(); } /** * Feed message builder with some data * @param data */ public feed(data: Buffer): void { if (!this.#_securityDefeated && !this.#_hasReceivedError) { this.#_packetAssembler.feed(data); } } protected _decodeMessageBody(_fullMessageBody: Buffer): boolean { return true; } protected _read_headers(binaryStream: BinaryStream): boolean { try { this.messageHeader = readMessageHeader(binaryStream); // assert(binaryStream.length === 8, "expecting message header to be 8 bytes"); this.channelId = binaryStream.readUInt32(); // assert(binaryStream.length === 12); // verifying secure ChannelId if (this.#_expectedChannelId && this.channelId !== this.#_expectedChannelId) { return this._report_error(StatusCodes2.BadTcpSecureChannelUnknown, "Invalid secure channel Id"); } return true; } catch (err) { return this._report_error(StatusCodes2.BadTcpInternalError, `_read_headers error ${err instanceof Error ? err.message : String(err)}`); } } protected _report_abandon(_channelId: number, _tokenId: number, sequenceHeader: SequenceHeader): false { // the server has not been able to send a complete message and has abandoned the request // the connection can probably continue this.#_hasReceivedError = false; /// this.emit("abandon", sequenceHeader.requestId); return false; } protected _report_error(statusCode: StatusCode, errorMessage: string): false { this.#_hasReceivedError = true; errorLog("Error ", this.id, errorMessage); // xx errorLog(new Error()); this.emit("error", new Error(errorMessage), statusCode, this.sequenceHeader?.requestId || null); return false; } #_init_new() { this.#_securityDefeated = false; this.#_hasReceivedError = false; this.totalBodySize = 0; this.totalMessageSize = 0; this.#blocks = []; this.messageChunks = []; } /** * append a message chunk * @param chunk * @private */ #_append(chunk: Buffer): boolean { if (this.#_hasReceivedError) { // the message builder is in error mode and further message chunks should be discarded. return false; } if (this.messageChunks.length + 1 > this.maxChunkCount) { return this._report_error(StatusCodes2.BadTcpMessageTooLarge, `max chunk count exceeded: ${this.maxChunkCount}`); } this.messageChunks.push(chunk); this.totalMessageSize += chunk.length; if (this.totalMessageSize > this.maxMessageSize) { return this._report_error( StatusCodes2.BadTcpMessageTooLarge, `max message size exceeded: ${this.maxMessageSize} : total message size ${this.totalMessageSize}` ); } const binaryStream = new BinaryStream(chunk); if (!this._read_headers(binaryStream)) { return false; // error already reported } assert(binaryStream.length >= 12); // verify message chunk length if (this.messageHeader?.length !== chunk.length) { // tslint:disable:max-line-length return this._report_error( StatusCodes2.BadTcpInternalError, `Invalid messageChunk size: the provided chunk is ${chunk.length} bytes long but header specifies ${ this.messageHeader?.length }` ); } // the start of the message body block const offsetBodyStart = binaryStream.length; // the end of the message body block const offsetBodyEnd = binaryStream.buffer.length; this.totalBodySize += offsetBodyEnd - offsetBodyStart; this.#offsetBodyStart = offsetBodyStart; // add message body to a queue // We use subarray here to avoid copy. // This assumes PacketAssembler manages the buffer lifecycle appropriately. const sharedBuffer = chunk.subarray(this.#offsetBodyStart, offsetBodyEnd); this.#blocks.push(sharedBuffer); return true; } #_feed_messageChunk(chunk: Buffer): boolean { assert(chunk); const messageHeader = readMessageHeader(new BinaryStream(chunk)); this.emit("chunk", chunk); if (messageHeader.isFinal === "F") { if (messageHeader.msgType === "ERR") { const binaryStream = new BinaryStream(chunk); binaryStream.length = 8; const errorCode = decodeStatusCode(binaryStream); const message = decodeString(binaryStream); this._report_error(errorCode, message || "Error message not specified"); return true; } else { this.#_append(chunk); // last message if (this.#_hasReceivedError) { return false; } const fullMessageBody: Buffer = this.#blocks.length === 1 ? this.#blocks[0] : Buffer.concat(this.#blocks); if (doPerfMonitoring) { // record tick 1: when a complete message has been received ( all chunks assembled) this._tick1 = get_clock_tick(); } this.emit("full_message_body", fullMessageBody); const messageOk = this._decodeMessageBody(fullMessageBody); // be ready for next block this.#_init_new(); return messageOk; } } else if (messageHeader.isFinal === "A") { try { // only valid for MSG, according to spec const stream = new BinaryStream(chunk); readMessageHeader(stream); assert(stream.length === 8); // instead of // const securityHeader = new SymmetricAlgorithmSecurityHeader(); // securityHeader.decode(stream); const channelId = stream.readUInt32(); const tokenId = decodeUInt32(stream); const sequenceHeader = new SequenceHeader(); sequenceHeader.decode(stream); return this._report_abandon(channelId, tokenId, sequenceHeader); } catch (err) { const errMessage = err instanceof Error ? err.message : String(err); warningLog(hexDump(chunk)); warningLog("Cannot interpret message chunk: ", errMessage); return this._report_error( StatusCodes2.BadTcpInternalError, `Error decoding message header ${errMessage}` ); } } else if (messageHeader.isFinal === "C") { return this.#_append(chunk); } return false; } }