UNPKG

node-opcua-transport

Version:

pure nodejs OPCUA SDK - module transport

338 lines (296 loc) 12.9 kB
/** * @module node-opcua-transport */ // tslint:disable:class-name // system import chalk from "chalk"; import { assert } from "node-opcua-assert"; import { BinaryStream } from "node-opcua-binary-stream"; import { verify_message_chunk } from "node-opcua-chunkmanager"; // opcua requires import { checkDebugFlag, hexDump, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug"; import { type ErrorCallback, type StatusCode, StatusCodes } from "node-opcua-status-code"; // this package requires import { AcknowledgeMessage } from "./AcknowledgeMessage"; import { HelloMessage } from "./HelloMessage"; import type { IHelloAckLimits } from "./i_hello_ack_limits"; import { type ISocketLike, TCP_transport } from "./tcp_transport"; import { decodeMessage, packTcpMessage } from "./tools"; import { doTraceHelloAck } from "./utils"; const debugLog = make_debugLog("TRANSPORT"); const errorLog = make_errorLog("TRANSPORT"); const warningLog = make_warningLog("TRANSPORT"); const doDebug = checkDebugFlag("TRANSPORT"); function clamp_value(value: number, minVal: number, maxVal: number): number { assert(minVal <= maxVal); if (value === 0) { return maxVal; } if (value < minVal) { return minVal; } /* c8 ignore next*/ if (value >= maxVal) { return maxVal; } return value; } const minimumBufferSize = 8192; export interface ITransportParameters { minBufferSize: number; maxBufferSize: number; minMaxMessageSize: number; defaultMaxMessageSize: number; maxMaxMessageSize: number; minMaxChunkCount: number; defaultMaxChunkCount: number; maxMaxChunkCount: number; } const defaultTransportParameters = { minBufferSize: 8192, maxBufferSize: 8 * 64 * 1024, minMaxMessageSize: 128 * 1024, defaultMaxMessageSize: 16 * 1024 * 1024, maxMaxMessageSize: 128 * 1024 * 1024, minMaxChunkCount: 1, defaultMaxChunkCount: Math.ceil((128 * 1024 * 1024) / (8 * 64 * 1024)), maxMaxChunkCount: 9000 }; export function adjustLimitsWithParameters(helloMessage: IHelloAckLimits, params: ITransportParameters): IHelloAckLimits { const defaultReceiveBufferSize = 64 * 1024; const defaultSendBufferSize = 64 * 1024; const receiveBufferSize = clamp_value( helloMessage.receiveBufferSize || defaultReceiveBufferSize, params.minBufferSize, params.maxBufferSize ); const sendBufferSize = clamp_value( helloMessage.sendBufferSize || defaultSendBufferSize, params.minBufferSize, params.maxBufferSize ); const maxMessageSize = clamp_value( helloMessage.maxMessageSize || params.defaultMaxMessageSize, params.minMaxMessageSize, params.maxMaxMessageSize ); if (!helloMessage.maxChunkCount && sendBufferSize) { helloMessage.maxChunkCount = Math.ceil(helloMessage.maxMessageSize / Math.min(sendBufferSize, receiveBufferSize)); } const maxChunkCount = clamp_value( helloMessage.maxChunkCount || params.defaultMaxChunkCount, params.minMaxChunkCount, params.maxMaxChunkCount ); return { receiveBufferSize, sendBufferSize, maxMessageSize, maxChunkCount }; } const defaultAdjustLimits = (hello: IHelloAckLimits) => adjustLimitsWithParameters(hello, defaultTransportParameters); interface ServerTCP_transportOptions { adjustLimits?: (hello: IHelloAckLimits) => IHelloAckLimits; } /** * @private */ export class ServerTCP_transport extends TCP_transport { public static throttleTime = 1000; private _aborted: number; private _helloReceived: boolean; private adjustLimits: (hello: IHelloAckLimits) => IHelloAckLimits; constructor(options?: ServerTCP_transportOptions) { super(); this._aborted = 0; this._helloReceived = false; // before HEL/ACK this.maxChunkCount = 1; this.maxMessageSize = 4 * 1024; this.receiveBufferSize = 4 * 1024; this.adjustLimits = options?.adjustLimits || defaultAdjustLimits; } public toString() { let str = super.toString(); str += `helloReceived...... = ${this._helloReceived}\n`; str += `aborted............ = ${this._aborted}\n`; return str; } protected _write_chunk(messageChunk: Buffer): void { // c8 ignore next if (this.sendBufferSize > 0 && messageChunk.length > this.sendBufferSize) { errorLog( "write chunk exceed sendBufferSize messageChunk length = ", messageChunk.length, "sendBufferSize = ", this.sendBufferSize ); } super._write_chunk(messageChunk); } /** * Initialize the server transport. * * * The ServerTCP_transport initialization process starts by waiting for the client to send a "HEL" message. * * The ServerTCP_transport replies with a "ACK" message and then start waiting for further messages of any size. * * The callback function received an error: * - if no message from the client is received within the ```self.timeout``` period, * - or, if the connection has dropped within the same interval. * - if the protocol version specified within the HEL message is invalid or is greater * than ```self.protocolVersion``` * * */ public init(socket: ISocketLike, callback: ErrorCallback): void { // c8 ignore next debugLog?.(chalk.cyan("init socket")); assert(!this._socket, "init already called!"); this._install_socket(socket); this._install_HEL_message_receiver(callback); } private _abortWithError(statusCode: StatusCode, extraErrorDescription: string, callback: ErrorCallback): void { // When a fatal error occurs, the Server shall send an Error Message to the Client and // closes the TransportConnection gracefully. doDebug && debugLog(this.name, chalk.cyan("_abortWithError", statusCode.toString(), extraErrorDescription)); /* c8 ignore next */ if (this._aborted) { errorLog("Internal Er!ror: _abortWithError already called! Should not happen here"); // already called callback(new Error(statusCode.name)); return; } this._aborted = 1; this._socket?.setTimeout(0); const err = new Error(`${extraErrorDescription} StatusCode = ${statusCode.name}`); this._theCloseError = err; setTimeout(() => { // send the error message and close the connection this.sendErrorMessage(statusCode, statusCode.description); this.prematureTerminate(err, statusCode); this._emitClose(err); callback(err); }, ServerTCP_transport.throttleTime); } private _send_ACK_response(helloMessage: HelloMessage): void { assert(helloMessage.receiveBufferSize >= minimumBufferSize); assert(helloMessage.sendBufferSize >= minimumBufferSize); const limits = this.adjustLimits(helloMessage); this.setLimits(limits); // c8 ignore next if (doTraceHelloAck) { warningLog(`received Hello \n${helloMessage.toString()}`); warningLog("Client accepts only message of size => ", this.maxMessageSize); } // c8 ignore next doDebug && debugLog("Client accepts only message of size => ", this.maxMessageSize); const acknowledgeMessage = new AcknowledgeMessage({ maxChunkCount: this.maxChunkCount, maxMessageSize: this.maxMessageSize, protocolVersion: this.protocolVersion, receiveBufferSize: this.receiveBufferSize, sendBufferSize: this.sendBufferSize }); // c8 ignore next doTraceHelloAck && warningLog(`sending Ack \n${acknowledgeMessage.toString()}`); const messageChunk = packTcpMessage("ACK", acknowledgeMessage); /* c8 ignore next*/ if (doDebug) { verify_message_chunk(messageChunk); debugLog(`server send: ${chalk.yellow("ACK")}`); debugLog(`server send: ${hexDump(messageChunk)}`); debugLog("acknowledgeMessage=", acknowledgeMessage); } // send the ACK reply this.write(messageChunk); } private _install_HEL_message_receiver(callback: ErrorCallback): void { // c8 ignore next doDebug && debugLog(chalk.cyan("_install_HEL_message_receiver ")); this._install_one_time_message_receiver((err?: Error | null, data?: Buffer) => { if (err) { callback(err); } else { // c8 ignore next if (!data) { callback(new Error("No data received")); return; } // pass to next stage handle the HEL message this._on_HEL_message(data, callback); } }); } private _on_HEL_message(data: Buffer, callback: ErrorCallback): void { // c8 ignore next doDebug && debugLog(chalk.cyan("_on_HEL_message")); assert(!this._helloReceived); const stream = new BinaryStream(data); const msgType = data.subarray(0, 3).toString("utf-8"); /* c8 ignore next*/ if (doDebug) { debugLog(`SERVER received ${chalk.yellow(msgType)}`); debugLog(`SERVER received ${hexDump(data)}`); } if (msgType === "HEL") { try { assert(data.length >= 24); const decoded = decodeMessage(stream, HelloMessage); if (!(decoded instanceof HelloMessage)) { throw new Error("expecting a HelloMessage"); } const helloMessage = decoded; // OPCUA Spec 1.03 part 6 - page 41 // The Server shall always accept versions greater than what it supports. if (helloMessage.protocolVersion !== this.protocolVersion) { // c8 ignore next doDebug && debugLog( `warning ! client sent helloMessage.protocolVersion = ` + ` 0x${helloMessage.protocolVersion.toString(16)} ` + `whereas server protocolVersion is 0x${this.protocolVersion.toString(16)}` ); } if (helloMessage.protocolVersion === 0xdeadbeef || helloMessage.protocolVersion < this.protocolVersion) { // Note: 0xDEADBEEF is our special version number to simulate BadProtocolVersionUnsupported in tests // invalid protocol version requested by client this._abortWithError( StatusCodes.BadProtocolVersionUnsupported, `Protocol Version Error${this.protocolVersion}`, callback ); return; } // OPCUA Spec 1.04 part 6 - page 45 // UASC is designed to operate with different TransportProtocols that may have limited buffer // sizes. For this reason, OPC UA Secure Conversation will break OPC UA Messages into several // pieces (called ‘MessageChunks’) that are smaller than the buffer size allowed by the // TransportProtocol. UASC requires a TransportProtocol buffer size that is at least 8 192 bytes if (helloMessage.receiveBufferSize < minimumBufferSize || helloMessage.sendBufferSize < minimumBufferSize) { this._abortWithError( StatusCodes.BadConnectionRejected, `Buffer size too small (should be at least ${minimumBufferSize}`, callback ); return; } // the helloMessage shall only be received once. this._helloReceived = true; this._send_ACK_response(helloMessage); callback(); // no Error } catch (err) { // connection rejected because of malformed message this._abortWithError(StatusCodes.BadConnectionRejected, err instanceof Error ? err.message : "", callback); return; } } else { // invalid packet , expecting HEL /* c8 ignore next*/ doDebug && debugLog(`${chalk.red("BadCommunicationError ")}Expecting 'HEL' message to initiate communication`); this._abortWithError(StatusCodes.BadCommunicationError, "Expecting 'HEL' message to initiate communication", callback); } } }