node-opcua-client
Version:
pure nodejs OPCUA SDK - module client
1,066 lines (1,064 loc) • 69.2 kB
JavaScript
"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