node-opcua-transport 2.51.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/.mocharc.yml +10 -0
- package/LICENSE +20 -0
- package/dist/source/AcknowledgeMessage.d.ts +27 -0
- package/dist/source/AcknowledgeMessage.js +79 -0
- package/dist/source/AcknowledgeMessage.js.map +1 -0
- package/dist/source/HelloMessage.d.ts +27 -0
- package/dist/source/HelloMessage.js +95 -0
- package/dist/source/HelloMessage.js.map +1 -0
- package/dist/source/TCPErrorMessage.d.ts +18 -0
- package/dist/source/TCPErrorMessage.js +47 -0
- package/dist/source/TCPErrorMessage.js.map +1 -0
- package/dist/source/client_tcp_transport.d.ts +68 -0
- package/dist/source/client_tcp_transport.js +315 -0
- package/dist/source/client_tcp_transport.js.map +1 -0
- package/dist/source/index.d.ts +11 -0
- package/dist/source/index.js +24 -0
- package/dist/source/index.js.map +1 -0
- package/dist/source/message_builder_base.d.ts +61 -0
- package/dist/source/message_builder_base.js +207 -0
- package/dist/source/message_builder_base.js.map +1 -0
- package/dist/source/server_tcp_transport.d.ts +45 -0
- package/dist/source/server_tcp_transport.js +232 -0
- package/dist/source/server_tcp_transport.js.map +1 -0
- package/dist/source/tcp_transport.d.ts +117 -0
- package/dist/source/tcp_transport.js +350 -0
- package/dist/source/tcp_transport.js.map +1 -0
- package/dist/source/tools.d.ts +13 -0
- package/dist/source/tools.js +99 -0
- package/dist/source/tools.js.map +1 -0
- package/dist/source/utils.d.ts +2 -0
- package/dist/source/utils.js +9 -0
- package/dist/source/utils.js.map +1 -0
- package/dist/test-fixtures/fixture_full_tcp_packets.d.ts +21 -0
- package/dist/test-fixtures/fixture_full_tcp_packets.js +413 -0
- package/dist/test-fixtures/fixture_full_tcp_packets.js.map +1 -0
- package/dist/test-fixtures/index.d.ts +1 -0
- package/dist/test-fixtures/index.js +14 -0
- package/dist/test-fixtures/index.js.map +1 -0
- package/dist/test_helpers/direct_transport.d.ts +14 -0
- package/dist/test_helpers/direct_transport.js +63 -0
- package/dist/test_helpers/direct_transport.js.map +1 -0
- package/dist/test_helpers/fake_server.d.ts +15 -0
- package/dist/test_helpers/fake_server.js +56 -0
- package/dist/test_helpers/fake_server.js.map +1 -0
- package/dist/test_helpers/half_com_channel.d.ts +10 -0
- package/dist/test_helpers/half_com_channel.js +35 -0
- package/dist/test_helpers/half_com_channel.js.map +1 -0
- package/dist/test_helpers/index.d.ts +4 -0
- package/dist/test_helpers/index.js +17 -0
- package/dist/test_helpers/index.js.map +1 -0
- package/dist/test_helpers/socket_transport.d.ts +8 -0
- package/dist/test_helpers/socket_transport.js +30 -0
- package/dist/test_helpers/socket_transport.js.map +1 -0
- package/package.json +50 -0
- package/source/AcknowledgeMessage.ts +112 -0
- package/source/HelloMessage.ts +133 -0
- package/source/TCPErrorMessage.ts +57 -0
- package/source/client_tcp_transport.ts +366 -0
- package/source/index.ts +11 -0
- package/source/message_builder_base.ts +263 -0
- package/source/server_tcp_transport.ts +284 -0
- package/source/tcp_transport.ts +450 -0
- package/source/tools.ts +113 -0
- package/source/utils.ts +4 -0
- package/test_helpers/direct_transport.ts +78 -0
- package/test_helpers/fake_server.ts +71 -0
- package/test_helpers/half_com_channel.ts +38 -0
- package/test_helpers/index.ts +4 -0
- package/test_helpers/socket_transport.ts +34 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-transport
|
|
3
|
+
*/
|
|
4
|
+
import * as chalk from "chalk";
|
|
5
|
+
import { EventEmitter } from "events";
|
|
6
|
+
import { Socket } from "net";
|
|
7
|
+
|
|
8
|
+
import { assert } from "node-opcua-assert";
|
|
9
|
+
import { createFastUninitializedBuffer } from "node-opcua-buffer-utils";
|
|
10
|
+
import * as debug from "node-opcua-debug";
|
|
11
|
+
import { ObjectRegistry } from "node-opcua-object-registry";
|
|
12
|
+
import { PacketAssembler } from "node-opcua-packet-assembler";
|
|
13
|
+
import { ErrorCallback, CallbackWithData } from "node-opcua-status-code";
|
|
14
|
+
|
|
15
|
+
import { readRawMessageHeader } from "./message_builder_base";
|
|
16
|
+
import { writeTCPMessageHeader } from "./tools";
|
|
17
|
+
|
|
18
|
+
const debugLog = debug.make_debugLog(__filename);
|
|
19
|
+
const doDebug = debug.checkDebugFlag(__filename);
|
|
20
|
+
const errorLog = debug.make_errorLog(__filename);
|
|
21
|
+
|
|
22
|
+
let fakeSocket: any = { invalid: true };
|
|
23
|
+
|
|
24
|
+
export function setFakeTransport(mockSocket: any) {
|
|
25
|
+
fakeSocket = mockSocket;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getFakeTransport() {
|
|
29
|
+
if (fakeSocket.invalid) {
|
|
30
|
+
throw new Error("getFakeTransport: setFakeTransport must be called first - BadProtocolVersionUnsupported");
|
|
31
|
+
}
|
|
32
|
+
return fakeSocket;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let counter = 0;
|
|
36
|
+
|
|
37
|
+
export interface TCP_transport {
|
|
38
|
+
on(eventName: "message", eventHandler: (message: Buffer) => void): this;
|
|
39
|
+
once(eventName: "message", eventHandler: (message: Buffer) => void): this;
|
|
40
|
+
on(eventName: "socket_closed", eventHandler: (err: Error | null) => void): this;
|
|
41
|
+
once(eventName: "socket_closed", eventHandler: (err: Error | null) => void): this;
|
|
42
|
+
on(eventName: "close", eventHandler: (err: Error | null) => void): this;
|
|
43
|
+
once(eventName: "close", eventHandler: (err: Error | null) => void): this;
|
|
44
|
+
}
|
|
45
|
+
// tslint:disable:class-name
|
|
46
|
+
export class TCP_transport extends EventEmitter {
|
|
47
|
+
private static registry = new ObjectRegistry();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* indicates the version number of the OPCUA protocol used
|
|
51
|
+
* @default 0
|
|
52
|
+
*/
|
|
53
|
+
public protocolVersion: number;
|
|
54
|
+
|
|
55
|
+
public bytesWritten: number;
|
|
56
|
+
public bytesRead: number;
|
|
57
|
+
public chunkWrittenCount: number;
|
|
58
|
+
public chunkReadCount: number;
|
|
59
|
+
public name: string;
|
|
60
|
+
|
|
61
|
+
public _socket: Socket | null;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* the size of the header in bytes
|
|
65
|
+
* @default 8
|
|
66
|
+
*/
|
|
67
|
+
private readonly headerSize: 8;
|
|
68
|
+
private _disconnecting: boolean;
|
|
69
|
+
private _timerId: NodeJS.Timer | null;
|
|
70
|
+
private _onSocketClosedHasBeenCalled: boolean;
|
|
71
|
+
private _onSocketEndedHasBeenCalled: boolean;
|
|
72
|
+
private _theCallback?: CallbackWithData;
|
|
73
|
+
private _on_error_during_one_time_message_receiver: any;
|
|
74
|
+
private _pendingBuffer?: any;
|
|
75
|
+
private packetAssembler?: PacketAssembler;
|
|
76
|
+
private _timeout: number;
|
|
77
|
+
|
|
78
|
+
constructor() {
|
|
79
|
+
super();
|
|
80
|
+
|
|
81
|
+
this.name = this.constructor.name + counter;
|
|
82
|
+
counter += 1;
|
|
83
|
+
|
|
84
|
+
this._timerId = null;
|
|
85
|
+
this._timeout = 30000; // 30 seconds timeout
|
|
86
|
+
this._socket = null;
|
|
87
|
+
this.headerSize = 8;
|
|
88
|
+
this.protocolVersion = 0;
|
|
89
|
+
|
|
90
|
+
this._disconnecting = false;
|
|
91
|
+
this._pendingBuffer = undefined;
|
|
92
|
+
|
|
93
|
+
this.bytesWritten = 0;
|
|
94
|
+
this.bytesRead = 0;
|
|
95
|
+
|
|
96
|
+
this._theCallback = undefined;
|
|
97
|
+
this.chunkWrittenCount = 0;
|
|
98
|
+
this.chunkReadCount = 0;
|
|
99
|
+
|
|
100
|
+
this._onSocketClosedHasBeenCalled = false;
|
|
101
|
+
this._onSocketEndedHasBeenCalled = false;
|
|
102
|
+
TCP_transport.registry.register(this);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public get timeout(): number {
|
|
106
|
+
return this._timeout;
|
|
107
|
+
}
|
|
108
|
+
public set timeout(value: number) {
|
|
109
|
+
debugLog("Setting socket " + this.name + " timeout = ", value);
|
|
110
|
+
this._timeout = value;
|
|
111
|
+
}
|
|
112
|
+
public dispose() {
|
|
113
|
+
this._cleanup_timers();
|
|
114
|
+
assert(!this._timerId);
|
|
115
|
+
if (this._socket) {
|
|
116
|
+
this._socket.destroy();
|
|
117
|
+
this._socket.removeAllListeners();
|
|
118
|
+
this._socket = null;
|
|
119
|
+
}
|
|
120
|
+
TCP_transport.registry.unregister(this);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* ```createChunk``` is used to construct a pre-allocated chunk to store up to ```length``` bytes of data.
|
|
125
|
+
* The created chunk includes a prepended header for ```chunk_type``` of size ```self.headerSize```.
|
|
126
|
+
*
|
|
127
|
+
* @method createChunk
|
|
128
|
+
* @param msgType
|
|
129
|
+
* @param chunkType {String} chunk type. should be 'F' 'C' or 'A'
|
|
130
|
+
* @param length
|
|
131
|
+
* @return a buffer object with the required length representing the chunk.
|
|
132
|
+
*
|
|
133
|
+
* Note:
|
|
134
|
+
* - only one chunk can be created at a time.
|
|
135
|
+
* - a created chunk should be committed using the ```write``` method before an other one is created.
|
|
136
|
+
*/
|
|
137
|
+
public createChunk(msgType: string, chunkType: string, length: number): Buffer {
|
|
138
|
+
assert(msgType === "MSG");
|
|
139
|
+
assert(this._pendingBuffer === undefined, "createChunk has already been called ( use write first)");
|
|
140
|
+
|
|
141
|
+
const totalLength = length + this.headerSize;
|
|
142
|
+
const buffer = createFastUninitializedBuffer(totalLength);
|
|
143
|
+
writeTCPMessageHeader("MSG", chunkType, totalLength, buffer);
|
|
144
|
+
this._pendingBuffer = buffer;
|
|
145
|
+
|
|
146
|
+
return buffer;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* write the message_chunk on the socket.
|
|
151
|
+
* @method write
|
|
152
|
+
* @param messageChunk
|
|
153
|
+
*
|
|
154
|
+
* Notes:
|
|
155
|
+
* - the message chunk must have been created by ```createChunk```.
|
|
156
|
+
* - once a message chunk has been written, it is possible to call ```createChunk``` again.
|
|
157
|
+
*
|
|
158
|
+
*/
|
|
159
|
+
public write(messageChunk: Buffer) {
|
|
160
|
+
assert(
|
|
161
|
+
this._pendingBuffer === undefined || this._pendingBuffer === messageChunk,
|
|
162
|
+
" write should be used with buffer created by createChunk"
|
|
163
|
+
);
|
|
164
|
+
const header = readRawMessageHeader(messageChunk);
|
|
165
|
+
assert(header.length === messageChunk.length);
|
|
166
|
+
assert(["F", "C", "A"].indexOf(header.messageHeader.isFinal) !== -1);
|
|
167
|
+
this._write_chunk(messageChunk);
|
|
168
|
+
this._pendingBuffer = undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public get isDisconnecting(): boolean {
|
|
172
|
+
return this._disconnecting;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* disconnect the TCP layer and close the underlying socket.
|
|
176
|
+
* The ```"close"``` event will be emitted to the observers with err=null.
|
|
177
|
+
*
|
|
178
|
+
* @method disconnect
|
|
179
|
+
* @async
|
|
180
|
+
* @param callback
|
|
181
|
+
*/
|
|
182
|
+
public disconnect(callback: ErrorCallback): void {
|
|
183
|
+
assert(typeof callback === "function", "expecting a callback function, but got " + callback);
|
|
184
|
+
|
|
185
|
+
if (this._disconnecting) {
|
|
186
|
+
callback();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
assert(!this._disconnecting, "TCP Transport has already been disconnected");
|
|
191
|
+
this._disconnecting = true;
|
|
192
|
+
|
|
193
|
+
// xx assert(!this._theCallback,
|
|
194
|
+
// "disconnect shall not be called while the 'one time message receiver' is in operation");
|
|
195
|
+
this._cleanup_timers();
|
|
196
|
+
|
|
197
|
+
if (this._socket) {
|
|
198
|
+
this._socket.end();
|
|
199
|
+
this._socket.destroy();
|
|
200
|
+
// xx this._socket.removeAllListeners();
|
|
201
|
+
this._socket = null;
|
|
202
|
+
}
|
|
203
|
+
this.on_socket_ended(null);
|
|
204
|
+
setImmediate(() => {
|
|
205
|
+
callback();
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public isValid(): boolean {
|
|
210
|
+
return this._socket !== null && !this._socket.destroyed && !this._disconnecting;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
protected _write_chunk(messageChunk: Buffer) {
|
|
214
|
+
if (this._socket !== null) {
|
|
215
|
+
this.bytesWritten += messageChunk.length;
|
|
216
|
+
this.chunkWrittenCount++;
|
|
217
|
+
this._socket.write(messageChunk);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
protected on_socket_ended(err: Error | null) {
|
|
222
|
+
assert(!this._onSocketEndedHasBeenCalled);
|
|
223
|
+
this._onSocketEndedHasBeenCalled = true; // we don't want to send close event twice ...
|
|
224
|
+
/**
|
|
225
|
+
* notify the observers that the transport layer has been disconnected.
|
|
226
|
+
* @event close
|
|
227
|
+
* @param err the Error object or null
|
|
228
|
+
*/
|
|
229
|
+
this.emit("close", err || null);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @method _install_socket
|
|
234
|
+
* @param socket {Socket}
|
|
235
|
+
* @protected
|
|
236
|
+
*/
|
|
237
|
+
protected _install_socket(socket: Socket) {
|
|
238
|
+
assert(socket);
|
|
239
|
+
this._socket = socket;
|
|
240
|
+
if (doDebug) {
|
|
241
|
+
debugLog(" TCP_transport#_install_socket ", this.name);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// install packet assembler ...
|
|
245
|
+
this.packetAssembler = new PacketAssembler({
|
|
246
|
+
readMessageFunc: readRawMessageHeader,
|
|
247
|
+
|
|
248
|
+
minimumSizeInBytes: this.headerSize
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/* istanbul ignore next */
|
|
252
|
+
if (!this.packetAssembler) {
|
|
253
|
+
throw new Error("Internal Error");
|
|
254
|
+
}
|
|
255
|
+
this.packetAssembler.on("message", (messageChunk: Buffer) => this._on_message_received(messageChunk));
|
|
256
|
+
|
|
257
|
+
this._socket
|
|
258
|
+
.on("data", (data: Buffer) => this._on_socket_data(data))
|
|
259
|
+
.on("close", (hadError) => this._on_socket_close(hadError))
|
|
260
|
+
.on("end", (err: Error) => this._on_socket_end(err))
|
|
261
|
+
.on("error", (err: Error) => this._on_socket_error(err));
|
|
262
|
+
|
|
263
|
+
// set socket timeout
|
|
264
|
+
debugLog(" TCP_transport#install => setting " + this.name + " _socket.setTimeout to ", this.timeout);
|
|
265
|
+
|
|
266
|
+
// let use a large timeout here to make sure that we not conflict with our internal timeout
|
|
267
|
+
this._socket!.setTimeout(this.timeout + 2000, () => {
|
|
268
|
+
debugLog(` _socket ${this.name} has timed out (timeout = ${this.timeout})`);
|
|
269
|
+
this.prematureTerminate(new Error("INTERNAL_EPIPE timeout=" + this.timeout));
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
public prematureTerminate(err: Error) {
|
|
274
|
+
debugLog("prematureTerminate", err ? err.message : "");
|
|
275
|
+
if (this._socket) {
|
|
276
|
+
err.message = "EPIPE_" + err.message;
|
|
277
|
+
// we consider this as an error
|
|
278
|
+
const _s = this._socket;
|
|
279
|
+
_s.end();
|
|
280
|
+
_s.destroy(); // new Error("Socket has timed out"));
|
|
281
|
+
_s.emit("error", err);
|
|
282
|
+
this._socket = null;
|
|
283
|
+
this.dispose();
|
|
284
|
+
_s.removeAllListeners();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* @method _install_one_time_message_receiver
|
|
289
|
+
*
|
|
290
|
+
* install a one time message receiver callback
|
|
291
|
+
*
|
|
292
|
+
* Rules:
|
|
293
|
+
* * TCP_transport will not emit the ```message``` event, while the "one time message receiver" is in operation.
|
|
294
|
+
* * the TCP_transport will wait for the next complete message chunk and call the provided callback func
|
|
295
|
+
* ```callback(null,messageChunk);```
|
|
296
|
+
*
|
|
297
|
+
* if a messageChunk is not received within ```TCP_transport.timeout``` or if the underlying socket reports
|
|
298
|
+
* an error, the callback function will be called with an Error.
|
|
299
|
+
*
|
|
300
|
+
*/
|
|
301
|
+
protected _install_one_time_message_receiver(callback: CallbackWithData) {
|
|
302
|
+
assert(!this._theCallback, "callback already set");
|
|
303
|
+
assert(typeof callback === "function");
|
|
304
|
+
this._theCallback = callback;
|
|
305
|
+
this._start_one_time_message_receiver();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private _fulfill_pending_promises(err: Error | null, data?: Buffer): boolean {
|
|
309
|
+
this._cleanup_timers();
|
|
310
|
+
|
|
311
|
+
if (this._socket && this._on_error_during_one_time_message_receiver) {
|
|
312
|
+
this._socket.removeListener("close", this._on_error_during_one_time_message_receiver);
|
|
313
|
+
this._on_error_during_one_time_message_receiver = null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const callback = this._theCallback;
|
|
317
|
+
this._theCallback = undefined;
|
|
318
|
+
|
|
319
|
+
if (callback) {
|
|
320
|
+
callback(err, data);
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
private _on_message_received(messageChunk: Buffer) {
|
|
328
|
+
const hasCallback = this._fulfill_pending_promises(null, messageChunk);
|
|
329
|
+
this.chunkReadCount++;
|
|
330
|
+
if (!hasCallback) {
|
|
331
|
+
/**
|
|
332
|
+
* notify the observers that a message chunk has been received
|
|
333
|
+
* @event message
|
|
334
|
+
* @param message_chunk the message chunk
|
|
335
|
+
*/
|
|
336
|
+
this.emit("message", messageChunk);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private _cleanup_timers() {
|
|
341
|
+
if (this._timerId) {
|
|
342
|
+
clearTimeout(this._timerId);
|
|
343
|
+
this._timerId = null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private _start_one_time_message_receiver() {
|
|
348
|
+
assert(!this._timerId, "timer already started");
|
|
349
|
+
|
|
350
|
+
// Setup timeout detection timer ....
|
|
351
|
+
this._timerId = setTimeout(() => {
|
|
352
|
+
this._timerId = null;
|
|
353
|
+
this._fulfill_pending_promises(new Error(`Timeout in waiting for data on socket ( timeout was = ${this.timeout} ms)`));
|
|
354
|
+
}, this.timeout);
|
|
355
|
+
|
|
356
|
+
// also monitored
|
|
357
|
+
if (this._socket) {
|
|
358
|
+
// to do = intercept socket error as well
|
|
359
|
+
this._on_error_during_one_time_message_receiver = (err?: Error) => {
|
|
360
|
+
this._fulfill_pending_promises(
|
|
361
|
+
new Error(`ERROR in waiting for data on socket ( timeout was = ${this.timeout} ms) ` + err?.message)
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
this._socket.on("close", this._on_error_during_one_time_message_receiver);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private on_socket_closed(err?: Error) {
|
|
369
|
+
if (this._onSocketClosedHasBeenCalled) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
assert(!this._onSocketClosedHasBeenCalled);
|
|
373
|
+
this._onSocketClosedHasBeenCalled = true; // we don't want to send close event twice ...
|
|
374
|
+
/**
|
|
375
|
+
* notify the observers that the transport layer has been disconnected.
|
|
376
|
+
* @event socket_closed
|
|
377
|
+
* @param err the Error object or null
|
|
378
|
+
*/
|
|
379
|
+
this.emit("socket_closed", err || null);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private _on_socket_data(data: Buffer): void {
|
|
383
|
+
if (!this.packetAssembler) {
|
|
384
|
+
throw new Error("internal Error");
|
|
385
|
+
}
|
|
386
|
+
this.bytesRead += data.length;
|
|
387
|
+
if (data.length > 0) {
|
|
388
|
+
this.packetAssembler.feed(data);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private _on_socket_close(hadError: boolean) {
|
|
393
|
+
// istanbul ignore next
|
|
394
|
+
if (doDebug) {
|
|
395
|
+
debugLog(chalk.red(" SOCKET CLOSE : "), chalk.yellow("had_error ="), chalk.cyan(hadError.toString()), this.name);
|
|
396
|
+
}
|
|
397
|
+
if (this._socket) {
|
|
398
|
+
debugLog(
|
|
399
|
+
" remote address = ",
|
|
400
|
+
this._socket.remoteAddress,
|
|
401
|
+
" ",
|
|
402
|
+
this._socket.remoteFamily,
|
|
403
|
+
" ",
|
|
404
|
+
this._socket.remotePort
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (hadError) {
|
|
408
|
+
if (this._socket) {
|
|
409
|
+
this._socket.destroy();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const err = hadError ? new Error("ERROR IN SOCKET " + hadError.toString()) : undefined;
|
|
413
|
+
this.on_socket_closed(err);
|
|
414
|
+
this.dispose();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private _on_socket_ended_message(err?: Error) {
|
|
418
|
+
if (this._disconnecting) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
debugLog(chalk.red("Transport Connection ended") + " " + this.name);
|
|
423
|
+
assert(!this._disconnecting);
|
|
424
|
+
err = err || new Error("_socket has been disconnected by third party");
|
|
425
|
+
|
|
426
|
+
this.on_socket_ended(err);
|
|
427
|
+
|
|
428
|
+
this._disconnecting = true;
|
|
429
|
+
|
|
430
|
+
debugLog(" bytesRead = ", this.bytesRead);
|
|
431
|
+
debugLog(" bytesWritten = ", this.bytesWritten);
|
|
432
|
+
this._fulfill_pending_promises(new Error("Connection aborted - ended by server : " + (err ? err.message : "")));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private _on_socket_end(err: Error) {
|
|
436
|
+
// istanbul ignore next
|
|
437
|
+
if (doDebug) {
|
|
438
|
+
debugLog(chalk.red(" SOCKET END : err="), chalk.yellow(err ? err.message : "null"), this.name);
|
|
439
|
+
}
|
|
440
|
+
this._on_socket_ended_message(err);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private _on_socket_error(err: Error) {
|
|
444
|
+
// istanbul ignore next
|
|
445
|
+
if (doDebug) {
|
|
446
|
+
debugLog(chalk.red(" SOCKET ERROR : "), chalk.yellow(err.message), this.name);
|
|
447
|
+
}
|
|
448
|
+
// node The "close" event will be called directly following this event.
|
|
449
|
+
}
|
|
450
|
+
}
|
package/source/tools.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module node-opcua-transport
|
|
3
|
+
*/
|
|
4
|
+
import * as url from "url";
|
|
5
|
+
|
|
6
|
+
import { assert } from "node-opcua-assert";
|
|
7
|
+
import { BinaryStream, OutputBinaryStream } from "node-opcua-binary-stream";
|
|
8
|
+
import { createFastUninitializedBuffer } from "node-opcua-buffer-utils";
|
|
9
|
+
import { readMessageHeader } from "node-opcua-chunkmanager";
|
|
10
|
+
import { BaseUAObject } from "node-opcua-factory";
|
|
11
|
+
import { TCPErrorMessage } from "./TCPErrorMessage";
|
|
12
|
+
import { UrlWithStringQuery } from "url";
|
|
13
|
+
|
|
14
|
+
function is_valid_msg_type(msgType: string): boolean {
|
|
15
|
+
assert(["HEL", "ACK", "ERR", // Connection Layer
|
|
16
|
+
"OPN", "MSG", "CLO" // OPC Unified Architecture, Part 6 page 36
|
|
17
|
+
].indexOf(msgType) >= 0, "invalid message type " + msgType);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ConstructorFunc = new (...args: any[]) => BaseUAObject;
|
|
22
|
+
|
|
23
|
+
export function decodeMessage(stream: BinaryStream, classNameConstructor: ConstructorFunc): BaseUAObject {
|
|
24
|
+
|
|
25
|
+
assert(stream instanceof BinaryStream);
|
|
26
|
+
assert(classNameConstructor instanceof Function, " expecting a function for " + classNameConstructor);
|
|
27
|
+
|
|
28
|
+
const header = readMessageHeader(stream);
|
|
29
|
+
assert(stream.length === 8);
|
|
30
|
+
|
|
31
|
+
let obj;
|
|
32
|
+
if (header.msgType === "ERR") {
|
|
33
|
+
obj = new TCPErrorMessage();
|
|
34
|
+
obj.decode(stream);
|
|
35
|
+
return obj;
|
|
36
|
+
} else {
|
|
37
|
+
obj = new classNameConstructor();
|
|
38
|
+
obj.decode(stream);
|
|
39
|
+
return obj;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function packTcpMessage(msgType: string, encodableObject: BaseUAObject): Buffer {
|
|
44
|
+
|
|
45
|
+
assert(is_valid_msg_type(msgType));
|
|
46
|
+
|
|
47
|
+
const messageChunk = createFastUninitializedBuffer(encodableObject.binaryStoreSize() + 8);
|
|
48
|
+
// encode encodeableObject in a packet
|
|
49
|
+
const stream = new BinaryStream(messageChunk);
|
|
50
|
+
encodeMessage(msgType, encodableObject, stream);
|
|
51
|
+
|
|
52
|
+
return messageChunk;
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// opc.tcp://xleuri11022:51210/UA/SampleServer
|
|
57
|
+
export function parseEndpointUrl(endpointUrl: string): url.Url {
|
|
58
|
+
const _url = url.parse(endpointUrl);
|
|
59
|
+
if (!_url.protocol || !_url.hostname) {
|
|
60
|
+
throw new Error("Invalid endpoint url " + endpointUrl);
|
|
61
|
+
}
|
|
62
|
+
return _url;
|
|
63
|
+
/*
|
|
64
|
+
const r = /^([a-z.]*):\/\/([a-zA-Z_\-.\-0-9]*):([0-9]*)(\/.*){0,1}/;
|
|
65
|
+
|
|
66
|
+
const matches = r.exec(endpointUrl);
|
|
67
|
+
|
|
68
|
+
if (!matches) {
|
|
69
|
+
throw new Error("Invalid endpoint url " + endpointUrl);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
protocol: matches[1],
|
|
73
|
+
|
|
74
|
+
hostname: matches[2],
|
|
75
|
+
|
|
76
|
+
port: parseInt(matches[3], 10),
|
|
77
|
+
|
|
78
|
+
address: matches[4] || ""
|
|
79
|
+
};
|
|
80
|
+
*/
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function is_valid_endpointUrl(endpointUrl: string): boolean {
|
|
84
|
+
const e = parseEndpointUrl(endpointUrl);
|
|
85
|
+
return e.hasOwnProperty("hostname");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function writeTCPMessageHeader(msgType: string, chunkType: string, totalLength: number, stream: OutputBinaryStream) {
|
|
89
|
+
|
|
90
|
+
if (stream instanceof Buffer) {
|
|
91
|
+
stream = new BinaryStream(stream);
|
|
92
|
+
}
|
|
93
|
+
assert(is_valid_msg_type(msgType));
|
|
94
|
+
assert(["A", "F", "C"].indexOf(chunkType) !== -1);
|
|
95
|
+
|
|
96
|
+
stream.writeUInt8(msgType.charCodeAt(0));
|
|
97
|
+
stream.writeUInt8(msgType.charCodeAt(1));
|
|
98
|
+
stream.writeUInt8(msgType.charCodeAt(2));
|
|
99
|
+
// Chunk type
|
|
100
|
+
stream.writeUInt8(chunkType.charCodeAt(0)); // reserved
|
|
101
|
+
|
|
102
|
+
stream.writeUInt32(totalLength);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function encodeMessage(msgType: string, messageContent: BaseUAObject, stream: OutputBinaryStream) {
|
|
106
|
+
|
|
107
|
+
// the length of the message, in bytes. (includes the 8 bytes of the message header)
|
|
108
|
+
const totalLength = messageContent.binaryStoreSize() + 8;
|
|
109
|
+
|
|
110
|
+
writeTCPMessageHeader(msgType, "F", totalLength, stream);
|
|
111
|
+
messageContent.encode(stream);
|
|
112
|
+
assert(totalLength === stream.length, "invalid message size");
|
|
113
|
+
}
|
package/source/utils.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import { assert } from "node-opcua-assert";
|
|
3
|
+
import { setFakeTransport } from "../source";
|
|
4
|
+
import { HalfComChannel } from "./half_com_channel";
|
|
5
|
+
|
|
6
|
+
export class DirectTransport extends EventEmitter {
|
|
7
|
+
|
|
8
|
+
public client: HalfComChannel;
|
|
9
|
+
public server: HalfComChannel;
|
|
10
|
+
public url: string;
|
|
11
|
+
|
|
12
|
+
private _responses?: any[];
|
|
13
|
+
constructor() {
|
|
14
|
+
|
|
15
|
+
super();
|
|
16
|
+
|
|
17
|
+
this.client = new HalfComChannel();
|
|
18
|
+
this.server = new HalfComChannel();
|
|
19
|
+
|
|
20
|
+
this.client.on("send_data", (data) => {
|
|
21
|
+
assert(data instanceof Buffer);
|
|
22
|
+
this.server.emit("data", data);
|
|
23
|
+
});
|
|
24
|
+
this.server.on("send_data", (data) => {
|
|
25
|
+
assert(data instanceof Buffer);
|
|
26
|
+
this.client.emit("data", data);
|
|
27
|
+
});
|
|
28
|
+
this.server.on("ending", () => {
|
|
29
|
+
this.client.emit("end");
|
|
30
|
+
this.client._hasEnded = true;
|
|
31
|
+
});
|
|
32
|
+
this.client.on("ending", () => {
|
|
33
|
+
this.server.emit("end");
|
|
34
|
+
this.server._hasEnded = true;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.server.on("end", (err: Error) => {
|
|
38
|
+
this.emit("end", err);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.server.on("data", (data: Buffer) => {
|
|
42
|
+
const func = this.popResponse();
|
|
43
|
+
if (func) {
|
|
44
|
+
func(this.server, data);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.url = "fake://localhost:2033/SomeAddress";
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public initialize(done: () => void) {
|
|
53
|
+
setFakeTransport(this.client);
|
|
54
|
+
done();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public shutdown(done: () => void) {
|
|
58
|
+
this.client.end();
|
|
59
|
+
this.server.end();
|
|
60
|
+
if (done) {
|
|
61
|
+
setImmediate(done);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public popResponse() {
|
|
66
|
+
if (!this._responses) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return this._responses.shift();
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public pushResponse(func: any) {
|
|
74
|
+
this._responses = this._responses || [];
|
|
75
|
+
this._responses.push(func);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
}
|