node-opcua-transport
Version:
pure nodejs OPCUA SDK - module transport
239 lines (202 loc) • 9.72 kB
text/typescript
/**
* @module node-opcua-transport
*
* Transport-agnostic base class for client-side OPC UA transports.
*
* Owns the UACP HEL/ACK handshake, the negotiated transport settings, and the
* post-connect connection-break detector. Concrete subclasses (`ClientTCP_transport`,
* `ClientWS_transport`, ...) implement only the socket-creation step in `connect()`.
*
* Browser-safe: does not import `node:net`, `node:os`, `node:util`, or any other
* Node-only built-in beyond what `TCP_transport` already inherits from `node:events`.
*/
import { assert } from "node-opcua-assert";
import { BinaryStream } from "node-opcua-binary-stream";
import { readMessageHeader } from "node-opcua-chunkmanager";
import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug";
import type { ErrorCallback } from "node-opcua-status-code";
import { AcknowledgeMessage } from "./AcknowledgeMessage";
import { HelloMessage } from "./HelloMessage";
import type { TransportSettingsOptions } from "./i_client_transport";
import { TCPErrorMessage } from "./TCPErrorMessage";
import { TCP_transport } from "./tcp_transport";
import { decodeMessage, packTcpMessage } from "./tools";
import { doTraceHelloAck } from "./utils";
// Use a string category instead of `__filename` so the module loads in
// browsers without a Node-style filename global.
const doDebug = checkDebugFlag("ClientTransportBase");
const debugLog = make_debugLog("ClientTransportBase");
const warningLog = make_warningLog("ClientTransportBase");
export interface ClientTransportBase {
on(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
on(eventName: "close", eventHandler: (err: Error | null) => void): this;
on(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
on(eventName: "connect", eventHandler: () => void): this;
once(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
once(eventName: "close", eventHandler: (err: Error | null) => void): this;
once(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
once(eventName: "connect", eventHandler: () => void): this;
emit(eventName: "chunk", messageChunk: Buffer): boolean;
emit(eventName: "close", err?: Error | null): boolean;
emit(eventName: "connection_break", err?: Error | null): boolean;
emit(eventName: "connect"): boolean;
}
// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: companion to the interface above
export abstract class ClientTransportBase extends TCP_transport {
public static defaultMaxChunk = 0; // 0 - no limits
public static defaultMaxMessageSize = 0; // 0 - no limits
public static defaultReceiveBufferSize = 1024 * 64 * 10;
public static defaultSendBufferSize = 1024 * 64 * 10; // 8192 min,
public endpointUrl: string;
public serverUri: string;
public numberOfRetry: number;
public parameters?: AcknowledgeMessage;
private _counter: number;
private _helloSettings: {
maxChunkCount: number;
maxMessageSize: number;
receiveBufferSize: number;
sendBufferSize: number;
};
constructor(transportSettings?: TransportSettingsOptions) {
super();
this.endpointUrl = "";
this.serverUri = "";
this._counter = 0;
this.numberOfRetry = 0;
// initially before HEL/ACK
this.maxChunkCount = 1;
this.maxMessageSize = 4 * 1024;
this.receiveBufferSize = 4 * 1024;
transportSettings = transportSettings || {};
this._helloSettings = {
maxChunkCount: transportSettings.maxChunkCount || ClientTransportBase.defaultMaxChunk,
maxMessageSize: transportSettings.maxMessageSize || ClientTransportBase.defaultMaxMessageSize,
receiveBufferSize: transportSettings.receiveBufferSize || ClientTransportBase.defaultReceiveBufferSize,
sendBufferSize: transportSettings.sendBufferSize || ClientTransportBase.defaultSendBufferSize
};
}
public getTransportSettings(): TransportSettingsOptions {
return this._helloSettings;
}
public dispose(): void {
/* c8 ignore next */
doDebug && debugLog(" ClientTransportBase disposed");
super.dispose();
}
/**
* Connect to `endpointUrl` and perform the UACP HEL/ACK handshake.
* Concrete subclasses are responsible for opening the underlying socket
* (TCP, WebSocket, ...) and then driving the inherited HEL/ACK machinery.
*/
public abstract connect(endpointUrl: string, callback: ErrorCallback): void;
/**
* Install the post-connect "connection break" detector. Subclasses call this
* once the underlying socket is open and the HEL/ACK transaction has succeeded.
*
* Detects ECONNRESET / EPIPE / premature socket termination on the live socket
* and re-emits them as `connection_break` so reconnection logic upstream can
* react.
*/
protected _install_post_connect_error_handler(endpointUrl: string): void {
if (!this._socket) return;
this._socket.on("error", (err: Error) => {
// EPIPE : a write on a pipe/socket/FIFO with no reader.
// ECONNRESET : connection forcibly closed by the peer (timeout, reboot, ...).
// "premature socket termination" : abrupt close mid-message.
if (err.message.match(/ECONNRESET|EPIPE|premature socket termination/)) {
/* c8 ignore next */
doDebug && debugLog("connection_break after reconnection", endpointUrl);
this.emit("connection_break", err);
}
});
}
protected _perform_HEL_ACK_transaction(callback: ErrorCallback): void {
/* c8 ignore next */
if (!this._socket) {
callback(new Error("No socket available to perform HEL/ACK transaction"));
return;
}
assert(this._socket, "expecting a valid socket to send a message");
assert(typeof callback === "function");
this._counter = 0;
/* c8 ignore next */
doDebug && debugLog("entering _perform_HEL_ACK_transaction");
this._install_one_time_message_receiver((err: Error | null, data?: Buffer) => {
/* c8 ignore next */
doDebug && debugLog("before _on_ACK_response ", err ? err.message : "");
this._on_ACK_response(callback, err, data);
});
this._send_HELLO_request();
}
private _send_HELLO_request(): void {
/* c8 ignore next */
doDebug && debugLog("entering _send_HELLO_request");
assert(this._socket);
assert(Number.isFinite(this.protocolVersion));
assert(this.endpointUrl.length > 0, " expecting a valid endpoint url");
const { maxChunkCount, maxMessageSize, receiveBufferSize, sendBufferSize } = this._helloSettings;
const helloMessage = new HelloMessage({
endpointUrl: this.endpointUrl,
protocolVersion: this.protocolVersion,
maxChunkCount,
maxMessageSize,
receiveBufferSize,
sendBufferSize
});
// c8 ignore next
doTraceHelloAck && warningLog(`sending Hello\n ${helloMessage.toString()} `);
const messageChunk = packTcpMessage("HEL", helloMessage);
this._write_chunk(messageChunk);
}
private _on_ACK_response(externalCallback: ErrorCallback, err: Error | null, data?: Buffer): void {
/* c8 ignore next */
doDebug && debugLog("entering _on_ACK_response");
assert(typeof externalCallback === "function");
assert(this._counter === 0, "Ack response should only be received once !");
this._counter += 1;
if (err || !data) {
if (this._socket) {
const s = this._socket;
this._socket = null;
s.destroy();
}
externalCallback(err || new Error("no data"));
} else {
this._handle_ACK_response(data, externalCallback);
}
}
private _handle_ACK_response(messageChunk: Buffer, callback: ErrorCallback): void {
const _stream = new BinaryStream(messageChunk);
const messageHeader = readMessageHeader(_stream);
let err: Error | null = null;
/* c8 ignore next */
if (messageHeader.isFinal !== "F") {
err = new Error(" invalid ACK message");
callback(err);
return;
}
let responseClass: typeof AcknowledgeMessage | typeof TCPErrorMessage;
let response: AcknowledgeMessage | TCPErrorMessage;
if (messageHeader.msgType === "ERR") {
responseClass = TCPErrorMessage;
_stream.rewind();
response = decodeMessage(_stream, responseClass) as TCPErrorMessage;
err = new Error(`ACK: ERR received ${response.statusCode.toString()} : ${response.reason}`);
// biome-ignore lint/suspicious/noExplicitAny: legacy diagnostic field tacked onto Error
(err as any).statusCode = response.statusCode;
// c8 ignore next
doTraceHelloAck && warningLog("receiving ERR instead of Ack", response.toString());
callback(err);
} else {
responseClass = AcknowledgeMessage;
_stream.rewind();
response = decodeMessage(_stream, responseClass) as AcknowledgeMessage;
this.parameters = response;
this.setLimits(response);
// c8 ignore next
doTraceHelloAck && warningLog("receiving Ack\n", response.toString());
callback();
}
}
}