node-opcua-client 2.175.6 → 2.177.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.
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClientReverseConnect = void 0;
4
+ /**
5
+ * @module node-opcua-client
6
+ *
7
+ * Client-side listener for OPC UA Reverse Connect (OPC UA Part 6 §7.1.3).
8
+ *
9
+ * A `ClientReverseConnect` owns a single TCP listener (`net.Server`). Servers dial into it and
10
+ * send a ReverseHello ("RHE"). The listener reads and validates the RHE, then hands the accepted
11
+ * socket to a waiting `OPCUAClient` (registered through {@link waitForConnection}), matched by
12
+ * `ServerUri` / `EndpointUrl`. This mirrors the OPC Foundation UA-.NETStandard
13
+ * `ReverseConnectManager`: one listener, many clients, demultiplexed by server identity.
14
+ *
15
+ * Security (Part 2 §6.14): reverse connect lets a Server open a socket to the Client, which is a
16
+ * denial-of-service surface the normal (client-initiated) connection does not have. The listener
17
+ * therefore (a) validates every inbound RHE against the caller's {@link ReverseConnectExpectation},
18
+ * (b) times out sockets that do not send a valid RHE quickly, and (c) caps the number of
19
+ * inbound sockets it is holding on to. Full OPC UA security (message security mode, certificate
20
+ * trust) is still enforced afterwards on the SecureChannel.
21
+ *
22
+ * Every accepted socket keeps a durable `error`/`close` guard for as long as the listener owns it,
23
+ * so a peer reset while a socket is being read or held can never surface as an unhandled `'error'`
24
+ * event (which Node would turn into a process-wide uncaught exception).
25
+ */
26
+ const node_net_1 = require("node:net");
27
+ const node_opcua_debug_1 = require("node-opcua-debug");
28
+ const node_opcua_transport_1 = require("node-opcua-transport");
29
+ const doDebug = (0, node_opcua_debug_1.checkDebugFlag)("ReverseConnect");
30
+ const debugLog = (0, node_opcua_debug_1.make_debugLog)("ReverseConnect");
31
+ const warningLog = (0, node_opcua_debug_1.make_warningLog)("ReverseConnect");
32
+ // header (8) + 2 × (UInt32 length prefix (4) + up to 4096 bytes). Generous fixed cap for the RHE frame.
33
+ const MAX_RHE_FRAME_LENGTH = 8 + 2 * (4 + node_opcua_transport_1.MAXIMUM_REVERSE_HELLO_FIELD_LENGTH);
34
+ const DEFAULT_ACCEPT_TIMEOUT = 120000; // spec: an application shall time out sockets that do not send RHE (≤ 2 min)
35
+ const DEFAULT_MAX_PENDING_SOCKETS = 20;
36
+ const DEFAULT_MATCH_TIMEOUT = 30000; // how long to hold a validated connection waiting for a matching client
37
+ function matches(expectation, accepted) {
38
+ if (!expectation) {
39
+ return true;
40
+ }
41
+ if (expectation.serverUri && expectation.serverUri !== accepted.serverUri) {
42
+ return false;
43
+ }
44
+ if (expectation.endpointUrl && expectation.endpointUrl !== accepted.endpointUrl) {
45
+ return false;
46
+ }
47
+ return true;
48
+ }
49
+ class ClientReverseConnect {
50
+ #connectionUrl;
51
+ #acceptTimeout;
52
+ #maxPendingSockets;
53
+ #matchTimeout;
54
+ #server;
55
+ #listeningPort = 0;
56
+ /** every socket the listener currently owns (being read, or held) — NOT sockets already handed to a transport */
57
+ #sockets = new Set();
58
+ #waiters = [];
59
+ #held = [];
60
+ constructor(options) {
61
+ const opts = typeof options === "string" ? { connectionUrl: options } : options;
62
+ this.#connectionUrl = opts.connectionUrl;
63
+ this.#acceptTimeout = opts.acceptTimeout ?? DEFAULT_ACCEPT_TIMEOUT;
64
+ this.#maxPendingSockets = opts.maxPendingSockets ?? DEFAULT_MAX_PENDING_SOCKETS;
65
+ this.#matchTimeout = opts.matchTimeout ?? DEFAULT_MATCH_TIMEOUT;
66
+ }
67
+ get connectionUrl() {
68
+ return this.#connectionUrl;
69
+ }
70
+ get listeningPort() {
71
+ return this.#listeningPort;
72
+ }
73
+ start() {
74
+ if (this.#server) {
75
+ return Promise.resolve();
76
+ }
77
+ const ep = (0, node_opcua_transport_1.parseEndpointUrl)(this.#connectionUrl);
78
+ const port = parseInt(ep.port || "4840", 10);
79
+ const host = ep.hostname;
80
+ const server = (0, node_net_1.createServer)((socket) => this.#handleIncomingSocket(socket));
81
+ this.#server = server;
82
+ // Permanent server-level error handler: net.Server can emit "error" AFTER a successful
83
+ // listen() (e.g. EMFILE/ENFILE on accept). Without a listener Node throws an uncaught
84
+ // exception and crashes the process, so keep one attached for the server's whole life.
85
+ const onServerError = (err) => {
86
+ // c8 ignore next
87
+ warningLog(`ClientReverseConnect server error: ${err.message}`);
88
+ };
89
+ server.on("error", onServerError);
90
+ return new Promise((resolve, reject) => {
91
+ const onListenError = (err) => {
92
+ server.removeListener("error", onServerError);
93
+ this.#server = undefined;
94
+ reject(err);
95
+ };
96
+ server.once("error", onListenError);
97
+ server.listen(port, host, () => {
98
+ server.removeListener("error", onListenError);
99
+ const addr = server.address();
100
+ this.#listeningPort = addr && typeof addr === "object" ? addr.port : port;
101
+ // c8 ignore next
102
+ doDebug && debugLog(`ClientReverseConnect listening on ${host}:${this.#listeningPort}`);
103
+ resolve();
104
+ });
105
+ });
106
+ }
107
+ stop() {
108
+ // reject any pending waiters
109
+ const waiters = this.#waiters;
110
+ this.#waiters = [];
111
+ for (const w of waiters) {
112
+ w.callback(new Error("ClientReverseConnect has been stopped"));
113
+ }
114
+ this.#held = [];
115
+ // destroy every socket the listener still owns (being read or held). Their durable
116
+ // error/close guards call forget(), which is a no-op once we clear the set below, so no
117
+ // dangling entries remain. This also unblocks server.close(), which otherwise waits for
118
+ // all live connections to end.
119
+ const sockets = [...this.#sockets];
120
+ this.#sockets.clear();
121
+ for (const socket of sockets) {
122
+ socket.destroy();
123
+ }
124
+ const server = this.#server;
125
+ this.#server = undefined;
126
+ if (!server) {
127
+ return Promise.resolve();
128
+ }
129
+ return new Promise((resolve) => server.close(() => resolve()));
130
+ }
131
+ /**
132
+ * Register interest in the next reverse connection matching `expectation`.
133
+ * The callback fires once with an accepted, RHE-validated connection (or an error).
134
+ */
135
+ waitForConnection(expectation, callback) {
136
+ // a validated connection may already be waiting
137
+ for (let i = 0; i < this.#held.length; i++) {
138
+ const h = this.#held[i];
139
+ if (matches(expectation, h.accepted)) {
140
+ this.#held.splice(i, 1);
141
+ h.handOff();
142
+ callback(null, h.accepted);
143
+ return { cancel: () => undefined };
144
+ }
145
+ }
146
+ const waiter = { expectation, callback };
147
+ this.#waiters.push(waiter);
148
+ return {
149
+ cancel: () => {
150
+ const idx = this.#waiters.indexOf(waiter);
151
+ if (idx >= 0) {
152
+ this.#waiters.splice(idx, 1);
153
+ }
154
+ }
155
+ };
156
+ }
157
+ #handleIncomingSocket(socket) {
158
+ // Track the socket and attach durable error/close guards FIRST, before any rejection path,
159
+ // so the socket is never without an "error" listener (an unhandled "error" would crash the
160
+ // whole process).
161
+ this.#sockets.add(socket);
162
+ let handedOff = false;
163
+ let removed = false;
164
+ let timer;
165
+ let buffer = Buffer.alloc(0);
166
+ const forget = () => {
167
+ if (removed) {
168
+ return;
169
+ }
170
+ removed = true;
171
+ if (timer) {
172
+ clearTimeout(timer);
173
+ timer = undefined;
174
+ }
175
+ this.#sockets.delete(socket);
176
+ const hi = this.#held.findIndex((h) => h.accepted.socket === socket);
177
+ if (hi >= 0) {
178
+ this.#held.splice(hi, 1);
179
+ }
180
+ };
181
+ // durable guards: kept for the socket's whole life in the listener. After hand-off they
182
+ // become no-ops (the adopting transport installs its own handlers), but stay attached so
183
+ // there is never a window with zero "error" listeners.
184
+ const onError = () => {
185
+ if (handedOff || removed) {
186
+ return;
187
+ }
188
+ forget();
189
+ socket.destroy();
190
+ };
191
+ const onClose = () => {
192
+ if (handedOff) {
193
+ return;
194
+ }
195
+ forget();
196
+ };
197
+ socket.on("error", onError);
198
+ socket.on("close", onClose);
199
+ // DoS guard: bound the number of sockets the listener holds (being read + held).
200
+ if (this.#sockets.size > this.#maxPendingSockets) {
201
+ warningLog("ClientReverseConnect: too many pending reverse connections, rejecting");
202
+ this.#rejectSocket(socket, forget, node_opcua_transport_1.StatusCodes2.BadTcpServerTooBusy, "too many pending reverse connections");
203
+ return;
204
+ }
205
+ socket.setNoDelay(true);
206
+ timer = setTimeout(() => {
207
+ this.#rejectSocket(socket, forget, node_opcua_transport_1.StatusCodes2.BadTimeout, "no ReverseHello received in time");
208
+ }, this.#acceptTimeout);
209
+ timer.unref?.();
210
+ const onData = (chunk) => {
211
+ if (removed || handedOff) {
212
+ return;
213
+ }
214
+ buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]);
215
+ if (buffer.length < 8) {
216
+ return;
217
+ }
218
+ const declaredLength = (0, node_opcua_transport_1.readRawMessageHeader)(buffer).length;
219
+ if (declaredLength > MAX_RHE_FRAME_LENGTH) {
220
+ socket.removeListener("data", onData);
221
+ this.#rejectSocket(socket, forget, node_opcua_transport_1.StatusCodes2.BadTcpMessageTooLarge, "ReverseHello message too large");
222
+ return;
223
+ }
224
+ if (buffer.length < declaredLength) {
225
+ return; // wait for the rest of the frame
226
+ }
227
+ const frame = buffer.subarray(0, declaredLength);
228
+ const leftover = buffer.subarray(declaredLength);
229
+ // stop reading the RHE and the accept timer; keep the error/close guards attached.
230
+ socket.removeListener("data", onData);
231
+ if (timer) {
232
+ clearTimeout(timer);
233
+ timer = undefined;
234
+ }
235
+ // pause before handing the socket over, then push back any bytes read past the RHE frame
236
+ socket.pause();
237
+ if (leftover.length > 0) {
238
+ socket.unshift(leftover);
239
+ }
240
+ let serverUri = "";
241
+ let endpointUrl = "";
242
+ try {
243
+ const rhe = (0, node_opcua_transport_1.decodeReverseHello)(frame);
244
+ serverUri = rhe.serverUri || "";
245
+ endpointUrl = rhe.endpointUrl || "";
246
+ }
247
+ catch (err) {
248
+ const statusCode = err.statusCode || node_opcua_transport_1.StatusCodes2.BadTcpEndpointUrlInvalid;
249
+ this.#rejectSocket(socket, forget, statusCode, err instanceof Error ? err.message : "invalid ReverseHello");
250
+ return;
251
+ }
252
+ const accepted = { socket, serverUri, endpointUrl };
253
+ const handOff = () => {
254
+ handedOff = true;
255
+ forget(); // release listener ownership; durable guards remain as no-ops
256
+ };
257
+ // immediate match?
258
+ for (let i = 0; i < this.#waiters.length; i++) {
259
+ if (matches(this.#waiters[i].expectation, accepted)) {
260
+ const [waiter] = this.#waiters.splice(i, 1);
261
+ handOff();
262
+ waiter.callback(null, accepted);
263
+ return;
264
+ }
265
+ }
266
+ // no waiter yet: hold the validated connection briefly in case a client registers imminently.
267
+ // c8 ignore next
268
+ doDebug && debugLog(`reverse connection from ${serverUri} held (no matching client yet)`);
269
+ timer = setTimeout(() => {
270
+ timer = undefined;
271
+ this.#rejectSocket(socket, forget, node_opcua_transport_1.StatusCodes2.BadTcpServerTooBusy, "no client is waiting for this reverse connection");
272
+ }, this.#matchTimeout);
273
+ timer.unref?.();
274
+ this.#held.push({ accepted, handOff });
275
+ };
276
+ socket.on("data", onData);
277
+ }
278
+ #rejectSocket(socket, forget, statusCode, reason) {
279
+ forget();
280
+ // destroy only AFTER the ERR has been flushed to the kernel, otherwise an immediate
281
+ // destroy() would discard the buffered write and the peer would never see the reason.
282
+ // The socket's durable "error" guard is still attached, so a failed write cannot crash.
283
+ let destroyed = false;
284
+ const destroyOnce = () => {
285
+ if (destroyed) {
286
+ return;
287
+ }
288
+ destroyed = true;
289
+ socket.destroy();
290
+ };
291
+ try {
292
+ const errorMessage = new node_opcua_transport_1.TCPErrorMessage({ statusCode, reason });
293
+ socket.write((0, node_opcua_transport_1.packTcpMessage)("ERR", errorMessage), () => destroyOnce());
294
+ // safety net in case the write callback never fires (e.g. socket already broken)
295
+ setTimeout(destroyOnce, 1000).unref?.();
296
+ }
297
+ catch {
298
+ destroyOnce();
299
+ }
300
+ }
301
+ }
302
+ exports.ClientReverseConnect = ClientReverseConnect;
303
+ //# sourceMappingURL=client_reverse_connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client_reverse_connect.js","sourceRoot":"","sources":["../../source/reverse/client_reverse_connect.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-opcua-client",
3
- "version": "2.175.6",
3
+ "version": "2.177.0",
4
4
  "description": "pure nodejs OPCUA SDK - module client",
5
5
  "scripts": {
6
6
  "clean": "npx rimraf -g node_modules dist *.tsbuildinfo certificates",
@@ -16,54 +16,54 @@
16
16
  "dependencies": {
17
17
  "@ster5/global-mutex": "^3.3.0",
18
18
  "chalk": "4.1.2",
19
- "node-opcua-alarm-condition": "2.175.6",
19
+ "node-opcua-alarm-condition": "2.177.0",
20
20
  "node-opcua-assert": "2.175.6",
21
- "node-opcua-basic-types": "2.175.6",
22
- "node-opcua-buffer-utils": "2.175.6",
23
- "node-opcua-certificate-manager": "2.175.6",
24
- "node-opcua-client-dynamic-extension-object": "2.175.6",
25
- "node-opcua-common": "2.175.6",
26
- "node-opcua-constants": "2.175.1",
27
- "node-opcua-crypto": "5.4.0",
28
- "node-opcua-data-model": "2.175.6",
29
- "node-opcua-data-value": "2.175.6",
30
- "node-opcua-date-time": "2.175.6",
21
+ "node-opcua-basic-types": "2.177.0",
22
+ "node-opcua-buffer-utils": "2.176.0",
23
+ "node-opcua-certificate-manager": "2.177.0",
24
+ "node-opcua-client-dynamic-extension-object": "2.177.0",
25
+ "node-opcua-common": "2.177.0",
26
+ "node-opcua-constants": "2.176.0",
27
+ "node-opcua-crypto": "5.5.0",
28
+ "node-opcua-data-model": "2.177.0",
29
+ "node-opcua-data-value": "2.177.0",
30
+ "node-opcua-date-time": "2.176.0",
31
31
  "node-opcua-debug": "2.175.6",
32
- "node-opcua-extension-object": "2.175.6",
32
+ "node-opcua-extension-object": "2.177.0",
33
33
  "node-opcua-hostname": "2.175.1",
34
- "node-opcua-nodeid": "2.175.6",
35
- "node-opcua-numeric-range": "2.175.6",
34
+ "node-opcua-nodeid": "2.177.0",
35
+ "node-opcua-numeric-range": "2.177.0",
36
36
  "node-opcua-object-registry": "2.175.6",
37
- "node-opcua-pki": "6.19.0",
38
- "node-opcua-pseudo-session": "2.175.6",
39
- "node-opcua-schemas": "2.175.6",
40
- "node-opcua-secure-channel": "2.175.6",
41
- "node-opcua-service-browse": "2.175.6",
42
- "node-opcua-service-call": "2.175.6",
43
- "node-opcua-service-discovery": "2.175.6",
44
- "node-opcua-service-endpoints": "2.175.6",
45
- "node-opcua-service-filter": "2.175.6",
46
- "node-opcua-service-history": "2.175.6",
47
- "node-opcua-service-query": "2.175.6",
48
- "node-opcua-service-read": "2.175.6",
49
- "node-opcua-service-register-node": "2.175.6",
50
- "node-opcua-service-secure-channel": "2.175.6",
51
- "node-opcua-service-session": "2.175.6",
52
- "node-opcua-service-subscription": "2.175.6",
53
- "node-opcua-service-translate-browse-path": "2.175.6",
54
- "node-opcua-service-write": "2.175.6",
55
- "node-opcua-status-code": "2.175.6",
56
- "node-opcua-transport": "2.175.6",
57
- "node-opcua-types": "2.175.6",
58
- "node-opcua-utils": "2.175.6",
59
- "node-opcua-variant": "2.175.6",
37
+ "node-opcua-pki": "6.19.1",
38
+ "node-opcua-pseudo-session": "2.177.0",
39
+ "node-opcua-schemas": "2.177.0",
40
+ "node-opcua-secure-channel": "2.177.0",
41
+ "node-opcua-service-browse": "2.177.0",
42
+ "node-opcua-service-call": "2.177.0",
43
+ "node-opcua-service-discovery": "2.177.0",
44
+ "node-opcua-service-endpoints": "2.177.0",
45
+ "node-opcua-service-filter": "2.177.0",
46
+ "node-opcua-service-history": "2.177.0",
47
+ "node-opcua-service-query": "2.177.0",
48
+ "node-opcua-service-read": "2.177.0",
49
+ "node-opcua-service-register-node": "2.177.0",
50
+ "node-opcua-service-secure-channel": "2.177.0",
51
+ "node-opcua-service-session": "2.177.0",
52
+ "node-opcua-service-subscription": "2.177.0",
53
+ "node-opcua-service-translate-browse-path": "2.177.0",
54
+ "node-opcua-service-write": "2.177.0",
55
+ "node-opcua-status-code": "2.176.0",
56
+ "node-opcua-transport": "2.177.0",
57
+ "node-opcua-types": "2.177.0",
58
+ "node-opcua-utils": "2.176.0",
59
+ "node-opcua-variant": "2.177.0",
60
60
  "thenify-ex": "4.4.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "dequeue": "^1.0.5",
64
- "node-opcua-address-space": "2.175.6",
65
- "node-opcua-binary-stream": "2.175.6",
66
- "node-opcua-factory": "2.175.6",
64
+ "node-opcua-address-space": "2.177.0",
65
+ "node-opcua-binary-stream": "2.176.0",
66
+ "node-opcua-factory": "2.177.0",
67
67
  "node-opcua-leak-detector": "2.175.6",
68
68
  "node-opcua-nodesets": "2.175.3"
69
69
  },
@@ -87,7 +87,7 @@
87
87
  "internet of things"
88
88
  ],
89
89
  "homepage": "http://node-opcua.github.io/",
90
- "gitHead": "e233d906138995583f42359831d1908e3cb005e7",
90
+ "gitHead": "a00d1d3f4db4522ed6bb2c6104d86c57df79946a",
91
91
  "files": [
92
92
  "dist",
93
93
  "source"
@@ -20,6 +20,7 @@ import type { ChannelSecurityToken, MessageSecurityMode } from "node-opcua-servi
20
20
  import type { ErrorCallback } from "node-opcua-status-code";
21
21
  import type { IClientTransportFactory } from "node-opcua-transport";
22
22
  import type { Request, Response } from "./common";
23
+ import type { ClientReverseConnect, ReverseConnectExpectation } from "./reverse/client_reverse_connect";
23
24
 
24
25
  export type FindServersRequestLike = FindServersRequestOptions;
25
26
  export type FindServersOnNetworkRequestLike = FindServersOnNetworkRequestOptions;
@@ -235,6 +236,20 @@ export interface OPCUAClientBase<Events extends OPCUAClientBaseEvents = OPCUACli
235
236
 
236
237
  connect(endpointUrl: string, callback: ErrorCallback): void;
237
238
 
239
+ /***
240
+ * Wait for a Server to initiate a Reverse Connection (OPC UA Part 6 §7.1.3).
241
+ *
242
+ * Instead of dialing the server, the client waits for the server to dial into the
243
+ * (already started) `reverseConnect` listener and send a ReverseHello. The optional
244
+ * `expectation` restricts which server may connect (matched by ServerUri / EndpointUrl).
245
+ *
246
+ * For secured connections the server certificate must be supplied up front
247
+ * (`serverCertificate` option) since the client cannot dial the server to fetch it.
248
+ */
249
+ connectReverse(reverseConnect: ClientReverseConnect, expectation?: ReverseConnectExpectation): Promise<void>;
250
+ connectReverse(reverseConnect: ClientReverseConnect, callback: ErrorCallback): void;
251
+ connectReverse(reverseConnect: ClientReverseConnect, expectation: ReverseConnectExpectation, callback: ErrorCallback): void;
252
+
238
253
  /***
239
254
  * causes the client to close and disconnect the communication with server
240
255
  *
package/source/index.ts CHANGED
@@ -37,6 +37,7 @@ export * from "./client_session";
37
37
  export * from "./client_subscription";
38
38
  export * from "./client_utils";
39
39
  export * from "./opcua_client";
40
+ export * from "./reverse/client_reverse_connect";
40
41
  export { ClientSidePublishEngine } from "./private/client_publish_engine";
41
42
  export * from "./tools/findservers";
42
43
  export * from "./tools/read_history_server_capabilities";
@@ -27,7 +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
+ import { type IAcceptedReverseConnection, type IClientTransportFactory, makeReverseClientTransportFactory } from "node-opcua-transport";
31
31
  import {
32
32
  FindServersOnNetworkRequest,
33
33
  type FindServersOnNetworkRequestOptions,
@@ -60,6 +60,7 @@ import {
60
60
  type TransportSettings
61
61
  } from "../client_base";
62
62
  import type { Request, Response } from "../common";
63
+ import type { ClientReverseConnect, ICancelableRegistration, ReverseConnectExpectation } from "../reverse/client_reverse_connect";
63
64
  import type { UserIdentityInfo } from "../user_identity_info";
64
65
  import { performCertificateSanityCheck } from "../verify";
65
66
  import type { ClientSessionImpl } from "./client_session_impl";
@@ -389,6 +390,10 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
389
390
  private _transportSettings: TransportSettings;
390
391
  private _transportTimeout?: number;
391
392
  private _transportFactory?: IClientTransportFactory;
393
+ /** true once connectReverse() has been used — suppresses outbound dials that would defeat reverse connect */
394
+ private _isReverseConnect = false;
395
+ /** the current reverse-connect waiter registration, cancelled on disconnect to avoid a leaked waiter */
396
+ private _reverseConnectRegistration?: ICancelableRegistration;
392
397
 
393
398
  public clientCertificateManager: ICertificateStore;
394
399
 
@@ -630,8 +635,13 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
630
635
  }
631
636
 
632
637
  if (
633
- err?.message.match("BadCertificateInvalid") ||
634
- err?.message.match(/socket has been disconnected by third party/)
638
+ // In reverse-connect mode we must NOT fetchServerCertificate(): that dials the
639
+ // server directly (via a temp client), which is impossible for a firewalled
640
+ // reverse-connect server and would hang reconnection. Fall through to the plain
641
+ // retry, which re-arms the reverse transport and waits for the server to re-dial.
642
+ !this._isReverseConnect &&
643
+ (err?.message.match("BadCertificateInvalid") ||
644
+ err?.message.match(/socket has been disconnected by third party/))
635
645
  ) {
636
646
  // it is possible also that hte server has shutdown innapropriately the connection
637
647
  warningLog(
@@ -892,12 +902,20 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
892
902
 
893
903
  protected _handleUnrecoverableConnectionFailure(err: Error, callback: ErrorCallback): void {
894
904
  debugLog(err.message);
905
+ // Terminal connect failure: make sure the client is removed from the leak registry. A
906
+ // synchronous throw out of _connectStep2 (e.g. an invalid SecureChannel config) lands here
907
+ // WITHOUT the error-branch unregister ever running, which would otherwise leak the client.
908
+ // unregister() is idempotent (a no-op when the client was never registered or already removed).
909
+ OPCUAClientBase.registry.unregister(this);
895
910
  this.emit("connection_failed", err);
896
911
  this._setInternalState("disconnected");
897
912
  callback(err);
898
913
  }
899
914
  private _handleDisconnectionWhileConnecting(err: Error, callback: ErrorCallback) {
900
915
  debugLog(err.message);
916
+ // see _handleUnrecoverableConnectionFailure: idempotent unregister guards against a leaked
917
+ // client when a connect attempt fails before the error-branch unregister runs.
918
+ OPCUAClientBase.registry.unregister(this);
901
919
  this.emit("connection_failed", err);
902
920
  this._setInternalState("disconnected");
903
921
  callback(err);
@@ -975,6 +993,94 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
975
993
  });
976
994
  }
977
995
 
996
+ public connectReverse(reverseConnect: ClientReverseConnect, expectation?: ReverseConnectExpectation): Promise<void>;
997
+ public connectReverse(reverseConnect: ClientReverseConnect, callback: ErrorCallback): void;
998
+ public connectReverse(reverseConnect: ClientReverseConnect, expectation: ReverseConnectExpectation, callback: ErrorCallback): void;
999
+ public connectReverse(...args: unknown[]): Promise<void> | void {
1000
+ const reverseConnect = args[0] as ClientReverseConnect;
1001
+ const callback = args[args.length - 1] as ErrorCallback;
1002
+ const expectation = (args.length >= 3 ? args[1] : undefined) as ReverseConnectExpectation | undefined;
1003
+ assert(typeof callback === "function", "expecting a callback");
1004
+
1005
+ if (!reverseConnect) {
1006
+ callback(new Error("connectReverse expects a ClientReverseConnect instance"));
1007
+ return;
1008
+ }
1009
+
1010
+ // c8 ignore next
1011
+ if (this._internalState !== "disconnected") {
1012
+ callback(new Error(`client#connectReverse failed, as invalid internal state = ${this._internalState}`));
1013
+ return;
1014
+ }
1015
+ if (this._secureChannel !== null) {
1016
+ setImmediate(() => callback(new Error("connect already called")));
1017
+ return;
1018
+ }
1019
+
1020
+ // A reverse-connect client cannot dial the server to fetch its certificate (that is the whole
1021
+ // point of reverse connect), so for a secure channel the server certificate MUST be supplied up
1022
+ // front via OPCUAClientOptions.serverCertificate. Reject early with a clear message rather than
1023
+ // letting the SecureChannel layer fail deep inside with an opaque error (which also used to leak
1024
+ // the client, since that failure path never reached the connect-failure unregister).
1025
+ if (this.securityMode !== MessageSecurityMode.None && !this.serverCertificate) {
1026
+ callback(
1027
+ new Error(
1028
+ "connectReverse: a secure connection (securityMode=" +
1029
+ MessageSecurityMode[this.securityMode] +
1030
+ ") requires the server certificate to be supplied up front " +
1031
+ "(OPCUAClientOptions.serverCertificate), because a reverse-connect client cannot dial the server to fetch it."
1032
+ )
1033
+ );
1034
+ return;
1035
+ }
1036
+
1037
+ this._isReverseConnect = true;
1038
+
1039
+ // Wire a reverse-connect transport factory. Because the SecureChannel layer may call
1040
+ // transport.connect() several times (backoff / reconnection), the provider is invoked on
1041
+ // EACH attempt so every attempt waits for the server to (re-)dial in. We keep the current
1042
+ // waiter registration so it can be cancelled on disconnect — otherwise a leaked waiter would
1043
+ // later hijack an incoming server socket on this (by-then disposed) transport.
1044
+ this._transportFactory = makeReverseClientTransportFactory(
1045
+ () =>
1046
+ new Promise<IAcceptedReverseConnection>((resolve, reject) => {
1047
+ // drop any stale waiter from a previous attempt before registering a new one
1048
+ this._reverseConnectRegistration?.cancel();
1049
+ this._reverseConnectRegistration = reverseConnect.waitForConnection(expectation, (err, accepted) => {
1050
+ this._reverseConnectRegistration = undefined;
1051
+ if (err || !accepted) {
1052
+ reject(err || new Error("no accepted reverse connection"));
1053
+ return;
1054
+ }
1055
+ // reconcile our endpointUrl with the one advertised in the ReverseHello, so
1056
+ // subsequent session creation uses the real URL rather than the placeholder below.
1057
+ this.endpointUrl = accepted.endpointUrl || this.endpointUrl;
1058
+ resolve(accepted);
1059
+ });
1060
+ })
1061
+ );
1062
+
1063
+ // Placeholder endpoint URL for the SecureChannel handshake; the reverse transport overrides
1064
+ // it from the RHE. Prefer an explicitly-expected URL when the caller provided one.
1065
+ const placeholderEndpointUrl = expectation?.endpointUrl || reverseConnect.connectionUrl || "opc.tcp://reverse-connect";
1066
+
1067
+ this._setInternalState("connecting");
1068
+
1069
+ this.initializeCM()
1070
+ .then(() => {
1071
+ debugLog("ClientBaseImpl#connectReverse ", placeholderEndpointUrl, this.clientName);
1072
+ if (this._internalState === "disconnecting" || this._internalState === "disconnected") {
1073
+ return this._handleDisconnectionWhileConnecting(new Error("premature disconnection 1"), callback);
1074
+ }
1075
+ // NOTE: unlike connect(), we deliberately do NOT fetchServerCertificate() here — that
1076
+ // would dial the server directly, defeating the purpose of reverse connect.
1077
+ this._connectStep2(placeholderEndpointUrl, callback);
1078
+ })
1079
+ .catch((err) => {
1080
+ return this._handleUnrecoverableConnectionFailure(err, callback);
1081
+ });
1082
+ }
1083
+
978
1084
  /**
979
1085
  * @private
980
1086
  */
@@ -1289,6 +1395,10 @@ export class ClientBaseImpl<Events extends OPCUAClientBaseEvents = OPCUAClientBa
1289
1395
  const callback = args[0] as ErrorCallback;
1290
1396
  assert(typeof callback === "function", "expecting a callback function here");
1291
1397
  this._reconnectionIsCanceled = true;
1398
+ // cancel any outstanding reverse-connect waiter so it cannot later hijack an incoming
1399
+ // server socket on this (now disconnecting) client.
1400
+ this._reverseConnectRegistration?.cancel();
1401
+ this._reverseConnectRegistration = undefined;
1292
1402
  if (this._tmpClient) {
1293
1403
  warningLog("disconnecting client while tmpClient exists", this._tmpClient.clientName);
1294
1404
  this._tmpClient.disconnect((_err) => {
@@ -1888,6 +1998,7 @@ class TmpClient extends ClientBaseImpl {
1888
1998
  import { withCallback } from "thenify-ex";
1889
1999
 
1890
2000
  ClientBaseImpl.prototype.connect = withCallback(ClientBaseImpl.prototype.connect);
2001
+ ClientBaseImpl.prototype.connectReverse = withCallback(ClientBaseImpl.prototype.connectReverse);
1891
2002
  ClientBaseImpl.prototype.disconnect = withCallback(ClientBaseImpl.prototype.disconnect);
1892
2003
  ClientBaseImpl.prototype.getEndpoints = withCallback(ClientBaseImpl.prototype.getEndpoints);
1893
2004
  ClientBaseImpl.prototype.findServers = withCallback(ClientBaseImpl.prototype.findServers);