diodejs 0.4.1 → 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 +29 -3
- package/clientManager.js +22 -0
- package/connection.js +24 -3
- package/examples/RPCTest.js +11 -1
- package/examples/fleetContractExample.js +45 -0
- package/examples/publishPortTest.js +3 -2
- package/package.json +1 -1
- package/publishPort.js +50 -27
- package/test/clientManager.relaySelection.test.js +78 -0
- package/test/fleetContract.test.js +71 -0
- package/test/publishPort.test.js +399 -0
- package/utils.js +26 -0
package/README.md
CHANGED
|
@@ -64,6 +64,27 @@ async function main() {
|
|
|
64
64
|
main();
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
Fleet contract selection is configured on the manager:
|
|
68
|
+
```javascript
|
|
69
|
+
const { DiodeClientManager } = require('diodejs');
|
|
70
|
+
|
|
71
|
+
async function main() {
|
|
72
|
+
const client = new DiodeClientManager({
|
|
73
|
+
keyLocation: './db/keys.json',
|
|
74
|
+
fleetContract: '0x1111111111111111111111111111111111111111',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await client.connect();
|
|
78
|
+
|
|
79
|
+
// Applies on the next ticket generated by each active relay connection.
|
|
80
|
+
client.setFleetContract('0x2222222222222222222222222222222222222222');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
main();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If `fleetContract` is omitted, tickets continue using the default contract `0x6000000000000000000000000000000000000000`.
|
|
87
|
+
|
|
67
88
|
### Multi-Relay Connections (Recommended)
|
|
68
89
|
|
|
69
90
|
You can connect to multiple Diode relays and automatically route binds to the relay where the target device is connected.
|
|
@@ -305,9 +326,11 @@ async function main() {
|
|
|
305
326
|
|
|
306
327
|
// Option 2: Object with port configurations for public/private access control
|
|
307
328
|
const publishedPortsWithConfig = {
|
|
308
|
-
8080: { mode: 'public' }, // Public port, accessible by any device
|
|
329
|
+
8080: { mode: 'public' }, // Public port on 127.0.0.1, accessible by any device
|
|
330
|
+
8081: { mode: 'public', host: '192.168.1.10' }, // Forward to another reachable host
|
|
309
331
|
3000: {
|
|
310
332
|
mode: 'private',
|
|
333
|
+
host: 'backend.internal',
|
|
311
334
|
whitelist: ['0x1234abcd5678...', '0x9876fedc5432...'] // Only these devices can connect
|
|
312
335
|
}
|
|
313
336
|
};
|
|
@@ -330,6 +353,7 @@ main();
|
|
|
330
353
|
- `options.port` (number, optional): Port to use when `options.host` has no port. Defaults to `41046`.
|
|
331
354
|
- `options.hosts` (string[] or comma-separated string, optional): Explicit relay list.
|
|
332
355
|
- `options.keyLocation` (string, optional): Key storage path (default: `./db/keys.json`).
|
|
356
|
+
- `options.fleetContract` (string, optional): Fleet contract used for relay tickets. Must be a 20-byte EVM address hex string. Defaults to `0x6000000000000000000000000000000000000000`.
|
|
333
357
|
- `options.deviceCacheTtlMs` (number, optional): Cache TTL for device relay resolution (default: `30000`).
|
|
334
358
|
- `options.relaySelection` (object, optional): Relay ranking and probing options.
|
|
335
359
|
- `enabled` (boolean, optional): Enables smart relay ranking. Defaults to `true`.
|
|
@@ -365,6 +389,7 @@ main();
|
|
|
365
389
|
|
|
366
390
|
- **Methods**:
|
|
367
391
|
- `connect()`: Probes the required initial relays, merges provider and optional `dio_network` candidates, ranks the successful relays by latency, trims the warm relay set, and returns a promise. In live network discovery mode this starts from a region-diverse seed bootstrap subset plus the bounded discovery sample, then continues probing the remaining seeds in the background.
|
|
392
|
+
- `setFleetContract(address)`: Updates the fleet contract used for future ticket generation on managed connections. Accepts a 20-byte EVM address hex string and returns the manager instance.
|
|
368
393
|
- `getNearestConnection()`: Returns the preferred connected relay. With relay selection enabled, this is the lowest-latency scored relay.
|
|
369
394
|
- `getConnectionForDevice(deviceId)`: Resolves and returns a relay connection for the device. Returns a promise.
|
|
370
395
|
- `getConnections()`: Returns a list of active connections.
|
|
@@ -428,13 +453,14 @@ Connections returned by `getConnections()` or `getConnectionForDevice()` emit `r
|
|
|
428
453
|
- `connection` (DiodeClientManager): An instance of `DiodeClientManager`.
|
|
429
454
|
- `publishedPorts` (array|object): Either:
|
|
430
455
|
- An array of ports to publish (all public mode)
|
|
431
|
-
- An object mapping ports to their configuration: `{ port: { mode: 'public'|'private', whitelist: ['0x123...'] } }`
|
|
456
|
+
- An object mapping ports to their configuration: `{ port: { mode: 'public'|'private', whitelist: ['0x123...'], host: '127.0.0.1' } }`
|
|
432
457
|
- `_certPath` (string): Has no functionality and maintained for backward compatibility.
|
|
433
458
|
|
|
434
459
|
- **Methods**:
|
|
435
460
|
- `addPort(port, config)`: Adds a new port to publish. Config is optional and defaults to public mode.
|
|
436
461
|
- `port` (number): The port number to publish.
|
|
437
|
-
- `config` (object): Optional configuration with `mode` ('public'|'private') and `
|
|
462
|
+
- `config` (object): Optional configuration with `mode` ('public'|'private'), `whitelist` array, and `host` string.
|
|
463
|
+
- `host` (string, optional): Target IP or hostname for the published service. Defaults to `127.0.0.1`.
|
|
438
464
|
- `removePort(port)`: Removes a published port.
|
|
439
465
|
- `port` (number): The port number to remove.
|
|
440
466
|
- `addPorts(ports)`: Adds multiple ports at once (equivalent to the constructor's publishedPorts parameter).
|
package/clientManager.js
CHANGED
|
@@ -6,6 +6,7 @@ const DiodeConnection = require('./connection');
|
|
|
6
6
|
const DiodeRPC = require('./rpc');
|
|
7
7
|
const { fetchNetworkDirectory } = require('./networkDiscoveryClient');
|
|
8
8
|
const logger = require('./logger');
|
|
9
|
+
const { DEFAULT_FLEET_CONTRACT, normalizeFleetContractAddress } = require('./utils');
|
|
9
10
|
|
|
10
11
|
const DEFAULT_DIODE_ADDRS = [
|
|
11
12
|
'as1.prenet.diode.io:41046',
|
|
@@ -176,11 +177,29 @@ class DiodeClientManager extends EventEmitter {
|
|
|
176
177
|
this._startupCoverageComplete = false;
|
|
177
178
|
this._lastNetworkDiscoveryStats = null;
|
|
178
179
|
this._lastDeviceResolutionTrace = null;
|
|
180
|
+
this.fleetContract = DEFAULT_FLEET_CONTRACT;
|
|
181
|
+
|
|
182
|
+
if (options.fleetContract !== undefined) {
|
|
183
|
+
this.setFleetContract(options.fleetContract);
|
|
184
|
+
}
|
|
179
185
|
|
|
180
186
|
this.initialHosts = this._buildInitialHosts(options);
|
|
181
187
|
this._loadRelayScores();
|
|
182
188
|
}
|
|
183
189
|
|
|
190
|
+
setFleetContract(address) {
|
|
191
|
+
const normalizedFleetContract = normalizeFleetContractAddress(address);
|
|
192
|
+
this.fleetContract = normalizedFleetContract;
|
|
193
|
+
|
|
194
|
+
for (const connection of this.connections) {
|
|
195
|
+
if (connection && typeof connection.setFleetContract === 'function') {
|
|
196
|
+
connection.setFleetContract(normalizedFleetContract);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
|
|
184
203
|
_buildRelaySelectionOptions(options = {}) {
|
|
185
204
|
const relaySelection = options && typeof options === 'object' ? options : {};
|
|
186
205
|
const legacyWarmConnections = parsePositiveInteger(relaySelection.desiredWarmConnections, NaN);
|
|
@@ -1157,6 +1176,9 @@ class DiodeClientManager extends EventEmitter {
|
|
|
1157
1176
|
connection._managerHostKey = hostKey;
|
|
1158
1177
|
this.connections.push(connection);
|
|
1159
1178
|
this.connectionByHost.set(hostKey, connection);
|
|
1179
|
+
if (typeof connection.setFleetContract === 'function') {
|
|
1180
|
+
connection.setFleetContract(this.fleetContract);
|
|
1181
|
+
}
|
|
1160
1182
|
if (typeof connection.setLocalAddressProvider === 'function') {
|
|
1161
1183
|
connection.setLocalAddressProvider(() => this._localAddressHintFor(connection));
|
|
1162
1184
|
}
|
package/connection.js
CHANGED
|
@@ -3,7 +3,19 @@ const tls = require('tls');
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const { RLP } = require('@ethereumjs/rlp');
|
|
5
5
|
const EventEmitter = require('events');
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
makeReadable,
|
|
8
|
+
parseRequestId,
|
|
9
|
+
parseResponseType,
|
|
10
|
+
parseReason,
|
|
11
|
+
parseUInt,
|
|
12
|
+
generateCert,
|
|
13
|
+
ensureDirectoryExistence,
|
|
14
|
+
loadOrGenerateKeyPair,
|
|
15
|
+
toBufferView,
|
|
16
|
+
DEFAULT_FLEET_CONTRACT,
|
|
17
|
+
normalizeFleetContractAddress,
|
|
18
|
+
} = require('./utils');
|
|
7
19
|
const { Buffer } = require('buffer'); // Import Buffer
|
|
8
20
|
const asn1 = require('asn1.js');
|
|
9
21
|
const secp256k1 = require('secp256k1');
|
|
@@ -48,6 +60,8 @@ class DiodeConnection extends EventEmitter {
|
|
|
48
60
|
this.certPem = null;
|
|
49
61
|
this._serverEthAddress = null; // cache after first read
|
|
50
62
|
this.localAddressProvider = null;
|
|
63
|
+
this.fleetContractHex = DEFAULT_FLEET_CONTRACT;
|
|
64
|
+
this.fleetContract = Buffer.from(DEFAULT_FLEET_CONTRACT.slice(2), 'hex');
|
|
51
65
|
// Load or generate keypair
|
|
52
66
|
this.keyPair = loadOrGenerateKeyPair(this.keyLocation);
|
|
53
67
|
|
|
@@ -283,6 +297,13 @@ class DiodeConnection extends EventEmitter {
|
|
|
283
297
|
return this;
|
|
284
298
|
}
|
|
285
299
|
|
|
300
|
+
setFleetContract(fleetContract) {
|
|
301
|
+
const normalizedFleetContract = normalizeFleetContractAddress(fleetContract);
|
|
302
|
+
this.fleetContractHex = normalizedFleetContract;
|
|
303
|
+
this.fleetContract = Buffer.from(normalizedFleetContract.slice(2), 'hex');
|
|
304
|
+
return this;
|
|
305
|
+
}
|
|
306
|
+
|
|
286
307
|
// Update close method to prevent reconnection when intentionally closing
|
|
287
308
|
close() {
|
|
288
309
|
if (this.ticketUpdateTimer) {
|
|
@@ -592,7 +613,7 @@ class DiodeConnection extends EventEmitter {
|
|
|
592
613
|
|
|
593
614
|
async createTicketSignature(serverIdBuffer, totalConnections, totalBytes, localAddress, epoch) {
|
|
594
615
|
const chainId = 1284;
|
|
595
|
-
const fleetContractBuffer =
|
|
616
|
+
const fleetContractBuffer = this.fleetContract;
|
|
596
617
|
|
|
597
618
|
const localAddressBytes = Buffer.isBuffer(localAddress) || localAddress instanceof Uint8Array
|
|
598
619
|
? toBufferView(localAddress)
|
|
@@ -637,7 +658,7 @@ class DiodeConnection extends EventEmitter {
|
|
|
637
658
|
|
|
638
659
|
async createTicketCommand() {
|
|
639
660
|
const chainId = 1284;
|
|
640
|
-
const fleetContract =
|
|
661
|
+
const fleetContract = this.fleetContract;
|
|
641
662
|
let localAddress = '';
|
|
642
663
|
if (typeof this.localAddressProvider === 'function') {
|
|
643
664
|
try {
|
package/examples/RPCTest.js
CHANGED
|
@@ -3,8 +3,13 @@ const { makeReadable } = require('../utils');
|
|
|
3
3
|
|
|
4
4
|
async function main() {
|
|
5
5
|
const keyLocation = './db/keys.json';
|
|
6
|
+
const fleetContract = process.env.DIODE_FLEET_CONTRACT;
|
|
7
|
+
const nextFleetContract = process.env.DIODE_NEXT_FLEET_CONTRACT;
|
|
6
8
|
|
|
7
|
-
const client = new DiodeClientManager({
|
|
9
|
+
const client = new DiodeClientManager({
|
|
10
|
+
keyLocation,
|
|
11
|
+
...(fleetContract ? { fleetContract } : {}),
|
|
12
|
+
});
|
|
8
13
|
await client.connect();
|
|
9
14
|
const [connection] = client.getConnections();
|
|
10
15
|
if (!connection) {
|
|
@@ -15,6 +20,11 @@ async function main() {
|
|
|
15
20
|
try {
|
|
16
21
|
const address = connection.getEthereumAddress();
|
|
17
22
|
console.log('Address:', address);
|
|
23
|
+
console.log('Current fleet contract:', connection.fleetContractHex);
|
|
24
|
+
if (nextFleetContract) {
|
|
25
|
+
client.setFleetContract(nextFleetContract);
|
|
26
|
+
console.log('Updated fleet contract for future tickets:', connection.fleetContractHex);
|
|
27
|
+
}
|
|
18
28
|
const ping = await rpc.ping();
|
|
19
29
|
console.log('Ping:', ping);
|
|
20
30
|
const blockPeak = await rpc.getBlockPeak();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { DiodeClientManager } = require('../index');
|
|
2
|
+
|
|
3
|
+
const keyLocation = './db/keys.json';
|
|
4
|
+
const initialFleetContract = process.env.DIODE_FLEET_CONTRACT || '0x1111111111111111111111111111111111111111';
|
|
5
|
+
const nextFleetContract = process.env.DIODE_NEXT_FLEET_CONTRACT || '0x2222222222222222222222222222222222222222';
|
|
6
|
+
const shouldConnect = process.env.DIODE_CONNECT === 'true';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const client = new DiodeClientManager({
|
|
10
|
+
keyLocation,
|
|
11
|
+
fleetContract: initialFleetContract,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
console.log('Initial manager fleet contract:', client.fleetContract);
|
|
15
|
+
|
|
16
|
+
if (!shouldConnect) {
|
|
17
|
+
client.setFleetContract(nextFleetContract);
|
|
18
|
+
console.log('Updated manager fleet contract:', client.fleetContract);
|
|
19
|
+
console.log('Set DIODE_CONNECT=true to connect and observe the updated contract on active relay connections.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await client.connect();
|
|
25
|
+
console.log(`Connected relay count: ${client.getConnections().length}`);
|
|
26
|
+
for (const connection of client.getConnections()) {
|
|
27
|
+
console.log(`Before update ${connection.host}:${connection.port} fleet contract: ${connection.fleetContractHex}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
client.setFleetContract(nextFleetContract);
|
|
31
|
+
console.log('Updated manager fleet contract:', client.fleetContract);
|
|
32
|
+
for (const connection of client.getConnections()) {
|
|
33
|
+
console.log(`After update ${connection.host}:${connection.port} fleet contract: ${connection.fleetContractHex}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log('The new fleet contract will be used on the next ticket generated by each connection.');
|
|
37
|
+
} finally {
|
|
38
|
+
client.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
main().catch((error) => {
|
|
43
|
+
console.error('Fleet contract example failed:', error);
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
});
|
|
@@ -10,9 +10,10 @@ async function startPublishing() {
|
|
|
10
10
|
|
|
11
11
|
// Create a PublishPort instance with initial ports
|
|
12
12
|
const publishPort = new PublishPort(client, {
|
|
13
|
-
3000: { mode: 'public' },
|
|
13
|
+
3000: { mode: 'public', host: '192.168.1.10' },
|
|
14
14
|
8080: {
|
|
15
15
|
mode: 'private',
|
|
16
|
+
host: 'backend.internal',
|
|
16
17
|
whitelist: ['0xca1e71d8105a598810578fb6042fa8cbc1e7f039'] // Replace with actual addresses
|
|
17
18
|
}
|
|
18
19
|
}, keyLocation);
|
|
@@ -29,7 +30,7 @@ async function startPublishing() {
|
|
|
29
30
|
// After 15 seconds, add multiple ports
|
|
30
31
|
setTimeout(() => {
|
|
31
32
|
console.log("Adding multiple ports");
|
|
32
|
-
publishPort.addPort(3000,{ mode: 'public' });
|
|
33
|
+
publishPort.addPort(3000,{ mode: 'public', host: '127.0.0.1' });
|
|
33
34
|
console.log('Updated published ports:', publishPort.getPublishedPorts());
|
|
34
35
|
}, 15000);
|
|
35
36
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diodejs",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "A JavaScript client for interacting with the Diode network. It provides functionalities to bind and publish ports, send RPC commands, and handle responses.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/publishPort.js
CHANGED
|
@@ -38,6 +38,25 @@ function normalizeDeviceId(raw) {
|
|
|
38
38
|
return '';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function normalizePublishedPortConfig(config = {}) {
|
|
42
|
+
const hasHost = Object.prototype.hasOwnProperty.call(config, 'host');
|
|
43
|
+
const rawHost = hasHost ? config.host : '127.0.0.1';
|
|
44
|
+
if (typeof rawHost !== 'string') {
|
|
45
|
+
throw new TypeError('PublishPort config.host must be a non-empty string');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const host = rawHost.trim();
|
|
49
|
+
if (!host) {
|
|
50
|
+
throw new TypeError('PublishPort config.host must be a non-empty string');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
mode: config.mode || 'public',
|
|
55
|
+
whitelist: Array.isArray(config.whitelist) ? config.whitelist : [],
|
|
56
|
+
host,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
class DiodeSocket extends Duplex {
|
|
42
61
|
constructor(ref, rpc) {
|
|
43
62
|
super({ readableHighWaterMark: 256 * 1024, writableHighWaterMark: 256 * 1024, allowHalfOpen: false });
|
|
@@ -93,14 +112,11 @@ class PublishPort extends EventEmitter {
|
|
|
93
112
|
const portNum = parseInt(port, 10);
|
|
94
113
|
|
|
95
114
|
// Normalize the configuration
|
|
96
|
-
const portConfig =
|
|
97
|
-
mode: config.mode || 'public',
|
|
98
|
-
whitelist: Array.isArray(config.whitelist) ? config.whitelist : []
|
|
99
|
-
};
|
|
115
|
+
const portConfig = normalizePublishedPortConfig(config);
|
|
100
116
|
|
|
101
117
|
// Add to map
|
|
102
118
|
this.publishedPorts.set(portNum, portConfig);
|
|
103
|
-
logger.info(() => `Added published port ${portNum} with mode: ${portConfig.mode}`);
|
|
119
|
+
logger.info(() => `Added published port ${portNum} with mode: ${portConfig.mode}, host: ${portConfig.host}`);
|
|
104
120
|
|
|
105
121
|
return true;
|
|
106
122
|
}
|
|
@@ -186,6 +202,10 @@ class PublishPort extends EventEmitter {
|
|
|
186
202
|
return rpc;
|
|
187
203
|
}
|
|
188
204
|
|
|
205
|
+
_getPublishedPortConfig(port) {
|
|
206
|
+
return this.publishedPorts.get(port);
|
|
207
|
+
}
|
|
208
|
+
|
|
189
209
|
startListening() {
|
|
190
210
|
if (this._listening) return this; // idempotent
|
|
191
211
|
// Listen for unsolicited messages from the connection
|
|
@@ -269,7 +289,7 @@ class PublishPort extends EventEmitter {
|
|
|
269
289
|
}
|
|
270
290
|
|
|
271
291
|
// Get port configuration and check whitelist if in private mode
|
|
272
|
-
const portConfig = this.
|
|
292
|
+
const portConfig = this._getPublishedPortConfig(port);
|
|
273
293
|
if (portConfig.mode === 'private' && Array.isArray(portConfig.whitelist)) {
|
|
274
294
|
if (!portConfig.whitelist.includes(deviceId)) {
|
|
275
295
|
logger.warn(() => `Device ${deviceId} is not whitelisted for port ${port}. Rejecting request.`);
|
|
@@ -281,15 +301,15 @@ class PublishPort extends EventEmitter {
|
|
|
281
301
|
|
|
282
302
|
// Handle based on protocol
|
|
283
303
|
if (protocol === 'tcp') {
|
|
284
|
-
this.handleTCPConnection(sessionId, ref, port, deviceId, connection);
|
|
304
|
+
this.handleTCPConnection(sessionId, ref, port, deviceId, portConfig, connection);
|
|
285
305
|
} else if (protocol === 'tls') {
|
|
286
306
|
if (isHandshake) {
|
|
287
307
|
this.handleTLSHandshake(sessionId, ref, port, deviceId, connection);
|
|
288
308
|
} else {
|
|
289
|
-
this.handleTLSConnection(sessionId, ref, port, deviceId, connection);
|
|
309
|
+
this.handleTLSConnection(sessionId, ref, port, deviceId, portConfig, connection);
|
|
290
310
|
}
|
|
291
311
|
} else if (protocol === 'udp') {
|
|
292
|
-
this.handleUDPConnection(sessionId, ref, port, deviceId, connection);
|
|
312
|
+
this.handleUDPConnection(sessionId, ref, port, deviceId, portConfig, connection);
|
|
293
313
|
} else {
|
|
294
314
|
logger.warn(() => `Unsupported protocol: ${protocol}`);
|
|
295
315
|
rpc.sendError(sessionId, ref, `Unsupported protocol: ${protocol}`);
|
|
@@ -449,7 +469,7 @@ class PublishPort extends EventEmitter {
|
|
|
449
469
|
}
|
|
450
470
|
|
|
451
471
|
// Get port configuration and check whitelist if in private mode
|
|
452
|
-
const portConfig = this.
|
|
472
|
+
const portConfig = this._getPublishedPortConfig(port);
|
|
453
473
|
if (portConfig.mode === 'private' && Array.isArray(portConfig.whitelist)) {
|
|
454
474
|
if (!portConfig.whitelist.includes(deviceId)) {
|
|
455
475
|
logger.warn(() => `Device ${deviceId} is not whitelisted for port ${port}. Rejecting request.`);
|
|
@@ -473,6 +493,7 @@ class PublishPort extends EventEmitter {
|
|
|
473
493
|
const session = {
|
|
474
494
|
physicalPort,
|
|
475
495
|
port,
|
|
496
|
+
host: portConfig.host,
|
|
476
497
|
protocol,
|
|
477
498
|
deviceId: deviceId.toLowerCase(),
|
|
478
499
|
connection,
|
|
@@ -527,7 +548,7 @@ class PublishPort extends EventEmitter {
|
|
|
527
548
|
|
|
528
549
|
handleNativeTCPRelay(sessionId, physicalPortRef, session, connection) {
|
|
529
550
|
const rpc = this._getRpcFor(connection);
|
|
530
|
-
const { physicalPort, port, deviceId } = session;
|
|
551
|
+
const { physicalPort, port, host, deviceId } = session;
|
|
531
552
|
let responded = false;
|
|
532
553
|
const sendOk = () => {
|
|
533
554
|
if (responded) return;
|
|
@@ -544,7 +565,7 @@ class PublishPort extends EventEmitter {
|
|
|
544
565
|
const relaySocket = net.connect({ host: relayHost, port: physicalPort }, () => {
|
|
545
566
|
relaySocket.setNoDelay(true);
|
|
546
567
|
});
|
|
547
|
-
const localSocket = net.connect({ port }, () => {
|
|
568
|
+
const localSocket = net.connect({ port, host }, () => {
|
|
548
569
|
localSocket.setNoDelay(true);
|
|
549
570
|
});
|
|
550
571
|
localSocket.pause();
|
|
@@ -616,7 +637,7 @@ class PublishPort extends EventEmitter {
|
|
|
616
637
|
|
|
617
638
|
handleNativeUDPRelay(sessionId, physicalPortRef, session, connection) {
|
|
618
639
|
const rpc = this._getRpcFor(connection);
|
|
619
|
-
const { physicalPort, port, deviceId } = session;
|
|
640
|
+
const { physicalPort, port, host, deviceId } = session;
|
|
620
641
|
let responded = false;
|
|
621
642
|
const sendOk = () => {
|
|
622
643
|
if (responded) return;
|
|
@@ -672,7 +693,7 @@ class PublishPort extends EventEmitter {
|
|
|
672
693
|
relayReady = true;
|
|
673
694
|
maybeReady();
|
|
674
695
|
});
|
|
675
|
-
localSocket.connect(port,
|
|
696
|
+
localSocket.connect(port, host, () => {
|
|
676
697
|
localReady = true;
|
|
677
698
|
maybeReady();
|
|
678
699
|
});
|
|
@@ -706,12 +727,12 @@ class PublishPort extends EventEmitter {
|
|
|
706
727
|
}
|
|
707
728
|
}
|
|
708
729
|
|
|
709
|
-
handleTCPConnection(sessionId, ref, port, deviceId, connection) {
|
|
730
|
+
handleTCPConnection(sessionId, ref, port, deviceId, portConfig, connection) {
|
|
710
731
|
const rpc = this._getRpcFor(connection);
|
|
711
732
|
// Create a TCP connection to the local service on the specified port
|
|
712
|
-
const localSocket = net.connect({ port:
|
|
733
|
+
const localSocket = net.connect({ port, host: portConfig.host }, () => {
|
|
713
734
|
localSocket.setNoDelay(true);
|
|
714
|
-
logger.info(() => `Connected to local TCP service on
|
|
735
|
+
logger.info(() => `Connected to local TCP service on ${portConfig.host}:${port}`);
|
|
715
736
|
// Send success response
|
|
716
737
|
rpc.sendResponse(sessionId, ref, 'ok');
|
|
717
738
|
});
|
|
@@ -720,10 +741,10 @@ class PublishPort extends EventEmitter {
|
|
|
720
741
|
this.setupLocalSocketHandlers(localSocket, ref, 'tcp', rpc, connection);
|
|
721
742
|
|
|
722
743
|
// Store the local socket with the ref using connection's method
|
|
723
|
-
connection.addConnection(ref, { socket: localSocket, protocol: 'tcp', port, deviceId });
|
|
744
|
+
connection.addConnection(ref, { socket: localSocket, protocol: 'tcp', port, host: portConfig.host, deviceId });
|
|
724
745
|
}
|
|
725
746
|
|
|
726
|
-
handleTLSConnection(sessionId, ref, port, deviceId, connection) {
|
|
747
|
+
handleTLSConnection(sessionId, ref, port, deviceId, portConfig, connection) {
|
|
727
748
|
const rpc = this._getRpcFor(connection);
|
|
728
749
|
// Create a DiodeSocket instance
|
|
729
750
|
const diodeSocket = new DiodeSocket(ref, rpc);
|
|
@@ -748,8 +769,8 @@ class PublishPort extends EventEmitter {
|
|
|
748
769
|
});
|
|
749
770
|
tlsSocket.setNoDelay(true);
|
|
750
771
|
// Connect to the local service (TCP or TLS as needed)
|
|
751
|
-
const localSocket = net.connect({ port:
|
|
752
|
-
logger.info(() => `Connected to local TCP service on
|
|
772
|
+
const localSocket = net.connect({ port, host: portConfig.host }, () => {
|
|
773
|
+
logger.info(() => `Connected to local TCP service on ${portConfig.host}:${port}`);
|
|
753
774
|
// Send success response
|
|
754
775
|
rpc.sendResponse(sessionId, ref, 'ok');
|
|
755
776
|
});
|
|
@@ -775,11 +796,12 @@ class PublishPort extends EventEmitter {
|
|
|
775
796
|
localSocket,
|
|
776
797
|
protocol: 'tls',
|
|
777
798
|
port,
|
|
799
|
+
host: portConfig.host,
|
|
778
800
|
deviceId,
|
|
779
801
|
});
|
|
780
802
|
}
|
|
781
803
|
|
|
782
|
-
handleUDPConnection(sessionId, ref, port, deviceId, connection) {
|
|
804
|
+
handleUDPConnection(sessionId, ref, port, deviceId, portConfig, connection) {
|
|
783
805
|
const rpc = this._getRpcFor(connection);
|
|
784
806
|
// Create a UDP socket
|
|
785
807
|
const localSocket = dgram.createSocket('udp4');
|
|
@@ -795,7 +817,7 @@ class PublishPort extends EventEmitter {
|
|
|
795
817
|
});
|
|
796
818
|
|
|
797
819
|
// Store the remote address and port from the Diode client
|
|
798
|
-
const remoteInfo = {port, address:
|
|
820
|
+
const remoteInfo = { port, address: portConfig.host };
|
|
799
821
|
|
|
800
822
|
// Send success response
|
|
801
823
|
rpc.sendResponse(sessionId, ref, 'ok');
|
|
@@ -806,10 +828,11 @@ class PublishPort extends EventEmitter {
|
|
|
806
828
|
protocol: 'udp',
|
|
807
829
|
remoteInfo,
|
|
808
830
|
port,
|
|
831
|
+
host: portConfig.host,
|
|
809
832
|
deviceId
|
|
810
833
|
});
|
|
811
834
|
|
|
812
|
-
logger.info(() => `UDP connection set up
|
|
835
|
+
logger.info(() => `UDP connection set up for ${portConfig.host}:${port}`);
|
|
813
836
|
|
|
814
837
|
// Handle messages from the local UDP service
|
|
815
838
|
localSocket.on('message', (msg, rinfo) => {
|
|
@@ -835,7 +858,7 @@ class PublishPort extends EventEmitter {
|
|
|
835
858
|
const connectionInfo = connection.getConnection(ref);
|
|
836
859
|
// Check if the port is still open and address is still in whitelist
|
|
837
860
|
if (connectionInfo) {
|
|
838
|
-
const { socket: localSocket, protocol, remoteInfo, port, deviceId } = connectionInfo;
|
|
861
|
+
const { socket: localSocket, protocol, remoteInfo, port, host, deviceId } = connectionInfo;
|
|
839
862
|
|
|
840
863
|
if (!this.publishedPorts.has(port)) {
|
|
841
864
|
logger.warn(() => `Port ${port} is not published. Sending portclose.`);
|
|
@@ -844,7 +867,7 @@ class PublishPort extends EventEmitter {
|
|
|
844
867
|
return;
|
|
845
868
|
}
|
|
846
869
|
|
|
847
|
-
const portConfig = this.
|
|
870
|
+
const portConfig = this._getPublishedPortConfig(port);
|
|
848
871
|
if (portConfig.mode === 'private' && Array.isArray(portConfig.whitelist)) {
|
|
849
872
|
if (!portConfig.whitelist.includes(deviceId)) {
|
|
850
873
|
logger.warn(() => `Device ${deviceId} is not whitelisted for port ${port}. Sending portclose.`);
|
|
@@ -865,7 +888,7 @@ class PublishPort extends EventEmitter {
|
|
|
865
888
|
|
|
866
889
|
// Update remoteInfo if not set
|
|
867
890
|
if (!localSocket.remoteAddress) {
|
|
868
|
-
localSocket.remoteAddress =
|
|
891
|
+
localSocket.remoteAddress = host || (remoteInfo && remoteInfo.address) || portConfig.host;
|
|
869
892
|
localSocket.remotePort = port;
|
|
870
893
|
}
|
|
871
894
|
} else if (protocol === 'tcp') {
|
|
@@ -8,6 +8,7 @@ const { WebSocketServer } = require('ws');
|
|
|
8
8
|
|
|
9
9
|
const DiodeClientManager = require('../clientManager');
|
|
10
10
|
const { fetchNetworkDirectory } = require('../networkDiscoveryClient');
|
|
11
|
+
const { DEFAULT_FLEET_CONTRACT } = require('../utils');
|
|
11
12
|
|
|
12
13
|
const networkSnapshot = JSON.parse(
|
|
13
14
|
fs.readFileSync(path.join(__dirname, 'fixtures', 'dio-network-snapshot.json'), 'utf8')
|
|
@@ -48,6 +49,8 @@ class FakeConnection extends EventEmitter {
|
|
|
48
49
|
this.socket = { destroyed: false };
|
|
49
50
|
this.closeCount = 0;
|
|
50
51
|
this.serverEthereumAddress = options.serverEthereumAddress || '0x' + Buffer.from(hostKey).toString('hex').slice(0, 40).padEnd(40, '0');
|
|
52
|
+
this.fleetContract = null;
|
|
53
|
+
this.fleetContractUpdates = [];
|
|
51
54
|
this.RPC = {
|
|
52
55
|
ping: async () => {
|
|
53
56
|
await delay(options.pingDelayMs || 0);
|
|
@@ -66,6 +69,12 @@ class FakeConnection extends EventEmitter {
|
|
|
66
69
|
this.localAddressProvider = provider;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
setFleetContract(address) {
|
|
73
|
+
this.fleetContract = address;
|
|
74
|
+
this.fleetContractUpdates.push(address);
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
close() {
|
|
70
79
|
this.closeCount += 1;
|
|
71
80
|
this.socket.destroyed = true;
|
|
@@ -117,6 +126,75 @@ class TestClientManager extends DiodeClientManager {
|
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
|
|
129
|
+
test('constructor-provided fleet contract propagates to new managed connections', async () => {
|
|
130
|
+
const tempDir = makeTempDir();
|
|
131
|
+
const keyLocation = path.join(tempDir, 'keys.json');
|
|
132
|
+
const connection = new FakeConnection('custom:41046');
|
|
133
|
+
const manager = new TestClientManager({
|
|
134
|
+
keyLocation,
|
|
135
|
+
hosts: ['custom:41046'],
|
|
136
|
+
fleetContract: '1234567890abcdef1234567890ABCDEF12345678',
|
|
137
|
+
relaySelection: { scoreCachePath: null, networkDiscovery: { enabled: false } },
|
|
138
|
+
}, new Map([
|
|
139
|
+
['custom:41046', { connection }],
|
|
140
|
+
]));
|
|
141
|
+
|
|
142
|
+
await manager.connect();
|
|
143
|
+
|
|
144
|
+
assert.equal(connection.fleetContract, '0x1234567890abcdef1234567890abcdef12345678');
|
|
145
|
+
manager.close();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('setFleetContract updates existing and future managed connections', async () => {
|
|
149
|
+
const tempDir = makeTempDir();
|
|
150
|
+
const keyLocation = path.join(tempDir, 'keys.json');
|
|
151
|
+
const manager = new TestClientManager({
|
|
152
|
+
keyLocation,
|
|
153
|
+
relaySelection: { scoreCachePath: null, networkDiscovery: { enabled: false } },
|
|
154
|
+
});
|
|
155
|
+
const existing = new FakeConnection('existing:41046');
|
|
156
|
+
const future = new FakeConnection('future:41046');
|
|
157
|
+
|
|
158
|
+
manager._registerConnection(existing, 'existing:41046');
|
|
159
|
+
manager.hostBehaviors.set('future:41046', { connection: future });
|
|
160
|
+
|
|
161
|
+
manager.setFleetContract('0xabcdefabcdefabcdefabcdefabcdefabcdefabcd');
|
|
162
|
+
await manager._ensureConnection('future:41046');
|
|
163
|
+
|
|
164
|
+
assert.equal(existing.fleetContract, '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd');
|
|
165
|
+
assert.equal(future.fleetContract, '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd');
|
|
166
|
+
manager.close();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('invalid fleet contract values fail fast', () => {
|
|
170
|
+
const tempDir = makeTempDir();
|
|
171
|
+
const keyLocation = path.join(tempDir, 'keys.json');
|
|
172
|
+
|
|
173
|
+
assert.throws(
|
|
174
|
+
() => new DiodeClientManager({ keyLocation, fleetContract: '0x1234', relaySelection: { scoreCachePath: null } }),
|
|
175
|
+
/fleetContract must be a 20-byte EVM address hex string/i,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const manager = new DiodeClientManager({ keyLocation, relaySelection: { scoreCachePath: null } });
|
|
179
|
+
assert.throws(
|
|
180
|
+
() => manager.setFleetContract('not-a-contract'),
|
|
181
|
+
/fleetContract must be a 20-byte EVM address hex string/i,
|
|
182
|
+
);
|
|
183
|
+
manager.close();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('default fleet contract is applied when manager config is omitted', () => {
|
|
187
|
+
const tempDir = makeTempDir();
|
|
188
|
+
const keyLocation = path.join(tempDir, 'keys.json');
|
|
189
|
+
const manager = new DiodeClientManager({ keyLocation, relaySelection: { scoreCachePath: null } });
|
|
190
|
+
const connection = new FakeConnection('default:41046');
|
|
191
|
+
|
|
192
|
+
manager._registerConnection(connection, 'default:41046');
|
|
193
|
+
|
|
194
|
+
assert.equal(connection.fleetContract, DEFAULT_FLEET_CONTRACT);
|
|
195
|
+
manager.close();
|
|
196
|
+
});
|
|
197
|
+
|
|
120
198
|
test('tested candidates rank by measured latency', () => {
|
|
121
199
|
const tempDir = makeTempDir();
|
|
122
200
|
const keyLocation = path.join(tempDir, 'keys.json');
|
|
@@ -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
|
};
|