diff --git a/cSpell.json b/cSpell.json index e7c0166f8..af0de7101 100644 --- a/cSpell.json +++ b/cSpell.json @@ -5,6 +5,7 @@ "automock", "Avicora", "Azureus", + "bdecode", "bencode", "bencoded", "beps", @@ -56,6 +57,7 @@ "reannounce", "repr", "reqwest", + "rerequests", "rngs", "rusqlite", "rustfmt", diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 8b4d9363d..d5beca236 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -64,9 +64,29 @@ pub struct Configuration { pub db_driver: DatabaseDriver, pub db_path: String, - /// Interval in seconds that the client should wait between sending regular announce requests to the tracker + /// Interval in seconds that the client should wait between sending regular + /// announce requests to the tracker. + /// + /// It's a **recommended** wait time between announcements. + /// + /// This is the standard amount of time that clients should wait between + /// sending consecutive announcements to the tracker. This value is set by + /// the tracker and is typically provided in the tracker's response to a + /// client's initial request. It serves as a guideline for clients to know + /// how often they should contact the tracker for updates on the peer list, + /// while ensuring that the tracker is not overwhelmed with requests. pub announce_interval: u32, - /// Minimum announce interval. Clients must not reannounce more frequently than this + /// Minimum announce interval. Clients must not reannounce more frequently + /// than this. + /// + /// It establishes the shortest allowed wait time. + /// + /// This is an optional parameter in the protocol that the tracker may + /// provide in its response. It sets a lower limit on the frequency at which + /// clients are allowed to send announcements. Clients should respect this + /// value to prevent sending too many requests in a short period, which + /// could lead to excessive load on the tracker or even getting banned by + /// the tracker for not adhering to the rules. pub min_announce_interval: u32, pub on_reverse_proxy: bool, pub external_ip: Option, diff --git a/src/lib.rs b/src/lib.rs index 3b9777b36..36d1792d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,9 +33,13 @@ //! - [Run with docker](#run-with-docker) //! - [Configuration](#configuration) //! - [Usage](#usage) +//! - [API](#api) +//! - [HTTP Tracker](#http-tracker) +//! - [UDP Tracker](#udp-tracker) //! - [Components](#components) //! - [Implemented BEPs](#implemented-beps) //! - [Contributing](#contributing) +//! - [Documentation](#documentation) //! //! # Features //! @@ -181,7 +185,7 @@ //! - UDP tracker: //! - HTTP tracker: //! -//! ## API usage +//! ## API //! //! In order to use the tracker API you need to enable it in the configuration: //! @@ -231,7 +235,7 @@ //! //! Refer to the [`API`](crate::servers::apis) documentation for more information about the [`API`](crate::servers::apis) endpoints. //! -//! ## HTTP tracker usage +//! ## HTTP tracker //! //! The HTTP tracker implements two type of requests: //! @@ -331,7 +335,7 @@ //! You can also use the Torrust Tracker together with the [Torrust Index](https://github.com/torrust/torrust-index). If that's the case, //! the Index will create the keys by using the tracker [API](crate::servers::apis). //! -//! ## UDP tracker usage +//! ## UDP tracker //! //! The UDP tracker also implements two type of requests: //! @@ -430,6 +434,15 @@ //! # Contributing //! //! If you want to contribute to this documentation you can [open a new pull request](https://github.com/torrust/torrust-tracker/pulls). +//! +//! # Documentation +//! +//! You can find this documentation on [docs.rs](https://docs.rs/torrust-tracker/). +//! +//! If you want to contribute to this documentation you can [open a new pull request](https://github.com/torrust/torrust-tracker/pulls). +//! +//! In addition to the production code documentation you can find a lot of +//! examples on the integration and unit tests. pub mod app; pub mod bootstrap; pub mod servers; diff --git a/src/servers/http/mod.rs b/src/servers/http/mod.rs index 4212f86c4..78c086892 100644 --- a/src/servers/http/mod.rs +++ b/src/servers/http/mod.rs @@ -1,22 +1,317 @@ -//! Tracker HTTP/HTTPS Protocol. +//! HTTP Tracker. //! -//! Original specification in BEP 3 (section "Trackers"): +//! This module contains the HTTP tracker implementation. //! -//! +//! The HTTP tracker is a simple HTTP server that responds to two `GET` requests: //! -//! Other resources: +//! - `Announce`: used to announce the presence of a peer to the tracker. +//! - `Scrape`: used to get information about a torrent. //! -//! - -//! - +//! Refer to the [`bit_torrent`](crate::shared::bit_torrent) module for more +//! information about the `BitTorrent` protocol. //! - +//! ## Table of Contents +//! +//! - [Requests](#requests) +//! - [Announce](#announce) +//! - [Scrape](#scrape) +//! - [Versioning](#versioning) +//! - [Links](#links) +//! +//! ## Requests +//! +//! ### Announce +//! +//! `Announce` requests are used to announce the presence of a peer to the +//! tracker. The tracker responds with a list of peers that are also downloading +//! the same torrent. A "swarm" is a group of peers that are downloading the +//! same torrent. +//! +//! `Announce` responses are encoded in [bencoded](https://en.wikipedia.org/wiki/Bencode) +//! format. +//! +//! There are two types of `Announce` responses: `compact` and `non-compact`. In +//! a compact response, the peers are encoded in a single string. In a +//! non-compact response, the peers are encoded in a list of dictionaries. The +//! compact response is more efficient than the non-compact response and it does +//! not contain the peer's IDs. +//! +//! **Query parameters** +//! +//! > **NOTICE**: you can click on the parameter name to see a full description +//! after extracting and parsing the parameter from the URL query component. +//! +//! Parameter | Type | Description | Required | Default | Example +//! ---|---|---|---|---|--- +//! [`info_hash`](crate::servers::http::v1::requests::announce::Announce::info_hash) | percent encoded of 40-byte array | The `Info Hash` of the torrent. | Yes | No | `%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00` +//! `peer_addr` | string |The IP address of the peer. | No | No | `2.137.87.41` +//! [`downloaded`](crate::servers::http::v1::requests::announce::Announce::downloaded) | positive integer |The number of bytes downloaded by the peer. | No | `0` | `0` +//! [`uploaded`](crate::servers::http::v1::requests::announce::Announce::uploaded) | positive integer | The number of bytes uploaded by the peer. | No | `0` | `0` +//! [`peer_id`](crate::servers::http::v1::requests::announce::Announce::peer_id) | percent encoded of 20-byte array | The ID of the peer. | Yes | No | `-qB00000000000000001` +//! [`port`](crate::servers::http::v1::requests::announce::Announce::port) | positive integer | The port used by the peer. | Yes | No | `17548` +//! [`left`](crate::servers::http::v1::requests::announce::Announce::left) | positive integer | The number of bytes pending to download. | No | `0` | `0` +//! [`event`](crate::servers::http::v1::requests::announce::Announce::event) | positive integer | The event that triggered the `Announce` request: `started`, `completed`, `stopped` | No | `None` | `completed` +//! [`compact`](crate::servers::http::v1::requests::announce::Announce::compact) | `0` or `1` | Whether the tracker should return a compact peer list. | No | `None` | `0` +//! `numwant` | positive integer | **Not implemented**. The maximum number of peers you want in the reply. | No | `50` | `50` +//! +//! Refer to the [`Announce`](crate::servers::http::v1::requests::announce::Announce) +//! request for more information about the parameters. +//! +//! > **NOTICE**: the [BEP 03](https://www.bittorrent.org/beps/bep_0003.html) +//! defines only the `ip` and `event` parameters as optional. However, the +//! tracker assigns default values to the optional parameters if they are not +//! provided. +//! +//! > **NOTICE**: the `peer_addr` parameter is not part of the original +//! specification. But the peer IP was added in the +//! [UDP Tracker protocol](https://www.bittorrent.org/beps/bep_0015.html). It is +//! used to provide the peer's IP address to the tracker, but it is ignored by +//! the tracker. The tracker uses the IP address of the peer that sent the +//! request or the right-most-ip in the `X-Forwarded-For` header if the tracker +//! is behind a reverse proxy. +//! +//! > **NOTICE**: the maximum number of peers that the tracker can return is +//! `74`. Defined with a hardcoded const [`MAX_SCRAPE_TORRENTS`](crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS). +//! Refer to [issue 262](https://github.com/torrust/torrust-tracker/issues/262) +//! for more information about this limitation. +//! +//! > **NOTICE**: the `info_hash` parameter is NOT a `URL` encoded string param. +//! It is percent encode of the raw `info_hash` bytes (40 bytes). URL `GET` params +//! can contain any bytes, not only well-formed UTF-8. The `info_hash` is a +//! 20-byte SHA1. Check the [`percent_encoding`](crate::servers::http::percent_encoding) +//! module to know more about the encoding. +//! +//! > **NOTICE**: the `peer_id` parameter is NOT a `URL` encoded string param. +//! It is percent encode of the raw peer ID bytes (20 bytes). URL `GET` params +//! can contain any bytes, not only well-formed UTF-8. The `info_hash` is a +//! 20-byte SHA1. Check the [`percent_encoding`](crate::servers::http::percent_encoding) +//! module to know more about the encoding. +//! +//! > **NOTICE**: by default, the tracker returns the non-compact peer list when +//! no `compact` parameter is provided or is empty. The +//! [BEP 23](https://www.bittorrent.org/beps/bep_0023.html) suggests to do the +//! opposite. The tracker should return the compact peer list by default and +//! return the non-compact peer list if the `compact` parameter is `0`. +//! +//! **Sample announce URL** +//! +//! A sample `GET` `announce` request: +//! +//! +//! +//! **Sample non-compact response** +//! +//! In [bencoded](https://en.wikipedia.org/wiki/Bencode) format: +//! +//! ```text +//! d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peersld2:ip15:105.105.105.1057:peer id20:-qB000000000000000014:porti28784eed2:ip39:6969:6969:6969:6969:6969:6969:6969:69697:peer id20:-qB000000000000000024:porti28784eeee +//! ``` +//! +//! And represented as a json: +//! +//! ```json +//! { +//! "complete": 333, +//! "incomplete": 444, +//! "interval": 111, +//! "min interval": 222, +//! "peers": [ +//! { +//! "ip": "105.105.105.105", +//! "peer id": "-qB00000000000000001", +//! "port": 28784 +//! }, +//! { +//! "ip": "6969:6969:6969:6969:6969:6969:6969:6969", +//! "peer id": "-qB00000000000000002", +//! "port": 28784 +//! } +//! ] +//! } +//! ``` +//! +//! If you save the response as a file and you open it with a program that can +//! handle binary data you would see: +//! +//! ```text +//! 00000000: 6438 3a63 6f6d 706c 6574 6569 3333 3365 d8:completei333e +//! 00000010: 3130 3a69 6e63 6f6d 706c 6574 6569 3434 10:incompletei44 +//! 00000020: 3465 383a 696e 7465 7276 616c 6931 3131 4e8:intervali111 +//! 00000030: 6531 323a 6d69 6e20 696e 7465 7276 616c e12:min interval +//! 00000040: 6932 3232 6535 3a70 6565 7273 6c64 323a i222e5:peersld2: +//! 00000050: 6970 3135 3a31 3035 2e31 3035 2e31 3035 ip15:105.105.105 +//! 00000060: 2e31 3035 373a 7065 6572 2069 6432 303a .1057:peer id20: +//! 00000070: 2d71 4230 3030 3030 3030 3030 3030 3030 -qB0000000000000 +//! 00000080: 3030 3031 343a 706f 7274 6932 3837 3834 00014:porti28784 +//! 00000090: 6565 6432 3a69 7033 393a 3639 3639 3a36 eed2:ip39:6969:6 +//! 000000a0: 3936 393a 3639 3639 3a36 3936 393a 3639 969:6969:6969:69 +//! 000000b0: 3639 3a36 3936 393a 3639 3639 3a36 3936 69:6969:6969:696 +//! 000000c0: 3937 3a70 6565 7220 6964 3230 3a2d 7142 97:peer id20:-qB +//! 000000d0: 3030 3030 3030 3030 3030 3030 3030 3030 0000000000000000 +//! 000000e0: 3234 3a70 6f72 7469 3238 3738 3465 6565 24:porti28784eee +//! 000000f0: 65 e +//! ``` +//! +//! Refer to the [`NonCompact`](crate::servers::http::v1::responses::announce::NonCompact) +//! response for more information about the response. +//! +//! **Sample compact response** +//! +//! In [bencoded](https://en.wikipedia.org/wiki/Bencode) format: +//! +//! ```text +//! d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peers6:iiiipp6:peers618:iiiiiiiiiiiiiiiippe +//! ``` +//! +//! And represented as a json: +//! +//! ```json +//! { +//! "complete": 333, +//! "incomplete": 444, +//! "interval": 111, +//! "min interval": 222, +//! "peers": "iiiipp", +//! "peers6": "iiiiiiiiiiiiiiiipp" +//! } +//! ``` +//! +//! If you save the response as a file and you open it with a program that can +//! handle binary data you would see: +//! +//! ```text +//! 0000000: 6438 3a63 6f6d 706c 6574 6569 3333 3365 d8:completei333e +//! 0000010: 3130 3a69 6e63 6f6d 706c 6574 6569 3434 10:incompletei44 +//! 0000020: 3465 383a 696e 7465 7276 616c 6931 3131 4e8:intervali111 +//! 0000030: 6531 323a 6d69 6e20 696e 7465 7276 616c e12:min interval +//! 0000040: 6932 3232 6535 3a70 6565 7273 363a 6969 i222e5:peers6:ii +//! 0000050: 6969 7070 363a 7065 6572 7336 3138 3a69 iipp6:peers618:i +//! 0000060: 6969 6969 6969 6969 6969 6969 6969 6970 iiiiiiiiiiiiiiip +//! 0000070: 7065 pe +//! ``` +//! +//! Refer to the [`Compact`](crate::servers::http::v1::responses::announce::Compact) +//! response for more information about the response. +//! +//! **Protocol** +//! +//! Original specification in [BEP 03. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html). +//! +//! If you want to know more about the `announce` request: +//! +//! - [BEP 03. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) +//! - [BEP 23. Tracker Returns Compact Peer Lists](https://www.bittorrent.org/beps/bep_0023.html) +//! - [Vuze announce docs](https://wiki.vuze.com/w/Announce) +//! - [wiki.theory.org - Announce](https://wiki.theory.org/BitTorrent_Tracker_Protocol#Basic_Tracker_Announce_Request) +//! +//! ### Scrape +//! +//! The `scrape` request allows a peer to get [swarm metadata](crate::tracker::torrent::SwarmMetadata) +//! for multiple torrents at the same time. +//! +//! The response contains the [swarm metadata](crate::tracker::torrent::SwarmMetadata) +//! for that torrent: +//! +//! - [complete](crate::tracker::torrent::SwarmMetadata::complete) +//! - [downloaded](crate::tracker::torrent::SwarmMetadata::downloaded) +//! - [incomplete](crate::tracker::torrent::SwarmMetadata::incomplete) +//! +//! **Query parameters** +//! +//! Parameter | Type | Description | Required | Default | Example +//! ---|---|---|---|---|--- +//! [`info_hash`](crate::servers::http::v1::requests::scrape::Scrape::info_hashes) | percent encoded of 40-byte array | The `Info Hash` of the torrent. | Yes | No | `%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00` +//! +//! > **NOTICE**: you can scrape multiple torrents at the same time by passing +//! multiple `info_hash` parameters. +//! +//! Refer to the [`Scrape`](crate::servers::http::v1::requests::scrape::Scrape) +//! request for more information about the parameters. +//! +//! **Sample scrape URL** +//! +//! A sample `scrape` request for only one torrent: +//! +//! +//! +//! In order to scrape multiple torrents at the same time you can pass multiple +//! `info_hash` parameters: `info_hash=%81%00%0...00%00%00&info_hash=%82%00%0...00%00%00` +//! +//! > **NOTICE**: the maximum number of torrent you can scrape at the same time +//! is `74`. Defined with a hardcoded const [`MAX_SCRAPE_TORRENTS`](crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS). +//! +//! **Sample response** +//! +//! The `scrape` response is a [bencoded](https://en.wikipedia.org/wiki/Bencode) +//! byte array like the following: +//! +//! ```text +//! d5:filesd20:iiiiiiiiiiiiiiiiiiiid8:completei1e10:downloadedi2e10:incompletei3eeee +//! ``` +//! +//! And represented as a json: +//! +//! ```json +//! { +//! "files": { +//! "iiiiiiiiiiiiiiiiiiii": { +//! "complete": 1, +//! "downloaded": 2, +//! "incomplete": 3 +//! } +//! } +//! } +//! ``` +//! +//! Where the `files` key contains a dictionary of dictionaries. The first +//! dictionary key is the `info_hash` of the torrent (`iiiiiiiiiiiiiiiiiiii` in +//! the example). The second level dictionary contains the +//! [swarm metadata](crate::tracker::torrent::SwarmMetadata) for that torrent. +//! +//! If you save the response as a file and you open it with a program that +//! can handle binary data you would see: +//! +//! ```text +//! 00000000: 6435 3a66 696c 6573 6432 303a 6969 6969 d5:filesd20:iiii +//! 00000010: 6969 6969 6969 6969 6969 6969 6969 6969 iiiiiiiiiiiiiiii +//! 00000020: 6438 3a63 6f6d 706c 6574 6569 3165 3130 d8:completei1e10 +//! 00000030: 3a64 6f77 6e6c 6f61 6465 6469 3265 3130 :downloadedi2e10 +//! 00000040: 3a69 6e63 6f6d 706c 6574 6569 3365 6565 :incompletei3eee +//! 00000050: 65 e +//! ``` +//! +//! **Protocol** +//! +//! If you want to know more about the `scrape` request: +//! +//! - [BEP 48. Tracker Protocol Extension: Scrape](https://www.bittorrent.org/beps/bep_0048.html) +//! - [Vuze scrape docs](https://wiki.vuze.com/w/Scrape) +//! +//! ## Versioning +//! +//! Right not there is only version `v1`. The HTTP tracker implements BEPS: +//! +//! - [BEP 03. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) +//! - [BEP 07. IPv6 Tracker Extension](https://www.bittorrent.org/beps/bep_0007.html) +//! - [BEP 23. Tracker Returns Compact Peer Lists](https://www.bittorrent.org/beps/bep_0023.html) +//! - [BEP 48. Tracker Protocol Extension: Scrape](https://www.bittorrent.org/beps/bep_0048.html) +//! +//! In the future there could be a `v2` that implements new BEPS with breaking +//! changes. +//! +//! ## Links +//! +//! - [Bencode](https://en.wikipedia.org/wiki/Bencode). +//! - [Bencode to Json Online converter](https://chocobo1.github.io/bencode_online). use serde::{Deserialize, Serialize}; pub mod percent_encoding; pub mod server; pub mod v1; +/// The version of the HTTP tracker. #[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)] pub enum Version { + /// The `v1` version of the HTTP tracker. V1, } diff --git a/src/servers/http/percent_encoding.rs b/src/servers/http/percent_encoding.rs index 019735e0f..b807e74c9 100644 --- a/src/servers/http/percent_encoding.rs +++ b/src/servers/http/percent_encoding.rs @@ -1,17 +1,76 @@ +//! This module contains functions for percent decoding infohashes and peer IDs. +//! +//! Percent encoding is an encoding format used to encode arbitrary data in a +//! format that is safe to use in URLs. It is used by the HTTP tracker protocol +//! to encode infohashes and peer ids in the URLs of requests. +//! +//! `BitTorrent` infohashes and peer ids are percent encoded like any other +//! arbitrary URL parameter. But they are encoded from binary data (byte arrays) +//! which may not be valid UTF-8. That makes hard to use the `percent_encoding` +//! crate to decode them because all of them expect a well-formed UTF-8 string. +//! However, percent encoding is not limited to UTF-8 strings. +//! +//! More information about "Percent Encoding" can be found here: +//! +//! - +//! - +//! - use crate::shared::bit_torrent::info_hash::{ConversionError, InfoHash}; use crate::tracker::peer::{self, IdConversionError}; +/// Percent decodes a percent encoded infohash. Internally an +/// [`InfoHash`](crate::shared::bit_torrent::info_hash::InfoHash) is a 40-byte array. +/// +/// For example, given the infohash `3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0`, +/// it's percent encoded representation is `%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0`. +/// +/// ```rust +/// use std::str::FromStr; +/// use torrust_tracker::servers::http::percent_encoding::percent_decode_info_hash; +/// use torrust_tracker::shared::bit_torrent::info_hash::InfoHash; +/// use torrust_tracker::tracker::peer; +/// +/// let encoded_infohash = "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"; +/// +/// let info_hash = percent_decode_info_hash(encoded_infohash).unwrap(); +/// +/// assert_eq!( +/// info_hash, +/// InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap() +/// ); +/// ``` +/// /// # Errors /// -/// Will return `Err` if the decoded bytes do not represent a valid `InfoHash`. +/// Will return `Err` if the decoded bytes do not represent a valid +/// [`InfoHash`](crate::shared::bit_torrent::info_hash::InfoHash). pub fn percent_decode_info_hash(raw_info_hash: &str) -> Result { let bytes = percent_encoding::percent_decode_str(raw_info_hash).collect::>(); InfoHash::try_from(bytes) } +/// Percent decodes a percent encoded peer id. Internally a peer [`Id`](crate::tracker::peer::Id) +/// is a 20-byte array. +/// +/// For example, given the peer id `*b"-qB00000000000000000"`, +/// it's percent encoded representation is `%2DqB00000000000000000`. +/// +/// ```rust +/// use std::str::FromStr; +/// use torrust_tracker::servers::http::percent_encoding::percent_decode_peer_id; +/// use torrust_tracker::shared::bit_torrent::info_hash::InfoHash; +/// use torrust_tracker::tracker::peer; +/// +/// let encoded_peer_id = "%2DqB00000000000000000"; +/// +/// let peer_id = percent_decode_peer_id(encoded_peer_id).unwrap(); +/// +/// assert_eq!(peer_id, peer::Id(*b"-qB00000000000000000")); +/// ``` +/// /// # Errors /// -/// Will return `Err` if if the decoded bytes do not represent a valid `peer::Id`. +/// Will return `Err` if if the decoded bytes do not represent a valid [`peer::Id`](crate::tracker::peer::Id). pub fn percent_decode_peer_id(raw_peer_id: &str) -> Result { let bytes = percent_encoding::percent_decode_str(raw_peer_id).collect::>(); peer::Id::try_from(bytes) diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs index 510c685d4..3008771ee 100644 --- a/src/servers/http/server.rs +++ b/src/servers/http/server.rs @@ -1,3 +1,4 @@ +//! Module to handle the HTTP server instances. use std::future::Future; use std::net::SocketAddr; use std::sync::Arc; @@ -7,7 +8,10 @@ use futures::future::BoxFuture; use crate::servers::signals::shutdown_signal; use crate::tracker::Tracker; -/// Trait to be implemented by a http server launcher for the tracker. +/// Trait to be implemented by a HTTP server launcher for the tracker. +/// +/// A launcher is responsible for starting the server and returning the +/// `SocketAddr` it is bound to. #[allow(clippy::module_name_repetitions)] pub trait HttpServerLauncher: Sync + Send { fn new() -> Self; @@ -22,26 +26,62 @@ pub trait HttpServerLauncher: Sync + Send { F: Future + Send + 'static; } +/// Error that can occur when starting or stopping the HTTP server. +/// +/// Some errors triggered while starting the server are: +/// +/// - The spawned server cannot send its `SocketAddr` back to the main thread. +/// - The launcher cannot receive the `SocketAddr` from the spawned server. +/// +/// Some errors triggered while stopping the server are: +/// +/// - The channel to send the shutdown signal to the server is closed. +/// - The task to shutdown the server on the spawned server failed to execute to +/// completion. #[derive(Debug)] pub enum Error { - Error(String), + /// Any kind of error starting or stopping the server. + Error(String), // todo: refactor to use thiserror and add more variants for specific errors. } +/// A stopped HTTP server. #[allow(clippy::module_name_repetitions)] pub type StoppedHttpServer = HttpServer>; + +/// A running HTTP server. #[allow(clippy::module_name_repetitions)] pub type RunningHttpServer = HttpServer>; +/// A HTTP running server controller. +/// +/// It's responsible for: +/// +/// - Keeping the initial configuration of the server. +/// - Starting and stopping the server. +/// - Keeping the state of the server: `running` or `stopped`. +/// +/// It's an state machine. Configurations cannot be changed. This struct +/// represents concrete configuration and state. It allows to start and stop the +/// server but always keeping the same configuration. +/// +/// > **NOTICE**: if the configurations changes after running the server it will +/// reset to the initial value after stopping the server. This struct is not +/// intended to persist configurations between runs. #[allow(clippy::module_name_repetitions)] pub struct HttpServer { + /// The configuration of the server that will be used every time the server + /// is started. pub cfg: torrust_tracker_configuration::HttpTracker, + /// The state of the server: `running` or `stopped`. pub state: S, } +/// A stopped HTTP server state. pub struct Stopped { launcher: I, } +/// A running HTTP server state. pub struct Running { pub bind_addr: SocketAddr, task_killer: tokio::sync::oneshot::Sender, @@ -56,6 +96,9 @@ impl HttpServer> { } } + /// It starts the server and returns a `HttpServer` controller in `running` + /// state. + /// /// # Errors /// /// It would return an error if no `SocketAddr` is returned after launching the server. @@ -93,6 +136,9 @@ impl HttpServer> { } impl HttpServer> { + /// It stops the server and returns a `HttpServer` controller in `stopped` + /// state. + /// /// # Errors /// /// It would return an error if the channel for the task killer signal was closed. diff --git a/src/servers/http/v1/extractors/announce_request.rs b/src/servers/http/v1/extractors/announce_request.rs index 501181c8c..5d947ef91 100644 --- a/src/servers/http/v1/extractors/announce_request.rs +++ b/src/servers/http/v1/extractors/announce_request.rs @@ -1,3 +1,32 @@ +//! Axum [`extractor`](axum::extract) for the [`Announce`](crate::servers::http::v1::requests::announce::Announce) +//! request. +//! +//! It parses the query parameters returning an [`Announce`](crate::servers::http::v1::requests::announce::Announce) +//! request. +//! +//! Refer to [`Announce`](crate::servers::http::v1::requests::announce) for more +//! information about the returned structure. +//! +//! It returns a bencoded [`Error`](crate::servers::http::v1::responses::error) +//! response (`500`) if the query parameters are missing or invalid. +//! +//! **Sample announce request** +//! +//! +//! +//! **Sample error response** +//! +//! Missing query params for `announce` request: +//! +//! ```text +//! d14:failure reason149:Cannot parse query params for announce request: missing query params for announce request in src/servers/http/v1/extractors/announce_request.rs:54:23e +//! ``` +//! +//! Invalid query param (`info_hash`): +//! +//! ```text +//! d14:failure reason240:Cannot parse query params for announce request: invalid param value invalid for info_hash in not enough bytes for infohash: got 7 bytes, expected 20 src/shared/bit_torrent/info_hash.rs:240:27, src/servers/http/v1/requests/announce.rs:182:42e +//! ``` use std::panic::Location; use axum::async_trait; @@ -9,6 +38,8 @@ use crate::servers::http::v1::query::Query; use crate::servers::http::v1::requests::announce::{Announce, ParseAnnounceQueryError}; use crate::servers::http::v1::responses; +/// Extractor for the [`Announce`](crate::servers::http::v1::requests::announce::Announce) +/// request. pub struct ExtractRequest(pub Announce); #[async_trait] diff --git a/src/servers/http/v1/extractors/authentication_key.rs b/src/servers/http/v1/extractors/authentication_key.rs index 71e9b9d25..20dc1c90b 100644 --- a/src/servers/http/v1/extractors/authentication_key.rs +++ b/src/servers/http/v1/extractors/authentication_key.rs @@ -1,4 +1,47 @@ -//! Wrapper for Axum `Path` extractor to return custom errors. +//! Axum [`extractor`](axum::extract) to extract the authentication [`Key`](crate::tracker::auth::Key) +//! from the URL path. +//! +//! It's only used when the tracker is running in private mode. +//! +//! Given the following URL route with a path param: `/announce/:key`, +//! it extracts the `key` param from the URL path. +//! +//! It's a wrapper for Axum `Path` extractor in order to return custom +//! authentication errors. +//! +//! It returns a bencoded [`Error`](crate::servers::http::v1::responses::error) +//! response (`500`) if the `key` parameter are missing or invalid. +//! +//! **Sample authentication error responses** +//! +//! When the key param is **missing**: +//! +//! ```text +//! d14:failure reason131:Authentication error: Missing authentication key param for private tracker. Error in src/servers/http/v1/handlers/announce.rs:79:31e +//! ``` +//! +//! When the key param has an **invalid format**: +//! +//! ```text +//! d14:failure reason134:Authentication error: Invalid format for authentication key param. Error in src/servers/http/v1/extractors/authentication_key.rs:73:23e +//! ``` +//! +//! When the key is **not found** in the database: +//! +//! ```text +//! d14:failure reason101:Authentication error: Failed to read key: YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ, src/tracker/mod.rs:848:27e +//! ``` +//! +//! When the key is found in the database but it's **expired**: +//! +//! ```text +//! d14:failure reason64:Authentication error: Key has expired, src/tracker/auth.rs:88:23e +//! ``` +//! +//! > **NOTICE**: the returned HTTP status code is always `200` for authentication errors. +//! Neither [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) +//! nor [The Private Torrents](https://www.bittorrent.org/beps/bep_0027.html) +//! specifications specify any HTTP status code for authentication errors. use std::panic::Location; use axum::async_trait; @@ -12,6 +55,7 @@ use crate::servers::http::v1::handlers::common::auth; use crate::servers::http::v1::responses; use crate::tracker::auth::Key; +/// Extractor for the [`Key`](crate::tracker::auth::Key) struct. pub struct Extract(pub Key); #[derive(Deserialize)] diff --git a/src/servers/http/v1/extractors/client_ip_sources.rs b/src/servers/http/v1/extractors/client_ip_sources.rs index b291eba12..f04300402 100644 --- a/src/servers/http/v1/extractors/client_ip_sources.rs +++ b/src/servers/http/v1/extractors/client_ip_sources.rs @@ -1,5 +1,40 @@ -//! Wrapper for two Axum extractors to get the relevant information -//! to resolve the remote client IP. +//! Axum [`extractor`](axum::extract) to get the relevant information to resolve the remote +//! client IP. +//! +//! It's a wrapper for two third-party Axum extractors. +//! +//! The first one is `RightmostXForwardedFor` from the `axum-client-ip` crate. +//! This extractor is used to get the right-most IP address from the +//! `X-Forwarded-For` header. +//! +//! The second one is `ConnectInfo` from the `axum` crate. This extractor is +//! used to get the IP address of the client from the connection info. +//! +//! The `ClientIpSources` struct is a wrapper for the two extractors. +//! +//! The tracker can be configured to run behind a reverse proxy. In this case, +//! the tracker will use the `X-Forwarded-For` header to get the client IP +//! address. +//! +//! See [`torrust_tracker_configuration::Configuration::on_reverse_proxy`]. +//! +//! The tracker can also be configured to run without a reverse proxy. In this +//! case, the tracker will use the IP address from the connection info. +//! +//! Given the following scenario: +//! +//! ```text +//! client <-> http proxy 1 <-> http proxy 2 <-> server +//! ip: 126.0.0.1 ip: 126.0.0.2 ip: 126.0.0.3 ip: 126.0.0.4 +//! X-Forwarded-For: 126.0.0.1 X-Forwarded-For: 126.0.0.1,126.0.0.2 +//! ``` +//! +//! This extractor returns these values: +//! +//! ```text +//! `right_most_x_forwarded_for` = 126.0.0.2 +//! `connection_info_ip` = 126.0.0.3 +//! ``` use std::net::SocketAddr; use axum::async_trait; @@ -10,6 +45,8 @@ use axum_client_ip::RightmostXForwardedFor; use crate::servers::http::v1::services::peer_ip_resolver::ClientIpSources; +/// Extractor for the [`ClientIpSources`](crate::servers::http::v1::services::peer_ip_resolver::ClientIpSources) +/// struct. pub struct Extract(pub ClientIpSources); #[async_trait] diff --git a/src/servers/http/v1/extractors/mod.rs b/src/servers/http/v1/extractors/mod.rs index 557330257..beab3f2b8 100644 --- a/src/servers/http/v1/extractors/mod.rs +++ b/src/servers/http/v1/extractors/mod.rs @@ -1,3 +1,7 @@ +//! Axum [`extractors`](axum::extract) for the HTTP server. +//! +//! This module contains the extractors used by the HTTP server to parse the +//! incoming requests. pub mod announce_request; pub mod authentication_key; pub mod client_ip_sources; diff --git a/src/servers/http/v1/extractors/scrape_request.rs b/src/servers/http/v1/extractors/scrape_request.rs index ee2502066..63c4dba69 100644 --- a/src/servers/http/v1/extractors/scrape_request.rs +++ b/src/servers/http/v1/extractors/scrape_request.rs @@ -1,3 +1,32 @@ +//! Axum [`extractor`](axum::extract) for the [`Scrape`](crate::servers::http::v1::requests::scrape::Scrape) +//! request. +//! +//! It parses the query parameters returning an [`Scrape`](crate::servers::http::v1::requests::scrape::Scrape) +//! request. +//! +//! Refer to [`Scrape`](crate::servers::http::v1::requests::scrape) for more +//! information about the returned structure. +//! +//! It returns a bencoded [`Error`](crate::servers::http::v1::responses::error) +//! response (`500`) if the query parameters are missing or invalid. +//! +//! **Sample scrape request** +//! +//! +//! +//! **Sample error response** +//! +//! Missing query params for scrape request: +//! +//! ```text +//! d14:failure reason143:Cannot parse query params for scrape request: missing query params for scrape request in src/servers/http/v1/extractors/scrape_request.rs:52:23e +//! ``` +//! +//! Invalid query params for scrape request: +//! +//! ```text +//! d14:failure reason235:Cannot parse query params for scrape request: invalid param value invalid for info_hash in not enough bytes for infohash: got 7 bytes, expected 20 src/shared/bit_torrent/info_hash.rs:240:27, src/servers/http/v1/requests/scrape.rs:66:46e +//! ``` use std::panic::Location; use axum::async_trait; @@ -9,6 +38,8 @@ use crate::servers::http::v1::query::Query; use crate::servers::http::v1::requests::scrape::{ParseScrapeQueryError, Scrape}; use crate::servers::http::v1::responses; +/// Extractor for the [`Scrape`](crate::servers::http::v1::requests::scrape::Scrape) +/// request. pub struct ExtractRequest(pub Scrape); #[async_trait] diff --git a/src/servers/http/v1/handlers/announce.rs b/src/servers/http/v1/handlers/announce.rs index db41388ab..5b26b3758 100644 --- a/src/servers/http/v1/handlers/announce.rs +++ b/src/servers/http/v1/handlers/announce.rs @@ -1,3 +1,10 @@ +//! Axum [`handlers`](axum#handlers) for the `announce` requests. +//! +//! Refer to [HTTP server](crate::servers::http) for more information about the +//! `announce` request. +//! +//! The handlers perform the authentication and authorization of the request, +//! and resolve the client IP address. use std::net::{IpAddr, SocketAddr}; use std::panic::Location; use std::sync::Arc; @@ -20,6 +27,8 @@ use crate::tracker::auth::Key; use crate::tracker::peer::Peer; use crate::tracker::{AnnounceData, Tracker}; +/// It handles the `announce` request when the HTTP tracker does not require +/// authentication (no PATH `key` parameter required). #[allow(clippy::unused_async)] pub async fn handle_without_key( State(tracker): State>, @@ -31,6 +40,8 @@ pub async fn handle_without_key( handle(&tracker, &announce_request, &client_ip_sources, None).await } +/// It handles the `announce` request when the HTTP tracker requires +/// authentication (PATH `key` parameter required). #[allow(clippy::unused_async)] pub async fn handle_with_key( State(tracker): State>, @@ -43,6 +54,10 @@ pub async fn handle_with_key( handle(&tracker, &announce_request, &client_ip_sources, Some(key)).await } +/// It handles the `announce` request. +/// +/// Internal implementation that handles both the `authenticated` and +/// `unauthenticated` modes. async fn handle( tracker: &Arc, announce_request: &Announce, @@ -59,6 +74,7 @@ async fn handle( /* code-review: authentication, authorization and peer IP resolution could be moved from the handler (Axum) layer into the app layer `services::announce::invoke`. That would make the handler even simpler and the code more reusable and decoupled from Axum. + See https://github.com/torrust/torrust-tracker/discussions/240. */ async fn handle_announce( @@ -111,6 +127,8 @@ fn build_response(announce_request: &Announce, announce_data: AnnounceData) -> R } } +/// It builds a `Peer` from the announce request. +/// /// It ignores the peer address in the announce request params. #[must_use] fn peer_from_request(announce_request: &Announce, peer_ip: &IpAddr) -> Peer { diff --git a/src/servers/http/v1/handlers/common/auth.rs b/src/servers/http/v1/handlers/common/auth.rs index 644556e95..f41635d69 100644 --- a/src/servers/http/v1/handlers/common/auth.rs +++ b/src/servers/http/v1/handlers/common/auth.rs @@ -1,3 +1,6 @@ +//! HTTP server authentication error and conversion to +//! [`responses::error::Error`](crate::servers::http::v1::responses::error::Error) +//! response. use std::panic::Location; use thiserror::Error; @@ -5,6 +8,11 @@ use thiserror::Error; use crate::servers::http::v1::responses; use crate::tracker::auth; +/// Authentication error. +/// +/// When the tracker is private, the authentication key is required in the URL +/// path. These are the possible errors that can occur when extracting the key +/// from the URL path. #[derive(Debug, Error)] pub enum Error { #[error("Missing authentication key param for private tracker. Error in {location}")] diff --git a/src/servers/http/v1/handlers/common/mod.rs b/src/servers/http/v1/handlers/common/mod.rs index dc028cabf..30eaf37b7 100644 --- a/src/servers/http/v1/handlers/common/mod.rs +++ b/src/servers/http/v1/handlers/common/mod.rs @@ -1,2 +1,3 @@ +//! Common logic for HTTP handlers. pub mod auth; pub mod peer_ip; diff --git a/src/servers/http/v1/handlers/common/peer_ip.rs b/src/servers/http/v1/handlers/common/peer_ip.rs index 685324b4a..d65efbc79 100644 --- a/src/servers/http/v1/handlers/common/peer_ip.rs +++ b/src/servers/http/v1/handlers/common/peer_ip.rs @@ -1,3 +1,9 @@ +//! Logic to convert peer IP resolution errors into responses. +//! +//! The HTTP tracker may fail to resolve the peer IP address. This module +//! contains the logic to convert those +//! [`PeerIpResolutionError`](crate::servers::http::v1::services::peer_ip_resolver::PeerIpResolutionError) +//! errors into responses. use crate::servers::http::v1::responses; use crate::servers::http::v1::services::peer_ip_resolver::PeerIpResolutionError; diff --git a/src/servers/http/v1/handlers/mod.rs b/src/servers/http/v1/handlers/mod.rs index 69b69127e..d78dee7d5 100644 --- a/src/servers/http/v1/handlers/mod.rs +++ b/src/servers/http/v1/handlers/mod.rs @@ -1,3 +1,7 @@ +//! Axum [`handlers`](axum#handlers) for the HTTP server. +//! +//! Refer to the generic [HTTP server documentation](crate::servers::http) for +//! more information about the HTTP tracker. use super::responses; use crate::tracker::error::Error; diff --git a/src/servers/http/v1/handlers/scrape.rs b/src/servers/http/v1/handlers/scrape.rs index f55194810..b8c1cbea1 100644 --- a/src/servers/http/v1/handlers/scrape.rs +++ b/src/servers/http/v1/handlers/scrape.rs @@ -1,3 +1,10 @@ +//! Axum [`handlers`](axum#handlers) for the `announce` requests. +//! +//! Refer to [HTTP server](crate::servers::http) for more information about the +//! `scrape` request. +//! +//! The handlers perform the authentication and authorization of the request, +//! and resolve the client IP address. use std::sync::Arc; use axum::extract::State; @@ -13,6 +20,8 @@ use crate::servers::http::v1::{responses, services}; use crate::tracker::auth::Key; use crate::tracker::{ScrapeData, Tracker}; +/// It handles the `scrape` request when the HTTP tracker is configured +/// to run in `public` mode. #[allow(clippy::unused_async)] pub async fn handle_without_key( State(tracker): State>, @@ -24,6 +33,10 @@ pub async fn handle_without_key( handle(&tracker, &scrape_request, &client_ip_sources, None).await } +/// It handles the `scrape` request when the HTTP tracker is configured +/// to run in `private` or `private_listed` mode. +/// +/// In this case, the authentication `key` parameter is required. #[allow(clippy::unused_async)] pub async fn handle_with_key( State(tracker): State>, @@ -52,6 +65,7 @@ async fn handle( /* code-review: authentication, authorization and peer IP resolution could be moved from the handler (Axum) layer into the app layer `services::announce::invoke`. That would make the handler even simpler and the code more reusable and decoupled from Axum. + See https://github.com/torrust/torrust-tracker/discussions/240. */ async fn handle_scrape( diff --git a/src/servers/http/v1/launcher.rs b/src/servers/http/v1/launcher.rs index 4cfa4295d..96dd1baac 100644 --- a/src/servers/http/v1/launcher.rs +++ b/src/servers/http/v1/launcher.rs @@ -1,3 +1,4 @@ +//! Logic to start new HTTP server instances. use std::future::Future; use std::net::SocketAddr; use std::str::FromStr; diff --git a/src/servers/http/v1/mod.rs b/src/servers/http/v1/mod.rs index 79d230255..464a7ee14 100644 --- a/src/servers/http/v1/mod.rs +++ b/src/servers/http/v1/mod.rs @@ -1,3 +1,7 @@ +//! HTTP server implementation for the `v1` API. +//! +//! Refer to the generic [HTTP server documentation](crate::servers::http) for +//! more information about the endpoints and their usage. pub mod extractors; pub mod handlers; pub mod launcher; diff --git a/src/servers/http/v1/query.rs b/src/servers/http/v1/query.rs index c40e7949f..6bbdc63e9 100644 --- a/src/servers/http/v1/query.rs +++ b/src/servers/http/v1/query.rs @@ -1,3 +1,8 @@ +//! The `Query` struct used to parse and store the URL query parameters. +//! +/// ```text +/// URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment] +/// ``` use std::panic::Location; use std::str::FromStr; @@ -7,7 +12,7 @@ use thiserror::Error; type ParamName = String; type ParamValue = String; -/// Represent a URL query component: +/// It represents a URL query component. /// /// ```text /// URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment] @@ -22,19 +27,60 @@ pub struct Query { } impl Query { - /// Returns only the first param value even if it has multiple values like this: + /// It return `Some(value)` for a URL query param if the param with the + /// input `name` exists. For example: + /// + /// ```rust + /// use torrust_tracker::servers::http::v1::query::Query; + /// + /// let raw_query = "param1=value1¶m2=value2"; /// - /// ```text - /// param1=value1¶m1=value2 + /// let query = raw_query.parse::().unwrap(); + /// + /// assert_eq!(query.get_param("param1").unwrap(), "value1"); + /// assert_eq!(query.get_param("param2").unwrap(), "value2"); /// ``` /// - /// In that case `get_param("param1")` will return `value1`. + /// It returns only the first param value even if it has multiple values: + /// + /// ```rust + /// use torrust_tracker::servers::http::v1::query::Query; + /// + /// let raw_query = "param1=value1¶m1=value2"; + /// + /// let query = raw_query.parse::().unwrap(); + /// + /// assert_eq!(query.get_param("param1").unwrap(), "value1"); + /// ``` #[must_use] pub fn get_param(&self, name: &str) -> Option { self.params.get(name).map(|pair| pair.value.clone()) } + /// Returns all the param values as a vector. + /// + /// ```rust + /// use torrust_tracker::servers::http::v1::query::Query; + /// + /// let query = "param1=value1¶m1=value2".parse::().unwrap(); + /// + /// assert_eq!( + /// query.get_param_vec("param1"), + /// Some(vec!["value1".to_string(), "value2".to_string()]) + /// ); + /// ``` + /// /// Returns all the param values as a vector even if it has only one value. + /// + /// ```rust + /// use torrust_tracker::servers::http::v1::query::Query; + /// + /// let query = "param1=value1".parse::().unwrap(); + /// + /// assert_eq!( + /// query.get_param_vec("param1"), Some(vec!["value1".to_string()]) + /// ); + /// ``` #[must_use] pub fn get_param_vec(&self, name: &str) -> Option> { self.params.get_vec(name).map(|pairs| { @@ -47,8 +93,12 @@ impl Query { } } +/// This error can be returned when parsing a [`Query`](crate::servers::http::v1::query::Query) +/// from a string. #[derive(Error, Debug)] pub enum ParseQueryError { + /// Invalid URL query param. For example: `"name=value=value"`. It contains + /// an unescaped `=` character. #[error("invalid param {raw_param} in {location}")] InvalidParam { location: &'static Location<'static>, diff --git a/src/servers/http/v1/requests/announce.rs b/src/servers/http/v1/requests/announce.rs index 7ab260d99..3725ee1df 100644 --- a/src/servers/http/v1/requests/announce.rs +++ b/src/servers/http/v1/requests/announce.rs @@ -1,3 +1,6 @@ +//! `Announce` request for the HTTP tracker. +//! +//! Data structures and logic for parsing the `announce` request. use std::fmt; use std::panic::Location; use std::str::FromStr; @@ -11,6 +14,8 @@ use crate::servers::http::v1::responses; use crate::shared::bit_torrent::info_hash::{ConversionError, InfoHash}; use crate::tracker::peer::{self, IdConversionError}; +/// The number of bytes `downloaded`, `uploaded` or `left`. It's used in the +/// `Announce` request for parameters that represent a number of bytes. pub type NumberOfBytes = i64; // Query param names @@ -23,22 +28,71 @@ const LEFT: &str = "left"; const EVENT: &str = "event"; const COMPACT: &str = "compact"; +/// The `Announce` request. Fields use the domain types after parsing the +/// query params of the request. +/// +/// ```rust +/// use torrust_tracker::servers::http::v1::requests::announce::{Announce, Compact, Event}; +/// use torrust_tracker::shared::bit_torrent::info_hash::InfoHash; +/// use torrust_tracker::tracker::peer; +/// +/// let request = Announce { +/// // Mandatory params +/// info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::().unwrap(), +/// peer_id: "-qB00000000000000001".parse::().unwrap(), +/// port: 17548, +/// // Optional params +/// downloaded: Some(1), +/// uploaded: Some(2), +/// left: Some(3), +/// event: Some(Event::Started), +/// compact: Some(Compact::NotAccepted) +/// }; +/// ``` +/// +/// > **NOTICE**: The [BEP 03. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) +/// specifies that only the peer `IP` and `event`are optional. However, the +/// tracker defines default values for some of the mandatory params. +/// +/// > **NOTICE**: The struct does not contain the `IP` of the peer. It's not +/// mandatory and it's not used by the tracker. The `IP` is obtained from the +/// request itself. #[derive(Debug, PartialEq)] pub struct Announce { // Mandatory params + /// The `InfoHash` of the torrent. pub info_hash: InfoHash, + /// The `peer::Id` of the peer. pub peer_id: peer::Id, + /// The port of the peer. pub port: u16, + // Optional params + /// The number of bytes downloaded by the peer. pub downloaded: Option, + + /// The number of bytes uploaded by the peer. pub uploaded: Option, + + /// The number of bytes left to download by the peer. pub left: Option, + + /// The event that the peer is reporting. It can be `Started`, `Stopped` or + /// `Completed`. pub event: Option, + + /// Whether the response should be in compact mode or not. pub compact: Option, } +/// Errors that can occur when parsing the `Announce` request. +/// +/// The `info_hash` and `peer_id` query params are special because they contain +/// binary data. The `info_hash` is a 40-byte SHA1 hash and the `peer_id` is a +/// 20-byte array. #[derive(Error, Debug)] pub enum ParseAnnounceQueryError { + /// A mandatory param is missing. #[error("missing query params for announce request in {location}")] MissingParams { location: &'static Location<'static> }, #[error("missing param {param_name} in {location}")] @@ -46,24 +100,28 @@ pub enum ParseAnnounceQueryError { location: &'static Location<'static>, param_name: String, }, + /// The param cannot be parsed into the domain type. #[error("invalid param value {param_value} for {param_name} in {location}")] InvalidParam { param_name: String, param_value: String, location: &'static Location<'static>, }, + /// The param value is out of range. #[error("param value overflow {param_value} for {param_name} in {location}")] NumberOfBytesOverflow { param_name: String, param_value: String, location: &'static Location<'static>, }, + /// The `info_hash` is invalid. #[error("invalid param value {param_value} for {param_name} in {source}")] InvalidInfoHashParam { param_name: String, param_value: String, source: LocatedError<'static, ConversionError>, }, + /// The `peer_id` is invalid. #[error("invalid param value {param_value} for {param_name} in {source}")] InvalidPeerIdParam { param_name: String, @@ -72,10 +130,21 @@ pub enum ParseAnnounceQueryError { }, } +/// The event that the peer is reporting: `started`, `completed` or `stopped`. +/// +/// If the event is not present or empty that means that the peer is just +/// updating its status. It's one of the announcements done at regular intervals. +/// +/// Refer to [BEP 03. The `BitTorrent Protocol` Specification](https://www.bittorrent.org/beps/bep_0003.html) +/// for more information. #[derive(PartialEq, Debug)] pub enum Event { + /// Event sent when a download first begins. Started, + /// Event sent when the downloader cease downloading. Stopped, + /// Event sent when the download is complete. + /// No `completed` is sent if the file was complete when started Completed, } @@ -106,9 +175,21 @@ impl fmt::Display for Event { } } +/// Whether the `announce` response should be in compact mode or not. +/// +/// Depending on the value of this param, the tracker will return a different +/// response: +/// +/// - [`NonCompact`](crate::servers::http::v1::responses::announce::NonCompact) response. +/// - [`Compact`](crate::servers::http::v1::responses::announce::Compact) response. +/// +/// Refer to [BEP 23. Tracker Returns Compact Peer Lists](https://www.bittorrent.org/beps/bep_0023.html) #[derive(PartialEq, Debug)] pub enum Compact { + /// The client advises the tracker that the client prefers compact format. Accepted = 1, + /// The client advises the tracker that is prefers the original format + /// described in [BEP 03. The BitTorrent Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) NotAccepted = 0, } diff --git a/src/servers/http/v1/requests/mod.rs b/src/servers/http/v1/requests/mod.rs index 776d2dfbf..ee34ca72a 100644 --- a/src/servers/http/v1/requests/mod.rs +++ b/src/servers/http/v1/requests/mod.rs @@ -1,2 +1,6 @@ +//! HTTP requests for the HTTP tracker. +//! +//! Refer to the generic [HTTP server documentation](crate::servers::http) for +//! more information about the HTTP tracker. pub mod announce; pub mod scrape; diff --git a/src/servers/http/v1/requests/scrape.rs b/src/servers/http/v1/requests/scrape.rs index a7ec962e2..227ea74ae 100644 --- a/src/servers/http/v1/requests/scrape.rs +++ b/src/servers/http/v1/requests/scrape.rs @@ -1,3 +1,6 @@ +//! `Scrape` request for the HTTP tracker. +//! +//! Data structures and logic for parsing the `scrape` request. use std::panic::Location; use thiserror::Error; diff --git a/src/servers/http/v1/responses/announce.rs b/src/servers/http/v1/responses/announce.rs index 4902e0d62..8fbe5df35 100644 --- a/src/servers/http/v1/responses/announce.rs +++ b/src/servers/http/v1/responses/announce.rs @@ -1,3 +1,6 @@ +//! `Announce` response for the HTTP tracker [`announce`](crate::servers::http::v1::requests::announce::Announce) request. +//! +//! Data structures and logic to build the `announce` response. use std::io::Write; use std::net::IpAddr; use std::panic::Location; @@ -11,25 +14,103 @@ use thiserror::Error; use crate::servers::http::v1::responses; use crate::tracker::{self, AnnounceData}; -/// Normal (non compact) "announce" response +/// Normal (non compact) `announce` response. /// -/// BEP 03: The ``BitTorrent`` Protocol Specification -/// +/// It's a bencoded dictionary. /// +/// ```rust +/// use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +/// use torrust_tracker::servers::http::v1::responses::announce::{NonCompact, Peer}; +/// +/// let response = NonCompact { +/// interval: 111, +/// interval_min: 222, +/// complete: 333, +/// incomplete: 444, +/// peers: vec![ +/// // IPV4 +/// Peer { +/// peer_id: *b"-qB00000000000000001", +/// ip: IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), // 105.105.105.105 +/// port: 0x7070, // 28784 +/// }, +/// // IPV6 +/// Peer { +/// peer_id: *b"-qB00000000000000002", +/// ip: IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)), +/// port: 0x7070, // 28784 +/// }, +/// ], +/// }; +/// +/// let bytes = response.body(); +/// +/// // The expected bencoded response. +/// let expected_bytes = b"d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peersld2:ip15:105.105.105.1057:peer id20:-qB000000000000000014:porti28784eed2:ip39:6969:6969:6969:6969:6969:6969:6969:69697:peer id20:-qB000000000000000024:porti28784eeee"; +/// +/// assert_eq!( +/// String::from_utf8(bytes).unwrap(), +/// String::from_utf8(expected_bytes.to_vec()).unwrap() +/// ); +/// ``` +/// +/// Refer to [BEP 03: The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) +/// for more information. #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct NonCompact { + /// Interval in seconds that the client should wait between sending regular + /// announce requests to the tracker. + /// + /// It's a **recommended** wait time between announcements. + /// + /// This is the standard amount of time that clients should wait between + /// sending consecutive announcements to the tracker. This value is set by + /// the tracker and is typically provided in the tracker's response to a + /// client's initial request. It serves as a guideline for clients to know + /// how often they should contact the tracker for updates on the peer list, + /// while ensuring that the tracker is not overwhelmed with requests. pub interval: u32, + /// Minimum announce interval. Clients must not reannounce more frequently + /// than this. + /// + /// It establishes the shortest allowed wait time. + /// + /// This is an optional parameter in the protocol that the tracker may + /// provide in its response. It sets a lower limit on the frequency at which + /// clients are allowed to send announcements. Clients should respect this + /// value to prevent sending too many requests in a short period, which + /// could lead to excessive load on the tracker or even getting banned by + /// the tracker for not adhering to the rules. #[serde(rename = "min interval")] pub interval_min: u32, + /// Number of peers with the entire file, i.e. seeders. pub complete: u32, + /// Number of non-seeder peers, aka "leechers". pub incomplete: u32, + /// A list of peers. The value is a list of dictionaries. pub peers: Vec, } +/// Peer information in the [`NonCompact`](crate::servers::http::v1::responses::announce::NonCompact) +/// response. +/// +/// ```rust +/// use std::net::{IpAddr, Ipv4Addr}; +/// use torrust_tracker::servers::http::v1::responses::announce::{NonCompact, Peer}; +/// +/// let peer = Peer { +/// peer_id: *b"-qB00000000000000001", +/// ip: IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), // 105.105.105.105 +/// port: 0x7070, // 28784 +/// }; +/// ``` #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct Peer { + /// The peer's ID. pub peer_id: [u8; 20], + /// The peer's IP address. pub ip: IpAddr, + /// The peer's port number. pub port: u16, } @@ -55,6 +136,8 @@ impl From for Peer { } impl NonCompact { + /// Returns the bencoded body of the non-compact response. + /// /// # Panics /// /// Will return an error if it can't access the bencode as a mutable `BListAccess`. @@ -97,31 +180,120 @@ impl From for NonCompact { } } -/// Compact "announce" response +/// Compact `announce` response. +/// +/// _"To reduce the size of tracker responses and to reduce memory and +/// computational requirements in trackers, trackers may return peers as a +/// packed string rather than as a bencoded list."_ +/// +/// ```rust +/// use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +/// use torrust_tracker::servers::http::v1::responses::announce::{Compact, CompactPeer}; /// -/// BEP 23: Tracker Returns Compact Peer Lists -/// +/// let response = Compact { +/// interval: 111, +/// interval_min: 222, +/// complete: 333, +/// incomplete: 444, +/// peers: vec![ +/// // IPV4 +/// CompactPeer { +/// ip: IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), // 105.105.105.105 +/// port: 0x7070, // 28784 +/// }, +/// // IPV6 +/// CompactPeer { +/// ip: IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)), +/// port: 0x7070, // 28784 +/// }, +/// ], +/// }; /// -/// BEP 07: IPv6 Tracker Extension -/// +/// let bytes = response.body().unwrap(); /// +/// // The expected bencoded response. +/// let expected_bytes = +/// // cspell:disable-next-line +/// b"d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peers6:iiiipp6:peers618:iiiiiiiiiiiiiiiippe"; +/// +/// assert_eq!( +/// String::from_utf8(bytes).unwrap(), +/// String::from_utf8(expected_bytes.to_vec()).unwrap() +/// ); +/// ``` +/// +/// Refer to the official BEPs for more information: +/// +/// - [BEP 23: Tracker Returns Compact Peer Lists](https://www.bittorrent.org/beps/bep_0023.html) +/// - [BEP 07: IPv6 Tracker Extension](https://www.bittorrent.org/beps/bep_0007.html) #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct Compact { + /// Interval in seconds that the client should wait between sending regular + /// announce requests to the tracker. + /// + /// It's a **recommended** wait time between announcements. + /// + /// This is the standard amount of time that clients should wait between + /// sending consecutive announcements to the tracker. This value is set by + /// the tracker and is typically provided in the tracker's response to a + /// client's initial request. It serves as a guideline for clients to know + /// how often they should contact the tracker for updates on the peer list, + /// while ensuring that the tracker is not overwhelmed with requests. pub interval: u32, + /// Minimum announce interval. Clients must not reannounce more frequently + /// than this. + /// + /// It establishes the shortest allowed wait time. + /// + /// This is an optional parameter in the protocol that the tracker may + /// provide in its response. It sets a lower limit on the frequency at which + /// clients are allowed to send announcements. Clients should respect this + /// value to prevent sending too many requests in a short period, which + /// could lead to excessive load on the tracker or even getting banned by + /// the tracker for not adhering to the rules. #[serde(rename = "min interval")] pub interval_min: u32, + /// Number of seeders, aka "completed". pub complete: u32, + /// Number of non-seeder peers, aka "incomplete". pub incomplete: u32, + /// Compact peer list. pub peers: Vec, } +/// Compact peer. It's used in the [`Compact`](crate::servers::http::v1::responses::announce::Compact) +/// response. +/// +/// _"To reduce the size of tracker responses and to reduce memory and +/// computational requirements in trackers, trackers may return peers as a +/// packed string rather than as a bencoded list."_ +/// +/// A part from reducing the size of the response, this format does not contain +/// the peer's ID. +/// +/// ```rust +/// use std::net::{IpAddr, Ipv4Addr}; +/// use torrust_tracker::servers::http::v1::responses::announce::CompactPeer; +/// +/// let compact_peer = CompactPeer { +/// ip: IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), // 105.105.105.105 +/// port: 0x7070 // 28784 +/// }; +/// ``` +/// +/// Refer to [BEP 23: Tracker Returns Compact Peer Lists](https://www.bittorrent.org/beps/bep_0023.html) +/// for more information. #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct CompactPeer { + /// The peer's IP address. pub ip: IpAddr, + /// The peer's port number. pub port: u16, } impl CompactPeer { + /// Returns the compact peer as a byte vector. + /// /// # Errors /// /// Will return `Err` if internally interrupted. @@ -150,6 +322,8 @@ impl From for CompactPeer { } impl Compact { + /// Returns the bencoded compact response as a byte vector. + /// /// # Errors /// /// Will return `Err` if internally interrupted. @@ -196,6 +370,7 @@ impl Compact { } } +/// `Compact` response serialization error. #[derive(Error, Debug)] pub enum CompactSerializationError { #[error("cannot write bytes: {inner_error} in {location}")] diff --git a/src/servers/http/v1/responses/error.rs b/src/servers/http/v1/responses/error.rs index 0bcdbd9fb..606ead3b2 100644 --- a/src/servers/http/v1/responses/error.rs +++ b/src/servers/http/v1/responses/error.rs @@ -1,17 +1,46 @@ +//! `Error` response for the [`HTTP tracker`](crate::servers::http). +//! +//! Data structures and logic to build the error responses. +//! +//! From the [BEP 03. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html): +//! +//! _"Tracker responses are bencoded dictionaries. If a tracker response has a +//! key failure reason, then that maps to a human readable string which explains +//! why the query failed, and no other keys are required."_ +//! +//! > **NOTICE**: error responses are bencoded and always have a `200 OK` status +//! code. The official `BitTorrent` specification does not specify the status +//! code. use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde::{self, Serialize}; +/// `Error` response for the [`HTTP tracker`](crate::servers::http). #[derive(Serialize, Debug, PartialEq)] pub struct Error { + /// Human readable string which explains why the request failed. #[serde(rename = "failure reason")] pub failure_reason: String, } impl Error { + /// Returns the bencoded representation of the `Error` struct. + /// + /// ```rust + /// use torrust_tracker::servers::http::v1::responses::error::Error; + /// + /// let err = Error { + /// failure_reason: "error message".to_owned(), + /// }; + /// + /// // cspell:disable-next-line + /// assert_eq!(err.write(), "d14:failure reason13:error messagee"); + /// ``` + /// /// # Panics /// - /// It would panic if the `Error` struct contained an inappropriate type. + /// It would panic if the `Error` struct contained an inappropriate field + /// type. #[must_use] pub fn write(&self) -> String { serde_bencode::to_string(&self).unwrap() diff --git a/src/servers/http/v1/responses/mod.rs b/src/servers/http/v1/responses/mod.rs index bdc689056..3c6632fed 100644 --- a/src/servers/http/v1/responses/mod.rs +++ b/src/servers/http/v1/responses/mod.rs @@ -1,3 +1,7 @@ +//! HTTP responses for the HTTP tracker. +//! +//! Refer to the generic [HTTP server documentation](crate::servers::http) for +//! more information about the HTTP tracker. pub mod announce; pub mod error; pub mod scrape; diff --git a/src/servers/http/v1/responses/scrape.rs b/src/servers/http/v1/responses/scrape.rs index 36e4f3282..6610f9dc4 100644 --- a/src/servers/http/v1/responses/scrape.rs +++ b/src/servers/http/v1/responses/scrape.rs @@ -1,3 +1,6 @@ +//! `Scrape` response for the HTTP tracker [`scrape`](crate::servers::http::v1::requests::scrape::Scrape) request. +//! +//! Data structures and logic to build the `scrape` response. use std::borrow::Cow; use axum::http::StatusCode; @@ -6,12 +9,46 @@ use bip_bencode::{ben_int, ben_map, BMutAccess}; use crate::tracker::ScrapeData; +/// The `Scrape` response for the HTTP tracker. +/// +/// ```rust +/// use torrust_tracker::servers::http::v1::responses::scrape::Bencoded; +/// use torrust_tracker::shared::bit_torrent::info_hash::InfoHash; +/// use torrust_tracker::tracker::torrent::SwarmMetadata; +/// use torrust_tracker::tracker::ScrapeData; +/// +/// let info_hash = InfoHash([0x69; 20]); +/// let mut scrape_data = ScrapeData::empty(); +/// scrape_data.add_file( +/// &info_hash, +/// SwarmMetadata { +/// complete: 1, +/// downloaded: 2, +/// incomplete: 3, +/// }, +/// ); +/// +/// let response = Bencoded::from(scrape_data); +/// +/// let bytes = response.body(); +/// +/// // cspell:disable-next-line +/// let expected_bytes = b"d5:filesd20:iiiiiiiiiiiiiiiiiiiid8:completei1e10:downloadedi2e10:incompletei3eeee"; +/// +/// assert_eq!( +/// String::from_utf8(bytes).unwrap(), +/// String::from_utf8(expected_bytes.to_vec()).unwrap() +/// ); +/// ``` #[derive(Debug, PartialEq, Default)] pub struct Bencoded { + /// The scrape data to be bencoded. scrape_data: ScrapeData, } impl Bencoded { + /// Returns the bencoded representation of the `Scrape` struct. + /// /// # Panics /// /// Will return an error if it can't access the bencode as a mutable `BDictAccess`. diff --git a/src/servers/http/v1/routes.rs b/src/servers/http/v1/routes.rs index a8e740f69..86bdf480f 100644 --- a/src/servers/http/v1/routes.rs +++ b/src/servers/http/v1/routes.rs @@ -1,3 +1,4 @@ +//! HTTP server routes for version `v1`. use std::sync::Arc; use axum::routing::get; @@ -7,6 +8,10 @@ use axum_client_ip::SecureClientIpSource; use super::handlers::{announce, scrape}; use crate::tracker::Tracker; +/// It adds the routes to the router. +/// +/// > **NOTICE**: it's added a layer to get the client IP from the connection +/// info. The tracker could use the connection info to get the client IP. #[allow(clippy::needless_pass_by_value)] pub fn router(tracker: Arc) -> Router { Router::new() diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index 116dc1e95..4c1b262ba 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -1,3 +1,13 @@ +//! The `announce` service. +//! +//! The service is responsible for handling the `announce` requests. +//! +//! It delegates the `announce` logic to the [`Tracker`](crate::tracker::Tracker::announce) +//! and it returns the [`AnnounceData`](crate::tracker::AnnounceData) returned +//! by the [`Tracker`](crate::tracker::Tracker). +//! +//! It also sends an [`statistics::Event`](crate::tracker::statistics::Event) +//! because events are specific for the HTTP tracker. use std::net::IpAddr; use std::sync::Arc; @@ -5,6 +15,16 @@ use crate::shared::bit_torrent::info_hash::InfoHash; use crate::tracker::peer::Peer; use crate::tracker::{statistics, AnnounceData, Tracker}; +/// The HTTP tracker `announce` service. +/// +/// The service sends an statistics event that increments: +/// +/// - The number of TCP connections handled by the HTTP tracker. +/// - The number of TCP `announce` requests handled by the HTTP tracker. +/// +/// > **NOTICE**: as the HTTP tracker does not requires a connection request +/// like the UDP tracker, the number of TCP connections is incremented for +/// each `announce` request. pub async fn invoke(tracker: Arc, info_hash: InfoHash, peer: &mut Peer) -> AnnounceData { let original_peer_ip = peer.peer_addr.ip(); diff --git a/src/servers/http/v1/services/mod.rs b/src/servers/http/v1/services/mod.rs index 5d1acd67d..2e6285d1a 100644 --- a/src/servers/http/v1/services/mod.rs +++ b/src/servers/http/v1/services/mod.rs @@ -1,3 +1,10 @@ +//! Application services for the HTTP tracker. +//! +//! These modules contain logic that is specific for the HTTP tracker but it +//! does depend on the Axum web server. It could be reused for other web +//! servers. +//! +//! Refer to [`torrust_tracker`](crate) documentation. pub mod announce; pub mod peer_ip_resolver; pub mod scrape; diff --git a/src/servers/http/v1/services/peer_ip_resolver.rs b/src/servers/http/v1/services/peer_ip_resolver.rs index ac5b8c79f..b8987bb4d 100644 --- a/src/servers/http/v1/services/peer_ip_resolver.rs +++ b/src/servers/http/v1/services/peer_ip_resolver.rs @@ -1,13 +1,23 @@ +//! This service resolves the peer IP from the request. +//! +//! The peer IP is used to identify the peer in the tracker. It's the peer IP +//! that is used in the `announce` responses (peer list). And it's also used to +//! send statistics events. +//! //! Given this request chain: //! +//! ```text //! client <-> http proxy 1 <-> http proxy 2 <-> server //! ip: 126.0.0.1 ip: 126.0.0.2 ip: 126.0.0.3 ip: 126.0.0.4 //! X-Forwarded-For: 126.0.0.1 X-Forwarded-For: 126.0.0.1,126.0.0.2 +//! ``` //! -//! This service resolves the peer IP from these values: +//! This service returns two options for the peer IP: //! -//! `right_most_x_forwarded_for` = 126.0.0.2 -//! `connection_info_ip` = 126.0.0.3 +//! ```text +//! right_most_x_forwarded_for = 126.0.0.2 +//! connection_info_ip = 126.0.0.3 +//! ``` //! //! Depending on the tracker configuration. use std::net::IpAddr; @@ -16,22 +26,81 @@ use std::panic::Location; use serde::{Deserialize, Serialize}; use thiserror::Error; +/// This struct contains the sources from which the peer IP can be obtained. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct ClientIpSources { + /// The right most IP from the `X-Forwarded-For` HTTP header. pub right_most_x_forwarded_for: Option, + /// The IP from the connection info. pub connection_info_ip: Option, } +/// The error that can occur when resolving the peer IP. #[derive(Error, Debug)] pub enum PeerIpResolutionError { + /// The peer IP cannot be obtained because the tracker is configured as a + /// reverse proxy but the `X-Forwarded-For` HTTP header is missing or + /// invalid. #[error( "missing or invalid the right most X-Forwarded-For IP (mandatory on reverse proxy tracker configuration) in {location}" )] MissingRightMostXForwardedForIp { location: &'static Location<'static> }, + /// The peer IP cannot be obtained because the tracker is not configured as + /// a reverse proxy but the connection info was not provided to the Axum + /// framework via a route extension. #[error("cannot get the client IP from the connection info in {location}")] MissingClientIp { location: &'static Location<'static> }, } +/// Resolves the peer IP from the request. +/// +/// Given the sources from which the peer IP can be obtained, this function +/// resolves the peer IP according to the tracker configuration. +/// +/// With the tracker running on reverse proxy mode: +/// +/// ```rust +/// use std::net::IpAddr; +/// use std::str::FromStr; +/// +/// use torrust_tracker::servers::http::v1::services::peer_ip_resolver::{invoke, ClientIpSources, PeerIpResolutionError}; +/// +/// let on_reverse_proxy = true; +/// +/// let ip = invoke( +/// on_reverse_proxy, +/// &ClientIpSources { +/// right_most_x_forwarded_for: Some(IpAddr::from_str("203.0.113.195").unwrap()), +/// connection_info_ip: None, +/// }, +/// ) +/// .unwrap(); +/// +/// assert_eq!(ip, IpAddr::from_str("203.0.113.195").unwrap()); +/// ``` +/// +/// With the tracker non running on reverse proxy mode: +/// +/// ```rust +/// use std::net::IpAddr; +/// use std::str::FromStr; +/// +/// use torrust_tracker::servers::http::v1::services::peer_ip_resolver::{invoke, ClientIpSources, PeerIpResolutionError}; +/// +/// let on_reverse_proxy = false; +/// +/// let ip = invoke( +/// on_reverse_proxy, +/// &ClientIpSources { +/// right_most_x_forwarded_for: None, +/// connection_info_ip: Some(IpAddr::from_str("203.0.113.195").unwrap()), +/// }, +/// ) +/// .unwrap(); +/// +/// assert_eq!(ip, IpAddr::from_str("203.0.113.195").unwrap()); +/// ``` +/// /// # Errors /// /// Will return an error if the peer IP cannot be obtained according to the configuration. diff --git a/src/servers/http/v1/services/scrape.rs b/src/servers/http/v1/services/scrape.rs index 82ecc72e0..240680ca3 100644 --- a/src/servers/http/v1/services/scrape.rs +++ b/src/servers/http/v1/services/scrape.rs @@ -1,9 +1,29 @@ +//! The `scrape` service. +//! +//! The service is responsible for handling the `scrape` requests. +//! +//! It delegates the `scrape` logic to the [`Tracker`](crate::tracker::Tracker::scrape) +//! and it returns the [`ScrapeData`](crate::tracker::ScrapeData) returned +//! by the [`Tracker`](crate::tracker::Tracker). +//! +//! It also sends an [`statistics::Event`](crate::tracker::statistics::Event) +//! because events are specific for the HTTP tracker. use std::net::IpAddr; use std::sync::Arc; use crate::shared::bit_torrent::info_hash::InfoHash; use crate::tracker::{statistics, ScrapeData, Tracker}; +/// The HTTP tracker `scrape` service. +/// +/// The service sends an statistics event that increments: +/// +/// - The number of TCP connections handled by the HTTP tracker. +/// - The number of TCP `scrape` requests handled by the HTTP tracker. +/// +/// > **NOTICE**: as the HTTP tracker does not requires a connection request +/// like the UDP tracker, the number of TCP connections is incremented for +/// each `scrape` request. pub async fn invoke(tracker: &Arc, info_hashes: &Vec, original_peer_ip: &IpAddr) -> ScrapeData { let scrape_data = tracker.scrape(info_hashes).await; @@ -12,8 +32,12 @@ pub async fn invoke(tracker: &Arc, info_hashes: &Vec, origina scrape_data } +/// The HTTP tracker fake `scrape` service. It returns zeroed stats. +/// /// When the peer is not authenticated and the tracker is running in `private` mode, /// the tracker returns empty stats for all the torrents. +/// +/// > **NOTICE**: tracker statistics are not updated in this case. pub async fn fake(tracker: &Arc, info_hashes: &Vec, original_peer_ip: &IpAddr) -> ScrapeData { send_scrape_event(original_peer_ip, tracker).await; diff --git a/src/servers/mod.rs b/src/servers/mod.rs index a71b3f029..38b4b70cd 100644 --- a/src/servers/mod.rs +++ b/src/servers/mod.rs @@ -1,3 +1,4 @@ +//! Servers. Services that can be started and stopped. pub mod apis; pub mod http; pub mod signals; diff --git a/src/servers/signals.rs b/src/servers/signals.rs index b5a25ded7..879a82d5e 100644 --- a/src/servers/signals.rs +++ b/src/servers/signals.rs @@ -1,3 +1,4 @@ +/// This module contains functions to handle signals. use log::info; /// Resolves on `ctrl_c` or the `terminate` signal. diff --git a/src/shared/bit_torrent/mod.rs b/src/shared/bit_torrent/mod.rs index 0e5d7e7f2..eba90b4ab 100644 --- a/src/shared/bit_torrent/mod.rs +++ b/src/shared/bit_torrent/mod.rs @@ -1,3 +1,71 @@ //! Common code for the `BitTorrent` protocol. +//! +//! # Glossary +//! +//! - [Announce](#announce) +//! - [Info Hash](#info-hash) +//! - [Leecher](#leechers) +//! - [Peer ID](#peer-id) +//! - [Peer List](#peer-list) +//! - [Peer](#peer) +//! - [Scrape](#scrape) +//! - [Seeders](#seeders) +//! - [Swarm](#swarm) +//! - [Tracker](#tracker) +//! +//! Glossary of `BitTorrent` terms. +//! +//! # Announce +//! +//! A request to the tracker to announce the presence of a peer. +//! +//! ## Info Hash +//! +//! A unique identifier for a torrent. +//! +//! ## Leecher +//! +//! Peers that are only downloading data. +//! +//! ## Peer ID +//! +//! A unique identifier for a peer. +//! +//! ## Peer List +//! +//! A list of peers that are downloading a torrent. +//! +//! ## Peer +//! +//! A client that is downloading or uploading a torrent. +//! +//! ## Scrape +//! +//! A request to the tracker to get information about a torrent. +//! +//! ## Seeder +//! +//! Peers that are only uploading data. +//! +//! ## Swarm +//! +//! A group of peers that are downloading the same torrent. +//! +//! ## Tracker +//! +//! A server that keeps track of peers that are downloading a torrent. +//! +//! # Links +//! +//! Description | Link +//! ---|--- +//! `BitTorrent.org`. A forum for developers to exchange ideas about the direction of the `BitTorrent` protocol | +//! Wikipedia entry for Glossary of `BitTorrent` term | +//! `BitTorrent` Specification Wiki | +//! Vuze Wiki. A `BitTorrent` client implementation | +//! `libtorrent`. Complete C++ bittorrent implementation| +//! UDP Tracker Protocol docs by `libtorrent` | +//! Percent Encoding spec | +//!Bencode & bdecode in your browser | pub mod common; pub mod info_hash; diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs index dd2e94660..03853e1aa 100644 --- a/src/tracker/mod.rs +++ b/src/tracker/mod.rs @@ -499,25 +499,36 @@ pub struct TorrentsMetrics { /// Structure that holds the data returned by the `announce` request. #[derive(Debug, PartialEq, Default)] pub struct AnnounceData { + /// The list of peers that are downloading the same torrent. + /// It excludes the peer that made the request. pub peers: Vec, + /// Swarm statistics pub swarm_stats: SwarmStats, + /// The interval in seconds that the client should wait between sending + /// regular requests to the tracker. + /// Refer to [`announce_interval`](torrust_tracker_configuration::Configuration::announce_interval). pub interval: u32, + /// The minimum announce interval in seconds that the client should wait. + /// Refer to [`min_announce_interval`](torrust_tracker_configuration::Configuration::min_announce_interval). pub interval_min: u32, } /// Structure that holds the data returned by the `scrape` request. #[derive(Debug, PartialEq, Default)] pub struct ScrapeData { + /// A map of infohashes and swarm metadata for each torrent. pub files: HashMap, } impl ScrapeData { + /// Creates a new empty `ScrapeData` with no files (torrents). #[must_use] pub fn empty() -> Self { let files: HashMap = HashMap::new(); Self { files } } + /// Creates a new `ScrapeData` with zeroed metadata for each torrent. #[must_use] pub fn zeroed(info_hashes: &Vec) -> Self { let mut scrape_data = Self::empty(); @@ -529,10 +540,12 @@ impl ScrapeData { scrape_data } + /// Adds a torrent to the `ScrapeData`. pub fn add_file(&mut self, info_hash: &InfoHash, swarm_metadata: SwarmMetadata) { self.files.insert(*info_hash, swarm_metadata); } + /// Adds a torrent to the `ScrapeData` with zeroed metadata. pub fn add_file_with_zeroed_metadata(&mut self, info_hash: &InfoHash) { self.files.insert(*info_hash, SwarmMetadata::zeroed()); } @@ -565,18 +578,22 @@ impl Tracker { }) } + /// Returns `true` is the tracker is in public mode. pub fn is_public(&self) -> bool { self.mode == TrackerMode::Public } + /// Returns `true` is the tracker is in private mode. pub fn is_private(&self) -> bool { self.mode == TrackerMode::Private || self.mode == TrackerMode::PrivateListed } + /// Returns `true` is the tracker is in whitelisted mode. pub fn is_whitelisted(&self) -> bool { self.mode == TrackerMode::Listed || self.mode == TrackerMode::PrivateListed } + /// Returns `true` if the tracker requires authentication. pub fn requires_authentication(&self) -> bool { self.is_private() } diff --git a/src/tracker/torrent.rs b/src/tracker/torrent.rs index 1e78cd909..22deed2b4 100644 --- a/src/tracker/torrent.rs +++ b/src/tracker/torrent.rs @@ -59,7 +59,6 @@ pub struct Entry { pub struct SwarmMetadata { /// The number of peers that have ever completed downloading pub downloaded: u32, - /// The number of active peers that have completed downloading (seeders) pub complete: u32, /// The number of active peers that have not completed downloading (leechers) @@ -80,7 +79,6 @@ impl SwarmMetadata { pub struct SwarmStats { /// The number of peers that have ever completed downloading pub completed: u32, - /// The number of active peers that have completed downloading (seeders) pub seeders: u32, /// The number of active peers that have not completed downloading (leechers)