/** * @module node-opcua-transport */ // system import chalk from "chalk"; 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.js"; import { HelloMessage } from "./HelloMessage.js"; import type { IHelloAckLimits } from "./i_hello_ack_limits.js"; import { ReverseHelloMessage, validateReverseHelloFields } from "./ReverseHelloMessage.js"; import { type ISocketLike, TCP_transport } from "./tcp_transport.js"; import { decodeMessage, packTcpMessage } from "./tools.js"; import { doTraceHelloAck } from "./utils.js"; 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 { if (minVal > maxVal) { throw new Error(`invalid transport parameters: minimum ${minVal} is greater than maximum ${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 ); // MaxMessageSize in the Hello is the largest response the *client* is willing // to receive (OPC 10000-6 7.1.2.3), so it is a ceiling the server must not // raise. clamp_value applies a floor as well, which turned a client asking // for less than minMaxMessageSize into a server sending up to that floor - // and the client then refusing what it had already said was too big. The // floor still applies to the server's own default, chosen when the client // announces no limit at all by sending zero. const maxMessageSize = helloMessage.maxMessageSize ? Math.min(helloMessage.maxMessageSize, params.maxMaxMessageSize) : clamp_value(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")); if (this._socket) { callback(new Error("ServerTCP_transport#init: init already called")); return; } try { this._install_socket(socket); } catch (err) { callback(err instanceof Error ? err : new Error(String(err))); return; } this._install_HEL_message_receiver(callback); } /** * Initialize the server transport for a REVERSE connection (OPC UA Part 6 §7.1.3). * * Unlike {@link init}, the Server has created the (outbound) socket, so it MUST send a * ReverseHello ("RHE") Message first and then wait for the Client's "HEL" exactly as a * normal inbound connection would. Everything after the RHE is byte-identical to `init`: * the client replies with HEL, the server answers ACK and the SecureChannel is opened. * * @param socket an already-connected outbound socket to the Client * @param reverseHello the ServerUri (server ApplicationUri) and EndpointUrl to advertise * @param callback called once the HEL/ACK handshake completes (or on error) */ public initReverse( socket: ISocketLike, reverseHello: { serverUri: string; endpointUrl: string }, callback: ErrorCallback ): void { // c8 ignore next debugLog?.(chalk.cyan("initReverse socket")); if (this._socket) { callback(new Error("ServerTCP_transport#initReverse: init already called")); return; } const reverseHelloMessage = new ReverseHelloMessage({ serverUri: reverseHello.serverUri, endpointUrl: reverseHello.endpointUrl }); try { // never emit an out-of-spec ReverseHello (Part 6 §7.1.2.6 : 4096-byte field limit) validateReverseHelloFields(reverseHelloMessage); } catch (err) { callback(err instanceof Error ? err : new Error(String(err))); return; } try { this._install_socket(socket); } catch (err) { callback(err instanceof Error ? err : new Error(String(err))); return; } // c8 ignore next doTraceHelloAck && warningLog(`sending ReverseHello\n${reverseHelloMessage.toString()}`); // send the ReverseHello, then reuse the exact same HEL receiver as a normal inbound connection. const messageChunk = packTcpMessage("RHE", reverseHelloMessage); this.write(messageChunk); 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); } // the caller has already checked the buffer sizes against minimumBufferSize private _send_ACK_response(helloMessage: HelloMessage): void { 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")); // c8 ignore next if (this._helloReceived) { callback(new Error("HEL message already received")); return; } 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 { if (data.length < 24) { throw new Error(`HEL message too short (${data.length} bytes)`); } 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); } } }