diodejs 0.4.0 → 0.4.2
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/README.md +185 -68
- package/clientManager.js +1679 -78
- package/connection.js +24 -3
- package/examples/RPCTest.js +11 -1
- package/examples/fleetContractExample.js +45 -0
- package/examples/publishPortTest.js +3 -2
- package/networkDiscoveryClient.js +89 -0
- package/package.json +4 -3
- package/publishPort.js +50 -27
- package/scripts/benchmark-relay-selection.js +476 -0
- package/test/clientManager.relaySelection.test.js +1454 -0
- package/test/fixtures/dio-network-snapshot.json +82 -0
- package/test/fleetContract.test.js +71 -0
- package/test/publishPort.test.js +399 -0
- package/utils.js +26 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"last_seen": "0x69aac815",
|
|
4
|
+
"retries": "0x00",
|
|
5
|
+
"last_error": "0x00",
|
|
6
|
+
"node_id": "0x7e4cd38d266902444dc9c8f7c0aa716a32497d0b",
|
|
7
|
+
"node": [
|
|
8
|
+
"server",
|
|
9
|
+
"144.126.157.138",
|
|
10
|
+
"0xa056",
|
|
11
|
+
"0xc76f",
|
|
12
|
+
"1.9.3",
|
|
13
|
+
[["name", "pause_chalk@diode-us2b"]]
|
|
14
|
+
],
|
|
15
|
+
"connected": true
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"last_seen": "0x69aac815",
|
|
19
|
+
"retries": "0x00",
|
|
20
|
+
"last_error": null,
|
|
21
|
+
"node_id": "0x3edd3d61a0f9b85a3adac02dfb97c7d48eaf5462",
|
|
22
|
+
"node": [
|
|
23
|
+
"server",
|
|
24
|
+
"192.168.100.4",
|
|
25
|
+
"0xa056",
|
|
26
|
+
"0xc76f",
|
|
27
|
+
"1.4.2",
|
|
28
|
+
[["name", "private_node"]]
|
|
29
|
+
],
|
|
30
|
+
"connected": true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"last_seen": "0x69aac815",
|
|
34
|
+
"retries": "0x01",
|
|
35
|
+
"last_error": "0x69a69d52",
|
|
36
|
+
"node_id": "0x1350d3b501d6842ed881b59de4b95b27372bfae8",
|
|
37
|
+
"node": [
|
|
38
|
+
"server",
|
|
39
|
+
"194.233.80.251",
|
|
40
|
+
"0xa056",
|
|
41
|
+
"0xc76f",
|
|
42
|
+
"1.9.3",
|
|
43
|
+
[["name", "fringe_quiz@diode-as2b"]]
|
|
44
|
+
],
|
|
45
|
+
"connected": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"last_seen": "0x69aac815",
|
|
49
|
+
"retries": "0x00",
|
|
50
|
+
"last_error": "0x00",
|
|
51
|
+
"node_id": "0x1111111111111111111111111111111111111111",
|
|
52
|
+
"node": [
|
|
53
|
+
"client",
|
|
54
|
+
"1.2.3.4",
|
|
55
|
+
"0xa056",
|
|
56
|
+
"0xc76f",
|
|
57
|
+
"1.9.3",
|
|
58
|
+
[["name", "wrong_type"]]
|
|
59
|
+
],
|
|
60
|
+
"connected": true
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"last_seen": "0x69aac815",
|
|
64
|
+
"retries": "0x00",
|
|
65
|
+
"last_error": "0x00",
|
|
66
|
+
"node_id": "0x2222222222222222222222222222222222222222",
|
|
67
|
+
"node": [
|
|
68
|
+
"server",
|
|
69
|
+
"34.129.36.236",
|
|
70
|
+
"0xa056",
|
|
71
|
+
"0xc76f",
|
|
72
|
+
"1.9.3",
|
|
73
|
+
[["name", "public-alt"]]
|
|
74
|
+
],
|
|
75
|
+
"connected": false
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"node_id": "0x3333333333333333333333333333333333333333",
|
|
79
|
+
"node": ["server", "", "0xa056", "0xc76f", "1.9.3", []],
|
|
80
|
+
"connected": true
|
|
81
|
+
}
|
|
82
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const DiodeConnection = require('../connection');
|
|
8
|
+
const { DEFAULT_FLEET_CONTRACT } = require('../utils');
|
|
9
|
+
|
|
10
|
+
function makeTempDir() {
|
|
11
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'diode-fleet-test-'));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function makeConnection() {
|
|
15
|
+
const tempDir = makeTempDir();
|
|
16
|
+
const keyLocation = path.join(tempDir, 'keys.json');
|
|
17
|
+
const connection = new DiodeConnection('relay.example', 41046, keyLocation);
|
|
18
|
+
connection.RPC = {
|
|
19
|
+
getEpoch: async () => 77,
|
|
20
|
+
};
|
|
21
|
+
connection._waitForServerEthereumAddress = async () => Buffer.from('aa'.repeat(20), 'hex');
|
|
22
|
+
return connection;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('createTicketCommand uses the configured fleet contract in ticketv2', async () => {
|
|
26
|
+
const connection = makeConnection();
|
|
27
|
+
|
|
28
|
+
connection.setFleetContract('0x1111111111111111111111111111111111111111');
|
|
29
|
+
const command = await connection.createTicketCommand();
|
|
30
|
+
|
|
31
|
+
assert.equal(command[0], 'ticketv2');
|
|
32
|
+
assert.equal(command[2], 77);
|
|
33
|
+
assert.ok(Buffer.isBuffer(command[3]));
|
|
34
|
+
assert.equal(command[3].toString('hex'), '1111111111111111111111111111111111111111');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('createTicketSignature changes when the fleet contract changes', async () => {
|
|
38
|
+
const connection = makeConnection();
|
|
39
|
+
const serverIdBuffer = Buffer.from('bb'.repeat(20), 'hex');
|
|
40
|
+
const totalConnections = 5;
|
|
41
|
+
const totalBytes = 123456;
|
|
42
|
+
const localAddress = 'client-a';
|
|
43
|
+
const epoch = 88;
|
|
44
|
+
|
|
45
|
+
const defaultSignature = await connection.createTicketSignature(
|
|
46
|
+
serverIdBuffer,
|
|
47
|
+
totalConnections,
|
|
48
|
+
totalBytes,
|
|
49
|
+
localAddress,
|
|
50
|
+
epoch,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
connection.setFleetContract('0x2222222222222222222222222222222222222222');
|
|
54
|
+
const updatedSignature = await connection.createTicketSignature(
|
|
55
|
+
serverIdBuffer,
|
|
56
|
+
totalConnections,
|
|
57
|
+
totalBytes,
|
|
58
|
+
localAddress,
|
|
59
|
+
epoch,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
assert.notDeepEqual(updatedSignature, defaultSignature);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('DiodeConnection defaults to the existing fleet contract', async () => {
|
|
66
|
+
const connection = makeConnection();
|
|
67
|
+
|
|
68
|
+
const command = await connection.createTicketCommand();
|
|
69
|
+
|
|
70
|
+
assert.equal(command[3].toString('hex'), DEFAULT_FLEET_CONTRACT.slice(2));
|
|
71
|
+
});
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const tls = require('tls');
|
|
6
|
+
const dgram = require('dgram');
|
|
7
|
+
|
|
8
|
+
const PublishPort = require('../publishPort');
|
|
9
|
+
|
|
10
|
+
class FakeStreamSocket extends EventEmitter {
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
this.destroyed = false;
|
|
14
|
+
this.remoteAddress = undefined;
|
|
15
|
+
this.remotePort = undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
setNoDelay() {}
|
|
19
|
+
pause() {}
|
|
20
|
+
write() {}
|
|
21
|
+
end() {}
|
|
22
|
+
destroy() {
|
|
23
|
+
this.destroyed = true;
|
|
24
|
+
}
|
|
25
|
+
pipe(destination) {
|
|
26
|
+
return destination;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class FakeDatagramSocket extends EventEmitter {
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
this.connectCalls = [];
|
|
34
|
+
this.sendCalls = [];
|
|
35
|
+
this.closed = false;
|
|
36
|
+
this.remoteAddress = undefined;
|
|
37
|
+
this.remotePort = undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
connect(port, host, callback) {
|
|
41
|
+
this.connectCalls.push({ port, host });
|
|
42
|
+
if (typeof callback === 'function') {
|
|
43
|
+
callback();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
send(...args) {
|
|
48
|
+
if (args.length >= 3 && typeof args[1] === 'number' && typeof args[2] === 'string') {
|
|
49
|
+
this.sendCalls.push({ data: args[0], port: args[1], address: args[2] });
|
|
50
|
+
} else {
|
|
51
|
+
this.sendCalls.push({ data: args[0] });
|
|
52
|
+
}
|
|
53
|
+
const callback = args.find((arg) => typeof arg === 'function');
|
|
54
|
+
if (callback) {
|
|
55
|
+
callback(null);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close() {
|
|
60
|
+
this.closed = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setRecvBufferSize() {}
|
|
64
|
+
setSendBufferSize() {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class FakeTlsSocket extends EventEmitter {
|
|
68
|
+
setNoDelay() {}
|
|
69
|
+
pipe(destination) {
|
|
70
|
+
return destination;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class FakeConnection extends EventEmitter {
|
|
75
|
+
constructor() {
|
|
76
|
+
super();
|
|
77
|
+
this.connections = new Map();
|
|
78
|
+
this.sentResponses = [];
|
|
79
|
+
this.sentErrors = [];
|
|
80
|
+
this.portCloseCalls = [];
|
|
81
|
+
this.portSendCalls = [];
|
|
82
|
+
this.RPC = {
|
|
83
|
+
sendResponse: async (...args) => {
|
|
84
|
+
this.sentResponses.push(args);
|
|
85
|
+
},
|
|
86
|
+
sendError: async (...args) => {
|
|
87
|
+
this.sentErrors.push(args);
|
|
88
|
+
},
|
|
89
|
+
portClose: async (...args) => {
|
|
90
|
+
this.portCloseCalls.push(args);
|
|
91
|
+
},
|
|
92
|
+
portSend: async (...args) => {
|
|
93
|
+
this.portSendCalls.push(args);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
addConnection(ref, connectionInfo) {
|
|
99
|
+
this.connections.set(ref.toString('hex'), connectionInfo);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getConnection(ref) {
|
|
103
|
+
return this.connections.get(ref.toString('hex'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
deleteConnection(ref) {
|
|
107
|
+
return this.connections.delete(ref.toString('hex'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getClientSocket() {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getServerRelayHost() {
|
|
115
|
+
return 'relay.example';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getDeviceCertificate() {
|
|
119
|
+
return 'fake-cert';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getEthereumAddress() {
|
|
123
|
+
return '0x' + 'aa'.repeat(20);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getPrivateKey() {
|
|
127
|
+
return Buffer.alloc(32, 1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function makeRef(value = '01') {
|
|
132
|
+
return Buffer.from(value.padStart(2, '0'), 'hex');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function makeSessionId(value = '02') {
|
|
136
|
+
return Buffer.from(value.padStart(2, '0'), 'hex');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function makeDeviceId(hexByte) {
|
|
140
|
+
const byte = hexByte.length === 1 ? hexByte.repeat(2) : hexByte;
|
|
141
|
+
return Buffer.from(byte.repeat(20), 'hex');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
test('PublishPort array input defaults host to 127.0.0.1', () => {
|
|
145
|
+
const connection = new FakeConnection();
|
|
146
|
+
const publishPort = new PublishPort(connection, [8080]);
|
|
147
|
+
|
|
148
|
+
assert.deepEqual(publishPort.getPublishedPorts(), {
|
|
149
|
+
8080: { mode: 'public', whitelist: [], host: '127.0.0.1' },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
publishPort.stopListening();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('PublishPort preserves explicit host in object config', () => {
|
|
156
|
+
const connection = new FakeConnection();
|
|
157
|
+
const publishPort = new PublishPort(connection, {
|
|
158
|
+
8080: { mode: 'private', whitelist: ['0xabc'], host: ' backend.internal ' },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
assert.deepEqual(publishPort.getPublishedPorts(), {
|
|
162
|
+
8080: { mode: 'private', whitelist: ['0xabc'], host: 'backend.internal' },
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
publishPort.stopListening();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('PublishPort rejects invalid host values', () => {
|
|
169
|
+
const connection = new FakeConnection();
|
|
170
|
+
|
|
171
|
+
assert.throws(() => {
|
|
172
|
+
new PublishPort(connection, { 8080: { mode: 'public', host: ' ' } });
|
|
173
|
+
}, TypeError);
|
|
174
|
+
|
|
175
|
+
assert.throws(() => {
|
|
176
|
+
new PublishPort(connection, { 8080: { mode: 'public', host: 42 } });
|
|
177
|
+
}, TypeError);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('TCP publish connects to configured host', () => {
|
|
181
|
+
const connection = new FakeConnection();
|
|
182
|
+
const publishPort = new PublishPort(connection, {
|
|
183
|
+
8080: { mode: 'public', host: '192.168.1.10' },
|
|
184
|
+
});
|
|
185
|
+
const originalConnect = net.connect;
|
|
186
|
+
const connectCalls = [];
|
|
187
|
+
|
|
188
|
+
net.connect = (options, callback) => {
|
|
189
|
+
const socket = new FakeStreamSocket();
|
|
190
|
+
connectCalls.push(options);
|
|
191
|
+
process.nextTick(() => {
|
|
192
|
+
if (typeof callback === 'function') {
|
|
193
|
+
callback();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
return socket;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
publishPort.handleTCPConnection(makeSessionId(), makeRef(), 8080, '0x' + '11'.repeat(20), publishPort.getPublishedPorts()[8080], connection);
|
|
201
|
+
} finally {
|
|
202
|
+
net.connect = originalConnect;
|
|
203
|
+
publishPort.stopListening();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
assert.equal(connectCalls.length, 1);
|
|
207
|
+
assert.deepEqual(connectCalls[0], { port: 8080, host: '192.168.1.10' });
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('TLS publish backend socket connects to configured host', () => {
|
|
211
|
+
const connection = new FakeConnection();
|
|
212
|
+
const publishPort = new PublishPort(connection, {
|
|
213
|
+
8443: { mode: 'public', host: 'backend.internal' },
|
|
214
|
+
});
|
|
215
|
+
const originalConnect = net.connect;
|
|
216
|
+
const originalTlsSocket = tls.TLSSocket;
|
|
217
|
+
const connectCalls = [];
|
|
218
|
+
|
|
219
|
+
net.connect = (options, callback) => {
|
|
220
|
+
const socket = new FakeStreamSocket();
|
|
221
|
+
connectCalls.push(options);
|
|
222
|
+
process.nextTick(() => {
|
|
223
|
+
if (typeof callback === 'function') {
|
|
224
|
+
callback();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
return socket;
|
|
228
|
+
};
|
|
229
|
+
tls.TLSSocket = FakeTlsSocket;
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
publishPort.handleTLSConnection(makeSessionId(), makeRef('03'), 8443, '0x' + '11'.repeat(20), publishPort.getPublishedPorts()[8443], connection);
|
|
233
|
+
} finally {
|
|
234
|
+
net.connect = originalConnect;
|
|
235
|
+
tls.TLSSocket = originalTlsSocket;
|
|
236
|
+
publishPort.stopListening();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
assert.equal(connectCalls.length, 1);
|
|
240
|
+
assert.deepEqual(connectCalls[0], { port: 8443, host: 'backend.internal' });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('UDP publish stores and sends to configured host', () => {
|
|
244
|
+
const connection = new FakeConnection();
|
|
245
|
+
const publishPort = new PublishPort(connection, {
|
|
246
|
+
5353: { mode: 'public', host: '192.168.1.20' },
|
|
247
|
+
});
|
|
248
|
+
const originalCreateSocket = dgram.createSocket;
|
|
249
|
+
const sockets = [];
|
|
250
|
+
|
|
251
|
+
dgram.createSocket = () => {
|
|
252
|
+
const socket = new FakeDatagramSocket();
|
|
253
|
+
sockets.push(socket);
|
|
254
|
+
return socket;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const ref = makeRef('04');
|
|
259
|
+
publishPort.handleUDPConnection(makeSessionId('05'), ref, 5353, '0x' + '11'.repeat(20), publishPort.getPublishedPorts()[5353], connection);
|
|
260
|
+
publishPort.handlePortSend(makeSessionId('06'), ['portsend', ref, Buffer.from('hello')], connection);
|
|
261
|
+
|
|
262
|
+
const connectionInfo = connection.getConnection(ref);
|
|
263
|
+
assert.deepEqual(connectionInfo.remoteInfo, { port: 5353, address: '192.168.1.20' });
|
|
264
|
+
assert.equal(sockets[0].sendCalls.length, 1);
|
|
265
|
+
assert.equal(sockets[0].sendCalls[0].address, '192.168.1.20');
|
|
266
|
+
assert.equal(sockets[0].sendCalls[0].port, 5353);
|
|
267
|
+
} finally {
|
|
268
|
+
dgram.createSocket = originalCreateSocket;
|
|
269
|
+
publishPort.stopListening();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('native TCP publish connects local socket to configured host', () => {
|
|
274
|
+
const connection = new FakeConnection();
|
|
275
|
+
const publishPort = new PublishPort(connection, {
|
|
276
|
+
8089: { mode: 'public', host: '10.0.0.8' },
|
|
277
|
+
});
|
|
278
|
+
const originalConnect = net.connect;
|
|
279
|
+
const connectCalls = [];
|
|
280
|
+
|
|
281
|
+
net.connect = (options, callback) => {
|
|
282
|
+
const socket = new FakeStreamSocket();
|
|
283
|
+
connectCalls.push(options);
|
|
284
|
+
process.nextTick(() => {
|
|
285
|
+
if (typeof callback === 'function') {
|
|
286
|
+
callback();
|
|
287
|
+
}
|
|
288
|
+
socket.emit('connect');
|
|
289
|
+
});
|
|
290
|
+
return socket;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
publishPort.handleNativeTCPRelay(
|
|
295
|
+
makeSessionId('07'),
|
|
296
|
+
41000,
|
|
297
|
+
{ physicalPort: 41000, port: 8089, host: '10.0.0.8', deviceId: '0x' + '11'.repeat(20), ready: false, session: null },
|
|
298
|
+
connection
|
|
299
|
+
);
|
|
300
|
+
} finally {
|
|
301
|
+
net.connect = originalConnect;
|
|
302
|
+
publishPort.stopListening();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
assert.equal(connectCalls.length, 2);
|
|
306
|
+
assert.deepEqual(connectCalls[0], { host: 'relay.example', port: 41000 });
|
|
307
|
+
assert.deepEqual(connectCalls[1], { port: 8089, host: '10.0.0.8' });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('native UDP publish connects local socket to configured host', () => {
|
|
311
|
+
const connection = new FakeConnection();
|
|
312
|
+
const publishPort = new PublishPort(connection, {
|
|
313
|
+
8090: { mode: 'public', host: 'backend.internal' },
|
|
314
|
+
});
|
|
315
|
+
const originalCreateSocket = dgram.createSocket;
|
|
316
|
+
const sockets = [];
|
|
317
|
+
|
|
318
|
+
dgram.createSocket = () => {
|
|
319
|
+
const socket = new FakeDatagramSocket();
|
|
320
|
+
sockets.push(socket);
|
|
321
|
+
return socket;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
publishPort.handleNativeUDPRelay(
|
|
326
|
+
makeSessionId('08'),
|
|
327
|
+
41001,
|
|
328
|
+
{ physicalPort: 41001, port: 8090, host: 'backend.internal', deviceId: '0x' + '11'.repeat(20), ready: false, session: null },
|
|
329
|
+
connection
|
|
330
|
+
);
|
|
331
|
+
} finally {
|
|
332
|
+
dgram.createSocket = originalCreateSocket;
|
|
333
|
+
publishPort.stopListening();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
assert.equal(sockets.length, 2);
|
|
337
|
+
assert.deepEqual(sockets[0].connectCalls[0], { port: 41001, host: 'relay.example' });
|
|
338
|
+
assert.deepEqual(sockets[1].connectCalls[0], { port: 8090, host: 'backend.internal' });
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('handlePortOpen preserves localhost default when host is omitted', () => {
|
|
342
|
+
const connection = new FakeConnection();
|
|
343
|
+
const publishPort = new PublishPort(connection, [8081]);
|
|
344
|
+
const originalConnect = net.connect;
|
|
345
|
+
const connectCalls = [];
|
|
346
|
+
|
|
347
|
+
net.connect = (options, callback) => {
|
|
348
|
+
const socket = new FakeStreamSocket();
|
|
349
|
+
connectCalls.push(options);
|
|
350
|
+
process.nextTick(() => {
|
|
351
|
+
if (typeof callback === 'function') {
|
|
352
|
+
callback();
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return socket;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
publishPort.handlePortOpen(
|
|
360
|
+
makeSessionId('09'),
|
|
361
|
+
['portopen', '8081', makeRef('0a'), makeDeviceId('1')],
|
|
362
|
+
connection
|
|
363
|
+
);
|
|
364
|
+
} finally {
|
|
365
|
+
net.connect = originalConnect;
|
|
366
|
+
publishPort.stopListening();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
assert.deepEqual(connectCalls[0], { port: 8081, host: '127.0.0.1' });
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('handlePortOpen rejects non-whitelisted devices before connecting', () => {
|
|
373
|
+
const connection = new FakeConnection();
|
|
374
|
+
const publishPort = new PublishPort(connection, {
|
|
375
|
+
3000: { mode: 'private', whitelist: ['0x' + '22'.repeat(20)], host: '192.168.1.30' },
|
|
376
|
+
});
|
|
377
|
+
const originalConnect = net.connect;
|
|
378
|
+
let connectCalled = false;
|
|
379
|
+
|
|
380
|
+
net.connect = () => {
|
|
381
|
+
connectCalled = true;
|
|
382
|
+
return new FakeStreamSocket();
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
publishPort.handlePortOpen(
|
|
387
|
+
makeSessionId('0b'),
|
|
388
|
+
['portopen', '3000', makeRef('0c'), makeDeviceId('1')],
|
|
389
|
+
connection
|
|
390
|
+
);
|
|
391
|
+
} finally {
|
|
392
|
+
net.connect = originalConnect;
|
|
393
|
+
publishPort.stopListening();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
assert.equal(connectCalled, false);
|
|
397
|
+
assert.equal(connection.sentErrors.length, 1);
|
|
398
|
+
assert.equal(connection.sentErrors[0][2], 'Device not whitelisted');
|
|
399
|
+
});
|
package/utils.js
CHANGED
|
@@ -199,6 +199,30 @@ function ensureDirectoryExistence(filePath) {
|
|
|
199
199
|
fs.mkdirSync(dirname);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
const DEFAULT_FLEET_CONTRACT = '0x6000000000000000000000000000000000000000';
|
|
203
|
+
|
|
204
|
+
function normalizeFleetContractAddress(value) {
|
|
205
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
206
|
+
const buffer = toBufferView(value);
|
|
207
|
+
if (buffer.length !== 20) {
|
|
208
|
+
throw new Error('fleetContract must be a 20-byte EVM address');
|
|
209
|
+
}
|
|
210
|
+
return `0x${buffer.toString('hex')}`.toLowerCase();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof value !== 'string') {
|
|
214
|
+
throw new Error('fleetContract must be a 20-byte EVM address hex string');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const trimmed = value.trim();
|
|
218
|
+
const hex = trimmed.toLowerCase().startsWith('0x') ? trimmed.slice(2) : trimmed;
|
|
219
|
+
if (!/^[0-9a-fA-F]{40}$/.test(hex)) {
|
|
220
|
+
throw new Error('fleetContract must be a 20-byte EVM address hex string');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return `0x${hex.toLowerCase()}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
202
226
|
module.exports = {
|
|
203
227
|
makeReadable,
|
|
204
228
|
parseRequestId,
|
|
@@ -209,4 +233,6 @@ module.exports = {
|
|
|
209
233
|
loadOrGenerateKeyPair,
|
|
210
234
|
ensureDirectoryExistence,
|
|
211
235
|
toBufferView,
|
|
236
|
+
DEFAULT_FLEET_CONTRACT,
|
|
237
|
+
normalizeFleetContractAddress,
|
|
212
238
|
};
|