UNPKG

node-opcua-transport

Version:

pure nodejs OPCUA SDK - module transport

372 lines (320 loc) 13.9 kB
/** * @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(); } }