librats 0.5.1 → 0.5.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 +1 -1
- package/lib/index.d.ts +2 -1
- package/native-src/src/dht.cpp +186 -68
- package/native-src/src/dht.h +28 -21
- package/native-src/src/librats.cpp +58 -38
- package/native-src/src/librats.h +5 -2
- package/native-src/src/librats_c.cpp +18 -2
- package/native-src/src/librats_c.h +3 -1
- package/package.json +1 -1
- package/src/librats_node.cpp +46 -1
package/README.md
CHANGED
|
@@ -205,7 +205,7 @@ client.publishJsonToTopic('general-chat', JSON.stringify(message));
|
|
|
205
205
|
- `startDhtDiscovery(dhtPort: number): boolean` - Start DHT discovery
|
|
206
206
|
- `stopDhtDiscovery(): void` - Stop DHT discovery
|
|
207
207
|
- `isDhtRunning(): boolean` - Check if DHT is running
|
|
208
|
-
- `announceForHash(contentHash: string, port: number): boolean` - Announce for hash
|
|
208
|
+
- `announceForHash(contentHash: string, port: number, callback?: (peers: string[]) => void): boolean` - Announce for hash with optional peer discovery callback
|
|
209
209
|
|
|
210
210
|
#### Encryption
|
|
211
211
|
|
package/lib/index.d.ts
CHANGED
|
@@ -370,9 +370,10 @@ export class RatsClient {
|
|
|
370
370
|
* Announce availability for a content hash
|
|
371
371
|
* @param contentHash - Hash to announce for
|
|
372
372
|
* @param port - Port to announce
|
|
373
|
+
* @param callback - Optional callback to receive discovered peers during DHT traversal
|
|
373
374
|
* @returns true if announced successfully
|
|
374
375
|
*/
|
|
375
|
-
announceForHash(contentHash: string, port: number): boolean;
|
|
376
|
+
announceForHash(contentHash: string, port: number, callback?: (peers: string[]) => void): boolean;
|
|
376
377
|
|
|
377
378
|
// ============ mDNS ============
|
|
378
379
|
|
package/native-src/src/dht.cpp
CHANGED
|
@@ -210,7 +210,7 @@ bool DhtClient::find_peers(const InfoHash& info_hash, PeerDiscoveryCallback call
|
|
|
210
210
|
return true;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
bool DhtClient::announce_peer(const InfoHash& info_hash, uint16_t port) {
|
|
213
|
+
bool DhtClient::announce_peer(const InfoHash& info_hash, uint16_t port, PeerDiscoveryCallback callback) {
|
|
214
214
|
if (!running_) {
|
|
215
215
|
LOG_DHT_ERROR("DHT client not running");
|
|
216
216
|
return false;
|
|
@@ -220,24 +220,64 @@ bool DhtClient::announce_peer(const InfoHash& info_hash, uint16_t port) {
|
|
|
220
220
|
port = port_;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
std::string hash_key = node_id_to_hex(info_hash);
|
|
224
|
+
LOG_DHT_INFO("Announcing peer for info hash: " << hash_key << " on port " << port);
|
|
224
225
|
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
226
|
+
// BEP 5 compliant announce:
|
|
227
|
+
// 1. Perform iterative Kademlia lookup (like find_peers)
|
|
228
|
+
// 2. Collect tokens from responding nodes
|
|
229
|
+
// 3. Send announce_peer to k closest nodes with their tokens
|
|
230
|
+
|
|
231
|
+
// Get initial nodes from routing table
|
|
232
|
+
auto closest_nodes = find_closest_nodes(info_hash, K_BUCKET_SIZE);
|
|
233
|
+
|
|
234
|
+
if (closest_nodes.empty()) {
|
|
235
|
+
LOG_DHT_WARN("No nodes in routing table to announce to for info_hash " << hash_key);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
DeferredCallbacks deferred;
|
|
240
|
+
|
|
241
|
+
{
|
|
242
|
+
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
243
|
+
|
|
244
|
+
// Check if a search/announce is already ongoing for this info_hash
|
|
245
|
+
auto search_it = pending_searches_.find(hash_key);
|
|
246
|
+
if (search_it != pending_searches_.end()) {
|
|
247
|
+
if (search_it->second.is_announce) {
|
|
248
|
+
LOG_DHT_INFO("Announce already in progress for info hash " << hash_key);
|
|
249
|
+
return true;
|
|
235
250
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
251
|
+
// Regular find_peers in progress - let it complete, then user can announce again
|
|
252
|
+
LOG_DHT_WARN("find_peers already in progress for info hash " << hash_key << " - announce will wait");
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Create new search with announce flag
|
|
257
|
+
PendingSearch new_search(info_hash);
|
|
258
|
+
new_search.is_announce = true;
|
|
259
|
+
new_search.announce_port = port;
|
|
260
|
+
|
|
261
|
+
// Add callback if provided - peers discovered during traversal will be returned through it
|
|
262
|
+
if (callback) {
|
|
263
|
+
new_search.callbacks.push_back(callback);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Initialize search_nodes with closest nodes from routing table (already sorted)
|
|
267
|
+
new_search.search_nodes = std::move(closest_nodes);
|
|
268
|
+
|
|
269
|
+
auto insert_result = pending_searches_.emplace(hash_key, std::move(new_search));
|
|
270
|
+
PendingSearch& search_ref = insert_result.first->second;
|
|
271
|
+
|
|
272
|
+
LOG_DHT_DEBUG("Initialized announce search with " << search_ref.search_nodes.size() << " nodes from routing table");
|
|
273
|
+
|
|
274
|
+
// Start sending requests
|
|
275
|
+
add_search_requests(search_ref, deferred);
|
|
239
276
|
}
|
|
240
277
|
|
|
278
|
+
// Invoke callbacks outside the lock to avoid deadlock
|
|
279
|
+
deferred.invoke();
|
|
280
|
+
|
|
241
281
|
return true;
|
|
242
282
|
}
|
|
243
283
|
|
|
@@ -262,6 +302,13 @@ bool DhtClient::is_search_active(const InfoHash& info_hash) const {
|
|
|
262
302
|
return it != pending_searches_.end() && !it->second.is_finished;
|
|
263
303
|
}
|
|
264
304
|
|
|
305
|
+
bool DhtClient::is_announce_active(const InfoHash& info_hash) const {
|
|
306
|
+
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
307
|
+
std::string hash_key = node_id_to_hex(info_hash);
|
|
308
|
+
auto it = pending_searches_.find(hash_key);
|
|
309
|
+
return it != pending_searches_.end() && !it->second.is_finished && it->second.is_announce;
|
|
310
|
+
}
|
|
311
|
+
|
|
265
312
|
size_t DhtClient::get_active_searches_count() const {
|
|
266
313
|
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
267
314
|
size_t count = 0;
|
|
@@ -273,6 +320,17 @@ size_t DhtClient::get_active_searches_count() const {
|
|
|
273
320
|
return count;
|
|
274
321
|
}
|
|
275
322
|
|
|
323
|
+
size_t DhtClient::get_active_announces_count() const {
|
|
324
|
+
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
325
|
+
size_t count = 0;
|
|
326
|
+
for (const auto& [hash, search] : pending_searches_) {
|
|
327
|
+
if (!search.is_finished && search.is_announce) {
|
|
328
|
+
count++;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return count;
|
|
332
|
+
}
|
|
333
|
+
|
|
276
334
|
std::vector<Peer> DhtClient::get_default_bootstrap_nodes() {
|
|
277
335
|
return {
|
|
278
336
|
{"router.bittorrent.com", 6881},
|
|
@@ -340,9 +398,6 @@ void DhtClient::maintenance_loop() {
|
|
|
340
398
|
// Cleanup stale peer tokens
|
|
341
399
|
cleanup_stale_peer_tokens();
|
|
342
400
|
|
|
343
|
-
// Cleanup stale pending announces
|
|
344
|
-
cleanup_stale_announces();
|
|
345
|
-
|
|
346
401
|
// Cleanup stale pending searches
|
|
347
402
|
cleanup_stale_searches();
|
|
348
403
|
|
|
@@ -763,9 +818,16 @@ void DhtClient::handle_krpc_response(const KrpcMessage& message, const Peer& sen
|
|
|
763
818
|
handle_get_peers_empty_response(message.transaction_id, sender);
|
|
764
819
|
}
|
|
765
820
|
|
|
766
|
-
//
|
|
821
|
+
// Save write token if present (needed for announce_peer after traversal completes)
|
|
767
822
|
if (!message.token.empty()) {
|
|
768
|
-
|
|
823
|
+
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
824
|
+
auto trans_it = transaction_to_search_.find(message.transaction_id);
|
|
825
|
+
if (trans_it != transaction_to_search_.end()) {
|
|
826
|
+
auto search_it = pending_searches_.find(trans_it->second.info_hash_hex);
|
|
827
|
+
if (search_it != pending_searches_.end()) {
|
|
828
|
+
save_write_token(search_it->second, trans_it->second.queried_node_id, message.token);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
769
831
|
}
|
|
770
832
|
|
|
771
833
|
// Clean up finished searches AFTER all response data has been processed
|
|
@@ -1028,8 +1090,10 @@ void DhtClient::print_statistics() {
|
|
|
1028
1090
|
|
|
1029
1091
|
// Pending searches statistics
|
|
1030
1092
|
size_t pending_searches = 0;
|
|
1093
|
+
size_t pending_announces = 0;
|
|
1031
1094
|
size_t total_search_nodes = 0;
|
|
1032
1095
|
size_t total_found_peers = 0;
|
|
1096
|
+
size_t total_write_tokens = 0;
|
|
1033
1097
|
size_t active_transactions = 0;
|
|
1034
1098
|
{
|
|
1035
1099
|
std::lock_guard<std::mutex> search_lock(pending_searches_mutex_);
|
|
@@ -1038,16 +1102,13 @@ void DhtClient::print_statistics() {
|
|
|
1038
1102
|
for (const auto& [hash, search] : pending_searches_) {
|
|
1039
1103
|
total_search_nodes += search.search_nodes.size();
|
|
1040
1104
|
total_found_peers += search.found_peers.size();
|
|
1105
|
+
total_write_tokens += search.write_tokens.size();
|
|
1106
|
+
if (search.is_announce) {
|
|
1107
|
+
pending_announces++;
|
|
1108
|
+
}
|
|
1041
1109
|
}
|
|
1042
1110
|
}
|
|
1043
1111
|
|
|
1044
|
-
// Pending announces statistics
|
|
1045
|
-
size_t pending_announces_count = 0;
|
|
1046
|
-
{
|
|
1047
|
-
std::lock_guard<std::mutex> announce_lock(pending_announces_mutex_);
|
|
1048
|
-
pending_announces_count = pending_announces_.size();
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
1112
|
// Announced peers statistics
|
|
1052
1113
|
size_t announced_peers_total = 0;
|
|
1053
1114
|
size_t announced_peers_infohashes = 0;
|
|
@@ -1084,9 +1145,11 @@ void DhtClient::print_statistics() {
|
|
|
1084
1145
|
<< ", Max bucket size: " << max_bucket_size << "/" << K_BUCKET_SIZE);
|
|
1085
1146
|
LOG_DHT_INFO("[ACTIVE OPERATIONS]");
|
|
1086
1147
|
LOG_DHT_INFO(" Pending searches: " << pending_searches
|
|
1087
|
-
<< " (
|
|
1148
|
+
<< " (announces: " << pending_announces
|
|
1149
|
+
<< ", nodes: " << total_search_nodes
|
|
1150
|
+
<< ", peers: " << total_found_peers
|
|
1151
|
+
<< ", tokens: " << total_write_tokens << ")");
|
|
1088
1152
|
LOG_DHT_INFO(" Active transactions: " << active_transactions);
|
|
1089
|
-
LOG_DHT_INFO(" Pending announces: " << pending_announces_count);
|
|
1090
1153
|
LOG_DHT_INFO(" Pending ping verifications: " << pending_pings
|
|
1091
1154
|
<< " (nodes being replaced: " << nodes_being_replaced << ")");
|
|
1092
1155
|
LOG_DHT_INFO("[STORED DATA]");
|
|
@@ -1203,23 +1266,6 @@ void DhtClient::refresh_buckets() {
|
|
|
1203
1266
|
}
|
|
1204
1267
|
}
|
|
1205
1268
|
|
|
1206
|
-
void DhtClient::cleanup_stale_announces() {
|
|
1207
|
-
std::lock_guard<std::mutex> lock(pending_announces_mutex_);
|
|
1208
|
-
|
|
1209
|
-
auto now = std::chrono::steady_clock::now();
|
|
1210
|
-
auto stale_threshold = std::chrono::minutes(5); // Remove announces older than 5 minutes
|
|
1211
|
-
|
|
1212
|
-
auto it = pending_announces_.begin();
|
|
1213
|
-
while (it != pending_announces_.end()) {
|
|
1214
|
-
if (now - it->second.created_at > stale_threshold) {
|
|
1215
|
-
LOG_DHT_DEBUG("Removing stale pending announce for transaction " << it->first);
|
|
1216
|
-
it = pending_announces_.erase(it);
|
|
1217
|
-
} else {
|
|
1218
|
-
++it;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
1269
|
void DhtClient::cleanup_stale_searches() {
|
|
1224
1270
|
std::lock_guard<std::mutex> lock(pending_searches_mutex_);
|
|
1225
1271
|
|
|
@@ -1475,24 +1521,6 @@ void DhtClient::cleanup_timed_out_search_requests() {
|
|
|
1475
1521
|
}
|
|
1476
1522
|
}
|
|
1477
1523
|
|
|
1478
|
-
void DhtClient::handle_get_peers_response_for_announce(const std::string& transaction_id, const Peer& responder, const std::string& token) {
|
|
1479
|
-
std::lock_guard<std::mutex> lock(pending_announces_mutex_);
|
|
1480
|
-
|
|
1481
|
-
auto it = pending_announces_.find(transaction_id);
|
|
1482
|
-
if (it != pending_announces_.end()) {
|
|
1483
|
-
const auto& pending_announce = it->second;
|
|
1484
|
-
LOG_DHT_DEBUG("Found pending announce for transaction " << transaction_id
|
|
1485
|
-
<< " - sending announce_peer for info_hash " << node_id_to_hex(pending_announce.info_hash)
|
|
1486
|
-
<< " to " << responder.ip << ":" << responder.port);
|
|
1487
|
-
|
|
1488
|
-
// Send announce_peer with the received token
|
|
1489
|
-
send_krpc_announce_peer(responder, pending_announce.info_hash, pending_announce.port, token);
|
|
1490
|
-
|
|
1491
|
-
// Remove the pending announce since we've handled it
|
|
1492
|
-
pending_announces_.erase(it);
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
1524
|
void DhtClient::handle_get_peers_empty_response(const std::string& transaction_id, const Peer& responder) {
|
|
1497
1525
|
DeferredCallbacks deferred;
|
|
1498
1526
|
{
|
|
@@ -1796,6 +1824,86 @@ void DhtClient::add_node_to_search(PendingSearch& search, const DhtNode& node) {
|
|
|
1796
1824
|
}
|
|
1797
1825
|
}
|
|
1798
1826
|
|
|
1827
|
+
void DhtClient::save_write_token(PendingSearch& search, const NodeId& node_id, const std::string& token) {
|
|
1828
|
+
// Save the write token received from a node (BEP 5 compliant)
|
|
1829
|
+
// This token will be used later when sending announce_peer to this node
|
|
1830
|
+
|
|
1831
|
+
if (token.empty()) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Only save token if we don't already have one from this node
|
|
1836
|
+
// (first token is usually the valid one)
|
|
1837
|
+
if (search.write_tokens.find(node_id) == search.write_tokens.end()) {
|
|
1838
|
+
search.write_tokens[node_id] = token;
|
|
1839
|
+
LOG_DHT_DEBUG("Saved write token from node " << node_id_to_hex(node_id)
|
|
1840
|
+
<< " for info_hash " << node_id_to_hex(search.info_hash)
|
|
1841
|
+
<< " (total tokens: " << search.write_tokens.size() << ")");
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
void DhtClient::send_announce_to_closest_nodes(PendingSearch& search) {
|
|
1846
|
+
// BEP 5: Send announce_peer to the k closest nodes that:
|
|
1847
|
+
// 1. Responded to our get_peers query
|
|
1848
|
+
// 2. Gave us a valid write token
|
|
1849
|
+
|
|
1850
|
+
if (!search.is_announce) {
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
std::string hash_key = node_id_to_hex(search.info_hash);
|
|
1855
|
+
|
|
1856
|
+
LOG_DHT_INFO("Sending announce_peer to closest nodes for info_hash " << hash_key
|
|
1857
|
+
<< " on port " << search.announce_port);
|
|
1858
|
+
|
|
1859
|
+
// Collect nodes that responded and have tokens, sorted by distance (closest first)
|
|
1860
|
+
std::vector<std::pair<DhtNode, std::string>> announce_targets;
|
|
1861
|
+
announce_targets.reserve(K_BUCKET_SIZE);
|
|
1862
|
+
|
|
1863
|
+
for (const auto& node : search.search_nodes) {
|
|
1864
|
+
if (announce_targets.size() >= K_BUCKET_SIZE) {
|
|
1865
|
+
break; // We have enough targets
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// Check if node responded successfully
|
|
1869
|
+
auto state_it = search.node_states.find(node.id);
|
|
1870
|
+
if (state_it == search.node_states.end()) {
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
if (!(state_it->second & SearchNodeFlags::RESPONDED)) {
|
|
1874
|
+
continue; // Node didn't respond
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// Check if we have a token from this node
|
|
1878
|
+
auto token_it = search.write_tokens.find(node.id);
|
|
1879
|
+
if (token_it == search.write_tokens.end()) {
|
|
1880
|
+
LOG_DHT_DEBUG("Node " << node_id_to_hex(node.id) << " responded but no token - skipping");
|
|
1881
|
+
continue; // No token from this node
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
announce_targets.emplace_back(node, token_it->second);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
if (announce_targets.empty()) {
|
|
1888
|
+
LOG_DHT_WARN("No nodes with tokens to announce to for info_hash " << hash_key);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
LOG_DHT_INFO("Announcing to " << announce_targets.size() << " closest nodes with tokens");
|
|
1893
|
+
|
|
1894
|
+
// Send announce_peer to each target
|
|
1895
|
+
for (const auto& [node, token] : announce_targets) {
|
|
1896
|
+
LOG_DHT_DEBUG("Sending announce_peer to node " << node_id_to_hex(node.id)
|
|
1897
|
+
<< " at " << node.peer.ip << ":" << node.peer.port
|
|
1898
|
+
<< " with token (distance: " << get_bucket_index(node.id) << ")");
|
|
1899
|
+
|
|
1900
|
+
send_krpc_announce_peer(node.peer, search.info_hash, search.announce_port, token);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
LOG_DHT_INFO("Announce completed: sent announce_peer to " << announce_targets.size()
|
|
1904
|
+
<< " nodes for info_hash " << hash_key);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1799
1907
|
bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& deferred) {
|
|
1800
1908
|
// Returns true if search is done (completed or should be finished)
|
|
1801
1909
|
|
|
@@ -1875,7 +1983,7 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
|
|
|
1875
1983
|
queries_sent++;
|
|
1876
1984
|
}
|
|
1877
1985
|
|
|
1878
|
-
LOG_DHT_DEBUG("Search [" << hash_key << "] progress [ms: " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - search.created_at).count() << "]:");
|
|
1986
|
+
LOG_DHT_DEBUG((search.is_announce ? "Announce" : "Search") << " [" << hash_key << "] progress [ms: " << std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - search.created_at).count() << "]:");
|
|
1879
1987
|
LOG_DHT_DEBUG(" * search_nodes: " << search.search_nodes.size());
|
|
1880
1988
|
LOG_DHT_DEBUG(" * queries_sent: " << queries_sent);
|
|
1881
1989
|
LOG_DHT_DEBUG(" * invoke_count: " << search.invoke_count);
|
|
@@ -1903,7 +2011,7 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
|
|
|
1903
2011
|
if (f & SearchNodeFlags::ABANDONED) abandoned_total++;
|
|
1904
2012
|
}
|
|
1905
2013
|
|
|
1906
|
-
LOG_DHT_INFO("=== Search Completed for info_hash " << hash_key << " ===");
|
|
2014
|
+
LOG_DHT_INFO("=== " << (search.is_announce ? "Announce" : "Search") << " Completed for info_hash " << hash_key << " ===");
|
|
1907
2015
|
LOG_DHT_INFO(" Duration: " << duration_ms << "ms");
|
|
1908
2016
|
LOG_DHT_INFO(" Total nodes queried: " << queried_total);
|
|
1909
2017
|
LOG_DHT_INFO(" Total nodes responded: " << responded_total);
|
|
@@ -1911,8 +2019,18 @@ bool DhtClient::add_search_requests(PendingSearch& search, DeferredCallbacks& de
|
|
|
1911
2019
|
LOG_DHT_INFO(" Nodes with short timeout: " << short_timeout_total);
|
|
1912
2020
|
LOG_DHT_INFO(" Nodes abandoned (truncation): " << abandoned_total);
|
|
1913
2021
|
LOG_DHT_INFO(" Final branch_factor: " << search.branch_factor << " (initial: " << ALPHA << ")");
|
|
1914
|
-
|
|
1915
|
-
|
|
2022
|
+
if (search.is_announce) {
|
|
2023
|
+
LOG_DHT_INFO(" Write tokens collected: " << search.write_tokens.size());
|
|
2024
|
+
LOG_DHT_INFO(" Announce port: " << search.announce_port);
|
|
2025
|
+
} else {
|
|
2026
|
+
LOG_DHT_INFO(" Total peers found: " << search.found_peers.size());
|
|
2027
|
+
LOG_DHT_INFO(" Callbacks to invoke: " << search.callbacks.size());
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// If this is an announce search, send announce_peer to k closest nodes with tokens
|
|
2031
|
+
if (search.is_announce) {
|
|
2032
|
+
send_announce_to_closest_nodes(search);
|
|
2033
|
+
}
|
|
1916
2034
|
|
|
1917
2035
|
// Collect callbacks for deferred invocation (avoid deadlock - don't call user callbacks while holding mutex)
|
|
1918
2036
|
deferred.should_invoke = true;
|
package/native-src/src/dht.h
CHANGED
|
@@ -198,9 +198,10 @@ public:
|
|
|
198
198
|
* Announce that this node is a peer for a specific info hash
|
|
199
199
|
* @param info_hash The info hash to announce
|
|
200
200
|
* @param port The port to announce (0 for DHT port)
|
|
201
|
+
* @param callback Optional callback to receive discovered peers during traversal
|
|
201
202
|
* @return true if announcement started successfully, false otherwise
|
|
202
203
|
*/
|
|
203
|
-
bool announce_peer(const InfoHash& info_hash, uint16_t port = 0);
|
|
204
|
+
bool announce_peer(const InfoHash& info_hash, uint16_t port = 0, PeerDiscoveryCallback callback = nullptr);
|
|
204
205
|
|
|
205
206
|
/**
|
|
206
207
|
* Get our node ID
|
|
@@ -227,12 +228,25 @@ public:
|
|
|
227
228
|
*/
|
|
228
229
|
bool is_search_active(const InfoHash& info_hash) const;
|
|
229
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Check if an announce is currently active for an info hash
|
|
233
|
+
* @param info_hash The info hash to check
|
|
234
|
+
* @return true if announce is active, false otherwise
|
|
235
|
+
*/
|
|
236
|
+
bool is_announce_active(const InfoHash& info_hash) const;
|
|
237
|
+
|
|
230
238
|
/**
|
|
231
239
|
* Get number of active searches
|
|
232
240
|
* @return Number of active searches
|
|
233
241
|
*/
|
|
234
242
|
size_t get_active_searches_count() const;
|
|
235
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Get number of active announces
|
|
246
|
+
* @return Number of active announces
|
|
247
|
+
*/
|
|
248
|
+
size_t get_active_announces_count() const;
|
|
249
|
+
|
|
236
250
|
/**
|
|
237
251
|
* Check if DHT is running
|
|
238
252
|
* @return true if running, false otherwise
|
|
@@ -279,10 +293,9 @@ private:
|
|
|
279
293
|
// 1. pending_pings_mutex_ (Ping verification state, nodes_being_replaced_)
|
|
280
294
|
// 2. pending_searches_mutex_ (Search state and transaction mappings)
|
|
281
295
|
// 3. routing_table_mutex_ (core routing data)
|
|
282
|
-
// 4.
|
|
283
|
-
// 5.
|
|
284
|
-
// 6.
|
|
285
|
-
// 7. shutdown_mutex_ (Lowest priority - can be locked independently)
|
|
296
|
+
// 4. announced_peers_mutex_ (Stored peer data)
|
|
297
|
+
// 5. peer_tokens_mutex_ (Token validation data)
|
|
298
|
+
// 6. shutdown_mutex_ (Lowest priority - can be locked independently)
|
|
286
299
|
//
|
|
287
300
|
// Routing table (k-buckets)
|
|
288
301
|
std::vector<std::vector<DhtNode>> routing_table_;
|
|
@@ -301,17 +314,6 @@ private:
|
|
|
301
314
|
std::mutex peer_tokens_mutex_; // Lock order: 6
|
|
302
315
|
|
|
303
316
|
|
|
304
|
-
// Pending announce tracking (for BEP 5 compliance)
|
|
305
|
-
struct PendingAnnounce {
|
|
306
|
-
InfoHash info_hash;
|
|
307
|
-
uint16_t port;
|
|
308
|
-
std::chrono::steady_clock::time_point created_at;
|
|
309
|
-
|
|
310
|
-
PendingAnnounce(const InfoHash& hash, uint16_t p)
|
|
311
|
-
: info_hash(hash), port(p), created_at(std::chrono::steady_clock::now()) {}
|
|
312
|
-
};
|
|
313
|
-
std::unordered_map<std::string, PendingAnnounce> pending_announces_;
|
|
314
|
-
std::mutex pending_announces_mutex_; // Lock order: 4
|
|
315
317
|
|
|
316
318
|
// Pending find_peers tracking (to map transaction IDs to info_hash)
|
|
317
319
|
struct PendingSearch {
|
|
@@ -332,9 +334,16 @@ private:
|
|
|
332
334
|
// Callbacks to invoke when peers are found (supports multiple concurrent searches for same info_hash)
|
|
333
335
|
std::vector<PeerDiscoveryCallback> callbacks;
|
|
334
336
|
|
|
337
|
+
// Announce support: tokens collected during traversal (BEP 5 compliant)
|
|
338
|
+
// Maps node_id -> write_token received from that node
|
|
339
|
+
std::unordered_map<NodeId, std::string> write_tokens;
|
|
340
|
+
bool is_announce; // true if this search is for announce_peer
|
|
341
|
+
uint16_t announce_port; // port to announce (only valid if is_announce)
|
|
342
|
+
|
|
335
343
|
PendingSearch(const InfoHash& hash)
|
|
336
344
|
: info_hash(hash), created_at(std::chrono::steady_clock::now()),
|
|
337
|
-
invoke_count(0), branch_factor(ALPHA), is_finished(false)
|
|
345
|
+
invoke_count(0), branch_factor(ALPHA), is_finished(false),
|
|
346
|
+
is_announce(false), announce_port(0) {}
|
|
338
347
|
};
|
|
339
348
|
std::unordered_map<std::string, PendingSearch> pending_searches_; // info_hash (hex) -> PendingSearch
|
|
340
349
|
mutable std::mutex pending_searches_mutex_; // Lock order: 2
|
|
@@ -434,10 +443,6 @@ private:
|
|
|
434
443
|
void refresh_buckets();
|
|
435
444
|
void print_statistics();
|
|
436
445
|
|
|
437
|
-
// Pending announce management
|
|
438
|
-
void cleanup_stale_announces();
|
|
439
|
-
void handle_get_peers_response_for_announce(const std::string& transaction_id, const Peer& responder, const std::string& token);
|
|
440
|
-
|
|
441
446
|
// Pending search management
|
|
442
447
|
void cleanup_stale_searches();
|
|
443
448
|
void cleanup_timed_out_search_requests();
|
|
@@ -445,8 +450,10 @@ private:
|
|
|
445
450
|
void handle_get_peers_response_for_search(const std::string& transaction_id, const Peer& responder, const std::vector<Peer>& peers);
|
|
446
451
|
void handle_get_peers_response_with_nodes(const std::string& transaction_id, const Peer& responder, const std::vector<KrpcNode>& nodes);
|
|
447
452
|
void handle_get_peers_empty_response(const std::string& transaction_id, const Peer& responder);
|
|
453
|
+
void save_write_token(PendingSearch& search, const NodeId& node_id, const std::string& token);
|
|
448
454
|
bool add_search_requests(PendingSearch& search, DeferredCallbacks& deferred);
|
|
449
455
|
void add_node_to_search(PendingSearch& search, const DhtNode& node);
|
|
456
|
+
void send_announce_to_closest_nodes(PendingSearch& search);
|
|
450
457
|
|
|
451
458
|
// Peer announcement storage management
|
|
452
459
|
void store_announced_peer(const InfoHash& info_hash, const Peer& peer);
|
|
@@ -1817,7 +1817,8 @@ bool RatsClient::find_peers_by_hash(const std::string& content_hash, std::functi
|
|
|
1817
1817
|
});
|
|
1818
1818
|
}
|
|
1819
1819
|
|
|
1820
|
-
bool RatsClient::announce_for_hash(const std::string& content_hash, uint16_t port
|
|
1820
|
+
bool RatsClient::announce_for_hash(const std::string& content_hash, uint16_t port,
|
|
1821
|
+
std::function<void(const std::vector<std::string>&)> callback) {
|
|
1821
1822
|
if (!dht_client_ || !dht_client_->is_running()) {
|
|
1822
1823
|
LOG_CLIENT_ERROR("DHT client not running");
|
|
1823
1824
|
return false;
|
|
@@ -1832,10 +1833,25 @@ bool RatsClient::announce_for_hash(const std::string& content_hash, uint16_t por
|
|
|
1832
1833
|
port = listen_port_;
|
|
1833
1834
|
}
|
|
1834
1835
|
|
|
1835
|
-
LOG_CLIENT_INFO("Announcing for content hash: " << content_hash << " on port " << port
|
|
1836
|
+
LOG_CLIENT_INFO("Announcing for content hash: " << content_hash << " on port " << port
|
|
1837
|
+
<< (callback ? " with peer callback" : ""));
|
|
1836
1838
|
|
|
1837
1839
|
InfoHash info_hash = hex_to_node_id(content_hash);
|
|
1838
|
-
|
|
1840
|
+
|
|
1841
|
+
// Create wrapper callback that converts Peer to string addresses (if callback provided)
|
|
1842
|
+
PeerDiscoveryCallback peer_callback = nullptr;
|
|
1843
|
+
if (callback) {
|
|
1844
|
+
peer_callback = [callback](const std::vector<Peer>& peers, const InfoHash& hash) {
|
|
1845
|
+
std::vector<std::string> peer_addresses;
|
|
1846
|
+
peer_addresses.reserve(peers.size());
|
|
1847
|
+
for (const auto& peer : peers) {
|
|
1848
|
+
peer_addresses.push_back(peer.ip + ":" + std::to_string(peer.port));
|
|
1849
|
+
}
|
|
1850
|
+
callback(peer_addresses);
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
return dht_client_->announce_peer(info_hash, port, peer_callback);
|
|
1839
1855
|
}
|
|
1840
1856
|
|
|
1841
1857
|
bool RatsClient::is_dht_running() const {
|
|
@@ -1931,46 +1947,23 @@ void RatsClient::automatic_discovery_loop() {
|
|
|
1931
1947
|
}
|
|
1932
1948
|
}
|
|
1933
1949
|
|
|
1934
|
-
//
|
|
1935
|
-
search_rats_peers();
|
|
1936
|
-
|
|
1937
|
-
{
|
|
1938
|
-
std::unique_lock<std::mutex> lock(shutdown_mutex_);
|
|
1939
|
-
if (shutdown_cv_.wait_for(lock, std::chrono::seconds(10), [this] { return !auto_discovery_running_.load() || !running_.load(); })) {
|
|
1940
|
-
LOG_CLIENT_INFO("Automatic peer discovery loop stopped during search delay");
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
// Announce immediately
|
|
1950
|
+
// Announce immediately - this also discovers peers during traversal
|
|
1946
1951
|
announce_rats_peer();
|
|
1947
1952
|
|
|
1948
1953
|
auto last_announce = std::chrono::steady_clock::now();
|
|
1949
|
-
auto last_search = std::chrono::steady_clock::now();
|
|
1950
1954
|
|
|
1951
1955
|
while (auto_discovery_running_.load()) {
|
|
1952
1956
|
auto now = std::chrono::steady_clock::now();
|
|
1953
1957
|
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
}
|
|
1964
|
-
} else {
|
|
1965
|
-
// Peers connected: less aggressive, similar to original logic
|
|
1966
|
-
if (now - last_search >= std::chrono::minutes(5)) {
|
|
1967
|
-
search_rats_peers();
|
|
1968
|
-
last_search = now;
|
|
1969
|
-
}
|
|
1970
|
-
if (now - last_announce >= std::chrono::minutes(10)) {
|
|
1971
|
-
announce_rats_peer();
|
|
1972
|
-
last_announce = now;
|
|
1973
|
-
}
|
|
1958
|
+
// Announce combines both announcing our presence and discovering peers
|
|
1959
|
+
// Adjust frequency based on whether we have peers
|
|
1960
|
+
auto interval = (get_peer_count() == 0)
|
|
1961
|
+
? std::chrono::seconds(15) // Aggressive when no peers
|
|
1962
|
+
: std::chrono::minutes(10); // Less aggressive when connected
|
|
1963
|
+
|
|
1964
|
+
if (now - last_announce >= interval) {
|
|
1965
|
+
announce_rats_peer();
|
|
1966
|
+
last_announce = now;
|
|
1974
1967
|
}
|
|
1975
1968
|
|
|
1976
1969
|
// Use conditional variable for responsive shutdown
|
|
@@ -1994,8 +1987,35 @@ void RatsClient::announce_rats_peer() {
|
|
|
1994
1987
|
std::string discovery_hash = get_discovery_hash();
|
|
1995
1988
|
LOG_CLIENT_INFO("Announcing peer for discovery hash: " << discovery_hash << " on port " << listen_port_);
|
|
1996
1989
|
|
|
1997
|
-
|
|
1998
|
-
|
|
1990
|
+
InfoHash info_hash = hex_to_node_id(discovery_hash);
|
|
1991
|
+
|
|
1992
|
+
if (dht_client_->is_announce_active(info_hash)) {
|
|
1993
|
+
LOG_CLIENT_WARN("Announce already in progress for info hash: " << node_id_to_hex(info_hash));
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// Use announce with callback - combines announce and find_peers in one traversal
|
|
1998
|
+
// Peers discovered during traversal will be returned through the callback
|
|
1999
|
+
if (announce_for_hash(discovery_hash, listen_port_, [this, info_hash](const std::vector<std::string>& peer_addresses) {
|
|
2000
|
+
LOG_CLIENT_INFO("Announce discovered " << peer_addresses.size() << " peers during traversal");
|
|
2001
|
+
|
|
2002
|
+
// Convert peer addresses to Peer objects for handle_dht_peer_discovery()
|
|
2003
|
+
std::vector<Peer> peers;
|
|
2004
|
+
peers.reserve(peer_addresses.size());
|
|
2005
|
+
for (const auto& peer_address : peer_addresses) {
|
|
2006
|
+
std::string ip;
|
|
2007
|
+
int port;
|
|
2008
|
+
if (parse_address_string(peer_address, ip, port)) {
|
|
2009
|
+
peers.push_back(Peer(ip, port));
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// Auto-connect to discovered peers
|
|
2014
|
+
if (!peers.empty()) {
|
|
2015
|
+
handle_dht_peer_discovery(peers, info_hash);
|
|
2016
|
+
}
|
|
2017
|
+
})) {
|
|
2018
|
+
LOG_CLIENT_DEBUG("Successfully started announce with peer discovery for discovery hash");
|
|
1999
2019
|
} else {
|
|
2000
2020
|
LOG_CLIENT_WARN("Failed to announce peer for discovery");
|
|
2001
2021
|
}
|
package/native-src/src/librats.h
CHANGED
|
@@ -606,12 +606,15 @@ public:
|
|
|
606
606
|
std::function<void(const std::vector<std::string>&)> callback);
|
|
607
607
|
|
|
608
608
|
/**
|
|
609
|
-
* Announce our presence for a content hash
|
|
609
|
+
* Announce our presence for a content hash with optional peer discovery callback
|
|
610
|
+
* If callback is provided, peers discovered during DHT traversal will be returned through it
|
|
610
611
|
* @param content_hash Hash to announce for (40-character hex string)
|
|
611
612
|
* @param port Port to announce (default: our listen port)
|
|
613
|
+
* @param callback Optional function to call with discovered peers during traversal
|
|
612
614
|
* @return true if announced successfully
|
|
613
615
|
*/
|
|
614
|
-
bool announce_for_hash(const std::string& content_hash, uint16_t port = 0
|
|
616
|
+
bool announce_for_hash(const std::string& content_hash, uint16_t port = 0,
|
|
617
|
+
std::function<void(const std::vector<std::string>&)> callback = nullptr);
|
|
615
618
|
|
|
616
619
|
/**
|
|
617
620
|
* Check if DHT is currently running
|
|
@@ -386,13 +386,29 @@ int rats_is_dht_running(rats_client_t handle) {
|
|
|
386
386
|
return wrap->client->is_dht_running() ? 1 : 0;
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
rats_error_t rats_announce_for_hash(rats_client_t handle, const char* content_hash, int port
|
|
389
|
+
rats_error_t rats_announce_for_hash(rats_client_t handle, const char* content_hash, int port,
|
|
390
|
+
rats_peers_found_cb callback, void* user_data) {
|
|
390
391
|
if (!handle || !content_hash) return RATS_ERROR_INVALID_PARAMETER;
|
|
391
392
|
if (strlen(content_hash) != 40) return RATS_ERROR_INVALID_PARAMETER; // SHA1 hash must be 40 chars
|
|
392
393
|
rats_client_wrapper* wrap = static_cast<rats_client_wrapper*>(handle);
|
|
393
394
|
|
|
394
395
|
uint16_t announce_port = (port <= 0) ? 0 : static_cast<uint16_t>(port);
|
|
395
|
-
|
|
396
|
+
|
|
397
|
+
// Create C++ callback wrapper if C callback is provided
|
|
398
|
+
std::function<void(const std::vector<std::string>&)> cpp_callback = nullptr;
|
|
399
|
+
if (callback) {
|
|
400
|
+
cpp_callback = [callback, user_data](const std::vector<std::string>& peers) {
|
|
401
|
+
// Convert vector to C-style array
|
|
402
|
+
std::vector<const char*> c_peers;
|
|
403
|
+
c_peers.reserve(peers.size());
|
|
404
|
+
for (const auto& peer : peers) {
|
|
405
|
+
c_peers.push_back(peer.c_str());
|
|
406
|
+
}
|
|
407
|
+
callback(user_data, c_peers.data(), static_cast<int>(c_peers.size()));
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return wrap->client->announce_for_hash(std::string(content_hash), announce_port, cpp_callback) ?
|
|
396
412
|
RATS_SUCCESS : RATS_ERROR_OPERATION_FAILED;
|
|
397
413
|
}
|
|
398
414
|
|
|
@@ -82,6 +82,7 @@ typedef void (*rats_json_cb)(void* user_data, const char* peer_id, const char* j
|
|
|
82
82
|
typedef void (*rats_disconnect_cb)(void* user_data, const char* peer_id);
|
|
83
83
|
typedef void (*rats_peer_discovered_cb)(void* user_data, const char* host, int port, const char* service_name);
|
|
84
84
|
typedef void (*rats_message_cb)(void* user_data, const char* peer_id, const char* message_data);
|
|
85
|
+
typedef void (*rats_peers_found_cb)(void* user_data, const char** peer_addresses, int count);
|
|
85
86
|
|
|
86
87
|
// Peer configuration
|
|
87
88
|
RATS_API rats_error_t rats_set_max_peers(rats_client_t client, int max_peers);
|
|
@@ -106,7 +107,8 @@ RATS_API int rats_broadcast_json(rats_client_t client, const char* json_str);
|
|
|
106
107
|
RATS_API rats_error_t rats_start_dht_discovery(rats_client_t client, int dht_port);
|
|
107
108
|
RATS_API void rats_stop_dht_discovery(rats_client_t client);
|
|
108
109
|
RATS_API int rats_is_dht_running(rats_client_t client);
|
|
109
|
-
RATS_API rats_error_t rats_announce_for_hash(rats_client_t client, const char* content_hash, int port
|
|
110
|
+
RATS_API rats_error_t rats_announce_for_hash(rats_client_t client, const char* content_hash, int port,
|
|
111
|
+
rats_peers_found_cb callback, void* user_data);
|
|
110
112
|
RATS_API size_t rats_get_dht_routing_table_size(rats_client_t client);
|
|
111
113
|
|
|
112
114
|
// Automatic discovery
|
package/package.json
CHANGED
package/src/librats_node.cpp
CHANGED
|
@@ -473,7 +473,52 @@ private:
|
|
|
473
473
|
std::string content_hash = info[0].As<Napi::String>().Utf8Value();
|
|
474
474
|
int port = info[1].As<Napi::Number>().Int32Value();
|
|
475
475
|
|
|
476
|
-
|
|
476
|
+
// Check if optional callback is provided
|
|
477
|
+
rats_peers_found_cb c_callback = nullptr;
|
|
478
|
+
void* callback_user_data = nullptr;
|
|
479
|
+
Napi::ThreadSafeFunction* tsfn_ptr = nullptr;
|
|
480
|
+
|
|
481
|
+
if (info.Length() >= 3 && info[2].IsFunction()) {
|
|
482
|
+
// Create thread-safe function for callback
|
|
483
|
+
auto tsfn = new Napi::ThreadSafeFunction();
|
|
484
|
+
*tsfn = Napi::ThreadSafeFunction::New(
|
|
485
|
+
env,
|
|
486
|
+
info[2].As<Napi::Function>(),
|
|
487
|
+
"AnnounceForHashCallback",
|
|
488
|
+
0,
|
|
489
|
+
1,
|
|
490
|
+
[](Napi::Env) {} // Release callback
|
|
491
|
+
);
|
|
492
|
+
tsfn_ptr = tsfn;
|
|
493
|
+
|
|
494
|
+
c_callback = [](void* user_data, const char** peer_addresses, int count) {
|
|
495
|
+
auto* tsfn = static_cast<Napi::ThreadSafeFunction*>(user_data);
|
|
496
|
+
if (!tsfn) return;
|
|
497
|
+
|
|
498
|
+
// Copy peer addresses for async callback
|
|
499
|
+
std::vector<std::string>* peers = new std::vector<std::string>();
|
|
500
|
+
for (int i = 0; i < count; i++) {
|
|
501
|
+
if (peer_addresses[i]) {
|
|
502
|
+
peers->push_back(peer_addresses[i]);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
tsfn->BlockingCall(peers, [](Napi::Env env, Napi::Function jsCallback, std::vector<std::string>* data) {
|
|
507
|
+
Napi::Array arr = Napi::Array::New(env, data->size());
|
|
508
|
+
for (size_t i = 0; i < data->size(); i++) {
|
|
509
|
+
arr.Set(static_cast<uint32_t>(i), Napi::String::New(env, (*data)[i]));
|
|
510
|
+
}
|
|
511
|
+
jsCallback.Call({arr});
|
|
512
|
+
delete data;
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
tsfn->Release();
|
|
516
|
+
delete tsfn;
|
|
517
|
+
};
|
|
518
|
+
callback_user_data = tsfn_ptr;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
rats_error_t result = rats_announce_for_hash(client_, content_hash.c_str(), port, c_callback, callback_user_data);
|
|
477
522
|
return Napi::Boolean::New(env, result == RATS_SUCCESS);
|
|
478
523
|
}
|
|
479
524
|
|