node-opcua-transport
Version:
pure nodejs OPCUA SDK - module transport
372 lines (320 loc) • 13.9 kB
text/typescript
/**
* @module node-opcua-transport
*/
import os from "os";
import { createConnection } from "net";
import { types } from "util";
import chalk from "chalk";
import { assert } from "node-opcua-assert";
import { BinaryStream } from "node-opcua-binary-stream";
import { readMessageHeader } from "node-opcua-chunkmanager";
import { ErrorCallback } from "node-opcua-status-code";
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
import { getFakeTransport, ISocketLike, TCP_transport } from "./tcp_transport";
import { decodeMessage, packTcpMessage, parseEndpointUrl } from "./tools";
import { AcknowledgeMessage } from "./AcknowledgeMessage";
import { HelloMessage } from "./HelloMessage";
import { TCPErrorMessage } from "./TCPErrorMessage";
import { doTraceHelloAck } from "./utils";
const doDebug = checkDebugFlag(__filename);
const debugLog = make_debugLog(__filename);
const warningLog = make_warningLog(__filename);
const errorLog = make_errorLog(__filename);
const gHostname = os.hostname();
function createClientSocket(endpointUrl: string, timeout: number): ISocketLike {
// create a socket based on Url
const ep = parseEndpointUrl(endpointUrl);
const port = parseInt(ep.port!, 10);
const hostname = ep.hostname!;
let socket: ISocketLike;
switch (ep.protocol) {
case "opc.tcp:":
socket = createConnection({ host: hostname, port, timeout }, () => {
doDebug && debugLog(`connected to server! ${hostname}:${port} timeout:${timeout} `);
});
return socket;
case "fake:":
assert(ep.protocol === "fake:", " Unsupported transport protocol");
socket = getFakeTransport();
return socket;
case "websocket:":
case "http:":
case "https:":
default: {
const msg = "[NODE-OPCUA-E05] this transport protocol is not supported :" + ep.protocol;
errorLog(msg);
throw new Error(msg);
}
}
}
export interface ClientTCP_transport {
on(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
on(eventName: "close", eventHandler: (err: Error | null) => void): this;
on(eventName: "connection_break", eventHandler: () => 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: () => 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"): boolean;
emit(eventName: "connect"): boolean;
}
export interface TransportSettingsOptions {
maxChunkCount?: number;
maxMessageSize?: number;
receiveBufferSize?: number;
sendBufferSize?: number;
}
/**
* a ClientTCP_transport connects to a remote server socket and
* initiates a communication with a HEL/ACK transaction.
* It negotiates the communication parameters with the other end.
* @example
*
* ```javascript
* const transport = ClientTCP_transport(url);
*
* transport.timeout = 10000;
*
* transport.connect(function (err)) {
* if (err) {
* // cannot connect
* } else {
* // connected
*
* }
* });
* ....
*
* transport.write(message_chunk, 'F');
*
* ....
*
* transport.on("chunk", function (message_chunk) {
* // do something with chunk from server...
* });
*
*
* ```
*
*
*/
export class ClientTCP_transport 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 || ClientTCP_transport.defaultMaxChunk,
maxMessageSize: transportSettings.maxMessageSize || ClientTCP_transport.defaultMaxMessageSize,
receiveBufferSize: transportSettings.receiveBufferSize || ClientTCP_transport.defaultReceiveBufferSize,
sendBufferSize: transportSettings.sendBufferSize || ClientTCP_transport.defaultSendBufferSize
};
}
public getTransportSettings(): TransportSettingsOptions {
return this._helloSettings;
}
public dispose(): void {
/* istanbul ignore next */
doDebug && debugLog(" ClientTCP_transport disposed");
super.dispose();
}
public connect(endpointUrl: string, callback: ErrorCallback): void {
const ep = parseEndpointUrl(endpointUrl);
this.endpointUrl = endpointUrl;
this.serverUri = "urn:" + gHostname + ":Sample";
/* istanbul ignore next */
doDebug && debugLog(chalk.cyan("ClientTCP_transport#connect(endpointUrl = " + endpointUrl + ")"));
let socket: ISocketLike | null = null;
try {
socket = createClientSocket(endpointUrl, this.timeout);
} catch (err) {
/* istanbul ignore next */
doDebug && debugLog("CreateClientSocket has failed");
return callback(err as Error);
}
/**
*
*/
const _on_socket_error_after_connection = (err: Error) => {
/* istanbul ignore next */
doDebug && debugLog(" _on_socket_error_after_connection ClientTCP_transport Socket Error", err.message);
// EPIPE : EPIPE (Broken pipe): A write on a pipe, socket, or FIFO for which there is no process to read the
// data. Commonly encountered at the net and http layers, indicative that the remote side of the stream
// being written to has been closed.
// ECONNRESET (Connection reset by peer): A connection was forcibly closed by a peer. This normally results
// from a loss of the connection on the remote socket due to a timeout or reboot. Commonly encountered
// via the http and net module
// socket termination could happen:
// * when the socket times out (lost of connection, network outage, etc...)
// * or, when the server abruptly disconnects the socket ( in case of invalid communication for instance)
if (err.message.match(/ECONNRESET|EPIPE|premature socket termination/)) {
/**
* @event connection_break
*
*/
warningLog("connection_break", endpointUrl);
this.emit("connection_break");
}
};
const _on_socket_connect = () => {
/* istanbul ignore next */
doDebug && debugLog("entering _on_socket_connect");
_remove_connect_listeners();
this._perform_HEL_ACK_transaction((err) => {
if (!err) {
/* istanbul ignore next */
if (!this._socket) {
return callback(new Error("Abandoned"));
}
// install error handler to detect connection break
this._socket.on("error", _on_socket_error_after_connection);
/**
* notify the observers that the transport is connected (the socket is connected and the the HEL/ACK
* transaction has been done)
* @event connect
*
*/
this.emit("connect");
} else {
debugLog("_perform_HEL_ACK_transaction has failed with err=", err.message);
}
callback(err);
});
};
const _on_socket_error_for_connect = (err: Error) => {
// this handler will catch attempt to connect to an inaccessible address.
/* istanbul ignore next */
doDebug && debugLog(chalk.cyan("ClientTCP_transport#connect - _on_socket_error_for_connect"), err.message);
assert(types.isNativeError(err));
_remove_connect_listeners();
callback(err);
};
const _on_socket_end_for_connect = () => {
/* istanbul ignore next */
doDebug &&
debugLog(chalk.cyan("ClientTCP_transport#connect -> _on_socket_end_for_connect Socket has been closed by server"));
};
const _remove_connect_listeners = () => {
/* istanbul ignore next */
if (!this._socket) {
return;
}
this._socket.removeListener("error", _on_socket_error_for_connect);
this._socket.removeListener("end", _on_socket_end_for_connect);
};
this._install_socket(socket);
this._socket!.once("error", _on_socket_error_for_connect);
this._socket!.once("end", _on_socket_end_for_connect);
this._socket!.once("connect", _on_socket_connect);
}
private _handle_ACK_response(messageChunk: Buffer, callback: ErrorCallback) {
const _stream = new BinaryStream(messageChunk);
const messageHeader = readMessageHeader(_stream);
let err;
/* istanbul ignore next */
if (messageHeader.isFinal !== "F") {
err = new Error(" invalid ACK message");
return callback(err);
}
let responseClass;
let response;
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);
(err as any).statusCode = response.statusCode;
// istanbul ignore next
doTraceHelloAck && warningLog("receiving ERR instead of Ack", response.toString());
callback(err);
} else {
responseClass = AcknowledgeMessage;
_stream.rewind();
response = decodeMessage(_stream, responseClass);
this.parameters = response as AcknowledgeMessage;
this.setLimits(response as AcknowledgeMessage);
// istanbul ignore next
doTraceHelloAck && warningLog("receiving Ack\n", response.toString());
callback();
}
}
private _send_HELLO_request() {
/* istanbul ignore next */
doDebug && debugLog("entering _send_HELLO_request");
assert(this._socket);
assert(isFinite(this.protocolVersion));
assert(this.endpointUrl.length > 0, " expecting a valid endpoint url");
const { maxChunkCount, maxMessageSize, receiveBufferSize, sendBufferSize } = this._helloSettings;
// Write a message to the socket as soon as the client is connected,
// the server will receive it as message from the client
const helloMessage = new HelloMessage({
endpointUrl: this.endpointUrl,
protocolVersion: this.protocolVersion,
maxChunkCount,
maxMessageSize,
receiveBufferSize,
sendBufferSize
});
// istanbul 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) {
/* istanbul 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) {
externalCallback(err || new Error("no data"));
if (this._socket) {
this._socket.end();
}
} else {
this._handle_ACK_response(data, externalCallback);
}
}
private _perform_HEL_ACK_transaction(callback: ErrorCallback) {
/* istanbul ignore next */
if (!this._socket) {
return callback(new Error("No socket available to perform HEL/ACK transaction"));
}
assert(this._socket, "expecting a valid socket to send a message");
assert(typeof callback === "function");
this._counter = 0;
/* istanbul ignore next */
doDebug && debugLog("entering _perform_HEL_ACK_transaction");
this._install_one_time_message_receiver((err: Error | null, data?: Buffer) => {
/* istanbul ignore next */
doDebug && debugLog("before _on_ACK_response ", err ? err.message : "");
this._on_ACK_response(callback, err, data);
});
this._send_HELLO_request();
}
}