node-opcua-client
Version:
pure nodejs OPCUA SDK - module client
1,216 lines (1,072 loc) • 74.9 kB
text/typescript
/**
* @module node-opcua-client-private
*/
// tslint:disable:no-unused-expression
import fs from "fs";
import path from "path";
import async from "async";
import chalk from "chalk";
import { withLock } from "@ster5/global-mutex";
import { assert } from "node-opcua-assert";
import { IOPCUASecureObjectOptions, OPCUASecureObject } from "node-opcua-common";
import { Certificate, makeSHA1Thumbprint, Nonce, 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 { makeApplicationUrn } from "node-opcua-common";
import { getHostname } from "node-opcua-hostname";
import { IBasicTransportSettings, ResponseCallback } from "node-opcua-pseudo-session";
import {
ClientSecureChannelLayer,
coerceConnectionStrategy,
coerceSecurityPolicy,
ConnectionStrategy,
ConnectionStrategyOptions,
Request as Request1,
Response as Response1,
SecurityPolicy
} from "node-opcua-secure-channel";
import {
FindServersOnNetworkRequest,
FindServersOnNetworkRequestOptions,
FindServersOnNetworkResponse,
FindServersRequest,
FindServersResponse,
ServerOnNetwork
} from "node-opcua-service-discovery";
import {
ApplicationDescription,
EndpointDescription,
GetEndpointsRequest,
GetEndpointsResponse
} from "node-opcua-service-endpoints";
import { ChannelSecurityToken, coerceMessageSecurityMode, MessageSecurityMode } from "node-opcua-service-secure-channel";
import { ErrorCallback, StatusCode, StatusCodes } from "node-opcua-status-code";
import { matchUri } from "node-opcua-utils";
import { getDefaultCertificateManager, makeSubject, OPCUACertificateManager } from "node-opcua-certificate-manager";
import { VerificationStatus } from "node-opcua-pki";
import { CloseSessionRequest, CloseSessionResponse } from "node-opcua-service-session";
import { Request, Response } from "../common";
import {
CreateSecureChannelCallbackFunc,
FindEndpointCallback,
FindEndpointOptions,
FindEndpointResult,
FindServersOnNetworkRequestLike,
FindServersRequestLike,
GetEndpointsOptions,
OPCUAClientBase,
OPCUAClientBaseOptions,
TransportSettings
} from "../client_base";
import { UserIdentityInfo } from "../user_identity_info";
import { performCertificateSanityCheck } from "../verify";
import { ClientSessionImpl } from "./client_session_impl";
import { IClientBase } from "./i_private_client";
import { waitUntilReconnectionIsCanceled } from "./reconnection/client_reconnection";
const debugLog = make_debugLog(__filename);
const doDebug = checkDebugFlag(__filename);
const errorLog = make_errorLog(__filename);
const warningLog = make_warningLog(__filename);
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 tasks = [
(innerCallback: ErrorCallback) => {
// rebind backoff handler
masterClient.listeners("backoff").forEach((handler: any) => client.on("backoff", handler));
// istanbul 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) {
// let's improve the error message with meaningful info
err.message =
"Fail to connect to server at " +
endpointUrl +
" to collect server's certificate (in findEndpoint) \n" +
" (err =" +
err.message +
")";
warningLog(err.message);
}
return innerCallback(err);
});
},
(innerCallback: ErrorCallback) => {
client.getEndpoints((err: Error | null, endpoints?: EndpointDescription[]) => {
if (err) {
err.message = "error in getEndpoints \n" + err.message;
return innerCallback(err);
}
// istanbul ignore next
if (!endpoints) {
return innerCallback(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
}
}
innerCallback();
});
},
(innerCallback: ErrorCallback) => {
client.disconnect(innerCallback);
}
];
async.series(tasks, (err) => {
if (err) {
client.disconnect(() => {
callback(err);
});
return;
}
if (!selectedEndpoint) {
return callback(
new Error(
"Cannot find an Endpoint matching " +
" security mode: " +
securityMode.toString() +
" policy: " +
securityPolicy.toString()
)
);
}
// istanbul ignore next
if (doDebug) {
debugLog(chalk.bgWhite.red("xxxxxxxxxxxxxxxxxxxxx => selected EndPoint = "), selectedEndpoint.toString());
}
const result = {
endpoints: allEndpoints,
selectedEndpoint
};
callback(null, result);
});
}
/**
* 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) {
// istanbul ignore next
if (doDebug) {
// do it again for debug purposes
const status1 = await certificateManager.verifyCertificate(serverCertificate);
debugLog(status1);
}
warningLog("serverCertificate = ", makeSHA1Thumbprint(serverCertificate).toString("hex"));
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 extends OPCUASecureObject implements OPCUAClientBase, 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 && 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;
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(eventName: string | symbol, ...others: any[]): boolean {
// istanbul ignore next
if (doDebug) {
debugLog(chalk.cyan(` Client ${this._instanceNumber} ${this.clientName} emitting `), chalk.magentaBright(eventName));
}
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._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(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...
// istanbul 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;
// istanbul 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();
});
this.once("reconnection_canceled", () => {
/* empty */
});
}
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");
return callback(new Error("Cannot reconnect, server endpoint is unknown - this.knowsServerEndpoint = false"));
}
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) {
// istanbul 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.")) {
return _failAndRetry(err!, "cannot create secure channel (backoff aborted)", callback);
}
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 ",
oldServerCertificate ? makeSHA1Thumbprint(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 ", makeSHA1Thumbprint(newServerCertificate).toString("hex"));
const sha1Old = oldServerCertificate ? makeSHA1Thumbprint(oldServerCertificate!) : null;
const sha1New = newServerCertificate ? makeSHA1Thumbprint(newServerCertificate) : 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);
async.series(
[
// ------------------------------------------------- STEP 2 : OpenSecureChannel
(innerCallback: ErrorCallback) => {
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 innerCallback(new Error("Secure Channel Closed"));
}
if (!err) {
this._install_secure_channel_event_handlers(secureChannel);
}
innerCallback(err);
});
},
// ------------------------------------------------- STEP 3 : GetEndpointsRequest
(innerCallback: ErrorCallback) => {
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 innerCallback(new Error("Secure Channel Closed"));
}
innerCallback(err ? err : undefined);
});
} else {
// end points are already known
innerCallback();
}
}
],
(err) => {
if (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);
}
} else {
assert(this._secureChannel !== null);
callback(null, secureChannel);
}
}
);
}
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
});
}
// istanbul ignore next
if (!fs.existsSync(certificateFile)) {
throw new Error(" cannot locate certificate file " + certificateFile);
}
}
public async createDefaultCertificate(): Promise<void> {
// istanbul ignore next
if ((this as any)._inCreateDefaultCertificate) {
errorLog("Internal error : re-entrancy in createDefaultCertificate!");
}
(this as any)._inCreateDefaultCertificate = true;
if (!fs.existsSync(this.certificateFile)) {
await withLock({ fileToLock: this.certificateFile + ".mutex" }, async () => {
if (fs.existsSync(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 any)._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();
// istanbul ignore next
if (!fs.existsSync(this.privateKeyFile)) {
throw new Error(" cannot locate private key file " + this.privateKeyFile);
}
if (this.isUnusable()) return;
await this.clientCertificateManager.withLock2(async () => {
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");
return callback(err);
}
private _handleDisconnectionWhileConnecting(err: Error, callback: ErrorCallback) {
debugLog(err.message);
this.emit("connection_failed", err);
this._setInternalState("disconnected");
return 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: any[]): any {
const endpointUrl = args[0];
const callback = args[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;
}
assert(typeof endpointUrl === "string" && endpointUrl.length > 0);
// istanbul 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.
return callback(
new Error("performMessageTransaction: No SecureChannel , connection may have been canceled abruptly by server")
);
}
if (
this._internalState !== "connected" &&
this._internalState !== "reconnecting_newchannel_connected" &&
this._internalState !== "connecting" &&
this._internalState !== "reconnecting"
) {
return callback(
new Error(
"performMessageTransaction: Invalid client state = " +
this._internalState +
" while performing a transaction " +
request.schema.name
)
);
}
assert(this._secureChannel);
assert(request);
assert(request.requestHeader);
assert(typeof callback === "function");
this._secureChannel.performMessageTransaction(request, callback as any);
}
/**
*
* 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: any[]): any {
if (args.length === 1) {
return this.getEndpoints({}, args[0]);
}
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) {
return callback(err);
}
this._serverEndpoints = [];
// istanbul ignore next
if (!response || !(response instanceof GetEndpointsResponse)) {
return callback(new Error("Internal Error"));
}
if (response && response.endpoints) {
this._serverEndpoints = response.endpoints;
}
callback(null, this._serverEndpoints);
});
}
/**
* @deprecated
*/
public getEndpointsRequest(options: GetEndpointsOptions, callback: ResponseCallback<EndpointDescription[]>): void {
warningLog("note: ClientBaseImpl#getEndpointsRequest is deprecated, use ClientBaseImpl#getEndpoints instead");
return 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: any[]): any {
if (args.length === 1) {
return this.findServers({}, args[0]);
}
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) {
return callback(err);
}
/* istanbul ignore next */
if (!response || !(response instanceof FindServersResponse)) {
return callback(new Error("Internal Error"));
}
response.servers = response.servers || [];
callback(null, response.servers);
});
}
public findServersOnNetwork(options?: FindServersOnNetworkRequestLike): Promise<ServerOnNetwork[]>;
public findServersOnNetwork(callback: ResponseCallback<ServerOnNetwork[]>): void;
public findServersOnNetwork(options: FindServersOnNetworkRequestLike, callback: ResponseCallback<ServerOnNetwork[]>): void;
public findServersOnNetwork(...args: any[]): any {
if (args.length === 1) {
return this.findServersOnNetwork({}, args[0]);
}
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) {
return callback(err);
}
/* istanbul ignore next */
if (!response || !(response instanceof FindServersOnNetworkResponse)) {
return new Error("Internal Error");
}
response.servers = response.servers || [];
callback(null, response.servers);
});
}
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?: CloseSessionResponse) => void
) {
assert(typeof callback === "function");
assert(typeof deleteSubscriptions === "boolean");
// istanbul ignore next
if (!this._secureChannel) {
return callback(null); // new Error("no channel"));
}
assert(this._secureChannel);
if (!this._secureChannel.isValid()) {
return callback(null);
}
debugLog(chalk.bgWhite.green("_closeSession ") + this._secureChannel!.channelId);
const request = new CloseSessionRequest({
deleteSubscriptions
});
session.performMessageTransaction(request, (err: Error | null, response?: Response) => {
if (err) {
callback(err);
} else {
callback(err, response as CloseSessionResponse);
}
});
}
public closeSession(...args: any[]): any {
const session = args[0] as ClientSessionImpl;
const deleteSubscriptions = args[1];
const callback = args[2];
assert(typeof deleteSubscriptions === "boolean");
assert(typeof callback === "function");
assert(session);
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?: Error | null, response?: CloseSessionResponse) => {
session.emitCloseEvent(StatusCodes.Good);
this._removeSession(session);
session.dispose();
assert(this._sessions.indexOf(session) === -1);
assert(session._closed, "session must indicate it is