torrust_tracker/servers/http/v1/requests/
announce.rs

1//! `Announce` request for the HTTP tracker.
2//!
3//! Data structures and logic for parsing the `announce` request.
4use std::fmt;
5use std::panic::Location;
6use std::str::FromStr;
7
8use aquatic_udp_protocol::{NumberOfBytes, PeerId};
9use thiserror::Error;
10use torrust_tracker_located_error::{Located, LocatedError};
11use torrust_tracker_primitives::info_hash::{self, InfoHash};
12use torrust_tracker_primitives::peer;
13
14use crate::servers::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id};
15use crate::servers::http::v1::query::{ParseQueryError, Query};
16use crate::servers::http::v1::responses;
17
18// Query param names
19const INFO_HASH: &str = "info_hash";
20const PEER_ID: &str = "peer_id";
21const PORT: &str = "port";
22const DOWNLOADED: &str = "downloaded";
23const UPLOADED: &str = "uploaded";
24const LEFT: &str = "left";
25const EVENT: &str = "event";
26const COMPACT: &str = "compact";
27const NUMWANT: &str = "numwant";
28
29/// The `Announce` request. Fields use the domain types after parsing the
30/// query params of the request.
31///
32/// ```rust
33/// use aquatic_udp_protocol::{NumberOfBytes, PeerId};
34/// use torrust_tracker::servers::http::v1::requests::announce::{Announce, Compact, Event};
35/// use torrust_tracker_primitives::info_hash::InfoHash;
36///
37/// let request = Announce {
38///     // Mandatory params
39///     info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(),
40///     peer_id: PeerId(*b"-qB00000000000000001"),
41///     port: 17548,
42///     // Optional params
43///     downloaded: Some(NumberOfBytes::new(1)),
44///     uploaded: Some(NumberOfBytes::new(1)),
45///     left: Some(NumberOfBytes::new(1)),
46///     event: Some(Event::Started),
47///     compact: Some(Compact::NotAccepted),
48///     numwant: Some(50)
49/// };
50/// ```
51///
52/// > **NOTICE**: The [BEP 03. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html)
53/// > specifies that only the peer `IP` and `event`are optional. However, the
54/// > tracker defines default values for some of the mandatory params.
55///
56/// > **NOTICE**: The struct does not contain the `IP` of the peer. It's not
57/// > mandatory and it's not used by the tracker. The `IP` is obtained from the
58/// > request itself.
59#[derive(Debug, PartialEq)]
60pub struct Announce {
61    // Mandatory params
62    /// The `InfoHash` of the torrent.
63    pub info_hash: InfoHash,
64
65    /// The `PeerId` of the peer.
66    pub peer_id: PeerId,
67
68    /// The port of the peer.
69    pub port: u16,
70
71    // Optional params
72    /// The number of bytes downloaded by the peer.
73    pub downloaded: Option<NumberOfBytes>,
74
75    /// The number of bytes uploaded by the peer.
76    pub uploaded: Option<NumberOfBytes>,
77
78    /// The number of bytes left to download by the peer.
79    pub left: Option<NumberOfBytes>,
80
81    /// The event that the peer is reporting. It can be `Started`, `Stopped` or
82    /// `Completed`.
83    pub event: Option<Event>,
84
85    /// Whether the response should be in compact mode or not.
86    pub compact: Option<Compact>,
87
88    /// Number of peers that the client would receive from the tracker. The
89    /// value is permitted to be zero.
90    pub numwant: Option<u32>,
91}
92
93/// Errors that can occur when parsing the `Announce` request.
94///
95/// The `info_hash` and `peer_id` query params are special because they contain
96/// binary data. The `info_hash` is a 20-byte SHA1 hash and the `peer_id` is a
97/// 20-byte array.
98#[derive(Error, Debug)]
99pub enum ParseAnnounceQueryError {
100    /// A mandatory param is missing.
101    #[error("missing query params for announce request in {location}")]
102    MissingParams { location: &'static Location<'static> },
103    #[error("missing param {param_name} in {location}")]
104    MissingParam {
105        location: &'static Location<'static>,
106        param_name: String,
107    },
108    /// The param cannot be parsed into the domain type.
109    #[error("invalid param value {param_value} for {param_name} in {location}")]
110    InvalidParam {
111        param_name: String,
112        param_value: String,
113        location: &'static Location<'static>,
114    },
115    /// The param value is out of range.
116    #[error("param value overflow {param_value} for {param_name} in {location}")]
117    NumberOfBytesOverflow {
118        param_name: String,
119        param_value: String,
120        location: &'static Location<'static>,
121    },
122    /// The `info_hash` is invalid.
123    #[error("invalid param value {param_value} for {param_name} in {source}")]
124    InvalidInfoHashParam {
125        param_name: String,
126        param_value: String,
127        source: LocatedError<'static, info_hash::ConversionError>,
128    },
129    /// The `peer_id` is invalid.
130    #[error("invalid param value {param_value} for {param_name} in {source}")]
131    InvalidPeerIdParam {
132        param_name: String,
133        param_value: String,
134        source: LocatedError<'static, peer::IdConversionError>,
135    },
136}
137
138/// The event that the peer is reporting: `started`, `completed` or `stopped`.
139///
140/// If the event is not present or empty that means that the peer is just
141/// updating its status. It's one of the announcements done at regular intervals.
142///
143/// Refer to [BEP 03. The `BitTorrent Protocol` Specification](https://www.bittorrent.org/beps/bep_0003.html)
144/// for more information.
145#[derive(PartialEq, Debug)]
146pub enum Event {
147    /// Event sent when a download first begins.
148    Started,
149    /// Event sent when the downloader cease downloading.
150    Stopped,
151    /// Event sent when the download is complete.
152    /// No `completed` is sent if the file was complete when started
153    Completed,
154}
155
156impl FromStr for Event {
157    type Err = ParseAnnounceQueryError;
158
159    fn from_str(raw_param: &str) -> Result<Self, Self::Err> {
160        match raw_param {
161            "started" => Ok(Self::Started),
162            "stopped" => Ok(Self::Stopped),
163            "completed" => Ok(Self::Completed),
164            _ => Err(ParseAnnounceQueryError::InvalidParam {
165                param_name: EVENT.to_owned(),
166                param_value: raw_param.to_owned(),
167                location: Location::caller(),
168            }),
169        }
170    }
171}
172
173impl fmt::Display for Event {
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        match self {
176            Event::Started => write!(f, "started"),
177            Event::Stopped => write!(f, "stopped"),
178            Event::Completed => write!(f, "completed"),
179        }
180    }
181}
182
183/// Whether the `announce` response should be in compact mode or not.
184///
185/// Depending on the value of this param, the tracker will return a different
186/// response:
187///
188/// - [`Normal`](crate::servers::http::v1::responses::announce::Normal), i.e. a `non-compact` response.
189/// - [`Compact`](crate::servers::http::v1::responses::announce::Compact) response.
190///
191/// Refer to [BEP 23. Tracker Returns Compact Peer Lists](https://www.bittorrent.org/beps/bep_0023.html)
192#[derive(PartialEq, Debug)]
193pub enum Compact {
194    /// The client advises the tracker that the client prefers compact format.
195    Accepted = 1,
196    /// The client advises the tracker that is prefers the original format
197    /// described in [BEP 03. The BitTorrent Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html)
198    NotAccepted = 0,
199}
200
201impl fmt::Display for Compact {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        match self {
204            Compact::Accepted => write!(f, "1"),
205            Compact::NotAccepted => write!(f, "0"),
206        }
207    }
208}
209
210impl FromStr for Compact {
211    type Err = ParseAnnounceQueryError;
212
213    fn from_str(raw_param: &str) -> Result<Self, Self::Err> {
214        match raw_param {
215            "1" => Ok(Self::Accepted),
216            "0" => Ok(Self::NotAccepted),
217            _ => Err(ParseAnnounceQueryError::InvalidParam {
218                param_name: COMPACT.to_owned(),
219                param_value: raw_param.to_owned(),
220                location: Location::caller(),
221            }),
222        }
223    }
224}
225
226impl From<ParseQueryError> for responses::error::Error {
227    fn from(err: ParseQueryError) -> Self {
228        responses::error::Error {
229            failure_reason: format!("Cannot parse query params: {err}"),
230        }
231    }
232}
233
234impl From<ParseAnnounceQueryError> for responses::error::Error {
235    fn from(err: ParseAnnounceQueryError) -> Self {
236        responses::error::Error {
237            failure_reason: format!("Cannot parse query params for announce request: {err}"),
238        }
239    }
240}
241
242impl TryFrom<Query> for Announce {
243    type Error = ParseAnnounceQueryError;
244
245    fn try_from(query: Query) -> Result<Self, Self::Error> {
246        Ok(Self {
247            info_hash: extract_info_hash(&query)?,
248            peer_id: extract_peer_id(&query)?,
249            port: extract_port(&query)?,
250            downloaded: extract_downloaded(&query)?,
251            uploaded: extract_uploaded(&query)?,
252            left: extract_left(&query)?,
253            event: extract_event(&query)?,
254            compact: extract_compact(&query)?,
255            numwant: extract_numwant(&query)?,
256        })
257    }
258}
259
260// Mandatory params
261
262fn extract_info_hash(query: &Query) -> Result<InfoHash, ParseAnnounceQueryError> {
263    match query.get_param(INFO_HASH) {
264        Some(raw_param) => {
265            Ok(
266                percent_decode_info_hash(&raw_param).map_err(|err| ParseAnnounceQueryError::InvalidInfoHashParam {
267                    param_name: INFO_HASH.to_owned(),
268                    param_value: raw_param.clone(),
269                    source: Located(err).into(),
270                })?,
271            )
272        }
273        None => Err(ParseAnnounceQueryError::MissingParam {
274            location: Location::caller(),
275            param_name: INFO_HASH.to_owned(),
276        }),
277    }
278}
279
280fn extract_peer_id(query: &Query) -> Result<PeerId, ParseAnnounceQueryError> {
281    match query.get_param(PEER_ID) {
282        Some(raw_param) => Ok(
283            percent_decode_peer_id(&raw_param).map_err(|err| ParseAnnounceQueryError::InvalidPeerIdParam {
284                param_name: PEER_ID.to_owned(),
285                param_value: raw_param.clone(),
286                source: Located(err).into(),
287            })?,
288        ),
289        None => Err(ParseAnnounceQueryError::MissingParam {
290            location: Location::caller(),
291            param_name: PEER_ID.to_owned(),
292        }),
293    }
294}
295
296fn extract_port(query: &Query) -> Result<u16, ParseAnnounceQueryError> {
297    match query.get_param(PORT) {
298        Some(raw_param) => Ok(u16::from_str(&raw_param).map_err(|_e| ParseAnnounceQueryError::InvalidParam {
299            param_name: PORT.to_owned(),
300            param_value: raw_param.clone(),
301            location: Location::caller(),
302        })?),
303        None => Err(ParseAnnounceQueryError::MissingParam {
304            location: Location::caller(),
305            param_name: PORT.to_owned(),
306        }),
307    }
308}
309
310// Optional params
311
312fn extract_downloaded(query: &Query) -> Result<Option<NumberOfBytes>, ParseAnnounceQueryError> {
313    extract_number_of_bytes_from_param(DOWNLOADED, query)
314}
315
316fn extract_uploaded(query: &Query) -> Result<Option<NumberOfBytes>, ParseAnnounceQueryError> {
317    extract_number_of_bytes_from_param(UPLOADED, query)
318}
319
320fn extract_left(query: &Query) -> Result<Option<NumberOfBytes>, ParseAnnounceQueryError> {
321    extract_number_of_bytes_from_param(LEFT, query)
322}
323
324fn extract_number_of_bytes_from_param(param_name: &str, query: &Query) -> Result<Option<NumberOfBytes>, ParseAnnounceQueryError> {
325    match query.get_param(param_name) {
326        Some(raw_param) => {
327            let number_of_bytes = u64::from_str(&raw_param).map_err(|_e| ParseAnnounceQueryError::InvalidParam {
328                param_name: param_name.to_owned(),
329                param_value: raw_param.clone(),
330                location: Location::caller(),
331            })?;
332
333            let number_of_bytes =
334                i64::try_from(number_of_bytes).map_err(|_e| ParseAnnounceQueryError::NumberOfBytesOverflow {
335                    param_name: param_name.to_owned(),
336                    param_value: raw_param.clone(),
337                    location: Location::caller(),
338                })?;
339
340            let number_of_bytes = NumberOfBytes::new(number_of_bytes);
341
342            Ok(Some(number_of_bytes))
343        }
344        None => Ok(None),
345    }
346}
347
348fn extract_event(query: &Query) -> Result<Option<Event>, ParseAnnounceQueryError> {
349    match query.get_param(EVENT) {
350        Some(raw_param) => Ok(Some(Event::from_str(&raw_param)?)),
351        None => Ok(None),
352    }
353}
354
355fn extract_compact(query: &Query) -> Result<Option<Compact>, ParseAnnounceQueryError> {
356    match query.get_param(COMPACT) {
357        Some(raw_param) => Ok(Some(Compact::from_str(&raw_param)?)),
358        None => Ok(None),
359    }
360}
361
362fn extract_numwant(query: &Query) -> Result<Option<u32>, ParseAnnounceQueryError> {
363    match query.get_param(NUMWANT) {
364        Some(raw_param) => match u32::from_str(&raw_param) {
365            Ok(numwant) => Ok(Some(numwant)),
366            Err(_) => Err(ParseAnnounceQueryError::InvalidParam {
367                param_name: NUMWANT.to_owned(),
368                param_value: raw_param.clone(),
369                location: Location::caller(),
370            }),
371        },
372        None => Ok(None),
373    }
374}
375
376#[cfg(test)]
377mod tests {
378
379    mod announce_request {
380
381        use aquatic_udp_protocol::{NumberOfBytes, PeerId};
382        use torrust_tracker_primitives::info_hash::InfoHash;
383
384        use crate::servers::http::v1::query::Query;
385        use crate::servers::http::v1::requests::announce::{
386            Announce, Compact, Event, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, NUMWANT, PEER_ID, PORT, UPLOADED,
387        };
388
389        #[test]
390        fn should_be_instantiated_from_the_url_query_with_only_the_mandatory_params() {
391            let raw_query = Query::from(vec![
392                (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
393                (PEER_ID, "-qB00000000000000001"),
394                (PORT, "17548"),
395            ])
396            .to_string();
397
398            let query = raw_query.parse::<Query>().unwrap();
399
400            let announce_request = Announce::try_from(query).unwrap();
401
402            assert_eq!(
403                announce_request,
404                Announce {
405                    info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(),
406                    peer_id: PeerId(*b"-qB00000000000000001"),
407                    port: 17548,
408                    downloaded: None,
409                    uploaded: None,
410                    left: None,
411                    event: None,
412                    compact: None,
413                    numwant: None,
414                }
415            );
416        }
417
418        #[test]
419        fn should_be_instantiated_from_the_url_query_params() {
420            let raw_query = Query::from(vec![
421                (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
422                (PEER_ID, "-qB00000000000000001"),
423                (PORT, "17548"),
424                (DOWNLOADED, "1"),
425                (UPLOADED, "2"),
426                (LEFT, "3"),
427                (EVENT, "started"),
428                (COMPACT, "0"),
429                (NUMWANT, "50"),
430            ])
431            .to_string();
432
433            let query = raw_query.parse::<Query>().unwrap();
434
435            let announce_request = Announce::try_from(query).unwrap();
436
437            assert_eq!(
438                announce_request,
439                Announce {
440                    info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(),
441                    peer_id: PeerId(*b"-qB00000000000000001"),
442                    port: 17548,
443                    downloaded: Some(NumberOfBytes::new(1)),
444                    uploaded: Some(NumberOfBytes::new(2)),
445                    left: Some(NumberOfBytes::new(3)),
446                    event: Some(Event::Started),
447                    compact: Some(Compact::NotAccepted),
448                    numwant: Some(50),
449                }
450            );
451        }
452
453        mod when_it_is_instantiated_from_the_url_query_params {
454
455            use crate::servers::http::v1::query::Query;
456            use crate::servers::http::v1::requests::announce::{
457                Announce, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, NUMWANT, PEER_ID, PORT, UPLOADED,
458            };
459
460            #[test]
461            fn it_should_fail_if_the_query_does_not_include_all_the_mandatory_params() {
462                let raw_query_without_info_hash = "peer_id=-qB00000000000000001&port=17548";
463
464                assert!(Announce::try_from(raw_query_without_info_hash.parse::<Query>().unwrap()).is_err());
465
466                let raw_query_without_peer_id = "info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&port=17548";
467
468                assert!(Announce::try_from(raw_query_without_peer_id.parse::<Query>().unwrap()).is_err());
469
470                let raw_query_without_port =
471                    "info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_id=-qB00000000000000001";
472
473                assert!(Announce::try_from(raw_query_without_port.parse::<Query>().unwrap()).is_err());
474            }
475
476            #[test]
477            fn it_should_fail_if_the_info_hash_param_is_invalid() {
478                let raw_query = Query::from(vec![
479                    (INFO_HASH, "INVALID_INFO_HASH_VALUE"),
480                    (PEER_ID, "-qB00000000000000001"),
481                    (PORT, "17548"),
482                ])
483                .to_string();
484
485                assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
486            }
487
488            #[test]
489            fn it_should_fail_if_the_peer_id_param_is_invalid() {
490                let raw_query = Query::from(vec![
491                    (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
492                    (PEER_ID, "INVALID_PEER_ID_VALUE"),
493                    (PORT, "17548"),
494                ])
495                .to_string();
496
497                assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
498            }
499
500            #[test]
501            fn it_should_fail_if_the_port_param_is_invalid() {
502                let raw_query = Query::from(vec![
503                    (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
504                    (PEER_ID, "-qB00000000000000001"),
505                    (PORT, "INVALID_PORT_VALUE"),
506                ])
507                .to_string();
508
509                assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
510            }
511
512            #[test]
513            fn it_should_fail_if_the_downloaded_param_is_invalid() {
514                let raw_query = Query::from(vec![
515                    (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
516                    (PEER_ID, "-qB00000000000000001"),
517                    (PORT, "17548"),
518                    (DOWNLOADED, "INVALID_DOWNLOADED_VALUE"),
519                ])
520                .to_string();
521
522                assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
523            }
524
525            #[test]
526            fn it_should_fail_if_the_uploaded_param_is_invalid() {
527                let raw_query = Query::from(vec![
528                    (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
529                    (PEER_ID, "-qB00000000000000001"),
530                    (PORT, "17548"),
531                    (UPLOADED, "INVALID_UPLOADED_VALUE"),
532                ])
533                .to_string();
534
535                assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
536            }
537
538            #[test]
539            fn it_should_fail_if_the_left_param_is_invalid() {
540                let raw_query = Query::from(vec![
541                    (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
542                    (PEER_ID, "-qB00000000000000001"),
543                    (PORT, "17548"),
544                    (LEFT, "INVALID_LEFT_VALUE"),
545                ])
546                .to_string();
547
548                assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
549            }
550
551            #[test]
552            fn it_should_fail_if_the_event_param_is_invalid() {
553                let raw_query = Query::from(vec![
554                    (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
555                    (PEER_ID, "-qB00000000000000001"),
556                    (PORT, "17548"),
557                    (EVENT, "INVALID_EVENT_VALUE"),
558                ])
559                .to_string();
560
561                assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
562            }
563
564            #[test]
565            fn it_should_fail_if_the_compact_param_is_invalid() {
566                let raw_query = Query::from(vec![
567                    (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
568                    (PEER_ID, "-qB00000000000000001"),
569                    (PORT, "17548"),
570                    (COMPACT, "INVALID_COMPACT_VALUE"),
571                ])
572                .to_string();
573
574                assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
575            }
576
577            #[test]
578            fn it_should_fail_if_the_numwant_param_is_invalid() {
579                let raw_query = Query::from(vec![
580                    (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
581                    (PEER_ID, "-qB00000000000000001"),
582                    (PORT, "17548"),
583                    (NUMWANT, "-1"),
584                ])
585                .to_string();
586
587                assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
588            }
589        }
590    }
591}