diodejs 0.2.2 → 0.4.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/README.md +56 -5
- package/bindPort.js +446 -66
- package/clientManager.js +435 -0
- package/connection.js +186 -72
- package/examples/RPCTest.js +9 -7
- package/examples/nativeBindTest.js +55 -0
- package/examples/nativeForwardTest.js +80 -0
- package/examples/nativeTcpClientTest.js +56 -0
- package/examples/nativeUdpClientTest.js +40 -0
- package/examples/portForwardTest.js +5 -7
- package/examples/publishAndBind.js +6 -10
- package/examples/publishPortTest.js +4 -6
- package/index.js +8 -1
- package/logger.js +10 -5
- package/nativeCrypto.js +321 -0
- package/package.json +6 -8
- package/publishPort.js +575 -94
- package/rpc.js +161 -41
- package/testServers/udpTest.js +1 -2
- package/utils.js +42 -9
package/clientManager.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const DiodeConnection = require('./connection');
|
|
3
|
+
const DiodeRPC = require('./rpc');
|
|
4
|
+
const logger = require('./logger');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_DIODE_ADDRS = [
|
|
7
|
+
'as1.prenet.diode.io:41046',
|
|
8
|
+
'as2.prenet.diode.io:41046',
|
|
9
|
+
'us1.prenet.diode.io:41046',
|
|
10
|
+
'us2.prenet.diode.io:41046',
|
|
11
|
+
'eu1.prenet.diode.io:41046',
|
|
12
|
+
'eu2.prenet.diode.io:41046',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function splitHostPort(input, defaultPort) {
|
|
16
|
+
if (!input || typeof input !== 'string') {
|
|
17
|
+
return { host: '', port: defaultPort };
|
|
18
|
+
}
|
|
19
|
+
const trimmed = input.trim();
|
|
20
|
+
if (!trimmed) {
|
|
21
|
+
return { host: '', port: defaultPort };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (trimmed.startsWith('[')) {
|
|
25
|
+
const idx = trimmed.indexOf(']');
|
|
26
|
+
if (idx !== -1) {
|
|
27
|
+
const host = trimmed.slice(1, idx);
|
|
28
|
+
const rest = trimmed.slice(idx + 1);
|
|
29
|
+
if (rest.startsWith(':')) {
|
|
30
|
+
const port = parseInt(rest.slice(1), 10);
|
|
31
|
+
return { host, port: Number.isFinite(port) && port > 0 ? port : defaultPort };
|
|
32
|
+
}
|
|
33
|
+
return { host, port: defaultPort };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lastColon = trimmed.lastIndexOf(':');
|
|
38
|
+
if (lastColon > -1 && trimmed.indexOf(':') === lastColon) {
|
|
39
|
+
const host = trimmed.slice(0, lastColon);
|
|
40
|
+
const portStr = trimmed.slice(lastColon + 1);
|
|
41
|
+
if (/^\d+$/.test(portStr)) {
|
|
42
|
+
const port = parseInt(portStr, 10);
|
|
43
|
+
return { host, port: Number.isFinite(port) && port > 0 ? port : defaultPort };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { host: trimmed, port: defaultPort };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function joinHostPort(host, port) {
|
|
51
|
+
if (!host) return '';
|
|
52
|
+
if (host.includes(':') && !host.startsWith('[')) {
|
|
53
|
+
return `[${host}]:${port}`;
|
|
54
|
+
}
|
|
55
|
+
return `${host}:${port}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeAddress(address) {
|
|
59
|
+
if (!address) return null;
|
|
60
|
+
if (Buffer.isBuffer(address)) return address;
|
|
61
|
+
if (address instanceof Uint8Array) return Buffer.from(address);
|
|
62
|
+
if (typeof address === 'string') {
|
|
63
|
+
const hex = address.toLowerCase().startsWith('0x') ? address.slice(2) : address;
|
|
64
|
+
if (!hex) return Buffer.alloc(0);
|
|
65
|
+
return Buffer.from(hex, 'hex');
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeServerIdHex(serverId) {
|
|
71
|
+
if (!serverId) return '';
|
|
72
|
+
if (Buffer.isBuffer(serverId) || serverId instanceof Uint8Array) {
|
|
73
|
+
return `0x${Buffer.from(serverId).toString('hex')}`.toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
if (typeof serverId === 'string') {
|
|
76
|
+
return (serverId.startsWith('0x') ? serverId : `0x${serverId}`).toLowerCase();
|
|
77
|
+
}
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isConnected(connection) {
|
|
82
|
+
return connection && connection.socket && !connection.socket.destroyed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
class DiodeClientManager extends EventEmitter {
|
|
86
|
+
constructor(options = {}) {
|
|
87
|
+
super();
|
|
88
|
+
|
|
89
|
+
this.keyLocation = options.keyLocation || './db/keys.json';
|
|
90
|
+
this.defaultPort = Number.isFinite(options.port) && options.port > 0 ? options.port : 41046;
|
|
91
|
+
this.deviceCacheTtlMs = Number.isFinite(options.deviceCacheTtlMs)
|
|
92
|
+
? options.deviceCacheTtlMs
|
|
93
|
+
: 30000;
|
|
94
|
+
|
|
95
|
+
this.connections = [];
|
|
96
|
+
this.connectionByHost = new Map();
|
|
97
|
+
this.serverIdToConnection = new Map();
|
|
98
|
+
this.pendingConnections = new Map();
|
|
99
|
+
this.deviceRelayCache = new Map();
|
|
100
|
+
this._rpcByConnection = new Map();
|
|
101
|
+
this._rrIndex = 0;
|
|
102
|
+
|
|
103
|
+
this.initialHosts = this._buildInitialHosts(options);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_buildInitialHosts(options) {
|
|
107
|
+
if (typeof options.host === 'string' && options.host.trim()) {
|
|
108
|
+
const { host, port } = splitHostPort(options.host, this.defaultPort);
|
|
109
|
+
return host ? [joinHostPort(host, port)] : [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let hosts = [];
|
|
113
|
+
if (Array.isArray(options.hosts)) {
|
|
114
|
+
hosts = options.hosts;
|
|
115
|
+
} else if (typeof options.hosts === 'string') {
|
|
116
|
+
hosts = options.hosts.split(',').map((entry) => entry.trim());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (hosts.length === 0) {
|
|
120
|
+
hosts = DEFAULT_DIODE_ADDRS.slice();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
const normalized = [];
|
|
125
|
+
for (const entry of hosts) {
|
|
126
|
+
if (!entry) continue;
|
|
127
|
+
const { host, port } = splitHostPort(entry, this.defaultPort);
|
|
128
|
+
if (!host) continue;
|
|
129
|
+
const key = joinHostPort(host, port);
|
|
130
|
+
const lower = key.toLowerCase();
|
|
131
|
+
if (!seen.has(lower)) {
|
|
132
|
+
seen.add(lower);
|
|
133
|
+
normalized.push(key);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return normalized;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_getRpcFor(connection) {
|
|
140
|
+
if (!connection) return null;
|
|
141
|
+
let rpc = this._rpcByConnection.get(connection);
|
|
142
|
+
if (!rpc) {
|
|
143
|
+
rpc = connection.RPC || new DiodeRPC(connection);
|
|
144
|
+
this._rpcByConnection.set(connection, rpc);
|
|
145
|
+
}
|
|
146
|
+
return rpc;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_updateServerIdMapping(connection) {
|
|
150
|
+
if (!connection) return;
|
|
151
|
+
try {
|
|
152
|
+
const serverId = connection.getServerEthereumAddress(true);
|
|
153
|
+
if (serverId) {
|
|
154
|
+
this.serverIdToConnection.set(serverId.toLowerCase(), connection);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
logger.debug(() => `Failed to map server ID for ${connection.host}:${connection.port}: ${error}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
_localAddressHintFor(connection) {
|
|
162
|
+
const connected = this._connectedConnections();
|
|
163
|
+
if (connected.length === 0) {
|
|
164
|
+
return Buffer.alloc(0);
|
|
165
|
+
}
|
|
166
|
+
const primary = connected[0];
|
|
167
|
+
const secondary = connected.length > 1 ? connected[1] : null;
|
|
168
|
+
|
|
169
|
+
let primaryId = '';
|
|
170
|
+
try {
|
|
171
|
+
primaryId = normalizeServerIdHex(primary.getServerEthereumAddress(true));
|
|
172
|
+
} catch (_) {
|
|
173
|
+
primaryId = '';
|
|
174
|
+
}
|
|
175
|
+
if (!primaryId) {
|
|
176
|
+
return Buffer.alloc(0);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (primary === connection) {
|
|
180
|
+
if (!secondary) return Buffer.alloc(0);
|
|
181
|
+
let secondaryId = '';
|
|
182
|
+
try {
|
|
183
|
+
secondaryId = normalizeServerIdHex(secondary.getServerEthereumAddress(true));
|
|
184
|
+
} catch (_) {
|
|
185
|
+
secondaryId = '';
|
|
186
|
+
}
|
|
187
|
+
if (!secondaryId) return Buffer.alloc(0);
|
|
188
|
+
return Buffer.concat([
|
|
189
|
+
Buffer.from([1]),
|
|
190
|
+
Buffer.from(secondaryId.slice(2), 'hex'),
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Buffer.concat([
|
|
195
|
+
Buffer.from([0]),
|
|
196
|
+
Buffer.from(primaryId.slice(2), 'hex'),
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_registerConnection(connection, hostKey) {
|
|
201
|
+
connection._managerHostKey = hostKey;
|
|
202
|
+
this.connections.push(connection);
|
|
203
|
+
this.connectionByHost.set(hostKey, connection);
|
|
204
|
+
if (typeof connection.setLocalAddressProvider === 'function') {
|
|
205
|
+
connection.setLocalAddressProvider(() => this._localAddressHintFor(connection));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
connection.on('unsolicited', (message) => {
|
|
209
|
+
this.emit('unsolicited', message, connection);
|
|
210
|
+
});
|
|
211
|
+
connection.on('reconnected', () => {
|
|
212
|
+
this._updateServerIdMapping(connection);
|
|
213
|
+
this.emit('reconnected', connection);
|
|
214
|
+
});
|
|
215
|
+
connection.on('reconnecting', (info) => {
|
|
216
|
+
this.emit('reconnecting', connection, info);
|
|
217
|
+
});
|
|
218
|
+
connection.on('reconnect_failed', () => {
|
|
219
|
+
this.emit('reconnect_failed', connection);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_unregisterConnection(connection, hostKey) {
|
|
224
|
+
if (!connection) return;
|
|
225
|
+
if (hostKey) {
|
|
226
|
+
this.connectionByHost.delete(hostKey);
|
|
227
|
+
}
|
|
228
|
+
this.connections = this.connections.filter((item) => item !== connection);
|
|
229
|
+
for (const [serverId, conn] of this.serverIdToConnection.entries()) {
|
|
230
|
+
if (conn === connection) {
|
|
231
|
+
this.serverIdToConnection.delete(serverId);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
this._rpcByConnection.delete(connection);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async _ensureConnection(hostEntry) {
|
|
238
|
+
const { host, port } = splitHostPort(hostEntry, this.defaultPort);
|
|
239
|
+
const hostKey = joinHostPort(host, port);
|
|
240
|
+
if (!host) {
|
|
241
|
+
throw new Error(`Invalid host entry: ${hostEntry}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (this.connectionByHost.has(hostKey)) {
|
|
245
|
+
return this.connectionByHost.get(hostKey);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (this.pendingConnections.has(hostKey)) {
|
|
249
|
+
return this.pendingConnections.get(hostKey);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const connection = new DiodeConnection(host, port, this.keyLocation);
|
|
253
|
+
this._registerConnection(connection, hostKey);
|
|
254
|
+
|
|
255
|
+
const promise = connection.connect()
|
|
256
|
+
.then(() => {
|
|
257
|
+
this._updateServerIdMapping(connection);
|
|
258
|
+
this.emit('connected', connection);
|
|
259
|
+
return connection;
|
|
260
|
+
})
|
|
261
|
+
.catch((error) => {
|
|
262
|
+
this._unregisterConnection(connection, hostKey);
|
|
263
|
+
throw error;
|
|
264
|
+
})
|
|
265
|
+
.finally(() => {
|
|
266
|
+
this.pendingConnections.delete(hostKey);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.pendingConnections.set(hostKey, promise);
|
|
270
|
+
return promise;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_connectedConnections() {
|
|
274
|
+
return this.connections.filter((connection) => isConnected(connection));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getNearestConnection() {
|
|
278
|
+
const connected = this._connectedConnections();
|
|
279
|
+
if (connected.length === 0) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
this._rrIndex = (this._rrIndex + 1) % connected.length;
|
|
283
|
+
return connected[this._rrIndex];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async connect() {
|
|
287
|
+
if (!this.initialHosts || this.initialHosts.length === 0) {
|
|
288
|
+
throw new Error('No Diode hosts configured');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const results = await Promise.allSettled(
|
|
292
|
+
this.initialHosts.map((host) => this._ensureConnection(host))
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const success = results.some((result) => result.status === 'fulfilled');
|
|
296
|
+
if (!success) {
|
|
297
|
+
const errorMessages = results
|
|
298
|
+
.filter((result) => result.status === 'rejected')
|
|
299
|
+
.map((result) => result.reason && result.reason.message ? result.reason.message : String(result.reason));
|
|
300
|
+
throw new Error(`Failed to connect to any Diode hosts. ${errorMessages.join('; ')}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async getConnectionForDevice(deviceId) {
|
|
307
|
+
const deviceIdBuffer = normalizeAddress(deviceId);
|
|
308
|
+
if (!deviceIdBuffer) {
|
|
309
|
+
throw new Error('Invalid device ID');
|
|
310
|
+
}
|
|
311
|
+
const deviceIdHex = deviceIdBuffer.toString('hex');
|
|
312
|
+
|
|
313
|
+
if (this.deviceCacheTtlMs > 0) {
|
|
314
|
+
const cached = this.deviceRelayCache.get(deviceIdHex);
|
|
315
|
+
if (cached && Date.now() - cached.ts < this.deviceCacheTtlMs) {
|
|
316
|
+
const cachedConn = this.serverIdToConnection.get(cached.serverIdHex) ||
|
|
317
|
+
this.connectionByHost.get(cached.hostKey);
|
|
318
|
+
if (cachedConn && isConnected(cachedConn)) {
|
|
319
|
+
return cachedConn;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const primary = this.getNearestConnection();
|
|
325
|
+
if (!primary) {
|
|
326
|
+
throw new Error('No connected relay available');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let ticket = null;
|
|
330
|
+
try {
|
|
331
|
+
ticket = await this._getRpcFor(primary).getObject(deviceIdBuffer);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
logger.warn(() => `Failed to resolve device ticket: ${error}`);
|
|
334
|
+
return primary;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const serverIdHex = normalizeServerIdHex(ticket && (ticket.serverIdHex || ticket.serverId));
|
|
338
|
+
if (!serverIdHex) {
|
|
339
|
+
return primary;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const existing = this.serverIdToConnection.get(serverIdHex);
|
|
343
|
+
if (existing && isConnected(existing)) {
|
|
344
|
+
this.deviceRelayCache.set(deviceIdHex, {
|
|
345
|
+
serverIdHex,
|
|
346
|
+
hostKey: existing._managerHostKey || '',
|
|
347
|
+
ts: Date.now(),
|
|
348
|
+
});
|
|
349
|
+
return existing;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let nodeInfo = null;
|
|
353
|
+
try {
|
|
354
|
+
const nodeId = Buffer.from(serverIdHex.slice(2), 'hex');
|
|
355
|
+
nodeInfo = await this._getRpcFor(primary).getNode(nodeId);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
logger.warn(() => `Failed to resolve relay node for ${serverIdHex}: ${error}`);
|
|
358
|
+
return primary;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!nodeInfo || !nodeInfo.host) {
|
|
362
|
+
return primary;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const relayPort = nodeInfo.edgePort || nodeInfo.serverPort;
|
|
366
|
+
if (!relayPort) {
|
|
367
|
+
return primary;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const hostKey = joinHostPort(nodeInfo.host, relayPort);
|
|
371
|
+
let relayConnection;
|
|
372
|
+
try {
|
|
373
|
+
relayConnection = await this._ensureConnection(hostKey);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
logger.warn(() => `Failed to connect to relay ${hostKey}: ${error}`);
|
|
376
|
+
return primary;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.deviceRelayCache.set(deviceIdHex, {
|
|
380
|
+
serverIdHex,
|
|
381
|
+
hostKey,
|
|
382
|
+
ts: Date.now(),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return relayConnection || primary;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async resolveRelayForDevice(deviceId) {
|
|
389
|
+
const deviceIdBuffer = normalizeAddress(deviceId);
|
|
390
|
+
if (!deviceIdBuffer) {
|
|
391
|
+
throw new Error('Invalid device ID');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const primary = this.getNearestConnection();
|
|
395
|
+
if (!primary) {
|
|
396
|
+
throw new Error('No connected relay available');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const ticket = await this._getRpcFor(primary).getObject(deviceIdBuffer);
|
|
400
|
+
const serverIdHex = normalizeServerIdHex(ticket && (ticket.serverIdHex || ticket.serverId));
|
|
401
|
+
if (!serverIdHex) {
|
|
402
|
+
throw new Error('Device ticket missing server ID');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const nodeId = Buffer.from(serverIdHex.slice(2), 'hex');
|
|
406
|
+
const nodeInfo = await this._getRpcFor(primary).getNode(nodeId);
|
|
407
|
+
if (!nodeInfo || !nodeInfo.host) {
|
|
408
|
+
throw new Error('Relay node info missing host');
|
|
409
|
+
}
|
|
410
|
+
const relayPort = nodeInfo.edgePort || nodeInfo.serverPort;
|
|
411
|
+
if (!relayPort) {
|
|
412
|
+
throw new Error('Relay node info missing port');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
serverId: serverIdHex,
|
|
417
|
+
host: nodeInfo.host,
|
|
418
|
+
port: relayPort,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
getConnections() {
|
|
423
|
+
return this.connections.slice();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
close() {
|
|
427
|
+
for (const connection of this.connections) {
|
|
428
|
+
try {
|
|
429
|
+
connection.close();
|
|
430
|
+
} catch (_) {}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
module.exports = DiodeClientManager;
|