tsc-p2pnet 1.0.1

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.
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const argv = require('minimist')(process.argv.slice(2));
6
+ const Node = require('../src/node');
7
+ const crypto = require('crypto');
8
+
9
+ const command = argv._[0] || 'help';
10
+
11
+ function showBanner() {
12
+ console.log('');
13
+ console.log(' ╔═══════════════════════════════════════════════════════════════╗');
14
+ console.log(' ║ T S C - P 2 P N E T ║');
15
+ console.log(' ║ Decentralized P2P Botnet Network v1.0.1 ║');
16
+ console.log(' ║ Kademlia DHT · gossip protocol · command & control ║');
17
+ console.log(' ╚═══════════════════════════════════════════════════════════════╝');
18
+ console.log('');
19
+ }
20
+
21
+ function help() {
22
+ showBanner();
23
+ console.log(`
24
+ USAGE:
25
+ tsc-p2pnet node --port <port> --id <identity> --bootstrap <host:port>
26
+ Start a P2P node
27
+
28
+ tsc-p2pnet send --key <key> --value <value> --bootstrap <host:port>
29
+ Gossip a command to the network
30
+
31
+ tsc-p2pnet status --bootstrap <host:port>
32
+ Query network status
33
+
34
+ tsc-p2pnet store --key <key> --value <value> --bootstrap <host:port>
35
+ Store a value in the DHT
36
+
37
+ tsc-p2pnet lookup --key <key> --bootstrap <host:port>
38
+ Look up a value in the DHT
39
+
40
+ tsc-p2pnet help
41
+ Show this help
42
+ `);
43
+ }
44
+
45
+ async function cmdNode() {
46
+ showBanner();
47
+ const port = parseInt(argv.port, 10) || 0;
48
+ const identity = argv.id || `node-${crypto.randomBytes(4).toString('hex')}`;
49
+ const bootstrap = argv.bootstrap ? argv.bootstrap.split(',').map(s => s.trim()) : [];
50
+
51
+ const node = new Node({ identity, port });
52
+ await node.start();
53
+
54
+ console.log(`Node started: ${node.nodeId.toString('hex')}`);
55
+ console.log(`Identity: ${identity}`);
56
+ console.log(`Port: ${node.port}`);
57
+ console.log(`Bootstrap: ${bootstrap.join(', ') || 'none'}`);
58
+
59
+ if (bootstrap.length > 0) {
60
+ console.log('Bootstrapping...');
61
+ await node.join(bootstrap);
62
+ console.log(`Routing table has ${node.routingTable.size()} peers`);
63
+ }
64
+
65
+ const allPeers = node.routingTable.getAll();
66
+ for (const p of allPeers.slice(0, 10)) {
67
+ console.log(` peer ${p.nodeId.toString('hex').substring(0, 16)}... @ ${p.address}:${p.port}`);
68
+ }
69
+
70
+ console.log('Listening... (Ctrl+C to stop)');
71
+
72
+ process.on('SIGINT', async () => {
73
+ console.log('\nShutting down...');
74
+ await node.stop();
75
+ process.exit(0);
76
+ });
77
+ }
78
+
79
+ async function cmdSend() {
80
+ showBanner();
81
+ const key = argv.key;
82
+ const value = argv.value;
83
+ const bootstrap = parseBootstrap(argv.bootstrap);
84
+
85
+ if (!key || !value) {
86
+ console.error('Error: --key and --value are required');
87
+ process.exit(1);
88
+ }
89
+
90
+ const node = new Node({ identity: `client-${crypto.randomBytes(4).toString('hex')}` });
91
+ await node.start();
92
+
93
+ if (bootstrap.length > 0) {
94
+ await node.join(bootstrap);
95
+ }
96
+
97
+ console.log(`Gossiping: ${key} = ${value}`);
98
+ await node.gossip(key, value);
99
+ console.log('Message propagated. Waiting for network to distribute...');
100
+
101
+ await delay(2000);
102
+ await node.stop();
103
+ console.log('Done.');
104
+ }
105
+
106
+ async function cmdStatus() {
107
+ showBanner();
108
+ const bootstrap = parseBootstrap(argv.bootstrap);
109
+
110
+ const node = new Node({ identity: `client-${crypto.randomBytes(4).toString('hex')}` });
111
+ await node.start();
112
+
113
+ if (bootstrap.length > 0) {
114
+ await node.join(bootstrap);
115
+ }
116
+
117
+ console.log(`Local Node ID: ${node.nodeId.toString('hex').substring(0, 16)}...`);
118
+ console.log(`Local Port: ${node.port}`);
119
+ console.log(`Known peers: ${node.routingTable.size()}`);
120
+
121
+ const allPeers = node.routingTable.getAll();
122
+ if (allPeers.length > 0) {
123
+ console.log('\nPeers in routing table:');
124
+ for (const p of allPeers) {
125
+ console.log(` ${p.nodeId.toString('hex').substring(0, 16)}... @ ${p.address}:${p.port} (seen ${new Date(p.lastSeen).toISOString()})`);
126
+ }
127
+ } else {
128
+ console.log('No peers discovered. Is the network reachable?');
129
+ }
130
+
131
+ await node.stop();
132
+ }
133
+
134
+ async function cmdStore() {
135
+ showBanner();
136
+ const key = argv.key;
137
+ const value = argv.value;
138
+ const bootstrap = parseBootstrap(argv.bootstrap);
139
+
140
+ if (!key || !value) {
141
+ console.error('Error: --key and --value are required');
142
+ process.exit(1);
143
+ }
144
+
145
+ const node = new Node({ identity: `client-${crypto.randomBytes(4).toString('hex')}` });
146
+ await node.start();
147
+
148
+ if (bootstrap.length > 0) {
149
+ await node.join(bootstrap);
150
+ }
151
+
152
+ console.log(`Storing: ${key} = ${value}`);
153
+ const results = await node.store(key, value);
154
+
155
+ if (results.length === 0) {
156
+ console.log('No peers to store on. Ensure --bootstrap connects you to the network.');
157
+ } else {
158
+ for (const r of results) {
159
+ console.log(` ${r.node}: ${r.success ? 'OK' : 'FAIL: ' + r.error}`);
160
+ }
161
+ }
162
+
163
+ await node.stop();
164
+ }
165
+
166
+ async function cmdLookup() {
167
+ showBanner();
168
+ const key = argv.key;
169
+ const bootstrap = parseBootstrap(argv.bootstrap);
170
+
171
+ if (!key) {
172
+ console.error('Error: --key is required');
173
+ process.exit(1);
174
+ }
175
+
176
+ const node = new Node({ identity: `client-${crypto.randomBytes(4).toString('hex')}` });
177
+ await node.start();
178
+
179
+ if (bootstrap.length > 0) {
180
+ await node.join(bootstrap);
181
+ }
182
+
183
+ console.log(`Looking up: ${key}`);
184
+ const result = await node.findValue(key);
185
+
186
+ if (result.found) {
187
+ console.log(`Found: ${result.value}`);
188
+ console.log(`Source: ${result.source}`);
189
+ } else {
190
+ console.log('Value not found in DHT');
191
+ }
192
+
193
+ await node.stop();
194
+ }
195
+
196
+ function parseBootstrap(bootstrapStr) {
197
+ if (!bootstrapStr) return [];
198
+ return bootstrapStr.split(',').map(s => s.trim()).filter(Boolean);
199
+ }
200
+
201
+ function delay(ms) {
202
+ return new Promise(resolve => setTimeout(resolve, ms));
203
+ }
204
+
205
+ (async () => {
206
+ switch (command) {
207
+ case 'node':
208
+ await cmdNode();
209
+ break;
210
+ case 'send':
211
+ await cmdSend();
212
+ break;
213
+ case 'status':
214
+ await cmdStatus();
215
+ break;
216
+ case 'store':
217
+ await cmdStore();
218
+ break;
219
+ case 'lookup':
220
+ await cmdLookup();
221
+ break;
222
+ case 'help':
223
+ default:
224
+ help();
225
+ process.exit(0);
226
+ }
227
+ })().catch(err => {
228
+ console.error('Fatal error:', err.message);
229
+ process.exit(1);
230
+ });
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "tsc-p2pnet",
3
+ "version": "1.0.1",
4
+ "description": "Decentralized P2P botnet mesh — Kademlia DHT + gossip protocol",
5
+ "bin": {
6
+ "tsc-p2pnet": "bin/tsc-p2pnet.js"
7
+ },
8
+ "dependencies": {
9
+ "minimist": "1.2.8"
10
+ },
11
+ "author": "SURUJ404",
12
+ "license": "GPL-3.0"
13
+ }
package/src/dht.js ADDED
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const VALUE_TTL = 3600000;
6
+
7
+ class DHT {
8
+ constructor(node) {
9
+ this.node = node;
10
+ this.localStore = new Map();
11
+ this._republishTimer = null;
12
+ }
13
+
14
+ start() {
15
+ this._republishTimer = setInterval(() => this._republish(), 300000);
16
+ }
17
+
18
+ stop() {
19
+ if (this._republishTimer) {
20
+ clearInterval(this._republishTimer);
21
+ this._republishTimer = null;
22
+ }
23
+ }
24
+
25
+ async store(key, value) {
26
+ const keyHash = crypto.createHash('sha1').update(key).digest();
27
+ const keyHex = keyHash.toString('hex');
28
+
29
+ this.localStore.set(keyHex, { value, timestamp: Date.now() });
30
+
31
+ const closest = this.node.routingTable.getClosest(keyHash, this.node.k);
32
+ const results = [];
33
+
34
+ for (const entry of closest) {
35
+ try {
36
+ await this.node._sendRPC(entry.address, entry.port, 'STORE', {
37
+ key: keyHex,
38
+ value: value,
39
+ ttl: VALUE_TTL
40
+ }, 5, entry.nodeId);
41
+ results.push({ node: `${entry.address}:${entry.port}`, success: true });
42
+ } catch (e) {
43
+ results.push({ node: `${entry.address}:${entry.port}`, success: false, error: e.message });
44
+ }
45
+ }
46
+
47
+ return results;
48
+ }
49
+
50
+ async findValue(key) {
51
+ const keyHash = crypto.createHash('sha1').update(key).digest();
52
+ const keyHex = keyHash.toString('hex');
53
+
54
+ if (this.localStore.has(keyHex)) {
55
+ return { found: true, value: this.localStore.get(keyHex).value, source: 'local' };
56
+ }
57
+
58
+ const queried = new Set();
59
+ let closest = this.node.routingTable.getClosest(keyHash, this.node.alpha || 3);
60
+
61
+ while (closest.length > 0) {
62
+ for (const entry of closest) {
63
+ const addr = `${entry.address}:${entry.port}`;
64
+ if (queried.has(addr)) continue;
65
+ queried.add(addr);
66
+
67
+ try {
68
+ const resp = await this.node._sendRPC(entry.address, entry.port, 'FIND_VALUE', { key: keyHex }, 5, entry.nodeId);
69
+
70
+ if (resp.type === 'VALUE' && resp.payload.value !== undefined) {
71
+ return { found: true, value: resp.payload.value, source: addr };
72
+ }
73
+
74
+ if (resp.type === 'FOUND_NODES' && resp.payload.nodes) {
75
+ for (const n of resp.payload.nodes) {
76
+ const nId = Buffer.from(n.nodeId, 'hex');
77
+ if (!queried.has(`${n.address}:${n.port}`)) {
78
+ this.node.routingTable.update(nId, n.address, n.port);
79
+ }
80
+ }
81
+ }
82
+ } catch (e) {
83
+ }
84
+ }
85
+
86
+ closest = this.node.routingTable.getClosest(keyHash, this.node.alpha || 3)
87
+ .filter(e => !queried.has(`${e.address}:${e.port}`));
88
+
89
+ if (queried.size > 20) break;
90
+ }
91
+
92
+ return { found: false };
93
+ }
94
+
95
+ getLocal(key) {
96
+ const entry = this.localStore.get(key);
97
+ if (entry && Date.now() - entry.timestamp < VALUE_TTL) {
98
+ return entry.value;
99
+ }
100
+ if (entry) this.localStore.delete(key);
101
+ return null;
102
+ }
103
+
104
+ setLocal(key, value) {
105
+ this.localStore.set(key, { value, timestamp: Date.now() });
106
+ }
107
+
108
+ _republish() {
109
+ const now = Date.now();
110
+ for (const [key, entry] of this.localStore) {
111
+ if (now - entry.timestamp > VALUE_TTL) {
112
+ this.localStore.delete(key);
113
+ } else {
114
+ const keyBuf = Buffer.from(key, 'hex');
115
+ const closest = this.node.routingTable.getClosest(keyBuf, this.node.k);
116
+ for (const c of closest) {
117
+ this.node._sendRPC(c.address, c.port, 'STORE', { key, value: entry.value, ttl: VALUE_TTL }, 5, c.nodeId).catch(() => {});
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ module.exports = DHT;
package/src/gossip.js ADDED
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ class Gossip {
6
+ constructor(node) {
7
+ this.node = node;
8
+ this.seen = new Set();
9
+ this.alpha = 3;
10
+ this.defaultTtl = 5;
11
+ this.heartbeatInterval = 30000;
12
+ this._heartbeatTimer = null;
13
+ }
14
+
15
+ start() {
16
+ this._heartbeatTimer = setInterval(() => this._sendHeartbeat(), this.heartbeatInterval);
17
+ }
18
+
19
+ stop() {
20
+ if (this._heartbeatTimer) {
21
+ clearInterval(this._heartbeatTimer);
22
+ this._heartbeatTimer = null;
23
+ }
24
+ }
25
+
26
+ _messageId(msg) {
27
+ return `${msg.type}:${msg.requestId}`;
28
+ }
29
+
30
+ hasSeen(msg) {
31
+ return this.seen.has(this._messageId(msg));
32
+ }
33
+
34
+ markSeen(msg) {
35
+ this.seen.add(this._messageId(msg));
36
+ if (this.seen.size > 10000) {
37
+ const first = this.seen.values().next().value;
38
+ this.seen.delete(first);
39
+ }
40
+ }
41
+
42
+ async propagate(msg, excludeAddr = null) {
43
+ const msgId = this._messageId(msg);
44
+ if (this.seen.has(msgId)) return;
45
+ this.markSeen(msg);
46
+
47
+ if (msg.ttl <= 0) return;
48
+
49
+ const forwardMsg = { ...msg, ttl: msg.ttl - 1 };
50
+
51
+ const closest = this.node.routingTable.getClosest(
52
+ Buffer.from(msg.senderId, 'hex'),
53
+ this.alpha
54
+ );
55
+
56
+ for (const entry of closest) {
57
+ if (excludeAddr && `${entry.address}:${entry.port}` === excludeAddr) continue;
58
+ try {
59
+ await this.node._sendRaw(entry.address, entry.port, forwardMsg);
60
+ } catch (e) {
61
+ }
62
+ }
63
+ }
64
+
65
+ async broadcast(key, value, ttl = this.defaultTtl) {
66
+ const msg = {
67
+ type: 'GOSSIP',
68
+ senderId: this.node.nodeId.toString('hex'),
69
+ requestId: crypto.randomBytes(4).readUInt32BE(0),
70
+ senderAddr: null,
71
+ ttl,
72
+ payload: { key, value }
73
+ };
74
+ this.markSeen(msg);
75
+ await this.propagate(msg);
76
+ }
77
+
78
+ async _sendHeartbeat() {
79
+ const msg = {
80
+ type: 'GOSSIP',
81
+ senderId: this.node.nodeId.toString('hex'),
82
+ requestId: crypto.randomBytes(4).readUInt32BE(0),
83
+ senderAddr: null,
84
+ ttl: 3,
85
+ payload: {
86
+ type: 'heartbeat',
87
+ peerCount: this.node.routingTable.size()
88
+ }
89
+ };
90
+ this.markSeen(msg);
91
+ await this.propagate(msg);
92
+ }
93
+ }
94
+
95
+ module.exports = Gossip;
package/src/kbucket.js ADDED
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ class KBucket {
4
+ constructor(localNodeId, k = 20) {
5
+ this.localNodeId = localNodeId;
6
+ this.k = k;
7
+ this.buckets = [];
8
+ for (let i = 0; i < 160; i++) {
9
+ this.buckets.push([]);
10
+ }
11
+ }
12
+
13
+ _bucketIndex(targetId) {
14
+ const d = xorDistance(this.localNodeId, targetId);
15
+ for (let i = 0; i < d.length; i++) {
16
+ if (d[i] !== 0) {
17
+ return i * 8 + Math.clz32(d[i]) - 24;
18
+ }
19
+ }
20
+ return -1;
21
+ }
22
+
23
+ update(nodeId, address, port) {
24
+ if (nodeId.equals(this.localNodeId)) return;
25
+ const idx = this._bucketIndex(nodeId);
26
+ if (idx === -1) return;
27
+ const bucket = this.buckets[idx];
28
+ const now = Date.now();
29
+ const existingIdx = bucket.findIndex(e => e.nodeId.equals(nodeId));
30
+
31
+ if (existingIdx !== -1) {
32
+ const entry = bucket[existingIdx];
33
+ entry.address = address;
34
+ entry.port = port;
35
+ entry.lastSeen = now;
36
+ bucket.splice(existingIdx, 1);
37
+ bucket.push(entry);
38
+ } else if (bucket.length < this.k) {
39
+ bucket.push({ nodeId, address, port, lastSeen: now, latency: 0 });
40
+ } else {
41
+ if (now - bucket[0].lastSeen > 3600000) {
42
+ bucket.shift();
43
+ bucket.push({ nodeId, address, port, lastSeen: now, latency: 0 });
44
+ }
45
+ }
46
+ }
47
+
48
+ getClosest(targetId, count = this.k) {
49
+ const target = typeof targetId === 'string' ? Buffer.from(targetId, 'hex') : targetId;
50
+ const all = [];
51
+ for (let i = 0; i < this.buckets.length; i++) {
52
+ for (const entry of this.buckets[i]) {
53
+ all.push(entry);
54
+ }
55
+ }
56
+ all.sort((a, b) => {
57
+ const da = xorDistance(a.nodeId, target);
58
+ const db = xorDistance(b.nodeId, target);
59
+ return Buffer.compare(da, db);
60
+ });
61
+ return all.slice(0, count);
62
+ }
63
+
64
+ getAll() {
65
+ const all = [];
66
+ for (let i = 0; i < this.buckets.length; i++) {
67
+ for (const entry of this.buckets[i]) {
68
+ all.push(entry);
69
+ }
70
+ }
71
+ return all;
72
+ }
73
+
74
+ removeNode(nodeId) {
75
+ const idx = this._bucketIndex(nodeId);
76
+ if (idx === -1) return;
77
+ const bucket = this.buckets[idx];
78
+ const entryIdx = bucket.findIndex(e => e.nodeId.equals(nodeId));
79
+ if (entryIdx !== -1) {
80
+ bucket.splice(entryIdx, 1);
81
+ }
82
+ }
83
+
84
+ size() {
85
+ let total = 0;
86
+ for (const bucket of this.buckets) total += bucket.length;
87
+ return total;
88
+ }
89
+ }
90
+
91
+ function xorDistance(a, b) {
92
+ const len = Math.min(a.length, b.length);
93
+ const result = Buffer.alloc(len);
94
+ for (let i = 0; i < len; i++) result[i] = a[i] ^ b[i];
95
+ return result;
96
+ }
97
+
98
+ module.exports = KBucket;
package/src/node.js ADDED
@@ -0,0 +1,278 @@
1
+ 'use strict';
2
+
3
+ const dgram = require('dgram');
4
+ const crypto = require('crypto');
5
+ const KBucket = require('./kbucket');
6
+ const DHT = require('./dht');
7
+ const Gossip = require('./gossip');
8
+ const Protocol = require('./protocol');
9
+
10
+ class Node {
11
+ constructor(options = {}) {
12
+ this.identity = options.identity || `node-${crypto.randomBytes(4).toString('hex')}`;
13
+ this.port = options.port || 0;
14
+ this.k = options.k || 20;
15
+ this.alpha = options.alpha || 3;
16
+
17
+ this.nodeId = crypto.createHash('sha1').update(this.identity).digest();
18
+ this.routingTable = new KBucket(this.nodeId, this.k);
19
+ this.dht = new DHT(this);
20
+ this.gossip = new Gossip(this);
21
+ this.socket = dgram.createSocket('udp4');
22
+ this.socket.on('error', (err) => {
23
+ console.error(`[${this.identity}] Socket error: ${err.message}`);
24
+ });
25
+ this.pendingRequests = new Map();
26
+ this.running = false;
27
+
28
+ this._onMessage = this._onMessage.bind(this);
29
+ this._refreshTimer = null;
30
+ }
31
+
32
+ async start() {
33
+ return new Promise((resolve) => {
34
+ this.socket.on('message', this._onMessage);
35
+ this.socket.bind(this.port, '0.0.0.0', () => {
36
+ this.port = this.socket.address().port;
37
+ this.running = true;
38
+ this.dht.start();
39
+ this.gossip.start();
40
+ this._refreshTimer = setInterval(() => this._refresh(), 60000);
41
+ resolve();
42
+ });
43
+ });
44
+ }
45
+
46
+ async stop() {
47
+ this.running = false;
48
+ if (this._refreshTimer) {
49
+ clearInterval(this._refreshTimer);
50
+ this._refreshTimer = null;
51
+ }
52
+ this.dht.stop();
53
+ this.gossip.stop();
54
+ this.socket.close();
55
+ }
56
+
57
+ async join(bootstrapNodes) {
58
+ for (const addr of bootstrapNodes) {
59
+ const [host, portStr] = addr.split(':');
60
+ const port = parseInt(portStr, 10);
61
+ if (isNaN(port) || port < 1 || port > 65535) continue;
62
+ try {
63
+ const resp = await this._sendRPC(host, port, 'FIND_NODE', {
64
+ targetId: this.nodeId.toString('hex')
65
+ });
66
+ if (resp.type === 'FOUND_NODES' && resp.payload.nodes) {
67
+ for (const n of resp.payload.nodes) {
68
+ const nId = Buffer.from(n.nodeId, 'hex');
69
+ this.routingTable.update(nId, n.address, n.port);
70
+ }
71
+ }
72
+ } catch (e) {
73
+ }
74
+ }
75
+ }
76
+
77
+ async findNode(targetId) {
78
+ const targetBuf = Buffer.from(targetId, 'hex');
79
+ const queried = new Set();
80
+ let closest = this.routingTable.getClosest(targetBuf, this.alpha);
81
+
82
+ while (closest.length > 0) {
83
+ for (const entry of closest) {
84
+ const addr = `${entry.address}:${entry.port}`;
85
+ if (queried.has(addr)) continue;
86
+ queried.add(addr);
87
+
88
+ try {
89
+ const resp = await this._sendRPC(entry.address, entry.port, 'FIND_NODE', { targetId }, 5, entry.nodeId);
90
+ if (resp.type === 'FOUND_NODES' && resp.payload.nodes) {
91
+ for (const n of resp.payload.nodes) {
92
+ const nId = Buffer.from(n.nodeId, 'hex');
93
+ this.routingTable.update(nId, n.address, n.port);
94
+ }
95
+ }
96
+ } catch (e) {
97
+ }
98
+ }
99
+
100
+ closest = this.routingTable.getClosest(targetBuf, this.alpha)
101
+ .filter(e => !queried.has(`${e.address}:${e.port}`));
102
+
103
+ if (queried.size > 20) break;
104
+ }
105
+
106
+ return this.routingTable.getClosest(targetBuf, this.k);
107
+ }
108
+
109
+ async store(key, value) {
110
+ return this.dht.store(key, value);
111
+ }
112
+
113
+ async findValue(key) {
114
+ return this.dht.findValue(key);
115
+ }
116
+
117
+ async gossip(key, value, ttl = 5) {
118
+ return this.gossip.broadcast(key, value, ttl);
119
+ }
120
+
121
+ _sendRPC(host, port, type, payload, ttl = 5, nodeId = null) {
122
+ return new Promise((resolve, reject) => {
123
+ const requestId = crypto.randomBytes(4).readUInt32BE(0);
124
+ const msg = {
125
+ type,
126
+ senderId: this.nodeId.toString('hex'),
127
+ requestId,
128
+ ttl,
129
+ payload: payload || {}
130
+ };
131
+
132
+ const timer = setTimeout(() => {
133
+ this.pendingRequests.delete(requestId);
134
+ if (nodeId) this.routingTable.removeNode(nodeId);
135
+ reject(new Error(`RPC timeout: ${type} to ${host}:${port}`));
136
+ }, 10000);
137
+
138
+ this.pendingRequests.set(requestId, { resolve, reject, timer });
139
+
140
+ try {
141
+ const data = Protocol.encode(msg);
142
+ this.socket.send(data, 0, data.length, port, host, (err) => {
143
+ if (err) {
144
+ clearTimeout(timer);
145
+ this.pendingRequests.delete(requestId);
146
+ if (nodeId) this.routingTable.removeNode(nodeId);
147
+ reject(err);
148
+ }
149
+ });
150
+ } catch (e) {
151
+ clearTimeout(timer);
152
+ this.pendingRequests.delete(requestId);
153
+ if (nodeId) this.routingTable.removeNode(nodeId);
154
+ reject(e);
155
+ }
156
+ });
157
+ }
158
+
159
+ _sendRaw(host, port, msg) {
160
+ return new Promise((resolve, reject) => {
161
+ try {
162
+ const data = Protocol.encode(msg);
163
+ this.socket.send(data, 0, data.length, port, host, (err) => {
164
+ if (err) reject(err);
165
+ else resolve();
166
+ });
167
+ } catch (e) {
168
+ reject(e);
169
+ }
170
+ });
171
+ }
172
+
173
+ _onMessage(buf, rinfo) {
174
+ try {
175
+ const msg = Protocol.decode(buf);
176
+ const senderId = Buffer.from(msg.senderId, 'hex');
177
+
178
+ this.routingTable.update(senderId, rinfo.address, rinfo.port);
179
+
180
+ if (Protocol.isResponse(msg.type)) {
181
+ const pending = this.pendingRequests.get(msg.requestId);
182
+ if (pending) {
183
+ clearTimeout(pending.timer);
184
+ this.pendingRequests.delete(msg.requestId);
185
+ pending.resolve(msg);
186
+ }
187
+ } else {
188
+ this._handleRPC(msg, rinfo).catch(() => {});
189
+ }
190
+ } catch (e) {
191
+ }
192
+ }
193
+
194
+ async _handleRPC(msg, rinfo) {
195
+ switch (msg.type) {
196
+ case 'PING': {
197
+ const resp = Protocol.createResponse(msg);
198
+ resp.senderId = this.nodeId.toString('hex');
199
+ this._sendRaw(rinfo.address, rinfo.port, resp).catch(() => {});
200
+ break;
201
+ }
202
+
203
+ case 'FIND_NODE': {
204
+ const targetId = msg.payload.targetId;
205
+ const targetBuf = Buffer.from(targetId, 'hex');
206
+ const closest = this.routingTable.getClosest(targetBuf, this.k);
207
+ const nodes = closest.map(e => ({
208
+ nodeId: e.nodeId.toString('hex'),
209
+ address: e.address,
210
+ port: e.port
211
+ }));
212
+ const resp = Protocol.createResponse(msg, { nodes });
213
+ resp.senderId = this.nodeId.toString('hex');
214
+ this._sendRaw(rinfo.address, rinfo.port, resp).catch(() => {});
215
+ break;
216
+ }
217
+
218
+ case 'STORE': {
219
+ const { key, value } = msg.payload;
220
+ this.dht.setLocal(key, value);
221
+ const resp = Protocol.createResponse(msg, { stored: true });
222
+ resp.senderId = this.nodeId.toString('hex');
223
+ this._sendRaw(rinfo.address, rinfo.port, resp).catch(() => {});
224
+ break;
225
+ }
226
+
227
+ case 'FIND_VALUE': {
228
+ const key = msg.payload.key;
229
+ const localValue = this.dht.getLocal(key);
230
+ if (localValue !== null) {
231
+ const resp = Protocol.createResponse(msg, { value: localValue });
232
+ resp.senderId = this.nodeId.toString('hex');
233
+ this._sendRaw(rinfo.address, rinfo.port, resp).catch(() => {});
234
+ } else {
235
+ const keyBuf = Buffer.from(key, 'hex');
236
+ const closest = this.routingTable.getClosest(keyBuf, this.k);
237
+ const nodes = closest.map(e => ({
238
+ nodeId: e.nodeId.toString('hex'),
239
+ address: e.address,
240
+ port: e.port
241
+ }));
242
+ const resp = Protocol.createResponse(msg, { nodes });
243
+ resp.type = 'FOUND_NODES';
244
+ resp.senderId = this.nodeId.toString('hex');
245
+ this._sendRaw(rinfo.address, rinfo.port, resp).catch(() => {});
246
+ }
247
+ break;
248
+ }
249
+
250
+ case 'GOSSIP': {
251
+ const payload = msg.payload;
252
+ if (payload.type === 'heartbeat') {
253
+ } else if (payload.key && payload.key.startsWith('cmd:')) {
254
+ console.log(`[${this.identity}] GOT COMMAND: ${payload.key} = ${payload.value}`);
255
+ }
256
+
257
+ this.gossip.propagate(msg, `${rinfo.address}:${rinfo.port}`).catch(() => {});
258
+
259
+ const resp = Protocol.createResponse(msg, { received: true });
260
+ resp.senderId = this.nodeId.toString('hex');
261
+ this._sendRaw(rinfo.address, rinfo.port, resp).catch(() => {});
262
+ break;
263
+ }
264
+ }
265
+ }
266
+
267
+ _refresh() {
268
+ const allNodes = this.routingTable.getAll();
269
+ if (allNodes.length === 0) return;
270
+
271
+ for (let i = 0; i < 3; i++) {
272
+ const randomId = crypto.randomBytes(20).toString('hex');
273
+ this.findNode(randomId).catch(() => {});
274
+ }
275
+ }
276
+ }
277
+
278
+ module.exports = Node;
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ const REQUEST_TYPES = ['PING', 'FIND_NODE', 'STORE', 'FIND_VALUE', 'GOSSIP'];
6
+ const RESPONSE_TYPES = ['PONG', 'FOUND_NODES', 'STORED', 'VALUE', 'GOSSIP_ACK'];
7
+ const MAX_PACKET_SIZE = 1400;
8
+
9
+ class Protocol {
10
+ static createMessage(type, senderId, payload = {}, ttl = 5) {
11
+ return {
12
+ type,
13
+ senderId: senderId.toString('hex'),
14
+ requestId: crypto.randomBytes(4).readUInt32BE(0),
15
+ senderAddr: null,
16
+ ttl,
17
+ payload
18
+ };
19
+ }
20
+
21
+ static encode(msg) {
22
+ const str = JSON.stringify(msg);
23
+ const buf = Buffer.from(str, 'utf8');
24
+ if (buf.length > MAX_PACKET_SIZE) {
25
+ throw new Error(`Message too large: ${buf.length} bytes > ${MAX_PACKET_SIZE}`);
26
+ }
27
+ return buf;
28
+ }
29
+
30
+ static decode(buf) {
31
+ const str = buf.toString('utf8');
32
+ return JSON.parse(str);
33
+ }
34
+
35
+ static isRequest(type) {
36
+ return REQUEST_TYPES.includes(type);
37
+ }
38
+
39
+ static isResponse(type) {
40
+ return RESPONSE_TYPES.includes(type);
41
+ }
42
+
43
+ static responseTypeFor(requestType) {
44
+ const map = {
45
+ 'PING': 'PONG',
46
+ 'FIND_NODE': 'FOUND_NODES',
47
+ 'STORE': 'STORED',
48
+ 'FIND_VALUE': 'VALUE',
49
+ 'GOSSIP': 'GOSSIP_ACK'
50
+ };
51
+ return map[requestType];
52
+ }
53
+
54
+ static createResponse(request, additionalPayload = {}) {
55
+ return {
56
+ type: Protocol.responseTypeFor(request.type),
57
+ senderId: null,
58
+ requestId: request.requestId,
59
+ senderAddr: null,
60
+ ttl: request.ttl,
61
+ payload: { ...additionalPayload }
62
+ };
63
+ }
64
+ }
65
+
66
+ module.exports = Protocol;