• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

node-opcua / node-opcua / 25883546329

14 May 2026 08:22PM UTC coverage: 92.025% (-0.01%) from 92.037%
25883546329

push

github

erossignon
v2.172.1

18396 of 21698 branches covered (84.78%)

163847 of 178047 relevant lines covered (92.02%)

431832.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.46
/packages/node-opcua-client/source/private/client_base_impl.ts
1
/**
2✔
2
 * @module node-opcua-client-private
2✔
3
 */
2✔
4
// tslint:disable:no-unused-expression
2✔
5
import fs from "node:fs";
2✔
6
import path from "node:path";
2✔
7

2✔
8
import { withLock } from "@ster5/global-mutex";
2✔
9

2✔
10
import chalk from "chalk";
2✔
11
import { assert } from "node-opcua-assert";
2✔
12
import { getDefaultCertificateManager, type OPCUACertificateManager } from "node-opcua-certificate-manager";
2✔
13
import { type ICertificateStore, InMemoryCertificateStore, type IOPCUASecureObjectOptions, makeApplicationUrn, makeSubject, OPCUASecureObject } from "node-opcua-common";
2✔
14
import { type Certificate, makeSHA1Thumbprint, split_der } from "node-opcua-crypto/web";
2✔
15
import { installPeriodicClockAdjustment, periodicClockAdjustment, uninstallPeriodicClockAdjustment } from "node-opcua-date-time";
2✔
16
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
2✔
17
import { getHostname } from "node-opcua-hostname";
2✔
18
import type { IBasicTransportSettings, ResponseCallback } from "node-opcua-pseudo-session";
2✔
19
import {
2✔
20
    ClientSecureChannelLayer,
2✔
21
    type ConnectionStrategy,
2✔
22
    type ConnectionStrategyOptions,
2✔
23
    coerceConnectionStrategy,
2✔
24
    coerceSecurityPolicy,
2✔
25
    type PerformTransactionCallback,
2✔
26
    type Request as Request1,
2✔
27
    type Response as Response1,
2✔
28
    type SecurityPolicy
2✔
29
} from "node-opcua-secure-channel";
2✔
30
import type { IClientTransportFactory } from "node-opcua-transport";
2✔
31
import {
2✔
32
    FindServersOnNetworkRequest,
2✔
33
    type FindServersOnNetworkRequestOptions,
2✔
34
    FindServersOnNetworkResponse,
2✔
35
    FindServersRequest,
2✔
36
    FindServersResponse,
2✔
37
    type ServerOnNetwork
2✔
38
} from "node-opcua-service-discovery";
2✔
39
import {
2✔
40
    type ApplicationDescription,
2✔
41
    type EndpointDescription,
2✔
42
    GetEndpointsRequest,
2✔
43
    GetEndpointsResponse
2✔
44
} from "node-opcua-service-endpoints";
2✔
45
import { type ChannelSecurityToken, coerceMessageSecurityMode, MessageSecurityMode } from "node-opcua-service-secure-channel";
2✔
46
import { CloseSessionRequest, type CloseSessionResponse } from "node-opcua-service-session";
2✔
47
import { type ErrorCallback, StatusCodes } from "node-opcua-status-code";
2✔
48
import { checkFileExistsAndIsNotEmpty, matchUri } from "node-opcua-utils";
2✔
49
import {
2✔
50
    type CreateSecureChannelCallbackFunc,
2✔
51
    type FindEndpointCallback,
2✔
52
    type FindEndpointOptions,
2✔
53
    type FindEndpointResult,
2✔
54
    type FindServersOnNetworkRequestLike,
2✔
55
    type FindServersRequestLike,
2✔
56
    type GetEndpointsOptions,
2✔
57
    OPCUAClientBase,
2✔
58
    type OPCUAClientBaseEvents,
2✔
59
    type OPCUAClientBaseOptions,
2✔
60
    type TransportSettings
2✔
61
} from "../client_base";
2✔
62
import type { Request, Response } from "../common";
2✔
63
import type { UserIdentityInfo } from "../user_identity_info";
2✔
64
import { performCertificateSanityCheck } from "../verify";
2✔
65
import type { ClientSessionImpl } from "./client_session_impl";
2✔
66
import type { IClientBase } from "./i_private_client";
2✔
67

2✔
68
const debugLog = make_debugLog(__filename);
2✔
69
const doDebug = checkDebugFlag(__filename);
2✔
70
const errorLog = make_errorLog(__filename);
2✔
71
const warningLog = make_warningLog(__filename);
2✔
72

2✔
73
function makeCertificateThumbPrint(certificate: Certificate | Certificate[] | null | undefined): Buffer | null {
19✔
74
    if (!certificate) return null;
19✔
75
    return makeSHA1Thumbprint(Array.isArray(certificate) ? certificate[0] : certificate);
19!
76
}
19✔
77

2✔
78
const traceInternalState = false;
2✔
79

2✔
80
const defaultConnectionStrategy: ConnectionStrategyOptions = {
2✔
81
    initialDelay: 1000,
2✔
82
    maxDelay: 20 * 1000, // 20 seconds
2✔
83
    maxRetry: -1, // infinite
2✔
84
    randomisationFactor: 0.1
2✔
85
};
2✔
86

2✔
87
function __findEndpoint(this: ClientBaseImpl, endpointUrl: string, params: FindEndpointOptions, _callback: FindEndpointCallback) {
119✔
88
    if (this.isUnusable()) {
119!
89
        return _callback(new Error("Client is not usable"));
×
90
    }
×
91
    const masterClient = this as ClientBaseImpl;
119✔
92
    doDebug && debugLog("findEndpoint : endpointUrl = ", endpointUrl);
119!
93
    doDebug && debugLog(" params ", params);
119!
94
    assert(!masterClient._tmpClient);
119✔
95

119✔
96
    const callback = (err: Error | null, result?: FindEndpointResult) => {
119✔
97
        masterClient._tmpClient = undefined;
119✔
98
        _callback(err, result);
119✔
99
    };
119✔
100

119✔
101
    const securityMode = params.securityMode;
119✔
102
    const securityPolicy = params.securityPolicy;
119✔
103
    const _connectionStrategy = params.connectionStrategy;
119✔
104
    const options: OPCUAClientBaseOptions = {
119✔
105
        applicationName: params.applicationName,
119✔
106
        applicationUri: params.applicationUri,
119✔
107
        certificateFile: params.certificateFile,
119✔
108
        clientCertificateManager: params.clientCertificateManager,
119✔
109

119✔
110
        clientName: "EndpointFetcher",
119✔
111

119✔
112
        // use same connectionStrategy as parent
119✔
113
        connectionStrategy: params.connectionStrategy,
119✔
114
        // connectionStrategy: {
119✔
115
        //     maxRetry: 0 /* no- retry */,
119✔
116
        //     maxDelay: 2000
119✔
117
        // },
119✔
118
        privateKeyFile: params.privateKeyFile
119✔
119
    };
119✔
120

119✔
121
    const client = new TmpClient(options);
119✔
122
    masterClient._tmpClient = client;
119✔
123

119✔
124
    let selectedEndpoint: EndpointDescription | undefined;
119✔
125
    const allEndpoints: EndpointDescription[] = [];
119✔
126
    const step1_connect = (): Promise<void> => {
119✔
127
        return new Promise<void>((resolve, reject) => {
119✔
128
            // rebind backoff handler
119✔
129
            masterClient.listeners("backoff").forEach((handler) => {
119✔
130
                client.on("backoff", handler as unknown as (retryCount: number, delay: number) => void);
107✔
131
            });
119✔
132

119✔
133
            // c8 ignore next
119✔
134
            if (doDebug) {
119!
135
                client.on("backoff", (retryCount: number, delay: number) => {
×
136
                    debugLog(
×
137
                        "finding Endpoint => reconnecting ",
×
138
                        " retry count",
×
139
                        retryCount,
×
140
                        " next attempt in ",
×
141
                        delay / 1000,
×
142
                        "seconds"
×
143
                    );
×
144
                });
×
145
            }
×
146

119✔
147
            client.connect(endpointUrl, (err?: Error) => {
119✔
148
                if (err) {
119✔
149
                    err.message =
2✔
150
                        "Fail to connect to server at " +
2✔
151
                        endpointUrl +
2✔
152
                        " to collect server's certificate (in findEndpoint) \n" +
2✔
153
                        " (err =" +
2✔
154
                        err.message +
2✔
155
                        ")";
2✔
156
                    warningLog(err.message);
2✔
157
                    return reject(err);
2✔
158
                }
2✔
159
                resolve();
117✔
160
            });
119✔
161
        });
119✔
162
    };
119✔
163

119✔
164
    const step2_getEndpoints = (): Promise<void> => {
119✔
165
        return new Promise<void>((resolve, reject) => {
117✔
166
            client.getEndpoints((err: Error | null, endpoints?: EndpointDescription[]) => {
117✔
167
                if (err) {
117!
168
                    err.message = `error in getEndpoints \n${err.message}`;
×
169
                    return reject(err);
×
170
                }
×
171
                // c8 ignore next
117✔
172
                if (!endpoints) {
117!
173
                    return reject(new Error("Internal Error"));
×
174
                }
×
175

117✔
176
                for (const endpoint of endpoints) {
117✔
177
                    if (endpoint.securityMode === securityMode && endpoint.securityPolicyUri === securityPolicy) {
914✔
178
                        if (selectedEndpoint) {
117!
179
                            errorLog(
×
180
                                "Warning more than one endpoint matching !",
×
181
                                endpoint.endpointUrl,
×
182
                                selectedEndpoint.endpointUrl
×
183
                            );
×
184
                        }
×
185
                        selectedEndpoint = endpoint; // found it
117✔
186
                    }
117✔
187
                }
914✔
188
                resolve();
117✔
189
            });
117✔
190
        });
117✔
191
    };
119✔
192

119✔
193
    const step3_disconnect = (): Promise<void> => {
119✔
194
        return new Promise<void>((resolve) => {
117✔
195
            client.disconnect(() => resolve());
117✔
196
        });
117✔
197
    };
119✔
198

119✔
199
    step1_connect()
119✔
200
        .then(() => step2_getEndpoints())
119✔
201
        .then(() => step3_disconnect())
119✔
202
        .then(() => {
119✔
203
            if (!selectedEndpoint) {
117!
204
                return callback(
×
205
                    new Error(
×
206
                        "Cannot find an Endpoint matching " +
×
207
                        " security mode: " +
×
208
                        securityMode.toString() +
×
209
                        " policy: " +
×
210
                        securityPolicy.toString()
×
211
                    )
×
212
                );
×
213
            }
×
214

117✔
215
            // c8 ignore next
117✔
216
            if (doDebug) {
117!
217
                debugLog(chalk.bgWhite.red("xxxxxxxxxxxxxxxxxxxxx => selected EndPoint = "), selectedEndpoint.toString());
×
218
            }
×
219

117✔
220
            callback(null, { endpoints: allEndpoints, selectedEndpoint });
117✔
221
        })
119✔
222
        .catch((err) => {
119✔
223
            client.disconnect(() => {
2✔
224
                callback(err);
2✔
225
            });
2✔
226
        });
119✔
227
}
119✔
228

2✔
229
/**
2✔
230
 * check if certificate is trusted or untrusted
2✔
231
 */
2✔
232
async function _verify_serverCertificate(certificateManager: ICertificateStore, serverCertificate: Certificate) {
117✔
233
    const status = await certificateManager.checkCertificate(serverCertificate);
117✔
234
    if (!status.isGood()) {
117✔
235
        // c8 ignore next
1✔
236
        if (doDebug) {
1!
237
            // do it again for debug purposes
×
238
            const status1 = await certificateManager.verifyCertificate(serverCertificate);
×
239
            debugLog(status1);
×
240
        }
×
241
        warningLog("serverCertificate = ", makeCertificateThumbPrint(serverCertificate)?.toString("hex") || "none");
1!
242
        warningLog("serverCertificate = ", serverCertificate.toString("base64"));
1✔
243
        throw new Error(`server Certificate verification failed with err ${status?.toString()}`);
1✔
244
    }
1✔
245
}
117✔
246

2✔
247
const forceEndpointDiscoveryOnConnect = !!parseInt(process.env.NODEOPCUA_CLIENT_FORCE_ENDPOINT_DISCOVERY || "0", 10);
2✔
248
debugLog("forceEndpointDiscoveryOnConnect = ", forceEndpointDiscoveryOnConnect);
2✔
249

2✔
250
class ClockAdjustment {
2✔
251
    constructor() {
2✔
252
        debugLog("installPeriodicClockAdjustment ", periodicClockAdjustment.timerInstallationCount);
1,349✔
253
        installPeriodicClockAdjustment();
1,349✔
254
    }
1,349✔
255
    dispose() {
2✔
256
        uninstallPeriodicClockAdjustment();
1,345✔
257
        debugLog("uninstallPeriodicClockAdjustment ", periodicClockAdjustment.timerInstallationCount);
1,345✔
258
    }
1,345✔
259
}
2✔
260

2✔
261
type InternalClientState =
2✔
262
    | "uninitialized"
2✔
263
    | "disconnected"
2✔
264
    | "connecting"
2✔
265
    | "connected"
2✔
266
    | "panic"
2✔
267
    | "reconnecting"
2✔
268
    | "reconnecting_newchannel_connected"
2✔
269
    | "disconnecting";
2✔
270

2✔
271
/*
2✔
272
 *    "disconnected"  ---[connect]----------------------> "connecting"
2✔
273
 *
2✔
274
 *    "connecting"    ---[(connection successful)]------> "connected"
2✔
275
 *
2✔
276
 *    "connecting"    ---[(connection failure)]---------> "disconnected"
2✔
277
 *
2✔
278
 *    "connecting"    ---[disconnect]-------------------> "disconnecting" --> "disconnected"
2✔
279
 *
2✔
280
 *    "connecting"    ---[lost of connection]-----------> "reconnecting" ->[reconnection]
2✔
281
 *
2✔
282
 *    "reconnecting"  ---[reconnection successful]------> "reconnecting_newchannel_connected"
2✔
283
 *
2✔
284
 *    "reconnecting_newchannel_connected" --(session failure) -->"reconnecting"
2✔
285
 *
2✔
286
 *    "reconnecting"  ---[reconnection failure]---------> [reconnection] ---> "reconnecting"
2✔
287
 *
2✔
288
 *    "reconnecting"  ---[disconnect]-------------------> "disconnecting" --> "disconnected"
2✔
289
 */
2✔
290

2✔
291
let g_ClientCounter = 0;
2✔
292

2✔
293
/**
2✔
294
 * @internal
2✔
295
 */
2✔
296
// tslint:disable-next-line: max-classes-per-file
2✔
297
export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBaseEvents>
2✔
298
    extends OPCUASecureObject<Events>
2✔
299
    implements OPCUAClientBase<Events>, IClientBase {
2!
300
    /**
1✔
301
     * total number of requests that been canceled due to timeout
1✔
302
     */
1✔
303
    public get timedOutRequestCount(): number {
1✔
304
        return this._timedOutRequestCount + (this._secureChannel ? this._secureChannel.timedOutRequestCount : 0);
3✔
305
    }
3✔
306

1✔
307
    /**
1✔
308
     * total number of transactions performed by the client
1✔
309
   x  */
1✔
310
    public get transactionsPerformed(): number {
1✔
311
        return this._transactionsPerformed + (this._secureChannel ? this._secureChannel.transactionsPerformed : 0);
16✔
312
    }
16✔
313

1✔
314
    /**
1✔
315
     * is true when the client has already requested the server end points.
1✔
316
     */
1✔
317
    get knowsServerEndpoint(): boolean {
1✔
318
        return this._serverEndpoints && this._serverEndpoints.length > 0;
2,545✔
319
    }
2,545✔
320

1✔
321
    /**
1✔
322
     * true if the client is trying to reconnect to the server after a connection break.
1✔
323
     */
1✔
324
    get isReconnecting(): boolean {
1✔
325
        return (
1,943✔
326
            !!this._secureChannel?.isConnecting ||
1,943✔
327
            this._internalState === "reconnecting_newchannel_connected" ||
1,943✔
328
            this._internalState === "reconnecting"
1,721✔
329
        );
1,943✔
330
    }
1,943✔
331

1✔
332
    /**
1✔
333
     * true if the connection strategy is set to automatically try to reconnect in case of failure
1✔
334
     */
1✔
335
    get reconnectOnFailure(): boolean {
1✔
336
        return this.connectionStrategy.maxRetry > 0 || this.connectionStrategy.maxRetry === -1;
98✔
337
    }
98✔
338

1✔
339
    /**
1✔
340
     * total number of bytes read by the client
1✔
341
     */
1✔
342
    get bytesRead(): number {
1✔
343
        return this._byteRead + (this._secureChannel ? this._secureChannel.bytesRead : 0);
6✔
344
    }
6✔
345

1✔
346
    /**
1✔
347
     * total number of bytes written by the client
1✔
348
     */
1✔
349
    public get bytesWritten(): number {
1✔
350
        return this._byteWritten + (this._secureChannel ? this._secureChannel.bytesWritten : 0);
5✔
351
    }
5✔
352

1✔
353
    public securityMode: MessageSecurityMode;
1✔
354
    public securityPolicy: SecurityPolicy;
1✔
355
    public serverCertificate?: Certificate | Certificate[];
1✔
356
    public clientName: string;
1✔
357
    public protocolVersion: 0;
1✔
358
    public defaultSecureTokenLifetime: number;
1✔
359
    public tokenRenewalInterval: number;
1✔
360
    public connectionStrategy: ConnectionStrategy;
1✔
361
    public keepPendingSessionsOnDisconnect: boolean;
1✔
362
    public endpointUrl: string;
1✔
363
    public discoveryUrl: string;
1✔
364
    public readonly applicationName: string;
1✔
365
    private _applicationUri: string;
1✔
366
    public defaultTransactionTimeout?: number;
1✔
367

1✔
368
    /**
1✔
369
     * true if session shall periodically probe the server to keep the session alive and prevent timeout
1✔
370
     */
1✔
371
    public keepSessionAlive: boolean;
1✔
372
    public readonly keepAliveInterval?: number;
1✔
373

1✔
374
    public _sessions: ClientSessionImpl[];
1✔
375
    protected _serverEndpoints: EndpointDescription[];
1✔
376
    public _secureChannel: ClientSecureChannelLayer | null;
1✔
377

1✔
378
    // statistics...
1✔
379
    private _byteRead: number;
1✔
380
    private _byteWritten: number;
1✔
381

1✔
382
    private _timedOutRequestCount: number;
1✔
383

1✔
384
    private _transactionsPerformed: number;
1✔
385
    private _reconnectionIsCanceled: boolean;
1✔
386
    private _clockAdjuster?: ClockAdjustment;
1✔
387
    protected _tmpClient?: OPCUAClientBase;
1✔
388
    private _instanceNumber: number;
1✔
389
    private _transportSettings: TransportSettings;
1✔
390
    private _transportTimeout?: number;
1✔
391
    private _transportFactory?: IClientTransportFactory;
1✔
392

1✔
393
    public clientCertificateManager: ICertificateStore;
1✔
394

1✔
395
    public isUnusable() {
1✔
396
        return (
2,470✔
397
            this._internalState === "disconnected" ||
2,470✔
398
            this._internalState === "disconnecting" ||
2,470✔
399
            this._internalState === "panic" ||
2,470✔
400
            this._internalState === "uninitialized"
2,452✔
401
        );
2,470✔
402
    }
2,470✔
403

1✔
404
    protected _setInternalState(internalState: InternalClientState): void {
1✔
405
        const previousState = this._internalState;
9,572✔
406
        if (doDebug || traceInternalState) {
9,572✔
407
            (traceInternalState ? warningLog : debugLog)(
×
408
                chalk.cyan(`  Client ${this._instanceNumber} ${this.clientName} : _internalState from    `),
×
409
                chalk.yellow(previousState),
×
410
                "to",
×
411
                chalk.yellow(internalState)
×
412
            );
×
413
        }
×
414
        if (this._internalState === "disconnecting" || this._internalState === "disconnected") {
9,572✔
415
            if (internalState === "reconnecting") {
2,667✔
416
                errorLog("Internal error, cannot switch to reconnecting when already disconnecting");
×
417
            } // when disconnecting, we cannot accept any other state
2,667✔
418
        }
2,667✔
419

9,572✔
420
        this._internalState = internalState;
9,572✔
421
    }
9,572✔
422
    public emit<K>(eventName: K, ...others: unknown[]): boolean {
1✔
423
        // c8 ignore next
190,448✔
424
        if (doDebug) {
190,448✔
425
            debugLog(
×
426
                chalk.cyan(`  Client ${this._instanceNumber} ${this.clientName} emitting `),
×
427
                chalk.magentaBright(eventName as string)
×
428
            );
×
429
        }
×
430
        // @ts-expect-error
190,448✔
431
        return super.emit(eventName, ...others);
190,448✔
432
    }
190,448✔
433
    constructor(options?: OPCUAClientBaseOptions) {
1✔
434
        options = options || {};
1,480✔
435

1,480✔
436
        if (options.certificateKeyPairProvider) {
1,480✔
437
            // In-memory path — use a lightweight in-memory store
×
438
            // when no cert manager was explicitly provided.
×
439
            if (!options.clientCertificateManager) {
×
440
                options.clientCertificateManager = new InMemoryCertificateStore();
×
441
            }
×
442
            super(options as IOPCUASecureObjectOptions);
×
443
        } else {
1,480✔
444
            // Disk path — derive cert/key paths from certificate manager
1,480✔
445
            if (!options.clientCertificateManager) {
1,480✔
446
                options.clientCertificateManager = getDefaultCertificateManager("PKI");
635✔
447
            }
635✔
448
            const cm = options.clientCertificateManager as OPCUACertificateManager;
1,480✔
449
            options.privateKeyFile = options.privateKeyFile || cm.privateKey;
1,480✔
450
            options.certificateFile =
1,480✔
451
                options.certificateFile || path.join(cm.rootDir, "own/certs/client_certificate.pem");
1,480✔
452
            super(options as IOPCUASecureObjectOptions);
1,480✔
453
        }
1,480✔
454

1,480✔
455
        this._setInternalState("uninitialized");
1,480✔
456

1,480✔
457
        this._instanceNumber = g_ClientCounter++;
1,480✔
458

1,480✔
459
        this.applicationName = options.applicationName || "NodeOPCUA-Client";
1,480✔
460
        assert(!this.applicationName.match(/^locale=/), "applicationName badly converted from LocalizedText");
1,480✔
461
        assert(!this.applicationName.match(/urn:/), "applicationName should not be a URI");
1,480✔
462

1,480✔
463
        // we need to delay _applicationUri initialization
1,480✔
464
        this._applicationUri = options.applicationUri || this._getBuiltApplicationUri();
1,480✔
465

1,480✔
466
        this.clientCertificateManager = options.clientCertificateManager!;
1,480✔
467
        this.clientCertificateManager.referenceCounter++;
1,480✔
468

1,480✔
469
        this._secureChannel = null;
1,480✔
470

1,480✔
471
        this._reconnectionIsCanceled = false;
1,480✔
472

1,480✔
473
        this.endpointUrl = "";
1,480✔
474

1,480✔
475
        this.clientName = options.clientName || "ClientSession";
1,480✔
476

1,480✔
477
        // must be ZERO with Spec 1.0.2
1,480✔
478
        this.protocolVersion = 0;
1,480✔
479

1,480✔
480
        this._sessions = [];
1,480✔
481

1,480✔
482
        this._serverEndpoints = [];
1,480✔
483

1,480✔
484
        this.defaultSecureTokenLifetime = options.defaultSecureTokenLifetime || 600000;
1,480✔
485

1,480✔
486
        this.defaultTransactionTimeout = options.defaultTransactionTimeout;
1,480✔
487

1,480✔
488
        this.tokenRenewalInterval = options.tokenRenewalInterval || 0;
1,480✔
489
        assert(Number.isFinite(this.tokenRenewalInterval) && this.tokenRenewalInterval >= 0);
1,480✔
490
        this.securityMode = coerceMessageSecurityMode(options.securityMode);
1,480✔
491
        this.securityPolicy = coerceSecurityPolicy(options.securityPolicy);
1,480✔
492

1,480✔
493
        this.serverCertificate = options.serverCertificate;
1,480✔
494

1,480✔
495
        this.keepSessionAlive = typeof options.keepSessionAlive === "boolean" ? options.keepSessionAlive : false;
1,480✔
496
        this.keepAliveInterval = options.keepAliveInterval;
1,480✔
497

1,480✔
498
        // statistics...
1,480✔
499
        this._byteRead = 0;
1,480✔
500
        this._byteWritten = 0;
1,480✔
501
        this._transactionsPerformed = 0;
1,480✔
502
        this._timedOutRequestCount = 0;
1,480✔
503

1,480✔
504
        this.connectionStrategy = coerceConnectionStrategy(options.connectionStrategy || defaultConnectionStrategy);
1,480✔
505

1,480✔
506
        /***
1,480✔
507
         * @property keepPendingSessionsOnDisconnect²
1,480✔
508
         * @type {boolean}
1,480✔
509
         */
1,480✔
510
        this.keepPendingSessionsOnDisconnect = options.keepPendingSessionsOnDisconnect || false;
1,480✔
511

1,480✔
512
        this.discoveryUrl = options.discoveryUrl || "";
1,480✔
513

1,480✔
514
        this._setInternalState("disconnected");
1,480✔
515

1,480✔
516
        this._transportSettings = options.transportSettings || {};
1,480✔
517
        this._transportTimeout = options.transportTimeout;
1,480✔
518
        this._transportFactory = options.transportFactory;
1,480✔
519
    }
1,480✔
520

1✔
521
    private _cancel_reconnection(callback: ErrorCallback) {
1✔
522
        // _cancel_reconnection is invoked during disconnection
×
523
        // when we detect that a reconnection is in progress...
×
524

×
525
        // c8 ignore next
×
526
        if (!this.isReconnecting) {
×
527
            warningLog("internal error: _cancel_reconnection should only be used when reconnecting is in progress");
×
528
        }
×
529

×
530
        debugLog("canceling reconnection : ", this.clientName);
×
531

×
532
        this._reconnectionIsCanceled = true;
×
533

×
534
        // c8 ignore next
×
535
        if (!this._secureChannel) {
×
536
            debugLog("_cancel_reconnection:  Nothing to do for !", this.clientName, " because secure channel doesn't exist");
×
537
            return callback(); // nothing to do
×
538
        }
×
539

×
540
        this._secureChannel.abortConnection((/*err?: Error*/) => {
×
541
            this._secureChannel = null;
×
542
            callback();
×
543
        });
×
544
    }
×
545

1✔
546
    public _recreate_secure_channel(callback: ErrorCallback): void {
1✔
547
        debugLog("_recreate_secure_channel... while internalState is", this._internalState);
38✔
548

38✔
549
        if (!this.knowsServerEndpoint) {
38✔
550
            debugLog("Cannot reconnect , server endpoint is unknown");
×
551
            callback(new Error("Cannot reconnect, server endpoint is unknown - this.knowsServerEndpoint = false"));
×
552
            return;
×
553
        }
×
554
        assert(this.knowsServerEndpoint);
38✔
555

38✔
556
        this._setInternalState("reconnecting");
38✔
557
        this.emit("start_reconnection"); // send after callback
38✔
558

38✔
559
        const infiniteConnectionRetry: ConnectionStrategyOptions = {
38✔
560
            initialDelay: this.connectionStrategy.initialDelay,
38✔
561
            maxDelay: this.connectionStrategy.maxDelay,
38✔
562
            maxRetry: -1
38✔
563
        };
38✔
564

38✔
565
        const _when_internal_error = (err: Error, callback: ErrorCallback) => {
38✔
566
            errorLog("INTERNAL ERROR", err.message);
×
567
            callback(err);
×
568
        };
38✔
569

38✔
570
        const _when_reconnectionIsCanceled = (callback: ErrorCallback) => {
38✔
571
            doDebug && debugLog("attempt to recreate a new secure channel : suspended because reconnection is canceled !");
2✔
572
            this.emit("reconnection_canceled");
2✔
573
            return callback(new Error(`Reconnection has been canceled - ${this.clientName}`));
2✔
574
        };
38✔
575

38✔
576
        const _failAndRetry = (err: Error, message: string, callback: ErrorCallback) => {
38✔
577
            debugLog("failAndRetry; ", message);
2✔
578
            if (this._reconnectionIsCanceled) {
2✔
579
                return _when_reconnectionIsCanceled(callback);
2✔
580
            }
2✔
581
            this._destroy_secure_channel();
✔
582
            warningLog("client = ", this.clientName, message, err.message);
×
583
            // else
×
584
            // let retry a little bit later
×
585
            this.emit("reconnection_attempt_has_failed", err, message); // send after callback
×
586
            setImmediate(_attempt_to_recreate_secure_channel, callback);
×
587
        };
38✔
588

38✔
589
        const _when_connected = (callback: ErrorCallback) => {
38✔
590
            this.emit("after_reconnection", null); // send after callback
36✔
591
            assert(this._secureChannel, "expecting a secureChannel here ");
36✔
592
            // a new channel has be created and a new connection is established
36✔
593
            debugLog(chalk.bgWhite.red("ClientBaseImpl:  RECONNECTED                !!!"));
36✔
594
            this._setInternalState("reconnecting_newchannel_connected");
36✔
595
            return callback();
36✔
596
        };
38✔
597

38✔
598
        const _attempt_to_recreate_secure_channel = (callback: ErrorCallback) => {
38✔
599
            debugLog("attempt to recreate a new secure channel");
38✔
600
            if (this._reconnectionIsCanceled) {
38✔
601
                return _when_reconnectionIsCanceled(callback);
×
602
            }
×
603
            if (this._secureChannel) {
38✔
604
                doDebug && debugLog("attempt to recreate a new secure channel, while channel already exists");
×
605
                // are we reentrant ?
×
606
                const err = new Error("_internal_create_secure_channel failed, this._secureChannel is supposed to be null");
×
607
                return _when_internal_error(err, callback);
×
608
            }
×
609
            assert(!this._secureChannel, "_attempt_to_recreate_secure_channel,  expecting this._secureChannel not to exist");
38✔
610

38✔
611
            this._internal_create_secure_channel(infiniteConnectionRetry, (err?: Error | null) => {
38✔
612
                if (err) {
38✔
613
                    // c8 ignore next
6✔
614
                    if (this._secureChannel) {
6✔
615
                        const err = new Error("_internal_create_secure_channel failed, expecting this._secureChannel not to exist");
×
616
                        return _when_internal_error(err, callback);
×
617
                    }
×
618

6✔
619
                    if (err.message.match(/ECONNREFUSED|ECONNABORTED|ETIMEDOUT/)) {
6✔
620
                        return _failAndRetry(
×
621
                            err,
×
622
                            `create secure channel failed with ECONNREFUSED|ECONNABORTED|ETIMEDOUT\n${err.message}`,
×
623
                            callback
×
624
                        );
×
625
                    }
×
626

6✔
627
                    if (err.message.match("Backoff aborted.")) {
6✔
628
                        _failAndRetry(err, "cannot create secure channel (backoff aborted)", callback);
×
629
                        return;
×
630
                    }
×
631

6✔
632
                    if (
6✔
633
                        err?.message.match("BadCertificateInvalid") ||
6✔
634
                        err?.message.match(/socket has been disconnected by third party/)
6✔
635
                    ) {
6✔
636
                        // it is possible also that hte server has shutdown innapropriately the connection
4✔
637
                        warningLog(
4✔
638
                            "the server certificate has changed,  we need to retrieve server certificate again: ",
4✔
639
                            err.message
4✔
640
                        );
4✔
641
                        const oldServerCertificate = this.serverCertificate;
4✔
642
                        warningLog(
4✔
643
                            "old server certificate ",
4✔
644
                            makeCertificateThumbPrint(oldServerCertificate)?.toString("hex") || "undefined"
4✔
645
                        );
4✔
646
                        // the server may have shut down the channel because its certificate
4✔
647
                        // has changed ....
4✔
648
                        // let request the server certificate again ....
4✔
649
                        return this.fetchServerCertificate(this.endpointUrl, (err1?: Error | null) => {
4✔
650
                            if (err1) {
4✔
651
                                return _failAndRetry(err1, "Failing to fetch new server certificate", callback);
×
652
                            }
×
653
                            const newServerCertificate = this.serverCertificate;
4✔
654
                            warningLog(
4✔
655
                                "new server certificate ",
4✔
656
                                makeCertificateThumbPrint(newServerCertificate)?.toString("hex") || "none"
4✔
657
                            );
4✔
658

4✔
659
                            const sha1Old = makeCertificateThumbPrint(oldServerCertificate)?.toString("hex") || null;
4✔
660
                            const sha1New = makeCertificateThumbPrint(newServerCertificate)?.toString("hex") || null;
4✔
661
                            if (sha1Old === sha1New) {
4✔
662
                                warningLog("server certificate has not changed, but was expected to have changed");
×
663
                                return _failAndRetry(
×
664
                                    new Error("Server Certificate not changed"),
×
665
                                    "Failing to fetch new server certificate",
×
666
                                    callback
×
667
                                );
×
668
                            }
×
669
                            this._internal_create_secure_channel(infiniteConnectionRetry, (err3?: Error | null) => {
4✔
670
                                if (err3) {
4✔
671
                                    return _failAndRetry(err3, "trying to create new channel with new certificate", callback);
×
672
                                }
×
673
                                return _when_connected(callback);
4✔
674
                            });
4✔
675
                        });
4✔
676
                    } else {
6✔
677
                        return _failAndRetry(err, "cannot create secure channel", callback);
2✔
678
                    }
2✔
679
                } else {
38✔
680
                    return _when_connected(callback);
32✔
681
                }
32✔
682
            });
38✔
683
        };
38✔
684

38✔
685
        // create a secure channel
38✔
686
        // a new secure channel must be established
38✔
687
        _attempt_to_recreate_secure_channel(callback);
38✔
688
    }
38✔
689

1✔
690
    public _internal_create_secure_channel(
1✔
691
        connectionStrategy: ConnectionStrategyOptions,
1,391✔
692
        callback: CreateSecureChannelCallbackFunc
1,391✔
693
    ): void {
1,391✔
694
        assert(this._secureChannel === null);
1,391✔
695
        assert(typeof this.endpointUrl === "string");
1,391✔
696

1,391✔
697
        debugLog(
1,391✔
698
            "_internal_create_secure_channel creating new ClientSecureChannelLayer _internalState =",
1,391✔
699
            this._internalState,
1,391✔
700
            this.clientName
1,391✔
701
        );
1,391✔
702
        const secureChannel = new ClientSecureChannelLayer({
1,391✔
703
            connectionStrategy,
1,391✔
704
            defaultSecureTokenLifetime: this.defaultSecureTokenLifetime,
1,391✔
705
            parent: this,
1,391✔
706
            securityMode: this.securityMode,
1,391✔
707
            securityPolicy: this.securityPolicy,
1,391✔
708
            serverCertificate: this.serverCertificate,
1,391✔
709
            tokenRenewalInterval: this.tokenRenewalInterval,
1,391✔
710
            transportSettings: this._transportSettings,
1,391✔
711
            transportTimeout: this._transportTimeout,
1,391✔
712
            transportFactory: this._transportFactory,
1,391✔
713
            defaultTransactionTimeout: this.defaultTransactionTimeout
1,391✔
714
        });
1,391✔
715
        secureChannel.on("backoff", (count: number, delay: number) => {
1,391✔
716
            this.emit("backoff", count, delay);
460✔
717
        });
1,391✔
718

1,391✔
719
        secureChannel.on("abort", () => {
1,391✔
720
            this.emit("abort");
13✔
721
        });
1,391✔
722

1,391✔
723
        secureChannel.protocolVersion = this.protocolVersion;
1,391✔
724

1,391✔
725
        this._secureChannel = secureChannel;
1,391✔
726

1,391✔
727
        this.emit("secure_channel_created", secureChannel);
1,391✔
728

1,391✔
729
        const step2_openSecureChannel = (): Promise<void> => {
1,391✔
730
            return new Promise<void>((resolve, reject) => {
1,391✔
731
                debugLog("_internal_create_secure_channel before secureChannel.create");
1,391✔
732

1,391✔
733
                secureChannel.create(this.endpointUrl, (err?: Error) => {
1,391✔
734
                    debugLog("_internal_create_secure_channel after secureChannel.create");
1,391✔
735
                    if (!this._secureChannel) {
1,391✔
736
                        debugLog("_secureChannel has been closed during the transaction !");
14✔
737
                        return reject(new Error("Secure Channel Closed"));
14✔
738
                    }
14✔
739
                    if (err) {
1,391✔
740
                        return reject(err);
76✔
741
                    }
76✔
742
                    this._install_secure_channel_event_handlers(secureChannel);
1,301✔
743
                    resolve();
1,301✔
744
                });
1,391✔
745
            });
1,391✔
746
        };
1,391✔
747

1,391✔
748
        const step3_getEndpoints = (): Promise<void> => {
1,391✔
749
            return new Promise<void>((resolve, reject) => {
1,301✔
750
                assert(this._secureChannel !== null);
1,301✔
751
                if (!this.knowsServerEndpoint) {
1,301✔
752
                    this._setInternalState("connecting");
1,123✔
753

1,123✔
754
                    this.getEndpoints((err: Error | null /*, endpoints?: EndpointDescription[]*/) => {
1,123✔
755
                        if (!this._secureChannel) {
1,123✔
756
                            debugLog("_secureChannel has been closed during the transaction ! (while getEndpoints)");
3✔
757
                            return reject(new Error("Secure Channel Closed"));
3✔
758
                        }
3✔
759
                        if (err) {
1,123✔
760
                            return reject(err);
1✔
761
                        }
1✔
762
                        resolve();
1,119✔
763
                    });
1,123✔
764
                } else {
1,301✔
765
                    // end points are already known
178✔
766
                    resolve();
178✔
767
                }
178✔
768
            });
1,301✔
769
        };
1,391✔
770

1,391✔
771
        step2_openSecureChannel()
1,391✔
772
            .then(() => step3_getEndpoints())
1,391✔
773
            .then(() => {
1,391✔
774
                assert(this._secureChannel !== null);
1,297✔
775
                callback(null, secureChannel);
1,297✔
776
            })
1,391✔
777
            .catch((err) => {
1,391✔
778
                doDebug && debugLog(this.clientName, " : Inner create secure channel has failed", err.message);
94✔
779
                if (this._secureChannel) {
94✔
780
                    this._secureChannel.abortConnection(() => {
77✔
781
                        this._destroy_secure_channel();
77✔
782
                        callback(err);
77✔
783
                    });
77✔
784
                } else {
94✔
785
                    callback(err);
17✔
786
                }
17✔
787
            });
1,391✔
788
    }
1,391✔
789

1✔
790
    static async createCertificate(
1✔
791
        clientCertificateManager: ICertificateStore,
9✔
792
        certificateFile: string,
9✔
793
        applicationName: string,
9✔
794
        applicationUri: string
9✔
795
    ): Promise<void> {
9✔
796
        if (!fs.existsSync(certificateFile)) {
9✔
797
            const hostname = getHostname();
9✔
798
            // this.serverInfo.applicationUri!;
9✔
799
            // Cast is safe: createDefaultCertificate only calls
9✔
800
            // this in the disk path, where the manager is always
9✔
801
            // an OPCUACertificateManager.
9✔
802
            const cm = clientCertificateManager as unknown as OPCUACertificateManager;
9✔
803
            await cm.createSelfSignedCertificate({
9✔
804
                applicationUri,
9✔
805
                dns: [hostname],
9✔
806
                // ip: await getIpAddresses(),
9✔
807
                outputFile: certificateFile,
9✔
808
                subject: makeSubject(applicationName, hostname),
9✔
809
                startDate: new Date(),
9✔
810
                validity: 365 * 10 // 10 years
9✔
811
            });
9✔
812
        }
9✔
813
        // c8 ignore next
9✔
814
        if (!fs.existsSync(certificateFile)) {
9✔
815
            throw new Error(` cannot locate certificate file ${certificateFile}`);
×
816
        }
×
817
    }
9✔
818

1✔
819
    public async createDefaultCertificate(): Promise<void> {
1✔
820
        // c8 ignore next
1,468✔
821
        if ((this as unknown as { _inCreateDefaultCertificate: boolean })._inCreateDefaultCertificate) {
1,468✔
822
            errorLog("Internal error : re-entrancy in createDefaultCertificate!");
×
823
        }
×
824
        (this as unknown as { _inCreateDefaultCertificate: boolean })._inCreateDefaultCertificate = true;
1,468✔
825

1,468✔
826
        if (!checkFileExistsAndIsNotEmpty(this.certificateFile)) {
1,468✔
827
            await withLock({ fileToLock: `${this.certificateFile}.mutex` }, async () => {
18✔
828
                if (checkFileExistsAndIsNotEmpty(this.certificateFile)) {
18✔
829
                    // the file may have been created in between
9✔
830
                    return;
9✔
831
                }
9✔
832
                warningLog("Creating default certificate ... please wait");
9✔
833

9✔
834
                await ClientBaseImpl.createCertificate(
9✔
835
                    this.clientCertificateManager,
9✔
836
                    this.certificateFile,
9✔
837
                    this.applicationName,
9✔
838
                    this._getBuiltApplicationUri()
9✔
839
                );
9✔
840
                debugLog("privateKey      = ", this.privateKeyFile);
9✔
841
                if ("privateKey" in this.clientCertificateManager) {
9✔
842
                    debugLog("                = ", (this.clientCertificateManager as unknown as OPCUACertificateManager).privateKey);
9✔
843
                }
9✔
844
                debugLog("certificateFile = ", this.certificateFile);
9✔
845
                const _certificate = this.getCertificate();
9✔
846
                const _privateKey = this.getPrivateKey();
9✔
847
            });
18✔
848
        }
18✔
849
        (this as unknown as { _inCreateDefaultCertificate: boolean })._inCreateDefaultCertificate = false;
1,468✔
850
    }
1,468✔
851

1✔
852
    protected _getBuiltApplicationUri(): string {
1✔
853
        if (!this._applicationUri) {
2,341✔
854
            this._applicationUri = makeApplicationUrn(getHostname(), this.applicationName);
1,096✔
855
        }
1,096✔
856
        return this._applicationUri;
2,341✔
857
    }
2,341✔
858

1✔
859
    protected async initializeCM(): Promise<void> {
1✔
860
        if (!this.clientCertificateManager) {
1,243✔
861
            // this usually happen when the client  has been already disconnected,
×
862
            // disconnect
×
863
            errorLog(
×
864
                "[NODE-OPCUA-E08] initializeCM: clientCertificateManager is null\n" +
×
865
                "                 This happen when you disconnected the client, to free resources.\n" +
×
866
                "                 Please create a new OPCUAClient instance if you want to reconnect"
×
867
            );
×
868
            return;
×
869
        }
×
870
        await this.clientCertificateManager.initialize();
1,243✔
871

1,243✔
872
        if (this.certificateFile === "<in-memory>" || this.certificateFile === "<unknown>") {
1,243✔
873
            // In-memory provider — cert already available, skip disk operations
×
874
        } else {
1,243✔
875
            // Disk path — create default cert if missing
1,243✔
876
            await this.createDefaultCertificate();
1,243✔
877
            // c8 ignore next
1,243✔
878
            if (!fs.existsSync(this.privateKeyFile)) {
1,243✔
879
                throw new Error(` cannot locate private key file ${this.privateKeyFile}`);
×
880
            }
×
881
        }
1,243✔
882
        if (this.isUnusable()) return;
1,243✔
883

1,235✔
884
        // Note: do NOT wrap this in withLock2 — performCertificateSanityCheck
1,235✔
885
        // calls trustCertificate and verifyCertificate which each acquire
1,235✔
886
        // withLock2 internally, so an outer withLock2 would cause a deadlock
1,235✔
887
        // on the same file-based mutex.
1,235✔
888
        await performCertificateSanityCheck(this, "client", this.clientCertificateManager, this._getBuiltApplicationUri());
1,235✔
889
    }
1,235✔
890

1✔
891
    protected _internalState: InternalClientState = "uninitialized";
1✔
892

1✔
893
    protected _handleUnrecoverableConnectionFailure(err: Error, callback: ErrorCallback): void {
1✔
894
        debugLog(err.message);
93✔
895
        this.emit("connection_failed", err);
93✔
896
        this._setInternalState("disconnected");
93✔
897
        callback(err);
93✔
898
    }
93✔
899
    private _handleDisconnectionWhileConnecting(err: Error, callback: ErrorCallback) {
1✔
900
        debugLog(err.message);
10✔
901
        this.emit("connection_failed", err);
10✔
902
        this._setInternalState("disconnected");
10✔
903
        callback(err);
10✔
904
    }
10✔
905
    private _handleSuccessfulConnection(callback: ErrorCallback) {
1✔
906
        debugLog(" Connected successfully  to ", this.endpointUrl);
1,261✔
907
        this.emit("connected");
1,261✔
908
        this._setInternalState("connected");
1,261✔
909
        callback();
1,261✔
910
    }
1,261✔
911

1✔
912
    /**
1✔
913
     * connect the OPC-UA client to a server end point.
1✔
914
     */
1✔
915
    public connect(endpointUrl: string): Promise<void>;
1✔
916
    public connect(endpointUrl: string, callback: ErrorCallback): void;
1✔
917
    public connect(...args: unknown[]): Promise<void> | void {
1✔
918
        const endpointUrl = args[0];
1,247✔
919
        const callback = args[1] as ErrorCallback;
1,247✔
920
        assert(typeof callback === "function", "expecting a callback");
1,247✔
921
        if (typeof endpointUrl !== "string" || endpointUrl.length <= 0) {
1,247✔
922
            errorLog(`[NODE-OPCUA-E03] OPCUAClient#connect expects a valid endpoint : ${endpointUrl}`);
1✔
923
            callback(new Error("Invalid endpoint"));
1✔
924
            return;
1✔
925
        }
1✔
926
        assert(typeof endpointUrl === "string" && endpointUrl.length > 0);
1,247✔
927

1,247✔
928
        // c8 ignore next
1,247✔
929
        if (this._internalState !== "disconnected") {
1,247✔
930
            callback(new Error(`client#connect failed, as invalid internal state = ${this._internalState}`));
3✔
931
            return;
3✔
932
        }
3✔
933

1,243✔
934
        // prevent illegal call to connect
1,243✔
935
        if (this._secureChannel !== null) {
1,247✔
936
            setImmediate(() => callback(new Error("connect already called")));
×
937
            return;
×
938
        }
×
939

1,243✔
940
        this._setInternalState("connecting");
1,243✔
941

1,243✔
942
        this.initializeCM()
1,243✔
943
            .then(() => {
1,243✔
944
                debugLog("ClientBaseImpl#connect ", endpointUrl, this.clientName);
1,243✔
945
                if (this._internalState === "disconnecting" || this._internalState === "disconnected") {
1,243✔
946
                    return this._handleDisconnectionWhileConnecting(new Error("premature disconnection 1"), callback);
10✔
947
                }
10✔
948

1,233✔
949
                if (
1,233✔
950
                    !this.serverCertificate &&
1,233✔
951
                    (forceEndpointDiscoveryOnConnect || this.securityMode !== MessageSecurityMode.None)
936✔
952
                ) {
1,243✔
953
                    debugLog("Fetching certificates from endpoints");
115✔
954
                    this.fetchServerCertificate(endpointUrl, (err: Error | null, adjustedEndpointUrl?: string) => {
115✔
955
                        if (err) {
115✔
956
                            return this._handleUnrecoverableConnectionFailure(err, callback);
3✔
957
                        }
3✔
958
                        if (this.isUnusable()) {
115✔
959
                            return this._handleDisconnectionWhileConnecting(new Error("premature disconnection 2"), callback);
×
960
                        }
×
961
                        if (forceEndpointDiscoveryOnConnect) {
115✔
962
                            debugLog("connecting with adjusted endpoint : ", adjustedEndpointUrl, "  was =", endpointUrl);
×
963
                            this._connectStep2(adjustedEndpointUrl || "", callback);
×
964
                        } else {
115✔
965
                            debugLog("connecting with endpoint : ", endpointUrl);
112✔
966
                            this._connectStep2(endpointUrl, callback);
112✔
967
                        }
112✔
968
                    });
115✔
969
                } else {
1,243✔
970
                    this._connectStep2(endpointUrl, callback);
1,118✔
971
                }
1,118✔
972
            })
1,243✔
973
            .catch((err) => {
1,243✔
974
                return this._handleUnrecoverableConnectionFailure(err, callback);
×
975
            });
1,243✔
976
    }
1,243✔
977

1✔
978
    /**
1✔
979
     * @private
1✔
980
     */
1✔
981
    public _connectStep2(endpointUrl: string, callback: ErrorCallback): void {
1✔
982
        // prevent illegal call to connect
1,349✔
983
        assert(this._secureChannel === null);
1,349✔
984
        this.endpointUrl = endpointUrl;
1,349✔
985

1,349✔
986
        this._clockAdjuster = this._clockAdjuster || new ClockAdjustment();
1,349✔
987
        OPCUAClientBase.registry.register(this);
1,349✔
988

1,349✔
989
        debugLog("__connectStep2 ", this._internalState);
1,349✔
990

1,349✔
991
        this._internal_create_secure_channel(this.connectionStrategy, (err: Error | null) => {
1,349✔
992
            if (!err) {
1,349✔
993
                this._handleSuccessfulConnection(callback);
1,261✔
994
            } else {
1,349✔
995
                OPCUAClientBase.registry.unregister(this);
88✔
996
                if (this._clockAdjuster) {
88✔
997
                    this._clockAdjuster.dispose();
76✔
998
                    this._clockAdjuster = undefined;
76✔
999
                }
76✔
1000
                debugLog(chalk.red("SecureChannel creation has failed with error :", err.message));
88✔
1001
                if (err.message.match(/ECONNABORTED/)) {
88✔
1002
                    debugLog(chalk.yellow(`- The client cannot to :${endpointUrl}. Connection has been aborted.`));
×
1003
                    err = new Error("The connection has been aborted");
×
1004
                    this._handleUnrecoverableConnectionFailure(err, callback);
×
1005
                } else if (err.message.match(/ECONNREF/)) {
88✔
1006
                    debugLog(chalk.yellow(`- The client cannot to :${endpointUrl}. Server is not reachable.`));
5✔
1007
                    err = new Error(
5✔
1008
                        `The connection cannot be established with server ${endpointUrl} .\n` +
5✔
1009
                        "Please check that the server is up and running or your network configuration.\n" +
5✔
1010
                        "Err = (" +
5✔
1011
                        err.message +
5✔
1012
                        ")"
5✔
1013
                    );
5✔
1014
                    this._handleUnrecoverableConnectionFailure(err, callback);
5✔
1015
                } else if (err.message.match(/disconnecting/)) {
88✔
1016
                    /* */
×
1017
                    this._handleDisconnectionWhileConnecting(err, callback);
×
1018
                } else {
83✔
1019
                    err = new Error(`The connection may have been rejected by server,\n Err = (${err.message})`);
83✔
1020
                    this._handleUnrecoverableConnectionFailure(err, callback);
83✔
1021
                }
83✔
1022
            }
88✔
1023
        });
1,349✔
1024
    }
1,349✔
1025

1✔
1026
    public performMessageTransaction(request: Request, callback: ResponseCallback<Response>): void {
1✔
1027
        if (!this._secureChannel) {
45,999✔
1028
            // this may happen if the Server has closed the connection abruptly for some unknown reason
×
1029
            // or if the tcp connection has been broken.
×
1030
            callback(
×
1031
                new Error(
×
1032
                    "performMessageTransaction: No SecureChannel , connection may have been canceled abruptly by server" +
×
1033
                    " while performing " + request.schema.name
×
1034
                )
×
1035
            );
×
1036
            return;
×
1037
        }
×
1038
        if (
45,999✔
1039
            this._internalState !== "connected" &&
45,999✔
1040
            this._internalState !== "reconnecting_newchannel_connected" &&
45,999✔
1041
            this._internalState !== "connecting" &&
45,999✔
1042
            this._internalState !== "reconnecting"
×
1043
        ) {
45,999✔
1044
            callback(
×
1045
                new Error(
×
1046
                    "performMessageTransaction: Invalid client state = " +
×
1047
                    this._internalState +
×
1048
                    " while performing a transaction " +
×
1049
                    request.schema.name
×
1050
                )
×
1051
            );
×
1052
            return;
×
1053
        }
×
1054
        assert(this._secureChannel);
45,999✔
1055
        assert(request);
45,999✔
1056
        assert(request.requestHeader);
45,999✔
1057
        assert(typeof callback === "function");
45,999✔
1058
        this._secureChannel.performMessageTransaction(request, callback as unknown as PerformTransactionCallback);
45,999✔
1059
    }
45,999✔
1060

1✔
1061
    /**
1✔
1062
     *
1✔
1063
     * return the endpoint information matching  security mode and security policy.
1✔
1064

1✔
1065
     */
1✔
1066
    public findEndpointForSecurity(
1✔
1067
        securityMode: MessageSecurityMode,
37✔
1068
        securityPolicy: SecurityPolicy
37✔
1069
    ): EndpointDescription | undefined {
37✔
1070
        securityMode = coerceMessageSecurityMode(securityMode);
37✔
1071
        securityPolicy = coerceSecurityPolicy(securityPolicy);
37✔
1072
        assert(this.knowsServerEndpoint, "Server end point are not known yet");
37✔
1073
        return this._serverEndpoints.find((endpoint) => {
37✔
1074
            return endpoint.securityMode === securityMode && endpoint.securityPolicyUri === securityPolicy;
39✔
1075
        });
37✔
1076
    }
37✔
1077

1✔
1078
    /**
1✔
1079
     *
1✔
1080
     * return the endpoint information matching the specified url , security mode and security policy.
1✔
1081

1✔
1082
     */
1✔
1083
    public findEndpoint(
1✔
1084
        endpointUrl: string,
1,011✔
1085
        securityMode: MessageSecurityMode,
1,011✔
1086
        securityPolicy: SecurityPolicy
1,011✔
1087
    ): EndpointDescription | undefined {
1,011✔
1088
        assert(this.knowsServerEndpoint, "Server end point are not known yet");
1,011✔
1089
        if (!this._serverEndpoints || this._serverEndpoints.length === 0) {
1,011✔
1090
            return undefined;
×
1091
        }
×
1092
        return this._serverEndpoints.find((endpoint: EndpointDescription) => {
1,011✔
1093
            return (
1,730✔
1094
                matchUri(endpoint.endpointUrl, endpointUrl) &&
1,730✔
1095
                endpoint.securityMode === securityMode &&
1,730✔
1096
                endpoint.securityPolicyUri === securityPolicy
1,061✔
1097
            );
1,730✔
1098
        });
1,011✔
1099
    }
1,011✔
1100

1✔
1101
    public async getEndpoints(options?: GetEndpointsOptions): Promise<EndpointDescription[]>;
1✔
1102
    public getEndpoints(options: GetEndpointsOptions, callback: ResponseCallback<EndpointDescription[]>): void;
1✔
1103
    public getEndpoints(callback: ResponseCallback<EndpointDescription[]>): void;
1✔
1104
    public getEndpoints(...args: unknown[]): Promise<EndpointDescription[]> | undefined {
1✔
1105
        if (args.length === 1) {
2,666✔
1106
            this.getEndpoints({} as GetEndpointsOptions, args[0] as unknown as ResponseCallback<EndpointDescription[]>);
1,326✔
1107
            return;
1,326✔
1108
        }
1,326✔
1109
        const options = args[0] as GetEndpointsOptions;
1,340✔
1110
        const callback = args[1] as ResponseCallback<EndpointDescription[]>;
1,340✔
1111
        assert(typeof callback === "function");
1,340✔
1112

1,340✔
1113
        options.localeIds = options.localeIds || [];
2,666✔
1114
        options.profileUris = options.profileUris || [];
2,666✔
1115

2,666✔
1116
        const request = new GetEndpointsRequest({
2,666✔
1117
            endpointUrl: options.endpointUrl || this.endpointUrl,
2,666✔
1118
            localeIds: options.localeIds,
2,666✔
1119
            profileUris: options.profileUris,
2,666✔
1120
            requestHeader: {
2,666✔
1121
                auditEntryId: null
2,666✔
1122
            }
2,666✔
1123
        });
2,666✔
1124

2,666✔
1125
        this.performMessageTransaction(request, (err: Error | null, response?: Response) => {
2,666✔
1126
            if (err) {
1,340✔
1127
                callback(err);
5✔
1128
                return;
5✔
1129
            }
5✔
1130
            this._serverEndpoints = [];
1,335✔
1131
            // c8 ignore next
1,335✔
1132
            if (!response || !(response instanceof GetEndpointsResponse)) {
1,340✔
1133
                callback(new Error("Internal Error"));
×
1134
                return;
×
1135
            }
×
1136
            if (response?.endpoints) {
1,340✔
1137
                this._serverEndpoints = response.endpoints;
1,335✔
1138
            }
1,335✔
1139
            callback(null, this._serverEndpoints);
1,335✔
1140
        });
2,666✔
1141
        return;
2,666✔
1142
    }
2,666✔
1143

1✔
1144
    /**
1✔
1145
     * @deprecated
1✔
1146
     */
1✔
1147
    public getEndpointsRequest(options: GetEndpointsOptions, callback: ResponseCallback<EndpointDescription[]>): void {
1✔
1148
        warningLog("note: ClientBaseImpl#getEndpointsRequest is deprecated, use ClientBaseImpl#getEndpoints instead");
×
1149
        this.getEndpoints(options, callback);
×
1150
    }
×
1151

1✔
1152
    /**
1✔
1153

1✔
1154
     */
1✔
1155
    public findServers(options?: FindServersRequestLike): Promise<ApplicationDescription[]>;
1✔
1156
    public findServers(options: FindServersRequestLike, callback: ResponseCallback<ApplicationDescription[]>): void;
1✔
1157
    public findServers(callback: ResponseCallback<ApplicationDescription[]>): void;
1✔
1158
    public findServers(...args: unknown[]): Promise<ApplicationDescription[]> | undefined {
1✔
1159
        if (args.length === 1) {
27✔
1160
            if (typeof args[0] === "function") {
12✔
1161
                this.findServers({} as FindServersRequestLike, args[0] as ResponseCallback<ApplicationDescription[]>);
12✔
1162
                return;
12✔
1163
            }
12✔
1164
            throw new Error("Invalid arguments");
✔
1165
        }
×
1166
        const options = args[0] as FindServersRequestLike;
15✔
1167
        const callback = args[1] as ResponseCallback<ApplicationDescription[]>;
15✔
1168

15✔
1169
        const request = new FindServersRequest({
15✔
1170
            endpointUrl: options.endpointUrl || this.endpointUrl,
27✔
1171
            localeIds: options.localeIds || [],
27✔
1172
            serverUris: options.serverUris || []
27✔
1173
        });
27✔
1174

27✔
1175
        this.performMessageTransaction(request, (err: Error | null, response?: Response) => {
27✔
1176
            if (err) {
15✔
1177
                callback(err);
×
1178
                return;
×
1179
            }
×
1180
            /* c8 ignore next */
2✔
1181
            if (!response || !(response instanceof FindServersResponse)) {
2✔
1182
                callback(new Error("Internal Error"));
×
1183
                return;
×
1184
            }
×
1185
            response.servers = response.servers || [];
15✔
1186

15✔
1187
            callback(null, response.servers);
15✔
1188
        });
27✔
1189
        return undefined;
27✔
1190
    }
27✔
1191

1✔
1192
    public findServersOnNetwork(options?: FindServersOnNetworkRequestLike): Promise<ServerOnNetwork[]>;
1✔
1193
    public findServersOnNetwork(callback: ResponseCallback<ServerOnNetwork[]>): void;
1✔
1194
    public findServersOnNetwork(options: FindServersOnNetworkRequestLike, callback: ResponseCallback<ServerOnNetwork[]>): void;
1✔
1195
    public findServersOnNetwork(...args: unknown[]): Promise<ServerOnNetwork[]> | undefined {
1✔
1196
        if (args.length === 1) {
8✔
1197
            this.findServersOnNetwork({} as FindServersOnNetworkRequestOptions, args[0] as ResponseCallback<ServerOnNetwork[]>);
4✔
1198
            return undefined;
4✔
1199
        }
4✔
1200
        const options = args[0] as FindServersOnNetworkRequestOptions;
4✔
1201
        const callback = args[1] as ResponseCallback<ServerOnNetwork[]>;
4✔
1202
        const request = new FindServersOnNetworkRequest(options);
4✔
1203

4✔
1204
        this.performMessageTransaction(request, (err: Error | null, response?: Response) => {
4✔
1205
            if (err) {
4✔
1206
                callback(err);
×
1207
                return;
×
1208
            }
×
1209
            /* c8 ignore next */
2✔
1210
            if (!response || !(response instanceof FindServersOnNetworkResponse)) {
2✔
1211
                callback(new Error("Internal Error"));
×
1212
                return;
×
1213
            }
×
1214
            response.servers = response.servers || [];
4✔
1215
            callback(null, response.servers);
4✔
1216
        });
4✔
1217
        return undefined;
4✔
1218
    }
4✔
1219

1✔
1220
    public _removeSession(session: ClientSessionImpl): void {
1✔
1221
        const index = this._sessions.indexOf(session);
984✔
1222
        if (index >= 0) {
984✔
1223
            const _s = this._sessions.splice(index, 1)[0];
960✔
1224
            // assert(s === session);
960✔
1225
            // assert(session._client === this);
960✔
1226
            session._client = null;
960✔
1227
        }
960✔
1228
        assert(this._sessions.indexOf(session) === -1);
984✔
1229
    }
984✔
1230

1✔
1231
    private _closeSession(
1✔
1232
        session: ClientSessionImpl,
980✔
1233
        deleteSubscriptions: boolean,
980✔
1234
        callback: (err: Error | null, response?: CloseSessionResponse) => void
980✔
1235
    ) {
980✔
1236
        assert(typeof callback === "function");
980✔
1237
        assert(typeof deleteSubscriptions === "boolean");
980✔
1238

980✔
1239
        // c8 ignore next
980✔
1240
        if (!this._secureChannel) {
980✔
1241
            return callback(null); // new Error("no channel"));
10✔
1242
        }
10✔
1243
        assert(this._secureChannel);
970✔
1244
        if (!this._secureChannel.isValid()) {
980✔
1245
            return callback(null);
1✔
1246
        }
1✔
1247

969✔
1248
        debugLog(chalk.bgWhite.green("_closeSession ") + this._secureChannel.channelId);
969✔
1249

969✔
1250
        const request = new CloseSessionRequest({
969✔
1251
            deleteSubscriptions
969✔
1252
        });
969✔
1253

969✔
1254
        session.performMessageTransaction(request, (err: Error | null, response?: Response) => {
969✔
1255
            if (err) {
969✔
1256
                callback(err);
25✔
1257
            } else {
969✔
1258
                callback(err, response as CloseSessionResponse);
944✔
1259
            }
944✔
1260
        });
969✔
1261
    }
969✔
1262

1✔
1263
    public closeSession(session: ClientSessionImpl, deleteSubscriptions: boolean): Promise<void>;
1✔
1264
    public closeSession(session: ClientSessionImpl, deleteSubscriptions: boolean, callback: ErrorCallback): void;
1✔
1265
    public closeSession(session: ClientSessionImpl, deleteSubscriptions: boolean, callback?: ErrorCallback): Promise<void> | void {
1✔
1266
        assert(typeof deleteSubscriptions === "boolean");
980✔
1267
        assert(session);
980✔
1268
        assert(session._client === this, "session must be attached to this");
980✔
1269
        session._closed = true;
980✔
1270

980✔
1271
        // todo : send close session on secure channel
980✔
1272
        this._closeSession(session, deleteSubscriptions, (err?: Error | null, _response?: CloseSessionResponse) => {
980✔
1273
            session.emitCloseEvent(StatusCodes.Good);
980✔
1274

980✔
1275
            this._removeSession(session);
980✔
1276
            session.dispose();
980✔
1277

980✔
1278
            assert(this._sessions.indexOf(session) === -1);
980✔
1279
            assert(session._closed, "session must indicate it is closed");
980✔
1280

980✔
1281
            callback?.(err ? err : undefined);
980✔
1282
        });
980✔
1283
    }
980✔
1284

1✔
1285
    public disconnect(): Promise<void>;
1✔
1286
    public disconnect(callback: ErrorCallback): void;
1✔
1287
    // eslint-disable-next-line max-statements
1✔
1288
    public disconnect(...args: unknown[]): undefined | Promise<void> {
1✔
1289
        const callback = args[0] as ErrorCallback;
1,454✔
1290
        assert(typeof callback === "function", "expecting a callback function here");
1,454✔
1291
        this._reconnectionIsCanceled = true;
1,454✔
1292
        if (this._tmpClient) {
1,454✔
1293
            warningLog("disconnecting client while tmpClient exists", this._tmpClient.clientName);
2✔
1294
            this._tmpClient.disconnect((_err) => {
2✔
1295
                this._tmpClient = undefined;
2✔
1296
                // retry disconnect on main client
2✔
1297
                this.disconnect(callback);
2✔
1298
            });
2✔
1299
            return undefined;
2✔
1300
        }
2✔
1301
        if (this._internalState === "disconnected" || this._internalState === "disconnecting") {
1,454✔
1302
            if (this._internalState === "disconnecting") {
158✔
1303
                warningLog(
2✔
1304
                    "[NODE-OPCUA-W26] OPCUAClient#disconnect called while already disconnecting. clientName=",
2✔
1305
                    this.clientName
2✔
1306
                );
2✔
1307
            }
2✔
1308
            if (!this._reconnectionIsCanceled && this.isReconnecting) {
158✔
1309
                errorLog("Internal Error : _reconnectionIsCanceled should be true if isReconnecting is true");
×
1310
            }
×
1311
            callback();
158✔
1312
            return undefined;
158✔
1313
        }
158✔
1314
        debugLog("disconnecting client! (will set reconnectionIsCanceled to true");
1,294✔
1315
        this._reconnectionIsCanceled = true;
1,294✔
1316

1,294✔
1317
        debugLog("ClientBaseImpl#disconnect", this.endpointUrl);
1,294✔
1318

1,294✔
1319
        if (this.isReconnecting && !this._reconnectionIsCanceled) {
1,454✔
1320
            debugLog("ClientBaseImpl#disconnect called while reconnection is in progress");
×
1321
            // let's abort the reconnection process
×
1322
            this._cancel_reconnection((err?: Error) => {
×
1323
                debugLog("ClientBaseImpl#disconnect reconnection has been canceled", this.applicationName);
×
1324
                assert(!err, " why would this fail ?");
×
1325
                // sessions cannot be cancelled properly and must be discarded.
×
1326
                this.disconnect(callback);
×
1327
            });
×
1328
            return undefined;
×
1329
        }
×
1330

1,294✔
1331
        if (this._sessions.length && !this.keepPendingSessionsOnDisconnect) {
1,454✔
1332
            debugLog("warning : disconnection : closing pending sessions");
15✔
1333
            // disconnect has been called whereas living session exists
15✔
1334
            // we need to close them first .... (unless keepPendingSessionsOnDisconnect)
15✔
1335
            this._close_pending_sessions((/*err*/) => {
15✔
1336
                this.disconnect(callback);
15✔
1337
            });
15✔
1338
            return undefined;
15✔
1339
        }
15✔
1340

1,279✔
1341
        debugLog("Disconnecting !");
1,279✔
1342
        this._setInternalState("disconnecting");
1,279✔
1343
        if (this.clientCertificateManager) {
1,279✔
1344
            const tmp = this.clientCertificateManager;
1,279✔
1345
            // (this as any).clientCertificateManager = null;
1,279✔
1346
            tmp.dispose().catch((err: Error) => {
1,279✔
1347
                debugLog("Error disposing clientCertificateManager", err.message);
×
1348
            });
1,279✔
1349
        }
1,279✔
1350

1,279✔
1351
        if (this._sessions.length) {
1,454✔
1352
            // transfer active session to  orphan and detach them from channel
2✔
1353
            const tmp = [...this._sessions];
2✔
1354
            for (const session of tmp) {
2✔
1355
                this._removeSession(session);
2✔
1356
            }
2✔
1357
            this._sessions = [];
2✔
1358
        }
2✔
1359
        assert(this._sessions.length === 0, " attempt to disconnect a client with live sessions ");
1,279✔
1360

1,279✔
1361
        OPCUAClientBase.registry.unregister(this);
1,279✔
1362
        if (this._clockAdjuster) {
1,454✔
1363
            this._clockAdjuster.dispose();
1,269✔
1364
            this._clockAdjuster = undefined;
1,269✔
1365
        }
1,269✔
1366

1,279✔
1367
        if (this._secureChannel) {
1,454✔
1368
            let tmpChannel: ClientSecureChannelLayer | null = this._secureChannel;
1,254✔
1369
            this._secureChannel = null;
1,254✔
1370
            debugLog("Closing channel");
1,254✔
1371
            tmpChannel.close(() => {
1,254✔
1372
                this._secureChannel = tmpChannel;
1,254✔
1373
                tmpChannel = null;
1,254✔
1374
                this._destroy_secure_channel();
1,254✔
1375
                this._setInternalState("disconnected");
1,254✔
1376
                callback();
1,254✔
1377
            });
1,254✔
1378
        } else {
1,454✔
1379
            this._setInternalState("disconnected");
25✔
1380
            //    this.emit("close", null);
25✔
1381
            callback();
25✔
1382
        }
25✔
1383
        return undefined;
1,279✔
1384
    }
1,279✔
1385

1✔
1386
    // override me !
1✔
1387
    public _on_connection_reestablished(callback: ErrorCallback): void {
1✔
1388
        callback();
36✔
1389
    }
36✔
1390

1✔
1391
    public toString(): string {
1✔
1392
        let str = "\n";
1✔
1393
        str += `  defaultSecureTokenLifetime.... ${this.defaultSecureTokenLifetime}\n`;
1✔
1394
        str += `  securityMode.................. ${MessageSecurityMode[this.securityMode]}\n`;
1✔
1395
        str += `  securityPolicy................ ${this.securityPolicy.toString()}\n`;
1✔
1396
        str += `  certificate fingerprint....... ${makeCertificateThumbPrint(this.getCertificate())?.toString("hex") || "none"}\n`;
1✔
1397

1✔
1398
        str += `  server certificate fingerprint ${makeCertificateThumbPrint(this.serverCertificate)?.toString("hex") || "none"}\n`;
1✔
1399
        // this.serverCertificate = options.serverCertificate || null + "\n";
1✔
1400
        str += `  keepSessionAlive.............. ${this.keepSessionAlive}\n`;
1✔
1401
        str += `  bytesRead..................... ${this.bytesRead}\n`;
1✔
1402
        str += `  bytesWritten.................. ${this.bytesWritten}\n`;
1✔
1403
        str += `  transactionsPerformed......... ${this.transactionsPerformed}\n`;
1✔
1404
        str += `  timedOutRequestCount.......... ${this.timedOutRequestCount}\n`;
1✔
1405
        str += "  connectionStrategy." + "\n";
1✔
1406
        str += `        .maxRetry............... ${this.connectionStrategy.maxRetry}\n`;
1✔
1407
        str += `        .initialDelay........... ${this.connectionStrategy.initialDelay}\n`;
1✔
1408
        str += `        .maxDelay............... ${this.connectionStrategy.maxDelay}\n`;
1✔
1409
        str += `        .randomisationFactor.... ${this.connectionStrategy.randomisationFactor}\n`;
1✔
1410
        str += `  keepSessionAlive.............. ${this.keepSessionAlive}\n`;
1✔
1411
        str += `  applicationName............... ${this.applicationName}\n`;
1✔
1412
        str += `  applicationUri................ ${this._getBuiltApplicationUri()}\n`;
1✔
1413
        str += `  clientName.................... ${this.clientName}\n`;
1✔
1414
        str += `  reconnectOnFailure............ ${this.reconnectOnFailure}\n`;
1✔
1415
        str += `  isReconnecting................ ${this.isReconnecting}\n`;
1✔
1416
        str += `  (internal state).............. ${this._internalState}\n`;
1✔
1417
        str += `  sessions count................ ${this.getSessions().length}\n`;
1✔
1418

1✔
1419
        if (this._secureChannel) {
1✔
1420
            str += `secureChannel:\n${this._secureChannel.toString()}`;
1✔
1421
        }
1✔
1422
        return str;
1✔
1423
    }
1✔
1424

1✔
1425
    public getSessions(): ClientSessionImpl[] {
1✔
1426
        return this._sessions;
37✔
1427
    }
37✔
1428

1✔
1429
    public getTransportSettings(): IBasicTransportSettings {
1✔
1430
        return this._secureChannel?.getTransportSettings() || ({} as IBasicTransportSettings);
2✔
1431
    }
2✔
1432

1✔
1433
    protected _addSession(session: ClientSessionImpl): void {
1✔
1434
        assert(!session._client || session._client === this);
964✔
1435
        assert(this._sessions.indexOf(session) === -1, "session already added");
964✔
1436
        session._client = this;
964✔
1437
        this._sessions.push(session);
964✔
1438
    }
964✔
1439

1✔
1440
    private fetchServerCertificate(endpointUrl: string, callback: (err: Error | null, adjustedEndpointUrl?: string) => void): void {
1✔
1441
        const discoveryUrl = this.discoveryUrl.length > 0 ? this.discoveryUrl : endpointUrl;
119✔
1442
        debugLog("OPCUAClientImpl : getting serverCertificate");
119✔
1443
        // we have not been given the serverCertificate but this certificate
119✔
1444
        // is required as the connection is to be secured.
119✔
1445
        //
119✔
1446
        // Let's explore the server endpoint that matches our security settings
119✔
1447
        // This will give us the missing Certificate as well from the server.
119✔
1448
        // todo :
119✔
1449
        // Once we have the certificate, we cannot trust it straight away
119✔
1450
        // we have to verify that the certificate is valid and not outdated and not revoked.
119✔
1451
        // if the certificate is self-signed the certificate must appear in the trust certificate
119✔
1452
        // list.
119✔
1453
        // if the certificate has been certified by an Certificate Authority we have to
119✔
1454
        // verify that the certificates in the chain are valid and not revoked.
119✔
1455
        //
119✔
1456
        const certificateFile = this.certificateFile;
119✔
1457
        const privateKeyFile = this.privateKeyFile;
119✔
1458
        const applicationName = this.applicationName;
119✔
1459
        const applicationUri = this._applicationUri;
119✔
1460
        const params = {
119✔
1461
            connectionStrategy: this.connectionStrategy,
119✔
1462
            endpointMustExist: false,
119✔
1463

119✔
1464
            // Node: May be the discovery endpoint only support security mode NONE
119✔
1465
            //       what should we do ?
119✔
1466
            securityMode: this.securityMode,
119✔
1467
            securityPolicy: this.securityPolicy,
119✔
1468

119✔
1469
            applicationName,
119✔
1470
            applicationUri,
119✔
1471

119✔
1472
            certificateFile,
119✔
1473
            privateKeyFile,
119✔
1474

119✔
1475
            clientCertificateManager: this.clientCertificateManager
119✔
1476
        };
119✔
1477
        __findEndpoint.call(this, discoveryUrl, params, (err: Error | null, result?: FindEndpointResult) => {
119✔
1478
            if (err) {
119✔
1479
                callback(err);
2✔
1480
                return;
2✔
1481
            }
2✔
1482

117✔
1483
            // c8 ignore next
117✔
1484
            if (!result) {
119✔
1485
                const err1 = new Error("internal error");
×
1486
                callback(err1);
×
1487
                return;
×
1488
            }
×
1489

117✔
1490
            const endpoint = result.selectedEndpoint;
117✔
1491
            if (!endpoint) {
119✔
1492
                // no matching end point can be found ...
×
1493
                const err1 = new Error(
×
1494
                    "cannot find endpoint for securityMode=" +
×
1495
                    MessageSecurityMode[this.securityMode] +
×
1496
                    " policy = " +
×
1497
                    this.securityPolicy
×
1498
                );
×
1499
                callback(err1);
×
1500
                return;
×
1501
            }
×
1502

117✔
1503
            assert(endpoint);
117✔
1504

117✔
1505
            _verify_serverCertificate(this.clientCertificateManager, endpoint.serverCertificate)
117✔
1506
                .then(() => {
117✔
1507
                    this.serverCertificate = endpoint.serverCertificate;
116✔
1508
                    callback(null, endpoint.endpointUrl || "");
116✔
1509
                })
117✔
1510
                .catch((err1: Error) => {
117✔
1511
                    warningLog("[NODE-OPCUA-W25] client's server certificate verification has failed ", err1.message);
1✔
1512
                    if ("rootDir" in this.clientCertificateManager) {
1✔
1513
                        warningLog("                 clientCertificateManager.rootDir = ", (this.clientCertificateManager as unknown as { rootDir: string }).rootDir);
1✔
1514
                    }
1✔
1515

1✔
1516
                    const chain = split_der(endpoint.serverCertificate);
1✔
1517
                    warningLog(`                 server certificate chain contains ${chain.length} element(s)`);
1✔
1518
                    for (let i = 0; i < chain.length; i++) {
1✔
1519
                        const c = chain[i];
1✔
1520
                        const thumbprint = makeSHA1Thumbprint(c).toString("hex");
1✔
1521
                        try {
1✔
1522
                            const { exploreCertificate } = require("node-opcua-crypto");
1✔
1523
                            const info = exploreCertificate(c);
1✔
1524
                            const tbs = info.tbsCertificate;
1✔
1525
                            const cn = tbs.subject?.commonName ?? "unknown";
1✔
1526
                            const issuerCN = tbs.issuer?.commonName ?? "unknown";
1✔
1527
                            const isCA = tbs.extensions?.basicConstraints?.cA === true;
1✔
1528
                            const isSelfSigned = JSON.stringify(tbs.subject) === JSON.stringify(tbs.issuer);
1✔
1529
                            const certType = isCA
1✔
1530
                                ? (isSelfSigned ? "Root CA" : "Intermediate CA")
1✔
1531
                                : (isSelfSigned ? "Application (self-signed)" : "Application (CA-signed)");
1✔
1532
                            const ski = tbs.extensions?.subjectKeyIdentifier ?? "-";
1✔
1533
                            const aki = tbs.extensions?.authorityKeyIdentifier?.keyIdentifier ?? "-";
1✔
1534
                            const notBefore = tbs.validity.notBefore.toISOString();
1✔
1535
                            const notAfter = tbs.validity.notAfter.toISOString();
1✔
1536
                            warningLog(`                 [${i + 1}/${chain.length}] "${cn}" (${certType})`);
1✔
1537
                            warningLog(`                   thumbprint: ${thumbprint}`);
1✔
1538
                            warningLog(`                   issuer:     "${issuerCN}"`);
1✔
1539
                            warningLog(`                   SKI:        ${ski}`);
1✔
1540
                            warningLog(`                   AKI:        ${aki}`);
1✔
1541
                            warningLog(`                   validity:   ${notBefore} → ${notAfter}`);
1✔
1542
                        } catch {
1✔
1543
                            warningLog(`                 [${i + 1}/${chain.length}] thumbprint: ${thumbprint} (details unavailable)`);
×
1544
                        }
×
1545
                    }
1✔
1546
                    if (chain.length > 1) {
1✔
1547
                        warningLog(
×
1548
                            "                 verify also that the issuer certificate is trusted and the issuer's certificate is present in the issuer.cert folder\n" +
×
1549
                            "                 of the client certificate manager located in ",
×
1550
                            "rootDir" in this.clientCertificateManager
×
1551
                                ? (this.clientCertificateManager as unknown as { rootDir: string }).rootDir
×
1552
                                : "<in-memory>"
×
1553
                        );
×
1554
                    } else {
1✔
1555
                        warningLog(
1✔
1556
                            "                 verify that the server certificate is trusted or that server certificate issuer's certificate is present in the issuer folder"
1✔
1557
                        );
1✔
1558
                    }
1✔
1559
                    callback(err1);
1✔
1560
                });
117✔
1561
        });
119✔
1562
    }
119✔
1563
    private _accumulate_statistics() {
1✔
1564
        if (this._secureChannel) {
1,387✔
1565
            // keep accumulated statistics
1,387✔
1566
            this._byteWritten += this._secureChannel.bytesWritten;
1,387✔
1567
            this._byteRead += this._secureChannel.bytesRead;
1,387✔
1568
            this._transactionsPerformed += this._secureChannel.transactionsPerformed;
1,387✔
1569
            this._timedOutRequestCount += this._secureChannel.timedOutRequestCount;
1,387✔
1570
            // c8 ignore next
1,387✔
1571
            if (doDebug) {
1,387✔
1572
                const h = `Client ${this._instanceNumber} ${this.clientName}`;
×
1573
                debugLog(chalk.cyan(`${h} byteWritten          = `), this._byteWritten);
×
1574
                debugLog(chalk.cyan(`${h} byteRead             = `), this._byteRead);
×
1575
                debugLog(chalk.cyan(`${h} transactions         = `), this._transactionsPerformed);
×
1576
                debugLog(chalk.cyan(`${h} timedOutRequestCount = `), this._timedOutRequestCount);
×
1577
            }
×
1578
        }
1,387✔
1579
    }
1,387✔
1580
    private _destroy_secure_channel() {
1✔
1581
        if (this._secureChannel) {
2,632✔
1582
            // c8 ignore next
1,387✔
1583
            if (doDebug) {
1,387✔
1584
                debugLog(
×
1585
                    " DESTROYING SECURE CHANNEL (isTransactionInProgress ?",
×
1586
                    this._secureChannel.isTransactionInProgress(),
×
1587
                    ")"
×
1588
                );
×
1589
            }
×
1590
            this._accumulate_statistics();
1,387✔
1591
            const secureChannel = this._secureChannel;
1,387✔
1592
            this._secureChannel = null;
1,387✔
1593
            secureChannel.dispose();
1,387✔
1594
            secureChannel.removeAllListeners();
1,387✔
1595
        }
1,387✔
1596
    }
2,632✔
1597

1✔
1598
    private _close_pending_sessions(callback: ErrorCallback) {
1✔
1599
        const sessions = [...this._sessions];
15✔
1600

15✔
1601
        const closeAll = async (): Promise<void> => {
15✔
1602
            for (const session of sessions) {
15✔
1603
                assert(session._client === this);
15✔
1604
                await new Promise<void>((resolve) => {
15✔
1605
                    let resolved = false;
15✔
1606
                    session.close((err?: Error) => {
15✔
1607
                        if (resolved) return;
15✔
1608
                        resolved = true;
15✔
1609
                        if (err) {
15✔
1610
                            const msg = session.authenticationToken ? session.authenticationToken.toString() : "";
×
1611
                            debugLog(` failing to close session ${msg}`);
×
1612
                        }
×
1613
                        resolve();
15✔
1614
                    });
15✔
1615
                });
15✔
1616
            }
15✔
1617
        };
15✔
1618

15✔
1619
        closeAll()
15✔
1620
            .then(() => {
15✔
1621
                // c8 ignore next
15✔
1622
                if (this._sessions.length > 0) {
15✔
1623
                    debugLog(
×
1624
                        this._sessions
×
1625
                            .map((s: ClientSessionImpl) => (s.authenticationToken ? s.authenticationToken.toString() : ""))
×
1626
                            .join(" ")
×
1627
                    );
×
1628
                }
×
1629
                assert(this._sessions.length === 0, " failed to disconnect exiting sessions ");
15✔
1630
                callback();
15✔
1631
            })
15✔
1632
            .catch((err) => callback(err));
15✔
1633
    }
15✔
1634

1✔
1635
    private _install_secure_channel_event_handlers(secureChannel: ClientSecureChannelLayer) {
1✔
1636
        assert(this instanceof ClientBaseImpl);
1,301✔
1637

1,301✔
1638
        secureChannel.on("send_chunk", (chunk: Buffer) => {
1,301✔
1639
            /**
47,573✔
1640
             * notify the observer that a message_chunk has been sent
47,573✔
1641
             * @event send_chunk
47,573✔
1642
             * @param message_chunk
47,573✔
1643
             */
47,573✔
1644
            this.emit("send_chunk", chunk);
47,573✔
1645
        });
1,301✔
1646

1,301✔
1647
        secureChannel.on("receive_chunk", (chunk: Buffer) => {
1,301✔
1648
            /**
46,295✔
1649
             * notify the observer that a message_chunk has been received
46,295✔
1650
             * @event receive_chunk
46,295✔
1651
             * @param message_chunk
46,295✔
1652
             */
46,295✔
1653
            this.emit("receive_chunk", chunk);
46,295✔
1654
        });
1,301✔
1655

1,301✔
1656
        secureChannel.on("send_request", (message: Request1) => {
1,301✔
1657
            /**
47,406✔
1658
             * notify the observer that a request has been sent to the server.
47,406✔
1659
             * @event send_request
47,406✔
1660
             * @param message
47,406✔
1661
             */
47,406✔
1662
            this.emit("send_request", message as unknown as Request);
47,406✔
1663
        });
1,301✔
1664

1,301✔
1665
        secureChannel.on("receive_response", (message: Response1) => {
1,301✔
1666
            /**
44,115✔
1667
             * notify the observer that a response has been received from the server.
44,115✔
1668
             * @event receive_response
44,115✔
1669
             * @param message
44,115✔
1670
             */
44,115✔
1671
            this.emit("receive_response", message as unknown as Response);
44,115✔
1672
        });
1,301✔
1673

1,301✔
1674
        secureChannel.on("lifetime_75", (token: ChannelSecurityToken) => {
1,301✔
1675
            // secureChannel requests a new token
167✔
1676
            debugLog(
167✔
1677
                "SecureChannel Security Token ",
167✔
1678
                token.tokenId,
167✔
1679
                "live time was =",
167✔
1680
                token.revisedLifetime,
167✔
1681
                " is about to expired , it's time to request a new token"
167✔
1682
            );
167✔
1683
            // forward message to upper level
167✔
1684
            this.emit("lifetime_75", token);
167✔
1685
        });
1,301✔
1686

1,301✔
1687
        secureChannel.on("security_token_renewed", (token: ChannelSecurityToken) => {
1,301✔
1688
            // forward message to upper level
165✔
1689
            this.emit("security_token_renewed", secureChannel, token);
165✔
1690
        });
1,301✔
1691

1,301✔
1692
        secureChannel.on("close", (err?: Error | null) => {
1,301✔
1693
            if (err) {
1,297✔
1694
                this._setInternalState("panic");
52✔
1695
            }
52✔
1696
            debugLog(chalk.yellow.bold(" ClientBaseImpl emitting close"), err?.message);
1,297✔
1697
            this._destroy_secure_channel();
1,297✔
1698
            if (!err || !this.reconnectOnFailure) {
1,297✔
1699
                if (err) {
1,252✔
1700
                    /**
7✔
1701
                     * @event connection_lost
7✔
1702
                     */
7✔
1703
                    this.emit("connection_lost"); // instead of "close"
7✔
1704
                }
7✔
1705
                // this is a normal close operation initiated by us
1,252✔
1706
                this.emit("close", err); // instead of "close"
1,252✔
1707
            } else {
1,297✔
1708
                if (
45✔
1709
                    this.reconnectOnFailure &&
45✔
1710
                    this._internalState !== "reconnecting" &&
45✔
1711
                    this._internalState !== "reconnecting_newchannel_connected"
45✔
1712
                ) {
45✔
1713
                    debugLog(" ClientBaseImpl emitting connection_lost");
45✔
1714
                    this._setInternalState("reconnecting");
45✔
1715
                    /**
45✔
1716
                     * @event connection_lost
45✔
1717
                     */
45✔
1718
                    this.emit("connection_lost"); // instead of "close"
45✔
1719
                    this._repairConnection();
45✔
1720
                }
45✔
1721
            }
45✔
1722
        });
1,301✔
1723

1,301✔
1724
        secureChannel.on("timed_out_request", (request: Request1) => {
1,301✔
1725
            /**
1✔
1726
             * send when a request has timed out without receiving a response
1✔
1727
             * @event timed_out_request
1✔
1728
             * @param request
1✔
1729
             */
1✔
1730
            this.emit("timed_out_request", request as unknown as Request);
1✔
1731
        });
1,301✔
1732
    }
1,301✔
1733

1✔
1734
    #insideRepairConnection = false;
1✔
1735

1✔
1736
    #shouldRepairAgain = false;
1✔
1737

1✔
1738
    /**
1✔
1739
     * @internal
1✔
1740
     * @private
1✔
1741
     *
1✔
1742
     * timeout to wait before client attempt to reconnect in case of failure
1✔
1743
     *
1✔
1744
     */
1✔
1745
    static retryDelay = 1000;
1✔
1746
    private _repairConnection() {
1✔
1747
        doDebug && debugLog("_repairConnection = ", this._internalState);
49✔
1748
        if (this.isUnusable()) return;
49✔
1749

46✔
1750
        const duration = ClientBaseImpl.retryDelay;
46✔
1751
        if (duration) {
46✔
1752
            this.emit("startingDelayBeforeReconnection", duration);
46✔
1753
            setTimeout(() => {
46✔
1754
                if (this.isUnusable()) return;
44✔
1755
                this.__innerRepairConnection();
39✔
1756
            }, duration);
46✔
1757
        } else {
49✔
1758
            this.__innerRepairConnection();
×
1759
        }
×
1760
    }
49✔
1761
    private __innerRepairConnection() {
1✔
1762
        if (this.isUnusable()) return;
39✔
1763

39✔
1764
        debugLog("Entering _repairConnection ", this._internalState);
39✔
1765
        if (this.#insideRepairConnection) {
39✔
1766
            errorLog(
1✔
1767
                "_repairConnection already in progress internal state = ",
1✔
1768
                this._internalState,
1✔
1769
                "clientName =",
1✔
1770
                this.clientName
1✔
1771
            );
1✔
1772
            this.#shouldRepairAgain = true;
1✔
1773
            return;
1✔
1774
        }
1✔
1775
        this.emit("repairConnectionStarted");
38✔
1776
        this.#insideRepairConnection = true;
38✔
1777
        debugLog("recreating new secure channel ", this._internalState);
38✔
1778
        this._recreate_secure_channel((err1?: Error) => {
38✔
1779
            debugLog("secureChannel#on(close) => _recreate_secure_channel returns ", err1 ? err1.message : "OK");
38✔
1780
            if (err1) {
38✔
1781
                debugLog("_recreate_secure_channel has failed: err = ", err1.message);
2✔
1782
                this.emit("close", err1);
2✔
1783
                this.#insideRepairConnection = false;
2✔
1784
                if (this.#shouldRepairAgain) {
2✔
1785
                    this.#shouldRepairAgain = false;
×
1786
                    this._repairConnection();
×
1787
                } else {
2✔
1788
                    this._setInternalState("disconnected");
2✔
1789
                }
2✔
1790
                return;
2✔
1791
            } else {
38✔
1792
                if (this.isUnusable()) return;
36✔
1793
                this._finalReconnectionStep((err2?: Error | null) => {
36✔
1794
                    if (err2) {
36✔
1795
                        // c8 ignore next
4✔
1796
                        if (doDebug) {
4✔
1797
                            debugLog("connection_reestablished has failed");
×
1798
                            debugLog("err= ", err2.message);
×
1799
                        }
×
1800
                        // we still need to retry connecting here !!!
4✔
1801
                        debugLog("Disconnected following reconnection failure", err2.message);
4✔
1802
                        debugLog(`I will retry OPCUA client reconnection in ${OPCUAClientBase.retryDelay / 1000} seconds`);
4✔
1803
                        this.#insideRepairConnection = false;
4✔
1804
                        this.#shouldRepairAgain = false;
4✔
1805

4✔
1806
                        this._destroy_secure_channel();
4✔
1807
                        // this._setInternalState("reconnecting_failed");
4✔
1808
                        setTimeout(() => this._repairConnection(), OPCUAClientBase.retryDelay);
4✔
1809

4✔
1810
                        return;
4✔
1811
                    } else {
36✔
1812
                        /**
32✔
1813
                         * @event connection_reestablished
32✔
1814
                         *        send when the connection is reestablished after a connection break
32✔
1815
                         */
32✔
1816
                        this.#insideRepairConnection = false;
32✔
1817
                        this.#shouldRepairAgain = false;
32✔
1818
                        this._setInternalState("connected");
32✔
1819
                        this.emit("connection_reestablished");
32✔
1820
                    }
32✔
1821
                });
36✔
1822
            }
36✔
1823
        });
38✔
1824
    }
38✔
1825
    private _finalReconnectionStep(callback: ErrorCallback) {
1✔
1826
        // now delegate to upper class the
36✔
1827
        if (this._on_connection_reestablished) {
36✔
1828
            assert(typeof this._on_connection_reestablished === "function");
36✔
1829
            this._on_connection_reestablished((err2?: Error) => {
36✔
1830
                callback(err2);
36✔
1831
            });
36✔
1832
        } else {
36✔
1833
            callback();
×
1834
        }
×
1835
    }
36✔
1836

1✔
1837
    /**
1✔
1838
     *
1✔
1839
     * @internal
1✔
1840
     * @private
1✔
1841
     */
1✔
1842
    public __createSession_step2(
1✔
1843
        _session: ClientSessionImpl,
×
1844
        _callback: (err: Error | null, session?: ClientSessionImpl) => void
×
1845
    ): void {
×
1846
        throw new Error("Please override");
×
1847
    }
×
1848
    public _activateSession(
1✔
1849
        _session: ClientSessionImpl,
×
1850
        _userIdentity: UserIdentityInfo,
×
1851
        _callback: (err: Error | null, session?: ClientSessionImpl) => void
×
1852
    ): void {
×
1853
        throw new Error("Please override");
×
1854
    }
×
1855
}
1✔
1856

2✔
1857
// tslint:disable-next-line: max-classes-per-file
2✔
1858
class TmpClient extends ClientBaseImpl {
2✔
1859
    constructor(options: OPCUAClientBaseOptions) {
2✔
1860
        options.clientName = `${options.clientName || ""}_TmpClient`;
119✔
1861
        super(options);
119✔
1862
    }
119✔
1863

2✔
1864
    async connect(endpoint: string): Promise<void>;
2✔
1865
    connect(endpoint: string, callback: ErrorCallback): void;
2✔
1866
    connect(endpoint: string, callback?: ErrorCallback): Promise<void> | void {
2✔
1867
        debugLog("connecting to TmpClient");
119✔
1868

119✔
1869
        // c8 ignore next
119✔
1870
        if (this._internalState !== "disconnected") {
119✔
1871
            callback?.(new Error(`TmpClient#connect: invalid internal state = ${this._internalState}`));
×
1872
            return;
×
1873
        }
×
1874

119✔
1875
        this._setInternalState("connecting");
119✔
1876
        this._connectStep2(endpoint, (err?: Error) => {
119✔
1877
            if (this.isUnusable()) {
119✔
1878
                this._handleUnrecoverableConnectionFailure(new Error("premature disconnection 4"), callback || (() => { }));
2✔
1879
                return;
2✔
1880
            }
2✔
1881
            callback?.(err);
119✔
1882
        });
119✔
1883
    }
119✔
1884
}
2✔
1885

2✔
1886
// tslint:disable:no-var-requires
2✔
1887
// tslint:disable:max-line-length
2✔
1888
import { withCallback } from "thenify-ex";
2✔
1889

2✔
1890
ClientBaseImpl.prototype.connect = withCallback(ClientBaseImpl.prototype.connect);
2✔
1891
ClientBaseImpl.prototype.disconnect = withCallback(ClientBaseImpl.prototype.disconnect);
2✔
1892
ClientBaseImpl.prototype.getEndpoints = withCallback(ClientBaseImpl.prototype.getEndpoints);
2✔
1893
ClientBaseImpl.prototype.findServers = withCallback(ClientBaseImpl.prototype.findServers);
2✔
1894
ClientBaseImpl.prototype.findServersOnNetwork = withCallback(ClientBaseImpl.prototype.findServersOnNetwork);
2✔
1895

2✔
1896
OPCUAClientBase.create = (options: OPCUAClientBaseOptions): OPCUAClientBase => {
2✔
1897
    return new ClientBaseImpl(options);
262✔
1898
};
262✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc