node-opcua-transport 2.172.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/i_client_transport.d.ts +10 -1
- 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 +1 -0
- package/dist/source/index.js +1 -0
- package/dist/source/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +29 -9
- package/source/client_tcp_transport.ts +16 -217
- package/source/client_transport_base.ts +238 -0
- package/source/i_client_transport.ts +11 -1
- package/source/index.browser.ts +62 -0
- package/source/index.ts +1 -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
|
+
}
|
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { AcknowledgeMessage } from "./AcknowledgeMessage";
|
|
6
|
-
|
|
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
|
+
}
|
|
7
17
|
|
|
8
18
|
/**
|
|
9
19
|
* The minimal surface that {@link ClientSecureChannelLayer} (and anything else acting as
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* The MIT License (MIT)
|
|
3
|
+
* Copyright (c) 2022-2025 Sterfive SAS - 833264583 RCS ORLEANS - France (https://www.sterfive.com)
|
|
4
|
+
*
|
|
5
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
* this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
* the Software without restriction, including without limitation the rights to
|
|
8
|
+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
* subject to the following conditions:
|
|
11
|
+
*
|
|
12
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
* copies or substantial portions of the Software.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* @module node-opcua-transport/browser
|
|
17
|
+
*
|
|
18
|
+
* Browser-safe subset of `node-opcua-transport`. Selected automatically by
|
|
19
|
+
* bundlers (esbuild, webpack, vite, rollup) via the `"browser"` condition in
|
|
20
|
+
* this package's `exports` map.
|
|
21
|
+
*
|
|
22
|
+
* Excludes Node-only modules whose top-level `import "node:net" | "node:os"`
|
|
23
|
+
* statements would otherwise crash a `platform: "browser"` bundle even though
|
|
24
|
+
* the runtime never reaches them:
|
|
25
|
+
* - `client_tcp_transport` — opens a `net.Socket`
|
|
26
|
+
* - `default_client_transport_factory` — instantiates `ClientTCP_transport`
|
|
27
|
+
* - `server_tcp_transport` — Node-side server endpoint
|
|
28
|
+
*
|
|
29
|
+
* Browser-side OPC UA transports (e.g. `ClientWS_transport` from
|
|
30
|
+
* `node-opcua-client-browser`) extend `ClientTransportBase` from this entry,
|
|
31
|
+
* implement their own `connect()`, and inherit the inherited HEL/ACK,
|
|
32
|
+
* packet-assembly, and lifecycle machinery.
|
|
33
|
+
*
|
|
34
|
+
* ## Bundler configuration required
|
|
35
|
+
*
|
|
36
|
+
* Several files still re-exported here import `node:events` (e.g.
|
|
37
|
+
* `tcp_transport.ts`, `message_builder_base.ts`). Browser bundlers do not
|
|
38
|
+
* auto-polyfill `node:`-prefixed built-ins; consumers must alias them to
|
|
39
|
+
* polyfill packages. Example (esbuild):
|
|
40
|
+
*
|
|
41
|
+
* alias: {
|
|
42
|
+
* "node:events": "events",
|
|
43
|
+
* "node:util": "util",
|
|
44
|
+
* "node:buffer": "buffer"
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* Transitively, `node-opcua-debug` and `node-opcua-utils` also need these
|
|
48
|
+
* aliases. We deliberately do not declare the polyfills as dependencies of
|
|
49
|
+
* the transport package — Node consumers would pay the install cost for no
|
|
50
|
+
* benefit, and Node would prefer the npm port over its own built-in.
|
|
51
|
+
*/
|
|
52
|
+
export * from "./AcknowledgeMessage";
|
|
53
|
+
export * from "./client_transport_base";
|
|
54
|
+
export * from "./HelloMessage";
|
|
55
|
+
export * from "./i_client_transport";
|
|
56
|
+
export * from "./i_hello_ack_limits";
|
|
57
|
+
export * from "./message_builder_base";
|
|
58
|
+
export * from "./status_codes";
|
|
59
|
+
export * from "./TCPErrorMessage";
|
|
60
|
+
export * from "./tcp_transport";
|
|
61
|
+
export * from "./tools";
|
|
62
|
+
export * from "./utils";
|
package/source/index.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
export * from "./AcknowledgeMessage";
|
|
27
27
|
export * from "./client_tcp_transport";
|
|
28
|
+
export * from "./client_transport_base";
|
|
28
29
|
export * from "./default_client_transport_factory";
|
|
29
30
|
export * from "./HelloMessage";
|
|
30
31
|
export * from "./i_client_transport";
|