@hive-p2p/server 1.0.21 → 1.0.23
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/core/node-services.mjs +25 -13
- package/core/node.mjs +6 -7
- package/core/topologist.mjs +53 -27
- package/package.json +1 -1
package/core/node-services.mjs
CHANGED
|
@@ -59,18 +59,30 @@ export class NodeServices {
|
|
|
59
59
|
ws.on('message', (data) => { // When peer proves his id, we can handle data normally
|
|
60
60
|
if (remoteId) for (const cb of this.peerStore.callbacks.data) cb(remoteId, data);
|
|
61
61
|
else { // FIRST MESSAGE SHOULD BE HANDSHAKE WITH ID
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
try {
|
|
63
|
+
const d = new Uint8Array(data); if (d[0] > 127) return; // not unicast, ignore
|
|
64
|
+
const message = this.cryptoCodex.readUnicastMessage(d);
|
|
65
|
+
if (!message) return; // invalid unicast message, ignore
|
|
66
|
+
|
|
67
|
+
const { route, type, neighborsList } = message;
|
|
68
|
+
if (type !== 'handshake' || route.length !== 2) return;
|
|
69
|
+
|
|
70
|
+
const { signatureStart, pubkey, signature } = message;
|
|
71
|
+
const signedData = d.subarray(0, signatureStart);
|
|
72
|
+
if (!this.cryptoCodex.verifySignature(pubkey, signature, signedData)) return;
|
|
73
|
+
|
|
74
|
+
remoteId = route[0];
|
|
75
|
+
this.peerStore.digestPeerNeighbors(remoteId, neighborsList); // Update known store
|
|
76
|
+
this.peerStore.connecting[remoteId]?.out?.close(); // close outgoing connection if any
|
|
77
|
+
if (!this.peerStore.connecting[remoteId]) this.peerStore.connecting[remoteId] = {};
|
|
78
|
+
this.peerStore.connecting[remoteId].in = new PeerConnection(remoteId, ws, 'in', true);
|
|
79
|
+
for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'in');
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(`Error handling WebSocket message on Node #${this.id}:`, error);
|
|
82
|
+
}
|
|
72
83
|
}
|
|
73
84
|
});
|
|
85
|
+
ws.send(this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList));
|
|
74
86
|
});
|
|
75
87
|
}
|
|
76
88
|
#startSTUNServer(host = 'localhost', port = SERVICE.PORT + 1) {
|
|
@@ -108,13 +120,13 @@ export class NodeServices {
|
|
|
108
120
|
if (this.verbose > 2) console.log(`%cSTUN Response: client will discover IP ${rinfo.address}:${rinfo.port}`, 'color: green;');
|
|
109
121
|
return response;
|
|
110
122
|
}
|
|
111
|
-
/** @param {
|
|
123
|
+
/** @param {string[]} bootstraps */
|
|
112
124
|
static deriveSTUNServers(bootstraps, includesCentralized = false) {
|
|
113
125
|
/** @type {Array<{urls: string}>} */
|
|
114
126
|
const stunUrls = [];
|
|
115
127
|
for (const b of bootstraps) {
|
|
116
|
-
const domain = b.
|
|
117
|
-
const port = parseInt(b.
|
|
128
|
+
const domain = b.split(':')[1].replace('//', '');
|
|
129
|
+
const port = parseInt(b.split(':')[2]) + 1;
|
|
118
130
|
stunUrls.push({ urls: `stun:${domain}:${port}` });
|
|
119
131
|
}
|
|
120
132
|
if (!includesCentralized) return stunUrls;
|
package/core/node.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { NodeServices } from './node-services.mjs';
|
|
|
10
10
|
|
|
11
11
|
/** Create and start a new PublicNode instance.
|
|
12
12
|
* @param {Object} options
|
|
13
|
-
* @param {
|
|
13
|
+
* @param {string[]} options.bootstraps List of bootstrap nodes used as P2P network entry
|
|
14
14
|
* @param {boolean} [options.autoStart] If true, the node will automatically start after creation (default: true)
|
|
15
15
|
* @param {CryptoCodex} [options.cryptoCodex] Identity of the node; if not provided, a new one will be generated
|
|
16
16
|
* @param {string} [options.domain] If provided, the node will operate as a public node and start necessary services (e.g., WebSocket server)
|
|
@@ -34,7 +34,7 @@ export async function createPublicNode(options) {
|
|
|
34
34
|
|
|
35
35
|
/** Create and start a new Node instance.
|
|
36
36
|
* @param {Object} options
|
|
37
|
-
* @param {
|
|
37
|
+
* @param {string[]} options.bootstraps List of bootstrap nodes used as P2P network entry
|
|
38
38
|
* @param {CryptoCodex} [options.cryptoCodex] Identity of the node; if not provided, a new one will be generated
|
|
39
39
|
* @param {boolean} [options.autoStart] If true, the node will automatically start after creation (default: true)
|
|
40
40
|
* @param {number} [options.verbose] Verbosity level for logging (default: NODE.DEFAULT_VERBOSE) */
|
|
@@ -60,7 +60,7 @@ export class Node {
|
|
|
60
60
|
|
|
61
61
|
/** Initialize a new P2P node instance, use .start() to init topologist
|
|
62
62
|
* @param {CryptoCodex} cryptoCodex - Identity of the node.
|
|
63
|
-
* @param {
|
|
63
|
+
* @param {string[]} bootstraps List of bootstrap nodes used as P2P network entry */
|
|
64
64
|
constructor(cryptoCodex, bootstraps = [], verbose = NODE.DEFAULT_VERBOSE) {
|
|
65
65
|
this.verbose = verbose;
|
|
66
66
|
if (this.topologist?.services) this.topologist.services.verbose = verbose;
|
|
@@ -72,7 +72,7 @@ export class Node {
|
|
|
72
72
|
this.peerStore = new PeerStore(this.id, this.cryptoCodex, this.offerManager, this.arbiter, verbose);
|
|
73
73
|
this.messager = new UnicastMessager(this.id, this.cryptoCodex, this.arbiter, this.peerStore, verbose);
|
|
74
74
|
this.gossip = new Gossip(this.id, this.cryptoCodex, this.arbiter, this.peerStore, verbose);
|
|
75
|
-
this.topologist = new Topologist(this.id, this.gossip, this.messager, this.peerStore, bootstraps || []);
|
|
75
|
+
this.topologist = new Topologist(this.id, this.cryptoCodex, this.gossip, this.messager, this.peerStore, bootstraps || []);
|
|
76
76
|
const { arbiter, peerStore, messager, gossip, topologist } = this;
|
|
77
77
|
|
|
78
78
|
// SETUP TRANSPORTS LISTENERS
|
|
@@ -97,8 +97,8 @@ export class Node {
|
|
|
97
97
|
const remoteIsPublic = this.cryptoCodex.isPublicNode(peerId);
|
|
98
98
|
if (this.publicUrl) return; // public node do not need to do anything special on connect
|
|
99
99
|
if (this.verbose > ((this.publicUrl || remoteIsPublic) ? 3 : 2)) console.log(`(${this.id}) ${direction === 'in' ? 'Incoming' : 'Outgoing'} connection established with peer ${peerId}`);
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
100
|
+
const bothAreNotPublic = !remoteIsPublic && !this.cryptoCodex.isPublicNode(this.id);
|
|
101
|
+
if (bothAreNotPublic || direction === 'in') this.sendMessage(peerId, this.id, 'handshake'); // send it in both case, no doubt...
|
|
102
102
|
|
|
103
103
|
const isHoverNeighbored = this.peerStore.neighborsList.length >= DISCOVERY.TARGET_NEIGHBORS_COUNT + this.halfTarget;
|
|
104
104
|
const dispatchEvents = () => {
|
|
@@ -136,7 +136,6 @@ export class Node {
|
|
|
136
136
|
|
|
137
137
|
// PUBLIC API
|
|
138
138
|
get publicUrl() { return this.services?.publicUrl; }
|
|
139
|
-
get publicIdentity() { return { id: this.id, publicUrl: this.publicUrl }; }
|
|
140
139
|
|
|
141
140
|
onMessageData(callback) { this.messager.on('message', callback); }
|
|
142
141
|
onGossipData(callback) { this.gossip.on('gossip', callback); }
|
package/core/topologist.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { CLOCK, SIMULATION, TRANSPORTS, NODE, DISCOVERY, GOSSIP } from './config.mjs';
|
|
2
2
|
import { PeerConnection } from './peer-store.mjs';
|
|
3
|
-
import { CryptoCodex } from './crypto-codex.mjs';
|
|
4
3
|
const { SANDBOX, ICE_CANDIDATE_EMITTER, TEST_WS_EVENT_MANAGER } = SIMULATION.ENABLED ? await import('../simulation/test-transports.mjs') : {};
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -47,10 +46,11 @@ class OfferQueue {
|
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export class Topologist {
|
|
50
|
-
id; gossip; messager; peerStore; bootstraps;
|
|
49
|
+
id; cryptoCodex; gossip; messager; peerStore; bootstraps;
|
|
51
50
|
halfTarget = Math.ceil(DISCOVERY.TARGET_NEIGHBORS_COUNT / 2);
|
|
52
51
|
twiceTarget = DISCOVERY.TARGET_NEIGHBORS_COUNT * 2;
|
|
53
|
-
/** @type {
|
|
52
|
+
/** @type {Map<string, boolean>} */ bootstrapsConnectionState = new Map();
|
|
53
|
+
|
|
54
54
|
get isPublicNode() { return this.services?.publicUrl ? true : false; }
|
|
55
55
|
/** @type {import('./node-services.mjs').NodeServices | undefined} */ services;
|
|
56
56
|
|
|
@@ -59,12 +59,12 @@ export class Topologist {
|
|
|
59
59
|
offersQueue = new OfferQueue();
|
|
60
60
|
maxBonus = NODE.CONNECTION_UPGRADE_TIMEOUT * .2; // 20% of 15sec: 3sec max
|
|
61
61
|
|
|
62
|
-
/** @param {string} selfId @param {import('./gossip.mjs').Gossip} gossip @param {import('./unicast.mjs').UnicastMessager} messager @param {import('./peer-store.mjs').PeerStore} peerStore @param {
|
|
63
|
-
constructor(selfId, gossip, messager, peerStore, bootstraps) {
|
|
64
|
-
this.id = selfId; this.gossip = gossip; this.messager = messager; this.peerStore = peerStore;
|
|
65
|
-
for (const
|
|
62
|
+
/** @param {string} selfId @param {import('./crypto-codex.mjs').CryptoCodex} cryptoCodex @param {import('./gossip.mjs').Gossip} gossip @param {import('./unicast.mjs').UnicastMessager} messager @param {import('./peer-store.mjs').PeerStore} peerStore @param {string[]} bootstraps */
|
|
63
|
+
constructor(selfId, cryptoCodex, gossip, messager, peerStore, bootstraps) {
|
|
64
|
+
this.id = selfId; this.cryptoCodex = cryptoCodex; this.gossip = gossip; this.messager = messager; this.peerStore = peerStore;
|
|
65
|
+
for (const url of bootstraps) this.bootstrapsConnectionState.set(url, false);
|
|
66
66
|
this.bootstraps = [...bootstraps].sort(() => Math.random() - 0.5); // shuffle
|
|
67
|
-
this.nextBootstrapIndex = Math.random() * this.
|
|
67
|
+
this.nextBootstrapIndex = Math.random() * this.bootstraps.length | 0;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// PUBLIC METHODS
|
|
@@ -109,21 +109,19 @@ export class Topologist {
|
|
|
109
109
|
this.offersQueue.pushSortTrim(offerItem);
|
|
110
110
|
}
|
|
111
111
|
tryConnectNextBootstrap(neighborsCount = 0, nonPublicNeighborsCount = 0) {
|
|
112
|
-
if (this.
|
|
112
|
+
if (this.bootstraps.length === 0) return;
|
|
113
113
|
const publicConnectedCount = neighborsCount - nonPublicNeighborsCount;
|
|
114
|
-
let
|
|
115
|
-
for (const id in this.peerStore.connecting)
|
|
116
|
-
if (this.bootstrapsIds.has(id)) publicConnectingCount++;
|
|
117
|
-
else connectingCount++;
|
|
114
|
+
let connectingCount = 0;
|
|
115
|
+
for (const id in this.peerStore.connecting) connectingCount++;
|
|
118
116
|
|
|
119
117
|
// MINIMIZE BOOTSTRAP CONNECTIONS DEPENDING ON HOW MANY NEIGHBORS WE HAVE
|
|
120
|
-
if (publicConnectedCount
|
|
118
|
+
if (publicConnectedCount >= this.halfTarget) return; // already connected to enough bootstraps
|
|
121
119
|
if (neighborsCount >= DISCOVERY.TARGET_NEIGHBORS_COUNT) return; // no more bootstrap needed
|
|
122
120
|
if (connectingCount + nonPublicNeighborsCount > this.twiceTarget) return; // no more bootstrap needed
|
|
123
121
|
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
this.#connectToPublicNode(
|
|
122
|
+
const publicUrl = this.bootstraps[this.nextBootstrapIndex++ % this.bootstraps.length];
|
|
123
|
+
if (this.bootstrapsConnectionState.get(publicUrl)) return; // already connecting/connected
|
|
124
|
+
this.#connectToPublicNode(publicUrl);
|
|
127
125
|
}
|
|
128
126
|
|
|
129
127
|
// INTERNAL METHODS
|
|
@@ -140,25 +138,53 @@ export class Topologist {
|
|
|
140
138
|
#getOverlap(peerId1 = 'toto') {
|
|
141
139
|
const p1n = this.peerStore.known[peerId1]?.neighbors || {};
|
|
142
140
|
const result = { overlap: 0, nonPublicCount: 0, p1nCount: this.peerStore.getUpdatedPeerConnectionsCount(peerId1) };
|
|
143
|
-
for (const id in p1n) if (!
|
|
141
|
+
for (const id in p1n) if (!this.cryptoCodex.isPublicNode(id)) result.nonPublicCount++;
|
|
144
142
|
for (const id of this.peerStore.standardNeighborsList) if (p1n[id]) result.overlap++;
|
|
145
143
|
return result;
|
|
146
144
|
}
|
|
147
145
|
/** Get overlap information for multiple peers @param {string[]} peerIds */
|
|
148
146
|
#getOverlaps(peerIds = []) { return peerIds.map(id => ({ id, ...this.#getOverlap(id) })); }
|
|
149
|
-
#connectToPublicNode(
|
|
150
|
-
|
|
147
|
+
#connectToPublicNode(publicUrl = 'localhost:8080') {
|
|
148
|
+
let remoteId = null;
|
|
151
149
|
const ws = new TRANSPORTS.WS_CLIENT(publicUrl); ws.binaryType = 'arraybuffer';
|
|
152
|
-
if (!this.peerStore.connecting[remoteId]) this.peerStore.connecting[remoteId] = {};
|
|
153
|
-
this.peerStore.connecting[remoteId].out = new PeerConnection(remoteId, ws, 'out', true);
|
|
154
150
|
ws.onerror = (error) => console.error(`WebSocket error:`, error.stack);
|
|
155
151
|
ws.onopen = () => {
|
|
156
|
-
|
|
157
|
-
ws.
|
|
158
|
-
|
|
159
|
-
|
|
152
|
+
this.bootstrapsConnectionState.set(publicUrl, true);
|
|
153
|
+
ws.onclose = () => {
|
|
154
|
+
this.bootstrapsConnectionState.set(publicUrl, false);
|
|
155
|
+
for (const cb of this.peerStore.callbacks.disconnect) cb(remoteId, 'out');
|
|
156
|
+
}
|
|
157
|
+
ws.onmessage = (data) => {
|
|
158
|
+
if (remoteId) for (const cb of this.peerStore.callbacks.data) cb(remoteId, data.data);
|
|
159
|
+
else { // FIRST MESSAGE SHOULD BE HANDSHAKE WITH ID
|
|
160
|
+
try {
|
|
161
|
+
const d = new Uint8Array(data.data); if (d[0] > 127) return; // not unicast, ignore
|
|
162
|
+
const message = this.cryptoCodex.readUnicastMessage(d);
|
|
163
|
+
if (!message) return; // invalid unicast message, ignore
|
|
164
|
+
|
|
165
|
+
const { route, type, neighborsList } = message;
|
|
166
|
+
if (type !== 'handshake' || route.length !== 2) return;
|
|
167
|
+
|
|
168
|
+
const { signatureStart, pubkey, signature } = message;
|
|
169
|
+
const signedData = d.subarray(0, signatureStart);
|
|
170
|
+
if (!this.cryptoCodex.verifySignature(pubkey, signature, signedData)) return;
|
|
171
|
+
|
|
172
|
+
remoteId = route[0];
|
|
173
|
+
this.peerStore.digestPeerNeighbors(remoteId, neighborsList); // Update known store
|
|
174
|
+
this.peerStore.connecting[remoteId]?.in?.close(); // close incoming connection if any
|
|
175
|
+
if (!this.peerStore.connecting[remoteId]) this.peerStore.connecting[remoteId] = {};
|
|
176
|
+
this.peerStore.connecting[remoteId].out = new PeerConnection(remoteId, ws, 'out', true);
|
|
177
|
+
for (const cb of this.peerStore.callbacks.connect) cb(remoteId, 'out');
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`Error handling WebSocket message on Node #${this.id}:`, error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
ws.send(this.cryptoCodex.createUnicastMessage('handshake', null, [this.id, this.id], this.peerStore.neighborsList));
|
|
184
|
+
};
|
|
160
185
|
}
|
|
161
186
|
#tryToSpreadSDP(nonPublicNeighborsCount = 0, isHalfReached = false) { // LOOP TO SELECT ONE UNSEND READY OFFER AND BROADCAST IT
|
|
187
|
+
if (!this.peerStore.neighborsList.length) return; // no neighbors, no need to spread offers
|
|
162
188
|
// LIMIT OFFER SPREADING IF WE ARE CONNECTING TO MANY PEERS, LOWER GOSSIP TRAFFIC
|
|
163
189
|
const connectingCount = Object.keys(this.peerStore.connecting).length;
|
|
164
190
|
const ingPlusEd = connectingCount + nonPublicNeighborsCount;
|
|
@@ -198,7 +224,7 @@ export class Topologist {
|
|
|
198
224
|
for (const id in this.peerStore.known) {
|
|
199
225
|
if (Math.random() > r) continue; // randomize a bit the search
|
|
200
226
|
if (--maxSearch <= 0) break;
|
|
201
|
-
if (id === this.id ||
|
|
227
|
+
if (id === this.id || this.cryptoCodex.isPublicNode(id) || this.peerStore.isKicked(id)) continue;
|
|
202
228
|
else if (this.peerStore.connected[id] || this.peerStore.connecting[id]) continue;
|
|
203
229
|
|
|
204
230
|
const { overlap, nonPublicCount } = this.#getOverlap(id);
|