librats 0.5.0 → 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/binding.gyp +1 -0
- package/lib/index.d.ts +2 -1
- package/native-src/3rdparty/android/ifaddrs-android.c +600 -0
- package/native-src/3rdparty/android/ifaddrs-android.h +54 -0
- package/native-src/CMakeLists.txt +360 -0
- package/native-src/LICENSE +21 -0
- package/native-src/src/bencode.cpp +485 -0
- package/native-src/src/bencode.h +145 -0
- package/native-src/src/bittorrent.cpp +3682 -0
- package/native-src/src/bittorrent.h +731 -0
- package/native-src/src/dht.cpp +2460 -0
- package/native-src/src/dht.h +508 -0
- package/native-src/src/encrypted_socket.cpp +817 -0
- package/native-src/src/encrypted_socket.h +239 -0
- package/native-src/src/file_transfer.cpp +1808 -0
- package/native-src/src/file_transfer.h +567 -0
- package/native-src/src/fs.cpp +639 -0
- package/native-src/src/fs.h +108 -0
- package/native-src/src/gossipsub.cpp +1137 -0
- package/native-src/src/gossipsub.h +403 -0
- package/native-src/src/ice.cpp +1386 -0
- package/native-src/src/ice.h +328 -0
- package/native-src/src/json.hpp +25526 -0
- package/native-src/src/krpc.cpp +558 -0
- package/native-src/src/krpc.h +145 -0
- package/native-src/src/librats.cpp +2735 -0
- package/native-src/src/librats.h +1732 -0
- package/native-src/src/librats_bittorrent.cpp +167 -0
- package/native-src/src/librats_c.cpp +1333 -0
- package/native-src/src/librats_c.h +239 -0
- package/native-src/src/librats_encryption.cpp +123 -0
- package/native-src/src/librats_file_transfer.cpp +226 -0
- package/native-src/src/librats_gossipsub.cpp +293 -0
- package/native-src/src/librats_ice.cpp +515 -0
- package/native-src/src/librats_logging.cpp +158 -0
- package/native-src/src/librats_mdns.cpp +171 -0
- package/native-src/src/librats_nat.cpp +571 -0
- package/native-src/src/librats_persistence.cpp +815 -0
- package/native-src/src/logger.h +412 -0
- package/native-src/src/mdns.cpp +1178 -0
- package/native-src/src/mdns.h +253 -0
- package/native-src/src/network_utils.cpp +598 -0
- package/native-src/src/network_utils.h +162 -0
- package/native-src/src/noise.cpp +981 -0
- package/native-src/src/noise.h +227 -0
- package/native-src/src/os.cpp +371 -0
- package/native-src/src/os.h +40 -0
- package/native-src/src/rats_export.h +17 -0
- package/native-src/src/sha1.cpp +163 -0
- package/native-src/src/sha1.h +42 -0
- package/native-src/src/socket.cpp +1376 -0
- package/native-src/src/socket.h +309 -0
- package/native-src/src/stun.cpp +484 -0
- package/native-src/src/stun.h +349 -0
- package/native-src/src/threadmanager.cpp +105 -0
- package/native-src/src/threadmanager.h +53 -0
- package/native-src/src/tracker.cpp +1110 -0
- package/native-src/src/tracker.h +268 -0
- package/native-src/src/version.cpp +24 -0
- package/native-src/src/version.h.in +45 -0
- package/native-src/version.rc.in +31 -0
- package/package.json +2 -8
- package/scripts/build-librats.js +59 -12
- package/scripts/prepare-package.js +133 -37
- package/src/librats_node.cpp +46 -1
|
@@ -0,0 +1,3682 @@
|
|
|
1
|
+
#include "bittorrent.h"
|
|
2
|
+
#include "tracker.h"
|
|
3
|
+
#include "fs.h"
|
|
4
|
+
#include "network_utils.h"
|
|
5
|
+
#include "socket.h"
|
|
6
|
+
#include <iostream>
|
|
7
|
+
#include <algorithm>
|
|
8
|
+
#include <random>
|
|
9
|
+
#include <sstream>
|
|
10
|
+
#include <iomanip>
|
|
11
|
+
#include <cstring>
|
|
12
|
+
#include <climits>
|
|
13
|
+
#include <cerrno> // For errno on POSIX systems
|
|
14
|
+
|
|
15
|
+
#define LOG_BT_DEBUG(message) LOG_DEBUG("bittorrent", message)
|
|
16
|
+
#define LOG_BT_INFO(message) LOG_INFO("bittorrent", message)
|
|
17
|
+
#define LOG_BT_WARN(message) LOG_WARN("bittorrent", message)
|
|
18
|
+
#define LOG_BT_ERROR(message) LOG_ERROR("bittorrent", message)
|
|
19
|
+
|
|
20
|
+
namespace librats {
|
|
21
|
+
|
|
22
|
+
//=============================================================================
|
|
23
|
+
// TorrentInfo Implementation
|
|
24
|
+
//=============================================================================
|
|
25
|
+
|
|
26
|
+
TorrentInfo::TorrentInfo()
|
|
27
|
+
: total_length_(0), piece_length_(0), private_(false), metadata_only_(false) {
|
|
28
|
+
info_hash_.fill(0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
TorrentInfo::~TorrentInfo() = default;
|
|
32
|
+
|
|
33
|
+
bool TorrentInfo::load_from_file(const std::string& torrent_file) {
|
|
34
|
+
LOG_BT_INFO("Loading torrent from file: " << torrent_file);
|
|
35
|
+
|
|
36
|
+
std::string content = read_file_text_cpp(torrent_file);
|
|
37
|
+
if (content.empty()) {
|
|
38
|
+
LOG_BT_ERROR("Failed to read torrent file: " << torrent_file);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
std::vector<uint8_t> data(content.begin(), content.end());
|
|
43
|
+
return load_from_data(data);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
bool TorrentInfo::load_from_data(const std::vector<uint8_t>& data) {
|
|
47
|
+
LOG_BT_DEBUG("Parsing torrent data (" << data.size() << " bytes)");
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
BencodeValue torrent = bencode::decode(data);
|
|
51
|
+
return load_from_bencode(torrent);
|
|
52
|
+
} catch (const std::exception& e) {
|
|
53
|
+
LOG_BT_ERROR("Failed to decode torrent bencode: " << e.what());
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
bool TorrentInfo::load_from_bencode(const BencodeValue& torrent_data) {
|
|
59
|
+
try {
|
|
60
|
+
if (!torrent_data.is_dict()) {
|
|
61
|
+
LOG_BT_ERROR("Torrent data is not a dictionary");
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Parse announce URL
|
|
66
|
+
if (torrent_data.has_key("announce")) {
|
|
67
|
+
announce_ = torrent_data["announce"].as_string();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Parse announce-list
|
|
71
|
+
if (torrent_data.has_key("announce-list")) {
|
|
72
|
+
const auto& announce_list = torrent_data["announce-list"];
|
|
73
|
+
if (announce_list.is_list()) {
|
|
74
|
+
for (size_t i = 0; i < announce_list.size(); ++i) {
|
|
75
|
+
const auto& tier = announce_list[i];
|
|
76
|
+
if (tier.is_list() && tier.size() > 0) {
|
|
77
|
+
announce_list_.push_back(tier[0].as_string());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Parse info dictionary
|
|
84
|
+
if (!torrent_data.has_key("info")) {
|
|
85
|
+
LOG_BT_ERROR("Torrent data missing 'info' dictionary");
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const auto& info_dict = torrent_data["info"];
|
|
90
|
+
if (!parse_info_dict(info_dict)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Calculate info hash
|
|
95
|
+
calculate_info_hash(info_dict);
|
|
96
|
+
|
|
97
|
+
LOG_BT_INFO("Successfully parsed torrent: " << name_);
|
|
98
|
+
LOG_BT_INFO(" Info hash: " << info_hash_to_hex(info_hash_));
|
|
99
|
+
LOG_BT_INFO(" Total size: " << total_length_ << " bytes");
|
|
100
|
+
LOG_BT_INFO(" Piece length: " << piece_length_ << " bytes");
|
|
101
|
+
LOG_BT_INFO(" Number of pieces: " << piece_hashes_.size());
|
|
102
|
+
LOG_BT_INFO(" Number of files: " << files_.size());
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
|
|
106
|
+
} catch (const std::exception& e) {
|
|
107
|
+
LOG_BT_ERROR("Failed to parse torrent: " << e.what());
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
bool TorrentInfo::parse_info_dict(const BencodeValue& info_dict) {
|
|
113
|
+
if (!info_dict.is_dict()) {
|
|
114
|
+
LOG_BT_ERROR("Info is not a dictionary");
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Parse name
|
|
119
|
+
if (!info_dict.has_key("name")) {
|
|
120
|
+
LOG_BT_ERROR("Info dictionary missing 'name'");
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
name_ = info_dict["name"].as_string();
|
|
124
|
+
|
|
125
|
+
// Parse piece length
|
|
126
|
+
if (!info_dict.has_key("piece length")) {
|
|
127
|
+
LOG_BT_ERROR("Info dictionary missing 'piece length'");
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
piece_length_ = static_cast<uint32_t>(info_dict["piece length"].as_integer());
|
|
131
|
+
|
|
132
|
+
// Parse pieces hashes
|
|
133
|
+
if (!info_dict.has_key("pieces")) {
|
|
134
|
+
LOG_BT_ERROR("Info dictionary missing 'pieces'");
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const std::string& pieces_data = info_dict["pieces"].as_string();
|
|
139
|
+
if (pieces_data.length() % 20 != 0) {
|
|
140
|
+
LOG_BT_ERROR("Invalid pieces length: " << pieces_data.length());
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
size_t num_pieces = pieces_data.length() / 20;
|
|
145
|
+
piece_hashes_.reserve(num_pieces);
|
|
146
|
+
|
|
147
|
+
for (size_t i = 0; i < num_pieces; ++i) {
|
|
148
|
+
std::array<uint8_t, 20> hash;
|
|
149
|
+
std::memcpy(hash.data(), pieces_data.data() + i * 20, 20);
|
|
150
|
+
piece_hashes_.push_back(hash);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Parse private flag
|
|
154
|
+
if (info_dict.has_key("private")) {
|
|
155
|
+
private_ = info_dict["private"].as_integer() != 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Build file list
|
|
159
|
+
build_file_list(info_dict);
|
|
160
|
+
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
void TorrentInfo::calculate_info_hash(const BencodeValue& info_dict) {
|
|
165
|
+
// Encode the info dictionary and calculate SHA1 hash
|
|
166
|
+
std::vector<uint8_t> encoded = info_dict.encode();
|
|
167
|
+
std::string hash_string = SHA1::hash_bytes(encoded);
|
|
168
|
+
|
|
169
|
+
// Convert hex string to bytes
|
|
170
|
+
for (size_t i = 0; i < 20; ++i) {
|
|
171
|
+
std::string byte_str = hash_string.substr(i * 2, 2);
|
|
172
|
+
info_hash_[i] = static_cast<uint8_t>(std::stoul(byte_str, nullptr, 16));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
void TorrentInfo::build_file_list(const BencodeValue& info_dict) {
|
|
177
|
+
files_.clear();
|
|
178
|
+
total_length_ = 0;
|
|
179
|
+
|
|
180
|
+
if (info_dict.has_key("files")) {
|
|
181
|
+
// Multi-file torrent
|
|
182
|
+
const auto& files_list = info_dict["files"];
|
|
183
|
+
if (!files_list.is_list()) {
|
|
184
|
+
LOG_BT_ERROR("Files is not a list");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
uint64_t offset = 0;
|
|
189
|
+
for (size_t i = 0; i < files_list.size(); ++i) {
|
|
190
|
+
const auto& file_info = files_list[i];
|
|
191
|
+
if (!file_info.is_dict()) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!file_info.has_key("length") || !file_info.has_key("path")) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
uint64_t length = static_cast<uint64_t>(file_info["length"].as_integer());
|
|
200
|
+
const auto& path_list = file_info["path"];
|
|
201
|
+
|
|
202
|
+
std::string path;
|
|
203
|
+
if (path_list.is_list()) {
|
|
204
|
+
for (size_t j = 0; j < path_list.size(); ++j) {
|
|
205
|
+
if (j > 0) path += "/";
|
|
206
|
+
path += path_list[j].as_string();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
files_.emplace_back(path, length, offset);
|
|
211
|
+
offset += length;
|
|
212
|
+
total_length_ += length;
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
// Single-file torrent
|
|
216
|
+
if (!info_dict.has_key("length")) {
|
|
217
|
+
LOG_BT_ERROR("Single-file torrent missing 'length'");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
uint64_t length = static_cast<uint64_t>(info_dict["length"].as_integer());
|
|
222
|
+
files_.emplace_back(name_, length, 0);
|
|
223
|
+
total_length_ = length;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
uint32_t TorrentInfo::get_piece_length(PieceIndex piece_index) const {
|
|
228
|
+
if (piece_index >= piece_hashes_.size()) {
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (piece_index == piece_hashes_.size() - 1) {
|
|
233
|
+
// Last piece might be smaller
|
|
234
|
+
uint32_t remainder = static_cast<uint32_t>(total_length_ % piece_length_);
|
|
235
|
+
return remainder > 0 ? remainder : piece_length_;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return piece_length_;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
bool TorrentInfo::is_valid() const {
|
|
242
|
+
return !name_.empty() &&
|
|
243
|
+
!piece_hashes_.empty() &&
|
|
244
|
+
piece_length_ > 0 &&
|
|
245
|
+
total_length_ > 0 &&
|
|
246
|
+
!files_.empty();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
TorrentInfo TorrentInfo::create_for_metadata_exchange(const InfoHash& info_hash) {
|
|
250
|
+
TorrentInfo torrent;
|
|
251
|
+
|
|
252
|
+
// Set the info hash directly
|
|
253
|
+
torrent.info_hash_ = info_hash;
|
|
254
|
+
|
|
255
|
+
// Mark as metadata-only (for BEP 9 metadata exchange)
|
|
256
|
+
torrent.metadata_only_ = true;
|
|
257
|
+
|
|
258
|
+
// Set minimal required fields - these are placeholders only
|
|
259
|
+
// No pieces or files are actually downloaded for metadata-only torrents
|
|
260
|
+
torrent.name_ = "metadata_download_" + info_hash_to_hex(info_hash).substr(0, 8);
|
|
261
|
+
torrent.piece_length_ = 0; // No real pieces
|
|
262
|
+
torrent.total_length_ = 0; // No real content
|
|
263
|
+
torrent.private_ = false;
|
|
264
|
+
|
|
265
|
+
// No piece hashes or files - we only need the info hash for peer connections
|
|
266
|
+
// Piece downloading will be skipped for metadata-only torrents
|
|
267
|
+
|
|
268
|
+
LOG_BT_DEBUG("Created metadata exchange TorrentInfo for hash: " << info_hash_to_hex(info_hash));
|
|
269
|
+
|
|
270
|
+
return torrent;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
//=============================================================================
|
|
274
|
+
// PeerMessage Implementation
|
|
275
|
+
//=============================================================================
|
|
276
|
+
|
|
277
|
+
std::vector<uint8_t> PeerMessage::serialize() const {
|
|
278
|
+
std::vector<uint8_t> result;
|
|
279
|
+
|
|
280
|
+
// Length prefix (4 bytes)
|
|
281
|
+
uint32_t length = 1 + static_cast<uint32_t>(payload.size()); // 1 byte for message type + payload
|
|
282
|
+
result.push_back((length >> 24) & 0xFF);
|
|
283
|
+
result.push_back((length >> 16) & 0xFF);
|
|
284
|
+
result.push_back((length >> 8) & 0xFF);
|
|
285
|
+
result.push_back(length & 0xFF);
|
|
286
|
+
|
|
287
|
+
// Message type (1 byte)
|
|
288
|
+
result.push_back(static_cast<uint8_t>(type));
|
|
289
|
+
|
|
290
|
+
// Payload
|
|
291
|
+
result.insert(result.end(), payload.begin(), payload.end());
|
|
292
|
+
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
PeerMessage PeerMessage::create_choke() {
|
|
297
|
+
return PeerMessage(MessageType::CHOKE);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
PeerMessage PeerMessage::create_unchoke() {
|
|
301
|
+
return PeerMessage(MessageType::UNCHOKE);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
PeerMessage PeerMessage::create_interested() {
|
|
305
|
+
return PeerMessage(MessageType::INTERESTED);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
PeerMessage PeerMessage::create_not_interested() {
|
|
309
|
+
return PeerMessage(MessageType::NOT_INTERESTED);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
PeerMessage PeerMessage::create_have(PieceIndex piece_index) {
|
|
313
|
+
std::vector<uint8_t> payload(4);
|
|
314
|
+
payload[0] = (piece_index >> 24) & 0xFF;
|
|
315
|
+
payload[1] = (piece_index >> 16) & 0xFF;
|
|
316
|
+
payload[2] = (piece_index >> 8) & 0xFF;
|
|
317
|
+
payload[3] = piece_index & 0xFF;
|
|
318
|
+
return PeerMessage(MessageType::HAVE, payload);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
PeerMessage PeerMessage::create_bitfield(const std::vector<bool>& bitfield) {
|
|
322
|
+
std::vector<uint8_t> payload((bitfield.size() + 7) / 8, 0);
|
|
323
|
+
|
|
324
|
+
for (size_t i = 0; i < bitfield.size(); ++i) {
|
|
325
|
+
if (bitfield[i]) {
|
|
326
|
+
size_t byte_index = i / 8;
|
|
327
|
+
size_t bit_index = 7 - (i % 8);
|
|
328
|
+
payload[byte_index] |= (1 << bit_index);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return PeerMessage(MessageType::BITFIELD, payload);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
PeerMessage PeerMessage::create_request(PieceIndex piece_index, uint32_t offset, uint32_t length) {
|
|
336
|
+
std::vector<uint8_t> payload(12);
|
|
337
|
+
|
|
338
|
+
// Piece index
|
|
339
|
+
payload[0] = (piece_index >> 24) & 0xFF;
|
|
340
|
+
payload[1] = (piece_index >> 16) & 0xFF;
|
|
341
|
+
payload[2] = (piece_index >> 8) & 0xFF;
|
|
342
|
+
payload[3] = piece_index & 0xFF;
|
|
343
|
+
|
|
344
|
+
// Offset
|
|
345
|
+
payload[4] = (offset >> 24) & 0xFF;
|
|
346
|
+
payload[5] = (offset >> 16) & 0xFF;
|
|
347
|
+
payload[6] = (offset >> 8) & 0xFF;
|
|
348
|
+
payload[7] = offset & 0xFF;
|
|
349
|
+
|
|
350
|
+
// Length
|
|
351
|
+
payload[8] = (length >> 24) & 0xFF;
|
|
352
|
+
payload[9] = (length >> 16) & 0xFF;
|
|
353
|
+
payload[10] = (length >> 8) & 0xFF;
|
|
354
|
+
payload[11] = length & 0xFF;
|
|
355
|
+
|
|
356
|
+
return PeerMessage(MessageType::REQUEST, payload);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
PeerMessage PeerMessage::create_piece(PieceIndex piece_index, uint32_t offset, const std::vector<uint8_t>& data) {
|
|
360
|
+
std::vector<uint8_t> payload(8 + data.size());
|
|
361
|
+
|
|
362
|
+
// Piece index
|
|
363
|
+
payload[0] = (piece_index >> 24) & 0xFF;
|
|
364
|
+
payload[1] = (piece_index >> 16) & 0xFF;
|
|
365
|
+
payload[2] = (piece_index >> 8) & 0xFF;
|
|
366
|
+
payload[3] = piece_index & 0xFF;
|
|
367
|
+
|
|
368
|
+
// Offset
|
|
369
|
+
payload[4] = (offset >> 24) & 0xFF;
|
|
370
|
+
payload[5] = (offset >> 16) & 0xFF;
|
|
371
|
+
payload[6] = (offset >> 8) & 0xFF;
|
|
372
|
+
payload[7] = offset & 0xFF;
|
|
373
|
+
|
|
374
|
+
// Data
|
|
375
|
+
std::copy(data.begin(), data.end(), payload.begin() + 8);
|
|
376
|
+
|
|
377
|
+
return PeerMessage(MessageType::PIECE, payload);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
PeerMessage PeerMessage::create_cancel(PieceIndex piece_index, uint32_t offset, uint32_t length) {
|
|
381
|
+
std::vector<uint8_t> payload(12);
|
|
382
|
+
|
|
383
|
+
// Piece index
|
|
384
|
+
payload[0] = (piece_index >> 24) & 0xFF;
|
|
385
|
+
payload[1] = (piece_index >> 16) & 0xFF;
|
|
386
|
+
payload[2] = (piece_index >> 8) & 0xFF;
|
|
387
|
+
payload[3] = piece_index & 0xFF;
|
|
388
|
+
|
|
389
|
+
// Offset
|
|
390
|
+
payload[4] = (offset >> 24) & 0xFF;
|
|
391
|
+
payload[5] = (offset >> 16) & 0xFF;
|
|
392
|
+
payload[6] = (offset >> 8) & 0xFF;
|
|
393
|
+
payload[7] = offset & 0xFF;
|
|
394
|
+
|
|
395
|
+
// Length
|
|
396
|
+
payload[8] = (length >> 24) & 0xFF;
|
|
397
|
+
payload[9] = (length >> 16) & 0xFF;
|
|
398
|
+
payload[10] = (length >> 8) & 0xFF;
|
|
399
|
+
payload[11] = length & 0xFF;
|
|
400
|
+
|
|
401
|
+
return PeerMessage(MessageType::CANCEL, payload);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
PeerMessage PeerMessage::create_port(uint16_t port) {
|
|
405
|
+
std::vector<uint8_t> payload(2);
|
|
406
|
+
payload[0] = (port >> 8) & 0xFF;
|
|
407
|
+
payload[1] = port & 0xFF;
|
|
408
|
+
return PeerMessage(MessageType::PORT, payload);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
//=============================================================================
|
|
412
|
+
// PeerConnection Implementation
|
|
413
|
+
//=============================================================================
|
|
414
|
+
|
|
415
|
+
PeerConnection::PeerConnection(TorrentDownload* torrent, const Peer& peer_info, socket_t socket)
|
|
416
|
+
: torrent_(torrent), peer_info_(peer_info), socket_(socket),
|
|
417
|
+
state_(PeerState::CONNECTING), should_disconnect_(false), handshake_completed_(false),
|
|
418
|
+
peer_choked_(true), am_choked_(true), peer_interested_(false),
|
|
419
|
+
am_interested_(false), am_choking_(true),
|
|
420
|
+
downloaded_bytes_(0), uploaded_bytes_(0),
|
|
421
|
+
last_speed_update_time_(std::chrono::steady_clock::now()),
|
|
422
|
+
last_speed_downloaded_(0), last_speed_uploaded_(0),
|
|
423
|
+
download_speed_(0.0), upload_speed_(0.0),
|
|
424
|
+
expected_message_length_(0),
|
|
425
|
+
supports_extensions_(false), supports_metadata_exchange_(false),
|
|
426
|
+
peer_ut_metadata_id_(0), peer_metadata_size_(0) {
|
|
427
|
+
|
|
428
|
+
peer_id_.fill(0);
|
|
429
|
+
|
|
430
|
+
// If socket is already provided, it means handshake is already done (incoming connection)
|
|
431
|
+
if (is_valid_socket(socket)) {
|
|
432
|
+
handshake_completed_ = true;
|
|
433
|
+
state_.store(PeerState::HANDSHAKING); // Will move to CONNECTED after extended handshake
|
|
434
|
+
LOG_BT_DEBUG("Created peer connection for incoming connection from " << peer_info_.ip << ":" << peer_info_.port);
|
|
435
|
+
} else {
|
|
436
|
+
LOG_BT_DEBUG("Created peer connection to " << peer_info_.ip << ":" << peer_info_.port);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
PeerConnection::~PeerConnection() {
|
|
441
|
+
disconnect();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
bool PeerConnection::connect() {
|
|
445
|
+
if (state_.load() != PeerState::CONNECTING) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
LOG_BT_INFO("Connecting to peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
450
|
+
|
|
451
|
+
// Start connection thread - socket creation happens inside the thread for async behavior
|
|
452
|
+
// This prevents blocking the caller when connecting to unresponsive peers
|
|
453
|
+
connection_thread_ = std::thread(&PeerConnection::connection_loop, this);
|
|
454
|
+
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
void PeerConnection::disconnect() {
|
|
459
|
+
should_disconnect_ = true;
|
|
460
|
+
|
|
461
|
+
// Reset all pending block requests so they can be requested from other peers
|
|
462
|
+
{
|
|
463
|
+
std::lock_guard<std::mutex> lock(requests_mutex_);
|
|
464
|
+
for (const auto& req : pending_requests_) {
|
|
465
|
+
uint32_t block_index = req.offset / BLOCK_SIZE;
|
|
466
|
+
torrent_->reset_block_request(req.piece_index, block_index);
|
|
467
|
+
}
|
|
468
|
+
pending_requests_.clear();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (is_valid_socket(socket_)) {
|
|
472
|
+
// Force shutdown for TCP socket to ensure immediate disconnect
|
|
473
|
+
close_socket(socket_, true);
|
|
474
|
+
socket_ = INVALID_SOCKET_VALUE;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (connection_thread_.joinable()) {
|
|
478
|
+
connection_thread_.join();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
state_.store(PeerState::DISCONNECTED);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
void PeerConnection::connection_loop() {
|
|
485
|
+
LOG_BT_DEBUG("Starting connection loop for peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
486
|
+
|
|
487
|
+
// Create socket if not already provided (outgoing connection)
|
|
488
|
+
// This is done in the thread to avoid blocking the caller
|
|
489
|
+
if (!is_valid_socket(socket_)) {
|
|
490
|
+
socket_ = create_tcp_client(peer_info_.ip, peer_info_.port, 5000); // 5-second timeout for faster failure
|
|
491
|
+
if (!is_valid_socket(socket_)) {
|
|
492
|
+
LOG_BT_ERROR("Failed to create connection to " << peer_info_.ip << ":" << peer_info_.port);
|
|
493
|
+
state_.store(PeerState::ERROR);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Perform handshake only if not already completed (for outgoing connections)
|
|
499
|
+
if (!handshake_completed_) {
|
|
500
|
+
if (!perform_handshake()) {
|
|
501
|
+
LOG_BT_ERROR("Handshake failed with peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
502
|
+
state_.store(PeerState::ERROR);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
LOG_BT_DEBUG("Handshake already completed for incoming peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
507
|
+
// For incoming connections, send extended handshake if needed
|
|
508
|
+
send_extended_handshake();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
state_.store(PeerState::CONNECTED);
|
|
512
|
+
LOG_BT_INFO("Successfully connected to peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
513
|
+
|
|
514
|
+
// Initialize peer bitfield
|
|
515
|
+
const auto& torrent_info = torrent_->get_torrent_info();
|
|
516
|
+
peer_bitfield_.resize(torrent_info.get_num_pieces(), false);
|
|
517
|
+
|
|
518
|
+
// Send our bitfield to the peer (for seeding)
|
|
519
|
+
send_bitfield();
|
|
520
|
+
|
|
521
|
+
// Automatically unchoke the peer (optimistic unchoking for now)
|
|
522
|
+
// In a more sophisticated implementation, we'd have choke/unchoke algorithms
|
|
523
|
+
set_choke(false);
|
|
524
|
+
|
|
525
|
+
// Main message processing loop
|
|
526
|
+
while (!should_disconnect_.load() && state_.load() == PeerState::CONNECTED) {
|
|
527
|
+
process_messages();
|
|
528
|
+
|
|
529
|
+
// Cleanup expired requests
|
|
530
|
+
cleanup_expired_requests();
|
|
531
|
+
|
|
532
|
+
// Use conditional variable for responsive shutdown
|
|
533
|
+
{
|
|
534
|
+
std::unique_lock<std::mutex> lock(shutdown_mutex_);
|
|
535
|
+
if (shutdown_cv_.wait_for(lock, std::chrono::milliseconds(10), [this] { return should_disconnect_.load(); })) {
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Log reason for connection loop exit
|
|
542
|
+
PeerState exit_state = state_.load();
|
|
543
|
+
if (exit_state == PeerState::ERROR) {
|
|
544
|
+
LOG_BT_WARN("Connection loop ended for peer " << peer_info_.ip << ":" << peer_info_.port << " due to error");
|
|
545
|
+
} else if (should_disconnect_.load()) {
|
|
546
|
+
LOG_BT_DEBUG("Connection loop ended for peer " << peer_info_.ip << ":" << peer_info_.port << " - disconnect requested");
|
|
547
|
+
} else {
|
|
548
|
+
LOG_BT_DEBUG("Connection loop ended for peer " << peer_info_.ip << ":" << peer_info_.port
|
|
549
|
+
<< " (state: " << static_cast<int>(exit_state) << ")");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
bool PeerConnection::perform_handshake() {
|
|
554
|
+
state_.store(PeerState::HANDSHAKING);
|
|
555
|
+
|
|
556
|
+
if (!send_handshake()) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!receive_handshake()) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
bool PeerConnection::send_handshake() {
|
|
568
|
+
const auto& torrent_info = torrent_->get_torrent_info();
|
|
569
|
+
PeerID our_peer_id = generate_peer_id();
|
|
570
|
+
|
|
571
|
+
auto handshake_data = create_handshake_message(torrent_info.get_info_hash(), our_peer_id);
|
|
572
|
+
|
|
573
|
+
// Set extension protocol bit (BEP 10) - bit 20 in reserved bytes (byte 5, bit 3)
|
|
574
|
+
handshake_data[25] |= 0x10; // 0x10 = 0001 0000 (bit 4 from right)
|
|
575
|
+
|
|
576
|
+
return write_data(handshake_data);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
bool PeerConnection::receive_handshake() {
|
|
580
|
+
std::vector<uint8_t> handshake_data(68); // Fixed handshake size
|
|
581
|
+
|
|
582
|
+
if (!read_data(handshake_data, 68)) {
|
|
583
|
+
LOG_BT_ERROR("Failed to read handshake from peer");
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
InfoHash received_info_hash;
|
|
588
|
+
if (!parse_handshake_message(handshake_data, received_info_hash, peer_id_)) {
|
|
589
|
+
LOG_BT_ERROR("Failed to parse handshake from peer");
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Verify info hash matches
|
|
594
|
+
const auto& expected_info_hash = torrent_->get_torrent_info().get_info_hash();
|
|
595
|
+
if (received_info_hash != expected_info_hash) {
|
|
596
|
+
LOG_BT_ERROR("Info hash mismatch in handshake");
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Check if peer supports extension protocol (BEP 10) - bit 20 in reserved bytes (byte 5, bit 3)
|
|
601
|
+
supports_extensions_ = (handshake_data[25] & 0x10) != 0;
|
|
602
|
+
|
|
603
|
+
if (supports_extensions_) {
|
|
604
|
+
LOG_BT_DEBUG("Peer " << peer_info_.ip << ":" << peer_info_.port << " supports extension protocol");
|
|
605
|
+
// Send extended handshake
|
|
606
|
+
send_extended_handshake();
|
|
607
|
+
} else {
|
|
608
|
+
LOG_BT_DEBUG("Peer " << peer_info_.ip << ":" << peer_info_.port << " does not support extension protocol");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
LOG_BT_DEBUG("Handshake successful with peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
bool PeerConnection::send_message(const PeerMessage& message) {
|
|
616
|
+
auto data = message.serialize();
|
|
617
|
+
return write_data(data);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
void PeerConnection::process_messages() {
|
|
621
|
+
// Check if we're still connected before processing
|
|
622
|
+
if (state_.load() != PeerState::CONNECTED) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Try to read message length first
|
|
627
|
+
if (expected_message_length_ == 0) {
|
|
628
|
+
std::vector<uint8_t> length_buffer(4);
|
|
629
|
+
if (read_data(length_buffer, 4)) {
|
|
630
|
+
expected_message_length_ = (static_cast<size_t>(length_buffer[0]) << 24) |
|
|
631
|
+
(static_cast<size_t>(length_buffer[1]) << 16) |
|
|
632
|
+
(static_cast<size_t>(length_buffer[2]) << 8) |
|
|
633
|
+
static_cast<size_t>(length_buffer[3]);
|
|
634
|
+
|
|
635
|
+
if (expected_message_length_ == 0) {
|
|
636
|
+
// Keep-alive message
|
|
637
|
+
expected_message_length_ = 0;
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
message_buffer_.clear();
|
|
642
|
+
message_buffer_.reserve(expected_message_length_);
|
|
643
|
+
} else {
|
|
644
|
+
return; // No data available or connection error (state already updated)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Try to read the message payload
|
|
649
|
+
if (expected_message_length_ > 0 && message_buffer_.size() < expected_message_length_) {
|
|
650
|
+
// Check state again in case it changed during previous read
|
|
651
|
+
if (state_.load() != PeerState::CONNECTED) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
size_t remaining = expected_message_length_ - message_buffer_.size();
|
|
656
|
+
std::vector<uint8_t> temp_buffer(remaining);
|
|
657
|
+
|
|
658
|
+
if (read_data(temp_buffer, remaining)) {
|
|
659
|
+
message_buffer_.insert(message_buffer_.end(), temp_buffer.begin(), temp_buffer.end());
|
|
660
|
+
} else {
|
|
661
|
+
return; // Read failed, state already updated if error
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Process complete message
|
|
666
|
+
if (message_buffer_.size() >= expected_message_length_) {
|
|
667
|
+
auto message = parse_message(message_buffer_);
|
|
668
|
+
if (message) {
|
|
669
|
+
handle_message(*message);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Reset for next message
|
|
673
|
+
expected_message_length_ = 0;
|
|
674
|
+
message_buffer_.clear();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
std::unique_ptr<PeerMessage> PeerConnection::parse_message(const std::vector<uint8_t>& data) {
|
|
679
|
+
if (data.empty()) {
|
|
680
|
+
return nullptr;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
MessageType type = static_cast<MessageType>(data[0]);
|
|
684
|
+
std::vector<uint8_t> payload(data.begin() + 1, data.end());
|
|
685
|
+
|
|
686
|
+
return std::make_unique<PeerMessage>(type, payload);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
void PeerConnection::handle_message(const PeerMessage& message) {
|
|
690
|
+
LOG_BT_DEBUG("Received message type " << static_cast<int>(message.type) << " from peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
691
|
+
|
|
692
|
+
switch (message.type) {
|
|
693
|
+
case MessageType::CHOKE:
|
|
694
|
+
handle_choke();
|
|
695
|
+
break;
|
|
696
|
+
case MessageType::UNCHOKE:
|
|
697
|
+
handle_unchoke();
|
|
698
|
+
break;
|
|
699
|
+
case MessageType::INTERESTED:
|
|
700
|
+
handle_interested();
|
|
701
|
+
break;
|
|
702
|
+
case MessageType::NOT_INTERESTED:
|
|
703
|
+
handle_not_interested();
|
|
704
|
+
break;
|
|
705
|
+
case MessageType::HAVE:
|
|
706
|
+
handle_have(message.payload);
|
|
707
|
+
break;
|
|
708
|
+
case MessageType::BITFIELD:
|
|
709
|
+
handle_bitfield(message.payload);
|
|
710
|
+
break;
|
|
711
|
+
case MessageType::REQUEST:
|
|
712
|
+
handle_request(message.payload);
|
|
713
|
+
break;
|
|
714
|
+
case MessageType::PIECE:
|
|
715
|
+
handle_piece(message.payload);
|
|
716
|
+
break;
|
|
717
|
+
case MessageType::CANCEL:
|
|
718
|
+
handle_cancel(message.payload);
|
|
719
|
+
break;
|
|
720
|
+
case MessageType::EXTENDED:
|
|
721
|
+
handle_extended(message.payload);
|
|
722
|
+
break;
|
|
723
|
+
default:
|
|
724
|
+
LOG_BT_WARN("Unknown message type: " << static_cast<int>(message.type));
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
void PeerConnection::handle_choke() {
|
|
730
|
+
peer_choked_ = true;
|
|
731
|
+
cancel_all_requests();
|
|
732
|
+
LOG_BT_DEBUG("Peer " << peer_info_.ip << ":" << peer_info_.port << " choked us");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
void PeerConnection::handle_unchoke() {
|
|
736
|
+
peer_choked_ = false;
|
|
737
|
+
LOG_BT_DEBUG("Peer " << peer_info_.ip << ":" << peer_info_.port << " unchoked us");
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
void PeerConnection::handle_interested() {
|
|
741
|
+
peer_interested_ = true;
|
|
742
|
+
LOG_BT_DEBUG("Peer " << peer_info_.ip << ":" << peer_info_.port << " is interested");
|
|
743
|
+
|
|
744
|
+
// Automatically unchoke interested peers (optimistic unchoking)
|
|
745
|
+
// In a more sophisticated implementation, we'd have a proper choke algorithm
|
|
746
|
+
if (am_choking_) {
|
|
747
|
+
set_choke(false);
|
|
748
|
+
LOG_BT_DEBUG("Unchoked interested peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
void PeerConnection::handle_not_interested() {
|
|
753
|
+
peer_interested_ = false;
|
|
754
|
+
LOG_BT_DEBUG("Peer " << peer_info_.ip << ":" << peer_info_.port << " is not interested");
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
void PeerConnection::handle_have(const std::vector<uint8_t>& payload) {
|
|
758
|
+
if (payload.size() != 4) {
|
|
759
|
+
LOG_BT_WARN("Invalid HAVE message size: " << payload.size());
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
PieceIndex piece_index = (static_cast<uint32_t>(payload[0]) << 24) |
|
|
764
|
+
(static_cast<uint32_t>(payload[1]) << 16) |
|
|
765
|
+
(static_cast<uint32_t>(payload[2]) << 8) |
|
|
766
|
+
static_cast<uint32_t>(payload[3]);
|
|
767
|
+
|
|
768
|
+
if (piece_index < peer_bitfield_.size()) {
|
|
769
|
+
peer_bitfield_[piece_index] = true;
|
|
770
|
+
LOG_BT_DEBUG("Peer " << peer_info_.ip << ":" << peer_info_.port << " has piece " << piece_index);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
void PeerConnection::handle_bitfield(const std::vector<uint8_t>& payload) {
|
|
775
|
+
const auto& torrent_info = torrent_->get_torrent_info();
|
|
776
|
+
uint32_t num_pieces = torrent_info.get_num_pieces();
|
|
777
|
+
|
|
778
|
+
peer_bitfield_.clear();
|
|
779
|
+
peer_bitfield_.resize(num_pieces, false);
|
|
780
|
+
|
|
781
|
+
for (uint32_t i = 0; i < num_pieces && i / 8 < payload.size(); ++i) {
|
|
782
|
+
size_t byte_index = i / 8;
|
|
783
|
+
size_t bit_index = 7 - (i % 8);
|
|
784
|
+
peer_bitfield_[i] = (payload[byte_index] & (1 << bit_index)) != 0;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
LOG_BT_DEBUG("Received bitfield from peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
void PeerConnection::handle_request(const std::vector<uint8_t>& payload) {
|
|
791
|
+
if (payload.size() != 12) {
|
|
792
|
+
LOG_BT_WARN("Invalid REQUEST message size: " << payload.size());
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
PieceIndex piece_index = (static_cast<uint32_t>(payload[0]) << 24) |
|
|
797
|
+
(static_cast<uint32_t>(payload[1]) << 16) |
|
|
798
|
+
(static_cast<uint32_t>(payload[2]) << 8) |
|
|
799
|
+
static_cast<uint32_t>(payload[3]);
|
|
800
|
+
uint32_t offset = (static_cast<uint32_t>(payload[4]) << 24) |
|
|
801
|
+
(static_cast<uint32_t>(payload[5]) << 16) |
|
|
802
|
+
(static_cast<uint32_t>(payload[6]) << 8) |
|
|
803
|
+
static_cast<uint32_t>(payload[7]);
|
|
804
|
+
uint32_t length = (static_cast<uint32_t>(payload[8]) << 24) |
|
|
805
|
+
(static_cast<uint32_t>(payload[9]) << 16) |
|
|
806
|
+
(static_cast<uint32_t>(payload[10]) << 8) |
|
|
807
|
+
static_cast<uint32_t>(payload[11]);
|
|
808
|
+
|
|
809
|
+
LOG_BT_DEBUG("Peer " << peer_info_.ip << ":" << peer_info_.port << " requested piece " << piece_index << " offset " << offset << " length " << length);
|
|
810
|
+
|
|
811
|
+
// Check if we're choking this peer
|
|
812
|
+
if (am_choking_) {
|
|
813
|
+
LOG_BT_DEBUG("Ignoring request from choked peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Validate request parameters
|
|
818
|
+
const auto& torrent_info = torrent_->get_torrent_info();
|
|
819
|
+
if (piece_index >= torrent_info.get_num_pieces()) {
|
|
820
|
+
LOG_BT_WARN("Invalid piece index requested: " << piece_index);
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
uint32_t piece_length = torrent_info.get_piece_length(piece_index);
|
|
825
|
+
if (offset >= piece_length || offset + length > piece_length) {
|
|
826
|
+
LOG_BT_WARN("Invalid offset/length in request: offset=" << offset << " length=" << length << " piece_length=" << piece_length);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Validate request length (up to BLOCK_SIZE, smaller is allowed for last block)
|
|
831
|
+
if (length > BLOCK_SIZE || length == 0) {
|
|
832
|
+
LOG_BT_WARN("Invalid request length: " << length);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Check if we have this piece
|
|
837
|
+
if (!torrent_->is_piece_complete(piece_index)) {
|
|
838
|
+
LOG_BT_DEBUG("Don't have piece " << piece_index << " yet, can't serve to peer");
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Read the piece from disk
|
|
843
|
+
std::vector<uint8_t> piece_data;
|
|
844
|
+
if (!torrent_->read_piece_from_disk(piece_index, piece_data)) {
|
|
845
|
+
LOG_BT_ERROR("Failed to read piece " << piece_index << " from disk for upload");
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Extract the requested block
|
|
850
|
+
if (offset + length > piece_data.size()) {
|
|
851
|
+
LOG_BT_ERROR("Block exceeds piece data size: offset=" << offset << " length=" << length << " piece_size=" << piece_data.size());
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
std::vector<uint8_t> block_data(piece_data.begin() + offset, piece_data.begin() + offset + length);
|
|
856
|
+
|
|
857
|
+
// Send the piece message
|
|
858
|
+
auto piece_msg = PeerMessage::create_piece(piece_index, offset, block_data);
|
|
859
|
+
if (send_message(piece_msg)) {
|
|
860
|
+
uploaded_bytes_ += block_data.size();
|
|
861
|
+
LOG_BT_DEBUG("Sent piece " << piece_index << " offset " << offset << " length " << length << " to peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
862
|
+
} else {
|
|
863
|
+
LOG_BT_ERROR("Failed to send piece data to peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
void PeerConnection::handle_piece(const std::vector<uint8_t>& payload) {
|
|
868
|
+
if (payload.size() < 8) {
|
|
869
|
+
LOG_BT_WARN("Invalid PIECE message size: " << payload.size());
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
PieceIndex piece_index = (static_cast<uint32_t>(payload[0]) << 24) |
|
|
874
|
+
(static_cast<uint32_t>(payload[1]) << 16) |
|
|
875
|
+
(static_cast<uint32_t>(payload[2]) << 8) |
|
|
876
|
+
static_cast<uint32_t>(payload[3]);
|
|
877
|
+
uint32_t offset = (static_cast<uint32_t>(payload[4]) << 24) |
|
|
878
|
+
(static_cast<uint32_t>(payload[5]) << 16) |
|
|
879
|
+
(static_cast<uint32_t>(payload[6]) << 8) |
|
|
880
|
+
static_cast<uint32_t>(payload[7]);
|
|
881
|
+
|
|
882
|
+
std::vector<uint8_t> block_data(payload.begin() + 8, payload.end());
|
|
883
|
+
downloaded_bytes_ += block_data.size();
|
|
884
|
+
|
|
885
|
+
LOG_BT_DEBUG("Received piece " << piece_index << " offset " << offset << " length " << block_data.size() << " from peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
886
|
+
|
|
887
|
+
// Store the piece block
|
|
888
|
+
if (torrent_->store_piece_block(piece_index, offset, block_data)) {
|
|
889
|
+
// Remove corresponding request
|
|
890
|
+
std::lock_guard<std::mutex> lock(requests_mutex_);
|
|
891
|
+
pending_requests_.erase(
|
|
892
|
+
std::remove_if(pending_requests_.begin(), pending_requests_.end(),
|
|
893
|
+
[piece_index, offset](const PeerRequest& req) {
|
|
894
|
+
return req.piece_index == piece_index && req.offset == offset;
|
|
895
|
+
}),
|
|
896
|
+
pending_requests_.end());
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
void PeerConnection::handle_cancel(const std::vector<uint8_t>& payload) {
|
|
901
|
+
if (payload.size() != 12) {
|
|
902
|
+
LOG_BT_WARN("Invalid CANCEL message size: " << payload.size());
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
PieceIndex piece_index = (static_cast<uint32_t>(payload[0]) << 24) |
|
|
907
|
+
(static_cast<uint32_t>(payload[1]) << 16) |
|
|
908
|
+
(static_cast<uint32_t>(payload[2]) << 8) |
|
|
909
|
+
static_cast<uint32_t>(payload[3]);
|
|
910
|
+
uint32_t offset = (static_cast<uint32_t>(payload[4]) << 24) |
|
|
911
|
+
(static_cast<uint32_t>(payload[5]) << 16) |
|
|
912
|
+
(static_cast<uint32_t>(payload[6]) << 8) |
|
|
913
|
+
static_cast<uint32_t>(payload[7]);
|
|
914
|
+
uint32_t length = (static_cast<uint32_t>(payload[8]) << 24) |
|
|
915
|
+
(static_cast<uint32_t>(payload[9]) << 16) |
|
|
916
|
+
(static_cast<uint32_t>(payload[10]) << 8) |
|
|
917
|
+
static_cast<uint32_t>(payload[11]);
|
|
918
|
+
|
|
919
|
+
LOG_BT_DEBUG("Peer " << peer_info_.ip << ":" << peer_info_.port << " cancelled request for piece " << piece_index << " offset " << offset << " length " << length);
|
|
920
|
+
|
|
921
|
+
// Note: For a simple implementation, we don't queue outgoing requests
|
|
922
|
+
// If we were queuing them, we'd remove this request from the queue here
|
|
923
|
+
// For now, just acknowledge the cancel
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
void PeerConnection::handle_extended(const std::vector<uint8_t>& payload) {
|
|
927
|
+
if (payload.empty()) {
|
|
928
|
+
LOG_BT_WARN("Empty extended message payload");
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
uint8_t extended_id = payload[0];
|
|
933
|
+
std::vector<uint8_t> extended_payload(payload.begin() + 1, payload.end());
|
|
934
|
+
|
|
935
|
+
LOG_BT_DEBUG("Received extended message with ID " << static_cast<int>(extended_id) << " from peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
936
|
+
|
|
937
|
+
if (extended_id == static_cast<uint8_t>(ExtendedMessageType::HANDSHAKE)) {
|
|
938
|
+
handle_extended_handshake(extended_payload);
|
|
939
|
+
} else if (extended_id == static_cast<uint8_t>(ExtendedMessageType::UT_METADATA) && supports_metadata_exchange_) {
|
|
940
|
+
// Note: Peers send messages using OUR extension ID (what we advertised in our handshake),
|
|
941
|
+
// not their ID. We use peer_ut_metadata_id_ only when SENDING to them.
|
|
942
|
+
handle_metadata_message(extended_payload);
|
|
943
|
+
} else {
|
|
944
|
+
LOG_BT_DEBUG("Unknown or unsupported extended message ID: " << static_cast<int>(extended_id));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
void PeerConnection::send_extended_handshake() {
|
|
949
|
+
// Create extended handshake bencode dictionary
|
|
950
|
+
BencodeValue handshake = BencodeValue::create_dict();
|
|
951
|
+
|
|
952
|
+
// Add supported extensions
|
|
953
|
+
BencodeValue extensions = BencodeValue::create_dict();
|
|
954
|
+
extensions["ut_metadata"] = BencodeValue::create_integer(static_cast<uint8_t>(ExtendedMessageType::UT_METADATA));
|
|
955
|
+
handshake["m"] = extensions;
|
|
956
|
+
|
|
957
|
+
// Add metadata size if we have metadata download
|
|
958
|
+
auto* metadata_download = torrent_->get_metadata_download();
|
|
959
|
+
if (metadata_download) {
|
|
960
|
+
handshake["metadata_size"] = BencodeValue::create_integer(static_cast<int64_t>(metadata_download->get_metadata_size()));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Encode the handshake
|
|
964
|
+
std::vector<uint8_t> encoded = handshake.encode();
|
|
965
|
+
|
|
966
|
+
// Create extended message payload: [extended_id][bencode_data]
|
|
967
|
+
std::vector<uint8_t> payload;
|
|
968
|
+
payload.push_back(static_cast<uint8_t>(ExtendedMessageType::HANDSHAKE));
|
|
969
|
+
payload.insert(payload.end(), encoded.begin(), encoded.end());
|
|
970
|
+
|
|
971
|
+
// Send as extended message
|
|
972
|
+
PeerMessage message(MessageType::EXTENDED, payload);
|
|
973
|
+
send_message(message);
|
|
974
|
+
|
|
975
|
+
LOG_BT_DEBUG("Sent extended handshake to peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
void PeerConnection::send_bitfield() {
|
|
979
|
+
// Get our bitfield from the torrent
|
|
980
|
+
std::vector<bool> our_bitfield = torrent_->get_piece_bitfield();
|
|
981
|
+
|
|
982
|
+
// Don't send empty bitfield or if we have no pieces
|
|
983
|
+
bool has_any_piece = false;
|
|
984
|
+
for (bool has_piece : our_bitfield) {
|
|
985
|
+
if (has_piece) {
|
|
986
|
+
has_any_piece = true;
|
|
987
|
+
break;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (!has_any_piece) {
|
|
992
|
+
LOG_BT_DEBUG("No pieces to advertise to peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Create and send bitfield message
|
|
997
|
+
auto bitfield_msg = PeerMessage::create_bitfield(our_bitfield);
|
|
998
|
+
if (send_message(bitfield_msg)) {
|
|
999
|
+
LOG_BT_DEBUG("Sent bitfield to peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
1000
|
+
} else {
|
|
1001
|
+
LOG_BT_WARN("Failed to send bitfield to peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
void PeerConnection::handle_extended_handshake(const std::vector<uint8_t>& payload) {
|
|
1006
|
+
LOG_BT_DEBUG("Handling extended handshake from peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
1007
|
+
|
|
1008
|
+
try {
|
|
1009
|
+
BencodeValue handshake = bencode::decode(payload);
|
|
1010
|
+
|
|
1011
|
+
if (!handshake.is_dict()) {
|
|
1012
|
+
LOG_BT_WARN("Extended handshake is not a dictionary");
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Check if peer supports ut_metadata extension
|
|
1017
|
+
if (handshake.has_key("m")) {
|
|
1018
|
+
const auto& extensions = handshake["m"];
|
|
1019
|
+
if (extensions.is_dict() && extensions.has_key("ut_metadata")) {
|
|
1020
|
+
peer_ut_metadata_id_ = static_cast<uint8_t>(extensions["ut_metadata"].as_integer());
|
|
1021
|
+
supports_metadata_exchange_ = true;
|
|
1022
|
+
LOG_BT_INFO("Peer " << peer_info_.ip << ":" << peer_info_.port
|
|
1023
|
+
<< " supports metadata exchange (ID: " << static_cast<int>(peer_ut_metadata_id_) << ")");
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Get metadata size if available
|
|
1028
|
+
if (handshake.has_key("metadata_size")) {
|
|
1029
|
+
peer_metadata_size_ = static_cast<size_t>(handshake["metadata_size"].as_integer());
|
|
1030
|
+
LOG_BT_DEBUG("Peer has metadata size: " << peer_metadata_size_ << " bytes");
|
|
1031
|
+
|
|
1032
|
+
// Check if we need to create a metadata download
|
|
1033
|
+
auto* metadata_download = torrent_->get_metadata_download();
|
|
1034
|
+
|
|
1035
|
+
// Only create metadata download if:
|
|
1036
|
+
// 1. No existing metadata download in progress
|
|
1037
|
+
// 2. Peer has valid metadata size
|
|
1038
|
+
// 3. This is a metadata-only torrent (for BEP 9 metadata exchange)
|
|
1039
|
+
if (!metadata_download && peer_metadata_size_ > 0 && peer_metadata_size_ <= MAX_METADATA_SIZE
|
|
1040
|
+
&& torrent_->get_torrent_info().is_metadata_only()) {
|
|
1041
|
+
// Create metadata download with the correct size
|
|
1042
|
+
const auto& info_hash = torrent_->get_torrent_info().get_info_hash();
|
|
1043
|
+
LOG_BT_INFO("Creating metadata download for hash " << info_hash_to_hex(info_hash)
|
|
1044
|
+
<< " with size " << peer_metadata_size_ << " bytes");
|
|
1045
|
+
|
|
1046
|
+
auto new_metadata_download = std::make_shared<MetadataDownload>(info_hash, peer_metadata_size_);
|
|
1047
|
+
|
|
1048
|
+
// Set completion callback - need to use a custom approach since we need to call back through TorrentDownload
|
|
1049
|
+
// For now, we'll leave the completion callback to be set by add_torrent_by_hash after metadata download is created
|
|
1050
|
+
// The BitTorrentClient will need to monitor metadata downloads and set their callbacks appropriately
|
|
1051
|
+
|
|
1052
|
+
torrent_->set_metadata_download(new_metadata_download);
|
|
1053
|
+
metadata_download = new_metadata_download.get();
|
|
1054
|
+
|
|
1055
|
+
LOG_BT_INFO("Metadata download created and ready");
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// If we are downloading metadata, start requesting pieces
|
|
1059
|
+
if (metadata_download && supports_metadata_exchange_) {
|
|
1060
|
+
// Request first metadata piece
|
|
1061
|
+
uint32_t next_piece = metadata_download->get_next_piece_to_request();
|
|
1062
|
+
if (next_piece < metadata_download->get_num_pieces()) {
|
|
1063
|
+
request_metadata_piece(next_piece);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
} catch (const std::exception& e) {
|
|
1069
|
+
LOG_BT_ERROR("Failed to parse extended handshake: " << e.what());
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
void PeerConnection::handle_metadata_message(const std::vector<uint8_t>& payload) {
|
|
1074
|
+
LOG_BT_DEBUG("Handling metadata message from peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
1075
|
+
|
|
1076
|
+
try {
|
|
1077
|
+
// Decode bencode dictionary at the start of payload
|
|
1078
|
+
BencodeValue msg_dict = bencode::decode(payload);
|
|
1079
|
+
|
|
1080
|
+
if (!msg_dict.is_dict()) {
|
|
1081
|
+
LOG_BT_WARN("Metadata message is not a dictionary");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (!msg_dict.has_key("msg_type") || !msg_dict.has_key("piece")) {
|
|
1086
|
+
LOG_BT_WARN("Metadata message missing required fields");
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
uint8_t msg_type = static_cast<uint8_t>(msg_dict["msg_type"].as_integer());
|
|
1091
|
+
uint32_t piece_index = static_cast<uint32_t>(msg_dict["piece"].as_integer());
|
|
1092
|
+
|
|
1093
|
+
LOG_BT_DEBUG("Metadata message type: " << static_cast<int>(msg_type) << ", piece: " << piece_index);
|
|
1094
|
+
|
|
1095
|
+
auto* metadata_download = torrent_->get_metadata_download();
|
|
1096
|
+
if (!metadata_download) {
|
|
1097
|
+
LOG_BT_DEBUG("No metadata download in progress, ignoring metadata message");
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (msg_type == static_cast<uint8_t>(MetadataMessageType::DATA)) {
|
|
1102
|
+
// Extract metadata piece data (comes after bencode dictionary)
|
|
1103
|
+
std::vector<uint8_t> encoded = msg_dict.encode();
|
|
1104
|
+
if (payload.size() <= encoded.size()) {
|
|
1105
|
+
LOG_BT_WARN("Metadata DATA message has no piece data");
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
std::vector<uint8_t> piece_data(payload.begin() + encoded.size(), payload.end());
|
|
1110
|
+
|
|
1111
|
+
LOG_BT_INFO("Received metadata piece " << piece_index << " (" << piece_data.size() << " bytes) from peer "
|
|
1112
|
+
<< peer_info_.ip << ":" << peer_info_.port);
|
|
1113
|
+
|
|
1114
|
+
// Store the piece
|
|
1115
|
+
if (metadata_download->store_metadata_piece(piece_index, piece_data)) {
|
|
1116
|
+
// Request next piece if available
|
|
1117
|
+
if (!metadata_download->is_complete()) {
|
|
1118
|
+
uint32_t next_piece = metadata_download->get_next_piece_to_request();
|
|
1119
|
+
if (next_piece < metadata_download->get_num_pieces()) {
|
|
1120
|
+
request_metadata_piece(next_piece);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
} else if (msg_type == static_cast<uint8_t>(MetadataMessageType::REJECT)) {
|
|
1125
|
+
LOG_BT_WARN("Peer " << peer_info_.ip << ":" << peer_info_.port
|
|
1126
|
+
<< " rejected metadata piece " << piece_index << " request");
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
} catch (const std::exception& e) {
|
|
1130
|
+
LOG_BT_ERROR("Failed to parse metadata message: " << e.what());
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
void PeerConnection::request_metadata_piece(uint32_t piece_index) {
|
|
1135
|
+
if (!supports_metadata_exchange_) {
|
|
1136
|
+
LOG_BT_WARN("Peer does not support metadata exchange");
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
LOG_BT_DEBUG("Requesting metadata piece " << piece_index << " from peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
1141
|
+
|
|
1142
|
+
// Create metadata request message
|
|
1143
|
+
BencodeValue msg_dict = BencodeValue::create_dict();
|
|
1144
|
+
msg_dict["msg_type"] = BencodeValue::create_integer(static_cast<int>(MetadataMessageType::REQUEST));
|
|
1145
|
+
msg_dict["piece"] = BencodeValue::create_integer(piece_index);
|
|
1146
|
+
|
|
1147
|
+
// Encode the message
|
|
1148
|
+
std::vector<uint8_t> encoded = msg_dict.encode();
|
|
1149
|
+
|
|
1150
|
+
// Create extended message payload: [peer_ut_metadata_id][bencode_data]
|
|
1151
|
+
std::vector<uint8_t> payload;
|
|
1152
|
+
payload.push_back(peer_ut_metadata_id_);
|
|
1153
|
+
payload.insert(payload.end(), encoded.begin(), encoded.end());
|
|
1154
|
+
|
|
1155
|
+
// Send as extended message
|
|
1156
|
+
PeerMessage message(MessageType::EXTENDED, payload);
|
|
1157
|
+
send_message(message);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
bool PeerConnection::request_piece_block(PieceIndex piece_index, uint32_t offset, uint32_t length) {
|
|
1161
|
+
if (peer_choked_ || state_ != PeerState::CONNECTED) {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
std::lock_guard<std::mutex> lock(requests_mutex_);
|
|
1166
|
+
|
|
1167
|
+
if (pending_requests_.size() >= MAX_REQUESTS_PER_PEER) {
|
|
1168
|
+
return false; // Too many pending requests
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
auto request_msg = PeerMessage::create_request(piece_index, offset, length);
|
|
1172
|
+
if (send_message(request_msg)) {
|
|
1173
|
+
pending_requests_.emplace_back(piece_index, offset, length);
|
|
1174
|
+
LOG_BT_DEBUG("Requested piece " << piece_index << " offset " << offset << " length " << length << " from peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
1175
|
+
return true;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
bool PeerConnection::is_block_requested(PieceIndex piece_index, uint32_t offset) const {
|
|
1182
|
+
std::lock_guard<std::mutex> lock(requests_mutex_);
|
|
1183
|
+
for (const auto& req : pending_requests_) {
|
|
1184
|
+
if (req.piece_index == piece_index && req.offset == offset) {
|
|
1185
|
+
return true;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
void PeerConnection::cancel_request(PieceIndex piece_index, uint32_t offset, uint32_t length) {
|
|
1192
|
+
auto cancel_msg = PeerMessage::create_cancel(piece_index, offset, length);
|
|
1193
|
+
send_message(cancel_msg);
|
|
1194
|
+
|
|
1195
|
+
{
|
|
1196
|
+
std::lock_guard<std::mutex> lock(requests_mutex_);
|
|
1197
|
+
pending_requests_.erase(
|
|
1198
|
+
std::remove_if(pending_requests_.begin(), pending_requests_.end(),
|
|
1199
|
+
[piece_index, offset, length](const PeerRequest& req) {
|
|
1200
|
+
return req.piece_index == piece_index && req.offset == offset && req.length == length;
|
|
1201
|
+
}),
|
|
1202
|
+
pending_requests_.end());
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Reset block request status so block can be requested from other peers
|
|
1206
|
+
uint32_t block_index = offset / BLOCK_SIZE;
|
|
1207
|
+
torrent_->reset_block_request(piece_index, block_index);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
void PeerConnection::cancel_all_requests() {
|
|
1211
|
+
std::vector<PeerRequest> requests_to_reset;
|
|
1212
|
+
|
|
1213
|
+
{
|
|
1214
|
+
std::lock_guard<std::mutex> lock(requests_mutex_);
|
|
1215
|
+
|
|
1216
|
+
for (const auto& request : pending_requests_) {
|
|
1217
|
+
auto cancel_msg = PeerMessage::create_cancel(request.piece_index, request.offset, request.length);
|
|
1218
|
+
send_message(cancel_msg);
|
|
1219
|
+
requests_to_reset.push_back(request);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
pending_requests_.clear();
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Reset block request status so blocks can be requested from other peers
|
|
1226
|
+
for (const auto& req : requests_to_reset) {
|
|
1227
|
+
uint32_t block_index = req.offset / BLOCK_SIZE;
|
|
1228
|
+
torrent_->reset_block_request(req.piece_index, block_index);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
void PeerConnection::set_interested(bool interested) {
|
|
1233
|
+
if (am_interested_ != interested) {
|
|
1234
|
+
am_interested_ = interested;
|
|
1235
|
+
auto msg = interested ? PeerMessage::create_interested() : PeerMessage::create_not_interested();
|
|
1236
|
+
send_message(msg);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
void PeerConnection::set_choke(bool choke) {
|
|
1241
|
+
if (am_choking_ != choke) {
|
|
1242
|
+
am_choking_ = choke;
|
|
1243
|
+
auto msg = choke ? PeerMessage::create_choke() : PeerMessage::create_unchoke();
|
|
1244
|
+
send_message(msg);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
bool PeerConnection::has_piece(PieceIndex piece_index) const {
|
|
1249
|
+
return piece_index < peer_bitfield_.size() && peer_bitfield_[piece_index];
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
void PeerConnection::update_bitfield(const std::vector<bool>& bitfield) {
|
|
1253
|
+
peer_bitfield_ = bitfield;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
void PeerConnection::cleanup_expired_requests() {
|
|
1257
|
+
std::vector<PeerRequest> expired_requests;
|
|
1258
|
+
|
|
1259
|
+
{
|
|
1260
|
+
std::lock_guard<std::mutex> lock(requests_mutex_);
|
|
1261
|
+
|
|
1262
|
+
auto now = std::chrono::steady_clock::now();
|
|
1263
|
+
auto timeout = std::chrono::milliseconds(REQUEST_TIMEOUT_MS);
|
|
1264
|
+
|
|
1265
|
+
// Collect expired requests before removing them
|
|
1266
|
+
for (const auto& req : pending_requests_) {
|
|
1267
|
+
if (now - req.requested_at > timeout) {
|
|
1268
|
+
expired_requests.push_back(req);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
pending_requests_.erase(
|
|
1273
|
+
std::remove_if(pending_requests_.begin(), pending_requests_.end(),
|
|
1274
|
+
[now, timeout](const PeerRequest& req) {
|
|
1275
|
+
return now - req.requested_at > timeout;
|
|
1276
|
+
}),
|
|
1277
|
+
pending_requests_.end());
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Reset block request status for expired requests (so they can be requested again)
|
|
1281
|
+
for (const auto& req : expired_requests) {
|
|
1282
|
+
uint32_t block_index = req.offset / BLOCK_SIZE;
|
|
1283
|
+
torrent_->reset_block_request(req.piece_index, block_index);
|
|
1284
|
+
LOG_BT_DEBUG("Request timeout: piece " << req.piece_index << " block " << block_index
|
|
1285
|
+
<< " from peer " << peer_info_.ip << ":" << peer_info_.port);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
void PeerConnection::update_speed_stats() {
|
|
1290
|
+
std::lock_guard<std::mutex> lock(speed_mutex_);
|
|
1291
|
+
|
|
1292
|
+
auto now = std::chrono::steady_clock::now();
|
|
1293
|
+
double elapsed_seconds = std::chrono::duration<double>(now - last_speed_update_time_).count();
|
|
1294
|
+
|
|
1295
|
+
// Update speed every second at minimum to avoid division by very small numbers
|
|
1296
|
+
if (elapsed_seconds >= 1.0) {
|
|
1297
|
+
uint64_t current_downloaded = downloaded_bytes_.load();
|
|
1298
|
+
uint64_t current_uploaded = uploaded_bytes_.load();
|
|
1299
|
+
|
|
1300
|
+
uint64_t downloaded_delta = current_downloaded - last_speed_downloaded_;
|
|
1301
|
+
uint64_t uploaded_delta = current_uploaded - last_speed_uploaded_;
|
|
1302
|
+
|
|
1303
|
+
download_speed_.store(static_cast<double>(downloaded_delta) / elapsed_seconds);
|
|
1304
|
+
upload_speed_.store(static_cast<double>(uploaded_delta) / elapsed_seconds);
|
|
1305
|
+
|
|
1306
|
+
last_speed_downloaded_ = current_downloaded;
|
|
1307
|
+
last_speed_uploaded_ = current_uploaded;
|
|
1308
|
+
last_speed_update_time_ = now;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
double PeerConnection::get_download_speed() const {
|
|
1313
|
+
return download_speed_.load();
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
double PeerConnection::get_upload_speed() const {
|
|
1317
|
+
return upload_speed_.load();
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
bool PeerConnection::read_data(std::vector<uint8_t>& buffer, size_t length) {
|
|
1321
|
+
if (!is_valid_socket(socket_)) {
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// Use the existing socket function to read exact bytes
|
|
1326
|
+
std::vector<uint8_t> data = receive_exact_bytes(socket_, length);
|
|
1327
|
+
if (data.size() != length) {
|
|
1328
|
+
// receive_exact_bytes returns empty on socket error or connection closed
|
|
1329
|
+
// If we got 0 bytes, the socket is likely dead - mark connection as error
|
|
1330
|
+
if (data.empty()) {
|
|
1331
|
+
LOG_BT_DEBUG("Socket read returned empty - connection likely closed by peer "
|
|
1332
|
+
<< peer_info_.ip << ":" << peer_info_.port);
|
|
1333
|
+
state_.store(PeerState::ERROR);
|
|
1334
|
+
} else {
|
|
1335
|
+
LOG_BT_DEBUG("Failed to read exact amount of data from socket (got "
|
|
1336
|
+
<< data.size() << " of " << length << " bytes)");
|
|
1337
|
+
}
|
|
1338
|
+
return false;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
buffer = std::move(data);
|
|
1342
|
+
return true;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
bool PeerConnection::write_data(const std::vector<uint8_t>& data) {
|
|
1346
|
+
if (!is_valid_socket(socket_)) {
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Use the existing socket function for binary data directly
|
|
1351
|
+
int sent = send_tcp_data(socket_, data);
|
|
1352
|
+
return sent > 0 && static_cast<size_t>(sent) == data.size();
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
//=============================================================================
|
|
1356
|
+
// MetadataDownload Implementation
|
|
1357
|
+
//=============================================================================
|
|
1358
|
+
|
|
1359
|
+
MetadataDownload::MetadataDownload(const InfoHash& info_hash, size_t metadata_size)
|
|
1360
|
+
: info_hash_(info_hash), metadata_size_(metadata_size), completion_triggered_(false) {
|
|
1361
|
+
|
|
1362
|
+
// Calculate number of pieces needed
|
|
1363
|
+
num_pieces_ = (metadata_size + METADATA_PIECE_SIZE - 1) / METADATA_PIECE_SIZE;
|
|
1364
|
+
|
|
1365
|
+
// Initialize piece storage
|
|
1366
|
+
pieces_.resize(num_pieces_);
|
|
1367
|
+
pieces_complete_.resize(num_pieces_, false);
|
|
1368
|
+
|
|
1369
|
+
LOG_BT_INFO("Created metadata download for info hash " << info_hash_to_hex(info_hash)
|
|
1370
|
+
<< " (size: " << metadata_size << " bytes, " << num_pieces_ << " pieces)");
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
MetadataDownload::~MetadataDownload() = default;
|
|
1374
|
+
|
|
1375
|
+
bool MetadataDownload::store_metadata_piece(uint32_t piece_index, const std::vector<uint8_t>& data) {
|
|
1376
|
+
MetadataCompleteCallback callback_to_invoke;
|
|
1377
|
+
TorrentInfo completed_torrent_info;
|
|
1378
|
+
bool should_invoke_callback = false;
|
|
1379
|
+
|
|
1380
|
+
{
|
|
1381
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
1382
|
+
|
|
1383
|
+
if (piece_index >= num_pieces_) {
|
|
1384
|
+
LOG_BT_ERROR("Invalid metadata piece index: " << piece_index << " (max: " << num_pieces_ - 1 << ")");
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Skip if this piece is already complete (received from another peer)
|
|
1389
|
+
if (pieces_complete_[piece_index]) {
|
|
1390
|
+
LOG_BT_DEBUG("Metadata piece " << piece_index << " already received, skipping duplicate");
|
|
1391
|
+
return true;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Skip if completion callback was already triggered
|
|
1395
|
+
if (completion_triggered_) {
|
|
1396
|
+
LOG_BT_DEBUG("Metadata download already completed, ignoring piece " << piece_index);
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// Calculate expected piece size
|
|
1401
|
+
size_t expected_size;
|
|
1402
|
+
if (piece_index == num_pieces_ - 1) {
|
|
1403
|
+
// Last piece might be smaller
|
|
1404
|
+
expected_size = metadata_size_ - (piece_index * METADATA_PIECE_SIZE);
|
|
1405
|
+
} else {
|
|
1406
|
+
expected_size = METADATA_PIECE_SIZE;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (data.size() != expected_size) {
|
|
1410
|
+
LOG_BT_ERROR("Invalid metadata piece size for piece " << piece_index
|
|
1411
|
+
<< ": expected " << expected_size << ", got " << data.size());
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
pieces_[piece_index] = data;
|
|
1416
|
+
pieces_complete_[piece_index] = true;
|
|
1417
|
+
|
|
1418
|
+
LOG_BT_DEBUG("Stored metadata piece " << piece_index << " (" << data.size() << " bytes)");
|
|
1419
|
+
|
|
1420
|
+
// Check if download is complete and prepare callback data
|
|
1421
|
+
// (callback will be invoked outside of lock to prevent deadlock)
|
|
1422
|
+
bool all_complete = std::all_of(pieces_complete_.begin(), pieces_complete_.end(),
|
|
1423
|
+
[](bool complete) { return complete; });
|
|
1424
|
+
|
|
1425
|
+
if (all_complete && !completion_triggered_) {
|
|
1426
|
+
completion_triggered_ = true; // Ensure callback is only triggered once
|
|
1427
|
+
LOG_BT_INFO("Metadata download complete for info hash " << info_hash_to_hex(info_hash_));
|
|
1428
|
+
|
|
1429
|
+
// Verify metadata (use unlocked version since we already hold the mutex)
|
|
1430
|
+
if (verify_metadata_unlocked()) {
|
|
1431
|
+
// Parse metadata into TorrentInfo (use unlocked version)
|
|
1432
|
+
std::vector<uint8_t> metadata = get_metadata_unlocked();
|
|
1433
|
+
|
|
1434
|
+
try {
|
|
1435
|
+
BencodeValue info_dict = bencode::decode(metadata);
|
|
1436
|
+
|
|
1437
|
+
// Create a fake torrent dictionary with just the info dict
|
|
1438
|
+
BencodeValue torrent_data = BencodeValue::create_dict();
|
|
1439
|
+
torrent_data["info"] = info_dict;
|
|
1440
|
+
|
|
1441
|
+
if (completed_torrent_info.load_from_bencode(torrent_data)) {
|
|
1442
|
+
callback_to_invoke = completion_callback_;
|
|
1443
|
+
should_invoke_callback = true;
|
|
1444
|
+
} else {
|
|
1445
|
+
LOG_BT_ERROR("Failed to parse metadata into TorrentInfo");
|
|
1446
|
+
}
|
|
1447
|
+
} catch (const std::exception& e) {
|
|
1448
|
+
LOG_BT_ERROR("Failed to decode metadata: " << e.what());
|
|
1449
|
+
}
|
|
1450
|
+
} else {
|
|
1451
|
+
LOG_BT_ERROR("Metadata verification failed - hash mismatch");
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
} // mutex_ released here
|
|
1455
|
+
|
|
1456
|
+
// Invoke callback OUTSIDE of lock to prevent deadlock
|
|
1457
|
+
// (as documented: "never call callbacks while holding this mutex!")
|
|
1458
|
+
if (should_invoke_callback && callback_to_invoke) {
|
|
1459
|
+
callback_to_invoke(completed_torrent_info);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
return true;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
bool MetadataDownload::is_complete() const {
|
|
1466
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
1467
|
+
return completion_triggered_ || std::all_of(pieces_complete_.begin(), pieces_complete_.end(), [](bool complete) { return complete; });
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
std::vector<uint8_t> MetadataDownload::get_metadata_unlocked() const {
|
|
1471
|
+
// Internal version - must be called with mutex_ already held
|
|
1472
|
+
std::vector<uint8_t> metadata;
|
|
1473
|
+
metadata.reserve(metadata_size_);
|
|
1474
|
+
|
|
1475
|
+
for (const auto& piece : pieces_) {
|
|
1476
|
+
metadata.insert(metadata.end(), piece.begin(), piece.end());
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
return metadata;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
std::vector<uint8_t> MetadataDownload::get_metadata() const {
|
|
1483
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
1484
|
+
return get_metadata_unlocked();
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
bool MetadataDownload::verify_metadata_unlocked() const {
|
|
1488
|
+
// Internal version - must be called with mutex_ already held
|
|
1489
|
+
|
|
1490
|
+
// Reconstruct full metadata
|
|
1491
|
+
std::vector<uint8_t> metadata;
|
|
1492
|
+
metadata.reserve(metadata_size_);
|
|
1493
|
+
|
|
1494
|
+
for (const auto& piece : pieces_) {
|
|
1495
|
+
metadata.insert(metadata.end(), piece.begin(), piece.end());
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Calculate SHA1 hash
|
|
1499
|
+
std::string calculated_hash = SHA1::hash_bytes(metadata);
|
|
1500
|
+
|
|
1501
|
+
// Convert stored info hash to hex string for comparison
|
|
1502
|
+
std::ostringstream stored_hash_hex;
|
|
1503
|
+
for (size_t i = 0; i < info_hash_.size(); ++i) {
|
|
1504
|
+
stored_hash_hex << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(info_hash_[i]);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
bool verified = (calculated_hash == stored_hash_hex.str());
|
|
1508
|
+
|
|
1509
|
+
if (verified) {
|
|
1510
|
+
LOG_BT_INFO("Metadata verification PASSED for info hash " << info_hash_to_hex(info_hash_));
|
|
1511
|
+
} else {
|
|
1512
|
+
LOG_BT_ERROR("Metadata verification FAILED for info hash " << info_hash_to_hex(info_hash_));
|
|
1513
|
+
LOG_BT_ERROR(" Expected: " << stored_hash_hex.str());
|
|
1514
|
+
LOG_BT_ERROR(" Calculated: " << calculated_hash);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
return verified;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
bool MetadataDownload::verify_metadata() const {
|
|
1521
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
1522
|
+
return verify_metadata_unlocked();
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
uint32_t MetadataDownload::get_next_piece_to_request() const {
|
|
1526
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
1527
|
+
|
|
1528
|
+
for (uint32_t i = 0; i < num_pieces_; ++i) {
|
|
1529
|
+
if (!pieces_complete_[i]) {
|
|
1530
|
+
return i;
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
return num_pieces_; // All pieces complete
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
bool MetadataDownload::is_piece_complete(uint32_t piece_index) const {
|
|
1538
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
1539
|
+
|
|
1540
|
+
if (piece_index >= num_pieces_) {
|
|
1541
|
+
return false;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
return pieces_complete_[piece_index];
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
//=============================================================================
|
|
1548
|
+
// TorrentDownload Implementation
|
|
1549
|
+
//=============================================================================
|
|
1550
|
+
|
|
1551
|
+
TorrentDownload::TorrentDownload(const TorrentInfo& torrent_info, const std::string& download_path)
|
|
1552
|
+
: torrent_info_(torrent_info), download_path_(download_path),
|
|
1553
|
+
running_(false), paused_(false), total_downloaded_(0), total_uploaded_(0),
|
|
1554
|
+
last_speed_update_time_(std::chrono::steady_clock::now()),
|
|
1555
|
+
last_speed_downloaded_(0), last_speed_uploaded_(0),
|
|
1556
|
+
download_speed_(0.0), upload_speed_(0.0),
|
|
1557
|
+
last_logged_progress_(0.0), last_state_log_time_(std::chrono::steady_clock::now()) {
|
|
1558
|
+
|
|
1559
|
+
// Initialize pieces
|
|
1560
|
+
uint32_t num_pieces = torrent_info_.get_num_pieces();
|
|
1561
|
+
pieces_.reserve(num_pieces);
|
|
1562
|
+
piece_completed_.resize(num_pieces, false);
|
|
1563
|
+
piece_downloading_.resize(num_pieces, false);
|
|
1564
|
+
|
|
1565
|
+
const auto& piece_hashes = torrent_info_.get_piece_hashes();
|
|
1566
|
+
for (uint32_t i = 0; i < num_pieces; ++i) {
|
|
1567
|
+
uint32_t piece_length = torrent_info_.get_piece_length(i);
|
|
1568
|
+
pieces_.push_back(std::make_unique<PieceInfo>(i, piece_hashes[i], piece_length));
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// Generate peer ID
|
|
1572
|
+
our_peer_id_ = generate_peer_id();
|
|
1573
|
+
|
|
1574
|
+
// Create tracker manager
|
|
1575
|
+
tracker_manager_ = std::make_unique<TrackerManager>(torrent_info_);
|
|
1576
|
+
|
|
1577
|
+
LOG_BT_INFO("Created torrent download for: " << torrent_info_.get_name());
|
|
1578
|
+
LOG_BT_INFO(" Download path: " << download_path_);
|
|
1579
|
+
LOG_BT_INFO(" Total size: " << torrent_info_.get_total_length() << " bytes");
|
|
1580
|
+
LOG_BT_INFO(" Number of pieces: " << num_pieces);
|
|
1581
|
+
LOG_BT_INFO(" Trackers: " << tracker_manager_->get_tracker_urls().size());
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
TorrentDownload::~TorrentDownload() {
|
|
1585
|
+
stop();
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
bool TorrentDownload::start() {
|
|
1589
|
+
if (running_) {
|
|
1590
|
+
return true;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
LOG_BT_INFO("Starting torrent download: " << torrent_info_.get_name());
|
|
1594
|
+
|
|
1595
|
+
// Skip file/directory operations for metadata-only downloads (empty download path)
|
|
1596
|
+
if (!download_path_.empty()) {
|
|
1597
|
+
// Create directory structure
|
|
1598
|
+
if (!create_directory_structure()) {
|
|
1599
|
+
LOG_BT_ERROR("Failed to create directory structure");
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Open files
|
|
1604
|
+
if (!open_files()) {
|
|
1605
|
+
LOG_BT_ERROR("Failed to open files");
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
} else {
|
|
1609
|
+
LOG_BT_DEBUG("Skipping file operations for metadata-only torrent");
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
running_ = true;
|
|
1613
|
+
paused_ = false;
|
|
1614
|
+
|
|
1615
|
+
// Start download threads
|
|
1616
|
+
download_thread_ = std::thread(&TorrentDownload::download_loop, this);
|
|
1617
|
+
peer_management_thread_ = std::thread(&TorrentDownload::peer_management_loop, this);
|
|
1618
|
+
|
|
1619
|
+
LOG_BT_INFO("Torrent download started: " << torrent_info_.get_name());
|
|
1620
|
+
return true;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
void TorrentDownload::stop() {
|
|
1624
|
+
if (!running_) {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
LOG_BT_INFO("Stopping torrent download: " << torrent_info_.get_name());
|
|
1629
|
+
|
|
1630
|
+
running_ = false;
|
|
1631
|
+
|
|
1632
|
+
// Stop all peer connections
|
|
1633
|
+
{
|
|
1634
|
+
std::lock_guard<std::mutex> lock(peers_mutex_);
|
|
1635
|
+
for (auto& peer : peer_connections_) {
|
|
1636
|
+
peer->disconnect();
|
|
1637
|
+
}
|
|
1638
|
+
peer_connections_.clear();
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// Wait for threads to finish
|
|
1642
|
+
if (download_thread_.joinable()) {
|
|
1643
|
+
download_thread_.join();
|
|
1644
|
+
}
|
|
1645
|
+
if (peer_management_thread_.joinable()) {
|
|
1646
|
+
peer_management_thread_.join();
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// Close files
|
|
1650
|
+
close_files();
|
|
1651
|
+
|
|
1652
|
+
LOG_BT_INFO("Torrent download stopped: " << torrent_info_.get_name());
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
void TorrentDownload::pause() {
|
|
1656
|
+
paused_ = true;
|
|
1657
|
+
LOG_BT_INFO("Paused torrent download: " << torrent_info_.get_name());
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
void TorrentDownload::resume() {
|
|
1661
|
+
paused_ = false;
|
|
1662
|
+
LOG_BT_INFO("Resumed torrent download: " << torrent_info_.get_name());
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
bool TorrentDownload::is_complete() const {
|
|
1666
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
1667
|
+
return std::all_of(piece_completed_.begin(), piece_completed_.end(), [](bool completed) { return completed; });
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
bool TorrentDownload::add_peer(const Peer& peer) {
|
|
1671
|
+
std::lock_guard<std::mutex> lock(peers_mutex_);
|
|
1672
|
+
|
|
1673
|
+
// Check if peer already exists
|
|
1674
|
+
for (const auto& conn : peer_connections_) {
|
|
1675
|
+
if (conn->get_peer_info().ip == peer.ip && conn->get_peer_info().port == peer.port) {
|
|
1676
|
+
return false; // Peer already exists
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Check peer limit
|
|
1681
|
+
if (peer_connections_.size() >= MAX_PEERS_PER_TORRENT) {
|
|
1682
|
+
LOG_BT_DEBUG("Peer limit reached for torrent " << torrent_info_.get_name());
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// Create new peer connection
|
|
1687
|
+
auto peer_conn = std::make_unique<PeerConnection>(this, peer);
|
|
1688
|
+
if (peer_conn->connect()) {
|
|
1689
|
+
peer_connections_.push_back(std::move(peer_conn));
|
|
1690
|
+
|
|
1691
|
+
if (peer_connected_callback_) {
|
|
1692
|
+
peer_connected_callback_(peer);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
LOG_BT_INFO("Added peer " << peer.ip << ":" << peer.port << " to torrent " << torrent_info_.get_name());
|
|
1696
|
+
return true;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
return false;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
bool TorrentDownload::add_peer(const Peer& peer, socket_t existing_socket) {
|
|
1703
|
+
std::lock_guard<std::mutex> lock(peers_mutex_);
|
|
1704
|
+
|
|
1705
|
+
// Check if peer already exists
|
|
1706
|
+
for (const auto& conn : peer_connections_) {
|
|
1707
|
+
if (conn->get_peer_info().ip == peer.ip && conn->get_peer_info().port == peer.port) {
|
|
1708
|
+
LOG_BT_DEBUG("Peer already exists: " << peer.ip << ":" << peer.port);
|
|
1709
|
+
close_socket(existing_socket); // Close the duplicate incoming connection
|
|
1710
|
+
return false;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// Check peer limit
|
|
1715
|
+
if (peer_connections_.size() >= MAX_PEERS_PER_TORRENT) {
|
|
1716
|
+
LOG_BT_DEBUG("Peer limit reached for torrent " << torrent_info_.get_name());
|
|
1717
|
+
close_socket(existing_socket);
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Create peer connection with existing socket
|
|
1722
|
+
auto peer_conn = std::make_unique<PeerConnection>(this, peer, existing_socket);
|
|
1723
|
+
if (peer_conn->connect()) {
|
|
1724
|
+
peer_connections_.push_back(std::move(peer_conn));
|
|
1725
|
+
|
|
1726
|
+
if (peer_connected_callback_) {
|
|
1727
|
+
peer_connected_callback_(peer);
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
LOG_BT_INFO("Added incoming peer " << peer.ip << ":" << peer.port << " to torrent " << torrent_info_.get_name());
|
|
1731
|
+
return true;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
void TorrentDownload::remove_peer(const Peer& peer) {
|
|
1738
|
+
std::lock_guard<std::mutex> lock(peers_mutex_);
|
|
1739
|
+
|
|
1740
|
+
peer_connections_.erase(
|
|
1741
|
+
std::remove_if(peer_connections_.begin(), peer_connections_.end(),
|
|
1742
|
+
[&peer](const std::unique_ptr<PeerConnection>& conn) {
|
|
1743
|
+
return conn->get_peer_info().ip == peer.ip && conn->get_peer_info().port == peer.port;
|
|
1744
|
+
}),
|
|
1745
|
+
peer_connections_.end());
|
|
1746
|
+
|
|
1747
|
+
if (peer_disconnected_callback_) {
|
|
1748
|
+
peer_disconnected_callback_(peer);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
LOG_BT_DEBUG("Removed peer " << peer.ip << ":" << peer.port << " from torrent " << torrent_info_.get_name());
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
size_t TorrentDownload::get_peer_count() const {
|
|
1755
|
+
std::lock_guard<std::mutex> lock(peers_mutex_);
|
|
1756
|
+
return peer_connections_.size();
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
std::vector<Peer> TorrentDownload::get_connected_peers() const {
|
|
1760
|
+
std::lock_guard<std::mutex> lock(peers_mutex_);
|
|
1761
|
+
|
|
1762
|
+
std::vector<Peer> peers;
|
|
1763
|
+
for (const auto& conn : peer_connections_) {
|
|
1764
|
+
if (conn->is_connected()) {
|
|
1765
|
+
peers.push_back(conn->get_peer_info());
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
return peers;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
bool TorrentDownload::is_piece_complete(PieceIndex piece_index) const {
|
|
1773
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
1774
|
+
return piece_index < piece_completed_.size() && piece_completed_[piece_index];
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
bool TorrentDownload::is_piece_downloading(PieceIndex piece_index) const {
|
|
1778
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
1779
|
+
return piece_index < piece_downloading_.size() && piece_downloading_[piece_index];
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
bool TorrentDownload::store_piece_block(PieceIndex piece_index, uint32_t offset, const std::vector<uint8_t>& data) {
|
|
1783
|
+
bool piece_just_completed = false;
|
|
1784
|
+
bool verification_failed = false;
|
|
1785
|
+
|
|
1786
|
+
{
|
|
1787
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
1788
|
+
|
|
1789
|
+
if (piece_index >= pieces_.size()) {
|
|
1790
|
+
LOG_BT_ERROR("Invalid piece index: " << piece_index);
|
|
1791
|
+
return false;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
auto& piece = pieces_[piece_index];
|
|
1795
|
+
|
|
1796
|
+
// Validate offset and data size
|
|
1797
|
+
if (offset + data.size() > piece->length) {
|
|
1798
|
+
LOG_BT_ERROR("Block data exceeds piece length for piece " << piece_index);
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
// Calculate block index
|
|
1803
|
+
uint32_t block_index = offset / BLOCK_SIZE;
|
|
1804
|
+
if (block_index >= piece->get_num_blocks()) {
|
|
1805
|
+
LOG_BT_ERROR("Invalid block index: " << block_index << " for piece " << piece_index);
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// Ensure piece data buffer is allocated (lazy allocation)
|
|
1810
|
+
piece->ensure_data_allocated();
|
|
1811
|
+
|
|
1812
|
+
// Store the block data
|
|
1813
|
+
std::copy(data.begin(), data.end(), piece->data.begin() + offset);
|
|
1814
|
+
piece->blocks_downloaded[block_index] = true;
|
|
1815
|
+
|
|
1816
|
+
LOG_BT_DEBUG("Stored block " << block_index << " for piece " << piece_index
|
|
1817
|
+
<< " (offset: " << offset << ", size: " << data.size() << ")");
|
|
1818
|
+
|
|
1819
|
+
// Check if piece is complete
|
|
1820
|
+
if (piece->is_complete() && !piece->verified) {
|
|
1821
|
+
LOG_BT_INFO("Piece " << piece_index << " downloaded, verifying...");
|
|
1822
|
+
if (verify_piece(piece_index)) {
|
|
1823
|
+
piece_completed_[piece_index] = true;
|
|
1824
|
+
piece_downloading_[piece_index] = false;
|
|
1825
|
+
|
|
1826
|
+
// Write piece to disk (files_mutex_ is after pieces_mutex_ in lock order)
|
|
1827
|
+
write_piece_to_disk(piece_index);
|
|
1828
|
+
|
|
1829
|
+
// Update statistics
|
|
1830
|
+
total_downloaded_ += piece->length;
|
|
1831
|
+
|
|
1832
|
+
piece_just_completed = true;
|
|
1833
|
+
LOG_BT_INFO("Piece " << piece_index << " verified and saved");
|
|
1834
|
+
} else {
|
|
1835
|
+
LOG_BT_ERROR("Piece " << piece_index << " verification failed, requesting re-download");
|
|
1836
|
+
// Reset piece for re-download - must reset both downloaded and requested status
|
|
1837
|
+
std::fill(piece->blocks_downloaded.begin(), piece->blocks_downloaded.end(), false);
|
|
1838
|
+
std::fill(piece->blocks_requested.begin(), piece->blocks_requested.end(), false);
|
|
1839
|
+
piece_downloading_[piece_index] = false;
|
|
1840
|
+
verification_failed = true;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
} // pieces_mutex_ released here
|
|
1844
|
+
|
|
1845
|
+
// Notify completion AFTER releasing pieces_mutex_ to respect lock order:
|
|
1846
|
+
// peers_mutex_ -> pieces_mutex_
|
|
1847
|
+
if (piece_just_completed) {
|
|
1848
|
+
on_piece_completed(piece_index);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
return !verification_failed;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
bool TorrentDownload::verify_piece(PieceIndex piece_index) {
|
|
1855
|
+
if (piece_index >= pieces_.size()) {
|
|
1856
|
+
return false;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
auto& piece = pieces_[piece_index];
|
|
1860
|
+
|
|
1861
|
+
// Check that piece data is allocated
|
|
1862
|
+
if (piece->data.empty()) {
|
|
1863
|
+
LOG_BT_ERROR("Cannot verify piece " << piece_index << " - data not allocated");
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// Calculate SHA1 hash of piece data
|
|
1868
|
+
std::string calculated_hash = SHA1::hash_bytes(piece->data);
|
|
1869
|
+
|
|
1870
|
+
// Convert stored hash to hex string for comparison
|
|
1871
|
+
std::ostringstream stored_hash_hex;
|
|
1872
|
+
for (size_t i = 0; i < piece->hash.size(); ++i) {
|
|
1873
|
+
stored_hash_hex << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(piece->hash[i]);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
bool verified = (calculated_hash == stored_hash_hex.str());
|
|
1877
|
+
piece->verified = verified;
|
|
1878
|
+
|
|
1879
|
+
LOG_BT_DEBUG("Piece " << piece_index << " verification: " << (verified ? "PASSED" : "FAILED"));
|
|
1880
|
+
return verified;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
void TorrentDownload::write_piece_to_disk(PieceIndex piece_index) {
|
|
1884
|
+
std::lock_guard<std::mutex> lock(files_mutex_);
|
|
1885
|
+
|
|
1886
|
+
// Skip for metadata-only torrents
|
|
1887
|
+
if (download_path_.empty()) {
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
if (piece_index >= pieces_.size()) {
|
|
1892
|
+
LOG_BT_ERROR("Invalid piece index for disk write: " << piece_index);
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
auto& piece = pieces_[piece_index];
|
|
1897
|
+
if (!piece->verified) {
|
|
1898
|
+
LOG_BT_ERROR("Attempting to write unverified piece " << piece_index << " to disk");
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// Calculate piece offset in the torrent
|
|
1903
|
+
uint64_t piece_offset = static_cast<uint64_t>(piece_index) * torrent_info_.get_piece_length();
|
|
1904
|
+
uint64_t remaining_data = piece->length;
|
|
1905
|
+
uint64_t data_offset = 0;
|
|
1906
|
+
|
|
1907
|
+
// Write piece data to the appropriate files
|
|
1908
|
+
for (const auto& file_info : torrent_info_.get_files()) {
|
|
1909
|
+
if (piece_offset >= file_info.offset + file_info.length) {
|
|
1910
|
+
continue; // This piece doesn't overlap with this file
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
if (piece_offset + piece->length <= file_info.offset) {
|
|
1914
|
+
break; // No more files will be affected by this piece
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// Calculate overlap
|
|
1918
|
+
uint64_t file_start_in_piece = (file_info.offset > piece_offset) ?
|
|
1919
|
+
file_info.offset - piece_offset : 0;
|
|
1920
|
+
uint64_t piece_end = piece_offset + piece->length;
|
|
1921
|
+
uint64_t file_end = file_info.offset + file_info.length;
|
|
1922
|
+
uint64_t write_end = (std::min)(piece_end, file_end);
|
|
1923
|
+
uint64_t write_length = write_end - (piece_offset + file_start_in_piece);
|
|
1924
|
+
|
|
1925
|
+
if (write_length == 0) {
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// Calculate file offset
|
|
1930
|
+
uint64_t file_offset = (piece_offset > file_info.offset) ?
|
|
1931
|
+
piece_offset - file_info.offset : 0;
|
|
1932
|
+
|
|
1933
|
+
// Write data chunk to file using fs module
|
|
1934
|
+
std::string file_path = download_path_ + "/" + file_info.path;
|
|
1935
|
+
const void* write_data = piece->data.data() + file_start_in_piece;
|
|
1936
|
+
|
|
1937
|
+
if (!write_file_chunk(file_path.c_str(), file_offset, write_data, write_length)) {
|
|
1938
|
+
LOG_BT_ERROR("Failed to write data to file: " << file_path);
|
|
1939
|
+
continue;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
LOG_BT_DEBUG("Wrote " << write_length << " bytes to file " << file_info.path
|
|
1943
|
+
<< " at offset " << file_offset);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
// Free piece data from memory after successful write to disk
|
|
1947
|
+
piece->free_data();
|
|
1948
|
+
LOG_BT_DEBUG("Freed piece " << piece_index << " data from memory after write to disk");
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
bool TorrentDownload::read_piece_from_disk(PieceIndex piece_index, std::vector<uint8_t>& data) {
|
|
1952
|
+
// Lock order: pieces_mutex_ -> files_mutex_
|
|
1953
|
+
std::lock_guard<std::mutex> pieces_lock(pieces_mutex_);
|
|
1954
|
+
std::lock_guard<std::mutex> files_lock(files_mutex_);
|
|
1955
|
+
|
|
1956
|
+
// Skip for metadata-only torrents
|
|
1957
|
+
if (download_path_.empty()) {
|
|
1958
|
+
return false;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
if (piece_index >= pieces_.size()) {
|
|
1962
|
+
LOG_BT_ERROR("Invalid piece index for disk read: " << piece_index);
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// Check if piece is complete
|
|
1967
|
+
if (!piece_completed_[piece_index]) {
|
|
1968
|
+
LOG_BT_DEBUG("Piece " << piece_index << " is not complete, can't read from disk");
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
auto& piece = pieces_[piece_index];
|
|
1973
|
+
|
|
1974
|
+
// Allocate buffer for piece data
|
|
1975
|
+
data.resize(piece->length);
|
|
1976
|
+
|
|
1977
|
+
// Calculate piece offset in the torrent
|
|
1978
|
+
uint64_t piece_offset = static_cast<uint64_t>(piece_index) * torrent_info_.get_piece_length();
|
|
1979
|
+
uint64_t data_offset = 0;
|
|
1980
|
+
|
|
1981
|
+
// Read piece data from the appropriate files
|
|
1982
|
+
for (const auto& file_info : torrent_info_.get_files()) {
|
|
1983
|
+
if (piece_offset >= file_info.offset + file_info.length) {
|
|
1984
|
+
continue; // This piece doesn't overlap with this file
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
if (piece_offset + piece->length <= file_info.offset) {
|
|
1988
|
+
break; // No more files will be affected by this piece
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// Calculate overlap
|
|
1992
|
+
uint64_t file_start_in_piece = (file_info.offset > piece_offset) ?
|
|
1993
|
+
file_info.offset - piece_offset : 0;
|
|
1994
|
+
uint64_t piece_end = piece_offset + piece->length;
|
|
1995
|
+
uint64_t file_end = file_info.offset + file_info.length;
|
|
1996
|
+
uint64_t read_end = (std::min)(piece_end, file_end);
|
|
1997
|
+
uint64_t read_length = read_end - (piece_offset + file_start_in_piece);
|
|
1998
|
+
|
|
1999
|
+
if (read_length == 0) {
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// Calculate file offset
|
|
2004
|
+
uint64_t file_offset = (piece_offset > file_info.offset) ?
|
|
2005
|
+
piece_offset - file_info.offset : 0;
|
|
2006
|
+
|
|
2007
|
+
// Read data chunk from file using fs module
|
|
2008
|
+
std::string file_path = download_path_ + "/" + file_info.path;
|
|
2009
|
+
void* read_buffer = data.data() + file_start_in_piece;
|
|
2010
|
+
|
|
2011
|
+
if (!read_file_chunk(file_path.c_str(), file_offset, read_buffer, read_length)) {
|
|
2012
|
+
LOG_BT_ERROR("Failed to read data from file: " << file_path);
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
LOG_BT_DEBUG("Read " << read_length << " bytes from file " << file_info.path
|
|
2017
|
+
<< " at offset " << file_offset);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
return true;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
void TorrentDownload::reset_block_request(PieceIndex piece_index, uint32_t block_index) {
|
|
2024
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
2025
|
+
|
|
2026
|
+
if (piece_index >= pieces_.size()) {
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
auto& piece = pieces_[piece_index];
|
|
2031
|
+
if (block_index < piece->blocks_requested.size()) {
|
|
2032
|
+
// Only reset if not already downloaded
|
|
2033
|
+
if (!piece->blocks_downloaded[block_index]) {
|
|
2034
|
+
piece->blocks_requested[block_index] = false;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
std::vector<PieceIndex> TorrentDownload::get_available_pieces() const {
|
|
2040
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
2041
|
+
std::vector<PieceIndex> available_pieces;
|
|
2042
|
+
|
|
2043
|
+
for (PieceIndex i = 0; i < piece_completed_.size(); ++i) {
|
|
2044
|
+
if (piece_completed_[i]) {
|
|
2045
|
+
available_pieces.push_back(i);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
return available_pieces;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
std::vector<PieceIndex> TorrentDownload::get_needed_pieces(const std::vector<bool>& peer_bitfield) const {
|
|
2053
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
2054
|
+
std::vector<PieceIndex> needed_pieces;
|
|
2055
|
+
|
|
2056
|
+
size_t min_size = (std::min)(peer_bitfield.size(), piece_completed_.size());
|
|
2057
|
+
for (size_t i = 0; i < min_size; ++i) {
|
|
2058
|
+
// Include pieces that are not complete (even if downloading - peer can help complete them)
|
|
2059
|
+
if (peer_bitfield[i] && !piece_completed_[i]) {
|
|
2060
|
+
needed_pieces.push_back(static_cast<PieceIndex>(i));
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
return needed_pieces;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
uint64_t TorrentDownload::get_downloaded_bytes() const {
|
|
2068
|
+
return total_downloaded_.load();
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
uint64_t TorrentDownload::get_uploaded_bytes() const {
|
|
2072
|
+
return total_uploaded_.load();
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
double TorrentDownload::get_progress_percentage() const {
|
|
2076
|
+
if (torrent_info_.get_total_length() == 0) {
|
|
2077
|
+
return 0.0;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
uint64_t downloaded = get_downloaded_bytes();
|
|
2081
|
+
return (static_cast<double>(downloaded) / torrent_info_.get_total_length()) * 100.0;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
uint32_t TorrentDownload::get_completed_pieces() const {
|
|
2085
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
2086
|
+
return static_cast<uint32_t>(std::count(piece_completed_.begin(), piece_completed_.end(), true));
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
std::vector<bool> TorrentDownload::get_piece_bitfield() const {
|
|
2090
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
2091
|
+
return piece_completed_;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
void TorrentDownload::update_speed_stats() {
|
|
2095
|
+
std::lock_guard<std::mutex> lock(speed_mutex_);
|
|
2096
|
+
|
|
2097
|
+
auto now = std::chrono::steady_clock::now();
|
|
2098
|
+
double elapsed_seconds = std::chrono::duration<double>(now - last_speed_update_time_).count();
|
|
2099
|
+
|
|
2100
|
+
// Update speed every second at minimum to avoid division by very small numbers
|
|
2101
|
+
if (elapsed_seconds >= 1.0) {
|
|
2102
|
+
uint64_t current_downloaded = total_downloaded_.load();
|
|
2103
|
+
uint64_t current_uploaded = total_uploaded_.load();
|
|
2104
|
+
|
|
2105
|
+
uint64_t downloaded_delta = current_downloaded - last_speed_downloaded_;
|
|
2106
|
+
uint64_t uploaded_delta = current_uploaded - last_speed_uploaded_;
|
|
2107
|
+
|
|
2108
|
+
download_speed_.store(static_cast<double>(downloaded_delta) / elapsed_seconds);
|
|
2109
|
+
upload_speed_.store(static_cast<double>(uploaded_delta) / elapsed_seconds);
|
|
2110
|
+
|
|
2111
|
+
last_speed_downloaded_ = current_downloaded;
|
|
2112
|
+
last_speed_uploaded_ = current_uploaded;
|
|
2113
|
+
last_speed_update_time_ = now;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// Also update speed stats for all peer connections
|
|
2117
|
+
{
|
|
2118
|
+
std::lock_guard<std::mutex> peers_lock(peers_mutex_);
|
|
2119
|
+
for (auto& peer : peer_connections_) {
|
|
2120
|
+
peer->update_speed_stats();
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
double TorrentDownload::get_download_speed() const {
|
|
2126
|
+
return download_speed_.load();
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
double TorrentDownload::get_upload_speed() const {
|
|
2130
|
+
return upload_speed_.load();
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
void TorrentDownload::download_loop() {
|
|
2134
|
+
LOG_BT_INFO("Download loop started for torrent: " << torrent_info_.get_name());
|
|
2135
|
+
|
|
2136
|
+
// For metadata-only torrents, we don't download pieces
|
|
2137
|
+
// The loop just waits until stopped (metadata completion triggers stop from callback)
|
|
2138
|
+
bool is_metadata_only = torrent_info_.is_metadata_only();
|
|
2139
|
+
|
|
2140
|
+
while (running_ && (is_metadata_only || !is_complete())) {
|
|
2141
|
+
if (paused_) {
|
|
2142
|
+
// Use conditional variable for responsive shutdown
|
|
2143
|
+
{
|
|
2144
|
+
std::unique_lock<std::mutex> lock(shutdown_mutex_);
|
|
2145
|
+
if (shutdown_cv_.wait_for(lock, std::chrono::milliseconds(100), [this] { return !running_.load() || !paused_.load(); })) {
|
|
2146
|
+
if (!running_) break;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// Schedule piece requests to peers (skipped for metadata-only)
|
|
2153
|
+
schedule_piece_requests();
|
|
2154
|
+
|
|
2155
|
+
// Update progress (skipped for metadata-only since no pieces)
|
|
2156
|
+
if (!is_metadata_only) {
|
|
2157
|
+
update_progress();
|
|
2158
|
+
|
|
2159
|
+
// Check for completion
|
|
2160
|
+
check_torrent_completion();
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// Use conditional variable for responsive shutdown
|
|
2164
|
+
{
|
|
2165
|
+
std::unique_lock<std::mutex> lock(shutdown_mutex_);
|
|
2166
|
+
if (shutdown_cv_.wait_for(lock, std::chrono::milliseconds(50), [this] { return !running_.load(); })) {
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
LOG_BT_INFO("Download loop ended for torrent: " << torrent_info_.get_name());
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
void TorrentDownload::peer_management_loop() {
|
|
2176
|
+
LOG_BT_INFO("Peer management loop started for torrent: " << torrent_info_.get_name());
|
|
2177
|
+
|
|
2178
|
+
bool is_metadata_only = torrent_info_.is_metadata_only();
|
|
2179
|
+
|
|
2180
|
+
while (running_) {
|
|
2181
|
+
// Clean up disconnected peers
|
|
2182
|
+
cleanup_disconnected_peers();
|
|
2183
|
+
|
|
2184
|
+
// Skip detailed state logging for metadata-only torrents
|
|
2185
|
+
// (they don't have meaningful piece progress to report)
|
|
2186
|
+
if (!is_metadata_only) {
|
|
2187
|
+
// Periodically log detailed torrent state (every 10 seconds when actively downloading)
|
|
2188
|
+
auto now = std::chrono::steady_clock::now();
|
|
2189
|
+
bool is_actively_downloading = !is_complete() && !paused_.load();
|
|
2190
|
+
|
|
2191
|
+
// Log every 10 seconds when downloading, every 30 seconds when seeding/idle
|
|
2192
|
+
auto log_interval = is_actively_downloading ? std::chrono::seconds(10) : std::chrono::seconds(30);
|
|
2193
|
+
|
|
2194
|
+
if (now - last_state_log_time_ > log_interval) {
|
|
2195
|
+
last_state_log_time_ = now;
|
|
2196
|
+
|
|
2197
|
+
// Only log detailed state if we have peers or are actively doing something
|
|
2198
|
+
size_t peer_count = get_peer_count();
|
|
2199
|
+
if (peer_count > 0 || is_actively_downloading) {
|
|
2200
|
+
log_detailed_state();
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// Use conditional variable for responsive shutdown
|
|
2206
|
+
{
|
|
2207
|
+
std::unique_lock<std::mutex> lock(shutdown_mutex_);
|
|
2208
|
+
if (shutdown_cv_.wait_for(lock, std::chrono::seconds(1), [this] { return !running_.load(); })) {
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
LOG_BT_INFO("Peer management loop ended for torrent: " << torrent_info_.get_name());
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
void TorrentDownload::schedule_piece_requests() {
|
|
2218
|
+
// Skip piece scheduling for metadata-only torrents
|
|
2219
|
+
// These torrents only need to exchange metadata via BEP 9, not download pieces
|
|
2220
|
+
if (torrent_info_.is_metadata_only()) {
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
std::lock_guard<std::mutex> lock(peers_mutex_);
|
|
2225
|
+
|
|
2226
|
+
for (auto& peer : peer_connections_) {
|
|
2227
|
+
if (!peer->is_connected()) {
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// Get pieces we need that this peer has
|
|
2232
|
+
std::vector<PieceIndex> needed_pieces = get_needed_pieces(peer->get_bitfield());
|
|
2233
|
+
|
|
2234
|
+
// IMPORTANT: Express interest BEFORE checking choke state
|
|
2235
|
+
// We must send INTERESTED to get unchoked, even if we're currently choked
|
|
2236
|
+
if (!needed_pieces.empty() && !peer->is_interested()) {
|
|
2237
|
+
peer->set_interested(true);
|
|
2238
|
+
LOG_BT_DEBUG("Expressed interest to peer " << peer->get_peer_info().ip << ":" << peer->get_peer_info().port);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// Skip actual piece requests if we're choked or have no needed pieces
|
|
2242
|
+
if (peer->is_choked() || needed_pieces.empty()) {
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// Select pieces to download using our piece selection strategy
|
|
2247
|
+
std::vector<PieceIndex> selected_pieces = select_pieces_for_download();
|
|
2248
|
+
|
|
2249
|
+
// Request blocks from selected pieces
|
|
2250
|
+
for (PieceIndex piece_index : selected_pieces) {
|
|
2251
|
+
if (peer->get_pending_requests() >= MAX_REQUESTS_PER_PEER) {
|
|
2252
|
+
break; // Don't overwhelm this peer
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
if (!peer->has_piece(piece_index)) {
|
|
2256
|
+
continue;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// Find blocks we need for this piece
|
|
2260
|
+
{
|
|
2261
|
+
std::lock_guard<std::mutex> pieces_lock(pieces_mutex_);
|
|
2262
|
+
if (piece_index >= pieces_.size() || piece_completed_[piece_index]) {
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
auto& piece = pieces_[piece_index];
|
|
2267
|
+
uint32_t block_size = BLOCK_SIZE;
|
|
2268
|
+
|
|
2269
|
+
for (uint32_t block_index = 0; block_index < piece->get_num_blocks(); ++block_index) {
|
|
2270
|
+
if (piece->blocks_downloaded[block_index]) {
|
|
2271
|
+
continue; // Already have this block
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// Check if this block is already requested globally (from any peer)
|
|
2275
|
+
if (piece->blocks_requested[block_index]) {
|
|
2276
|
+
continue; // Already requested from another peer
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
uint32_t offset = block_index * BLOCK_SIZE;
|
|
2280
|
+
uint32_t length = (std::min)(block_size, piece->length - offset);
|
|
2281
|
+
|
|
2282
|
+
if (peer->request_piece_block(piece_index, offset, length)) {
|
|
2283
|
+
piece->blocks_requested[block_index] = true; // Mark as globally requested
|
|
2284
|
+
piece_downloading_[piece_index] = true;
|
|
2285
|
+
LOG_BT_DEBUG("Requested block " << block_index << " of piece " << piece_index
|
|
2286
|
+
<< " from peer " << peer->get_peer_info().ip);
|
|
2287
|
+
break; // Request one block at a time per piece per peer
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
void TorrentDownload::cleanup_disconnected_peers() {
|
|
2296
|
+
std::lock_guard<std::mutex> lock(peers_mutex_);
|
|
2297
|
+
|
|
2298
|
+
peer_connections_.erase(
|
|
2299
|
+
std::remove_if(peer_connections_.begin(), peer_connections_.end(),
|
|
2300
|
+
[](const std::unique_ptr<PeerConnection>& peer) {
|
|
2301
|
+
return !peer->is_connected();
|
|
2302
|
+
}),
|
|
2303
|
+
peer_connections_.end());
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
bool TorrentDownload::open_files() {
|
|
2307
|
+
std::lock_guard<std::mutex> lock(files_mutex_);
|
|
2308
|
+
|
|
2309
|
+
const auto& files = torrent_info_.get_files();
|
|
2310
|
+
|
|
2311
|
+
for (const auto& file_info : files) {
|
|
2312
|
+
std::string file_path = download_path_ + "/" + file_info.path;
|
|
2313
|
+
|
|
2314
|
+
// Pre-allocate file with correct size using fs module
|
|
2315
|
+
if (!create_file_with_size(file_path.c_str(), file_info.length)) {
|
|
2316
|
+
LOG_BT_ERROR("Failed to create file: " << file_path);
|
|
2317
|
+
return false;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
LOG_BT_DEBUG("Created file: " << file_path << " (size: " << file_info.length << ")");
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
return true;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
void TorrentDownload::close_files() {
|
|
2327
|
+
std::lock_guard<std::mutex> lock(files_mutex_);
|
|
2328
|
+
|
|
2329
|
+
// With fs module, files are opened/closed per operation
|
|
2330
|
+
// No persistent file handles to close
|
|
2331
|
+
LOG_BT_DEBUG("Files closed (using fs module, no persistent handles)");
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
bool TorrentDownload::create_directory_structure() {
|
|
2335
|
+
const auto& files = torrent_info_.get_files();
|
|
2336
|
+
|
|
2337
|
+
for (const auto& file_info : files) {
|
|
2338
|
+
std::string file_path = download_path_ + "/" + file_info.path;
|
|
2339
|
+
|
|
2340
|
+
// Extract directory path
|
|
2341
|
+
size_t last_slash = file_path.find_last_of('/');
|
|
2342
|
+
if (last_slash != std::string::npos) {
|
|
2343
|
+
std::string dir_path = file_path.substr(0, last_slash);
|
|
2344
|
+
|
|
2345
|
+
// Create directory structure
|
|
2346
|
+
if (!create_directories(dir_path.c_str())) {
|
|
2347
|
+
LOG_BT_ERROR("Failed to create directory: " << dir_path);
|
|
2348
|
+
return false;
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
return true;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
std::vector<PieceIndex> TorrentDownload::select_pieces_for_download() {
|
|
2357
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
2358
|
+
|
|
2359
|
+
std::vector<PieceIndex> downloading_pieces;
|
|
2360
|
+
std::vector<PieceIndex> new_pieces;
|
|
2361
|
+
|
|
2362
|
+
// Find pieces we need - prioritize pieces already being downloaded
|
|
2363
|
+
for (PieceIndex i = 0; i < piece_completed_.size(); ++i) {
|
|
2364
|
+
if (!piece_completed_[i]) {
|
|
2365
|
+
if (piece_downloading_[i]) {
|
|
2366
|
+
// Prioritize completing pieces we've already started
|
|
2367
|
+
downloading_pieces.push_back(i);
|
|
2368
|
+
} else {
|
|
2369
|
+
new_pieces.push_back(i);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// Implement piece selection strategy: complete existing pieces first, then start new ones
|
|
2375
|
+
std::vector<PieceIndex> selected_pieces;
|
|
2376
|
+
|
|
2377
|
+
// First, add pieces we're already downloading (prioritize completing them)
|
|
2378
|
+
for (PieceIndex piece : downloading_pieces) {
|
|
2379
|
+
selected_pieces.push_back(piece);
|
|
2380
|
+
if (selected_pieces.size() >= 10) break;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
// Then add new pieces if we have room
|
|
2384
|
+
for (PieceIndex piece : new_pieces) {
|
|
2385
|
+
if (selected_pieces.size() >= 10) break;
|
|
2386
|
+
selected_pieces.push_back(piece);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
return selected_pieces;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
PieceIndex TorrentDownload::select_rarest_piece(const std::vector<bool>& available_pieces) {
|
|
2393
|
+
// Lock order: peers_mutex_ -> pieces_mutex_
|
|
2394
|
+
std::lock_guard<std::mutex> peers_lock(peers_mutex_);
|
|
2395
|
+
std::lock_guard<std::mutex> pieces_lock(pieces_mutex_);
|
|
2396
|
+
|
|
2397
|
+
// Count how many peers have each piece
|
|
2398
|
+
std::vector<int> piece_counts(piece_completed_.size(), 0);
|
|
2399
|
+
|
|
2400
|
+
for (const auto& peer : peer_connections_) {
|
|
2401
|
+
if (!peer->is_connected()) {
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
const auto& peer_bitfield = peer->get_bitfield();
|
|
2406
|
+
for (size_t i = 0; i < peer_bitfield.size() && i < piece_counts.size(); ++i) {
|
|
2407
|
+
if (peer_bitfield[i]) {
|
|
2408
|
+
piece_counts[i]++;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// Find the rarest piece we need and is available
|
|
2414
|
+
int min_count = INT_MAX;
|
|
2415
|
+
PieceIndex rarest_piece = static_cast<PieceIndex>(-1);
|
|
2416
|
+
|
|
2417
|
+
for (size_t i = 0; i < available_pieces.size() && i < piece_completed_.size(); ++i) {
|
|
2418
|
+
if (available_pieces[i] && !piece_completed_[i] && !piece_downloading_[i]) {
|
|
2419
|
+
if (piece_counts[i] < min_count) {
|
|
2420
|
+
min_count = piece_counts[i];
|
|
2421
|
+
rarest_piece = static_cast<PieceIndex>(i);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
return rarest_piece;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
// Helper function to format bytes per second as human-readable string
|
|
2430
|
+
static std::string format_speed(double bytes_per_second) {
|
|
2431
|
+
std::ostringstream oss;
|
|
2432
|
+
oss << std::fixed << std::setprecision(1);
|
|
2433
|
+
|
|
2434
|
+
if (bytes_per_second >= 1024 * 1024) {
|
|
2435
|
+
oss << (bytes_per_second / (1024 * 1024)) << " MB/s";
|
|
2436
|
+
} else if (bytes_per_second >= 1024) {
|
|
2437
|
+
oss << (bytes_per_second / 1024) << " KB/s";
|
|
2438
|
+
} else {
|
|
2439
|
+
oss << bytes_per_second << " B/s";
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
return oss.str();
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// Helper function to format bytes as human-readable string
|
|
2446
|
+
static std::string format_bytes(uint64_t bytes) {
|
|
2447
|
+
std::ostringstream oss;
|
|
2448
|
+
oss << std::fixed << std::setprecision(2);
|
|
2449
|
+
|
|
2450
|
+
if (bytes >= 1024ULL * 1024 * 1024) {
|
|
2451
|
+
oss << (static_cast<double>(bytes) / (1024 * 1024 * 1024)) << " GB";
|
|
2452
|
+
} else if (bytes >= 1024 * 1024) {
|
|
2453
|
+
oss << (static_cast<double>(bytes) / (1024 * 1024)) << " MB";
|
|
2454
|
+
} else if (bytes >= 1024) {
|
|
2455
|
+
oss << (static_cast<double>(bytes) / 1024) << " KB";
|
|
2456
|
+
} else {
|
|
2457
|
+
oss << bytes << " B";
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
return oss.str();
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
void TorrentDownload::update_progress() {
|
|
2464
|
+
// Update speed statistics
|
|
2465
|
+
update_speed_stats();
|
|
2466
|
+
|
|
2467
|
+
uint64_t downloaded = get_downloaded_bytes();
|
|
2468
|
+
uint64_t total = torrent_info_.get_total_length();
|
|
2469
|
+
double percentage = get_progress_percentage();
|
|
2470
|
+
|
|
2471
|
+
// Call progress callback
|
|
2472
|
+
if (progress_callback_) {
|
|
2473
|
+
progress_callback_(downloaded, total, percentage);
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// Only log progress if it changed by at least 1% or we went from 0 to non-zero
|
|
2477
|
+
double progress_delta = percentage - last_logged_progress_;
|
|
2478
|
+
bool should_log_progress = (progress_delta >= 1.0) ||
|
|
2479
|
+
(last_logged_progress_ == 0.0 && percentage > 0.0);
|
|
2480
|
+
|
|
2481
|
+
if (should_log_progress) {
|
|
2482
|
+
double dl_speed = get_download_speed();
|
|
2483
|
+
double ul_speed = get_upload_speed();
|
|
2484
|
+
|
|
2485
|
+
LOG_BT_INFO("Progress [" << torrent_info_.get_name() << "]: "
|
|
2486
|
+
<< std::fixed << std::setprecision(1) << percentage << "% "
|
|
2487
|
+
<< "(" << format_bytes(downloaded) << " / " << format_bytes(total) << ") "
|
|
2488
|
+
<< "DL: " << format_speed(dl_speed) << " "
|
|
2489
|
+
<< "UL: " << format_speed(ul_speed));
|
|
2490
|
+
|
|
2491
|
+
last_logged_progress_ = percentage;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
void TorrentDownload::on_piece_completed(PieceIndex piece_index) {
|
|
2496
|
+
if (piece_complete_callback_) {
|
|
2497
|
+
piece_complete_callback_(piece_index);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// Notify all peers that we have this piece (for seeding)
|
|
2501
|
+
notify_peers_have_piece(piece_index);
|
|
2502
|
+
|
|
2503
|
+
double dl_speed = get_download_speed();
|
|
2504
|
+
LOG_BT_INFO("Piece " << piece_index << " completed. Progress: "
|
|
2505
|
+
<< std::fixed << std::setprecision(1) << get_progress_percentage() << "% ("
|
|
2506
|
+
<< get_completed_pieces() << "/" << torrent_info_.get_num_pieces() << " pieces) "
|
|
2507
|
+
<< "@ " << format_speed(dl_speed));
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
void TorrentDownload::notify_peers_have_piece(PieceIndex piece_index) {
|
|
2511
|
+
std::lock_guard<std::mutex> lock(peers_mutex_);
|
|
2512
|
+
|
|
2513
|
+
// Send HAVE message to all connected peers
|
|
2514
|
+
auto have_msg = PeerMessage::create_have(piece_index);
|
|
2515
|
+
|
|
2516
|
+
for (auto& peer : peer_connections_) {
|
|
2517
|
+
if (peer->is_connected()) {
|
|
2518
|
+
peer->send_message(have_msg);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
LOG_BT_DEBUG("Notified " << peer_connections_.size() << " peers that we have piece " << piece_index);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
void TorrentDownload::check_torrent_completion() {
|
|
2526
|
+
if (is_complete() && torrent_complete_callback_) {
|
|
2527
|
+
torrent_complete_callback_(torrent_info_.get_name());
|
|
2528
|
+
LOG_BT_INFO("Torrent download completed: " << torrent_info_.get_name());
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
void TorrentDownload::log_detailed_state() {
|
|
2533
|
+
// Get current stats - be careful with lock ordering (peers_mutex_ -> pieces_mutex_)
|
|
2534
|
+
uint32_t completed_pieces = 0;
|
|
2535
|
+
uint32_t downloading_pieces = 0;
|
|
2536
|
+
uint32_t total_pieces = 0;
|
|
2537
|
+
std::vector<PieceIndex> currently_downloading;
|
|
2538
|
+
|
|
2539
|
+
{
|
|
2540
|
+
std::lock_guard<std::mutex> lock(pieces_mutex_);
|
|
2541
|
+
total_pieces = static_cast<uint32_t>(piece_completed_.size());
|
|
2542
|
+
completed_pieces = static_cast<uint32_t>(std::count(piece_completed_.begin(), piece_completed_.end(), true));
|
|
2543
|
+
|
|
2544
|
+
for (size_t i = 0; i < piece_downloading_.size(); ++i) {
|
|
2545
|
+
if (piece_downloading_[i] && !piece_completed_[i]) {
|
|
2546
|
+
downloading_pieces++;
|
|
2547
|
+
if (currently_downloading.size() < 10) { // Limit to first 10
|
|
2548
|
+
currently_downloading.push_back(static_cast<PieceIndex>(i));
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// Get peer information
|
|
2555
|
+
size_t total_peers = 0;
|
|
2556
|
+
size_t connected_peers = 0;
|
|
2557
|
+
size_t unchoked_peers = 0;
|
|
2558
|
+
size_t interested_peers = 0;
|
|
2559
|
+
std::vector<std::tuple<std::string, double, double, size_t, bool>> peer_stats; // ip:port, dl_speed, ul_speed, pending_requests, choked
|
|
2560
|
+
|
|
2561
|
+
{
|
|
2562
|
+
std::lock_guard<std::mutex> lock(peers_mutex_);
|
|
2563
|
+
total_peers = peer_connections_.size();
|
|
2564
|
+
|
|
2565
|
+
for (const auto& peer : peer_connections_) {
|
|
2566
|
+
if (peer->is_connected()) {
|
|
2567
|
+
connected_peers++;
|
|
2568
|
+
if (!peer->is_choked()) {
|
|
2569
|
+
unchoked_peers++;
|
|
2570
|
+
}
|
|
2571
|
+
if (peer->peer_is_interested()) {
|
|
2572
|
+
interested_peers++;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
std::string peer_addr = peer->get_peer_info().ip + ":" + std::to_string(peer->get_peer_info().port);
|
|
2576
|
+
peer_stats.emplace_back(
|
|
2577
|
+
peer_addr,
|
|
2578
|
+
peer->get_download_speed(),
|
|
2579
|
+
peer->get_upload_speed(),
|
|
2580
|
+
peer->get_pending_requests(),
|
|
2581
|
+
peer->is_choked()
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
// Calculate overall speeds
|
|
2588
|
+
double dl_speed = get_download_speed();
|
|
2589
|
+
double ul_speed = get_upload_speed();
|
|
2590
|
+
uint64_t downloaded = get_downloaded_bytes();
|
|
2591
|
+
uint64_t uploaded = get_uploaded_bytes();
|
|
2592
|
+
uint64_t total_size = torrent_info_.get_total_length();
|
|
2593
|
+
double progress = get_progress_percentage();
|
|
2594
|
+
|
|
2595
|
+
// Calculate ETA
|
|
2596
|
+
std::string eta_str = "N/A";
|
|
2597
|
+
if (dl_speed > 0 && !is_complete()) {
|
|
2598
|
+
uint64_t remaining = total_size - downloaded;
|
|
2599
|
+
double eta_seconds = static_cast<double>(remaining) / dl_speed;
|
|
2600
|
+
|
|
2601
|
+
if (eta_seconds < 60) {
|
|
2602
|
+
std::ostringstream oss;
|
|
2603
|
+
oss << std::fixed << std::setprecision(0) << eta_seconds << "s";
|
|
2604
|
+
eta_str = oss.str();
|
|
2605
|
+
} else if (eta_seconds < 3600) {
|
|
2606
|
+
int minutes = static_cast<int>(eta_seconds / 60);
|
|
2607
|
+
int seconds = static_cast<int>(eta_seconds) % 60;
|
|
2608
|
+
std::ostringstream oss;
|
|
2609
|
+
oss << minutes << "m " << seconds << "s";
|
|
2610
|
+
eta_str = oss.str();
|
|
2611
|
+
} else {
|
|
2612
|
+
int hours = static_cast<int>(eta_seconds / 3600);
|
|
2613
|
+
int minutes = static_cast<int>((static_cast<int>(eta_seconds) % 3600) / 60);
|
|
2614
|
+
std::ostringstream oss;
|
|
2615
|
+
oss << hours << "h " << minutes << "m";
|
|
2616
|
+
eta_str = oss.str();
|
|
2617
|
+
}
|
|
2618
|
+
} else if (is_complete()) {
|
|
2619
|
+
eta_str = "Complete";
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
// Log detailed state
|
|
2623
|
+
LOG_BT_INFO("=== Torrent State: " << torrent_info_.get_name() << " ===");
|
|
2624
|
+
LOG_BT_INFO(" Progress: " << std::fixed << std::setprecision(1) << progress << "% "
|
|
2625
|
+
<< "(" << format_bytes(downloaded) << " / " << format_bytes(total_size) << ")");
|
|
2626
|
+
LOG_BT_INFO(" Pieces: " << completed_pieces << "/" << total_pieces << " complete, "
|
|
2627
|
+
<< downloading_pieces << " downloading");
|
|
2628
|
+
LOG_BT_INFO(" Speed: DL " << format_speed(dl_speed) << " | UL " << format_speed(ul_speed));
|
|
2629
|
+
LOG_BT_INFO(" Uploaded: " << format_bytes(uploaded) << " | ETA: " << eta_str);
|
|
2630
|
+
LOG_BT_INFO(" Peers: " << connected_peers << "/" << total_peers << " connected, "
|
|
2631
|
+
<< unchoked_peers << " unchoked, " << interested_peers << " interested in us");
|
|
2632
|
+
|
|
2633
|
+
// Log pieces being downloaded (if any)
|
|
2634
|
+
if (!currently_downloading.empty()) {
|
|
2635
|
+
std::ostringstream pieces_oss;
|
|
2636
|
+
pieces_oss << " Downloading pieces: [";
|
|
2637
|
+
for (size_t i = 0; i < currently_downloading.size(); ++i) {
|
|
2638
|
+
if (i > 0) pieces_oss << ", ";
|
|
2639
|
+
pieces_oss << currently_downloading[i];
|
|
2640
|
+
}
|
|
2641
|
+
if (downloading_pieces > currently_downloading.size()) {
|
|
2642
|
+
pieces_oss << ", ... +" << (downloading_pieces - currently_downloading.size()) << " more";
|
|
2643
|
+
}
|
|
2644
|
+
pieces_oss << "]";
|
|
2645
|
+
LOG_BT_INFO(pieces_oss.str());
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// Log per-peer stats if we have peers
|
|
2649
|
+
if (!peer_stats.empty()) {
|
|
2650
|
+
LOG_BT_INFO(" Per-peer stats:");
|
|
2651
|
+
for (const auto& [addr, peer_dl, peer_ul, pending, choked] : peer_stats) {
|
|
2652
|
+
LOG_BT_INFO(" " << addr << ": DL " << format_speed(peer_dl)
|
|
2653
|
+
<< " | UL " << format_speed(peer_ul)
|
|
2654
|
+
<< " | Pending: " << pending
|
|
2655
|
+
<< " | " << (choked ? "CHOKED" : "unchoked"));
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
LOG_BT_INFO("=== End Torrent State ===");
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
void TorrentDownload::announce_to_dht(DhtClient* dht_client) {
|
|
2663
|
+
if (!dht_client || !dht_client->is_running()) {
|
|
2664
|
+
LOG_BT_WARN("DHT client not available for torrent announcement");
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// Convert info hash to DHT format
|
|
2669
|
+
InfoHash dht_info_hash;
|
|
2670
|
+
const auto& torrent_hash = torrent_info_.get_info_hash();
|
|
2671
|
+
std::copy(torrent_hash.begin(), torrent_hash.end(), dht_info_hash.begin());
|
|
2672
|
+
|
|
2673
|
+
// Announce with default BitTorrent port (we don't have a BitTorrent listen port yet)
|
|
2674
|
+
uint16_t announce_port = 6881; // Default BitTorrent port
|
|
2675
|
+
|
|
2676
|
+
if (dht_client->announce_peer(dht_info_hash, announce_port)) {
|
|
2677
|
+
LOG_BT_INFO("Announced torrent to DHT: " << torrent_info_.get_name());
|
|
2678
|
+
} else {
|
|
2679
|
+
LOG_BT_WARN("Failed to announce torrent to DHT: " << torrent_info_.get_name());
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
void TorrentDownload::request_peers_from_dht(DhtClient* dht_client) {
|
|
2684
|
+
if (!dht_client || !dht_client->is_running()) {
|
|
2685
|
+
LOG_BT_WARN("DHT client not available for peer discovery");
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// Convert info hash to DHT format
|
|
2690
|
+
InfoHash dht_info_hash;
|
|
2691
|
+
const auto& torrent_hash = torrent_info_.get_info_hash();
|
|
2692
|
+
std::copy(torrent_hash.begin(), torrent_hash.end(), dht_info_hash.begin());
|
|
2693
|
+
|
|
2694
|
+
LOG_BT_INFO("Requesting peers from DHT for torrent: " << torrent_info_.get_name());
|
|
2695
|
+
|
|
2696
|
+
dht_client->find_peers(dht_info_hash, [this](const std::vector<Peer>& peers, const InfoHash& info_hash) {
|
|
2697
|
+
// Check if torrent is still running
|
|
2698
|
+
if (!running_.load()) {
|
|
2699
|
+
LOG_BT_DEBUG("Torrent stopped, ignoring DHT peer discovery results");
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
LOG_BT_INFO("DHT discovered " << peers.size() << " peers for torrent: " << torrent_info_.get_name());
|
|
2704
|
+
|
|
2705
|
+
if (peers.empty()) {
|
|
2706
|
+
LOG_BT_WARN("No peers found via DHT for torrent: " << torrent_info_.get_name());
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
size_t peers_added = 0;
|
|
2711
|
+
for (const auto& peer : peers) {
|
|
2712
|
+
if (get_peer_count() >= MAX_PEERS_PER_TORRENT) {
|
|
2713
|
+
LOG_BT_DEBUG("Peer limit reached, stopping peer additions");
|
|
2714
|
+
break;
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
if (add_peer(peer)) {
|
|
2718
|
+
peers_added++;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
LOG_BT_INFO("Added " << peers_added << " of " << peers.size() << " DHT peers for torrent: " << torrent_info_.get_name());
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
void TorrentDownload::announce_to_trackers() {
|
|
2727
|
+
if (!tracker_manager_) {
|
|
2728
|
+
LOG_BT_WARN("No tracker manager available");
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
LOG_BT_INFO("Announcing to trackers for torrent: " << torrent_info_.get_name());
|
|
2733
|
+
|
|
2734
|
+
// Prepare tracker request
|
|
2735
|
+
TrackerRequest request;
|
|
2736
|
+
request.info_hash = torrent_info_.get_info_hash();
|
|
2737
|
+
request.peer_id = our_peer_id_;
|
|
2738
|
+
request.port = 6881; // Default BitTorrent port (should come from BitTorrentClient)
|
|
2739
|
+
request.uploaded = total_uploaded_.load();
|
|
2740
|
+
request.downloaded = total_downloaded_.load();
|
|
2741
|
+
request.left = torrent_info_.get_total_length() - total_downloaded_.load();
|
|
2742
|
+
|
|
2743
|
+
// Determine event
|
|
2744
|
+
if (total_downloaded_.load() == 0 && !running_) {
|
|
2745
|
+
request.event = TrackerEvent::STARTED;
|
|
2746
|
+
} else if (is_complete()) {
|
|
2747
|
+
request.event = TrackerEvent::COMPLETED;
|
|
2748
|
+
} else {
|
|
2749
|
+
request.event = TrackerEvent::NONE;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// Announce to all trackers
|
|
2753
|
+
tracker_manager_->announce(request, [this](const TrackerResponse& response, const std::string& tracker_url) {
|
|
2754
|
+
if (!response.success) {
|
|
2755
|
+
LOG_BT_WARN("Tracker announce failed (" << tracker_url << "): " << response.failure_reason);
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
LOG_BT_INFO("Tracker announce successful (" << tracker_url << ")");
|
|
2760
|
+
LOG_BT_INFO(" Peers: " << response.peers.size()
|
|
2761
|
+
<< ", Seeders: " << response.complete
|
|
2762
|
+
<< ", Leechers: " << response.incomplete);
|
|
2763
|
+
|
|
2764
|
+
// Add discovered peers
|
|
2765
|
+
for (const auto& peer : response.peers) {
|
|
2766
|
+
if (get_peer_count() >= MAX_PEERS_PER_TORRENT) {
|
|
2767
|
+
break;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
add_peer(peer);
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
void TorrentDownload::request_peers_from_trackers() {
|
|
2776
|
+
if (!tracker_manager_) {
|
|
2777
|
+
LOG_BT_WARN("No tracker manager available");
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// Same as announce - trackers provide peer lists in announce responses
|
|
2782
|
+
announce_to_trackers();
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
void TorrentDownload::set_metadata_download(std::shared_ptr<MetadataDownload> metadata_download) {
|
|
2786
|
+
metadata_download_ = metadata_download;
|
|
2787
|
+
|
|
2788
|
+
// Set up the completion callback to forward to our metadata_complete_callback_
|
|
2789
|
+
if (metadata_download_) {
|
|
2790
|
+
metadata_download_->set_completion_callback([this](const TorrentInfo& torrent_info) {
|
|
2791
|
+
LOG_BT_INFO("Metadata download completed, notifying callback");
|
|
2792
|
+
if (metadata_complete_callback_) {
|
|
2793
|
+
metadata_complete_callback_(torrent_info);
|
|
2794
|
+
}
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
//=============================================================================
|
|
2800
|
+
// BitTorrentClient Implementation
|
|
2801
|
+
//=============================================================================
|
|
2802
|
+
|
|
2803
|
+
BitTorrentClient::BitTorrentClient()
|
|
2804
|
+
: running_(false), listen_port_(0), listen_socket_(INVALID_SOCKET_VALUE),
|
|
2805
|
+
dht_client_(nullptr), max_connections_per_torrent_(MAX_PEERS_PER_TORRENT),
|
|
2806
|
+
download_rate_limit_(0), upload_rate_limit_(0) {
|
|
2807
|
+
|
|
2808
|
+
LOG_BT_INFO("BitTorrent client created");
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
BitTorrentClient::~BitTorrentClient() {
|
|
2812
|
+
stop();
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
bool BitTorrentClient::start(int listen_port) {
|
|
2816
|
+
if (running_.load()) {
|
|
2817
|
+
LOG_BT_WARN("BitTorrent client is already running");
|
|
2818
|
+
return false;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
listen_port_ = listen_port;
|
|
2822
|
+
if (listen_port_ == 0) {
|
|
2823
|
+
listen_port_ = 6881; // Default BitTorrent port
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
LOG_BT_INFO("Starting BitTorrent client on port " << listen_port_);
|
|
2827
|
+
|
|
2828
|
+
// Initialize socket library (safe to call multiple times)
|
|
2829
|
+
if (!init_socket_library()) {
|
|
2830
|
+
LOG_BT_ERROR("Failed to initialize socket library");
|
|
2831
|
+
return false;
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// Create listen socket
|
|
2835
|
+
listen_socket_ = create_tcp_server(listen_port_);
|
|
2836
|
+
if (!is_valid_socket(listen_socket_)) {
|
|
2837
|
+
LOG_BT_ERROR("Failed to create BitTorrent listen socket on port " << listen_port_);
|
|
2838
|
+
return false;
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
running_.store(true);
|
|
2842
|
+
|
|
2843
|
+
// Start incoming connections handler
|
|
2844
|
+
incoming_connections_thread_ = std::thread(&BitTorrentClient::handle_incoming_connections, this);
|
|
2845
|
+
|
|
2846
|
+
LOG_BT_INFO("BitTorrent client started successfully");
|
|
2847
|
+
return true;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
void BitTorrentClient::stop() {
|
|
2851
|
+
if (!running_.load()) {
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
LOG_BT_INFO("Stopping BitTorrent client");
|
|
2856
|
+
running_.store(false);
|
|
2857
|
+
|
|
2858
|
+
// Stop all torrents
|
|
2859
|
+
{
|
|
2860
|
+
std::lock_guard<std::mutex> lock(torrents_mutex_);
|
|
2861
|
+
for (auto& pair : torrents_) {
|
|
2862
|
+
pair.second->stop();
|
|
2863
|
+
}
|
|
2864
|
+
torrents_.clear();
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// Close listen socket
|
|
2868
|
+
if (is_valid_socket(listen_socket_)) {
|
|
2869
|
+
close_socket(listen_socket_);
|
|
2870
|
+
listen_socket_ = INVALID_SOCKET_VALUE;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// Wait for threads
|
|
2874
|
+
if (incoming_connections_thread_.joinable()) {
|
|
2875
|
+
incoming_connections_thread_.join();
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
LOG_BT_INFO("BitTorrent client stopped");
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
std::shared_ptr<TorrentDownload> BitTorrentClient::add_torrent(const std::string& torrent_file,
|
|
2882
|
+
const std::string& download_path) {
|
|
2883
|
+
TorrentInfo torrent_info;
|
|
2884
|
+
if (!torrent_info.load_from_file(torrent_file)) {
|
|
2885
|
+
LOG_BT_ERROR("Failed to load torrent file: " << torrent_file);
|
|
2886
|
+
return nullptr;
|
|
2887
|
+
}
|
|
2888
|
+
|
|
2889
|
+
return add_torrent(torrent_info, download_path);
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
std::shared_ptr<TorrentDownload> BitTorrentClient::add_torrent(const TorrentInfo& torrent_info,
|
|
2893
|
+
const std::string& download_path) {
|
|
2894
|
+
if (!running_.load()) {
|
|
2895
|
+
LOG_BT_ERROR("BitTorrent client is not running");
|
|
2896
|
+
return nullptr;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
const InfoHash& info_hash = torrent_info.get_info_hash();
|
|
2900
|
+
|
|
2901
|
+
{
|
|
2902
|
+
std::lock_guard<std::mutex> lock(torrents_mutex_);
|
|
2903
|
+
|
|
2904
|
+
// Check if torrent already exists
|
|
2905
|
+
if (torrents_.find(info_hash) != torrents_.end()) {
|
|
2906
|
+
LOG_BT_WARN("Torrent already exists: " << torrent_info.get_name());
|
|
2907
|
+
return torrents_[info_hash];
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// Create new torrent download
|
|
2911
|
+
auto torrent_download = std::make_shared<TorrentDownload>(torrent_info, download_path);
|
|
2912
|
+
|
|
2913
|
+
// Set up callbacks
|
|
2914
|
+
torrent_download->set_progress_callback([this, info_hash](uint64_t downloaded, uint64_t total, double percentage) {
|
|
2915
|
+
LOG_BT_DEBUG("Torrent progress: " << percentage << "% (" << downloaded << "/" << total << " bytes)");
|
|
2916
|
+
});
|
|
2917
|
+
|
|
2918
|
+
torrent_download->set_torrent_complete_callback([this, info_hash](const std::string& torrent_name) {
|
|
2919
|
+
LOG_BT_INFO("Torrent completed: " << torrent_name);
|
|
2920
|
+
if (torrent_completed_callback_) {
|
|
2921
|
+
torrent_completed_callback_(info_hash);
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
|
|
2925
|
+
torrents_[info_hash] = torrent_download;
|
|
2926
|
+
|
|
2927
|
+
// Start the download
|
|
2928
|
+
if (!torrent_download->start()) {
|
|
2929
|
+
LOG_BT_ERROR("Failed to start torrent download: " << torrent_info.get_name());
|
|
2930
|
+
torrents_.erase(info_hash);
|
|
2931
|
+
return nullptr;
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
// Announce to trackers
|
|
2935
|
+
torrent_download->announce_to_trackers();
|
|
2936
|
+
|
|
2937
|
+
// Announce to DHT if available
|
|
2938
|
+
if (dht_client_) {
|
|
2939
|
+
torrent_download->announce_to_dht(dht_client_);
|
|
2940
|
+
torrent_download->request_peers_from_dht(dht_client_);
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
LOG_BT_INFO("Added torrent: " << torrent_info.get_name());
|
|
2944
|
+
|
|
2945
|
+
if (torrent_added_callback_) {
|
|
2946
|
+
torrent_added_callback_(info_hash);
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
return torrent_download;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
std::shared_ptr<TorrentDownload> BitTorrentClient::add_torrent_by_hash(const InfoHash& info_hash,
|
|
2954
|
+
const std::string& download_path) {
|
|
2955
|
+
if (!running_.load()) {
|
|
2956
|
+
LOG_BT_ERROR("BitTorrent client is not running");
|
|
2957
|
+
return nullptr;
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
if (!dht_client_ || !dht_client_->is_running()) {
|
|
2961
|
+
LOG_BT_ERROR("DHT client is required for adding torrents by hash. DHT is not available.");
|
|
2962
|
+
return nullptr;
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
LOG_BT_INFO("Adding torrent by hash: " << info_hash_to_hex(info_hash));
|
|
2966
|
+
LOG_BT_INFO("This will use DHT to find peers and download metadata via BEP 9...");
|
|
2967
|
+
|
|
2968
|
+
{
|
|
2969
|
+
std::lock_guard<std::mutex> lock(torrents_mutex_);
|
|
2970
|
+
|
|
2971
|
+
// Check if torrent already exists
|
|
2972
|
+
if (torrents_.find(info_hash) != torrents_.end()) {
|
|
2973
|
+
LOG_BT_WARN("Torrent already exists with hash: " << info_hash_to_hex(info_hash));
|
|
2974
|
+
return torrents_[info_hash];
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
// Check if metadata download already in progress
|
|
2979
|
+
{
|
|
2980
|
+
std::lock_guard<std::mutex> lock(metadata_mutex_);
|
|
2981
|
+
if (metadata_downloads_.find(info_hash) != metadata_downloads_.end()) {
|
|
2982
|
+
LOG_BT_WARN("Metadata download already in progress for hash: " << info_hash_to_hex(info_hash));
|
|
2983
|
+
return nullptr;
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
// Store download path for later use
|
|
2987
|
+
metadata_download_paths_[info_hash] = download_path;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
// Create a minimal TorrentInfo for metadata exchange
|
|
2991
|
+
// We need a valid TorrentInfo to create peer connections, but we don't have the real metadata yet
|
|
2992
|
+
TorrentInfo metadata_torrent_info = TorrentInfo::create_for_metadata_exchange(info_hash);
|
|
2993
|
+
|
|
2994
|
+
// Create a temporary torrent download for metadata exchange only
|
|
2995
|
+
// Use empty download path since we don't download any pieces for metadata-only torrents
|
|
2996
|
+
auto metadata_torrent = std::make_shared<TorrentDownload>(metadata_torrent_info, "");
|
|
2997
|
+
|
|
2998
|
+
// Set up metadata completion callback
|
|
2999
|
+
// When metadata download completes, we'll stop the temporary torrent and create the real one
|
|
3000
|
+
// IMPORTANT: This callback is invoked from within a PeerConnection's thread, so we must NOT
|
|
3001
|
+
// call stop() directly (which would try to join the calling thread, causing a deadlock).
|
|
3002
|
+
// Instead, we defer the cleanup to a detached thread.
|
|
3003
|
+
metadata_torrent->set_metadata_complete_callback([this, info_hash, download_path, metadata_torrent](const TorrentInfo& torrent_info) {
|
|
3004
|
+
LOG_BT_INFO("Metadata download completed for " << torrent_info.get_name());
|
|
3005
|
+
|
|
3006
|
+
// Capture torrent_info by value since we're deferring to another thread
|
|
3007
|
+
TorrentInfo captured_info = torrent_info;
|
|
3008
|
+
|
|
3009
|
+
// Defer stop() and cleanup to a separate thread to avoid deadlock
|
|
3010
|
+
// (callback is called from PeerConnection::connection_thread_ which would try to join itself)
|
|
3011
|
+
std::thread([this, info_hash, download_path, metadata_torrent, captured_info]() {
|
|
3012
|
+
// Stop the temporary metadata torrent
|
|
3013
|
+
metadata_torrent->stop();
|
|
3014
|
+
|
|
3015
|
+
// Complete the metadata download and create the real torrent
|
|
3016
|
+
complete_metadata_download(info_hash, captured_info, download_path);
|
|
3017
|
+
}).detach();
|
|
3018
|
+
});
|
|
3019
|
+
|
|
3020
|
+
// Note: We don't create MetadataDownload here yet
|
|
3021
|
+
// It will be created dynamically when we get the metadata size from a peer's extended handshake
|
|
3022
|
+
// This happens in handle_extended_handshake when peer sends metadata_size
|
|
3023
|
+
// When set_metadata_download() is called, it will automatically set up the completion callback chain
|
|
3024
|
+
|
|
3025
|
+
// Start the metadata torrent (just for peer connections)
|
|
3026
|
+
if (!metadata_torrent->start()) {
|
|
3027
|
+
LOG_BT_ERROR("Failed to start metadata download torrent");
|
|
3028
|
+
return nullptr;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// Use DHT to find peers for this info hash
|
|
3032
|
+
LOG_BT_INFO("Discovering peers via DHT for hash: " << info_hash_to_hex(info_hash));
|
|
3033
|
+
|
|
3034
|
+
// Find peers via DHT
|
|
3035
|
+
dht_client_->find_peers(info_hash, [this, info_hash, metadata_torrent](const std::vector<Peer>& peers, const InfoHash& hash) {
|
|
3036
|
+
// Check if metadata torrent is still running (may have completed or been stopped)
|
|
3037
|
+
if (!metadata_torrent->is_running()) {
|
|
3038
|
+
LOG_BT_DEBUG("Metadata torrent already stopped, ignoring DHT peer discovery results");
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// Check if metadata is already downloaded
|
|
3043
|
+
if (auto* md = metadata_torrent->get_metadata_download()) {
|
|
3044
|
+
if (md->is_complete()) {
|
|
3045
|
+
LOG_BT_DEBUG("Metadata already downloaded, ignoring DHT peer discovery results");
|
|
3046
|
+
return;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
LOG_BT_INFO("DHT found " << peers.size() << " peers for hash " << info_hash_to_hex(info_hash));
|
|
3051
|
+
|
|
3052
|
+
if (peers.empty()) {
|
|
3053
|
+
LOG_BT_WARN("No peers found via DHT for hash " << info_hash_to_hex(info_hash));
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// Connect to peers to get metadata
|
|
3058
|
+
size_t max_peers_to_try = (std::min)(peers.size(), static_cast<size_t>(10));
|
|
3059
|
+
size_t peers_added = 0;
|
|
3060
|
+
for (size_t i = 0; i < max_peers_to_try; ++i) {
|
|
3061
|
+
// Check again if metadata is complete (could have completed during loop)
|
|
3062
|
+
if (auto* md = metadata_torrent->get_metadata_download()) {
|
|
3063
|
+
if (md->is_complete()) {
|
|
3064
|
+
LOG_BT_DEBUG("Metadata download completed, stopping peer additions");
|
|
3065
|
+
break;
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
// Check if torrent is still running
|
|
3070
|
+
if (!metadata_torrent->is_running()) {
|
|
3071
|
+
LOG_BT_DEBUG("Metadata torrent stopped, stopping peer additions");
|
|
3072
|
+
break;
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
const auto& peer = peers[i];
|
|
3076
|
+
LOG_BT_INFO("Connecting to peer " << peer.ip << ":" << peer.port << " for metadata exchange");
|
|
3077
|
+
|
|
3078
|
+
// Add peer to the metadata torrent
|
|
3079
|
+
// The peer connection will automatically:
|
|
3080
|
+
// 1. Perform BitTorrent handshake with the info hash
|
|
3081
|
+
// 2. Exchange extended handshake
|
|
3082
|
+
// 3. Detect metadata support and size
|
|
3083
|
+
// 4. Request metadata pieces
|
|
3084
|
+
// 5. Complete metadata download via callback
|
|
3085
|
+
if (metadata_torrent->add_peer(peer)) {
|
|
3086
|
+
peers_added++;
|
|
3087
|
+
} else {
|
|
3088
|
+
LOG_BT_DEBUG("Failed to add peer " << peer.ip << ":" << peer.port);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
LOG_BT_INFO("Added " << peers_added << " peers for metadata exchange");
|
|
3093
|
+
});
|
|
3094
|
+
|
|
3095
|
+
LOG_BT_INFO("Metadata download initiated for hash " << info_hash_to_hex(info_hash));
|
|
3096
|
+
LOG_BT_INFO("Waiting for DHT peer discovery and metadata exchange...");
|
|
3097
|
+
LOG_BT_INFO("Note: Metadata download will complete asynchronously via BEP 9");
|
|
3098
|
+
|
|
3099
|
+
return nullptr; // Will return the real torrent via completion callback
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
std::shared_ptr<TorrentDownload> BitTorrentClient::add_torrent_by_hash(const std::string& info_hash_hex,
|
|
3103
|
+
const std::string& download_path) {
|
|
3104
|
+
InfoHash info_hash = hex_to_info_hash(info_hash_hex);
|
|
3105
|
+
|
|
3106
|
+
// Validate the parsed hash
|
|
3107
|
+
bool is_zero = true;
|
|
3108
|
+
for (const auto& byte : info_hash) {
|
|
3109
|
+
if (byte != 0) {
|
|
3110
|
+
is_zero = false;
|
|
3111
|
+
break;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
if (is_zero && info_hash_hex.length() == 40) {
|
|
3116
|
+
LOG_BT_ERROR("Invalid info hash format: " << info_hash_hex);
|
|
3117
|
+
return nullptr;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
return add_torrent_by_hash(info_hash, download_path);
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
bool BitTorrentClient::remove_torrent(const InfoHash& info_hash) {
|
|
3124
|
+
std::lock_guard<std::mutex> lock(torrents_mutex_);
|
|
3125
|
+
|
|
3126
|
+
auto it = torrents_.find(info_hash);
|
|
3127
|
+
if (it == torrents_.end()) {
|
|
3128
|
+
return false;
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
// Stop the torrent
|
|
3132
|
+
it->second->stop();
|
|
3133
|
+
torrents_.erase(it);
|
|
3134
|
+
|
|
3135
|
+
if (torrent_removed_callback_) {
|
|
3136
|
+
torrent_removed_callback_(info_hash);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
LOG_BT_INFO("Removed torrent with info hash: " << info_hash_to_hex(info_hash));
|
|
3140
|
+
return true;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
std::shared_ptr<TorrentDownload> BitTorrentClient::get_torrent(const InfoHash& info_hash) {
|
|
3144
|
+
std::lock_guard<std::mutex> lock(torrents_mutex_);
|
|
3145
|
+
|
|
3146
|
+
auto it = torrents_.find(info_hash);
|
|
3147
|
+
return (it != torrents_.end()) ? it->second : nullptr;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
std::vector<std::shared_ptr<TorrentDownload>> BitTorrentClient::get_all_torrents() {
|
|
3151
|
+
std::lock_guard<std::mutex> lock(torrents_mutex_);
|
|
3152
|
+
|
|
3153
|
+
std::vector<std::shared_ptr<TorrentDownload>> result;
|
|
3154
|
+
result.reserve(torrents_.size());
|
|
3155
|
+
|
|
3156
|
+
for (const auto& pair : torrents_) {
|
|
3157
|
+
result.push_back(pair.second);
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
return result;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
void BitTorrentClient::discover_peers_for_torrent(const InfoHash& info_hash) {
|
|
3164
|
+
if (!dht_client_ || !dht_client_->is_running()) {
|
|
3165
|
+
LOG_BT_WARN("DHT client not available for peer discovery");
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
auto torrent = get_torrent(info_hash);
|
|
3170
|
+
if (!torrent) {
|
|
3171
|
+
LOG_BT_WARN("Torrent not found for peer discovery");
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
torrent->request_peers_from_dht(dht_client_);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
void BitTorrentClient::announce_torrent_to_dht(const InfoHash& info_hash) {
|
|
3179
|
+
if (!dht_client_ || !dht_client_->is_running()) {
|
|
3180
|
+
LOG_BT_WARN("DHT client not available for torrent announcement");
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
auto torrent = get_torrent(info_hash);
|
|
3185
|
+
if (!torrent) {
|
|
3186
|
+
LOG_BT_WARN("Torrent not found for DHT announcement");
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
torrent->announce_to_dht(dht_client_);
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
size_t BitTorrentClient::get_active_torrents_count() const {
|
|
3194
|
+
std::lock_guard<std::mutex> lock(torrents_mutex_);
|
|
3195
|
+
return torrents_.size();
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
uint64_t BitTorrentClient::get_total_downloaded() const {
|
|
3199
|
+
std::lock_guard<std::mutex> lock(torrents_mutex_);
|
|
3200
|
+
|
|
3201
|
+
uint64_t total = 0;
|
|
3202
|
+
for (const auto& pair : torrents_) {
|
|
3203
|
+
total += pair.second->get_downloaded_bytes();
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
return total;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
uint64_t BitTorrentClient::get_total_uploaded() const {
|
|
3210
|
+
std::lock_guard<std::mutex> lock(torrents_mutex_);
|
|
3211
|
+
|
|
3212
|
+
uint64_t total = 0;
|
|
3213
|
+
for (const auto& pair : torrents_) {
|
|
3214
|
+
total += pair.second->get_uploaded_bytes();
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
return total;
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
void BitTorrentClient::handle_incoming_connections() {
|
|
3221
|
+
LOG_BT_INFO("BitTorrent incoming connections handler started");
|
|
3222
|
+
|
|
3223
|
+
while (running_.load()) {
|
|
3224
|
+
socket_t client_socket = accept_client(listen_socket_);
|
|
3225
|
+
if (!is_valid_socket(client_socket)) {
|
|
3226
|
+
if (running_.load()) {
|
|
3227
|
+
LOG_BT_ERROR("Failed to accept BitTorrent client connection");
|
|
3228
|
+
}
|
|
3229
|
+
continue;
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
// Handle the connection in a separate thread
|
|
3233
|
+
std::thread([this, client_socket]() {
|
|
3234
|
+
handle_incoming_connection(client_socket);
|
|
3235
|
+
}).detach();
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
LOG_BT_INFO("BitTorrent incoming connections handler stopped");
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
void BitTorrentClient::handle_incoming_connection(socket_t client_socket) {
|
|
3242
|
+
LOG_BT_DEBUG("Handling incoming BitTorrent connection");
|
|
3243
|
+
|
|
3244
|
+
InfoHash info_hash;
|
|
3245
|
+
PeerID peer_id;
|
|
3246
|
+
|
|
3247
|
+
if (!perform_incoming_handshake(client_socket, info_hash, peer_id)) {
|
|
3248
|
+
LOG_BT_WARN("Failed to perform incoming handshake");
|
|
3249
|
+
close_socket(client_socket);
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// Find the torrent for this info hash
|
|
3254
|
+
auto torrent = get_torrent(info_hash);
|
|
3255
|
+
if (!torrent) {
|
|
3256
|
+
LOG_BT_WARN("Received connection for unknown torrent");
|
|
3257
|
+
close_socket(client_socket);
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
// Get peer address
|
|
3262
|
+
std::string peer_address = get_peer_address(client_socket);
|
|
3263
|
+
|
|
3264
|
+
// Parse peer address to get IP and port
|
|
3265
|
+
std::string ip;
|
|
3266
|
+
int port = 0;
|
|
3267
|
+
size_t colon_pos = peer_address.find_last_of(':');
|
|
3268
|
+
if (colon_pos != std::string::npos) {
|
|
3269
|
+
ip = peer_address.substr(0, colon_pos);
|
|
3270
|
+
try {
|
|
3271
|
+
port = std::stoi(peer_address.substr(colon_pos + 1));
|
|
3272
|
+
} catch (const std::exception&) {
|
|
3273
|
+
port = 0;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
if (ip.empty() || port == 0) {
|
|
3278
|
+
LOG_BT_WARN("Failed to parse peer address: " << peer_address);
|
|
3279
|
+
close_socket(client_socket);
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
|
|
3283
|
+
// Create peer object and add to torrent with the existing socket
|
|
3284
|
+
Peer peer_info{ip, static_cast<uint16_t>(port)};
|
|
3285
|
+
if (torrent->add_peer(peer_info, client_socket)) {
|
|
3286
|
+
LOG_BT_INFO("Added incoming peer: " << peer_address);
|
|
3287
|
+
// Socket ownership transferred to PeerConnection
|
|
3288
|
+
} else {
|
|
3289
|
+
LOG_BT_WARN("Failed to add incoming peer: " << peer_address);
|
|
3290
|
+
// Socket already closed by add_peer in failure case
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
bool BitTorrentClient::perform_incoming_handshake(socket_t socket, InfoHash& info_hash, PeerID& peer_id) {
|
|
3295
|
+
// Receive handshake
|
|
3296
|
+
std::vector<uint8_t> handshake_data(68); // Fixed handshake size
|
|
3297
|
+
std::string received_data = receive_tcp_string(socket, 68);
|
|
3298
|
+
|
|
3299
|
+
if (received_data.length() != 68) {
|
|
3300
|
+
LOG_BT_ERROR("Invalid handshake size received: " << received_data.length());
|
|
3301
|
+
return false;
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
std::copy(received_data.begin(), received_data.end(), handshake_data.begin());
|
|
3305
|
+
|
|
3306
|
+
if (!parse_handshake_message(handshake_data, info_hash, peer_id)) {
|
|
3307
|
+
LOG_BT_ERROR("Failed to parse incoming handshake");
|
|
3308
|
+
return false;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
// Send our handshake response
|
|
3312
|
+
PeerID our_peer_id = generate_peer_id();
|
|
3313
|
+
auto response_data = create_handshake_message(info_hash, our_peer_id);
|
|
3314
|
+
|
|
3315
|
+
std::string response_str(response_data.begin(), response_data.end());
|
|
3316
|
+
if (send_tcp_string(socket, response_str) <= 0) {
|
|
3317
|
+
LOG_BT_ERROR("Failed to send handshake response");
|
|
3318
|
+
return false;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
LOG_BT_DEBUG("Incoming handshake completed successfully");
|
|
3322
|
+
return true;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
std::shared_ptr<MetadataDownload> BitTorrentClient::get_metadata_download(const InfoHash& info_hash) {
|
|
3326
|
+
std::lock_guard<std::mutex> lock(metadata_mutex_);
|
|
3327
|
+
|
|
3328
|
+
auto it = metadata_downloads_.find(info_hash);
|
|
3329
|
+
if (it != metadata_downloads_.end()) {
|
|
3330
|
+
return it->second;
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
return nullptr;
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
void BitTorrentClient::register_metadata_download(const InfoHash& info_hash, std::shared_ptr<MetadataDownload> metadata_download) {
|
|
3337
|
+
std::lock_guard<std::mutex> lock(metadata_mutex_);
|
|
3338
|
+
metadata_downloads_[info_hash] = metadata_download;
|
|
3339
|
+
LOG_BT_INFO("Registered metadata download for info hash " << info_hash_to_hex(info_hash));
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
void BitTorrentClient::complete_metadata_download(const InfoHash& info_hash, const TorrentInfo& torrent_info, const std::string& download_path) {
|
|
3343
|
+
LOG_BT_INFO("Completing metadata download for info hash " << info_hash_to_hex(info_hash));
|
|
3344
|
+
|
|
3345
|
+
// Remove from metadata downloads
|
|
3346
|
+
{
|
|
3347
|
+
std::lock_guard<std::mutex> lock(metadata_mutex_);
|
|
3348
|
+
metadata_downloads_.erase(info_hash);
|
|
3349
|
+
metadata_download_paths_.erase(info_hash);
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
// Add as regular torrent
|
|
3353
|
+
add_torrent(torrent_info, download_path);
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
void BitTorrentClient::get_torrent_metadata_by_hash(const InfoHash& info_hash, MetadataRetrievalCallback callback) {
|
|
3357
|
+
if (!running_.load()) {
|
|
3358
|
+
LOG_BT_ERROR("BitTorrent client is not running");
|
|
3359
|
+
if (callback) {
|
|
3360
|
+
callback(TorrentInfo(), false, "BitTorrent client is not running");
|
|
3361
|
+
}
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
if (!dht_client_ || !dht_client_->is_running()) {
|
|
3366
|
+
LOG_BT_ERROR("DHT client is required for metadata retrieval. DHT is not available.");
|
|
3367
|
+
if (callback) {
|
|
3368
|
+
callback(TorrentInfo(), false, "DHT client is required but not available");
|
|
3369
|
+
}
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
LOG_BT_INFO("Retrieving metadata for hash: " << info_hash_to_hex(info_hash));
|
|
3374
|
+
LOG_BT_INFO("This will use DHT to find peers and download metadata via BEP 9...");
|
|
3375
|
+
|
|
3376
|
+
// Check if metadata retrieval already in progress
|
|
3377
|
+
{
|
|
3378
|
+
std::lock_guard<std::mutex> lock(metadata_mutex_);
|
|
3379
|
+
if (metadata_only_torrents_.find(info_hash) != metadata_only_torrents_.end()) {
|
|
3380
|
+
LOG_BT_WARN("Metadata retrieval already in progress for hash: " << info_hash_to_hex(info_hash));
|
|
3381
|
+
if (callback) {
|
|
3382
|
+
callback(TorrentInfo(), false, "Metadata retrieval already in progress");
|
|
3383
|
+
}
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// Store the callback
|
|
3388
|
+
metadata_retrieval_callbacks_[info_hash] = callback;
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
// Create a minimal TorrentInfo for metadata exchange
|
|
3392
|
+
TorrentInfo metadata_torrent_info = TorrentInfo::create_for_metadata_exchange(info_hash);
|
|
3393
|
+
|
|
3394
|
+
// Create a temporary torrent download for metadata exchange only
|
|
3395
|
+
auto metadata_torrent = std::make_shared<TorrentDownload>(metadata_torrent_info, ""); // No download path needed
|
|
3396
|
+
|
|
3397
|
+
// Set up metadata completion callback
|
|
3398
|
+
// IMPORTANT: This callback is invoked from within a PeerConnection's thread, so we must NOT
|
|
3399
|
+
// call stop() directly (which would try to join the calling thread, causing a deadlock).
|
|
3400
|
+
// Instead, we defer the cleanup to a detached thread.
|
|
3401
|
+
metadata_torrent->set_metadata_complete_callback([this, info_hash](const TorrentInfo& torrent_info) {
|
|
3402
|
+
LOG_BT_INFO("Metadata retrieval completed for " << torrent_info.get_name());
|
|
3403
|
+
|
|
3404
|
+
// Capture torrent_info by value since we're deferring to another thread
|
|
3405
|
+
TorrentInfo captured_info = torrent_info;
|
|
3406
|
+
|
|
3407
|
+
// Defer stop() and cleanup to a separate thread to avoid deadlock
|
|
3408
|
+
// (callback is called from PeerConnection::connection_thread_ which would try to join itself)
|
|
3409
|
+
std::thread([this, info_hash, captured_info]() {
|
|
3410
|
+
MetadataRetrievalCallback user_callback;
|
|
3411
|
+
std::shared_ptr<TorrentDownload> temp_torrent;
|
|
3412
|
+
|
|
3413
|
+
// Get callback and cleanup
|
|
3414
|
+
{
|
|
3415
|
+
std::lock_guard<std::mutex> lock(metadata_mutex_);
|
|
3416
|
+
auto it = metadata_retrieval_callbacks_.find(info_hash);
|
|
3417
|
+
if (it != metadata_retrieval_callbacks_.end()) {
|
|
3418
|
+
user_callback = it->second;
|
|
3419
|
+
metadata_retrieval_callbacks_.erase(it);
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
// Get temporary torrent to stop it
|
|
3423
|
+
auto torrent_it = metadata_only_torrents_.find(info_hash);
|
|
3424
|
+
if (torrent_it != metadata_only_torrents_.end()) {
|
|
3425
|
+
temp_torrent = torrent_it->second;
|
|
3426
|
+
metadata_only_torrents_.erase(torrent_it);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3430
|
+
// Stop the temporary torrent
|
|
3431
|
+
if (temp_torrent) {
|
|
3432
|
+
temp_torrent->stop();
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
// Call user callback with success
|
|
3436
|
+
if (user_callback) {
|
|
3437
|
+
user_callback(captured_info, true, "");
|
|
3438
|
+
}
|
|
3439
|
+
}).detach();
|
|
3440
|
+
});
|
|
3441
|
+
|
|
3442
|
+
// Store the temporary torrent
|
|
3443
|
+
{
|
|
3444
|
+
std::lock_guard<std::mutex> lock(metadata_mutex_);
|
|
3445
|
+
metadata_only_torrents_[info_hash] = metadata_torrent;
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
// Start the metadata torrent (just for peer connections)
|
|
3449
|
+
if (!metadata_torrent->start()) {
|
|
3450
|
+
LOG_BT_ERROR("Failed to start metadata retrieval torrent");
|
|
3451
|
+
|
|
3452
|
+
// Cleanup and call callback with error
|
|
3453
|
+
{
|
|
3454
|
+
std::lock_guard<std::mutex> lock(metadata_mutex_);
|
|
3455
|
+
metadata_only_torrents_.erase(info_hash);
|
|
3456
|
+
auto it = metadata_retrieval_callbacks_.find(info_hash);
|
|
3457
|
+
if (it != metadata_retrieval_callbacks_.end()) {
|
|
3458
|
+
if (it->second) {
|
|
3459
|
+
it->second(TorrentInfo(), false, "Failed to start metadata retrieval");
|
|
3460
|
+
}
|
|
3461
|
+
metadata_retrieval_callbacks_.erase(it);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
// Use DHT to find peers for this info hash
|
|
3468
|
+
LOG_BT_INFO("Discovering peers via DHT for hash: " << info_hash_to_hex(info_hash));
|
|
3469
|
+
|
|
3470
|
+
// Find peers via DHT
|
|
3471
|
+
dht_client_->find_peers(info_hash, [this, info_hash, metadata_torrent](const std::vector<Peer>& peers, const InfoHash& hash) {
|
|
3472
|
+
// Check if metadata torrent is still running (may have completed or been stopped)
|
|
3473
|
+
if (!metadata_torrent->is_running()) {
|
|
3474
|
+
LOG_BT_DEBUG("Metadata torrent already stopped, ignoring DHT peer discovery results");
|
|
3475
|
+
return;
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
// Check if metadata is already downloaded
|
|
3479
|
+
if (auto* md = metadata_torrent->get_metadata_download()) {
|
|
3480
|
+
if (md->is_complete()) {
|
|
3481
|
+
LOG_BT_DEBUG("Metadata already downloaded, ignoring DHT peer discovery results");
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
LOG_BT_INFO("DHT found " << peers.size() << " peers for hash " << info_hash_to_hex(info_hash));
|
|
3487
|
+
|
|
3488
|
+
if (peers.empty()) {
|
|
3489
|
+
LOG_BT_WARN("No peers found via DHT for hash " << info_hash_to_hex(info_hash));
|
|
3490
|
+
|
|
3491
|
+
// Call callback with error
|
|
3492
|
+
MetadataRetrievalCallback user_callback;
|
|
3493
|
+
{
|
|
3494
|
+
std::lock_guard<std::mutex> lock(metadata_mutex_);
|
|
3495
|
+
auto it = metadata_retrieval_callbacks_.find(info_hash);
|
|
3496
|
+
if (it != metadata_retrieval_callbacks_.end()) {
|
|
3497
|
+
user_callback = it->second;
|
|
3498
|
+
metadata_retrieval_callbacks_.erase(it);
|
|
3499
|
+
}
|
|
3500
|
+
metadata_only_torrents_.erase(info_hash);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
metadata_torrent->stop();
|
|
3504
|
+
|
|
3505
|
+
if (user_callback) {
|
|
3506
|
+
user_callback(TorrentInfo(), false, "No peers found via DHT");
|
|
3507
|
+
}
|
|
3508
|
+
return;
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
// Connect to peers to get metadata
|
|
3512
|
+
size_t max_peers_to_try = (std::min)(peers.size(), static_cast<size_t>(10));
|
|
3513
|
+
size_t peers_added = 0;
|
|
3514
|
+
for (size_t i = 0; i < max_peers_to_try; ++i) {
|
|
3515
|
+
// Check again if metadata is complete (could have completed during loop)
|
|
3516
|
+
if (auto* md = metadata_torrent->get_metadata_download()) {
|
|
3517
|
+
if (md->is_complete()) {
|
|
3518
|
+
LOG_BT_DEBUG("Metadata download completed, stopping peer additions");
|
|
3519
|
+
break;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
// Check if torrent is still running
|
|
3524
|
+
if (!metadata_torrent->is_running()) {
|
|
3525
|
+
LOG_BT_DEBUG("Metadata torrent stopped, stopping peer additions");
|
|
3526
|
+
break;
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
const auto& peer = peers[i];
|
|
3530
|
+
LOG_BT_INFO("Connecting to peer " << peer.ip << ":" << peer.port << " for metadata retrieval");
|
|
3531
|
+
|
|
3532
|
+
if (metadata_torrent->add_peer(peer)) {
|
|
3533
|
+
peers_added++;
|
|
3534
|
+
} else {
|
|
3535
|
+
LOG_BT_DEBUG("Failed to add peer " << peer.ip << ":" << peer.port);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
LOG_BT_INFO("Added " << peers_added << " peers for metadata retrieval");
|
|
3540
|
+
});
|
|
3541
|
+
|
|
3542
|
+
LOG_BT_INFO("Metadata retrieval initiated for hash " << info_hash_to_hex(info_hash));
|
|
3543
|
+
LOG_BT_INFO("Waiting for DHT peer discovery and metadata exchange...");
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
void BitTorrentClient::get_torrent_metadata_by_hash(const std::string& info_hash_hex, MetadataRetrievalCallback callback) {
|
|
3547
|
+
InfoHash info_hash = hex_to_info_hash(info_hash_hex);
|
|
3548
|
+
|
|
3549
|
+
// Validate the parsed hash
|
|
3550
|
+
bool is_zero = true;
|
|
3551
|
+
for (const auto& byte : info_hash) {
|
|
3552
|
+
if (byte != 0) {
|
|
3553
|
+
is_zero = false;
|
|
3554
|
+
break;
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
if (is_zero && info_hash_hex.length() == 40) {
|
|
3559
|
+
LOG_BT_ERROR("Invalid info hash format: " << info_hash_hex);
|
|
3560
|
+
if (callback) {
|
|
3561
|
+
callback(TorrentInfo(), false, "Invalid info hash format");
|
|
3562
|
+
}
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
get_torrent_metadata_by_hash(info_hash, callback);
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
//=============================================================================
|
|
3570
|
+
// Utility Functions Implementation
|
|
3571
|
+
//=============================================================================
|
|
3572
|
+
|
|
3573
|
+
InfoHash calculate_info_hash(const BencodeValue& info_dict) {
|
|
3574
|
+
// Encode the info dictionary and calculate SHA1 hash
|
|
3575
|
+
std::vector<uint8_t> encoded = info_dict.encode();
|
|
3576
|
+
std::string hash_string = SHA1::hash_bytes(encoded);
|
|
3577
|
+
|
|
3578
|
+
InfoHash result;
|
|
3579
|
+
// Convert hex string to bytes
|
|
3580
|
+
for (size_t i = 0; i < 20; ++i) {
|
|
3581
|
+
std::string byte_str = hash_string.substr(i * 2, 2);
|
|
3582
|
+
result[i] = static_cast<uint8_t>(std::stoul(byte_str, nullptr, 16));
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
return result;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
std::string info_hash_to_hex(const InfoHash& hash) {
|
|
3589
|
+
std::ostringstream hex_stream;
|
|
3590
|
+
for (size_t i = 0; i < hash.size(); ++i) {
|
|
3591
|
+
hex_stream << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(hash[i]);
|
|
3592
|
+
}
|
|
3593
|
+
return hex_stream.str();
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
InfoHash hex_to_info_hash(const std::string& hex) {
|
|
3597
|
+
InfoHash result;
|
|
3598
|
+
result.fill(0);
|
|
3599
|
+
|
|
3600
|
+
if (hex.length() != 40) { // 20 bytes * 2 hex chars per byte
|
|
3601
|
+
return result;
|
|
3602
|
+
}
|
|
3603
|
+
|
|
3604
|
+
for (size_t i = 0; i < 20; ++i) {
|
|
3605
|
+
std::string byte_str = hex.substr(i * 2, 2);
|
|
3606
|
+
try {
|
|
3607
|
+
result[i] = static_cast<uint8_t>(std::stoul(byte_str, nullptr, 16));
|
|
3608
|
+
} catch (const std::exception&) {
|
|
3609
|
+
result.fill(0);
|
|
3610
|
+
return result;
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
return result;
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
PeerID generate_peer_id() {
|
|
3618
|
+
PeerID peer_id;
|
|
3619
|
+
|
|
3620
|
+
// Use librats- prefix to identify our client
|
|
3621
|
+
std::string prefix = "-LR0001-"; // LR = LibRats, 0001 = version
|
|
3622
|
+
std::copy(prefix.begin(), prefix.end(), peer_id.begin());
|
|
3623
|
+
|
|
3624
|
+
// Fill the rest with random data
|
|
3625
|
+
std::random_device rd;
|
|
3626
|
+
std::mt19937 gen(rd());
|
|
3627
|
+
std::uniform_int_distribution<> dis(0, 255);
|
|
3628
|
+
|
|
3629
|
+
for (size_t i = prefix.length(); i < peer_id.size(); ++i) {
|
|
3630
|
+
peer_id[i] = static_cast<uint8_t>(dis(gen));
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
return peer_id;
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
std::vector<uint8_t> create_handshake_message(const InfoHash& info_hash, const PeerID& peer_id) {
|
|
3637
|
+
std::vector<uint8_t> handshake(68);
|
|
3638
|
+
|
|
3639
|
+
// Protocol identifier length
|
|
3640
|
+
handshake[0] = BITTORRENT_PROTOCOL_ID_LENGTH;
|
|
3641
|
+
|
|
3642
|
+
// Protocol identifier
|
|
3643
|
+
std::copy_n(BITTORRENT_PROTOCOL_ID, BITTORRENT_PROTOCOL_ID_LENGTH, handshake.begin() + 1);
|
|
3644
|
+
|
|
3645
|
+
// Reserved bytes (8 bytes of zeros)
|
|
3646
|
+
std::fill_n(handshake.begin() + 20, 8, 0);
|
|
3647
|
+
|
|
3648
|
+
// Info hash (20 bytes)
|
|
3649
|
+
std::copy(info_hash.begin(), info_hash.end(), handshake.begin() + 28);
|
|
3650
|
+
|
|
3651
|
+
// Peer ID (20 bytes)
|
|
3652
|
+
std::copy(peer_id.begin(), peer_id.end(), handshake.begin() + 48);
|
|
3653
|
+
|
|
3654
|
+
return handshake;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
bool parse_handshake_message(const std::vector<uint8_t>& data, InfoHash& info_hash, PeerID& peer_id) {
|
|
3658
|
+
if (data.size() != 68) {
|
|
3659
|
+
return false;
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
// Check protocol identifier length
|
|
3663
|
+
if (data[0] != BITTORRENT_PROTOCOL_ID_LENGTH) {
|
|
3664
|
+
return false;
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
// Check protocol identifier
|
|
3668
|
+
std::string protocol_id(data.begin() + 1, data.begin() + 1 + BITTORRENT_PROTOCOL_ID_LENGTH);
|
|
3669
|
+
if (protocol_id != std::string(reinterpret_cast<const char*>(BITTORRENT_PROTOCOL_ID), BITTORRENT_PROTOCOL_ID_LENGTH)) {
|
|
3670
|
+
return false;
|
|
3671
|
+
}
|
|
3672
|
+
|
|
3673
|
+
// Extract info hash
|
|
3674
|
+
std::copy(data.begin() + 28, data.begin() + 48, info_hash.begin());
|
|
3675
|
+
|
|
3676
|
+
// Extract peer ID
|
|
3677
|
+
std::copy(data.begin() + 48, data.begin() + 68, peer_id.begin());
|
|
3678
|
+
|
|
3679
|
+
return true;
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
} // namespace librats
|