node-opcua-client 2.169.0 → 2.172.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,9 +12,19 @@ import type { DataValue } from "node-opcua-data-value";
12
12
  import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug";
13
13
  import { coerceNodeId } from "node-opcua-nodeid";
14
14
  import { ClientSecureChannelLayer } from "node-opcua-secure-channel";
15
+ import type { StatusCode } from "node-opcua-status-code";
16
+ import { StatusCodes } from "node-opcua-status-code";
15
17
  import type { ClientSessionImpl } from "./private/client_session_impl";
16
18
  import type { IClientBase } from "./private/i_private_client";
17
19
 
20
+ interface ServiceFaultAnnotatedError extends Error {
21
+ response?: {
22
+ responseHeader?: {
23
+ serviceResult?: StatusCode;
24
+ };
25
+ };
26
+ }
27
+
18
28
  const serverStatusStateNodeId = coerceNodeId(VariableIds.Server_ServerStatus_State);
19
29
 
20
30
  const debugLog = make_debugLog(__filename);
@@ -24,14 +34,18 @@ const warningLog = make_warningLog(__filename);
24
34
  export interface ClientSessionKeepAliveManagerEvents {
25
35
  on(event: "keepalive", eventHandler: (lastKnownServerState: ServerState, count: number) => void): this;
26
36
  on(event: "failure", eventHandler: () => void): this;
37
+ on(event: "keepalive_failure", eventHandler: () => void): this;
27
38
  }
28
39
 
40
+ const maxBackoffInterval = 60_000;
41
+
29
42
  export class ClientSessionKeepAliveManager extends EventEmitter implements ClientSessionKeepAliveManagerEvents {
30
43
  private readonly session: ClientSessionImpl;
31
44
  private timerId?: NodeJS.Timeout;
32
45
  private pingTimeout: number;
33
46
  private lastKnownState?: ServerState;
34
47
  private transactionInProgress = false;
48
+ private consecutiveFailures = 0;
35
49
  public count = 0;
36
50
  public checkInterval: number;
37
51
 
@@ -86,7 +100,9 @@ export class ClientSessionKeepAliveManager extends EventEmitter implements Clien
86
100
  return; // stop here
87
101
  }
88
102
  if (this.timerId) {
89
- const timeout = Math.max(1, this.checkInterval - delta);
103
+ // When delta exceeds checkInterval it is an explicit backoff requested by _ping_server;
104
+ // otherwise delta is the time already consumed this cycle.
105
+ const timeout = delta > this.checkInterval ? delta : Math.max(1, this.checkInterval - delta);
90
106
  this.timerId = setTimeout(() => this.ping_server(), timeout);
91
107
  }
92
108
  });
@@ -153,25 +169,68 @@ export class ClientSessionKeepAliveManager extends EventEmitter implements Clien
153
169
  (err: Error | null, dataValue?: DataValue) => {
154
170
  this.transactionInProgress = false;
155
171
 
156
- if (err || !dataValue || !dataValue.value) {
157
- if (err) {
158
- warningLog(
159
- chalk.cyan(" warning : ClientSessionKeepAliveManager#ping_server "),
160
- chalk.yellow(err.message)
161
- );
172
+ if (err) {
173
+ warningLog(chalk.cyan(" warning : ClientSessionKeepAliveManager#ping_server "), chalk.yellow(err.message));
174
+ const serviceFaultResponse = (err as ServiceFaultAnnotatedError).response;
175
+ if (serviceFaultResponse) {
176
+ const sc = serviceFaultResponse.responseHeader?.serviceResult;
177
+ if (sc?.equals(StatusCodes.BadSessionIdInvalid) || sc?.equals(StatusCodes.BadSessionClosed)) {
178
+ this.emit("failure");
179
+ warningLog(
180
+ "Keep alive has failed, considering a network outage is in place, forcing a reconnection"
181
+ );
182
+ terminateConnection(session._client);
183
+ resolve(0);
184
+ } else {
185
+ if (sc?.equals(StatusCodes.BadInvalidTimestamp)) {
186
+ // BadInvalidTimestamp (OPC UA Part 4 7.38.2, Table 178:
187
+ // "The timestamp is outside the range allowed by the Server")
188
+ // refers to the timestamp field of the RequestHeader
189
+ // (OPC UA Part 4 7.32), which the spec states is used
190
+ // "only for diagnostic and logging purposes in the Server".
191
+ //
192
+ // The server responded at the OPC UA application layer:
193
+ // the SecureChannel and Session are intact. The cause is
194
+ // clock skew between client and server; this is an
195
+ // infrastructure concern outside the scope of the keepalive
196
+ // manager.
197
+ //
198
+ // Treating this as a keepalive failure is semantically
199
+ // incorrect: the round-trip succeeded. Incrementing
200
+ // consecutiveFailures leads to unbounded exponential backoff
201
+ // and eventual session expiry server-side, triggering an
202
+ // unnecessary reconnect loop.
203
+ //
204
+ // See: https://reference.opcfoundation.org/Core/Part4/v105/docs/7.38.2
205
+ // https://reference.opcfoundation.org/Core/Part4/v105/docs/7.32
206
+ this.consecutiveFailures = 0;
207
+ debugLog("emit keepalive (BadInvalidTimestamp: session alive, clock skew on request timestamp)");
208
+ this.emit("keepalive", this.lastKnownState ?? ServerState.Unknown, this.count);
209
+ resolve(0);
210
+ return;
211
+ }
212
+ this.consecutiveFailures++;
213
+ warningLog("Keep alive received ServiceFault from server (session intact):", sc?.toString());
214
+ this.emit("keepalive_failure");
215
+ resolve(Math.min(this.checkInterval * 2 ** this.consecutiveFailures, maxBackoffInterval));
216
+ }
217
+ } else {
218
+ this.emit("failure");
219
+ warningLog("Keep alive has failed, considering a network outage is in place, forcing a reconnection");
220
+ terminateConnection(session._client);
221
+ resolve(0);
162
222
  }
223
+ return;
224
+ }
225
+ if (!dataValue || !dataValue.value) {
163
226
  /**
164
227
  * @event failure
165
228
  * raised when the server is not responding or is responding with en error to
166
229
  * the keep alive read Variable value transaction
167
230
  */
168
231
  this.emit("failure");
169
-
170
- // also simulate a connection by closing the channel abruptly from our end ...
171
232
  warningLog("Keep alive has failed, considering a network outage is in place, forcing a reconnection");
172
-
173
233
  terminateConnection(session._client);
174
-
175
234
  resolve(0);
176
235
  return;
177
236
  }
@@ -190,6 +249,7 @@ export class ClientSessionKeepAliveManager extends EventEmitter implements Clien
190
249
  this.lastKnownState = newState;
191
250
  this.count++; // increase successful counter
192
251
  }
252
+ this.consecutiveFailures = 0;
193
253
  debugLog("emit keepalive");
194
254
  this.emit("keepalive", this.lastKnownState, this.count);
195
255
  resolve(0);
@@ -9,8 +9,8 @@ import { withLock } from "@ster5/global-mutex";
9
9
 
10
10
  import chalk from "chalk";
11
11
  import { assert } from "node-opcua-assert";
12
- import { getDefaultCertificateManager, makeSubject, type OPCUACertificateManager } from "node-opcua-certificate-manager";
13
- import { type IOPCUASecureObjectOptions, makeApplicationUrn, OPCUASecureObject } from "node-opcua-common";
12
+ import { getDefaultCertificateManager, type OPCUACertificateManager } from "node-opcua-certificate-manager";
13
+ import { type ICertificateStore, InMemoryCertificateStore, type IOPCUASecureObjectOptions, makeApplicationUrn, makeSubject, OPCUASecureObject } from "node-opcua-common";
14
14
  import { type Certificate, makeSHA1Thumbprint, split_der } from "node-opcua-crypto/web";
15
15
  import { installPeriodicClockAdjustment, periodicClockAdjustment, uninstallPeriodicClockAdjustment } from "node-opcua-date-time";
16
16
  import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
@@ -27,6 +27,7 @@ import {
27
27
  type Response as Response1,
28
28
  type SecurityPolicy
29
29
  } from "node-opcua-secure-channel";
30
+ import type { IClientTransportFactory } from "node-opcua-transport";
30
31
  import {
31
32
  FindServersOnNetworkRequest,
32
33
  type FindServersOnNetworkRequestOptions,
@@ -228,7 +229,7 @@ function __findEndpoint(this: ClientBaseImpl, endpointUrl: string, params: FindE
228
229
  /**
229
230
  * check if certificate is trusted or untrusted
230
231
  */
231
- async function _verify_serverCertificate(certificateManager: OPCUACertificateManager, serverCertificate: Certificate) {
232
+ async function _verify_serverCertificate(certificateManager: ICertificateStore, serverCertificate: Certificate) {
232
233
  const status = await certificateManager.checkCertificate(serverCertificate);
233
234
  if (!status.isGood()) {
234
235
  // c8 ignore next
@@ -387,8 +388,9 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
387
388
  private _instanceNumber: number;
388
389
  private _transportSettings: TransportSettings;
389
390
  private _transportTimeout?: number;
391
+ private _transportFactory?: IClientTransportFactory;
390
392
 
391
- public clientCertificateManager: OPCUACertificateManager;
393
+ public clientCertificateManager: ICertificateStore;
392
394
 
393
395
  public isUnusable() {
394
396
  return (
@@ -430,14 +432,25 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
430
432
  }
431
433
  constructor(options?: OPCUAClientBaseOptions) {
432
434
  options = options || {};
433
- if (!options.clientCertificateManager) {
434
- options.clientCertificateManager = getDefaultCertificateManager("PKI");
435
- }
436
- options.privateKeyFile = options.privateKeyFile || options.clientCertificateManager.privateKey;
437
- options.certificateFile =
438
- options.certificateFile || path.join(options.clientCertificateManager.rootDir, "own/certs/client_certificate.pem");
439
435
 
440
- super(options as IOPCUASecureObjectOptions);
436
+ if (options.certificateKeyPairProvider) {
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 {
444
+ // Disk path — derive cert/key paths from certificate manager
445
+ if (!options.clientCertificateManager) {
446
+ options.clientCertificateManager = getDefaultCertificateManager("PKI");
447
+ }
448
+ const cm = options.clientCertificateManager as OPCUACertificateManager;
449
+ options.privateKeyFile = options.privateKeyFile || cm.privateKey;
450
+ options.certificateFile =
451
+ options.certificateFile || path.join(cm.rootDir, "own/certs/client_certificate.pem");
452
+ super(options as IOPCUASecureObjectOptions);
453
+ }
441
454
 
442
455
  this._setInternalState("uninitialized");
443
456
 
@@ -450,7 +463,7 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
450
463
  // we need to delay _applicationUri initialization
451
464
  this._applicationUri = options.applicationUri || this._getBuiltApplicationUri();
452
465
 
453
- this.clientCertificateManager = options.clientCertificateManager;
466
+ this.clientCertificateManager = options.clientCertificateManager!;
454
467
  this.clientCertificateManager.referenceCounter++;
455
468
 
456
469
  this._secureChannel = null;
@@ -502,6 +515,7 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
502
515
 
503
516
  this._transportSettings = options.transportSettings || {};
504
517
  this._transportTimeout = options.transportTimeout;
518
+ this._transportFactory = options.transportFactory;
505
519
  }
506
520
 
507
521
  private _cancel_reconnection(callback: ErrorCallback) {
@@ -695,6 +709,7 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
695
709
  tokenRenewalInterval: this.tokenRenewalInterval,
696
710
  transportSettings: this._transportSettings,
697
711
  transportTimeout: this._transportTimeout,
712
+ transportFactory: this._transportFactory,
698
713
  defaultTransactionTimeout: this.defaultTransactionTimeout
699
714
  });
700
715
  secureChannel.on("backoff", (count: number, delay: number) => {
@@ -773,7 +788,7 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
773
788
  }
774
789
 
775
790
  static async createCertificate(
776
- clientCertificateManager: OPCUACertificateManager,
791
+ clientCertificateManager: ICertificateStore,
777
792
  certificateFile: string,
778
793
  applicationName: string,
779
794
  applicationUri: string
@@ -781,7 +796,11 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
781
796
  if (!fs.existsSync(certificateFile)) {
782
797
  const hostname = getHostname();
783
798
  // this.serverInfo.applicationUri!;
784
- await clientCertificateManager.createSelfSignedCertificate({
799
+ // Cast is safe: createDefaultCertificate only calls
800
+ // this in the disk path, where the manager is always
801
+ // an OPCUACertificateManager.
802
+ const cm = clientCertificateManager as unknown as OPCUACertificateManager;
803
+ await cm.createSelfSignedCertificate({
785
804
  applicationUri,
786
805
  dns: [hostname],
787
806
  // ip: await getIpAddresses(),
@@ -819,7 +838,9 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
819
838
  this._getBuiltApplicationUri()
820
839
  );
821
840
  debugLog("privateKey = ", this.privateKeyFile);
822
- debugLog(" = ", this.clientCertificateManager.privateKey);
841
+ if ("privateKey" in this.clientCertificateManager) {
842
+ debugLog(" = ", (this.clientCertificateManager as unknown as OPCUACertificateManager).privateKey);
843
+ }
823
844
  debugLog("certificateFile = ", this.certificateFile);
824
845
  const _certificate = this.getCertificate();
825
846
  const _privateKey = this.getPrivateKey();
@@ -847,10 +868,16 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
847
868
  return;
848
869
  }
849
870
  await this.clientCertificateManager.initialize();
850
- await this.createDefaultCertificate();
851
- // c8 ignore next
852
- if (!fs.existsSync(this.privateKeyFile)) {
853
- throw new Error(` cannot locate private key file ${this.privateKeyFile}`);
871
+
872
+ if (this.certificateFile === "<in-memory>" || this.certificateFile === "<unknown>") {
873
+ // In-memory provider — cert already available, skip disk operations
874
+ } else {
875
+ // Disk path — create default cert if missing
876
+ await this.createDefaultCertificate();
877
+ // c8 ignore next
878
+ if (!fs.existsSync(this.privateKeyFile)) {
879
+ throw new Error(` cannot locate private key file ${this.privateKeyFile}`);
880
+ }
854
881
  }
855
882
  if (this.isUnusable()) return;
856
883
 
@@ -1001,7 +1028,10 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
1001
1028
  // this may happen if the Server has closed the connection abruptly for some unknown reason
1002
1029
  // or if the tcp connection has been broken.
1003
1030
  callback(
1004
- new Error("performMessageTransaction: No SecureChannel , connection may have been canceled abruptly by server")
1031
+ new Error(
1032
+ "performMessageTransaction: No SecureChannel , connection may have been canceled abruptly by server" +
1033
+ " while performing " + request.schema.name
1034
+ )
1005
1035
  );
1006
1036
  return;
1007
1037
  }
@@ -1479,7 +1509,9 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
1479
1509
  })
1480
1510
  .catch((err1: Error) => {
1481
1511
  warningLog("[NODE-OPCUA-W25] client's server certificate verification has failed ", err1.message);
1482
- warningLog(" clientCertificateManager.rootDir = ", this.clientCertificateManager.rootDir);
1512
+ if ("rootDir" in this.clientCertificateManager) {
1513
+ warningLog(" clientCertificateManager.rootDir = ", (this.clientCertificateManager as unknown as { rootDir: string }).rootDir);
1514
+ }
1483
1515
 
1484
1516
  const chain = split_der(endpoint.serverCertificate);
1485
1517
  warningLog(` server certificate chain contains ${chain.length} element(s)`);
@@ -1515,7 +1547,9 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
1515
1547
  warningLog(
1516
1548
  " verify also that the issuer certificate is trusted and the issuer's certificate is present in the issuer.cert folder\n" +
1517
1549
  " of the client certificate manager located in ",
1518
- this.clientCertificateManager.rootDir
1550
+ "rootDir" in this.clientCertificateManager
1551
+ ? (this.clientCertificateManager as unknown as { rootDir: string }).rootDir
1552
+ : "<in-memory>"
1519
1553
  );
1520
1554
  } else {
1521
1555
  warningLog(
package/source/verify.ts CHANGED
@@ -1,9 +1,7 @@
1
- import type { OPCUACertificateManager } from "node-opcua-certificate-manager";
2
- import type { OPCUASecureObject } from "node-opcua-common";
1
+ import type { ICertificateStore, OPCUASecureObject } from "node-opcua-common";
3
2
 
4
3
  import { type Certificate, exploreCertificate, explorePrivateKey, publicKeyAndPrivateKeyMatches } from "node-opcua-crypto/web";
5
4
  import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
6
- import type { VerifyCertificateOptions } from "node-opcua-pki";
7
5
 
8
6
  const _doDebug = checkDebugFlag(__filename);
9
7
  const _debugLog = make_debugLog(__filename);
@@ -117,7 +115,7 @@ export function verifyIsOPCUAValidCertificate(
117
115
  export async function performCertificateSanityCheck(
118
116
  secureObject: OPCUASecureObject,
119
117
  serverOrClient: "server" | "client",
120
- certificateManager: OPCUACertificateManager,
118
+ certificateStore: ICertificateStore,
121
119
  applicationUri: string
122
120
  ): Promise<void> {
123
121
  // verify that certificate is matching private key, and inform the developer if not
@@ -128,7 +126,6 @@ export async function performCertificateSanityCheck(
128
126
  errorLog("[NODE-OPCUA-E01] Configuration error : the certificate and the private key do not match !");
129
127
  errorLog(" please check the configuration of the OPCUA Server");
130
128
  errorLog(" privateKey= ", secureObject.privateKeyFile);
131
- errorLog(" certificateManager.privateKey= ", certificateManager.privateKey);
132
129
  errorLog(" certificateFile= ", secureObject.certificateFile);
133
130
  throw new Error(
134
131
  "[NODE-OPCUA-E01] Configuration error : the certificate and the private key do not match ! please fix your configuration"
@@ -145,13 +142,11 @@ export async function performCertificateSanityCheck(
145
142
  );
146
143
  }
147
144
 
148
- const options: VerifyCertificateOptions = {
149
- acceptOutdatedCertificate: false,
150
- acceptOutDatedIssuerCertificate: false,
151
- acceptPendingCertificate: false
145
+ const options = {
146
+ acceptOutdatedCertificate: false
152
147
  };
153
148
 
154
- const status = await certificateManager.verifyCertificate(certificate, options);
149
+ const status = await certificateStore.verifyCertificate(certificate, options);
155
150
 
156
151
  // BadCertificateUntrusted is expected for the application's own
157
152
  // certificate — it does not need to be in its own trust list.