node-opcua-transport
Version:
pure nodejs OPCUA SDK - module transport
360 lines (299 loc) • 12.9 kB
text/typescript
/**
* @module node-opcua-transport
*/
import { EventEmitter } from "events";
import { assert } from "node-opcua-assert";
import { decodeStatusCode, decodeString, decodeUInt32 } from "node-opcua-basic-types";
import { BinaryStream } from "node-opcua-binary-stream";
import { createFastUninitializedBuffer } from "node-opcua-buffer-utils";
import { readMessageHeader, SequenceHeader } from "node-opcua-chunkmanager";
import { make_errorLog, make_debugLog, make_warningLog, hexDump } from "node-opcua-debug";
import { MessageHeader, PacketAssembler, PacketInfo } from "node-opcua-packet-assembler";
import { 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 MessageBuilderBase {
/**
*
* notify the observers that a new message is being built
* @event start_chunk
* @param info
* @param data
*/
on(eventName: "startChunk", eventHandler: (info: PacketInfo, data: Buffer) => void): this;
/**
* notify the observers that new message chunk has been received
* @event chunk
* @param messageChunk the raw message chunk
*/
on(eventName: "chunk", eventHandler: (chunk: Buffer) => void): this;
/**
* notify the observers that an error has occurred
* @event error
* @param error the error to raise
*/
on(eventName: "error", eventHandler: (err: Error, statusCode: StatusCode, requestId: number | null) => void): this;
/**
* notify the observers that a full message has been received
* @event full_message_body
* @param full_message_body the full message body made of all concatenated chunks.
*/
on(eventName: "full_message_body", eventHandler: (fullMessageBody: Buffer) => void): this;
/**
*
* @param eventName "abandon"
* @param info
* @param data
*/
on(eventName: "abandon", eventHandler: (requestId: number) => void): this;
emit(eventName: "startChunk", info: PacketInfo, data: Buffer): boolean;
emit(eventName: "chunk", chunk: Buffer): boolean;
emit(eventName: "error", err: Error, statusCode: StatusCode, requestId: number | null): boolean;
emit(eventName: "full_message_body", fullMessageBody: Buffer): boolean;
emit(eventName: "abandon", requestId: number): boolean;
}
/**
*
*/
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);
return 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 as Error).message);
}
}
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
// note : Buffer.slice create a shared memory !
// use Buffer.clone
const sharedBuffer = chunk.subarray(this.#offsetBodyStart, offsetBodyEnd);
const clonedBuffer = createFastUninitializedBuffer(sharedBuffer.length);
sharedBuffer.copy(clonedBuffer, 0, 0);
this.#blocks.push(clonedBuffer);
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) {
warningLog(hexDump(chunk));
warningLog("Cannot interpret message chunk: ", (err as Error).message);
return this._report_error(
StatusCodes2.BadTcpInternalError,
"Error decoding message header " + (err as Error).message
);
}
} else if (messageHeader.isFinal === "C") {
return this.#_append(chunk);
}
return false;
}
}