1#![allow(clippy::empty_line_after_doc_comments)]
2
3use crate::engine::http_tracker_client::{TrackerEvent, build_tracker_client, is_https_tracker};
4use crate::error::{Aria2Error, RecoverableError, Result};
5use std::time::{Duration, Instant};
6use tracing::{debug, info};
7
8const TRACKER_REQUEST_TIMEOUT_SECS: u64 = 5;
10
11pub fn urlencode_infohash(hash: &[u8; 20]) -> String {
30 hash.iter().map(|b| format!("%{:02X}", b)).collect()
31}
32
33pub async fn announce_to_public_tracker(
59 tracker_url: &str,
60 info_hash: &[u8; 20],
61 peer_id: &[u8; 20],
62 total_size: u64,
63) -> std::result::Result<Vec<(String, u16)>, String> {
64 announce_to_public_tracker_with_event(
65 tracker_url,
66 info_hash,
67 peer_id,
68 total_size,
69 TrackerEvent::Started, )
71 .await
72}
73
74pub async fn announce_to_public_tracker_with_event(
86 tracker_url: &str,
87 info_hash: &[u8; 20],
88 peer_id: &[u8; 20],
89 total_size: u64,
90 event: TrackerEvent,
91) -> std::result::Result<Vec<(String, u16)>, String> {
92 let is_https = is_https_tracker(tracker_url);
94 if is_https {
95 debug!("HTTPS tracker detected: {} (using native-tls)", tracker_url);
96 }
97
98 let event_param = if event == TrackerEvent::None {
99 String::new()
100 } else {
101 format!("&event={}", event.as_str())
102 };
103
104 let url = format!(
105 "{}?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left={}{}&compact=1",
106 tracker_url,
107 urlencode_infohash(info_hash),
108 urlencode_infohash(peer_id),
109 total_size,
110 event_param,
111 );
112
113 let client = build_tracker_client(TRACKER_REQUEST_TIMEOUT_SECS)
114 .map_err(|e| format!("build client: {}", e))?;
115
116 let resp = client
117 .get(&url)
118 .send()
119 .await
120 .map_err(|e| format!("request failed: {}", e))?;
121
122 if !resp.status().is_success() {
123 return Err(format!("HTTP {}", resp.status()));
124 }
125
126 let body = resp
127 .bytes()
128 .await
129 .map_err(|e| format!("read body: {}", e))?;
130
131 let tracker_resp = aria2_protocol::bittorrent::tracker::response::TrackerResponse::parse(&body)
132 .map_err(|e| format!("parse response: {}", e))?;
133
134 if tracker_resp.is_failure() {
135 return Err(tracker_resp
136 .failure_reason
137 .unwrap_or_else(|| "tracker failure".to_string()));
138 }
139
140 Ok(tracker_resp
141 .peers
142 .into_iter()
143 .map(|p| (p.ip, p.port))
144 .collect())
145}
146
147pub async fn perform_http_tracker_announce(
171 announce_url: &str,
172 info_hash_raw: &[u8; 20],
173 my_peer_id: &[u8; 20],
174 total_size: u64,
175) -> Result<Vec<aria2_protocol::bittorrent::peer::connection::PeerAddr>> {
176 let is_https = is_https_tracker(announce_url);
178 if is_https {
179 debug!("[BT] HTTPS tracker detected for announce: {}", announce_url);
180 }
181
182 let url = format!(
183 "{}?info_hash={}&peer_id={}&port=6881&uploaded=0&downloaded=0&left={}&event=started&compact=1",
184 announce_url,
185 urlencode_infohash(info_hash_raw),
186 urlencode_infohash(my_peer_id),
187 total_size,
188 );
189
190 info!("[BT] Announcing to tracker: {}", url);
191 let client = build_tracker_client(TRACKER_REQUEST_TIMEOUT_SECS).map_err(|e| {
192 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
193 message: format!("Failed to build tracker client: {}", e),
194 })
195 })?;
196
197 let resp = client.get(&url).send().await.map_err(|e| {
198 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
199 message: format!("Tracker HTTP failed: {}", e),
200 })
201 })?;
202 info!("[BT] Tracker response status: {}", resp.status());
203 let body = resp.bytes().await.map_err(|e| {
204 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
205 message: format!("Tracker body read failed: {}", e),
206 })
207 })?;
208 debug!("[BT] Tracker body: {:?}", String::from_utf8_lossy(&body));
209
210 let tracker_resp = aria2_protocol::bittorrent::tracker::response::TrackerResponse::parse(&body)
211 .map_err(|e| {
212 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
213 message: format!("Tracker parse failed: {}", e),
214 })
215 })?;
216
217 info!("[BT] Tracker response: {} peers", tracker_resp.peer_count());
218 for peer in &tracker_resp.peers {
219 debug!("[BT] Peer: {}:{}", peer.ip, peer.port);
220 }
221
222 if tracker_resp.is_failure() {
223 return Err(Aria2Error::Recoverable(
224 RecoverableError::TemporaryNetworkFailure {
225 message: tracker_resp.failure_reason.unwrap_or_default(),
226 },
227 ));
228 }
229
230 Ok(tracker_resp
231 .peers
232 .iter()
233 .map(|p| aria2_protocol::bittorrent::peer::connection::PeerAddr::new(&p.ip, p.port))
234 .collect())
235}
236
237pub async fn perform_announce_with_event(
241 announce_url: &str,
242 info_hash_raw: &[u8; 20],
243 my_peer_id: &[u8; 20],
244 downloaded: u64,
245 left: u64,
246 uploaded: u64,
247 event: TrackerEvent,
248) -> Result<()> {
249 let is_https = is_https_tracker(announce_url);
250
251 let event_str = event.as_str();
252 let event_param = if event_str.is_empty() {
253 String::new()
254 } else {
255 format!("&event={}", event_str)
256 };
257
258 let url = format!(
259 "{}?info_hash={}&peer_id={}&port=6881&uploaded={}downloaded={}left={}{}compact=1",
260 announce_url,
261 urlencode_infohash(info_hash_raw),
262 urlencode_infohash(my_peer_id),
263 uploaded,
264 downloaded,
265 left,
266 event_param,
267 );
268
269 info!(
270 "[BT] Announce to {} (event={}, https={})",
271 announce_url, event_str, is_https
272 );
273
274 let client = build_tracker_client(TRACKER_REQUEST_TIMEOUT_SECS).map_err(|e| {
275 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
276 message: format!("Failed to build tracker client: {}", e),
277 })
278 })?;
279
280 let resp = client.get(&url).send().await.map_err(|e| {
281 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
282 message: format!("Tracker HTTP failed: {}", e),
283 })
284 })?;
285
286 let body = resp.bytes().await.map_err(|e| {
287 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
288 message: format!("Tracker body read failed: {}", e),
289 })
290 })?;
291
292 let tracker_resp = aria2_protocol::bittorrent::tracker::response::TrackerResponse::parse(&body)
293 .map_err(|e| {
294 Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
295 message: format!("Tracker parse failed: {}", e),
296 })
297 })?;
298
299 if tracker_resp.is_failure() {
300 return Err(Aria2Error::Recoverable(
301 RecoverableError::TemporaryNetworkFailure {
302 message: tracker_resp.failure_reason.unwrap_or_default(),
303 },
304 ));
305 }
306
307 info!("[BT] Announce success (event={})", event_str);
308 Ok(())
309}
310
311#[derive(Debug, Clone)]
317pub struct TrackerEntry {
318 pub url: String,
319 pub last_success: Option<Instant>,
320 pub last_failure: Option<Instant>,
321 pub failure_count: u32,
322 pub success_count: u32,
323 pub avg_response_ms: f64,
324 pub next_retry_after: Option<Instant>,
325}
326
327impl TrackerEntry {
328 pub fn new(url: String) -> Self {
330 Self {
331 url,
332 last_success: None,
333 last_failure: None,
334 failure_count: 0,
335 success_count: 0,
336 avg_response_ms: 0.0,
337 next_retry_after: None,
338 }
339 }
340
341 pub fn reliability_score(&self) -> f64 {
343 let total = self.success_count + self.failure_count;
344 if total == 0 {
345 return 0.5; }
347 let base_score = self.success_count as f64 / (total as f64 + 1.0);
348 let recency_penalty = match self.last_failure {
350 Some(t) if t.elapsed().as_secs() < 300 => 0.3,
351 Some(_) => 0.1,
352 None => 0.0,
353 };
354 (base_score - recency_penalty).clamp(0.0, 1.0)
355 }
356
357 pub fn record_success(&mut self, latency_ms: f64) {
359 self.success_count += 1;
360 self.last_success = Some(Instant::now());
361 self.failure_count = 0; if self.avg_response_ms <= 0.0 {
363 self.avg_response_ms = latency_ms;
364 } else {
365 self.avg_response_ms = self.avg_response_ms * 0.9 + latency_ms * 0.1;
366 }
367 }
368
369 pub fn record_failure(&mut self) {
371 self.failure_count += 1;
372 self.last_failure = Some(Instant::now());
373 self.schedule_backoff(10);
374 }
375
376 pub fn schedule_backoff(&mut self, base_secs: u64) {
378 let exp = self.failure_count.saturating_sub(1).min(10);
379 let delay = base_secs.saturating_mul(1 << exp);
380 let capped = delay.min(3600);
381 self.next_retry_after = Some(Instant::now() + Duration::from_secs(capped));
382 }
383
384 pub fn is_available(&self) -> bool {
386 if let Some(retry_at) = self.next_retry_after {
387 Instant::now() >= retry_at
388 } else {
389 true
390 }
391 }
392}
393
394#[derive(Debug, Clone)]
396pub struct TrackerTier {
397 pub trackers: Vec<TrackerEntry>,
398 pub current_index: usize,
399 pub consecutive_failures: u32,
400}
401
402impl TrackerTier {
403 pub fn new(urls: Vec<String>) -> Self {
405 let trackers = urls.into_iter().map(TrackerEntry::new).collect();
406 Self {
407 trackers,
408 current_index: 0,
409 consecutive_failures: 0,
410 }
411 }
412
413 pub fn select_next(&mut self) -> Option<&TrackerEntry> {
415 if self.current_index < self.trackers.len()
417 && self.trackers[self.current_index].is_available()
418 {
419 return Some(&self.trackers[self.current_index]);
420 }
421
422 let mut best_idx = None;
424 let mut best_score = -1.0f64;
425 for (i, t) in self.trackers.iter().enumerate() {
426 if t.is_available() {
427 let score = t.reliability_score();
428 if score > best_score {
429 best_score = score;
430 best_idx = Some(i);
431 }
432 }
433 }
434
435 if let Some(idx) = best_idx {
436 self.current_index = idx;
437 return Some(&self.trackers[idx]);
438 }
439
440 None }
442
443 pub fn mark_current_success(&mut self, latency_ms: f64) {
445 if self.current_index < self.trackers.len() {
446 self.trackers[self.current_index].record_success(latency_ms);
447 }
448 self.consecutive_failures = 0;
449 }
450
451 pub fn mark_current_failure(&mut self) {
453 if self.current_index < self.trackers.len() {
454 self.trackers[self.current_index].record_failure();
455 }
456 self.consecutive_failures += 1;
457 }
458}
459
460#[derive(Debug, Clone)]
462pub struct AnnounceList {
463 pub tiers: Vec<TrackerTier>,
464 pub current_tier: usize,
465}
466
467impl AnnounceList {
468 pub fn new(announce_list: &[Vec<String>], announce: &Option<String>) -> Self {
473 let mut tiers = Vec::new();
474 if !announce_list.is_empty() {
475 for tier_urls in announce_list {
476 tiers.push(TrackerTier::new(tier_urls.clone()));
477 }
478 } else if let Some(url) = announce {
479 tiers.push(TrackerTier::new(vec![url.clone()]));
480 }
481 Self {
482 tiers,
483 current_tier: 0,
484 }
485 }
486
487 pub fn select_next_tracker(&mut self) -> Option<(usize, usize)> {
489 if self.tiers.is_empty() {
490 return None;
491 }
492
493 if let Some(_entry) = self.tiers[self.current_tier].select_next() {
495 return Some((
496 self.current_tier,
497 self.tiers[self.current_tier].current_index,
498 ));
499 }
500
501 for offset in 1..=self.tiers.len() {
503 let tier_idx = (self.current_tier + offset) % self.tiers.len();
504 if let Some(_entry) = self.tiers[tier_idx].select_next() {
505 self.current_tier = tier_idx;
506 return Some((tier_idx, self.tiers[tier_idx].current_index));
507 }
508 }
509
510 None }
512
513 pub fn record_success(&mut self, tier_idx: usize, latency_ms: f64) {
515 if tier_idx < self.tiers.len() {
516 self.tiers[tier_idx].mark_current_success(latency_ms);
517 }
518 }
519
520 pub fn record_failure(&mut self, tier_idx: usize) {
522 if tier_idx < self.tiers.len() {
523 self.tiers[tier_idx].mark_current_failure();
524 }
525 }
526
527 pub fn get_tracker_url(&self, tier_idx: usize, entry_idx: usize) -> Option<&String> {
529 self.tiers
530 .get(tier_idx)
531 .and_then(|t| t.trackers.get(entry_idx))
532 .map(|e| &e.url)
533 }
534}
535
536#[cfg(test)]
541mod tests {
542 use super::*;
543
544 #[test]
545 fn test_announce_list_creation() {
546 let list1 = AnnounceList::new(&[], &Some("http://tracker1.com/announce".to_string()));
548 assert_eq!(list1.tiers.len(), 1);
549 assert_eq!(list1.tiers[0].trackers.len(), 1);
550 assert_eq!(
551 list1.get_tracker_url(0, 0).unwrap(),
552 &"http://tracker1.com/announce".to_string()
553 );
554
555 let multi_tier = vec![
557 vec![
558 "http://tier1-1.com/announce".to_string(),
559 "http://tier1-2.com/announce".to_string(),
560 ],
561 vec!["http://tier2-1.com/announce".to_string()],
562 ];
563 let list2 = AnnounceList::new(&multi_tier, &None);
564 assert_eq!(list2.tiers.len(), 2);
565 assert_eq!(list2.tiers[0].trackers.len(), 2);
566 assert_eq!(list2.tiers[1].trackers.len(), 1);
567
568 let mut list3 = AnnounceList::new(&[], &None);
570 assert_eq!(list3.tiers.len(), 0);
571 assert!(list3.select_next_tracker().is_none());
572 }
573
574 #[test]
575 fn test_tier_selection_order() {
576 let mut tier = TrackerTier::new(vec![
577 "http://tracker-a.com".to_string(),
578 "http://tracker-b.com".to_string(),
579 "http://tracker-c.com".to_string(),
580 ]);
581
582 tier.trackers[1].record_success(50.0);
584 tier.trackers[1].record_success(45.0);
585 tier.trackers[1].record_success(55.0);
586
587 tier.trackers[2].record_failure();
589
590 tier.trackers[0].record_failure();
592
593 let selected = tier.select_next();
595 assert!(selected.is_some());
596 assert_eq!(tier.current_index, 1);
598 }
599
600 #[test]
601 fn test_failover_across_tiers() {
602 let mut announce_list = AnnounceList::new(
603 &[
604 vec!["http://tier1-tracker.com/announce".to_string()],
605 vec!["http://tier2-tracker.com/announce".to_string()],
606 ],
607 &None,
608 );
609
610 let selection1 = announce_list.select_next_tracker();
612 assert_eq!(selection1, Some((0, 0)));
613
614 announce_list.tiers[0].trackers[0].record_failure();
616 announce_list.tiers[0].trackers[0].record_failure();
617 announce_list.tiers[0].trackers[0].record_failure(); let selection2 = announce_list.select_next_tracker();
621 assert_eq!(selection2, Some((1, 0)));
622 assert_eq!(announce_list.current_tier, 1);
623 }
624
625 #[test]
626 fn test_exponential_backoff_sequence() {
627 let mut entry = TrackerEntry::new("http://tracker.test/announce".to_string());
628
629 let expected_delays = [10u64, 20, 40, 80, 160, 320, 640, 1280, 2560, 3600];
631
632 for (i, &expected) in expected_delays.iter().enumerate() {
633 entry.record_failure();
634
635 let base: u64 = 10;
637 let exp = entry.failure_count.saturating_sub(1).min(10);
638 let calculated_delay = base.saturating_mul(1 << exp).min(3600);
639 assert_eq!(
640 calculated_delay,
641 expected,
642 "Failure {}: expected {}s, got {}s",
643 i + 1,
644 expected,
645 calculated_delay
646 );
647 }
648
649 assert!(entry.next_retry_after.is_some());
651 }
652
653 #[test]
654 fn test_reliability_scoring() {
655 let mut entry1 = TrackerEntry::new("http://good.tracker/announce".to_string());
656 let mut entry2 = TrackerEntry::new("http://bad.tracker/announce".to_string());
657 let entry3 = TrackerEntry::new("http://unknown.tracker/announce".to_string());
658
659 for _ in 0..10 {
661 entry1.record_success(100.0);
662 }
663
664 for _ in 0..5 {
666 entry2.record_failure();
667 }
668
669 let score1 = entry1.reliability_score();
672 let score2 = entry2.reliability_score();
673 let score3 = entry3.reliability_score();
674
675 assert!(
677 score1 > score3,
678 "Good tracker ({}) should beat unknown ({})",
679 score1,
680 score3
681 );
682
683 assert!(
685 score3 > score2,
686 "Unknown ({}) should beat bad tracker ({})",
687 score3,
688 score2
689 );
690
691 assert!(
693 score1 > score2,
694 "Good tracker ({}) should beat bad tracker ({})",
695 score1,
696 score2
697 );
698 }
699}