Skip to main content

aria2_core/engine/
http_tracker_client.rs

1//! HTTP/HTTPS Tracker Client with Event State Machine
2//!
3//! This module provides a stateful tracker client that supports both HTTP and HTTPS
4//! tracker URLs, with proper event lifecycle management (Started -> Completed -> Stopped).
5//!
6//! # Event State Machine
7//!
8//! The tracker follows a well-defined event sequence:
9//! - **Started**: Sent when download begins (first announce)
10//! - **Completed**: Sent when all pieces are downloaded successfully
11//! - **Stopped**: Sent when download is cancelled or removed
12//! - **None**: Regular interval announces (no specific event)
13
14use std::time::{Duration, Instant};
15
16use tracing::{debug, info};
17
18/// Tracker announce events as defined in BEP 3 / BEP 15.
19///
20/// These events control the lifecycle of tracker announcements.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum TrackerEvent {
23    /// Download started - sent on first announce
24    Started,
25    /// Download stopped/cancelled - sent on shutdown
26    Stopped,
27    /// Download completed - sent when all pieces downloaded
28    Completed,
29    /// No specific event - used for regular interval announces
30    None,
31}
32
33impl TrackerEvent {
34    /// Convert event to the string value expected by trackers
35    pub fn as_str(&self) -> &'static str {
36        match self {
37            TrackerEvent::Started => "started",
38            TrackerEvent::Stopped => "stopped",
39            TrackerEvent::Completed => "completed",
40            TrackerEvent::None => "",
41        }
42    }
43}
44
45/// State machine for tracking announce events and intervals.
46///
47/// Manages the current event state, timing of announces, and ensures
48/// proper event sequencing according to BitTorrent specification.
49#[derive(Debug)]
50pub struct TrackerState {
51    /// Current event to send on next announce
52    pub current_event: TrackerEvent,
53    /// Timestamp of last successful announce
54    pub last_announce_time: Option<Instant>,
55    /// Minimum interval between announces (from tracker response)
56    pub min_interval_secs: u64,
57    /// Normal interval between announces (from tracker response)
58    pub interval_secs: u64,
59    /// Total number of announces sent
60    pub announce_count: u64,
61    /// Whether we have already sent the 'completed' event
62    completed_sent: bool,
63    /// Whether we have already sent the 'stopped' event
64    stopped_sent: bool,
65}
66
67impl Default for TrackerState {
68    fn default() -> Self {
69        Self {
70            current_event: TrackerEvent::Started, // First announce should be Started
71            last_announce_time: None,
72            min_interval_secs: 300, // Default 5 minutes
73            interval_secs: 1800,    // Default 30 minutes
74            announce_count: 0,
75            completed_sent: false,
76            stopped_sent: false,
77        }
78    }
79}
80
81impl TrackerState {
82    /// Create a new tracker state with default values
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    /// Check if it's time to send another announce based on interval constraints
88    ///
89    /// Returns true if enough time has elapsed since the last announce
90    /// according to min_interval_secs
91    pub fn should_announce(&self) -> bool {
92        match self.last_announce_time {
93            Some(last) => {
94                let elapsed = last.elapsed().as_secs();
95                elapsed >= self.min_interval_secs
96            }
97            None => true, // Never announced before
98        }
99    }
100
101    /// Get the recommended wait time until next announce
102    pub fn secs_until_next_announce(&self) -> u64 {
103        match self.last_announce_time {
104            Some(last) => {
105                let elapsed = last.elapsed().as_secs();
106                if elapsed >= self.min_interval_secs {
107                    0
108                } else {
109                    self.min_interval_secs.saturating_sub(elapsed)
110                }
111            }
112            None => 0,
113        }
114    }
115
116    /// Mark that an announce has been sent and advance the event state
117    ///
118    /// This updates internal timing and transitions the event state:
119    /// - After Started -> None (for regular announces)
120    /// - After Completed -> None
121    /// - After Stopped -> stays Stopped (terminal state)
122    pub fn record_announce(&mut self, event: TrackerEvent) {
123        self.current_event = match event {
124            TrackerEvent::Started => {
125                debug!("TrackerState: Recorded Started event");
126                TrackerEvent::None // Next announce has no special event
127            }
128            TrackerEvent::Completed => {
129                self.completed_sent = true;
130                info!("TrackerState: Recorded Completed event");
131                TrackerEvent::None // Next announce has no special event
132            }
133            TrackerEvent::Stopped => {
134                self.stopped_sent = true;
135                info!("TrackerState: Recorded Stopped event (terminal)");
136                TrackerEvent::Stopped // Stay in stopped state
137            }
138            TrackerEvent::None => TrackerEvent::None,
139        };
140
141        self.last_announce_time = Some(Instant::now());
142        self.announce_count += 1;
143    }
144
145    /// Update interval values from tracker response
146    ///
147    /// Trackers return `interval` (recommended) and optionally `min interval`
148    /// which constrain how often we may re-announce.
149    pub fn update_intervals(&mut self, interval: Option<u64>, min_interval: Option<u64>) {
150        if let Some(iv) = interval {
151            // Sanity check: don't allow extremely short intervals (< 60s)
152            self.interval_secs = iv.max(60);
153            debug!("TrackerState: Updated interval to {}s", self.interval_secs);
154        }
155        if let Some(mi) = min_interval {
156            // Sanity check: don't allow extremely short min intervals (< 30s)
157            self.min_interval_secs = mi.max(30);
158            debug!(
159                "TrackerState: Updated min_interval to {}s",
160                self.min_interval_secs
161            );
162        }
163    }
164
165    /// Transition to completed state - call when all pieces are downloaded
166    pub fn mark_completed(&mut self) {
167        if !self.completed_sent {
168            self.current_event = TrackerEvent::Completed;
169            info!("TrackerState: Transitioning to Completed event");
170        }
171    }
172
173    /// Transition to stopped state - call on shutdown/cancel
174    pub fn mark_stopped(&mut self) {
175        if !self.stopped_sent {
176            self.current_event = TrackerEvent::Stopped;
177            info!("TrackerState: Transitioning to Stopped event");
178        }
179    }
180
181    /// Reset state for a new download session
182    pub fn reset(&mut self) {
183        *self = Self::default();
184    }
185
186    /// Check if the tracker is in terminal (stopped) state
187    pub fn is_stopped(&self) -> bool {
188        self.stopped_sent
189    }
190}
191
192/// Detect whether a tracker URL uses HTTPS scheme.
193///
194/// reqwest supports HTTPS natively with default features (native-tls),
195/// but this function allows explicit checking for logging or configuration purposes.
196///
197/// # Arguments
198/// * `url` - The tracker URL to check
199///
200/// # Returns
201/// true if the URL starts with "https://", false otherwise
202pub fn is_https_tracker(url: &str) -> bool {
203    url.to_lowercase().starts_with("https://")
204}
205
206/// Build a reqwest::Client configured appropriately for the given URL scheme.
207///
208/// For HTTPS URLs, returns a standard client (reqwest handles TLS by default).
209/// For HTTP URLs, returns a standard client without TLS requirements.
210///
211/// # Arguments
212/// * `timeout_secs` - Request timeout in seconds
213pub fn build_tracker_client(timeout_secs: u64) -> Result<reqwest::Client, String> {
214    reqwest::Client::builder()
215        .timeout(Duration::from_secs(timeout_secs))
216        .build()
217        .map_err(|e| format!("Failed to build HTTP client: {}", e))
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_https_tracker_url_detected() {
226        assert!(is_https_tracker("https://tracker.example.com/announce"));
227        assert!(is_https_tracker("HTTPS://TRACKER.EXAMPLE.COM/ANNOUNCE"));
228        assert!(!is_https_tracker("http://tracker.example.com/announce"));
229        assert!(!is_https_tracker("udp://tracker.example.com:1337/announce"));
230    }
231
232    #[test]
233    fn test_tracker_event_as_str() {
234        assert_eq!(TrackerEvent::Started.as_str(), "started");
235        assert_eq!(TrackerEvent::Stopped.as_str(), "stopped");
236        assert_eq!(TrackerEvent::Completed.as_str(), "completed");
237        assert_eq!(TrackerEvent::None.as_str(), "");
238    }
239
240    #[test]
241    fn test_tracker_state_default() {
242        let state = TrackerState::new();
243        assert_eq!(state.current_event, TrackerEvent::Started);
244        assert!(state.last_announce_time.is_none());
245        assert_eq!(state.announce_count, 0);
246        assert!(!state.completed_sent);
247        assert!(!state.stopped_sent);
248    }
249
250    #[test]
251    fn test_tracker_state_should_announce_initially() {
252        let state = TrackerState::new();
253        assert!(
254            state.should_announce(),
255            "Should be able to announce immediately on fresh state"
256        );
257    }
258
259    #[test]
260    fn test_tracker_state_sequence_started_to_completed() {
261        let mut state = TrackerState::new();
262
263        // Initial state should be Started
264        assert_eq!(state.current_event, TrackerEvent::Started);
265
266        // Record started event
267        state.record_announce(TrackerEvent::Started);
268        assert_eq!(state.announce_count, 1);
269        assert_eq!(state.current_event, TrackerEvent::None); // Transitions to None
270
271        // Now mark as completed
272        state.mark_completed();
273        assert_eq!(state.current_event, TrackerEvent::Completed);
274        assert!(!state.completed_sent); // Not sent yet
275
276        // Record completed event
277        state.record_announce(TrackerEvent::Completed);
278        assert_eq!(state.announce_count, 2);
279        assert!(state.completed_sent);
280        assert_eq!(state.current_event, TrackerEvent::None); // Transitions back to None
281    }
282
283    #[test]
284    fn test_tracker_event_stopped_on_cancel() {
285        let mut state = TrackerState::new();
286
287        // Simulate started
288        state.record_announce(TrackerEvent::Started);
289
290        // Cancel the download
291        state.mark_stopped();
292        assert_eq!(state.current_event, TrackerEvent::Stopped);
293        assert!(!state.stopped_sent); // Not sent yet
294
295        // Record stopped event
296        state.record_announce(TrackerEvent::Stopped);
297        assert!(state.stopped_sent);
298        // Stopped is terminal - should stay stopped
299        assert_eq!(state.current_event, TrackerEvent::Stopped);
300    }
301
302    #[test]
303    fn test_min_interval_respected() {
304        let mut state = TrackerState::new();
305        state.min_interval_secs = 10;
306
307        // Initial announce
308        assert!(state.should_announce());
309        state.record_announce(TrackerEvent::Started);
310
311        // Immediately after - should NOT be allowed
312        assert!(
313            !state.should_announce(),
314            "Should not re-announce before min_interval"
315        );
316
317        // Check that secs_until_next_announce is positive
318        let wait = state.secs_until_next_announce();
319        assert!(wait > 0, "Should need to wait at least some seconds");
320        assert!(wait <= 10, "Should not wait more than min_interval");
321    }
322
323    #[test]
324    fn test_update_intervals() {
325        let mut state = TrackerState::new();
326        assert_eq!(state.interval_secs, 1800); // Default 30 min
327        assert_eq!(state.min_interval_secs, 300); // Default 5 min
328
329        state.update_intervals(Some(900), Some(300));
330        assert_eq!(state.interval_secs, 900);
331        assert_eq!(state.min_interval_secs, 300);
332
333        // Test floor enforcement
334        state.update_intervals(Some(10), Some(5));
335        assert_eq!(state.interval_secs, 60); // Floor at 60s
336        assert_eq!(state.min_interval_secs, 30); // Floor at 30s
337    }
338
339    #[test]
340    fn test_build_tracker_client_succeeds() {
341        let client = build_tracker_client(30);
342        assert!(client.is_ok(), "Should build client with valid timeout");
343    }
344
345    #[test]
346    fn test_is_stopped_terminal_state() {
347        let mut state = TrackerState::new();
348        assert!(!state.is_stopped());
349
350        state.mark_stopped();
351        state.record_announce(TrackerEvent::Stopped);
352        assert!(state.is_stopped());
353    }
354
355    #[test]
356    fn test_reset_clears_state() {
357        let mut state = TrackerState::new();
358        state.record_announce(TrackerEvent::Started);
359        state.mark_completed();
360        state.record_announce(TrackerEvent::Completed);
361
362        state.reset();
363        assert_eq!(state.current_event, TrackerEvent::Started);
364        assert_eq!(state.announce_count, 0);
365        assert!(!state.completed_sent);
366        assert!(!state.stopped_sent);
367    }
368
369    #[test]
370    fn test_multiple_completes_only_one_event() {
371        let mut state = TrackerState::new();
372        state.record_announce(TrackerEvent::Started);
373
374        // First complete
375        state.mark_completed();
376        assert_eq!(state.current_event, TrackerEvent::Completed);
377        state.record_announce(TrackerEvent::Completed);
378
379        // Second complete attempt should not trigger another Completed event
380        state.mark_completed();
381        assert_eq!(
382            state.current_event,
383            TrackerEvent::None,
384            "Second mark_completed should not change state"
385        );
386    }
387}