UNPKG

node-opcua-client

Version:

pure nodejs OPCUA SDK - module client

1,204 lines (1,066 loc) • 76.9 kB
/** * @module node-opcua-client-private */ // tslint:disable:no-unused-expression import fs from "node:fs"; import path from "node:path"; import { withLock } from "@ster5/global-mutex"; import chalk from "chalk"; import { assert } from "node-opcua-assert"; import { getDefaultCertificateManager, makeSubject, type OPCUACertificateManager } from "node-opcua-certificate-manager"; import { type IOPCUASecureObjectOptions, makeApplicationUrn, OPCUASecureObject } from "node-opcua-common"; import { type Certificate, makeSHA1Thumbprint, split_der } from "node-opcua-crypto/web"; import { installPeriodicClockAdjustment, periodicClockAdjustment, uninstallPeriodicClockAdjustment } from "node-opcua-date-time"; import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug"; import { getHostname } from "node-opcua-hostname"; import type { IBasicTransportSettings, ResponseCallback } from "node-opcua-pseudo-session"; import { ClientSecureChannelLayer, type ConnectionStrategy, type ConnectionStrategyOptions, coerceConnectionStrategy, coerceSecurityPolicy, type PerformTransactionCallback, type Request as Request1, type Response as Response1, type SecurityPolicy } from "node-opcua-secure-channel"; import { FindServersOnNetworkRequest, type FindServersOnNetworkRequestOptions, FindServersOnNetworkResponse, FindServersRequest, FindServersResponse, type ServerOnNetwork } from "node-opcua-service-discovery"; import { type ApplicationDescription, type EndpointDescription, GetEndpointsRequest, GetEndpointsResponse } from "node-opcua-service-endpoints"; import { type ChannelSecurityToken, coerceMessageSecurityMode, MessageSecurityMode } from "node-opcua-service-secure-channel"; import { CloseSessionRequest, type CloseSessionResponse } from "node-opcua-service-session"; import { type ErrorCallback, StatusCodes } from "node-opcua-status-code"; import { checkFileExistsAndIsNotEmpty, matchUri } from "node-opcua-utils"; import { type CreateSecureChannelCallbackFunc, type FindEndpointCallback, type FindEndpointOptions, type FindEndpointResult, type FindServersOnNetworkRequestLike, type FindServersRequestLike, type GetEndpointsOptions, OPCUAClientBase, type OPCUAClientBaseEvents, type OPCUAClientBaseOptions, type TransportSettings } from "../client_base"; import type { Request, Response } from "../common"; import type { UserIdentityInfo } from "../user_identity_info"; import { performCertificateSanityCheck } from "../verify"; import type { ClientSessionImpl } from "./client_session_impl"; import type { IClientBase } from "./i_private_client"; const debugLog = make_debugLog(__filename); const doDebug = checkDebugFlag(__filename); const errorLog = make_errorLog(__filename); const warningLog = make_warningLog(__filename); function makeCertificateThumbPrint(certificate: Certificate | Certificate[] | null | undefined): Buffer | null { if (!certificate) return null; return makeSHA1Thumbprint(Array.isArray(certificate) ? certificate[0] : certificate); } const traceInternalState = false; const defaultConnectionStrategy: ConnectionStrategyOptions = { initialDelay: 1000, maxDelay: 20 * 1000, // 20 seconds maxRetry: -1, // infinite randomisationFactor: 0.1 }; function __findEndpoint(this: ClientBaseImpl, endpointUrl: string, params: FindEndpointOptions, _callback: FindEndpointCallback) { if (this.isUnusable()) { return _callback(new Error("Client is not usable")); } const masterClient = this as ClientBaseImpl; doDebug && debugLog("findEndpoint : endpointUrl = ", endpointUrl); doDebug && debugLog(" params ", params); assert(!masterClient._tmpClient); const callback = (err: Error | null, result?: FindEndpointResult) => { masterClient._tmpClient = undefined; _callback(err, result); }; const securityMode = params.securityMode; const securityPolicy = params.securityPolicy; const _connectionStrategy = params.connectionStrategy; const options: OPCUAClientBaseOptions = { 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: EndpointDescription | undefined; const allEndpoints: EndpointDescription[] = []; const step1_connect = (): Promise<void> => { return new Promise<void>((resolve, reject) => { // rebind backoff handler masterClient.listeners("backoff").forEach((handler) => { client.on("backoff", handler as unknown as (retryCount: number, delay: number) => void); }); // c8 ignore next if (doDebug) { client.on("backoff", (retryCount: number, delay: number) => { debugLog( "finding Endpoint => reconnecting ", " retry count", retryCount, " next attempt in ", delay / 1000, "seconds" ); }); } client.connect(endpointUrl, (err?: Error) => { 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 = (): Promise<void> => { return new Promise<void>((resolve, reject) => { client.getEndpoints((err: Error | null, endpoints?: EndpointDescription[]) => { 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 = (): Promise<void> => { return new Promise<void>((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.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: OPCUACertificateManager, serverCertificate: Certificate) { const status = await certificateManager.checkCertificate(serverCertificate); if (status !== 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 ", periodicClockAdjustment.timerInstallationCount); installPeriodicClockAdjustment(); } dispose() { uninstallPeriodicClockAdjustment(); debugLog("uninstallPeriodicClockAdjustment ", periodicClockAdjustment.timerInstallationCount); } } type InternalClientState = | "uninitialized" | "disconnected" | "connecting" | "connected" | "panic" | "reconnecting" | "reconnecting_newchannel_connected" | "disconnecting"; /* * "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 export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBaseEvents> extends OPCUASecureObject<Events> implements OPCUAClientBase<Events>, IClientBase { /** * total number of requests that been canceled due to timeout */ public get timedOutRequestCount(): number { return this._timedOutRequestCount + (this._secureChannel ? this._secureChannel.timedOutRequestCount : 0); } /** * total number of transactions performed by the client x */ public get transactionsPerformed(): number { return this._transactionsPerformed + (this._secureChannel ? this._secureChannel.transactionsPerformed : 0); } /** * is true when the client has already requested the server end points. */ get knowsServerEndpoint(): boolean { return this._serverEndpoints && this._serverEndpoints.length > 0; } /** * true if the client is trying to reconnect to the server after a connection break. */ get isReconnecting(): boolean { 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(): boolean { return this.connectionStrategy.maxRetry > 0 || this.connectionStrategy.maxRetry === -1; } /** * total number of bytes read by the client */ get bytesRead(): number { return this._byteRead + (this._secureChannel ? this._secureChannel.bytesRead : 0); } /** * total number of bytes written by the client */ public get bytesWritten(): number { return this._byteWritten + (this._secureChannel ? this._secureChannel.bytesWritten : 0); } public securityMode: MessageSecurityMode; public securityPolicy: SecurityPolicy; public serverCertificate?: Certificate | Certificate[]; public clientName: string; public protocolVersion: 0; public defaultSecureTokenLifetime: number; public tokenRenewalInterval: number; public connectionStrategy: ConnectionStrategy; public keepPendingSessionsOnDisconnect: boolean; public endpointUrl: string; public discoveryUrl: string; public readonly applicationName: string; private _applicationUri: string; public defaultTransactionTimeout?: number; /** * true if session shall periodically probe the server to keep the session alive and prevent timeout */ public keepSessionAlive: boolean; public readonly keepAliveInterval?: number; public _sessions: ClientSessionImpl[]; protected _serverEndpoints: EndpointDescription[]; public _secureChannel: ClientSecureChannelLayer | null; // statistics... private _byteRead: number; private _byteWritten: number; private _timedOutRequestCount: number; private _transactionsPerformed: number; private _reconnectionIsCanceled: boolean; private _clockAdjuster?: ClockAdjustment; protected _tmpClient?: OPCUAClientBase; private _instanceNumber: number; private _transportSettings: TransportSettings; private _transportTimeout?: number; public clientCertificateManager: OPCUACertificateManager; public isUnusable() { return ( this._internalState === "disconnected" || this._internalState === "disconnecting" || this._internalState === "panic" || this._internalState === "uninitialized" ); } protected _setInternalState(internalState: InternalClientState): void { const previousState = this._internalState; if (doDebug || traceInternalState) { (traceInternalState ? warningLog : debugLog)( chalk.cyan(` Client ${this._instanceNumber} ${this.clientName} : _internalState from `), chalk.yellow(previousState), "to", chalk.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; } public emit<K>(eventName: K, ...others: unknown[]): boolean { // c8 ignore next if (doDebug) { debugLog( chalk.cyan(` Client ${this._instanceNumber} ${this.clientName} emitting `), chalk.magentaBright(eventName as string) ); } // @ts-expect-error return super.emit(eventName, ...others); } constructor(options?: OPCUAClientBaseOptions) { options = options || {}; if (!options.clientCertificateManager) { options.clientCertificateManager = getDefaultCertificateManager("PKI"); } options.privateKeyFile = options.privateKeyFile || options.clientCertificateManager.privateKey; options.certificateFile = options.certificateFile || path.join(options.clientCertificateManager.rootDir, "own/certs/client_certificate.pem"); super(options as IOPCUASecureObjectOptions); this._setInternalState("uninitialized"); this._instanceNumber = g_ClientCounter++; this.applicationName = options.applicationName || "NodeOPCUA-Client"; assert(!this.applicationName.match(/^locale=/), "applicationName badly converted from LocalizedText"); 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; assert(Number.isFinite(this.tokenRenewalInterval) && this.tokenRenewalInterval >= 0); this.securityMode = coerceMessageSecurityMode(options.securityMode); this.securityPolicy = 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 = 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; } private _cancel_reconnection(callback: ErrorCallback) { // _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(); }); } public _recreate_secure_channel(callback: ErrorCallback): void { 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; } assert(this.knowsServerEndpoint); this._setInternalState("reconnecting"); this.emit("start_reconnection"); // send after callback const infiniteConnectionRetry: ConnectionStrategyOptions = { initialDelay: this.connectionStrategy.initialDelay, maxDelay: this.connectionStrategy.maxDelay, maxRetry: -1 }; const _when_internal_error = (err: Error, callback: ErrorCallback) => { errorLog("INTERNAL ERROR", err.message); callback(err); }; const _when_reconnectionIsCanceled = (callback: ErrorCallback) => { 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: Error, message: string, callback: ErrorCallback) => { 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: ErrorCallback) => { this.emit("after_reconnection", null); // send after callback assert(this._secureChannel, "expecting a secureChannel here "); // a new channel has be created and a new connection is established debugLog(chalk.bgWhite.red("ClientBaseImpl: RECONNECTED !!!")); this._setInternalState("reconnecting_newchannel_connected"); return callback(); }; const _attempt_to_recreate_secure_channel = (callback: ErrorCallback) => { 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); } assert(!this._secureChannel, "_attempt_to_recreate_secure_channel, expecting this._secureChannel not to exist"); this._internal_create_secure_channel(infiniteConnectionRetry, (err?: Error | null) => { 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?: Error | null) => { 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?: Error | null) => { 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); } public _internal_create_secure_channel( connectionStrategy: ConnectionStrategyOptions, callback: CreateSecureChannelCallbackFunc ): void { assert(this._secureChannel === null); assert(typeof this.endpointUrl === "string"); debugLog( "_internal_create_secure_channel creating new ClientSecureChannelLayer _internalState =", this._internalState, this.clientName ); const secureChannel = new 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: number, delay: number) => { 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 = (): Promise<void> => { return new Promise<void>((resolve, reject) => { debugLog("_internal_create_secure_channel before secureChannel.create"); secureChannel.create(this.endpointUrl, (err?: Error) => { 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 = (): Promise<void> => { return new Promise<void>((resolve, reject) => { assert(this._secureChannel !== null); if (!this.knowsServerEndpoint) { this._setInternalState("connecting"); this.getEndpoints((err: Error | null /*, 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(() => { 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: OPCUACertificateManager, certificateFile: string, applicationName: string, applicationUri: string ): Promise<void> { if (!fs.existsSync(certificateFile)) { const hostname = getHostname(); // this.serverInfo.applicationUri!; await clientCertificateManager.createSelfSignedCertificate({ applicationUri, dns: [hostname], // ip: await getIpAddresses(), outputFile: certificateFile, subject: makeSubject(applicationName, hostname), startDate: new Date(), validity: 365 * 10 // 10 years }); } // c8 ignore next if (!fs.existsSync(certificateFile)) { throw new Error(` cannot locate certificate file ${certificateFile}`); } } public async createDefaultCertificate(): Promise<void> { // c8 ignore next if ((this as unknown as { _inCreateDefaultCertificate: boolean })._inCreateDefaultCertificate) { errorLog("Internal error : re-entrancy in createDefaultCertificate!"); } (this as unknown as { _inCreateDefaultCertificate: boolean })._inCreateDefaultCertificate = true; if (!checkFileExistsAndIsNotEmpty(this.certificateFile)) { await withLock({ fileToLock: `${this.certificateFile}.mutex` }, async () => { if (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 as unknown as { _inCreateDefaultCertificate: boolean })._inCreateDefaultCertificate = false; } protected _getBuiltApplicationUri(): string { if (!this._applicationUri) { this._applicationUri = makeApplicationUrn(getHostname(), this.applicationName); } return this._applicationUri; } protected async initializeCM(): Promise<void> { 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 (!fs.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 performCertificateSanityCheck(this, "client", this.clientCertificateManager, this._getBuiltApplicationUri()); } protected _internalState: InternalClientState = "uninitialized"; protected _handleUnrecoverableConnectionFailure(err: Error, callback: ErrorCallback): void { debugLog(err.message); this.emit("connection_failed", err); this._setInternalState("disconnected"); callback(err); } private _handleDisconnectionWhileConnecting(err: Error, callback: ErrorCallback) { debugLog(err.message); this.emit("connection_failed", err); this._setInternalState("disconnected"); callback(err); } private _handleSuccessfulConnection(callback: ErrorCallback) { debugLog(" Connected successfully to ", this.endpointUrl); this.emit("connected"); this._setInternalState("connected"); callback(); } /** * connect the OPC-UA client to a server end point. */ public connect(endpointUrl: string): Promise<void>; public connect(endpointUrl: string, callback: ErrorCallback): void; public connect(...args: unknown[]): Promise<void> | void { const endpointUrl = args[0]; const callback = args[1] as ErrorCallback; 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; } 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 !== MessageSecurityMode.None) ) { debugLog("Fetching certificates from endpoints"); this.fetchServerCertificate(endpointUrl, (err: Error | null, adjustedEndpointUrl?: string) => { 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 */ public _connectStep2(endpointUrl: string, callback: ErrorCallback): void { // prevent illegal call to connect assert(this._secureChannel === null); this.endpointUrl = endpointUrl; this._clockAdjuster = this._clockAdjuster || new ClockAdjustment(); OPCUAClientBase.registry.register(this); debugLog("__connectStep2 ", this._internalState); this._internal_create_secure_channel(this.connectionStrategy, (err: Error | null) => { if (!err) { this._handleSuccessfulConnection(callback); } else { OPCUAClientBase.registry.unregister(this); if (this._clockAdjuster) { this._clockAdjuster.dispose(); this._clockAdjuster = undefined; } debugLog(chalk.red("SecureChannel creation has failed with error :", err.message)); if (err.message.match(/ECONNABORTED/)) { debugLog(chalk.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.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); } } }); } public performMessageTransaction(request: Request, callback: ResponseCallback<Response>): void { 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; } assert(this._secureChannel); assert(request); assert(request.requestHeader); assert(typeof callback === "function"); this._secureChannel.performMessageTransaction(request, callback as unknown as PerformTransactionCallback); } /** * * return the endpoint information matching security mode and security policy. */ public findEndpointForSecurity( securityMode: MessageSecurityMode, securityPolicy: SecurityPolicy ): EndpointDescription | undefined { securityMode = coerceMessageSecurityMode(securityMode); securityPolicy = coerceSecurityPolicy(securityPolicy); 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. */ public findEndpoint( endpointUrl: string, securityMode: MessageSecurityMode, securityPolicy: SecurityPolicy ): EndpointDescription | undefined { assert(this.knowsServerEndpoint, "Server end point are not known yet"); if (!this._serverEndpoints || this._serverEndpoints.length === 0) { return undefined; } return this._serverEndpoints.find((endpoint: EndpointDescription) => { return ( matchUri(endpoint.endpointUrl, endpointUrl) && endpoint.securityMode === securityMode && endpoint.securityPolicyUri === securityPolicy ); }); } public async getEndpoints(options?: GetEndpointsOptions): Promise<EndpointDescription[]>; public getEndpoints(options: GetEndpointsOptions, callback: ResponseCallback<EndpointDescription[]>): void; public getEndpoints(callback: ResponseCallback<EndpointDescription[]>): void; public getEndpoints(...args: unknown[]): Promise<EndpointDescription[]> | undefined { if (args.length === 1) { this.getEndpoints({} as GetEndpointsOptions, args[0] as unknown as ResponseCallback<EndpointDescription[]>); return; } const options = args[0] as GetEndpointsOptions; const callback = args[1] as ResponseCallback<EndpointDescription[]>; assert(typeof callback === "function"); options.localeIds = options.localeIds || []; options.profileUris = options.profileUris || []; const request = new GetEndpointsRequest({ endpointUrl: options.endpointUrl || this.endpointUrl, localeIds: options.localeIds, profileUris: options.profileUris, requestHeader: { auditEntryId: null } }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { if (err) { callback(err); return; } this._serverEndpoints = []; // c8 ignore next if (!response || !(response instanceof GetEndpointsResponse)) { callback(new Error("Internal Error")); return; } if (response?.endpoints) { this._serverEndpoints = response.endpoints; } callback(null, this._serverEndpoints); }); return; } /** * @deprecated */ public getEndpointsRequest(options: GetEndpointsOptions, callback: ResponseCallback<EndpointDescription[]>): void { warningLog("note: ClientBaseImpl#getEndpointsRequest is deprecated, use ClientBaseImpl#getEndpoints instead"); this.getEndpoints(options, callback); } /** */ public findServers(options?: FindServersRequestLike): Promise<ApplicationDescription[]>; public findServers(options: FindServersRequestLike, callback: ResponseCallback<ApplicationDescription[]>): void; public findServers(callback: ResponseCallback<ApplicationDescription[]>): void; public findServers(...args: unknown[]): Promise<ApplicationDescription[]> | undefined { if (args.length === 1) { if (typeof args[0] === "function") { this.findServers({} as FindServersRequestLike, args[0] as ResponseCallback<ApplicationDescription[]>); return; } throw new Error("Invalid arguments"); } const options = args[0] as FindServersRequestLike; const callback = args[1] as ResponseCallback<ApplicationDescription[]>; const request = new FindServersRequest({ endpointUrl: options.endpointUrl || this.endpointUrl, localeIds: options.localeIds || [], serverUris: options.serverUris || [] }); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { if (err) { callback(err); return; } /* c8 ignore next */ if (!response || !(response instanceof FindServersResponse)) { callback(new Error("Internal Error")); return; } response.servers = response.servers || []; callback(null, response.servers); }); return undefined; } public findServersOnNetwork(options?: FindServersOnNetworkRequestLike): Promise<ServerOnNetwork[]>; public findServersOnNetwork(callback: ResponseCallback<ServerOnNetwork[]>): void; public findServersOnNetwork(options: FindServersOnNetworkRequestLike, callback: ResponseCallback<ServerOnNetwork[]>): void; public findServersOnNetwork(...args: unknown[]): Promise<ServerOnNetwork[]> | undefined { if (args.length === 1) { this.findServersOnNetwork({} as FindServersOnNetworkRequestOptions, args[0] as ResponseCallback<ServerOnNetwork[]>); return undefined; } const options = args[0] as FindServersOnNetworkRequestOptions; const callback = args[1] as ResponseCallback<ServerOnNetwork[]>; const request = new FindServersOnNetworkRequest(options); this.performMessageTransaction(request, (err: Error | null, response?: Response) => { if (err) { callback(err); return; } /* c8 ignore next */ if (!response || !(response instanceof FindServersOnNetworkResponse)) { callback(new Error("Internal Error")); return; } response.servers = response.servers || []; callback(null, response.servers); }); return undefined; } public _removeSession(session: ClientSessionImpl): void { 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; } assert(this._sessions.indexOf(session) === -1); } private _closeSession( session: ClientSessionImpl, deleteSubscriptions: boolean, callback: (err: Error | null, response?: CloseSession