the-invisible-billion-cli 1.0.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/LICENSE +21 -0
- package/README.md +371 -0
- package/bin/ib.js +90 -0
- package/package.json +28 -0
- package/src/cli/commands/file.js +20 -0
- package/src/cli/commands/scan.js +50 -0
- package/src/cli/commands/send.js +113 -0
- package/src/cli/commands/start.js +126 -0
- package/src/cli/commands/status.js +43 -0
- package/src/cli/commands/stop.js +15 -0
- package/src/cli/commands/sync.js +31 -0
- package/src/cli/ipc.js +127 -0
- package/src/crypto/encrypt.js +100 -0
- package/src/crypto/keys.js +95 -0
- package/src/daemon/epidemic.js +103 -0
- package/src/daemon/index.js +683 -0
- package/src/daemon/network.js +84 -0
- package/src/daemon/tcp.js +200 -0
- package/src/daemon/udp.js +282 -0
- package/src/db/cleanup.js +39 -0
- package/src/db/index.js +74 -0
- package/src/db/messages.js +213 -0
- package/src/identity/index.js +92 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');//it provides information about system
|
|
4
|
+
|
|
5
|
+
//returns current machine ipv4 address if not present in network returns null
|
|
6
|
+
function getCurrentIP() {
|
|
7
|
+
const ifaces = os.networkInterfaces(); // our machine has different network interfaces like wlan,eth,virtualbox net
|
|
8
|
+
//this networkInterfaces function gives all netowrk interface and their addresses
|
|
9
|
+
|
|
10
|
+
//something like this
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* {
|
|
14
|
+
lo: [
|
|
15
|
+
{ address: "127.0.0.1", family: "IPv4", internal: true }
|
|
16
|
+
],
|
|
17
|
+
eth0: [
|
|
18
|
+
{ address: "192.168.1.12", family: "IPv4", internal: false }
|
|
19
|
+
],
|
|
20
|
+
wlan0: [
|
|
21
|
+
{ address: "192.168.1.15", family: "IPv4", internal: false }
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
*/
|
|
25
|
+
for (const name of Object.keys(ifaces)) {
|
|
26
|
+
for (const iface of ifaces[name]) {
|
|
27
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
28
|
+
//as we know eth0 and wlan0 are external so we have to make this condition true
|
|
29
|
+
//also lo(localhost) we cant take this, because own cannot connect with own
|
|
30
|
+
return iface.address;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Start polling for network changes every 30 seconds.
|
|
39
|
+
* Returns a function that stops the watcher when called.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// The function does not define onChangeCallback itself.
|
|
43
|
+
//It expects the caller to pass a function when calling startNetworkWatcher
|
|
44
|
+
function startNetworkWatcher(onChangeCallback) {
|
|
45
|
+
let currentIP = getCurrentIP();
|
|
46
|
+
console.log(`[network] Current IP: ${currentIP || 'none'}`);
|
|
47
|
+
|
|
48
|
+
const interval = setInterval(() => {
|
|
49
|
+
const newIP = getCurrentIP();
|
|
50
|
+
if (newIP !== currentIP) {
|
|
51
|
+
const previousIP = currentIP;
|
|
52
|
+
currentIP = newIP;
|
|
53
|
+
console.log(`[network] Network change detected: ${previousIP} → ${newIP}`);
|
|
54
|
+
try {
|
|
55
|
+
/**
|
|
56
|
+
* Function that accepts a callback
|
|
57
|
+
function greet(callback) {
|
|
58
|
+
console.log("Hello");
|
|
59
|
+
callback();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Now call it:
|
|
63
|
+
|
|
64
|
+
greet(() => {
|
|
65
|
+
console.log("Callback executed");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
Output:
|
|
69
|
+
|
|
70
|
+
Hello
|
|
71
|
+
Callback executed
|
|
72
|
+
*/
|
|
73
|
+
onChangeCallback(newIP, previousIP);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('[network] onChangeCallback error:', err.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}, 15_000);
|
|
79
|
+
|
|
80
|
+
// Return a stop function
|
|
81
|
+
return () => clearInterval(interval);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { getCurrentIP, startNetworkWatcher };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const net = require('net');
|
|
5
|
+
|
|
6
|
+
const TCP_PORT = 41235;
|
|
7
|
+
const CONNECT_TIMEOUT_MS = 2000; //wait 2second for peers to respond to TCP connection attempts
|
|
8
|
+
const SEND_TIMEOUT_MS = 8000;
|
|
9
|
+
|
|
10
|
+
let _server = null;
|
|
11
|
+
let _identity = null;
|
|
12
|
+
let _privateKey = null;
|
|
13
|
+
let _onMessage = null;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
function startTCP({ identity, privateKey, onMessage }) {
|
|
17
|
+
if (_server) return () => stopTCP();
|
|
18
|
+
|
|
19
|
+
_identity = identity;
|
|
20
|
+
_privateKey = privateKey;
|
|
21
|
+
_onMessage = onMessage;
|
|
22
|
+
|
|
23
|
+
_server = net.createServer((socket) => {
|
|
24
|
+
handleIncomingConnection(socket); //creates tcp server and listens for incoming connections, this function will read message frame, parse JSON, process message
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
_server.listen(TCP_PORT, '0.0.0.0', () => { //0.0.0.0 means listen on all interfaces
|
|
28
|
+
console.log(`[tcp] Server listening on port ${TCP_PORT}`); // means node is ready to receive TCP messages from peers. Peers will connect to this port to send messages directly to this node.
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
_server.on('error', (err) => {
|
|
32
|
+
if (err.code === 'EADDRINUSE') {
|
|
33
|
+
console.error(`[tcp] Port ${TCP_PORT} is already in use. Another daemon may be running.`);
|
|
34
|
+
} else {
|
|
35
|
+
console.error('[tcp] Server error:', err.message);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return () => stopTCP();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Handle an incoming TCP connection from a peer.
|
|
44
|
+
* Uses newline-delimited JSON framing.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
//Shivam (192.168.1.12) ───TCP───> Amrit (192.168.1.25)
|
|
48
|
+
function handleIncomingConnection(socket) {
|
|
49
|
+
let buffer = '';
|
|
50
|
+
const remoteIP = socket.remoteAddress; //get the IP address of the peer that connected to us. This is used for logging and to pass to the onMessage handler so it knows where the message came from.
|
|
51
|
+
|
|
52
|
+
socket.on('data', (chunk) => { //this runs whenever we receive data from the peer over TCP. chunk = <Buffer ...>
|
|
53
|
+
buffer += chunk.toString('utf8'); //convert the chunk to a string and append it to the buffer. We use a buffer because TCP can split messages into multiple chunks, so we need to accumulate them until we get a full message (delimited by newline).
|
|
54
|
+
const lines = buffer.split('\n');
|
|
55
|
+
buffer = lines.pop(); // keep trailing incomplete chunk, Because the last piece might be incomplete.
|
|
56
|
+
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (!line.trim()) continue;
|
|
59
|
+
let frame;
|
|
60
|
+
try {
|
|
61
|
+
frame = JSON.parse(line); //converts JSON string to a JavaScript object. If the peer sent invalid JSON, this will throw an error, which we catch and respond with an ACK indicating the error.
|
|
62
|
+
} catch {
|
|
63
|
+
socket.write(JSON.stringify({ type: 'ack', ok: false, error: 'Invalid JSON' }) + '\n');//if parsing fails, we send an ACK back to the peer indicating that the frame was malformed. We then skip processing this frame and wait for the next one.
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
processFrame(frame, socket, remoteIP);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
socket.on('error', () => { }); // peer disconnect — ignore
|
|
71
|
+
socket.on('close', () => { });
|
|
72
|
+
|
|
73
|
+
// Auto-close after 30 s of inactivity
|
|
74
|
+
socket.setTimeout(30_000, () => socket.destroy());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Process frames received from peers. This is where we handle both "deliver" frames (messages addressed to us) and "relay" frames (messages that we need to store and forward later).
|
|
78
|
+
function processFrame(frame, socket, remoteIP) {
|
|
79
|
+
//frame: { type: 'deliver' | 'relay', message: { id, from_identity, destination, ... } } --->parsed JSON from perr,
|
|
80
|
+
//socket: the TCP socket connected to the peer that sent this frame. We use this to send ACKs back to the peer after processing the frame.
|
|
81
|
+
//remoteIP: the IP address of the peer that sent this frame. This is used for logging and is also passed to the onMessage handler so it knows where the message came from.
|
|
82
|
+
const { type, message } = frame;
|
|
83
|
+
|
|
84
|
+
if (!message || !message.id) {
|
|
85
|
+
socket.write(JSON.stringify({ type: 'ack', ok: false, error: 'Malformed frame' }) + '\n');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (type === 'deliver') {
|
|
90
|
+
// Message is addressed to us — call the delivery handler
|
|
91
|
+
console.log(`[tcp] Incoming message id=${message.id} from=${message.from_identity || '?'} via ${remoteIP}`);
|
|
92
|
+
try {
|
|
93
|
+
if (_onMessage) _onMessage(message, remoteIP);
|
|
94
|
+
socket.write(JSON.stringify({ type: 'ack', messageId: message.id, ok: true }) + '\n');
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error('[tcp] onMessage handler error:', err.message);
|
|
97
|
+
socket.write(JSON.stringify({ type: 'ack', messageId: message.id, ok: false, error: err.message }) + '\n');
|
|
98
|
+
}
|
|
99
|
+
} else if (type === 'relay') {
|
|
100
|
+
// Message is for someone else — store it
|
|
101
|
+
console.log(`[tcp] Relay message id=${message.id} → ${message.destination} via ${remoteIP}`);
|
|
102
|
+
try {
|
|
103
|
+
if (_onMessage) _onMessage(message, remoteIP, true /* isRelay */);
|
|
104
|
+
socket.write(JSON.stringify({ type: 'ack', messageId: message.id, ok: true }) + '\n');
|
|
105
|
+
} catch (err) {
|
|
106
|
+
socket.write(JSON.stringify({ type: 'ack', messageId: message.id, ok: false, error: err.message }) + '\n');
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
socket.write(JSON.stringify({ type: 'ack', ok: false, error: `Unknown frame type: ${type}` }) + '\n');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stopTCP() {
|
|
114
|
+
if (_server) {
|
|
115
|
+
_server.close();
|
|
116
|
+
_server = null;
|
|
117
|
+
console.log('[tcp] Stopped.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Send a message to a peer over TCP.
|
|
123
|
+
* Waits for ACK before resolving.
|
|
124
|
+
*/
|
|
125
|
+
function sendMessage(peerIP, frameType, message) {
|
|
126
|
+
//connect → send frame → wait for ACK → resolve/reject
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const socket = new net.Socket();
|
|
129
|
+
let buffer = '';
|
|
130
|
+
let settled = false; //A Promise must only resolve/reject once.
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
*8 seconds passed
|
|
134
|
+
no response
|
|
135
|
+
abort connection
|
|
136
|
+
*/
|
|
137
|
+
const timeout = setTimeout(() => {
|
|
138
|
+
if (!settled) {
|
|
139
|
+
settled = true;
|
|
140
|
+
socket.destroy();
|
|
141
|
+
reject(new Error(`TCP send to ${peerIP} timed out`));
|
|
142
|
+
}
|
|
143
|
+
}, SEND_TIMEOUT_MS);
|
|
144
|
+
|
|
145
|
+
socket.setTimeout(CONNECT_TIMEOUT_MS);
|
|
146
|
+
socket.on('timeout', () => {
|
|
147
|
+
if (!settled) {
|
|
148
|
+
settled = true;
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
socket.destroy();
|
|
151
|
+
reject(new Error(`TCP connect to ${peerIP}:${TCP_PORT} timed out`));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
socket.connect(TCP_PORT, peerIP, () => {
|
|
156
|
+
// Connection established — send the frame
|
|
157
|
+
const frame = JSON.stringify({ type: frameType, message }) + '\n';
|
|
158
|
+
socket.write(frame);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
socket.on('data', (chunk) => {
|
|
162
|
+
buffer += chunk.toString('utf8');
|
|
163
|
+
const lines = buffer.split('\n');
|
|
164
|
+
buffer = lines.pop();
|
|
165
|
+
|
|
166
|
+
for (const line of lines) {
|
|
167
|
+
if (!line.trim()) continue;
|
|
168
|
+
try {
|
|
169
|
+
const ack = JSON.parse(line);
|
|
170
|
+
if (!settled && ack.type === 'ack') {
|
|
171
|
+
settled = true;
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
socket.destroy();
|
|
174
|
+
resolve({ ok: ack.ok, messageId: ack.messageId });
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// wait for more data
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
socket.on('error', (err) => {
|
|
183
|
+
if (!settled) {
|
|
184
|
+
settled = true;
|
|
185
|
+
clearTimeout(timeout);
|
|
186
|
+
reject(err);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
socket.on('close', () => {
|
|
191
|
+
if (!settled) {
|
|
192
|
+
settled = true;
|
|
193
|
+
clearTimeout(timeout);
|
|
194
|
+
reject(new Error(`Connection to ${peerIP} closed before ACK`));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { startTCP, stopTCP, sendMessage, TCP_PORT };
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UDP is used for:
|
|
5
|
+
* 1. Peer discovery (broadcast)
|
|
6
|
+
* 2. Message delivery fallback when TCP is blocked by firewall
|
|
7
|
+
*
|
|
8
|
+
* Broadcast address: computed from interface IP + netmask for subnet accuracy.
|
|
9
|
+
* Falls back to 255.255.255.255 if no usable interface found.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const dgram = require('dgram');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
const UDP_PORT = 41234;
|
|
16
|
+
const BROADCAST_INTERVAL_MS = 15_000; // brodcast every 15 seconds as heartbeat, to notify peers of our presence and IP changes
|
|
17
|
+
const ANNOUNCE_TYPE = 'ib-announce'; // announces our presence and public key to peers on the LAN, wether new or existing peers, so they can connect to us via TCP or UDP direct message delivery
|
|
18
|
+
const GOODBYE_TYPE = 'ib-goodbye'; //send when we are leaving, so peers can immediately know we are leaving and remove us from their peer list, instead of waiting for a timeout to detect our absence
|
|
19
|
+
const DELIVER_TYPE = 'ib-deliver'; //used for direct message delivery via UDP unicast, when TCP is blocked by firewall, so we can still deliver messages to peers on the same LAN without relying on TCP, but with best effort delivery (no retries, no ordering guarantees)
|
|
20
|
+
const RELAY_TYPE = 'ib-relay'; //used for epidemic routing
|
|
21
|
+
|
|
22
|
+
// Max safe UDP payload size, because headers also need space
|
|
23
|
+
const MAX_UDP_PAYLOAD = 60000;
|
|
24
|
+
|
|
25
|
+
let _socket = null; //handles UDP communication (broadcasts + direct messages)
|
|
26
|
+
let _broadcastInterval = null; //handles periodic broadcast timer
|
|
27
|
+
let _myIdentity = null; //our identity string e.g. shivam@a3f2, included in broadcasts so peers know who we are
|
|
28
|
+
let _myPublicKey = null; //our RSA public key PEM, included in broadcasts so peers can encrypt messages to us for direct UDP delivery when TCP is blocked
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
function computeBroadcast(ip, netmask) {
|
|
32
|
+
const ipParts = ip.split('.').map(Number);
|
|
33
|
+
const maskParts = netmask.split('.').map(Number);
|
|
34
|
+
const broadcast = ipParts.map((b, i) => (b | (~maskParts[i] & 0xff))); //xor of ip and subnet mask
|
|
35
|
+
return broadcast.join('.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get all LAN broadcast addresses for every non-loopback IPv4 interface.
|
|
40
|
+
* Falls back to ['255.255.255.255'] if none found.
|
|
41
|
+
*/
|
|
42
|
+
function getBroadcastAddresses() {
|
|
43
|
+
const addrs = [];
|
|
44
|
+
const ifaces = os.networkInterfaces();
|
|
45
|
+
for (const ifaceList of Object.values(ifaces)) {
|
|
46
|
+
for (const iface of ifaceList) {
|
|
47
|
+
if (iface.family === 'IPv4' && !iface.internal && iface.netmask) {
|
|
48
|
+
addrs.push(computeBroadcast(iface.address, iface.netmask));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return addrs.length > 0 ? addrs : ['255.255.255.255'];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Send a UDP broadcast announce on all LAN interfaces.
|
|
57
|
+
* Goal: Tell every device on the LAN:
|
|
58
|
+
* I am an IB node. Here is my identity and public key.
|
|
59
|
+
*/
|
|
60
|
+
function sendBroadcast() {
|
|
61
|
+
if (!_socket || !_myIdentity || !_myPublicKey) return; //all are compulsory for broadcast
|
|
62
|
+
|
|
63
|
+
//first this creates JSON, then converts it into binary data UDP sends bytes, not objects
|
|
64
|
+
const packet = Buffer.from(JSON.stringify({
|
|
65
|
+
type: ANNOUNCE_TYPE,
|
|
66
|
+
identity: _myIdentity,
|
|
67
|
+
publicKey: _myPublicKey,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
for (const addr of getBroadcastAddresses()) {
|
|
71
|
+
//packet is the data we want to send,
|
|
72
|
+
// 0 is the offset in the buffer,
|
|
73
|
+
// packet.length is the number of bytes to send,
|
|
74
|
+
// UDP_PORT is the destination port,
|
|
75
|
+
// addr is the destination IP address,the callback handles any errors that occur during sending
|
|
76
|
+
_socket.send(packet, 0, packet.length, UDP_PORT, addr, (err) => {
|
|
77
|
+
if (err) console.error(`[udp] Broadcast to ${addr} failed:`, err.message);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Send a goodbye broadcast so peers immediately know we are leaving.
|
|
84
|
+
*/
|
|
85
|
+
function sendGoodbye() {
|
|
86
|
+
if (!_socket || !_myIdentity) return;
|
|
87
|
+
|
|
88
|
+
const packet = Buffer.from(JSON.stringify({
|
|
89
|
+
type: GOODBYE_TYPE,
|
|
90
|
+
identity: _myIdentity,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
for (const addr of getBroadcastAddresses()) {
|
|
94
|
+
_socket.send(packet, 0, packet.length, UDP_PORT, addr, (err) => {
|
|
95
|
+
if (err) console.error(`[udp] Goodbye broadcast to ${addr} failed:`, err.message);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Send a message directly to a peer via UDP unicast.
|
|
102
|
+
* This is the fallback when TCP is blocked by firewall.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} peerIP - Target peer's IP address
|
|
105
|
+
* @param {'deliver'|'relay'} frameType - 'deliver' for direct, 'relay' for epidemic
|
|
106
|
+
* @param {object} message - Full message object
|
|
107
|
+
* @returns {Promise<{ok: boolean}>}
|
|
108
|
+
*/
|
|
109
|
+
function sendDirectMessage(peerIP, frameType, message) {
|
|
110
|
+
return new Promise((resolve, reject) => {
|
|
111
|
+
if (!_socket) {
|
|
112
|
+
reject(new Error('UDP socket not initialized'));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const udpType = frameType === 'deliver' ? DELIVER_TYPE : RELAY_TYPE;
|
|
117
|
+
const data = JSON.stringify({ type: udpType, message });
|
|
118
|
+
|
|
119
|
+
if (data.length > MAX_UDP_PAYLOAD) {
|
|
120
|
+
reject(new Error(`Message too large for UDP delivery (${data.length} bytes > ${MAX_UDP_PAYLOAD})`));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const packet = Buffer.from(data);
|
|
125
|
+
_socket.send(packet, 0, packet.length, UDP_PORT, peerIP, (err) => {
|
|
126
|
+
if (err) {
|
|
127
|
+
console.error(`[udp] Direct send to ${peerIP} failed:`, err.message);
|
|
128
|
+
reject(err);
|
|
129
|
+
} else {
|
|
130
|
+
console.log(`[udp] Direct ${frameType} sent to ${peerIP} (${packet.length} bytes)`);
|
|
131
|
+
resolve({ ok: true });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Basically it does 3 main jobs:
|
|
139
|
+
*
|
|
140
|
+
Start a UDP socket and listen for packets
|
|
141
|
+
|
|
142
|
+
Discover peers using broadcast
|
|
143
|
+
|
|
144
|
+
Receive messages from other peers
|
|
145
|
+
*
|
|
146
|
+
* @param {object} opts
|
|
147
|
+
* @param {string} opts.identity - Our identity string e.g. shivam@a3f2
|
|
148
|
+
* @param {string} opts.publicKey - Our RSA public key PEM
|
|
149
|
+
* @param {function} opts.onPeer - Called with (identity, publicKeyPem, remoteIP) on discovery
|
|
150
|
+
* @param {function} [opts.onPeerGoodbye] - Called with (identity) when a peer sends goodbye
|
|
151
|
+
* @param {function} [opts.onMessage] - Called with (message, remoteIP, isRelay) for UDP-delivered messages
|
|
152
|
+
* @returns {function} stop — call to shut down UDP
|
|
153
|
+
*/
|
|
154
|
+
function startUDP({ identity, publicKey, onPeer, onPeerGoodbye, onMessage }) {
|
|
155
|
+
if (_socket) return () => stopUDP(); //if connection already exists, return the stop function without creating a new socket
|
|
156
|
+
|
|
157
|
+
_myIdentity = identity;
|
|
158
|
+
_myPublicKey = publicKey;
|
|
159
|
+
|
|
160
|
+
//opens a UDP socket for IPv4 with address reuse enabled (allows multiple processes to bind to the same port, useful for multiple IB instances on the same LAN)
|
|
161
|
+
_socket = dgram.createSocket({ type: 'udp4', reuseAddr: true });
|
|
162
|
+
|
|
163
|
+
_socket.on('error', (err) => {
|
|
164
|
+
console.error('[udp] Socket error:', err.message);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
//this runs whenever a UDP packet is recieved
|
|
168
|
+
_socket.on('message', (msg, rinfo) => {
|
|
169
|
+
let packet;
|
|
170
|
+
try {
|
|
171
|
+
packet = JSON.parse(msg.toString('utf8'));
|
|
172
|
+
} catch {
|
|
173
|
+
return; // ignore malformed packets
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//goodbye
|
|
177
|
+
if (packet.type === GOODBYE_TYPE && packet.identity) {
|
|
178
|
+
if (packet.identity === _myIdentity) return; // ignore our own
|
|
179
|
+
console.log(`[udp] Peer goodbye: ${packet.identity}`);
|
|
180
|
+
try {
|
|
181
|
+
if (onPeerGoodbye) onPeerGoodbye(packet.identity);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error('[udp] onPeerGoodbye handler error:', err.message);
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//direct message delivery via UDP
|
|
189
|
+
if (packet.type === DELIVER_TYPE && packet.message) {
|
|
190
|
+
console.log(`[udp] Received direct message id=${packet.message.id} from ${rinfo.address}`);
|
|
191
|
+
try {
|
|
192
|
+
if (onMessage) onMessage(packet.message, rinfo.address, false); //value false for direct delivery, so onMessage can distinguish between direct and relay messages if needed
|
|
193
|
+
} catch (err) {
|
|
194
|
+
console.error('[udp] onMessage (deliver) error:', err.message);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// relay message via UDP for epidemic routing (used when destination peer is not currently on the network, so we relay to other peers in the hope that one of them will eventually meet the destination peer and deliver it)
|
|
200
|
+
if (packet.type === RELAY_TYPE && packet.message) {
|
|
201
|
+
console.log(`[udp] Received relay message id=${packet.message.id} from ${rinfo.address}`);
|
|
202
|
+
try {
|
|
203
|
+
if (onMessage) onMessage(packet.message, rinfo.address, true);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
console.error('[udp] onMessage (relay) error:', err.message);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Handle announce
|
|
211
|
+
if (packet.type !== ANNOUNCE_TYPE) return;
|
|
212
|
+
if (!packet.identity || !packet.publicKey) return;
|
|
213
|
+
|
|
214
|
+
// Ignore our own broadcasts
|
|
215
|
+
if (packet.identity === _myIdentity) return;
|
|
216
|
+
|
|
217
|
+
console.log(`[udp] Peer discovered: ${packet.identity} at ${rinfo.address}`);
|
|
218
|
+
try {
|
|
219
|
+
onPeer(packet.identity, packet.publicKey, rinfo.address);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
console.error('[udp] onPeer handler error:', err.message);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
_socket.bind(UDP_PORT, () => {
|
|
226
|
+
_socket.setBroadcast(true);
|
|
227
|
+
console.log(`[udp] Listening on port ${UDP_PORT}`);
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
// Broadcast immediately so peers on the LAN discover us fast
|
|
231
|
+
sendBroadcast();
|
|
232
|
+
|
|
233
|
+
// Re-broadcast every 15 s, setInterval will automatically call sendBroadcast every 15 seconds
|
|
234
|
+
_broadcastInterval = setInterval(sendBroadcast, BROADCAST_INTERVAL_MS);
|
|
235
|
+
/**
|
|
236
|
+
* if nothing else is running
|
|
237
|
+
allow program to exit
|
|
238
|
+
*/
|
|
239
|
+
_broadcastInterval.unref();
|
|
240
|
+
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
_socket.on('error', (err) => {
|
|
244
|
+
if (err.code === 'EADDRINUSE') {
|
|
245
|
+
console.error(`[udp] Port ${UDP_PORT} already in use.`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return () => stopUDP();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function stopUDP() {
|
|
253
|
+
if (_broadcastInterval) {
|
|
254
|
+
clearInterval(_broadcastInterval);
|
|
255
|
+
_broadcastInterval = null;
|
|
256
|
+
}
|
|
257
|
+
if (_socket) {
|
|
258
|
+
_socket.close();
|
|
259
|
+
_socket = null;
|
|
260
|
+
}
|
|
261
|
+
console.log('[udp] Stopped.');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Broadcast a goodbye (exit ke waqt call karna).
|
|
265
|
+
function broadcastGoodbye() {
|
|
266
|
+
sendGoodbye();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Broadcast our identity immediately on the current network.
|
|
271
|
+
* Called when a network change is detected.
|
|
272
|
+
*/
|
|
273
|
+
function broadcastNow() {
|
|
274
|
+
sendBroadcast();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get the current UDP socket
|
|
279
|
+
*/
|
|
280
|
+
function getSocket() { return _socket; }
|
|
281
|
+
|
|
282
|
+
module.exports = { startUDP, stopUDP, broadcastNow, broadcastGoodbye, sendDirectMessage, getSocket, UDP_PORT };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { openDb } = require('./index');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Delete all messages that have expired (ttl < now) and are not delivered.
|
|
7
|
+
* Delivered messages are kept as a record so senders can still check
|
|
8
|
+
* ib status <id>. They are cleaned up after 30 days.
|
|
9
|
+
* Also clean up peers that haven't been seen in 1 hour, to prevent stale peer entries.
|
|
10
|
+
*/
|
|
11
|
+
function cleanExpired() {
|
|
12
|
+
const db = openDb();
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const thirtyDaysAgo = now - 30 * 24 * 60 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
// Delete expired undelivered/intransit messages
|
|
17
|
+
const r1 = db.prepare(`
|
|
18
|
+
DELETE FROM messages
|
|
19
|
+
WHERE ttl < ? AND status != 'delivered'
|
|
20
|
+
`).run(now);
|
|
21
|
+
|
|
22
|
+
//clean up old delivered messages
|
|
23
|
+
const r2 = db.prepare(`
|
|
24
|
+
DELETE FROM messages
|
|
25
|
+
WHERE status = 'delivered' AND created_at < ?
|
|
26
|
+
`).run(thirtyDaysAgo);
|
|
27
|
+
|
|
28
|
+
// Clean up peers not seen in 1 hour (with 15s heartbeats, a live peer would have re-announced many times)
|
|
29
|
+
const oneHourAgo = now - 60 * 60 * 1000;
|
|
30
|
+
db.prepare(`DELETE FROM peers WHERE last_seen < ?`).run(oneHourAgo);
|
|
31
|
+
|
|
32
|
+
const deleted = r1.changes + r2.changes;
|
|
33
|
+
if (deleted > 0) {
|
|
34
|
+
console.log(`[cleanup] Removed ${deleted} expired message(s) from storage.`);
|
|
35
|
+
}
|
|
36
|
+
return deleted;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { cleanExpired };
|
package/src/db/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path'); //help safely create path files
|
|
4
|
+
const fs = require('fs'); //help read and write files, also check if file exists
|
|
5
|
+
const Database = require('better-sqlite3'); //importing SQLite from better-sqlite3
|
|
6
|
+
const { IB_DIR } = require('../identity/index'); //main folder file path
|
|
7
|
+
|
|
8
|
+
const DB_PATH = path.join(IB_DIR, 'ib.db');
|
|
9
|
+
|
|
10
|
+
let _db = null; //stores database connection, so we dont need to open the database multiple times,
|
|
11
|
+
|
|
12
|
+
//open or create the database and initialize the schema,uses cache instance also
|
|
13
|
+
function openDb() {
|
|
14
|
+
if (_db) return _db;
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(IB_DIR)) {
|
|
17
|
+
fs.mkdirSync(IB_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_db = new Database(DB_PATH); //if file exists, open ib.db
|
|
21
|
+
//else create ib.db
|
|
22
|
+
|
|
23
|
+
// Enable WAL mode for better concurrent read performance
|
|
24
|
+
_db.pragma('journal_mode = WAL');
|
|
25
|
+
_db.pragma('foreign_keys = ON');
|
|
26
|
+
|
|
27
|
+
// Main messages table
|
|
28
|
+
_db.exec(`
|
|
29
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
30
|
+
id TEXT PRIMARY KEY,
|
|
31
|
+
from_identity TEXT NOT NULL,
|
|
32
|
+
destination TEXT NOT NULL,
|
|
33
|
+
payload TEXT NOT NULL, -- JSON-encoded encrypted blob
|
|
34
|
+
status TEXT NOT NULL DEFAULT 'undelivered',
|
|
35
|
+
-- undelivered | intransit | delivered
|
|
36
|
+
hop_count INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
ttl INTEGER NOT NULL, -- Unix epoch ms — message dies after this
|
|
38
|
+
created_at INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
`);
|
|
41
|
+
|
|
42
|
+
// Known peer public keys
|
|
43
|
+
_db.exec(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS peers (
|
|
45
|
+
identity TEXT PRIMARY KEY, -- e.g. raj@a3f2
|
|
46
|
+
public_key TEXT NOT NULL, -- RSA PEM
|
|
47
|
+
ip TEXT, -- last known IP address
|
|
48
|
+
last_seen INTEGER NOT NULL -- Unix epoch ms
|
|
49
|
+
);
|
|
50
|
+
`);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
_db.exec(`ALTER TABLE peers ADD COLUMN ip TEXT`);
|
|
54
|
+
} catch (_) {
|
|
55
|
+
// column already exists, leave it
|
|
56
|
+
//for safety purpose
|
|
57
|
+
|
|
58
|
+
return _db;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Close the database connection.
|
|
64
|
+
* Should be called on daemon shutdown.
|
|
65
|
+
*/
|
|
66
|
+
function closeDb() {
|
|
67
|
+
if (_db) {
|
|
68
|
+
_db.close();
|
|
69
|
+
_db = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
module.exports = { openDb, closeDb, DB_PATH };
|