node-opcua-transport
Version:
pure nodejs OPCUA SDK - module transport
256 lines (254 loc) • 11.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MessageBuilderBase = void 0;
exports.readRawMessageHeader = readRawMessageHeader;
/**
* @module node-opcua-transport
*/
const node_events_1 = require("node:events");
const node_opcua_assert_1 = require("node-opcua-assert");
const node_opcua_basic_types_1 = require("node-opcua-basic-types");
const node_opcua_binary_stream_1 = require("node-opcua-binary-stream");
const node_opcua_chunkmanager_1 = require("node-opcua-chunkmanager");
const node_opcua_debug_1 = require("node-opcua-debug");
const node_opcua_packet_assembler_1 = require("node-opcua-packet-assembler");
const node_opcua_utils_1 = require("node-opcua-utils");
const status_codes_1 = require("./status_codes");
const doPerfMonitoring = process.env.NODEOPCUADEBUG && process.env.NODEOPCUADEBUG.indexOf("PERF") >= 0;
const errorLog = (0, node_opcua_debug_1.make_errorLog)("MessageBuilder");
const _debugLog = (0, node_opcua_debug_1.make_debugLog)("MessageBuilder");
const warningLog = (0, node_opcua_debug_1.make_warningLog)("MessageBuilder");
function readRawMessageHeader(data) {
const messageHeader = (0, node_opcua_chunkmanager_1.readMessageHeader)(new node_opcua_binary_stream_1.BinaryStream(data));
return {
extra: "",
length: messageHeader.length,
messageHeader
};
}
/**
*
*/
class MessageBuilderBase extends node_events_1.EventEmitter {
static defaultMaxChunkCount = 1000;
static defaultMaxMessageSize = 1024 * 64 * 1024; // 64Mo
static defaultMaxChunkSize = 1024 * 8;
signatureLength;
maxMessageSize;
maxChunkCount;
maxChunkSize;
options;
channelId;
totalMessageSize;
sequenceHeader;
_tick0;
_tick1;
id;
totalBodySize;
messageChunks;
messageHeader;
#_packetAssembler;
#_securityDefeated;
#_hasReceivedError;
#blocks;
#_expectedChannelId;
#offsetBodyStart;
constructor(options) {
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 node_opcua_packet_assembler_1.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 = (0, node_opcua_utils_1.get_clock_tick)();
}
this.emit("startChunk", info, data);
});
this.#_packetAssembler.on("error", (err) => {
warningLog("packet assembler ", err.message);
this._report_error(status_codes_1.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();
}
dispose() {
this.removeAllListeners();
}
/**
* Feed message builder with some data
* @param data
*/
feed(data) {
if (!this.#_securityDefeated && !this.#_hasReceivedError) {
this.#_packetAssembler.feed(data);
}
}
_decodeMessageBody(_fullMessageBody) {
return true;
}
_read_headers(binaryStream) {
try {
this.messageHeader = (0, node_opcua_chunkmanager_1.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(status_codes_1.StatusCodes2.BadTcpSecureChannelUnknown, "Invalid secure channel Id");
}
return true;
}
catch (err) {
return this._report_error(status_codes_1.StatusCodes2.BadTcpInternalError, `_read_headers error ${err instanceof Error ? err.message : String(err)}`);
}
}
_report_abandon(_channelId, _tokenId, sequenceHeader) {
// 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;
}
_report_error(statusCode, errorMessage) {
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) {
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(status_codes_1.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(status_codes_1.StatusCodes2.BadTcpMessageTooLarge, `max message size exceeded: ${this.maxMessageSize} : total message size ${this.totalMessageSize}`);
}
const binaryStream = new node_opcua_binary_stream_1.BinaryStream(chunk);
if (!this._read_headers(binaryStream)) {
return false; // error already reported
}
(0, node_opcua_assert_1.assert)(binaryStream.length >= 12);
// verify message chunk length
if (this.messageHeader?.length !== chunk.length) {
// tslint:disable:max-line-length
return this._report_error(status_codes_1.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) {
(0, node_opcua_assert_1.assert)(chunk);
const messageHeader = (0, node_opcua_chunkmanager_1.readMessageHeader)(new node_opcua_binary_stream_1.BinaryStream(chunk));
this.emit("chunk", chunk);
if (messageHeader.isFinal === "F") {
if (messageHeader.msgType === "ERR") {
const binaryStream = new node_opcua_binary_stream_1.BinaryStream(chunk);
binaryStream.length = 8;
const errorCode = (0, node_opcua_basic_types_1.decodeStatusCode)(binaryStream);
const message = (0, node_opcua_basic_types_1.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 = 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 = (0, node_opcua_utils_1.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 node_opcua_binary_stream_1.BinaryStream(chunk);
(0, node_opcua_chunkmanager_1.readMessageHeader)(stream);
(0, node_opcua_assert_1.assert)(stream.length === 8);
// instead of
// const securityHeader = new SymmetricAlgorithmSecurityHeader();
// securityHeader.decode(stream);
const channelId = stream.readUInt32();
const tokenId = (0, node_opcua_basic_types_1.decodeUInt32)(stream);
const sequenceHeader = new node_opcua_chunkmanager_1.SequenceHeader();
sequenceHeader.decode(stream);
return this._report_abandon(channelId, tokenId, sequenceHeader);
}
catch (err) {
const errMessage = err instanceof Error ? err.message : String(err);
warningLog((0, node_opcua_debug_1.hexDump)(chunk));
warningLog("Cannot interpret message chunk: ", errMessage);
return this._report_error(status_codes_1.StatusCodes2.BadTcpInternalError, `Error decoding message header ${errMessage}`);
}
}
else if (messageHeader.isFinal === "C") {
return this.#_append(chunk);
}
return false;
}
}
exports.MessageBuilderBase = MessageBuilderBase;
//# sourceMappingURL=message_builder_base.js.map