librats 0.7.0 → 0.7.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/lib/index.d.ts +15 -0
- package/native-src/CMakeLists.txt +5 -0
- package/native-src/src/bt_client.cpp +18 -5
- package/native-src/src/bt_client.h +3 -1
- package/native-src/src/bt_network.cpp +1 -1
- package/native-src/src/bt_peer_connection.cpp +11 -0
- package/native-src/src/bt_peer_connection.h +6 -0
- package/native-src/src/bt_torrent.cpp +17 -6
- package/native-src/src/bt_torrent.h +1 -0
- package/native-src/src/bt_types.h +287 -20
- package/native-src/src/dht.cpp +26 -8
- package/native-src/src/dht.h +1 -1
- package/native-src/src/ice.cpp +1 -1
- package/native-src/src/krpc.cpp +8 -1
- package/native-src/src/krpc.h +3 -2
- package/native-src/src/librats.cpp +162 -74
- package/native-src/src/librats.h +43 -6
- package/native-src/src/librats_bittorrent.cpp +3 -2
- package/native-src/src/librats_c.cpp +20 -0
- package/native-src/src/librats_c.h +4 -0
- package/native-src/src/librats_encryption.cpp +5 -0
- package/native-src/src/librats_logging.cpp +21 -0
- package/native-src/src/logger.h +64 -12
- package/native-src/src/network_utils.cpp +175 -373
- package/native-src/src/network_utils.h +2 -113
- package/native-src/src/socket.cpp +485 -883
- package/native-src/src/socket.h +36 -130
- package/native-src/src/stun.cpp +3 -5
- package/native-src/src/tracker.cpp +3 -2
- package/native-src/src/turn.cpp +4 -6
- package/package.json +1 -1
- package/src/librats_node.cpp +23 -0
package/lib/index.d.ts
CHANGED
|
@@ -696,3 +696,18 @@ export function getGitDescribe(): string;
|
|
|
696
696
|
*/
|
|
697
697
|
export function getAbi(): number;
|
|
698
698
|
|
|
699
|
+
// ============ Logging Control Functions ============
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Enable or disable console logging.
|
|
703
|
+
* When disabled, log messages will not be printed to stdout/stderr.
|
|
704
|
+
* File logging (if enabled) will still work.
|
|
705
|
+
* @param enabled - Whether to enable console logging (default: true)
|
|
706
|
+
*/
|
|
707
|
+
export function setConsoleLoggingEnabled(enabled: boolean): void;
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Check if console logging is currently enabled.
|
|
711
|
+
* @returns true if console logging is enabled
|
|
712
|
+
*/
|
|
713
|
+
export function isConsoleLoggingEnabled(): boolean;
|
|
@@ -326,7 +326,8 @@ Torrent::Ptr BtClient::add_torrent(const TorrentInfo& info,
|
|
|
326
326
|
}
|
|
327
327
|
|
|
328
328
|
Torrent::Ptr BtClient::add_magnet(const std::string& magnet_uri,
|
|
329
|
-
const std::string& save_path
|
|
329
|
+
const std::string& save_path,
|
|
330
|
+
bool skip_dht_search) {
|
|
330
331
|
auto info = TorrentInfo::from_magnet(magnet_uri);
|
|
331
332
|
if (!info) {
|
|
332
333
|
if (on_alert_) {
|
|
@@ -345,7 +346,14 @@ Torrent::Ptr BtClient::add_magnet(const std::string& magnet_uri,
|
|
|
345
346
|
|
|
346
347
|
// Create torrent without full metadata
|
|
347
348
|
TorrentConfig torrent_config;
|
|
348
|
-
|
|
349
|
+
// For metadata-only mode (skip_dht_search=true with empty save_path), keep save_path empty
|
|
350
|
+
// Otherwise use default download path if not specified
|
|
351
|
+
if (skip_dht_search && save_path.empty()) {
|
|
352
|
+
// Metadata-only mode - keep empty save_path for proper detection
|
|
353
|
+
torrent_config.save_path = "";
|
|
354
|
+
} else {
|
|
355
|
+
torrent_config.save_path = save_path.empty() ? config_.download_path : save_path;
|
|
356
|
+
}
|
|
349
357
|
torrent_config.resume_data_path = config_.resume_data_path;
|
|
350
358
|
torrent_config.max_connections = config_.max_connections_per_torrent;
|
|
351
359
|
torrent_config.max_uploads = config_.max_uploads;
|
|
@@ -362,8 +370,8 @@ Torrent::Ptr BtClient::add_magnet(const std::string& magnet_uri,
|
|
|
362
370
|
if (running_) {
|
|
363
371
|
torrent->start();
|
|
364
372
|
|
|
365
|
-
// Find peers via DHT for metadata
|
|
366
|
-
if (dht_running_) {
|
|
373
|
+
// Find peers via DHT for metadata (skip if using direct peer connection)
|
|
374
|
+
if (dht_running_ && !skip_dht_search) {
|
|
367
375
|
find_peers_dht(info->info_hash());
|
|
368
376
|
}
|
|
369
377
|
}
|
|
@@ -891,6 +899,9 @@ void BtClient::announce_torrents_to_trackers() {
|
|
|
891
899
|
for (auto& [hash, torrent] : torrents_) {
|
|
892
900
|
if (!torrent->is_active()) continue;
|
|
893
901
|
|
|
902
|
+
// Skip metadata-only torrents (empty save_path means only fetching metadata)
|
|
903
|
+
if (torrent->save_path().empty()) continue;
|
|
904
|
+
|
|
894
905
|
auto tracker_it = tracker_managers_.find(hash);
|
|
895
906
|
if (tracker_it == tracker_managers_.end()) continue;
|
|
896
907
|
|
|
@@ -993,12 +1004,14 @@ void BtClient::tick_loop() {
|
|
|
993
1004
|
}
|
|
994
1005
|
|
|
995
1006
|
// Periodic DHT announces (every 15 minutes)
|
|
1007
|
+
// Skip metadata-only torrents (empty save_path) to avoid unnecessary network traffic
|
|
996
1008
|
auto dht_elapsed = std::chrono::duration_cast<std::chrono::minutes>(
|
|
997
1009
|
now - last_dht_announce_).count();
|
|
998
1010
|
if (dht_elapsed >= 15 && dht_running_) {
|
|
999
1011
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
1000
1012
|
for (auto& [hash, torrent] : torrents_) {
|
|
1001
|
-
|
|
1013
|
+
// Skip metadata-only torrents (empty save_path means only fetching metadata)
|
|
1014
|
+
if (torrent->is_active() && !torrent->save_path().empty()) {
|
|
1002
1015
|
announce_to_dht(hash);
|
|
1003
1016
|
}
|
|
1004
1017
|
}
|
|
@@ -170,10 +170,12 @@ public:
|
|
|
170
170
|
*
|
|
171
171
|
* @param magnet_uri Magnet URI
|
|
172
172
|
* @param save_path Directory to save files
|
|
173
|
+
* @param skip_dht_search If true, don't search DHT for peers (for metadata-only from specific peer)
|
|
173
174
|
* @return Torrent pointer, or nullptr on failure
|
|
174
175
|
*/
|
|
175
176
|
Torrent::Ptr add_magnet(const std::string& magnet_uri,
|
|
176
|
-
const std::string& save_path = ""
|
|
177
|
+
const std::string& save_path = "",
|
|
178
|
+
bool skip_dht_search = false);
|
|
177
179
|
|
|
178
180
|
/**
|
|
179
181
|
* @brief Add a torrent with resume data for fast resume
|
|
@@ -69,7 +69,7 @@ bool BtNetworkManager::start() {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Get actual port (in case 0 was specified)
|
|
72
|
-
actual_listen_port_ = static_cast<uint16_t>(
|
|
72
|
+
actual_listen_port_ = static_cast<uint16_t>(get_bound_port(listen_socket_));
|
|
73
73
|
if (actual_listen_port_ == 0) {
|
|
74
74
|
actual_listen_port_ = config_.listen_port;
|
|
75
75
|
}
|
|
@@ -358,6 +358,9 @@ void BtPeerConnection::process_handshake() {
|
|
|
358
358
|
|
|
359
359
|
handshake_received_ = true;
|
|
360
360
|
|
|
361
|
+
// Identify client from peer_id
|
|
362
|
+
peer_client_name_ = identify_client(peer_id_);
|
|
363
|
+
|
|
361
364
|
// Format peer_id for logging (first 8 chars, printable only)
|
|
362
365
|
std::string peer_id_str;
|
|
363
366
|
for (size_t i = 0; i < 8 && i < peer_id_.size(); ++i) {
|
|
@@ -366,6 +369,7 @@ void BtPeerConnection::process_handshake() {
|
|
|
366
369
|
}
|
|
367
370
|
|
|
368
371
|
LOG_INFO("BtPeerConn", "Handshake received from " + ip_ + ", peer_id=" + peer_id_str +
|
|
372
|
+
", client=" + peer_client_name_ +
|
|
369
373
|
", ext_protocol=" + (hs->extensions.extension_protocol ? "yes" : "no") +
|
|
370
374
|
", fast=" + (hs->extensions.fast ? "yes" : "no"));
|
|
371
375
|
|
|
@@ -604,6 +608,13 @@ void BtPeerConnection::parse_extension_handshake(const std::vector<uint8_t>& pay
|
|
|
604
608
|
std::to_string(peer_ut_metadata_id_));
|
|
605
609
|
}
|
|
606
610
|
}
|
|
611
|
+
|
|
612
|
+
// Extract client version string from 'v' field
|
|
613
|
+
auto v_it = dict.find("v");
|
|
614
|
+
if (v_it != dict.end() && v_it->second.is_string()) {
|
|
615
|
+
peer_client_name_ = v_it->second.as_string();
|
|
616
|
+
LOG_INFO("BtPeerConn", "Peer " + ip_ + " client: " + peer_client_name_);
|
|
617
|
+
}
|
|
607
618
|
}
|
|
608
619
|
|
|
609
620
|
void BtPeerConnection::queue_send(const std::vector<uint8_t>& data) {
|
|
@@ -355,6 +355,11 @@ public:
|
|
|
355
355
|
*/
|
|
356
356
|
uint8_t peer_ut_metadata_id() const { return peer_ut_metadata_id_; }
|
|
357
357
|
|
|
358
|
+
/**
|
|
359
|
+
* @brief Get peer's client name (from extension handshake "v" field, or identified from peer_id)
|
|
360
|
+
*/
|
|
361
|
+
const std::string& peer_client_name() const { return peer_client_name_; }
|
|
362
|
+
|
|
358
363
|
//=========================================================================
|
|
359
364
|
// Sending Messages
|
|
360
365
|
//=========================================================================
|
|
@@ -549,6 +554,7 @@ private:
|
|
|
549
554
|
bool extension_handshake_received_;
|
|
550
555
|
size_t peer_metadata_size_;
|
|
551
556
|
uint8_t peer_ut_metadata_id_;
|
|
557
|
+
std::string peer_client_name_; ///< Client name (from "v" field or peer_id identification)
|
|
552
558
|
|
|
553
559
|
// Protocol state
|
|
554
560
|
bool am_choking_;
|
|
@@ -135,7 +135,7 @@ Torrent::Torrent(const BtInfoHash& info_hash,
|
|
|
135
135
|
, have_pieces_(0) {
|
|
136
136
|
|
|
137
137
|
LOG_INFO("Torrent", "Creating torrent '" + name + "' from magnet link (no metadata yet), info_hash=" +
|
|
138
|
-
info_hash_to_hex(info_hash)
|
|
138
|
+
info_hash_to_hex(info_hash));
|
|
139
139
|
|
|
140
140
|
// No picker until we have metadata
|
|
141
141
|
|
|
@@ -233,6 +233,7 @@ void Torrent::stop() {
|
|
|
233
233
|
// Clear metadata exchange state
|
|
234
234
|
metadata_buffer_.clear();
|
|
235
235
|
metadata_pieces_received_.clear();
|
|
236
|
+
metadata_pieces_requested_.clear();
|
|
236
237
|
peer_metadata_size_.clear();
|
|
237
238
|
peer_ut_metadata_id_.clear();
|
|
238
239
|
|
|
@@ -725,6 +726,7 @@ void Torrent::tick() {
|
|
|
725
726
|
// Clear metadata state
|
|
726
727
|
metadata_buffer_.clear();
|
|
727
728
|
metadata_pieces_received_.clear();
|
|
729
|
+
metadata_pieces_requested_.clear();
|
|
728
730
|
peer_metadata_size_.clear();
|
|
729
731
|
peer_ut_metadata_id_.clear();
|
|
730
732
|
|
|
@@ -778,8 +780,9 @@ void Torrent::tick() {
|
|
|
778
780
|
//=============================================================================
|
|
779
781
|
|
|
780
782
|
void Torrent::on_peer_connected(BtPeerConnection* peer) {
|
|
781
|
-
LOG_DEBUG("Torrent", "Peer " + peer->ip() + " connected
|
|
782
|
-
(
|
|
783
|
+
LOG_DEBUG("Torrent", "Peer " + peer->ip() + " connected" +
|
|
784
|
+
(peer->peer_client_name().empty() ? "" : " [" + peer->peer_client_name() + "]") +
|
|
785
|
+
", has_metadata=" + (has_metadata_unlocked() ? "true" : "false"));
|
|
783
786
|
|
|
784
787
|
// Send extension handshake if peer supports it
|
|
785
788
|
if (peer->peer_extensions().extension_protocol) {
|
|
@@ -1506,11 +1509,15 @@ void Torrent::request_metadata(BtPeerConnection* peer) {
|
|
|
1506
1509
|
if (metadata_buffer_.empty()) {
|
|
1507
1510
|
metadata_buffer_.resize(metadata_size);
|
|
1508
1511
|
metadata_pieces_received_.resize(num_pieces, false);
|
|
1512
|
+
metadata_pieces_requested_.resize(num_pieces, false);
|
|
1509
1513
|
}
|
|
1514
|
+
|
|
1515
|
+
// Request pieces we don't have yet (pipeline up to 4 requests)
|
|
1516
|
+
int requests_sent = 0;
|
|
1517
|
+
const int max_pending_requests = 4;
|
|
1510
1518
|
|
|
1511
|
-
// Request pieces we don't have yet
|
|
1512
1519
|
for (uint32_t piece = 0; piece < num_pieces; ++piece) {
|
|
1513
|
-
if (!metadata_pieces_received_[piece]) {
|
|
1520
|
+
if (!metadata_pieces_received_[piece] && !metadata_pieces_requested_[piece]) {
|
|
1514
1521
|
// Create request message
|
|
1515
1522
|
BencodeValue req = BencodeValue::create_dict();
|
|
1516
1523
|
req["msg_type"] = BencodeValue(static_cast<int64_t>(0)); // Request
|
|
@@ -1518,10 +1525,11 @@ void Torrent::request_metadata(BtPeerConnection* peer) {
|
|
|
1518
1525
|
|
|
1519
1526
|
auto req_payload = req.encode();
|
|
1520
1527
|
peer->send_extended(peer_ut_id, req_payload);
|
|
1528
|
+
metadata_pieces_requested_[piece] = true;
|
|
1521
1529
|
|
|
1522
1530
|
LOG_DEBUG("Torrent", "Requesting metadata piece " + std::to_string(piece) +
|
|
1523
1531
|
" from " + peer->ip());
|
|
1524
|
-
|
|
1532
|
+
if (++requests_sent >= max_pending_requests) break;
|
|
1525
1533
|
}
|
|
1526
1534
|
}
|
|
1527
1535
|
}
|
|
@@ -1580,6 +1588,7 @@ void Torrent::on_metadata_message(BtPeerConnection* peer,
|
|
|
1580
1588
|
|
|
1581
1589
|
if (piece < metadata_pieces_received_.size()) {
|
|
1582
1590
|
metadata_pieces_received_[piece] = true;
|
|
1591
|
+
metadata_pieces_requested_[piece] = false;
|
|
1583
1592
|
}
|
|
1584
1593
|
|
|
1585
1594
|
LOG_DEBUG("Torrent", "Received metadata piece " + std::to_string(piece) +
|
|
@@ -1647,6 +1656,7 @@ void Torrent::on_metadata_complete() {
|
|
|
1647
1656
|
// Clear metadata tracking data
|
|
1648
1657
|
metadata_buffer_.clear();
|
|
1649
1658
|
metadata_pieces_received_.clear();
|
|
1659
|
+
metadata_pieces_requested_.clear();
|
|
1650
1660
|
peer_metadata_size_.clear();
|
|
1651
1661
|
peer_ut_metadata_id_.clear();
|
|
1652
1662
|
|
|
@@ -1672,6 +1682,7 @@ void Torrent::on_metadata_complete() {
|
|
|
1672
1682
|
// Could retry from other peers
|
|
1673
1683
|
metadata_buffer_.clear();
|
|
1674
1684
|
metadata_pieces_received_.clear();
|
|
1685
|
+
metadata_pieces_requested_.clear();
|
|
1675
1686
|
}
|
|
1676
1687
|
}
|
|
1677
1688
|
|
|
@@ -631,6 +631,7 @@ private:
|
|
|
631
631
|
// Metadata exchange (for magnet links)
|
|
632
632
|
std::vector<uint8_t> metadata_buffer_;
|
|
633
633
|
std::vector<bool> metadata_pieces_received_;
|
|
634
|
+
std::vector<bool> metadata_pieces_requested_;
|
|
634
635
|
std::unordered_map<BtPeerConnection*, size_t> peer_metadata_size_;
|
|
635
636
|
std::unordered_map<BtPeerConnection*, uint8_t> peer_ut_metadata_id_;
|
|
636
637
|
std::chrono::steady_clock::time_point metadata_download_started_;
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
#include <array>
|
|
12
12
|
#include <cstdint>
|
|
13
|
+
#include <cstring>
|
|
14
|
+
#include <cstdio>
|
|
13
15
|
#include <string>
|
|
14
16
|
#include <random>
|
|
15
17
|
#include <chrono>
|
|
@@ -270,39 +272,304 @@ inline bool is_zero_hash(const BtInfoHash& hash) {
|
|
|
270
272
|
}
|
|
271
273
|
|
|
272
274
|
//=============================================================================
|
|
273
|
-
// Client ID Parsing
|
|
275
|
+
// Client ID Parsing (BEP 20)
|
|
274
276
|
//=============================================================================
|
|
275
277
|
|
|
278
|
+
namespace detail {
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @brief Decode a version digit from peer ID (supports 0-9 and A-Z)
|
|
282
|
+
*/
|
|
283
|
+
inline int decode_version_digit(uint8_t c) {
|
|
284
|
+
if (c >= '0' && c <= '9') return c - '0';
|
|
285
|
+
if (c >= 'A' && c <= 'Z') return c - 'A' + 10;
|
|
286
|
+
if (c >= 'a' && c <= 'z') return c - 'a' + 10;
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
276
290
|
/**
|
|
277
|
-
* @brief
|
|
291
|
+
* @brief Azureus-style client name lookup table
|
|
292
|
+
* Based on BEP 20 and libtorrent's identify_client implementation
|
|
293
|
+
*/
|
|
294
|
+
inline const char* lookup_az_client(const char* code) {
|
|
295
|
+
// Sorted alphabetically by 2-char code for binary search potential
|
|
296
|
+
// Using linear search for simplicity - table is small enough
|
|
297
|
+
struct Entry { const char* code; const char* name; };
|
|
298
|
+
static const Entry entries[] = {
|
|
299
|
+
{"7T", "aTorrent"},
|
|
300
|
+
{"AB", "AnyEvent BitTorrent"},
|
|
301
|
+
{"AG", "Ares"},
|
|
302
|
+
{"AR", "Arctic Torrent"},
|
|
303
|
+
{"AT", "Artemis"},
|
|
304
|
+
{"AV", "Avicora"},
|
|
305
|
+
{"AX", "BitPump"},
|
|
306
|
+
{"AZ", "Azureus"},
|
|
307
|
+
{"A~", "Ares"},
|
|
308
|
+
{"BB", "BitBuddy"},
|
|
309
|
+
{"BC", "BitComet"},
|
|
310
|
+
{"BE", "baretorrent"},
|
|
311
|
+
{"BF", "Bitflu"},
|
|
312
|
+
{"BG", "BTG"},
|
|
313
|
+
{"BI", "BiglyBT"},
|
|
314
|
+
{"BL", "BitBlinder"},
|
|
315
|
+
{"BP", "BitTorrent Pro"},
|
|
316
|
+
{"BR", "BitRocket"},
|
|
317
|
+
{"BS", "BTSlave"},
|
|
318
|
+
{"BT", "BitTorrent"},
|
|
319
|
+
{"BU", "BigUp"},
|
|
320
|
+
{"BW", "BitWombat"},
|
|
321
|
+
{"BX", "BittorrentX"},
|
|
322
|
+
{"CD", "Enhanced CTorrent"},
|
|
323
|
+
{"CT", "CTorrent"},
|
|
324
|
+
{"DE", "Deluge"},
|
|
325
|
+
{"DP", "Propagate Data Client"},
|
|
326
|
+
{"EB", "EBit"},
|
|
327
|
+
{"ES", "electric sheep"},
|
|
328
|
+
{"FC", "FileCroc"},
|
|
329
|
+
{"FT", "FoxTorrent"},
|
|
330
|
+
{"FW", "FrostWire"},
|
|
331
|
+
{"FX", "Freebox BitTorrent"},
|
|
332
|
+
{"GS", "GSTorrent"},
|
|
333
|
+
{"HK", "Hekate"},
|
|
334
|
+
{"HL", "Halite"},
|
|
335
|
+
{"HN", "Hydranode"},
|
|
336
|
+
{"IL", "iLivid"},
|
|
337
|
+
{"KC", "Koinonein"},
|
|
338
|
+
{"KG", "KGet"},
|
|
339
|
+
{"KT", "KTorrent"},
|
|
340
|
+
{"LC", "LeechCraft"},
|
|
341
|
+
{"LH", "LH-ABC"},
|
|
342
|
+
{"LK", "Linkage"},
|
|
343
|
+
{"LP", "lphant"},
|
|
344
|
+
{"LR", "librats"},
|
|
345
|
+
{"LT", "libtorrent"},
|
|
346
|
+
{"LW", "Limewire"},
|
|
347
|
+
{"ML", "MLDonkey"},
|
|
348
|
+
{"MO", "Mono Torrent"},
|
|
349
|
+
{"MP", "MooPolice"},
|
|
350
|
+
{"MR", "Miro"},
|
|
351
|
+
{"MT", "Moonlight Torrent"},
|
|
352
|
+
{"NX", "Net Transport"},
|
|
353
|
+
{"OS", "OneSwarm"},
|
|
354
|
+
{"OT", "OmegaTorrent"},
|
|
355
|
+
{"PD", "Pando"},
|
|
356
|
+
{"QD", "QQDownload"},
|
|
357
|
+
{"QT", "Qt 4"},
|
|
358
|
+
{"RT", "Retriever"},
|
|
359
|
+
{"RZ", "RezTorrent"},
|
|
360
|
+
{"SB", "Swiftbit"},
|
|
361
|
+
{"SD", "Xunlei"},
|
|
362
|
+
{"SK", "spark"},
|
|
363
|
+
{"SN", "ShareNet"},
|
|
364
|
+
{"SS", "SwarmScope"},
|
|
365
|
+
{"ST", "SymTorrent"},
|
|
366
|
+
{"SZ", "Shareaza"},
|
|
367
|
+
{"S~", "Shareaza (beta)"},
|
|
368
|
+
{"TB", "Torch"},
|
|
369
|
+
{"TL", "Tribler"},
|
|
370
|
+
{"TN", "Torrent.NET"},
|
|
371
|
+
{"TR", "Transmission"},
|
|
372
|
+
{"TS", "TorrentStorm"},
|
|
373
|
+
{"TT", "TuoTu"},
|
|
374
|
+
{"UL", "uLeecher"},
|
|
375
|
+
{"UM", "uTorrent Mac"},
|
|
376
|
+
{"UT", "uTorrent"},
|
|
377
|
+
{"VG", "Vagaa"},
|
|
378
|
+
{"WT", "BitLet"},
|
|
379
|
+
{"WY", "FireTorrent"},
|
|
380
|
+
{"XF", "Xfplay"},
|
|
381
|
+
{"XL", "Xunlei"},
|
|
382
|
+
{"XS", "XSwifter"},
|
|
383
|
+
{"XT", "XanTorrent"},
|
|
384
|
+
{"XX", "Xtorrent"},
|
|
385
|
+
{"ZO", "Zona"},
|
|
386
|
+
{"ZT", "ZipTorrent"},
|
|
387
|
+
{"lt", "rTorrent"},
|
|
388
|
+
{"pX", "pHoeniX"},
|
|
389
|
+
{"qB", "qBittorrent"},
|
|
390
|
+
{"st", "SharkTorrent"},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
for (const auto& e : entries) {
|
|
394
|
+
if (e.code[0] == code[0] && e.code[1] == code[1]) {
|
|
395
|
+
return e.name;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return nullptr;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* @brief Generic (non-standard) client name lookup
|
|
403
|
+
*/
|
|
404
|
+
inline const char* lookup_generic_client(const uint8_t* id) {
|
|
405
|
+
struct Entry { int offset; const char* pattern; const char* name; };
|
|
406
|
+
static const Entry entries[] = {
|
|
407
|
+
{0, "Deadman Walking-", "Deadman"},
|
|
408
|
+
{5, "Azureus", "Azureus 2.0.3.2"},
|
|
409
|
+
{0, "DansClient", "XanTorrent"},
|
|
410
|
+
{4, "btfans", "SimpleBT"},
|
|
411
|
+
{0, "PRC.P---", "Bittorrent Plus! II"},
|
|
412
|
+
{0, "P87.P---", "Bittorrent Plus!"},
|
|
413
|
+
{0, "S587Plus", "Bittorrent Plus!"},
|
|
414
|
+
{0, "martini", "Martini Man"},
|
|
415
|
+
{0, "Plus---", "Bittorrent Plus"},
|
|
416
|
+
{0, "turbobt", "TurboBT"},
|
|
417
|
+
{0, "a00---0", "Swarmy"},
|
|
418
|
+
{0, "a02---0", "Swarmy"},
|
|
419
|
+
{0, "T00---0", "Teeweety"},
|
|
420
|
+
{0, "BTDWV-", "Deadman Walking"},
|
|
421
|
+
{2, "BS", "BitSpirit"},
|
|
422
|
+
{0, "-SP", "BitSpirit 3.6"},
|
|
423
|
+
{0, "Pando-", "Pando"},
|
|
424
|
+
{0, "LIME", "LimeWire"},
|
|
425
|
+
{0, "btuga", "BTugaXP"},
|
|
426
|
+
{0, "oernu", "BTugaXP"},
|
|
427
|
+
{0, "Mbrst", "Burst!"},
|
|
428
|
+
{0, "PEERAPP", "PeerApp"},
|
|
429
|
+
{0, "Plus", "Plus!"},
|
|
430
|
+
{0, "-Qt-", "Qt"},
|
|
431
|
+
{0, "exbc", "BitComet"},
|
|
432
|
+
{0, "DNA", "BitTorrent DNA"},
|
|
433
|
+
{0, "-G3", "G3 Torrent"},
|
|
434
|
+
{0, "-FG", "FlashGet"},
|
|
435
|
+
{0, "-ML", "MLdonkey"},
|
|
436
|
+
{0, "-MG", "Media Get"},
|
|
437
|
+
{0, "XBT", "XBT"},
|
|
438
|
+
{0, "OP", "Opera"},
|
|
439
|
+
{2, "RS", "Rufus"},
|
|
440
|
+
{0, "AZ2500BT", "BitTyrant"},
|
|
441
|
+
{0, "btpd/", "BitTorrent Protocol Daemon"},
|
|
442
|
+
{0, "TIX", "Tixati"},
|
|
443
|
+
{0, "QVOD", "Qvod"},
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
for (const auto& e : entries) {
|
|
447
|
+
const char* p = e.pattern;
|
|
448
|
+
const char* s = reinterpret_cast<const char*>(id) + e.offset;
|
|
449
|
+
size_t len = std::strlen(p);
|
|
450
|
+
if (e.offset + len <= BT_PEER_ID_SIZE && std::memcmp(s, p, len) == 0) {
|
|
451
|
+
return e.name;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return nullptr;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
} // namespace detail
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* @brief Identify BitTorrent client from peer ID (BEP 20)
|
|
278
461
|
*
|
|
279
|
-
*
|
|
462
|
+
* Supports three encoding styles:
|
|
463
|
+
* - Azureus-style: -XX1234-xxxxxxxxxxxx
|
|
464
|
+
* - Shadow-style: Xyyy--xxxxxxxxxxxxxx
|
|
465
|
+
* - Mainline-style: M1-2-3--xxxxxxxxxxxx
|
|
466
|
+
* Also recognizes many non-standard encodings.
|
|
280
467
|
*
|
|
281
|
-
* @param id
|
|
282
|
-
* @return
|
|
468
|
+
* @param id 20-byte peer ID
|
|
469
|
+
* @return Human-readable client name with version, or "Unknown [...]"
|
|
283
470
|
*/
|
|
284
471
|
inline std::string identify_client(const PeerID& id) {
|
|
472
|
+
// Check if all zeros
|
|
473
|
+
bool all_zero = true;
|
|
474
|
+
for (uint8_t b : id) { if (b != 0) { all_zero = false; break; } }
|
|
475
|
+
if (all_zero) return "Unknown";
|
|
476
|
+
|
|
477
|
+
// Check non-standard encodings first
|
|
478
|
+
const char* generic = detail::lookup_generic_client(id.data());
|
|
479
|
+
if (generic) return generic;
|
|
480
|
+
|
|
481
|
+
// Check Bits on Wheels special case
|
|
482
|
+
if (id[0] == '-' && id[1] == 'B' && id[2] == 'O' && id[3] == 'W' && id[7] == '-') {
|
|
483
|
+
return "Bits on Wheels " + std::string(reinterpret_cast<const char*>(id.data()) + 4, 3);
|
|
484
|
+
}
|
|
485
|
+
|
|
285
486
|
// Azureus-style: -XX1234-
|
|
286
|
-
if (id[0] == '-' && id[7] == '-'
|
|
287
|
-
|
|
288
|
-
|
|
487
|
+
if (id[0] == '-' && id[7] == '-' &&
|
|
488
|
+
(id[1] >= 'A' || id[1] >= 'a') &&
|
|
489
|
+
id[3] >= '0' && id[4] >= '0' && id[5] >= '0' && id[6] >= '0') {
|
|
490
|
+
|
|
491
|
+
char code[3] = {static_cast<char>(id[1]), static_cast<char>(id[2]), '\0'};
|
|
492
|
+
|
|
493
|
+
int v1 = detail::decode_version_digit(id[3]);
|
|
494
|
+
int v2 = detail::decode_version_digit(id[4]);
|
|
495
|
+
int v3 = detail::decode_version_digit(id[5]);
|
|
496
|
+
int v4 = detail::decode_version_digit(id[6]);
|
|
497
|
+
|
|
498
|
+
const char* name = detail::lookup_az_client(code);
|
|
499
|
+
std::string client_name = name ? name : std::string("Unknown (") + code + ")";
|
|
289
500
|
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
501
|
+
std::string version = std::to_string(v1) + "." + std::to_string(v2) + "." + std::to_string(v3);
|
|
502
|
+
if (v4 != 0) {
|
|
503
|
+
version += "." + std::to_string(v4);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return client_name + " " + version;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Shadow-style: first char is client, next 3 are version
|
|
510
|
+
if ((id[0] >= 'A' && id[0] <= 'Z') || (id[0] >= 'a' && id[0] <= 'z')) {
|
|
511
|
+
// Check for shadow format: X + 3 version bytes + "--"
|
|
512
|
+
if (id[4] == '-' && id[5] == '-') {
|
|
513
|
+
if (id[1] >= '0' && id[2] >= '0' && id[3] >= '0') {
|
|
514
|
+
char c = static_cast<char>(id[0]);
|
|
515
|
+
const char* name = nullptr;
|
|
516
|
+
switch (c) {
|
|
517
|
+
case 'A': name = "ABC"; break;
|
|
518
|
+
case 'M': name = "Mainline"; break;
|
|
519
|
+
case 'O': name = "Osprey Permaseed"; break;
|
|
520
|
+
case 'Q': name = "BTQueue"; break;
|
|
521
|
+
case 'R': name = "Tribler"; break;
|
|
522
|
+
case 'S': name = "Shadow"; break;
|
|
523
|
+
case 'T': name = "BitTornado"; break;
|
|
524
|
+
case 'U': name = "UPnP"; break;
|
|
525
|
+
default: break;
|
|
526
|
+
}
|
|
527
|
+
if (name) {
|
|
528
|
+
int v1 = detail::decode_version_digit(id[1]);
|
|
529
|
+
int v2 = detail::decode_version_digit(id[2]);
|
|
530
|
+
int v3 = detail::decode_version_digit(id[3]);
|
|
531
|
+
return std::string(name) + " " + std::to_string(v1) + "." +
|
|
532
|
+
std::to_string(v2) + "." + std::to_string(v3);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
298
536
|
|
|
299
|
-
|
|
537
|
+
// Mainline-style: M1-2-3--
|
|
538
|
+
char ids[21];
|
|
539
|
+
std::memcpy(ids, id.data(), 20);
|
|
540
|
+
ids[20] = '\0';
|
|
541
|
+
char name_ch = '\0';
|
|
542
|
+
int v1 = 0, v2 = 0, v3 = 0;
|
|
543
|
+
if (std::sscanf(ids, "%c%3d-%3d-%3d--", &name_ch, &v1, &v2, &v3) == 4 &&
|
|
544
|
+
name_ch >= 32 && name_ch < 127) {
|
|
545
|
+
const char* name = nullptr;
|
|
546
|
+
switch (name_ch) {
|
|
547
|
+
case 'M': name = "Mainline"; break;
|
|
548
|
+
default: break;
|
|
549
|
+
}
|
|
550
|
+
if (name) {
|
|
551
|
+
return std::string(name) + " " + std::to_string(v1) + "." +
|
|
552
|
+
std::to_string(v2) + "." + std::to_string(v3);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
300
555
|
}
|
|
301
556
|
|
|
302
|
-
//
|
|
303
|
-
|
|
557
|
+
// All zeros in first 12 bytes - special cases
|
|
558
|
+
bool first_12_zero = true;
|
|
559
|
+
for (int i = 0; i < 12; ++i) { if (id[i] != 0) { first_12_zero = false; break; } }
|
|
560
|
+
if (first_12_zero) {
|
|
561
|
+
if (id[12] == 0x97) return "Experimental 3.2.1b2";
|
|
562
|
+
if (id[12] == 0x00) return "Experimental 3.1";
|
|
563
|
+
return "Generic";
|
|
564
|
+
}
|
|
304
565
|
|
|
305
|
-
|
|
566
|
+
// Unknown - show printable representation
|
|
567
|
+
std::string unknown("Unknown [");
|
|
568
|
+
for (uint8_t c : id) {
|
|
569
|
+
unknown += (c >= 32 && c < 127) ? static_cast<char>(c) : '.';
|
|
570
|
+
}
|
|
571
|
+
unknown += "]";
|
|
572
|
+
return unknown;
|
|
306
573
|
}
|
|
307
574
|
|
|
308
575
|
//=============================================================================
|