node-opcua-transport 2.170.0 → 2.173.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.
- package/dist/source/client_tcp_transport.d.ts +4 -39
- package/dist/source/client_tcp_transport.js +7 -158
- package/dist/source/client_tcp_transport.js.map +1 -1
- package/dist/source/client_transport_base.d.ts +64 -0
- package/dist/source/client_transport_base.js +183 -0
- package/dist/source/client_transport_base.js.map +1 -0
- package/dist/source/default_client_transport_factory.d.ts +12 -0
- package/dist/source/default_client_transport_factory.js +20 -0
- package/dist/source/default_client_transport_factory.js.map +1 -0
- package/dist/source/i_client_transport.d.ts +90 -0
- package/dist/source/i_client_transport.js +6 -0
- package/dist/source/i_client_transport.js.map +1 -0
- package/dist/source/index.browser.d.ts +62 -0
- package/dist/source/index.browser.js +79 -0
- package/dist/source/index.browser.js.map +1 -0
- package/dist/source/index.d.ts +3 -0
- package/dist/source/index.js +3 -0
- package/dist/source/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +33 -13
- package/source/client_tcp_transport.ts +16 -217
- package/source/client_transport_base.ts +238 -0
- package/source/default_client_transport_factory.ts +19 -0
- package/source/i_client_transport.ts +134 -0
- package/source/index.browser.ts +62 -0
- package/source/index.ts +3 -0
|
@@ -8,28 +8,27 @@ import { types } from "node:util";
|
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
|
|
10
10
|
import { assert } from "node-opcua-assert";
|
|
11
|
-
import {
|
|
12
|
-
import { readMessageHeader } from "node-opcua-chunkmanager";
|
|
13
|
-
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
|
|
11
|
+
import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";
|
|
14
12
|
import type { ErrorCallback } from "node-opcua-status-code";
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { getFakeTransport, type ISocketLike
|
|
19
|
-
import {
|
|
20
|
-
import { doTraceHelloAck } from "./utils";
|
|
13
|
+
|
|
14
|
+
import { ClientTransportBase } from "./client_transport_base";
|
|
15
|
+
import type { TransportSettingsOptions } from "./i_client_transport";
|
|
16
|
+
import { getFakeTransport, type ISocketLike } from "./tcp_transport";
|
|
17
|
+
import { parseEndpointUrl } from "./tools";
|
|
21
18
|
|
|
22
19
|
const doDebug = checkDebugFlag(__filename);
|
|
23
20
|
const debugLog = make_debugLog(__filename);
|
|
24
|
-
const warningLog = make_warningLog(__filename);
|
|
25
21
|
const errorLog = make_errorLog(__filename);
|
|
26
22
|
const gHostname = os.hostname();
|
|
27
23
|
|
|
24
|
+
// Re-exported for source-level backwards compatibility: this type used to live in this file.
|
|
25
|
+
export type { TransportSettingsOptions };
|
|
26
|
+
|
|
28
27
|
function createClientSocket(endpointUrl: string, timeout: number): ISocketLike {
|
|
29
28
|
// create a socket based on Url
|
|
30
29
|
const ep = parseEndpointUrl(endpointUrl);
|
|
31
|
-
const port = parseInt(ep.port
|
|
32
|
-
const hostname = ep.hostname
|
|
30
|
+
const port = parseInt(ep.port || "4840", 10);
|
|
31
|
+
const hostname = ep.hostname;
|
|
33
32
|
|
|
34
33
|
let socket: ISocketLike;
|
|
35
34
|
switch (ep.protocol) {
|
|
@@ -53,29 +52,6 @@ function createClientSocket(endpointUrl: string, timeout: number): ISocketLike {
|
|
|
53
52
|
}
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
|
-
export interface ClientTCP_transport {
|
|
57
|
-
on(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
|
|
58
|
-
on(eventName: "close", eventHandler: (err: Error | null) => void): this;
|
|
59
|
-
on(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
|
|
60
|
-
on(eventName: "connect", eventHandler: () => void): this;
|
|
61
|
-
|
|
62
|
-
once(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
|
|
63
|
-
once(eventName: "close", eventHandler: (err: Error | null) => void): this;
|
|
64
|
-
once(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
|
|
65
|
-
once(eventName: "connect", eventHandler: () => void): this;
|
|
66
|
-
|
|
67
|
-
emit(eventName: "chunk", messageChunk: Buffer): boolean;
|
|
68
|
-
emit(eventName: "close", err?: Error | null): boolean;
|
|
69
|
-
emit(eventName: "connection_break", err?: Error | null): boolean;
|
|
70
|
-
emit(eventName: "connect"): boolean;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export interface TransportSettingsOptions {
|
|
74
|
-
maxChunkCount?: number;
|
|
75
|
-
maxMessageSize?: number;
|
|
76
|
-
receiveBufferSize?: number;
|
|
77
|
-
sendBufferSize?: number;
|
|
78
|
-
}
|
|
79
55
|
|
|
80
56
|
/**
|
|
81
57
|
* a ClientTCP_transport connects to a remote server socket and
|
|
@@ -112,50 +88,7 @@ export interface TransportSettingsOptions {
|
|
|
112
88
|
*
|
|
113
89
|
*
|
|
114
90
|
*/
|
|
115
|
-
|
|
116
|
-
export class ClientTCP_transport extends TCP_transport {
|
|
117
|
-
public static defaultMaxChunk = 0; // 0 - no limits
|
|
118
|
-
public static defaultMaxMessageSize = 0; // 0 - no limits
|
|
119
|
-
public static defaultReceiveBufferSize = 1024 * 64 * 10;
|
|
120
|
-
public static defaultSendBufferSize = 1024 * 64 * 10; // 8192 min,
|
|
121
|
-
|
|
122
|
-
public endpointUrl: string;
|
|
123
|
-
public serverUri: string;
|
|
124
|
-
public numberOfRetry: number;
|
|
125
|
-
public parameters?: AcknowledgeMessage;
|
|
126
|
-
|
|
127
|
-
private _counter: number;
|
|
128
|
-
private _helloSettings: {
|
|
129
|
-
maxChunkCount: number;
|
|
130
|
-
maxMessageSize: number;
|
|
131
|
-
receiveBufferSize: number;
|
|
132
|
-
sendBufferSize: number;
|
|
133
|
-
};
|
|
134
|
-
constructor(transportSettings?: TransportSettingsOptions) {
|
|
135
|
-
super();
|
|
136
|
-
this.endpointUrl = "";
|
|
137
|
-
this.serverUri = "";
|
|
138
|
-
this._counter = 0;
|
|
139
|
-
this.numberOfRetry = 0;
|
|
140
|
-
|
|
141
|
-
// initially before HEL/ACK
|
|
142
|
-
this.maxChunkCount = 1;
|
|
143
|
-
this.maxMessageSize = 4 * 1024;
|
|
144
|
-
this.receiveBufferSize = 4 * 1024;
|
|
145
|
-
|
|
146
|
-
transportSettings = transportSettings || {};
|
|
147
|
-
this._helloSettings = {
|
|
148
|
-
maxChunkCount: transportSettings.maxChunkCount || ClientTCP_transport.defaultMaxChunk,
|
|
149
|
-
maxMessageSize: transportSettings.maxMessageSize || ClientTCP_transport.defaultMaxMessageSize,
|
|
150
|
-
receiveBufferSize: transportSettings.receiveBufferSize || ClientTCP_transport.defaultReceiveBufferSize,
|
|
151
|
-
sendBufferSize: transportSettings.sendBufferSize || ClientTCP_transport.defaultSendBufferSize
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
public getTransportSettings(): TransportSettingsOptions {
|
|
156
|
-
return this._helloSettings;
|
|
157
|
-
}
|
|
158
|
-
|
|
91
|
+
export class ClientTCP_transport extends ClientTransportBase {
|
|
159
92
|
public dispose(): void {
|
|
160
93
|
/* c8 ignore next */
|
|
161
94
|
doDebug && debugLog(" ClientTCP_transport disposed");
|
|
@@ -170,7 +103,8 @@ export class ClientTCP_transport extends TCP_transport {
|
|
|
170
103
|
doDebug && debugLog(chalk.cyan(`ClientTCP_transport#connect(endpointUrl = ${endpointUrl})`));
|
|
171
104
|
let socket: ISocketLike | null = null;
|
|
172
105
|
try {
|
|
173
|
-
|
|
106
|
+
// validate the URL upfront so a parse error is reported synchronously
|
|
107
|
+
parseEndpointUrl(endpointUrl);
|
|
174
108
|
socket = createClientSocket(endpointUrl, this.timeout);
|
|
175
109
|
|
|
176
110
|
socket.setTimeout(this.timeout >> 1, () => {
|
|
@@ -184,34 +118,6 @@ export class ClientTCP_transport extends TCP_transport {
|
|
|
184
118
|
return;
|
|
185
119
|
}
|
|
186
120
|
|
|
187
|
-
/**
|
|
188
|
-
*
|
|
189
|
-
*/
|
|
190
|
-
const _on_socket_error_after_connection = (err: Error) => {
|
|
191
|
-
/* c8 ignore next */
|
|
192
|
-
doDebug && debugLog(" _on_socket_error_after_connection ClientTCP_transport Socket Error", err.message);
|
|
193
|
-
|
|
194
|
-
// EPIPE : EPIPE (Broken pipe): A write on a pipe, socket, or FIFO for which there is no process to read the
|
|
195
|
-
// data. Commonly encountered at the net and http layers, indicative that the remote side of the stream
|
|
196
|
-
// being written to has been closed.
|
|
197
|
-
|
|
198
|
-
// ECONNRESET (Connection reset by peer): A connection was forcibly closed by a peer. This normally results
|
|
199
|
-
// from a loss of the connection on the remote socket due to a timeout or reboot. Commonly encountered
|
|
200
|
-
// via the http and net module
|
|
201
|
-
|
|
202
|
-
// socket termination could happen:
|
|
203
|
-
// * when the socket times out (lost of connection, network outage, etc...)
|
|
204
|
-
// * or, when the server abruptly disconnects the socket ( in case of invalid communication for instance)
|
|
205
|
-
if (err.message.match(/ECONNRESET|EPIPE|premature socket termination/)) {
|
|
206
|
-
/**
|
|
207
|
-
* @event connection_break
|
|
208
|
-
*
|
|
209
|
-
*/
|
|
210
|
-
doDebug && debugLog("connection_break after reconnection", endpointUrl);
|
|
211
|
-
this.emit("connection_break", err);
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
121
|
const _on_socket_connect = () => {
|
|
216
122
|
/* c8 ignore next */
|
|
217
123
|
doDebug && debugLog("entering _on_socket_connect");
|
|
@@ -223,13 +129,12 @@ export class ClientTCP_transport extends TCP_transport {
|
|
|
223
129
|
if (!this._socket) {
|
|
224
130
|
return callback(new Error("Abandoned"));
|
|
225
131
|
}
|
|
226
|
-
// install
|
|
227
|
-
this.
|
|
132
|
+
// install the post-connect "connection break" detector inherited from ClientTransportBase
|
|
133
|
+
this._install_post_connect_error_handler(endpointUrl);
|
|
228
134
|
/**
|
|
229
135
|
* notify the observers that the transport is connected (the socket is connected and the the HEL/ACK
|
|
230
136
|
* transaction has been done)
|
|
231
137
|
* @event connect
|
|
232
|
-
*
|
|
233
138
|
*/
|
|
234
139
|
this.emit("connect");
|
|
235
140
|
} else {
|
|
@@ -269,110 +174,4 @@ export class ClientTCP_transport extends TCP_transport {
|
|
|
269
174
|
this._socket?.once("end", _on_socket_end_for_connect);
|
|
270
175
|
this._socket?.once("connect", _on_socket_connect);
|
|
271
176
|
}
|
|
272
|
-
|
|
273
|
-
private _handle_ACK_response(messageChunk: Buffer, callback: ErrorCallback) {
|
|
274
|
-
const _stream = new BinaryStream(messageChunk);
|
|
275
|
-
const messageHeader = readMessageHeader(_stream);
|
|
276
|
-
let err: Error | null = null;
|
|
277
|
-
/* c8 ignore next */
|
|
278
|
-
if (messageHeader.isFinal !== "F") {
|
|
279
|
-
err = new Error(" invalid ACK message");
|
|
280
|
-
return callback(err);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
let responseClass: typeof AcknowledgeMessage | typeof TCPErrorMessage;
|
|
284
|
-
let response: AcknowledgeMessage | TCPErrorMessage;
|
|
285
|
-
|
|
286
|
-
if (messageHeader.msgType === "ERR") {
|
|
287
|
-
responseClass = TCPErrorMessage;
|
|
288
|
-
_stream.rewind();
|
|
289
|
-
response = decodeMessage(_stream, responseClass) as TCPErrorMessage;
|
|
290
|
-
|
|
291
|
-
err = new Error(`ACK: ERR received ${response.statusCode.toString()} : ${response.reason}`);
|
|
292
|
-
(err as any).statusCode = response.statusCode;
|
|
293
|
-
// c8 ignore next
|
|
294
|
-
doTraceHelloAck && warningLog("receiving ERR instead of Ack", response.toString());
|
|
295
|
-
|
|
296
|
-
callback(err);
|
|
297
|
-
} else {
|
|
298
|
-
responseClass = AcknowledgeMessage;
|
|
299
|
-
_stream.rewind();
|
|
300
|
-
response = decodeMessage(_stream, responseClass) as AcknowledgeMessage;
|
|
301
|
-
|
|
302
|
-
this.parameters = response;
|
|
303
|
-
this.setLimits(response);
|
|
304
|
-
|
|
305
|
-
// c8 ignore next
|
|
306
|
-
doTraceHelloAck && warningLog("receiving Ack\n", response.toString());
|
|
307
|
-
|
|
308
|
-
callback();
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private _send_HELLO_request() {
|
|
313
|
-
/* c8 ignore next */
|
|
314
|
-
doDebug && debugLog("entering _send_HELLO_request");
|
|
315
|
-
|
|
316
|
-
assert(this._socket);
|
|
317
|
-
assert(Number.isFinite(this.protocolVersion));
|
|
318
|
-
assert(this.endpointUrl.length > 0, " expecting a valid endpoint url");
|
|
319
|
-
|
|
320
|
-
const { maxChunkCount, maxMessageSize, receiveBufferSize, sendBufferSize } = this._helloSettings;
|
|
321
|
-
|
|
322
|
-
// Write a message to the socket as soon as the client is connected,
|
|
323
|
-
// the server will receive it as message from the client
|
|
324
|
-
const helloMessage = new HelloMessage({
|
|
325
|
-
endpointUrl: this.endpointUrl,
|
|
326
|
-
protocolVersion: this.protocolVersion,
|
|
327
|
-
maxChunkCount,
|
|
328
|
-
maxMessageSize,
|
|
329
|
-
receiveBufferSize,
|
|
330
|
-
sendBufferSize
|
|
331
|
-
});
|
|
332
|
-
// c8 ignore next
|
|
333
|
-
doTraceHelloAck && warningLog(`sending Hello\n ${helloMessage.toString()} `);
|
|
334
|
-
|
|
335
|
-
const messageChunk = packTcpMessage("HEL", helloMessage);
|
|
336
|
-
this._write_chunk(messageChunk);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private _on_ACK_response(externalCallback: ErrorCallback, err: Error | null, data?: Buffer) {
|
|
340
|
-
/* c8 ignore next */
|
|
341
|
-
doDebug && debugLog("entering _on_ACK_response");
|
|
342
|
-
|
|
343
|
-
assert(typeof externalCallback === "function");
|
|
344
|
-
assert(this._counter === 0, "Ack response should only be received once !");
|
|
345
|
-
this._counter += 1;
|
|
346
|
-
|
|
347
|
-
if (err || !data) {
|
|
348
|
-
if (this._socket) {
|
|
349
|
-
const s = this._socket;
|
|
350
|
-
this._socket = null;
|
|
351
|
-
s.destroy();
|
|
352
|
-
}
|
|
353
|
-
externalCallback(err || new Error("no data"));
|
|
354
|
-
} else {
|
|
355
|
-
this._handle_ACK_response(data, externalCallback);
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
private _perform_HEL_ACK_transaction(callback: ErrorCallback) {
|
|
360
|
-
/* c8 ignore next */
|
|
361
|
-
if (!this._socket) {
|
|
362
|
-
return callback(new Error("No socket available to perform HEL/ACK transaction"));
|
|
363
|
-
}
|
|
364
|
-
assert(this._socket, "expecting a valid socket to send a message");
|
|
365
|
-
assert(typeof callback === "function");
|
|
366
|
-
this._counter = 0;
|
|
367
|
-
/* c8 ignore next */
|
|
368
|
-
doDebug && debugLog("entering _perform_HEL_ACK_transaction");
|
|
369
|
-
|
|
370
|
-
this._install_one_time_message_receiver((err: Error | null, data?: Buffer) => {
|
|
371
|
-
/* c8 ignore next */
|
|
372
|
-
doDebug && debugLog("before _on_ACK_response ", err ? err.message : "");
|
|
373
|
-
|
|
374
|
-
this._on_ACK_response(callback, err, data);
|
|
375
|
-
});
|
|
376
|
-
this._send_HELLO_request();
|
|
377
|
-
}
|
|
378
177
|
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-transport
|
|
3
|
+
*
|
|
4
|
+
* Transport-agnostic base class for client-side OPC UA transports.
|
|
5
|
+
*
|
|
6
|
+
* Owns the UACP HEL/ACK handshake, the negotiated transport settings, and the
|
|
7
|
+
* post-connect connection-break detector. Concrete subclasses (`ClientTCP_transport`,
|
|
8
|
+
* `ClientWS_transport`, ...) implement only the socket-creation step in `connect()`.
|
|
9
|
+
*
|
|
10
|
+
* Browser-safe: does not import `node:net`, `node:os`, `node:util`, or any other
|
|
11
|
+
* Node-only built-in beyond what `TCP_transport` already inherits from `node:events`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { assert } from "node-opcua-assert";
|
|
15
|
+
import { BinaryStream } from "node-opcua-binary-stream";
|
|
16
|
+
import { readMessageHeader } from "node-opcua-chunkmanager";
|
|
17
|
+
import { checkDebugFlag, make_debugLog, make_warningLog } from "node-opcua-debug";
|
|
18
|
+
import type { ErrorCallback } from "node-opcua-status-code";
|
|
19
|
+
|
|
20
|
+
import { AcknowledgeMessage } from "./AcknowledgeMessage";
|
|
21
|
+
import { HelloMessage } from "./HelloMessage";
|
|
22
|
+
import type { TransportSettingsOptions } from "./i_client_transport";
|
|
23
|
+
import { TCPErrorMessage } from "./TCPErrorMessage";
|
|
24
|
+
import { TCP_transport } from "./tcp_transport";
|
|
25
|
+
import { decodeMessage, packTcpMessage } from "./tools";
|
|
26
|
+
import { doTraceHelloAck } from "./utils";
|
|
27
|
+
|
|
28
|
+
// Use a string category instead of `__filename` so the module loads in
|
|
29
|
+
// browsers without a Node-style filename global.
|
|
30
|
+
const doDebug = checkDebugFlag("ClientTransportBase");
|
|
31
|
+
const debugLog = make_debugLog("ClientTransportBase");
|
|
32
|
+
const warningLog = make_warningLog("ClientTransportBase");
|
|
33
|
+
|
|
34
|
+
export interface ClientTransportBase {
|
|
35
|
+
on(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
|
|
36
|
+
on(eventName: "close", eventHandler: (err: Error | null) => void): this;
|
|
37
|
+
on(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
|
|
38
|
+
on(eventName: "connect", eventHandler: () => void): this;
|
|
39
|
+
|
|
40
|
+
once(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
|
|
41
|
+
once(eventName: "close", eventHandler: (err: Error | null) => void): this;
|
|
42
|
+
once(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
|
|
43
|
+
once(eventName: "connect", eventHandler: () => void): this;
|
|
44
|
+
|
|
45
|
+
emit(eventName: "chunk", messageChunk: Buffer): boolean;
|
|
46
|
+
emit(eventName: "close", err?: Error | null): boolean;
|
|
47
|
+
emit(eventName: "connection_break", err?: Error | null): boolean;
|
|
48
|
+
emit(eventName: "connect"): boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: companion to the interface above
|
|
52
|
+
export abstract class ClientTransportBase extends TCP_transport {
|
|
53
|
+
public static defaultMaxChunk = 0; // 0 - no limits
|
|
54
|
+
public static defaultMaxMessageSize = 0; // 0 - no limits
|
|
55
|
+
public static defaultReceiveBufferSize = 1024 * 64 * 10;
|
|
56
|
+
public static defaultSendBufferSize = 1024 * 64 * 10; // 8192 min,
|
|
57
|
+
|
|
58
|
+
public endpointUrl: string;
|
|
59
|
+
public serverUri: string;
|
|
60
|
+
public numberOfRetry: number;
|
|
61
|
+
public parameters?: AcknowledgeMessage;
|
|
62
|
+
|
|
63
|
+
private _counter: number;
|
|
64
|
+
private _helloSettings: {
|
|
65
|
+
maxChunkCount: number;
|
|
66
|
+
maxMessageSize: number;
|
|
67
|
+
receiveBufferSize: number;
|
|
68
|
+
sendBufferSize: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
constructor(transportSettings?: TransportSettingsOptions) {
|
|
72
|
+
super();
|
|
73
|
+
this.endpointUrl = "";
|
|
74
|
+
this.serverUri = "";
|
|
75
|
+
this._counter = 0;
|
|
76
|
+
this.numberOfRetry = 0;
|
|
77
|
+
|
|
78
|
+
// initially before HEL/ACK
|
|
79
|
+
this.maxChunkCount = 1;
|
|
80
|
+
this.maxMessageSize = 4 * 1024;
|
|
81
|
+
this.receiveBufferSize = 4 * 1024;
|
|
82
|
+
|
|
83
|
+
transportSettings = transportSettings || {};
|
|
84
|
+
this._helloSettings = {
|
|
85
|
+
maxChunkCount: transportSettings.maxChunkCount || ClientTransportBase.defaultMaxChunk,
|
|
86
|
+
maxMessageSize: transportSettings.maxMessageSize || ClientTransportBase.defaultMaxMessageSize,
|
|
87
|
+
receiveBufferSize: transportSettings.receiveBufferSize || ClientTransportBase.defaultReceiveBufferSize,
|
|
88
|
+
sendBufferSize: transportSettings.sendBufferSize || ClientTransportBase.defaultSendBufferSize
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public getTransportSettings(): TransportSettingsOptions {
|
|
93
|
+
return this._helloSettings;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public dispose(): void {
|
|
97
|
+
/* c8 ignore next */
|
|
98
|
+
doDebug && debugLog(" ClientTransportBase disposed");
|
|
99
|
+
|
|
100
|
+
super.dispose();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Connect to `endpointUrl` and perform the UACP HEL/ACK handshake.
|
|
105
|
+
* Concrete subclasses are responsible for opening the underlying socket
|
|
106
|
+
* (TCP, WebSocket, ...) and then driving the inherited HEL/ACK machinery.
|
|
107
|
+
*/
|
|
108
|
+
public abstract connect(endpointUrl: string, callback: ErrorCallback): void;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Install the post-connect "connection break" detector. Subclasses call this
|
|
112
|
+
* once the underlying socket is open and the HEL/ACK transaction has succeeded.
|
|
113
|
+
*
|
|
114
|
+
* Detects ECONNRESET / EPIPE / premature socket termination on the live socket
|
|
115
|
+
* and re-emits them as `connection_break` so reconnection logic upstream can
|
|
116
|
+
* react.
|
|
117
|
+
*/
|
|
118
|
+
protected _install_post_connect_error_handler(endpointUrl: string): void {
|
|
119
|
+
if (!this._socket) return;
|
|
120
|
+
this._socket.on("error", (err: Error) => {
|
|
121
|
+
// EPIPE : a write on a pipe/socket/FIFO with no reader.
|
|
122
|
+
// ECONNRESET : connection forcibly closed by the peer (timeout, reboot, ...).
|
|
123
|
+
// "premature socket termination" : abrupt close mid-message.
|
|
124
|
+
if (err.message.match(/ECONNRESET|EPIPE|premature socket termination/)) {
|
|
125
|
+
/* c8 ignore next */
|
|
126
|
+
doDebug && debugLog("connection_break after reconnection", endpointUrl);
|
|
127
|
+
this.emit("connection_break", err);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected _perform_HEL_ACK_transaction(callback: ErrorCallback): void {
|
|
133
|
+
/* c8 ignore next */
|
|
134
|
+
if (!this._socket) {
|
|
135
|
+
callback(new Error("No socket available to perform HEL/ACK transaction"));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
assert(this._socket, "expecting a valid socket to send a message");
|
|
139
|
+
assert(typeof callback === "function");
|
|
140
|
+
this._counter = 0;
|
|
141
|
+
/* c8 ignore next */
|
|
142
|
+
doDebug && debugLog("entering _perform_HEL_ACK_transaction");
|
|
143
|
+
|
|
144
|
+
this._install_one_time_message_receiver((err: Error | null, data?: Buffer) => {
|
|
145
|
+
/* c8 ignore next */
|
|
146
|
+
doDebug && debugLog("before _on_ACK_response ", err ? err.message : "");
|
|
147
|
+
|
|
148
|
+
this._on_ACK_response(callback, err, data);
|
|
149
|
+
});
|
|
150
|
+
this._send_HELLO_request();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private _send_HELLO_request(): void {
|
|
154
|
+
/* c8 ignore next */
|
|
155
|
+
doDebug && debugLog("entering _send_HELLO_request");
|
|
156
|
+
|
|
157
|
+
assert(this._socket);
|
|
158
|
+
assert(Number.isFinite(this.protocolVersion));
|
|
159
|
+
assert(this.endpointUrl.length > 0, " expecting a valid endpoint url");
|
|
160
|
+
|
|
161
|
+
const { maxChunkCount, maxMessageSize, receiveBufferSize, sendBufferSize } = this._helloSettings;
|
|
162
|
+
|
|
163
|
+
const helloMessage = new HelloMessage({
|
|
164
|
+
endpointUrl: this.endpointUrl,
|
|
165
|
+
protocolVersion: this.protocolVersion,
|
|
166
|
+
maxChunkCount,
|
|
167
|
+
maxMessageSize,
|
|
168
|
+
receiveBufferSize,
|
|
169
|
+
sendBufferSize
|
|
170
|
+
});
|
|
171
|
+
// c8 ignore next
|
|
172
|
+
doTraceHelloAck && warningLog(`sending Hello\n ${helloMessage.toString()} `);
|
|
173
|
+
|
|
174
|
+
const messageChunk = packTcpMessage("HEL", helloMessage);
|
|
175
|
+
this._write_chunk(messageChunk);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private _on_ACK_response(externalCallback: ErrorCallback, err: Error | null, data?: Buffer): void {
|
|
179
|
+
/* c8 ignore next */
|
|
180
|
+
doDebug && debugLog("entering _on_ACK_response");
|
|
181
|
+
|
|
182
|
+
assert(typeof externalCallback === "function");
|
|
183
|
+
assert(this._counter === 0, "Ack response should only be received once !");
|
|
184
|
+
this._counter += 1;
|
|
185
|
+
|
|
186
|
+
if (err || !data) {
|
|
187
|
+
if (this._socket) {
|
|
188
|
+
const s = this._socket;
|
|
189
|
+
this._socket = null;
|
|
190
|
+
s.destroy();
|
|
191
|
+
}
|
|
192
|
+
externalCallback(err || new Error("no data"));
|
|
193
|
+
} else {
|
|
194
|
+
this._handle_ACK_response(data, externalCallback);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private _handle_ACK_response(messageChunk: Buffer, callback: ErrorCallback): void {
|
|
199
|
+
const _stream = new BinaryStream(messageChunk);
|
|
200
|
+
const messageHeader = readMessageHeader(_stream);
|
|
201
|
+
let err: Error | null = null;
|
|
202
|
+
/* c8 ignore next */
|
|
203
|
+
if (messageHeader.isFinal !== "F") {
|
|
204
|
+
err = new Error(" invalid ACK message");
|
|
205
|
+
callback(err);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let responseClass: typeof AcknowledgeMessage | typeof TCPErrorMessage;
|
|
210
|
+
let response: AcknowledgeMessage | TCPErrorMessage;
|
|
211
|
+
|
|
212
|
+
if (messageHeader.msgType === "ERR") {
|
|
213
|
+
responseClass = TCPErrorMessage;
|
|
214
|
+
_stream.rewind();
|
|
215
|
+
response = decodeMessage(_stream, responseClass) as TCPErrorMessage;
|
|
216
|
+
|
|
217
|
+
err = new Error(`ACK: ERR received ${response.statusCode.toString()} : ${response.reason}`);
|
|
218
|
+
// biome-ignore lint/suspicious/noExplicitAny: legacy diagnostic field tacked onto Error
|
|
219
|
+
(err as any).statusCode = response.statusCode;
|
|
220
|
+
// c8 ignore next
|
|
221
|
+
doTraceHelloAck && warningLog("receiving ERR instead of Ack", response.toString());
|
|
222
|
+
|
|
223
|
+
callback(err);
|
|
224
|
+
} else {
|
|
225
|
+
responseClass = AcknowledgeMessage;
|
|
226
|
+
_stream.rewind();
|
|
227
|
+
response = decodeMessage(_stream, responseClass) as AcknowledgeMessage;
|
|
228
|
+
|
|
229
|
+
this.parameters = response;
|
|
230
|
+
this.setLimits(response);
|
|
231
|
+
|
|
232
|
+
// c8 ignore next
|
|
233
|
+
doTraceHelloAck && warningLog("receiving Ack\n", response.toString());
|
|
234
|
+
|
|
235
|
+
callback();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-transport
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ClientTCP_transport, type TransportSettingsOptions } from "./client_tcp_transport";
|
|
6
|
+
import type { IClientTransport, IClientTransportFactory } from "./i_client_transport";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The default client-transport factory, which returns a {@link ClientTCP_transport}.
|
|
10
|
+
*
|
|
11
|
+
* This is the implicit factory used by {@link ClientSecureChannelLayer} when no
|
|
12
|
+
* `transportFactory` option is provided, preserving the historical (Node-only)
|
|
13
|
+
* behavior byte-for-byte.
|
|
14
|
+
*/
|
|
15
|
+
export const defaultClientTransportFactory: IClientTransportFactory = {
|
|
16
|
+
create(settings?: TransportSettingsOptions): IClientTransport {
|
|
17
|
+
return new ClientTCP_transport(settings) as IClientTransport;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-transport
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AcknowledgeMessage } from "./AcknowledgeMessage";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options used to construct a client transport. Passed through {@link IClientTransportFactory.create}
|
|
9
|
+
* and applied to the UACP HEL message during the handshake.
|
|
10
|
+
*/
|
|
11
|
+
export interface TransportSettingsOptions {
|
|
12
|
+
maxChunkCount?: number;
|
|
13
|
+
maxMessageSize?: number;
|
|
14
|
+
receiveBufferSize?: number;
|
|
15
|
+
sendBufferSize?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The minimal surface that {@link ClientSecureChannelLayer} (and anything else acting as
|
|
20
|
+
* a secure-channel client) uses from a transport. {@link ClientTCP_transport} already
|
|
21
|
+
* satisfies this interface; browser transports (e.g. a WebSocket-based one) must also
|
|
22
|
+
* satisfy it to be pluggable via {@link IClientTransportFactory}.
|
|
23
|
+
*
|
|
24
|
+
* This interface is intentionally the smallest superset of what the existing
|
|
25
|
+
* `ClientTCP_transport` exposes and that the secure-channel layer actually consumes,
|
|
26
|
+
* so adding new transports does not require replicating Node-specific machinery.
|
|
27
|
+
*/
|
|
28
|
+
export interface IClientTransport {
|
|
29
|
+
// ──────────────────────────────────────────────────────────────
|
|
30
|
+
// mutable configuration / runtime state
|
|
31
|
+
// ──────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** diagnostic name, useful in debug logs */
|
|
34
|
+
readonly name: string;
|
|
35
|
+
|
|
36
|
+
/** OPC UA UACP protocol version advertised in HEL */
|
|
37
|
+
protocolVersion: number;
|
|
38
|
+
|
|
39
|
+
/** overall timeout applied to the underlying socket / connection lifecycle */
|
|
40
|
+
timeout: number;
|
|
41
|
+
|
|
42
|
+
/** number of times the owning channel has retried. Advisory; bumped by callers. */
|
|
43
|
+
numberOfRetry: number;
|
|
44
|
+
|
|
45
|
+
/** endpoint URL the transport was connected to (set by `connect`) */
|
|
46
|
+
endpointUrl: string;
|
|
47
|
+
|
|
48
|
+
/** URI reported by the local application to the peer */
|
|
49
|
+
serverUri: string;
|
|
50
|
+
|
|
51
|
+
// ──────────────────────────────────────────────────────────────
|
|
52
|
+
// HEL/ACK negotiated values (populated after successful connect)
|
|
53
|
+
// ──────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
readonly parameters?: AcknowledgeMessage;
|
|
56
|
+
|
|
57
|
+
readonly receiveBufferSize: number;
|
|
58
|
+
readonly sendBufferSize: number;
|
|
59
|
+
readonly maxChunkCount: number;
|
|
60
|
+
readonly maxMessageSize: number;
|
|
61
|
+
|
|
62
|
+
// ──────────────────────────────────────────────────────────────
|
|
63
|
+
// diagnostics
|
|
64
|
+
// ──────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
readonly bytesRead: number;
|
|
67
|
+
readonly bytesWritten: number;
|
|
68
|
+
readonly chunkReadCount: number;
|
|
69
|
+
readonly chunkWrittenCount: number;
|
|
70
|
+
|
|
71
|
+
// ──────────────────────────────────────────────────────────────
|
|
72
|
+
// lifecycle
|
|
73
|
+
// ──────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
/** connect to `endpointUrl` and perform the UACP HEL/ACK handshake */
|
|
76
|
+
connect(endpointUrl: string, callback: (err?: Error | null) => void): void;
|
|
77
|
+
|
|
78
|
+
/** gracefully disconnect; invokes `callback` when the underlying connection is closed */
|
|
79
|
+
disconnect(callback: (err?: Error | null) => void): void;
|
|
80
|
+
|
|
81
|
+
/** forcibly release resources (close the connection if still open) */
|
|
82
|
+
dispose(): void;
|
|
83
|
+
|
|
84
|
+
/** write a single UACP chunk to the transport */
|
|
85
|
+
write(chunk: Buffer, callback?: (err?: Error | null) => undefined): void;
|
|
86
|
+
|
|
87
|
+
/** emit an ERR back to the peer and destroy the underlying connection */
|
|
88
|
+
prematureTerminate(err: Error, statusCode: import("node-opcua-status-code").StatusCode): void;
|
|
89
|
+
|
|
90
|
+
/** simulate a connection break (used by reconnection logic in tests) */
|
|
91
|
+
forceConnectionBreak(): void;
|
|
92
|
+
|
|
93
|
+
/** `true` when the underlying connection is open and usable */
|
|
94
|
+
isValid(): boolean;
|
|
95
|
+
|
|
96
|
+
/** `true` when `disconnect()` has started or the connection is gone */
|
|
97
|
+
isDisconnecting(): boolean;
|
|
98
|
+
|
|
99
|
+
/** return the effective transport settings (`maxChunkCount` etc.) */
|
|
100
|
+
getTransportSettings(): TransportSettingsOptions;
|
|
101
|
+
|
|
102
|
+
// ──────────────────────────────────────────────────────────────
|
|
103
|
+
// typed events (mirrors ClientTCP_transport's declaration merge)
|
|
104
|
+
// ──────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
on(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
|
|
107
|
+
on(eventName: "close", eventHandler: (err: Error | null) => void): this;
|
|
108
|
+
on(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
|
|
109
|
+
on(eventName: "connect", eventHandler: () => void): this;
|
|
110
|
+
|
|
111
|
+
once(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
|
|
112
|
+
once(eventName: "close", eventHandler: (err: Error | null) => void): this;
|
|
113
|
+
once(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
|
|
114
|
+
once(eventName: "connect", eventHandler: () => void): this;
|
|
115
|
+
|
|
116
|
+
removeListener(eventName: "chunk", eventHandler: (messageChunk: Buffer) => void): this;
|
|
117
|
+
removeListener(eventName: "close", eventHandler: (err: Error | null) => void): this;
|
|
118
|
+
removeListener(eventName: "connection_break", eventHandler: (err: Error | null) => void): this;
|
|
119
|
+
removeListener(eventName: "connect", eventHandler: () => void): this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A factory that produces an {@link IClientTransport}. Injected into
|
|
124
|
+
* {@link ClientSecureChannelLayerOptions.transportFactory} to swap the default Node TCP
|
|
125
|
+
* transport for an alternative (for example, a browser WebSocket transport or a tracing
|
|
126
|
+
* proxy wrapped around the default).
|
|
127
|
+
*/
|
|
128
|
+
export interface IClientTransportFactory {
|
|
129
|
+
/**
|
|
130
|
+
* Create a new transport. Called once per secure-channel open; the factory must not
|
|
131
|
+
* return the same instance twice.
|
|
132
|
+
*/
|
|
133
|
+
create(settings?: TransportSettingsOptions): IClientTransport;
|
|
134
|
+
}
|