node-opcua-client
Version:
pure nodejs OPCUA SDK - module client
1,204 lines (1,066 loc) • 76.9 kB
text/typescript
/**
* @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