UNPKG

node-opcua-client

Version:

pure nodejs OPCUA SDK - module client

1,066 lines (1,064 loc) 69.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClientBaseImpl = void 0; /** * @module node-opcua-client-private */ // tslint:disable:no-unused-expression const node_fs_1 = __importDefault(require("node:fs")); const node_path_1 = __importDefault(require("node:path")); const global_mutex_1 = require("@ster5/global-mutex"); const chalk_1 = __importDefault(require("chalk")); const node_opcua_assert_1 = require("node-opcua-assert"); const node_opcua_certificate_manager_1 = require("node-opcua-certificate-manager"); const node_opcua_common_1 = require("node-opcua-common"); const web_1 = require("node-opcua-crypto/web"); const node_opcua_date_time_1 = require("node-opcua-date-time"); const node_opcua_debug_1 = require("node-opcua-debug"); const node_opcua_hostname_1 = require("node-opcua-hostname"); const node_opcua_secure_channel_1 = require("node-opcua-secure-channel"); const node_opcua_service_discovery_1 = require("node-opcua-service-discovery"); const node_opcua_service_endpoints_1 = require("node-opcua-service-endpoints"); const node_opcua_service_secure_channel_1 = require("node-opcua-service-secure-channel"); const node_opcua_service_session_1 = require("node-opcua-service-session"); const node_opcua_status_code_1 = require("node-opcua-status-code"); const node_opcua_utils_1 = require("node-opcua-utils"); const client_base_1 = require("../client_base"); const verify_1 = require("../verify"); const debugLog = (0, node_opcua_debug_1.make_debugLog)(__filename); const doDebug = (0, node_opcua_debug_1.checkDebugFlag)(__filename); const errorLog = (0, node_opcua_debug_1.make_errorLog)(__filename); const warningLog = (0, node_opcua_debug_1.make_warningLog)(__filename); function makeCertificateThumbPrint(certificate) { if (!certificate) return null; return (0, web_1.makeSHA1Thumbprint)(Array.isArray(certificate) ? certificate[0] : certificate); } const traceInternalState = false; const defaultConnectionStrategy = { initialDelay: 1000, maxDelay: 20 * 1000, // 20 seconds maxRetry: -1, // infinite randomisationFactor: 0.1 }; function __findEndpoint(endpointUrl, params, _callback) { if (this.isUnusable()) { return _callback(new Error("Client is not usable")); } const masterClient = this; doDebug && debugLog("findEndpoint : endpointUrl = ", endpointUrl); doDebug && debugLog(" params ", params); (0, node_opcua_assert_1.assert)(!masterClient._tmpClient); const callback = (err, result) => { masterClient._tmpClient = undefined; _callback(err, result); }; const securityMode = params.securityMode; const securityPolicy = params.securityPolicy; const _connectionStrategy = params.connectionStrategy; const options = { applicationName: params.applicationName, applicationUri: params.applicationUri, certificateFile: params.certificateFile, clientCertificateManager: params.clientCertificateManager, clientName: "EndpointFetcher", // use same connectionStrategy as parent connectionStrategy: params.connectionStrategy, // connectionStrategy: { // maxRetry: 0 /* no- retry */, // maxDelay: 2000 // }, privateKeyFile: params.privateKeyFile }; const client = new TmpClient(options); masterClient._tmpClient = client; let selectedEndpoint; const allEndpoints = []; const step1_connect = () => { return new Promise((resolve, reject) => { // rebind backoff handler masterClient.listeners("backoff").forEach((handler) => { client.on("backoff", handler); }); // c8 ignore next if (doDebug) { client.on("backoff", (retryCount, delay) => { debugLog("finding Endpoint => reconnecting ", " retry count", retryCount, " next attempt in ", delay / 1000, "seconds"); }); } client.connect(endpointUrl, (err) => { if (err) { err.message = "Fail to connect to server at " + endpointUrl + " to collect server's certificate (in findEndpoint) \n" + " (err =" + err.message + ")"; warningLog(err.message); return reject(err); } resolve(); }); }); }; const step2_getEndpoints = () => { return new Promise((resolve, reject) => { client.getEndpoints((err, endpoints) => { if (err) { err.message = `error in getEndpoints \n${err.message}`; return reject(err); } // c8 ignore next if (!endpoints) { return reject(new Error("Internal Error")); } for (const endpoint of endpoints) { if (endpoint.securityMode === securityMode && endpoint.securityPolicyUri === securityPolicy) { if (selectedEndpoint) { errorLog("Warning more than one endpoint matching !", endpoint.endpointUrl, selectedEndpoint.endpointUrl); } selectedEndpoint = endpoint; // found it } } resolve(); }); }); }; const step3_disconnect = () => { return new Promise((resolve) => { client.disconnect(() => resolve()); }); }; step1_connect() .then(() => step2_getEndpoints()) .then(() => step3_disconnect()) .then(() => { if (!selectedEndpoint) { return callback(new Error("Cannot find an Endpoint matching " + " security mode: " + securityMode.toString() + " policy: " + securityPolicy.toString())); } // c8 ignore next if (doDebug) { debugLog(chalk_1.default.bgWhite.red("xxxxxxxxxxxxxxxxxxxxx => selected EndPoint = "), selectedEndpoint.toString()); } callback(null, { endpoints: allEndpoints, selectedEndpoint }); }) .catch((err) => { client.disconnect(() => { callback(err); }); }); } /** * check if certificate is trusted or untrusted */ async function _verify_serverCertificate(certificateManager, serverCertificate) { const status = await certificateManager.checkCertificate(serverCertificate); if (status !== node_opcua_status_code_1.StatusCodes.Good) { // c8 ignore next if (doDebug) { // do it again for debug purposes const status1 = await certificateManager.verifyCertificate(serverCertificate); debugLog(status1); } warningLog("serverCertificate = ", makeCertificateThumbPrint(serverCertificate)?.toString("hex") || "none"); warningLog("serverCertificate = ", serverCertificate.toString("base64")); throw new Error(`server Certificate verification failed with err ${status?.toString()}`); } } const forceEndpointDiscoveryOnConnect = !!parseInt(process.env.NODEOPCUA_CLIENT_FORCE_ENDPOINT_DISCOVERY || "0", 10); debugLog("forceEndpointDiscoveryOnConnect = ", forceEndpointDiscoveryOnConnect); class ClockAdjustment { constructor() { debugLog("installPeriodicClockAdjustment ", node_opcua_date_time_1.periodicClockAdjustment.timerInstallationCount); (0, node_opcua_date_time_1.installPeriodicClockAdjustment)(); } dispose() { (0, node_opcua_date_time_1.uninstallPeriodicClockAdjustment)(); debugLog("uninstallPeriodicClockAdjustment ", node_opcua_date_time_1.periodicClockAdjustment.timerInstallationCount); } } /* * "disconnected" ---[connect]----------------------> "connecting" * * "connecting" ---[(connection successful)]------> "connected" * * "connecting" ---[(connection failure)]---------> "disconnected" * * "connecting" ---[disconnect]-------------------> "disconnecting" --> "disconnected" * * "connecting" ---[lost of connection]-----------> "reconnecting" ->[reconnection] * * "reconnecting" ---[reconnection successful]------> "reconnecting_newchannel_connected" * * "reconnecting_newchannel_connected" --(session failure) -->"reconnecting" * * "reconnecting" ---[reconnection failure]---------> [reconnection] ---> "reconnecting" * * "reconnecting" ---[disconnect]-------------------> "disconnecting" --> "disconnected" */ let g_ClientCounter = 0; /** * @internal */ // tslint:disable-next-line: max-classes-per-file class ClientBaseImpl extends node_opcua_common_1.OPCUASecureObject { /** * total number of requests that been canceled due to timeout */ get timedOutRequestCount() { return this._timedOutRequestCount + (this._secureChannel ? this._secureChannel.timedOutRequestCount : 0); } /** * total number of transactions performed by the client x */ get transactionsPerformed() { return this._transactionsPerformed + (this._secureChannel ? this._secureChannel.transactionsPerformed : 0); } /** * is true when the client has already requested the server end points. */ get knowsServerEndpoint() { return this._serverEndpoints && this._serverEndpoints.length > 0; } /** * true if the client is trying to reconnect to the server after a connection break. */ get isReconnecting() { return (!!this._secureChannel?.isConnecting || this._internalState === "reconnecting_newchannel_connected" || this._internalState === "reconnecting"); } /** * true if the connection strategy is set to automatically try to reconnect in case of failure */ get reconnectOnFailure() { return this.connectionStrategy.maxRetry > 0 || this.connectionStrategy.maxRetry === -1; } /** * total number of bytes read by the client */ get bytesRead() { return this._byteRead + (this._secureChannel ? this._secureChannel.bytesRead : 0); } /** * total number of bytes written by the client */ get bytesWritten() { return this._byteWritten + (this._secureChannel ? this._secureChannel.bytesWritten : 0); } securityMode; securityPolicy; serverCertificate; clientName; protocolVersion; defaultSecureTokenLifetime; tokenRenewalInterval; connectionStrategy; keepPendingSessionsOnDisconnect; endpointUrl; discoveryUrl; applicationName; _applicationUri; defaultTransactionTimeout; /** * true if session shall periodically probe the server to keep the session alive and prevent timeout */ keepSessionAlive; keepAliveInterval; _sessions; _serverEndpoints; _secureChannel; // statistics... _byteRead; _byteWritten; _timedOutRequestCount; _transactionsPerformed; _reconnectionIsCanceled; _clockAdjuster; _tmpClient; _instanceNumber; _transportSettings; _transportTimeout; clientCertificateManager; isUnusable() { return (this._internalState === "disconnected" || this._internalState === "disconnecting" || this._internalState === "panic" || this._internalState === "uninitialized"); } _setInternalState(internalState) { const previousState = this._internalState; if (doDebug || traceInternalState) { (traceInternalState ? warningLog : debugLog)(chalk_1.default.cyan(` Client ${this._instanceNumber} ${this.clientName} : _internalState from `), chalk_1.default.yellow(previousState), "to", chalk_1.default.yellow(internalState)); } if (this._internalState === "disconnecting" || this._internalState === "disconnected") { if (internalState === "reconnecting") { errorLog("Internal error, cannot switch to reconnecting when already disconnecting"); } // when disconnecting, we cannot accept any other state } this._internalState = internalState; } emit(eventName, ...others) { // c8 ignore next if (doDebug) { debugLog(chalk_1.default.cyan(` Client ${this._instanceNumber} ${this.clientName} emitting `), chalk_1.default.magentaBright(eventName)); } // @ts-expect-error return super.emit(eventName, ...others); } constructor(options) { options = options || {}; if (!options.clientCertificateManager) { options.clientCertificateManager = (0, node_opcua_certificate_manager_1.getDefaultCertificateManager)("PKI"); } options.privateKeyFile = options.privateKeyFile || options.clientCertificateManager.privateKey; options.certificateFile = options.certificateFile || node_path_1.default.join(options.clientCertificateManager.rootDir, "own/certs/client_certificate.pem"); super(options); this._setInternalState("uninitialized"); this._instanceNumber = g_ClientCounter++; this.applicationName = options.applicationName || "NodeOPCUA-Client"; (0, node_opcua_assert_1.assert)(!this.applicationName.match(/^locale=/), "applicationName badly converted from LocalizedText"); (0, node_opcua_assert_1.assert)(!this.applicationName.match(/urn:/), "applicationName should not be a URI"); // we need to delay _applicationUri initialization this._applicationUri = options.applicationUri || this._getBuiltApplicationUri(); this.clientCertificateManager = options.clientCertificateManager; this.clientCertificateManager.referenceCounter++; this._secureChannel = null; this._reconnectionIsCanceled = false; this.endpointUrl = ""; this.clientName = options.clientName || "ClientSession"; // must be ZERO with Spec 1.0.2 this.protocolVersion = 0; this._sessions = []; this._serverEndpoints = []; this.defaultSecureTokenLifetime = options.defaultSecureTokenLifetime || 600000; this.defaultTransactionTimeout = options.defaultTransactionTimeout; this.tokenRenewalInterval = options.tokenRenewalInterval || 0; (0, node_opcua_assert_1.assert)(Number.isFinite(this.tokenRenewalInterval) && this.tokenRenewalInterval >= 0); this.securityMode = (0, node_opcua_service_secure_channel_1.coerceMessageSecurityMode)(options.securityMode); this.securityPolicy = (0, node_opcua_secure_channel_1.coerceSecurityPolicy)(options.securityPolicy); this.serverCertificate = options.serverCertificate; this.keepSessionAlive = typeof options.keepSessionAlive === "boolean" ? options.keepSessionAlive : false; this.keepAliveInterval = options.keepAliveInterval; // statistics... this._byteRead = 0; this._byteWritten = 0; this._transactionsPerformed = 0; this._timedOutRequestCount = 0; this.connectionStrategy = (0, node_opcua_secure_channel_1.coerceConnectionStrategy)(options.connectionStrategy || defaultConnectionStrategy); /*** * @property keepPendingSessionsOnDisconnect² * @type {boolean} */ this.keepPendingSessionsOnDisconnect = options.keepPendingSessionsOnDisconnect || false; this.discoveryUrl = options.discoveryUrl || ""; this._setInternalState("disconnected"); this._transportSettings = options.transportSettings || {}; this._transportTimeout = options.transportTimeout; } _cancel_reconnection(callback) { // _cancel_reconnection is invoked during disconnection // when we detect that a reconnection is in progress... // c8 ignore next if (!this.isReconnecting) { warningLog("internal error: _cancel_reconnection should only be used when reconnecting is in progress"); } debugLog("canceling reconnection : ", this.clientName); this._reconnectionIsCanceled = true; // c8 ignore next if (!this._secureChannel) { debugLog("_cancel_reconnection: Nothing to do for !", this.clientName, " because secure channel doesn't exist"); return callback(); // nothing to do } this._secureChannel.abortConnection(( /*err?: Error*/) => { this._secureChannel = null; callback(); }); } _recreate_secure_channel(callback) { debugLog("_recreate_secure_channel... while internalState is", this._internalState); if (!this.knowsServerEndpoint) { debugLog("Cannot reconnect , server endpoint is unknown"); callback(new Error("Cannot reconnect, server endpoint is unknown - this.knowsServerEndpoint = false")); return; } (0, node_opcua_assert_1.assert)(this.knowsServerEndpoint); this._setInternalState("reconnecting"); this.emit("start_reconnection"); // send after callback const infiniteConnectionRetry = { initialDelay: this.connectionStrategy.initialDelay, maxDelay: this.connectionStrategy.maxDelay, maxRetry: -1 }; const _when_internal_error = (err, callback) => { errorLog("INTERNAL ERROR", err.message); callback(err); }; const _when_reconnectionIsCanceled = (callback) => { doDebug && debugLog("attempt to recreate a new secure channel : suspended because reconnection is canceled !"); this.emit("reconnection_canceled"); return callback(new Error(`Reconnection has been canceled - ${this.clientName}`)); }; const _failAndRetry = (err, message, callback) => { debugLog("failAndRetry; ", message); if (this._reconnectionIsCanceled) { return _when_reconnectionIsCanceled(callback); } this._destroy_secure_channel(); warningLog("client = ", this.clientName, message, err.message); // else // let retry a little bit later this.emit("reconnection_attempt_has_failed", err, message); // send after callback setImmediate(_attempt_to_recreate_secure_channel, callback); }; const _when_connected = (callback) => { this.emit("after_reconnection", null); // send after callback (0, node_opcua_assert_1.assert)(this._secureChannel, "expecting a secureChannel here "); // a new channel has be created and a new connection is established debugLog(chalk_1.default.bgWhite.red("ClientBaseImpl: RECONNECTED !!!")); this._setInternalState("reconnecting_newchannel_connected"); return callback(); }; const _attempt_to_recreate_secure_channel = (callback) => { debugLog("attempt to recreate a new secure channel"); if (this._reconnectionIsCanceled) { return _when_reconnectionIsCanceled(callback); } if (this._secureChannel) { doDebug && debugLog("attempt to recreate a new secure channel, while channel already exists"); // are we reentrant ? const err = new Error("_internal_create_secure_channel failed, this._secureChannel is supposed to be null"); return _when_internal_error(err, callback); } (0, node_opcua_assert_1.assert)(!this._secureChannel, "_attempt_to_recreate_secure_channel, expecting this._secureChannel not to exist"); this._internal_create_secure_channel(infiniteConnectionRetry, (err) => { if (err) { // c8 ignore next if (this._secureChannel) { const err = new Error("_internal_create_secure_channel failed, expecting this._secureChannel not to exist"); return _when_internal_error(err, callback); } if (err.message.match(/ECONNREFUSED|ECONNABORTED|ETIMEDOUT/)) { return _failAndRetry(err, `create secure channel failed with ECONNREFUSED|ECONNABORTED|ETIMEDOUT\n${err.message}`, callback); } if (err.message.match("Backoff aborted.")) { _failAndRetry(err, "cannot create secure channel (backoff aborted)", callback); return; } if (err?.message.match("BadCertificateInvalid") || err?.message.match(/socket has been disconnected by third party/)) { // it is possible also that hte server has shutdown innapropriately the connection warningLog("the server certificate has changed, we need to retrieve server certificate again: ", err.message); const oldServerCertificate = this.serverCertificate; warningLog("old server certificate ", makeCertificateThumbPrint(oldServerCertificate)?.toString("hex") || "undefined"); // the server may have shut down the channel because its certificate // has changed .... // let request the server certificate again .... return this.fetchServerCertificate(this.endpointUrl, (err1) => { if (err1) { return _failAndRetry(err1, "Failing to fetch new server certificate", callback); } const newServerCertificate = this.serverCertificate; warningLog("new server certificate ", makeCertificateThumbPrint(newServerCertificate)?.toString("hex") || "none"); const sha1Old = makeCertificateThumbPrint(oldServerCertificate)?.toString("hex") || null; const sha1New = makeCertificateThumbPrint(newServerCertificate)?.toString("hex") || null; if (sha1Old === sha1New) { warningLog("server certificate has not changed, but was expected to have changed"); return _failAndRetry(new Error("Server Certificate not changed"), "Failing to fetch new server certificate", callback); } this._internal_create_secure_channel(infiniteConnectionRetry, (err3) => { if (err3) { return _failAndRetry(err3, "trying to create new channel with new certificate", callback); } return _when_connected(callback); }); }); } else { return _failAndRetry(err, "cannot create secure channel", callback); } } else { return _when_connected(callback); } }); }; // create a secure channel // a new secure channel must be established _attempt_to_recreate_secure_channel(callback); } _internal_create_secure_channel(connectionStrategy, callback) { (0, node_opcua_assert_1.assert)(this._secureChannel === null); (0, node_opcua_assert_1.assert)(typeof this.endpointUrl === "string"); debugLog("_internal_create_secure_channel creating new ClientSecureChannelLayer _internalState =", this._internalState, this.clientName); const secureChannel = new node_opcua_secure_channel_1.ClientSecureChannelLayer({ connectionStrategy, defaultSecureTokenLifetime: this.defaultSecureTokenLifetime, parent: this, securityMode: this.securityMode, securityPolicy: this.securityPolicy, serverCertificate: this.serverCertificate, tokenRenewalInterval: this.tokenRenewalInterval, transportSettings: this._transportSettings, transportTimeout: this._transportTimeout, defaultTransactionTimeout: this.defaultTransactionTimeout }); secureChannel.on("backoff", (count, delay) => { this.emit("backoff", count, delay); }); secureChannel.on("abort", () => { this.emit("abort"); }); secureChannel.protocolVersion = this.protocolVersion; this._secureChannel = secureChannel; this.emit("secure_channel_created", secureChannel); const step2_openSecureChannel = () => { return new Promise((resolve, reject) => { debugLog("_internal_create_secure_channel before secureChannel.create"); secureChannel.create(this.endpointUrl, (err) => { debugLog("_internal_create_secure_channel after secureChannel.create"); if (!this._secureChannel) { debugLog("_secureChannel has been closed during the transaction !"); return reject(new Error("Secure Channel Closed")); } if (err) { return reject(err); } this._install_secure_channel_event_handlers(secureChannel); resolve(); }); }); }; const step3_getEndpoints = () => { return new Promise((resolve, reject) => { (0, node_opcua_assert_1.assert)(this._secureChannel !== null); if (!this.knowsServerEndpoint) { this._setInternalState("connecting"); this.getEndpoints((err /*, endpoints?: EndpointDescription[]*/) => { if (!this._secureChannel) { debugLog("_secureChannel has been closed during the transaction ! (while getEndpoints)"); return reject(new Error("Secure Channel Closed")); } if (err) { return reject(err); } resolve(); }); } else { // end points are already known resolve(); } }); }; step2_openSecureChannel() .then(() => step3_getEndpoints()) .then(() => { (0, node_opcua_assert_1.assert)(this._secureChannel !== null); callback(null, secureChannel); }) .catch((err) => { doDebug && debugLog(this.clientName, " : Inner create secure channel has failed", err.message); if (this._secureChannel) { this._secureChannel.abortConnection(() => { this._destroy_secure_channel(); callback(err); }); } else { callback(err); } }); } static async createCertificate(clientCertificateManager, certificateFile, applicationName, applicationUri) { if (!node_fs_1.default.existsSync(certificateFile)) { const hostname = (0, node_opcua_hostname_1.getHostname)(); // this.serverInfo.applicationUri!; await clientCertificateManager.createSelfSignedCertificate({ applicationUri, dns: [hostname], // ip: await getIpAddresses(), outputFile: certificateFile, subject: (0, node_opcua_certificate_manager_1.makeSubject)(applicationName, hostname), startDate: new Date(), validity: 365 * 10 // 10 years }); } // c8 ignore next if (!node_fs_1.default.existsSync(certificateFile)) { throw new Error(` cannot locate certificate file ${certificateFile}`); } } async createDefaultCertificate() { // c8 ignore next if (this._inCreateDefaultCertificate) { errorLog("Internal error : re-entrancy in createDefaultCertificate!"); } this._inCreateDefaultCertificate = true; if (!(0, node_opcua_utils_1.checkFileExistsAndIsNotEmpty)(this.certificateFile)) { await (0, global_mutex_1.withLock)({ fileToLock: `${this.certificateFile}.mutex` }, async () => { if ((0, node_opcua_utils_1.checkFileExistsAndIsNotEmpty)(this.certificateFile)) { // the file may have been created in between return; } warningLog("Creating default certificate ... please wait"); await ClientBaseImpl.createCertificate(this.clientCertificateManager, this.certificateFile, this.applicationName, this._getBuiltApplicationUri()); debugLog("privateKey = ", this.privateKeyFile); debugLog(" = ", this.clientCertificateManager.privateKey); debugLog("certificateFile = ", this.certificateFile); const _certificate = this.getCertificate(); const _privateKey = this.getPrivateKey(); }); } this._inCreateDefaultCertificate = false; } _getBuiltApplicationUri() { if (!this._applicationUri) { this._applicationUri = (0, node_opcua_common_1.makeApplicationUrn)((0, node_opcua_hostname_1.getHostname)(), this.applicationName); } return this._applicationUri; } async initializeCM() { if (!this.clientCertificateManager) { // this usually happen when the client has been already disconnected, // disconnect errorLog("[NODE-OPCUA-E08] initializeCM: clientCertificateManager is null\n" + " This happen when you disconnected the client, to free resources.\n" + " Please create a new OPCUAClient instance if you want to reconnect"); return; } await this.clientCertificateManager.initialize(); await this.createDefaultCertificate(); // c8 ignore next if (!node_fs_1.default.existsSync(this.privateKeyFile)) { throw new Error(` cannot locate private key file ${this.privateKeyFile}`); } if (this.isUnusable()) return; // Note: do NOT wrap this in withLock2 — performCertificateSanityCheck // calls trustCertificate and verifyCertificate which each acquire // withLock2 internally, so an outer withLock2 would cause a deadlock // on the same file-based mutex. await (0, verify_1.performCertificateSanityCheck)(this, "client", this.clientCertificateManager, this._getBuiltApplicationUri()); } _internalState = "uninitialized"; _handleUnrecoverableConnectionFailure(err, callback) { debugLog(err.message); this.emit("connection_failed", err); this._setInternalState("disconnected"); callback(err); } _handleDisconnectionWhileConnecting(err, callback) { debugLog(err.message); this.emit("connection_failed", err); this._setInternalState("disconnected"); callback(err); } _handleSuccessfulConnection(callback) { debugLog(" Connected successfully to ", this.endpointUrl); this.emit("connected"); this._setInternalState("connected"); callback(); } connect(...args) { const endpointUrl = args[0]; const callback = args[1]; (0, node_opcua_assert_1.assert)(typeof callback === "function", "expecting a callback"); if (typeof endpointUrl !== "string" || endpointUrl.length <= 0) { errorLog(`[NODE-OPCUA-E03] OPCUAClient#connect expects a valid endpoint : ${endpointUrl}`); callback(new Error("Invalid endpoint")); return; } (0, node_opcua_assert_1.assert)(typeof endpointUrl === "string" && endpointUrl.length > 0); // c8 ignore next if (this._internalState !== "disconnected") { callback(new Error(`client#connect failed, as invalid internal state = ${this._internalState}`)); return; } // prevent illegal call to connect if (this._secureChannel !== null) { setImmediate(() => callback(new Error("connect already called"))); return; } this._setInternalState("connecting"); this.initializeCM() .then(() => { debugLog("ClientBaseImpl#connect ", endpointUrl, this.clientName); if (this._internalState === "disconnecting" || this._internalState === "disconnected") { return this._handleDisconnectionWhileConnecting(new Error("premature disconnection 1"), callback); } if (!this.serverCertificate && (forceEndpointDiscoveryOnConnect || this.securityMode !== node_opcua_service_secure_channel_1.MessageSecurityMode.None)) { debugLog("Fetching certificates from endpoints"); this.fetchServerCertificate(endpointUrl, (err, adjustedEndpointUrl) => { if (err) { return this._handleUnrecoverableConnectionFailure(err, callback); } if (this.isUnusable()) { return this._handleDisconnectionWhileConnecting(new Error("premature disconnection 2"), callback); } if (forceEndpointDiscoveryOnConnect) { debugLog("connecting with adjusted endpoint : ", adjustedEndpointUrl, " was =", endpointUrl); this._connectStep2(adjustedEndpointUrl || "", callback); } else { debugLog("connecting with endpoint : ", endpointUrl); this._connectStep2(endpointUrl, callback); } }); } else { this._connectStep2(endpointUrl, callback); } }) .catch((err) => { return this._handleUnrecoverableConnectionFailure(err, callback); }); } /** * @private */ _connectStep2(endpointUrl, callback) { // prevent illegal call to connect (0, node_opcua_assert_1.assert)(this._secureChannel === null); this.endpointUrl = endpointUrl; this._clockAdjuster = this._clockAdjuster || new ClockAdjustment(); client_base_1.OPCUAClientBase.registry.register(this); debugLog("__connectStep2 ", this._internalState); this._internal_create_secure_channel(this.connectionStrategy, (err) => { if (!err) { this._handleSuccessfulConnection(callback); } else { client_base_1.OPCUAClientBase.registry.unregister(this); if (this._clockAdjuster) { this._clockAdjuster.dispose(); this._clockAdjuster = undefined; } debugLog(chalk_1.default.red("SecureChannel creation has failed with error :", err.message)); if (err.message.match(/ECONNABORTED/)) { debugLog(chalk_1.default.yellow(`- The client cannot to :${endpointUrl}. Connection has been aborted.`)); err = new Error("The connection has been aborted"); this._handleUnrecoverableConnectionFailure(err, callback); } else if (err.message.match(/ECONNREF/)) { debugLog(chalk_1.default.yellow(`- The client cannot to :${endpointUrl}. Server is not reachable.`)); err = new Error(`The connection cannot be established with server ${endpointUrl} .\n` + "Please check that the server is up and running or your network configuration.\n" + "Err = (" + err.message + ")"); this._handleUnrecoverableConnectionFailure(err, callback); } else if (err.message.match(/disconnecting/)) { /* */ this._handleDisconnectionWhileConnecting(err, callback); } else { err = new Error(`The connection may have been rejected by server,\n Err = (${err.message})`); this._handleUnrecoverableConnectionFailure(err, callback); } } }); } performMessageTransaction(request, callback) { if (!this._secureChannel) { // this may happen if the Server has closed the connection abruptly for some unknown reason // or if the tcp connection has been broken. callback(new Error("performMessageTransaction: No SecureChannel , connection may have been canceled abruptly by server")); return; } if (this._internalState !== "connected" && this._internalState !== "reconnecting_newchannel_connected" && this._internalState !== "connecting" && this._internalState !== "reconnecting") { callback(new Error("performMessageTransaction: Invalid client state = " + this._internalState + " while performing a transaction " + request.schema.name)); return; } (0, node_opcua_assert_1.assert)(this._secureChannel); (0, node_opcua_assert_1.assert)(request); (0, node_opcua_assert_1.assert)(request.requestHeader); (0, node_opcua_assert_1.assert)(typeof callback === "function"); this._secureChannel.performMessageTransaction(request, callback); } /** * * return the endpoint information matching security mode and security policy. */ findEndpointForSecurity(securityMode, securityPolicy) { securityMode = (0, node_opcua_service_secure_channel_1.coerceMessageSecurityMode)(securityMode); securityPolicy = (0, node_opcua_secure_channel_1.coerceSecurityPolicy)(securityPolicy); (0, node_opcua_assert_1.assert)(this.knowsServerEndpoint, "Server end point are not known yet"); return this._serverEndpoints.find((endpoint) => { return endpoint.securityMode === securityMode && endpoint.securityPolicyUri === securityPolicy; }); } /** * * return the endpoint information matching the specified url , security mode and security policy. */ findEndpoint(endpointUrl, securityMode, securityPolicy) { (0, node_opcua_assert_1.assert)(this.knowsServerEndpoint, "Server end point are not known yet"); if (!this._serverEndpoints || this._serverEndpoints.length === 0) { return undefined; } return this._serverEndpoints.find((endpoint) => { return ((0, node_opcua_utils_1.matchUri)(endpoint.endpointUrl, endpointUrl) && endpoint.securityMode === securityMode && endpoint.securityPolicyUri === securityPolicy); }); } getEndpoints(...args) { if (args.length === 1) { this.getEndpoints({}, args[0]); return; } const options = args[0]; const callback = args[1]; (0, node_opcua_assert_1.assert)(typeof callback === "function"); options.localeIds = options.localeIds || []; options.profileUris = options.profileUris || []; const request = new node_opcua_service_endpoints_1.GetEndpointsRequest({ endpointUrl: options.endpointUrl || this.endpointUrl, localeIds: options.localeIds, profileUris: options.profileUris, requestHeader: { auditEntryId: null } }); this.performMessageTransaction(request, (err, response) => { if (err) { callback(err); return; } this._serverEndpoints = []; // c8 ignore next if (!response || !(response instanceof node_opcua_service_endpoints_1.GetEndpointsResponse)) { callback(new Error("Internal Error")); return; } if (response?.endpoints) { this._serverEndpoints = response.endpoints; } callback(null, this._serverEndpoints); }); return; } /** * @deprecated */ getEndpointsRequest(options, callback) { warningLog("note: ClientBaseImpl#getEndpointsRequest is deprecated, use ClientBaseImpl#getEndpoints instead"); this.getEndpoints(options, callback); } findServers(...args) { if (args.length === 1) { if (typeof args[0] === "function") { this.findServers({}, args[0]); return; } throw new Error("Invalid arguments"); } const options = args[0]; const callback = args[1]; const request = new node_opcua_service_discovery_1.FindServersRequest({ endpointUrl: options.endpointUrl || this.endpointUrl, localeIds: options.localeIds || [], serverUris: options.serverUris || [] }); this.performMessageTransaction(request, (err, response) => { if (err) { callback(err); return; } /* c8 ignore next */ if (!response || !(response instanceof node_opcua_service_discovery_1.FindServersResponse)) { callback(new Error("Internal Error")); return; } response.servers = response.servers || []; callback(null, response.servers); }); return undefined; } findServersOnNetwork(...args) { if (args.length === 1) { this.findServersOnNetwork({}, args[0]); return undefined; } const options = args[0]; const callback = args[1]; const request = new node_opcua_service_discovery_1.FindServersOnNetworkRequest(options); this.performMessageTransaction(request, (err, response) => { if (err) { callback(err); return; } /* c8 ignore next */ if (!response || !(response instanceof node_opcua_service_discovery_1.FindServersOnNetworkResponse)) { callback(new Error("Internal Error")); return; } response.servers = response.servers || []; callback(null, response.servers); }); return undefined; } _removeSession(session) { const index = this._sessions.indexOf(session); if (index >= 0) { const _s = this._sessions.splice(index, 1)[0]; // assert(s === session); // assert(session._client === this); session._client = null; } (0, node_opcua_assert_1.assert)(this._sessions.indexOf(session) === -1); } _closeSession(session, deleteSubscriptions, callback) { (0, node_opcua_assert_1.assert)(typeof callback === "function"); (0, node_opcua_assert_1.assert)(typeof deleteSubscriptions === "boolean"); // c8 ignore next if (!this._secureChannel) { return callback(null); // new Error("no channel")); } (0, node_opcua_assert_1.assert)(this._secureChannel); if (!this._secureChannel.isValid()) { return callback(null); } debugLog(chalk_1.default.bgWhite.green("_closeSession ") + this._secureChannel.channelId); const request = new node_opcua_service_session_1.CloseSessionRequest({ deleteSubscriptions }); session.performMessageTransaction(request, (err, response) => { if (err) { callback(err); } else { callback(err, response); } }); } closeSession(session, deleteSubscriptions, callback) { (0, node_opcua_assert_1.assert)(typeof deleteSubscriptions === "boolean"); (0, node_opcua_assert_1.assert)(session); (0, node_opcua_assert_1.assert)(session._client === this, "session must be attached to this"); session._closed = true; // todo : send close session on secure channel this._closeSession(session, deleteSubscriptions, (err, _response) => { session.emitCloseEvent(node_opcua_status_code_1.StatusCodes.Good); this._removeSession(session); session.dispose(); (0, node_opcua_assert_1.assert)(this._sessions.indexOf(session) === -1); (0, node_opcua_assert_1.assert)(session._closed, "session must indicate it is closed"); callback?.(err ? err : undefined); }); } // eslint-disable-next-line max-statements disconnect(...args) { const callback = args[0]; (0, node_opcua_assert_1.assert)(typeof callback === "function", "expecting a callback function here"); this._reconnectionIsCanceled = true; if (this._tmpClient) { warningLog("disconnecting client while tmpClient exists", this._tmpClient.clientName); this._tmpClient.disconnect((_err) => { this._tmpClient = undefined; // retry disconnect on main client this.disconnect(callback); }); return undefined; } if (this._internalState === "disconnected" || this._internalState === "disconnecting") { if (this._internalState === "disconnecting") { warningLog("[NODE-OPCUA-W26] OPCUAClient#disconnect called while already disconnecting. clientName=", this.clientName); } if (!this._reconnectionIsCanceled && this.isReconnecting) { errorLog("Internal Error : _reconnectionIsCanceled should be true if isReconnecting is true"); } callback(); return undefined; } debugLog("disconnecting client! (will set reconnectionIsCanceled to true"); this._reconnectionIsCanceled = true; debugLog("ClientBaseImpl#disconnect", this.endpointUrl); if (this.isReconnecting && !this._reconnectionIsCanceled) { debugLog("ClientBaseImpl#disconnect called while reconnection is in progress"); // let's abort the reconnection process this._cancel_reconnection((err) => { debugLog("ClientBaseImpl#disconnect reconnection has been canceled", this.applicationName); (0, node_opcua_assert_1.assert)(!err, " why would this fail ?"); // sessions cannot be cancelled properly and must be discarded. this.disconnect(callback); }); return undefined; } if (this._sessions.length && !this.keepPendingSessionsOnDisconnect) { debugLog("warning : disconnection : closing pending sessions"); // disconnect has been called whereas living session exists // we need to close them first .... (unless keepPendingSessionsOnDisconnect) this._close_pending_sessions(( /*err*/) => { this.disconnect(callback); }); return undefined; } debugLog("Disconnecting !"); this._setInternalState("disconnecting"); if (this.clientCertificateManager) { const tmp = this.clientCertificateManager; // (this as any).clientCertificateManager = null; tmp.dispose().catch((err) => { debugLog("Error disposing clientCertificateManager", err.message); }); } if (this._sessions.length) { // transfer active session to orphan and detach them from channel const tmp = [...this._sessions]; for (const session of tmp) { this._removeSession(session); } this._sessions = []; } (0, node_opcua_assert_1.assert)(this._sessions.length === 0, " attempt to disconnect a client with live sessions "); client_base_1.OPCUAClientBase.registry.unregister(this); if (this._clockAdjuster) { this._clockAdjuster.dispose(); this._clockAdjuster = undefined; } if (this._secureChannel) { let tmpChannel = this._secureChannel; this._secureChannel = null; debugLog("Closing channel"); tmpChannel.close(() => { this._secureChannel = tmpChannel; tmpChannel = null; this._destroy_secure_channel(); this._setInternalState("disconnected"); callback(); }); } else { this._setInternalState("disconnected"); // this.emit("close", null); callback(); } return undefined; } // override me ! _on_connection_reestablished(callback) { callback(); } toString() { let str = "\n"; str += ` defaultSecureTokenLifetime.... ${this.defaultSecureTokenLifetime}\n`; str += ` securityMode.................. ${node_op