From e0761c49e1d0d6c8ff2ff5caf1e569e769c67cfd Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 2 Feb 2023 10:20:54 +0000 Subject: [PATCH 01/28] feat: upload code coverage to codecov.io --- .github/workflows/codecov.yml | 40 +++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/codecov.yml diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 000000000..05551bafc --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,40 @@ +name: Upload code coverage + +on: + push: + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly + components: rustfmt, llvm-tools-preview + - name: Build + run: cargo build --release + env: + CARGO_INCREMENTAL: "0" + RUSTFLAGS: "-Cinstrument-coverage" + RUSTDOCFLAGS: "-Cinstrument-coverage" + - name: Test + run: cargo test --all-features --no-fail-fast + env: + CARGO_INCREMENTAL: "0" + RUSTFLAGS: "-Cinstrument-coverage" + RUSTDOCFLAGS: "-Cinstrument-coverage" + - name: Install grcov + run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi + - name: Run grcov + run: grcov . --binary-path target/debug/deps/ -s . -t lcov --branch --ignore-not-existing --ignore '../**' --ignore '/*' -o coverage.lcov + - uses: codecov/codecov-action@v3 + with: + files: ./coverage.lcov + flags: rust + fail_ci_if_error: true # optional (default = false) From a58554a13ddfd674eb3c73ce66e8a2ad489f28fe Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 20 Mar 2023 18:00:42 +0000 Subject: [PATCH 02/28] docs: [#253] crate docs for lib.rs Entrypoint for the crate documentation, the `lib.rs` file. --- cSpell.json | 1 + docs/media/torrust-tracker-components.png | Bin 0 -> 84935 bytes src/lib.rs | 423 ++++++++++++++++++++++ 3 files changed, 424 insertions(+) create mode 100644 docs/media/torrust-tracker-components.png diff --git a/cSpell.json b/cSpell.json index 4a9b11ce9..d8dee5c6b 100644 --- a/cSpell.json +++ b/cSpell.json @@ -7,6 +7,7 @@ "Azureus", "bencode", "bencoded", + "beps", "binascii", "Bitflu", "bools", diff --git a/docs/media/torrust-tracker-components.png b/docs/media/torrust-tracker-components.png new file mode 100644 index 0000000000000000000000000000000000000000..19fe3c0b897a2413a4a6fbf807a0dbdac19b6268 GIT binary patch literal 84935 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 SBJPMMwE4nr**S|%`~M5@Hz=$C literal 0 HcmV?d00001 diff --git a/src/lib.rs b/src/lib.rs index bd775f8cf..3e7225923 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,426 @@ +//! **Torrust Tracker** is a modern and feature-rich (private) [`BitTorrent`](https://www.bittorrent.org/) tracker. +//! +//! [`BitTorrent`](https://en.wikipedia.org/wiki/BitTorrent) is a protocol for distributing files using a peer-to-peer network. +//! +//! Peers in the networks need to know where they can find other peers with the files they are looking for. +//! +//! Tracker are services that allow peers to quickly find other peers. Client peers announce their existence to a tracker, +//! and the tracker responds to the peer with a list of other peers in the swarm. +//! +//! You can learn more about `BitTorrent` and `BitTorrent` Trackers on these sites: +//! +//! - +//! - +//! - +//! +//! Torrust Tracker is a `BitTorrent` tracker with a focus on: +//! +//! - Performance +//! - Robustness +//! - Extensibility +//! - Security +//! - Usability +//! - And with a community-driven development +//! +//! # Table of contents +//! +//! - [Features](#features) +//! - [Services](#services) +//! - [Installation](#installation) +//! - [Configuration](#configuration) +//! - [Usage](#usage) +//! - [Components](#components) +//! - [Implemented BEPs](#implemented-beps) +//! +//! # Features +//! +//! - Multiple UDP server and HTTP(S) server blocks for socket binding possible +//! - Full IPv4 and IPv6 support for both UDP and HTTP(S) +//! - Private and Whitelisted mode +//! - Built-in API +//! - Peer authentication using time-bound keys +//! - Database persistence for authentication keys, whitelist and completed peers counter +//! - DB Support for `SQLite` and `MySQl` +//! +//! # Services +//! +//! From the end-user perspective the Torrust Tracker exposes three different services. +//! +//! - A REST [`API`](crate::servers::apis) +//! - One or more [`UDP`](crate::servers::udp) trackers +//! - One or more [`HTTP`](crate::servers::http) trackers +//! +//! # Installation +//! +//! ## Minimum requirements +//! +//! - Rust Stable `1.68` +//! - You might have problems compiling with a machine with low resources. Or with low resources limits on docker containers. It has been tested with docker containers with 6 CPUs, 7.5 GM of memory and 2GB of swap. +//! +//! ## Prerequisites +//! +//! With the default configuration you will need to create the `storage` directory: +//! +//! ```text +//! storage/ +//! ├── database +//! │   └── data.db +//! └── ssl_certificates +//! ├── localhost.crt +//! └── localhost.key +//! ``` +//! +//! The default configuration expects a directory `./storage/database` to be writable by the tracker process. +//! +//! By default the tracker uses `SQLite` and the database file name `data.db`. +//! +//! You only need the `ssl_certificates` directory in case you are setting up SSL for the HTTP tracker or the tracker API. +//! Visit [`HTTP`](crate::servers::http) or [`API`](crate::servers::apis) if you want to know how you can use HTTPS. +//! +//! ## Install from sources +//! +//! ```text +//! git clone https://github.com/torrust/torrust-tracker.git \ +//! && cd torrust-tracker \ +//! && cargo build --release \ +//! && mkdir -p ./storage/database \ +//! && mkdir -p ./storage/ssl_certificates +//! ``` +//! +//! ## Run with docker +//! +//! You can run the tracker with a pre-built docker image: +//! +//! ```text +//! mkdir -p ./storage/database \ +//! && mkdir -p ./storage/ssl_certificates \ +//! && export TORRUST_TRACKER_USER_UID=1000 \ +//! && docker run -it \ +//! --user="$TORRUST_TRACKER_USER_UID" \ +//! --publish 6969:6969/udp \ +//! --publish 7070:7070/tcp \ +//! --publish 1212:1212/tcp \ +//! --volume "$(pwd)/storage":"/app/storage" \ +//! torrust/tracker:3.0.0-alpha.1 +//! ``` +//! +//! For more information about using docker visit the [tracker docker documentation](https://github.com/torrust/torrust-tracker/tree/develop/docker). +//! +//! # Configuration +//! +//! In order to run the tracker you need to provide the configuration. If you run the tracker without providing the configuration, +//! the tracker will generate the default configuration the first time you run it. It will generate a `config.toml` file with +//! in the root directory. +//! +//! The default configuration is: +//! +//! ```toml +//! log_level = "info" +//! mode = "public" +//! db_driver = "Sqlite3" +//! db_path = "./storage/database/data.db" +//! announce_interval = 120 +//! min_announce_interval = 120 +//! max_peer_timeout = 900 +//! on_reverse_proxy = false +//! external_ip = "0.0.0.0" +//! tracker_usage_statistics = true +//! persistent_torrent_completed_stat = false +//! inactive_peer_cleanup_interval = 600 +//! remove_peerless_torrents = true +//! +//! [[udp_trackers]] +//! enabled = false +//! bind_address = "0.0.0.0:6969" +//! +//! [[http_trackers]] +//! enabled = false +//! bind_address = "0.0.0.0:7070" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! +//! [http_api] +//! enabled = true +//! bind_address = "127.0.0.1:1212" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! ``` +//! +//! The default configuration includes one disabled UDP server, one disabled HTTP server and the enabled API. +//! +//! For more information about each service and options you can visit the documentation for the [torrust-tracker-configuration crate](https://docs.rs/torrust-tracker-configuration/3.0.0-alpha.1/torrust_tracker_configuration/). +//! +//! Alternatively to the `config.toml` file you can use one environment variable `TORRUST_TRACKER_CONFIG` to pass the configuration to the tracker: +//! +//! ```text +//! TORRUST_TRACKER_CONFIG=$(cat config.toml) +//! cargo run +//! ``` +//! +//! In the previous example you are just setting the env var with the contents of the `config.toml` file. +//! +//! The env var contains the same data as the `config.toml`. It's particularly useful in you are [running the tracker with docker](https://github.com/torrust/torrust-tracker/tree/develop/docker). +//! +//! > NOTE: The `TORRUST_TRACKER_CONFIG` env var has priority over the `config.toml` file. +//! +//! # Usage +//! +//! Running the tracker with the default configuration and enabling the UDP and HTTP trackers will expose the services on these URLs: +//! +//! - REST API: +//! - UDP tracker: +//! - HTTP tracker: +//! +//! ## API usage +//! +//! In order to use the tracker API you need to enable it in the configuration: +//! +//! ```toml +//! [http_api] +//! enabled = true +//! bind_address = "127.0.0.1:1212" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! ``` +//! +//! By default it's enabled on port `1212`. You also need to add access tokens in the configuration: +//! +//! ```toml +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! LABEL = "YOUR_TOKEN" +//! ``` +//! +//! All tokens give full access the the API. Once you have defined you token you can make request adding the token as a `GET` parameter. For example: +//! +//! +//! +//! That endpoint will give you the tracker metrics: +//! +//! ```json +//! { +//! "torrents": 0, +//! "seeders": 0, +//! "completed": 0, +//! "leechers": 0, +//! "tcp4_connections_handled": 0, +//! "tcp4_announces_handled": 0, +//! "tcp4_scrapes_handled": 0, +//! "tcp6_connections_handled": 0, +//! "tcp6_announces_handled": 0, +//! "tcp6_scrapes_handled": 0, +//! "udp4_connections_handled": 0, +//! "udp4_announces_handled": 0, +//! "udp4_scrapes_handled": 0, +//! "udp6_connections_handled": 0, +//! "udp6_announces_handled": 0, +//! "udp6_scrapes_handled": 0 +//! } +//! ``` +//! +//! Refer to the [`API`](crate::servers::apis) documentation for more information about the [`API`](crate::servers::apis) endpoints. +//! +//! ## HTTP tracker usage +//! +//! The HTTP tracker implements two type of requests: +//! +//! - Announce: +//! - Scrape: +//! +//! In you are using the tracker in `private` or `private_listed` mode you will need to append the authentication key: +//! +//! - Announce: +//! - Scrape: +//! +//! In order to use the HTTP tracker you need to enable at least one server in the configuration: +//! +//! ```toml +//! [[http_trackers]] +//! enabled = true +//! bind_address = "0.0.0.0:7070" +//! ``` +//! +//! Refer to the [`HTTP`](crate::servers::http) documentation for more information about the [`HTTP`](crate::servers::http) tracker. +//! +//! ### Announce +//! +//! The `announce` request allows a peer to announce itself and obtain a list of peer for an specific torrent. +//! +//! A sample `announce` request: +//! +//! +//! +//! 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) +//! +//! ### Scrape +//! +//! The `scrape` request allows a peer to get swarm metadata for multiple torrents at the same time. +//! +//! A sample `scrape` request for only one torrent: +//! +//! +//! +//! The response contains the swarm metadata for that torrent: +//! +//! - `complete`: the number of active peers that have completed downloading, also known as seeders. Peers from which other peers can get a full copy of the torrent. +//! - `downloaded`: the number of peers that have ever completed downloading. +//! - `incomplete`: the number of active peers that have not completed downloading, also known as leechers. +//! +//! The `scrape` response is a bencoded byte array like the following: +//! +//! ```text +//! d5:filesd20:xxxxxxxxxxxxxxxxxxxxd8:completei11e10:downloadedi13772e10:incompletei19e20:yyyyyyyyyyyyyyyyyyyyd8:completei21e10:downloadedi206e10:incompletei20eee +//! ``` +//! +//! 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 8100 0000 d5:filesd20:.... +//! 00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................ +//! 00000020: 6438 3a63 6f6d 706c 6574 6569 3165 3130 d8:completei1e10 +//! 00000030: 3a64 6f77 6e6c 6f61 6465 6469 3065 3130 :downloadedi0e10 +//! 00000040: 3a69 6e63 6f6d 706c 6574 6569 3065 6565 :incompletei0eee +//! 00000050: 65 e +//! ``` +//! +//! `BitTorrent` uses a data formatting specification called [Bencode](https://en.wikipedia.org/wiki/Bencode). +//! +//! 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) +//! +//! ### Authentication keys +//! +//! If the tracker is running in `private` or `private_listed` mode you will need to provide a valid authentication key. +//! +//! Right now the only way to add new keys is via the REST [`API`](crate::servers::apis). The endpoint `POST /api/vi/key/:duration_in_seconds` +//! will return an expiring key that will be valid for `duration_in_seconds` seconds. +//! +//! Using `curl` you can create a 2-minute valid auth key: +//! +//! ```text +//! $ curl -X POST http://127.0.0.1:1212/api/v1/key/120?token=MyAccessToken +//! ``` +//! +//! Response: +//! +//! ```json +//! { +//! "key": "nvCFlJCq7fz7Qx6KoKTDiMZvns8l5Kw7", +//! "valid_until": 1679334334, +//! "expiry_time": "2023-03-20 17:45:34.712077008 UTC" +//! } +//! ``` +//! +//! 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 API. +//! +//! ## UDP tracker usage +//! +//! The UDP tracker also implements two type of requests: +//! +//! - Announce: +//! - Scrape: +//! +//! In order to use the UDP tracker you need to enable at least one server in the configuration: +//! +//! ```toml +//! [[udp_trackers]] +//! enabled = true +//! bind_address = "0.0.0.0:6969" +//! ``` +//! +//! Refer to the [`UDP`](crate::servers::udp) documentation for more information about the [`UDP`](crate::servers::udp) tracker. +//! +//! If you want to know more about the UDP tracker protocol: +//! +//! - [BEP 15. UDP Tracker Protocol for `BitTorrent`](https://www.bittorrent.org/beps/bep_0015.html) +//! +//! # Components +//! +//! Torrust Tracker has four main components: +//! +//! - The core [`tracker`](crate::tracker) +//! - The tracker REST [`API`](crate::servers::apis) +//! - The [`UDP`](crate::servers::udp) tracker +//! - The [`HTTP`](crate::servers::http) tracker +//! +//! ![Torrust Tracker Components](https://github.com/torrust/torrust-tracker/blob/main/docs/media/torrust-tracker-components.png) +//! +//! ## Core tracker +//! +//! The core tracker is the main containing the tracker generic tracker logic. +//! +//! The core tracker handles: +//! +//! - Authentication with keys +//! - Authorization using a torrent whitelist +//! - Statistics +//! - Persistence +//! +//! See [`tracker`](crate::tracker) for more details on the [`tracker`](crate::tracker) module. +//! +//! ## Tracker API +//! +//! The tracker exposes a REST API. The API has four resource groups: +//! +//! - Authentication keys: to handle the keys for the HTTP tracker +//! - Statistics: to get the tracker metrics like requests counters +//! - Torrents: to get peers for a torrent +//! - Whitelist: to handle the torrent whitelist when the tracker runs on `listed` or `private_listed` mode +//! +//! See [`API`](crate::servers::apis) for more details on the REST API. +//! +//! ## UDP tracker +//! +//! UDP trackers are trackers with focus on performance. By Using UDP instead of HTTP the tracker removed the overhead +//! of opening and closing TCP connections. It also reduces the response size. +//! +//! You can find more information about UDP tracker on: +//! +//! - [Wikipedia: UDP tracker](https://en.wikipedia.org/wiki/UDP_tracker) +//! - [BEP 15: UDP Tracker Protocol for `BitTorrent`](https://www.bittorrent.org/beps/bep_0015.html) +//! +//! See [`UDP`](crate::servers::udp) for more details on the UDP tracker. +//! +//! ## HTTP tracker +//! +//! HTTP tracker was the original tracker specification defined on the [BEP 3]((https://www.bittorrent.org/beps/bep_0003.html)). +//! +//! See [`HTTP`](crate::servers::http) for more details on the HTTP tracker. +//! +//! You can find more information about UDP tracker on: +//! +//! - [Wikipedia: `BitTorrent` tracker](https://en.wikipedia.org/wiki/BitTorrent_tracker) +//! - [BEP 3: The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) +//! +//! # Implemented BEPs +//! +//! BEP stands for `BitTorrent` Enhancement Proposal. BEPs are documents providing information to the `BitTorrent` +//! community or describing a new feature for the `BitTorrent` protocols. +//! +//! You can find all BEPs on +//! +//! Torrust Tracker implements these BEPs: +//! +//! - [BEP 3](https://www.bittorrent.org/beps/bep_0003.html): The `BitTorrent` Protocol +//! - [BEP 7](https://www.bittorrent.org/beps/bep_0007.html): IPv6 Support +//! - [BEP 15](https://www.bittorrent.org/beps/bep_0015.html): UDP Tracker Protocol for `BitTorrent` +//! - [BEP 23](https://www.bittorrent.org/beps/bep_0023.html): Tracker Returns Compact Peer Lists +//! - [BEP 27](https://www.bittorrent.org/beps/bep_0027.html): Private Torrents +//! - [BEP 41](https://www.bittorrent.org/beps/bep_0041.html): UDP Tracker Protocol Extensions +//! - [BEP 48](https://www.bittorrent.org/beps/bep_0048.html): Tracker Protocol Extension: Scrape pub mod app; pub mod bootstrap; pub mod servers; From 78f295bebf9ffbe1c4617f080fc69d0f7665ca6e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 21 Mar 2023 11:53:54 +0000 Subject: [PATCH 03/28] docs: [#253] crate docs for app.rs --- src/app.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/app.rs b/src/app.rs index 5f75449ca..3fc790a23 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,22 @@ +//! Torrust Tracker application. +//! +//! The tracker application has a global configuration for multiple jobs. +//! It's basically a container for other services. +//! It also check constraint and dependencies between services. For example: +//! It's not safe to run a UDP tracker on top of a core public tracker, as UDP trackers +//! do not allow private access to the tracker data. +//! +//! The application is responsible for: +//! +//! - Loading data from the database when it's needed. +//! - Starting some jobs depending on the configuration. +//! +//! The started jobs may be: +//! +//! - Torrent cleaner: it removes inactive peers and (optionally) peerless torrents. +//! - UDP trackers: the user can enable multiple UDP tracker on several ports. +//! - HTTP trackers: the user can enable multiple HTTP tracker on several ports. +//! - Tracker REST API: the tracker API can be enabled/disabled. use std::sync::Arc; use log::warn; From fe28ef5cd18d0c9c36d89f034a1bc3c99ed65355 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 21 Mar 2023 18:07:58 +0000 Subject: [PATCH 04/28] docs: [#256] crate docs for tracker and bootstrap mods --- cSpell.json | 1 + packages/configuration/src/lib.rs | 11 +- src/bootstrap/app.rs | 41 ++ src/bootstrap/jobs/http_tracker.rs | 20 + src/bootstrap/jobs/mod.rs | 8 + src/bootstrap/jobs/torrent_cleanup.rs | 17 + src/bootstrap/jobs/tracker_apis.rs | 34 + src/bootstrap/jobs/udp_tracker.rs | 11 + src/bootstrap/logging.rs | 13 + src/bootstrap/mod.rs | 7 + src/lib.rs | 11 +- src/tracker/auth.rs | 60 ++ src/tracker/databases/driver.rs | 27 +- src/tracker/databases/error.rs | 9 + src/tracker/databases/mod.rs | 167 ++++- src/tracker/databases/mysql.rs | 17 + src/tracker/databases/sqlite.rs | 17 + src/tracker/error.rs | 9 + src/tracker/mod.rs | 887 ++++++++++++++++++----- src/tracker/peer.rs | 55 +- src/tracker/services/mod.rs | 8 + src/tracker/services/statistics/mod.rs | 47 ++ src/tracker/services/statistics/setup.rs | 12 + src/tracker/services/torrent.rs | 24 + src/tracker/statistics.rs | 45 ++ src/tracker/torrent.rs | 78 +- 26 files changed, 1423 insertions(+), 213 deletions(-) diff --git a/cSpell.json b/cSpell.json index d8dee5c6b..88794b2ad 100644 --- a/cSpell.json +++ b/cSpell.json @@ -51,6 +51,7 @@ "proot", "Quickstart", "Rasterbar", + "reannounce", "repr", "reqwest", "rngs", diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index d42c82df9..8b4d9363d 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -59,17 +59,26 @@ impl HttpApi { pub struct Configuration { pub log_level: Option, pub mode: TrackerMode, + + // Database 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 pub announce_interval: u32, + /// Minimum announce interval. Clients must not reannounce more frequently than this pub min_announce_interval: u32, - pub max_peer_timeout: u32, pub on_reverse_proxy: bool, pub external_ip: Option, pub tracker_usage_statistics: bool, pub persistent_torrent_completed_stat: bool, + + // Cleanup job configuration + pub max_peer_timeout: u32, pub inactive_peer_cleanup_interval: u64, pub remove_peerless_torrents: bool, + + // Server jobs configuration pub udp_trackers: Vec, pub http_trackers: Vec, pub http_api: HttpApi, diff --git a/src/bootstrap/app.rs b/src/bootstrap/app.rs index e845feac0..c0e688a0d 100644 --- a/src/bootstrap/app.rs +++ b/src/bootstrap/app.rs @@ -1,3 +1,16 @@ +//! Setup for the main tracker application. +//! +//! The [`setup`](bootstrap::app::setup) only builds the application and its dependencies but it does not start the application. +//! In fact, there is no such thing as the main application process. When the application starts, the only thing it does is +//! starting a bunch of independent jobs. If you are looking for how things are started you should read [`app::start`](crate::app::start) +//! function documentation. +//! +//! Setup steps: +//! +//! 1. Load the global application configuration. +//! 2. Initialize static variables. +//! 3. Initialize logging. +//! 4. Initialize the domain tracker. use std::env; use std::sync::Arc; @@ -9,6 +22,7 @@ use crate::shared::crypto::ephemeral_instance_keys; use crate::tracker::services::tracker_factory; use crate::tracker::Tracker; +/// It loads the configuration from the environment and builds the main domain [`tracker`](crate::tracker::Tracker) struct. #[must_use] pub fn setup() -> (Arc, Arc) { let configuration = Arc::new(initialize_configuration()); @@ -17,6 +31,9 @@ pub fn setup() -> (Arc, Arc) { (configuration, tracker) } +/// It initializes the application with the given configuration. +/// +/// The configuration may be obtained from the environment (via config file or env vars). #[must_use] pub fn initialize_with_configuration(configuration: &Arc) -> Arc { initialize_static(); @@ -24,6 +41,12 @@ pub fn initialize_with_configuration(configuration: &Arc) -> Arc< Arc::new(initialize_tracker(configuration)) } +/// It initializes the application static values. +/// +/// These values are accessible throughout the entire application: +/// +/// - The time when the application started. +/// - An ephemeral instance random seed. This seed is used for encryption and it's changed when the main application process is restarted. pub fn initialize_static() { // Set the time of Torrust app starting lazy_static::initialize(&static_time::TIME_AT_APP_START); @@ -32,6 +55,17 @@ pub fn initialize_static() { lazy_static::initialize(&ephemeral_instance_keys::RANDOM_SEED); } +/// It loads the application configuration from the environment. +/// +/// There are two methods to inject the configuration: +/// +/// 1. By using a config file: `config.toml`. The file must be in the same folder where you are running the tracker. +/// 2. Environment variable: `TORRUST_TRACKER_CONFIG`. The variable contains the same contents as the `config.toml` file. +/// +/// Environment variable has priority over the config file. +/// +/// Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) for the configuration options. +/// /// # Panics /// /// Will panic if it can't load the configuration from either @@ -50,11 +84,18 @@ fn initialize_configuration() -> Configuration { } } +/// It builds the domain tracker +/// +/// The tracker is the domain layer service. It's the entrypoint to make requests to the domain layer. +/// It's used by other higher-level components like the UDP and HTTP trackers or the tracker API. #[must_use] pub fn initialize_tracker(config: &Arc) -> Tracker { tracker_factory(config.clone()) } +/// It initializes the log level, format and channel. +/// +/// See [the logging setup](crate::bootstrap::logging::setup) for more info about logging. pub fn initialize_logging(config: &Arc) { bootstrap::logging::setup(config); } diff --git a/src/bootstrap/jobs/http_tracker.rs b/src/bootstrap/jobs/http_tracker.rs index 43bd0076f..ac0161640 100644 --- a/src/bootstrap/jobs/http_tracker.rs +++ b/src/bootstrap/jobs/http_tracker.rs @@ -1,3 +1,16 @@ +//! HTTP tracker job starter. +//! +//! The function [`http_tracker::start_job`](crate::bootstrap::jobs::http_tracker::start_job) starts a new HTTP tracker server. +//! +//! > **NOTICE**: the application can launch more than one HTTP tracker on different ports. +//! Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) for the configuration options. +//! +//! The [`http_tracker::start_job`](crate::bootstrap::jobs::http_tracker::start_job) function spawns a new asynchronous task, +//! that tasks is the "**launcher**". The "**launcher**" starts the actual server and sends a message back to the main application. +//! The main application waits until receives the message [`ServerJobStarted`](crate::bootstrap::jobs::http_tracker::ServerJobStarted) from the "**launcher**". +//! +//! The "**launcher**" is an intermediary thread that decouples the HTTP servers from the process that handles it. The HTTP could be used independently in the future. +//! In that case it would not need to notify a parent process. use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; @@ -10,9 +23,16 @@ use crate::servers::http::v1::launcher; use crate::servers::http::Version; use crate::tracker; +/// This is the message that the "**launcher**" spawned task sends to the main application process to notify that the HTTP server was successfully started. +/// +/// > **NOTICE**: it does not mean the HTTP server is ready to receive requests. It only means the new server started. It might take some time to the server to be ready to accept request. #[derive(Debug)] pub struct ServerJobStarted(); +/// It starts a new HTTP server with the provided configuration and version. +/// +/// Right now there is only one version but in the future we could support more than one HTTP tracker version at the same time. +/// This feature allows supporting breaking changes on `BitTorrent` BEPs. pub async fn start_job(config: &HttpTracker, tracker: Arc, version: Version) -> JoinHandle<()> { match version { Version::V1 => start_v1(config, tracker.clone()).await, diff --git a/src/bootstrap/jobs/mod.rs b/src/bootstrap/jobs/mod.rs index ba44a56ad..c519a9f4b 100644 --- a/src/bootstrap/jobs/mod.rs +++ b/src/bootstrap/jobs/mod.rs @@ -1,3 +1,11 @@ +//! Application jobs launchers. +//! +//! The main application setup has only two main stages: +//! +//! 1. Setup the domain layer: the core tracker. +//! 2. Launch all the application services as concurrent jobs. +//! +//! This modules contains all the functions needed to start those jobs. pub mod http_tracker; pub mod torrent_cleanup; pub mod tracker_apis; diff --git a/src/bootstrap/jobs/torrent_cleanup.rs b/src/bootstrap/jobs/torrent_cleanup.rs index 64240bffe..d48769139 100644 --- a/src/bootstrap/jobs/torrent_cleanup.rs +++ b/src/bootstrap/jobs/torrent_cleanup.rs @@ -1,3 +1,15 @@ +//! Job that runs a task on intervals to clean up torrents. +//! +//! It removes inactive peers and (optionally) peerless torrents. +//! +//! **Inactive peers** are peers that have not been updated for more than `max_peer_timeout` seconds. +//! `max_peer_timeout` is a customizable core tracker option. +//! +//! If the core tracker configuration option `remove_peerless_torrents` is true, the cleanup job will also +//! remove **peerless torrents** which are torrents with an empty peer list. +//! +//! Refer to [`torrust-tracker-configuration documentation`](https://docs.rs/torrust-tracker-configuration) for more info about those options. + use std::sync::Arc; use chrono::Utc; @@ -7,6 +19,11 @@ use torrust_tracker_configuration::Configuration; use crate::tracker; +/// It starts a jobs for cleaning up the torrent data in the tracker. +/// +/// The cleaning task is executed on an `inactive_peer_cleanup_interval`. +/// +/// Refer to [`torrust-tracker-configuration documentation`](https://docs.rs/torrust-tracker-configuration) for more info about that option. #[must_use] pub fn start_job(config: &Arc, tracker: &Arc) -> JoinHandle<()> { let weak_tracker = std::sync::Arc::downgrade(tracker); diff --git a/src/bootstrap/jobs/tracker_apis.rs b/src/bootstrap/jobs/tracker_apis.rs index cdebc21a8..9afe4ab24 100644 --- a/src/bootstrap/jobs/tracker_apis.rs +++ b/src/bootstrap/jobs/tracker_apis.rs @@ -1,3 +1,25 @@ +//! Tracker API job starter. +//! +//! The [`tracker_apis::start_job`](crate::bootstrap::jobs::tracker_apis::start_job) +//! function starts a the HTTP tracker REST API. +//! +//! > **NOTICE**: that even thought there is only one job the API has different +//! versions. API consumers can choose which version to use. The API version is +//! part of the URL, for example: `http://localhost:1212/api/v1/stats`. +//! +//! The [`tracker_apis::start_job`](crate::bootstrap::jobs::tracker_apis::start_job) +//! function spawns a new asynchronous task, that tasks is the "**launcher**". +//! The "**launcher**" starts the actual server and sends a message back +//! to the main application. The main application waits until receives +//! the message [`ApiServerJobStarted`](crate::bootstrap::jobs::tracker_apis::ApiServerJobStarted) +//! from the "**launcher**". +//! +//! The "**launcher**" is an intermediary thread that decouples the API server +//! from the process that handles it. The API could be used independently +//! in the future. In that case it would not need to notify a parent process. +//! +//! Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) +//! for the API configuration options. use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; @@ -9,9 +31,21 @@ use torrust_tracker_configuration::HttpApi; use crate::servers::apis::server; use crate::tracker; +/// This is the message that the "launcher" spawned task sends to the main +/// application process to notify the API server was successfully started. +/// +/// > **NOTICE**: it does not mean the API server is ready to receive requests. +/// It only means the new server started. It might take some time to the server +/// to be ready to accept request. #[derive(Debug)] pub struct ApiServerJobStarted(); +/// This function starts a new API server with the provided configuration. +/// +/// The functions starts a new concurrent task that will run the API server. +/// This task will send a message to the main application process to notify +/// that the API server was successfully started. +/// /// # Panics /// /// It would panic if unable to send the `ApiServerJobStarted` notice. diff --git a/src/bootstrap/jobs/udp_tracker.rs b/src/bootstrap/jobs/udp_tracker.rs index 138222daf..76c465a8d 100644 --- a/src/bootstrap/jobs/udp_tracker.rs +++ b/src/bootstrap/jobs/udp_tracker.rs @@ -1,3 +1,11 @@ +//! UDP tracker job starter. +//! +//! The [`udp_tracker::start_job`](crate::bootstrap::jobs::udp_tracker::start_job) +//! function starts a new UDP tracker server. +//! +//! > **NOTICE**: that the application can launch more than one UDP tracker +//! on different ports. Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) +//! for the configuration options. use std::sync::Arc; use log::{error, info, warn}; @@ -7,6 +15,9 @@ use torrust_tracker_configuration::UdpTracker; use crate::servers::udp::server::Udp; use crate::tracker; +/// It starts a new UDP server with the provided configuration. +/// +/// It spawns a new asynchronous task for the new UDP server. #[must_use] pub fn start_job(config: &UdpTracker, tracker: Arc) -> JoinHandle<()> { let bind_addr = config.bind_address.clone(); diff --git a/src/bootstrap/logging.rs b/src/bootstrap/logging.rs index 83e2c9360..97e26919d 100644 --- a/src/bootstrap/logging.rs +++ b/src/bootstrap/logging.rs @@ -1,3 +1,15 @@ +//! Setup for the application logging. +//! +//! It redirects the log info to the standard output with the log level defined in the configuration. +//! +//! - `Off` +//! - `Error` +//! - `Warn` +//! - `Info` +//! - `Debug` +//! - `Trace` +//! +//! Refer to the [configuration crate documentation](https://docs.rs/torrust-tracker-configuration) to know how to change log settings. use std::str::FromStr; use std::sync::Once; @@ -6,6 +18,7 @@ use torrust_tracker_configuration::Configuration; static INIT: Once = Once::new(); +/// It redirects the log info to the standard output with the log level defined in the configuration pub fn setup(cfg: &Configuration) { let level = config_level_or_default(&cfg.log_level); diff --git a/src/bootstrap/mod.rs b/src/bootstrap/mod.rs index e3b6467ee..e39cf3adc 100644 --- a/src/bootstrap/mod.rs +++ b/src/bootstrap/mod.rs @@ -1,3 +1,10 @@ +//! Tracker application bootstrapping. +//! +//! This module includes all the functions to build the application, its dependencies, and run the jobs. +//! +//! Jobs are tasks executed concurrently. Some of them are concurrent because of the asynchronous nature of the task, +//! like cleaning torrents, and other jobs because they can be enabled/disabled depending on the configuration. +//! For example, you can have more than one UDP and HTTP tracker, each server is executed like a independent job. pub mod app; pub mod jobs; pub mod logging; diff --git a/src/lib.rs b/src/lib.rs index 3e7225923..a460a28b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,10 +27,15 @@ //! - [Features](#features) //! - [Services](#services) //! - [Installation](#installation) +//! - [Minimum requirements](#minimum-requirements) +//! - [Prerequisites](#prerequisites) +//! - [Install from sources](#install-from-sources) +//! - [Run with docker](#run-with-docker) //! - [Configuration](#configuration) //! - [Usage](#usage) //! - [Components](#components) //! - [Implemented BEPs](#implemented-beps) +//! - [Contributing](#contributing) //! //! # Features //! @@ -356,7 +361,7 @@ //! - The [`UDP`](crate::servers::udp) tracker //! - The [`HTTP`](crate::servers::http) tracker //! -//! ![Torrust Tracker Components](https://github.com/torrust/torrust-tracker/blob/main/docs/media/torrust-tracker-components.png) +//! ![Torrust Tracker Components](https://raw.githubusercontent.com/torrust/torrust-tracker/main/docs/media/torrust-tracker-components.png) //! //! ## Core tracker //! @@ -421,6 +426,10 @@ //! - [BEP 27](https://www.bittorrent.org/beps/bep_0027.html): Private Torrents //! - [BEP 41](https://www.bittorrent.org/beps/bep_0041.html): UDP Tracker Protocol Extensions //! - [BEP 48](https://www.bittorrent.org/beps/bep_0048.html): Tracker Protocol Extension: Scrape +//! +//! # Contributing +//! +//! If you want to contribute to this documentation you can [open a new pull request](https://github.com/torrust/torrust-tracker/pulls). pub mod app; pub mod bootstrap; pub mod servers; diff --git a/src/tracker/auth.rs b/src/tracker/auth.rs index 31e1f50e4..9068a94f0 100644 --- a/src/tracker/auth.rs +++ b/src/tracker/auth.rs @@ -1,3 +1,38 @@ +//! Tracker authentication services and structs. +//! +//! This module contains functions to handle tracker keys. +//! Tracker keys are tokens used to authenticate the tracker clients when the tracker runs +//! in `private` or `private_listed` modes. +//! +//! There are services to [`generate`](crate::tracker::auth::generate) and [`verify`](crate::tracker::auth::verify) authentication keys. +//! +//! Authentication keys are used only by [`HTTP`](crate::servers::http) trackers. All keys have an expiration time, that means +//! they are only valid during a period of time. After that time the expiring key will no longer be valid. +//! +//! Keys are stored in this struct: +//! +//! ```rust,no_run +//! pub struct ExpiringKey { +//! /// Random 32-char string. For example: `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ` +//! pub key: Key, +//! /// Timestamp, the key will be no longer valid after this timestamp +//! pub valid_until: DurationSinceUnixEpoch, +//! } +//! ``` +//! +//! You can generate a new key valid for `9999` seconds and `0` nanoseconds from the current time with the following: +//! +//! ```rust,no_run +//! let expiring_key = auth::generate(Duration::new(9999, 0)); +//! +//! assert!(auth::verify(&expiring_key).is_ok()); +//! ``` +//! +//! And you can later verify it with: +//! +//! ```rust,no_run +//! assert!(auth::verify(&expiring_key).is_ok()); +//! ``` use std::panic::Location; use std::str::FromStr; use std::sync::Arc; @@ -15,6 +50,8 @@ use crate::shared::bit_torrent::common::AUTH_KEY_LENGTH; use crate::shared::clock::{convert_from_timestamp_to_datetime_utc, Current, DurationSinceUnixEpoch, Time, TimeNow}; #[must_use] +/// It generates a new random 32-char authentication [`ExpiringKey`](crate::tracker::auth::ExpiringKey) +/// /// # Panics /// /// It would panic if the `lifetime: Duration` + Duration is more than `Duration::MAX`. @@ -33,6 +70,8 @@ pub fn generate(lifetime: Duration) -> ExpiringKey { } } +/// It verifies an [`ExpiringKey`](crate::tracker::auth::ExpiringKey). It checks if the expiration date has passed. +/// /// # Errors /// /// Will return `Error::KeyExpired` if `auth_key.valid_until` is past the `current_time`. @@ -50,9 +89,13 @@ pub fn verify(auth_key: &ExpiringKey) -> Result<(), Error> { } } +/// An authentication key which has an expiration time. +/// After that time is will automatically become invalid. #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] pub struct ExpiringKey { + /// Random 32-char string. For example: `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ` pub key: Key, + /// Timestamp, the key will be no longer valid after this timestamp pub valid_until: DurationSinceUnixEpoch, } @@ -82,9 +125,24 @@ impl ExpiringKey { } } +/// A randomly generated token used for authentication. +/// +/// It contains lower and uppercase letters and numbers. +/// It's a 32-char string. #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Display, Hash)] pub struct Key(String); +/// Error returned when a key cannot be parsed from a string. +/// +/// ```rust,no_run +/// let key_string = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ"; +/// let key = Key::from_str(key_string); +/// +/// assert!(key.is_ok()); +/// assert_eq!(key.unwrap().to_string(), key_string); +/// ``` +/// +/// If the string does not contains a valid key, the parser function will return this error. #[derive(Debug, PartialEq, Eq)] pub struct ParseKeyError; @@ -100,6 +158,8 @@ impl FromStr for Key { } } +/// Verification error. Error returned when an [`ExpiringKey`](crate::tracker::auth::ExpiringKey) cannot be verified with the [`verify(...)`](crate::tracker::auth::verify) function. +/// #[derive(Debug, Error)] #[allow(dead_code)] pub enum Error { diff --git a/src/tracker/databases/driver.rs b/src/tracker/databases/driver.rs index 4ce6ea515..ef9a4eb07 100644 --- a/src/tracker/databases/driver.rs +++ b/src/tracker/databases/driver.rs @@ -1,3 +1,7 @@ +//! Database driver factory. +//! +//! See [`databases::driver::build`](crate::tracker::databases::driver::build) +//! function for more information. use torrust_tracker_primitives::DatabaseDriver; use super::error::Error; @@ -5,7 +9,28 @@ use super::mysql::Mysql; use super::sqlite::Sqlite; use super::{Builder, Database}; -/// . +/// It builds a new database driver. +/// +/// Example for `SQLite3`: +/// +/// ```rust,no_run +/// let db_driver = "Sqlite3".to_string(); +/// let db_path = "./storage/database/data.db".to_string(); +/// let database = databases::driver::build(&db_driver, &db_path)?; +/// ``` +/// +/// Example for `MySQL`: +/// +/// ```rust,no_run +/// let db_driver = "MySQL".to_string(); +/// let db_path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker".to_string(); +/// let database = databases::driver::build(&db_driver, &db_path)?; +/// ``` +/// +/// Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) +/// for more information about the database configuration. +/// +/// > **WARNING**: The driver instantiation runs database migrations. /// /// # Errors /// diff --git a/src/tracker/databases/error.rs b/src/tracker/databases/error.rs index 68b732190..d89ec05de 100644 --- a/src/tracker/databases/error.rs +++ b/src/tracker/databases/error.rs @@ -1,3 +1,6 @@ +//! Database errors. +//! +//! This module contains the [Database errors](crate::tracker::databases::error::Error). use std::panic::Location; use std::sync::Arc; @@ -7,24 +10,28 @@ use torrust_tracker_primitives::DatabaseDriver; #[derive(thiserror::Error, Debug, Clone)] pub enum Error { + /// The query unexpectedly returned nothing. #[error("The {driver} query unexpectedly returned nothing: {source}")] QueryReturnedNoRows { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, driver: DatabaseDriver, }, + /// The query was malformed. #[error("The {driver} query was malformed: {source}")] InvalidQuery { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, driver: DatabaseDriver, }, + /// Unable to insert a record into the database #[error("Unable to insert record into {driver} database, {location}")] InsertFailed { location: &'static Location<'static>, driver: DatabaseDriver, }, + /// Unable to delete a record into the database #[error("Failed to remove record from {driver} database, error-code: {error_code}, {location}")] DeleteFailed { location: &'static Location<'static>, @@ -32,12 +39,14 @@ pub enum Error { driver: DatabaseDriver, }, + /// Unable to connect to the database #[error("Failed to connect to {driver} database: {source}")] ConnectionError { source: LocatedError<'static, UrlError>, driver: DatabaseDriver, }, + /// Unable to create a connection pool #[error("Failed to create r2d2 {driver} connection pool: {source}")] ConnectionPool { source: LocatedError<'static, r2d2::Error>, diff --git a/src/tracker/databases/mod.rs b/src/tracker/databases/mod.rs index f68288bbe..3b02415df 100644 --- a/src/tracker/databases/mod.rs +++ b/src/tracker/databases/mod.rs @@ -1,3 +1,48 @@ +//! The persistence module. +//! +//! Persistence is currently implemented with one [`Database`](crate::tracker::databases::Database) trait. +//! +//! There are two implementations of the trait (two drivers): +//! +//! - [`Mysql`](crate::tracker::databases::mysql::Mysql) +//! - [`Sqlite`](crate::tracker::databases::sqlite::Sqlite) +//! +//! > **NOTICE**: There are no database migrations. If there are any changes, +//! we will implemented them or provide a script to migrate to the new schema. +//! +//! The persistent objects are: +//! +//! - [Torrent metrics](#torrent-metrics) +//! - [Torrent whitelist](torrent-whitelist) +//! - [Authentication keys](authentication-keys) +//! +//! # Torrent metrics +//! +//! Field | Sample data | Description +//! ---|---|--- +//! `id` | 1 | Autoincrement id +//! `info_hash` | `c1277613db1d28709b034a017ab2cae4be07ae10` | `BitTorrent` infohash V1 +//! `completed` | 20 | The number of peers that have ever completed downloading the torrent associated to this entry. See [`Entry`](crate::tracker::torrent::Entry) for more information. +//! +//! > **NOTICE**: The peer list for a torrent is not persisted. Since peer have to re-announce themselves on intervals, the data is be +//! regenerated again after some minutes. +//! +//! # Torrent whitelist +//! +//! Field | Sample data | Description +//! ---|---|--- +//! `id` | 1 | Autoincrement id +//! `info_hash` | `c1277613db1d28709b034a017ab2cae4be07ae10` | `BitTorrent` infohash V1 +//! +//! # Authentication keys +//! +//! Field | Sample data | Description +//! ---|---|--- +//! `id` | 1 | Autoincrement id +//! `key` | `IrweYtVuQPGbG9Jzx1DihcPmJGGpVy82` | Token +//! `valid_until` | 1672419840 | Timestamp for the expiring date +//! +//! > **NOTICE**: All keys must have an expiration date. pub mod driver; pub mod error; pub mod mysql; @@ -32,9 +77,10 @@ where } } +/// The persistence trait. It contains all the methods to interact with the database. #[async_trait] pub trait Database: Sync + Send { - /// . + /// It instantiates a new database driver. /// /// # Errors /// @@ -43,39 +89,142 @@ pub trait Database: Sync + Send { where Self: std::marker::Sized; - /// . + // Schema + + /// It generates the database tables. SQL queries are hardcoded in the trait + /// implementation. + /// + /// # Context: Schema /// /// # Errors /// /// Will return `Error` if unable to create own tables. fn create_database_tables(&self) -> Result<(), Error>; + /// It drops the database tables. + /// + /// # Context: Schema + /// /// # Errors /// /// Will return `Err` if unable to drop tables. fn drop_database_tables(&self) -> Result<(), Error>; + // Torrent Metrics + + /// It loads the torrent metrics data from the database. + /// + /// It returns an array of tuples with the torrent + /// [`InfoHash`](crate::shared::bit_torrent::info_hash::InfoHash) and the + /// [`completed`](crate::tracker::torrent::Entry::completed) counter + /// which is the number of times the torrent has been downloaded. + /// See [`Entry::completed`](crate::tracker::torrent::Entry::completed). + /// + /// # Context: Torrent Metrics + /// + /// # Errors + /// + /// Will return `Err` if unable to load. async fn load_persistent_torrents(&self) -> Result, Error>; - async fn load_keys(&self) -> Result, Error>; + /// It saves the torrent metrics data into the database. + /// + /// # Context: Torrent Metrics + /// + /// # Errors + /// + /// Will return `Err` if unable to save. + async fn save_persistent_torrent(&self, info_hash: &InfoHash, completed: u32) -> Result<(), Error>; - async fn load_whitelist(&self) -> Result, Error>; + // Whitelist - async fn save_persistent_torrent(&self, info_hash: &InfoHash, completed: u32) -> Result<(), Error>; + /// It loads the whitelisted torrents from the database. + /// + /// # Context: Whitelist + /// + /// # Errors + /// + /// Will return `Err` if unable to load. + async fn load_whitelist(&self) -> Result, Error>; + /// It checks if the torrent is whitelisted. + /// + /// It returns `Some(InfoHash)` if the torrent is whitelisted, `None` otherwise. + /// + /// # Context: Whitelist + /// + /// # Errors + /// + /// Will return `Err` if unable to load. async fn get_info_hash_from_whitelist(&self, info_hash: &InfoHash) -> Result, Error>; + /// It adds the torrent to the whitelist. + /// + /// # Context: Whitelist + /// + /// # Errors + /// + /// Will return `Err` if unable to save. async fn add_info_hash_to_whitelist(&self, info_hash: InfoHash) -> Result; + /// It checks if the torrent is whitelisted. + /// + /// # Context: Whitelist + /// + /// # Errors + /// + /// Will return `Err` if unable to load. + async fn is_info_hash_whitelisted(&self, info_hash: &InfoHash) -> Result { + Ok(self.get_info_hash_from_whitelist(info_hash).await?.is_some()) + } + + /// It removes the torrent from the whitelist. + /// + /// # Context: Whitelist + /// + /// # Errors + /// + /// Will return `Err` if unable to save. async fn remove_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result; + // Authentication keys + + /// It loads the expiring authentication keys from the database. + /// + /// # Context: Authentication Keys + /// + /// # Errors + /// + /// Will return `Err` if unable to load. + async fn load_keys(&self) -> Result, Error>; + + /// It gets an expiring authentication key from the database. + /// + /// It returns `Some(ExpiringKey)` if a [`ExpiringKey`](crate::tracker::auth::ExpiringKey) + /// with the input [`Key`](crate::tracker::auth::Key) exists, `None` otherwise. + /// + /// # Context: Authentication Keys + /// + /// # Errors + /// + /// Will return `Err` if unable to load. async fn get_key_from_keys(&self, key: &Key) -> Result, Error>; + /// It adds an expiring authentication key to the database. + /// + /// # Context: Authentication Keys + /// + /// # Errors + /// + /// Will return `Err` if unable to save. async fn add_key_to_keys(&self, auth_key: &auth::ExpiringKey) -> Result; + /// It removes an expiring authentication key from the database. + /// + /// # Context: Authentication Keys + /// + /// # Errors + /// + /// Will return `Err` if unable to load. async fn remove_key_from_keys(&self, key: &Key) -> Result; - - async fn is_info_hash_whitelisted(&self, info_hash: &InfoHash) -> Result { - Ok(self.get_info_hash_from_whitelist(info_hash).await?.is_some()) - } } diff --git a/src/tracker/databases/mysql.rs b/src/tracker/databases/mysql.rs index 7e4aab99e..4419666ab 100644 --- a/src/tracker/databases/mysql.rs +++ b/src/tracker/databases/mysql.rs @@ -1,3 +1,4 @@ +//! The `MySQL` database driver. use std::str::FromStr; use std::time::Duration; @@ -22,6 +23,10 @@ pub struct Mysql { #[async_trait] impl Database for Mysql { + /// It instantiates a new `MySQL` database driver. + /// + /// Refer to [`databases::Database::new`](crate::tracker::databases::Database::new). + /// /// # Errors /// /// Will return `r2d2::Error` if `db_path` is not able to create `MySQL` database. @@ -34,6 +39,7 @@ impl Database for Mysql { Ok(Self { pool }) } + /// Refer to [`databases::Database::create_database_tables`](crate::tracker::databases::Database::create_database_tables). fn create_database_tables(&self) -> Result<(), Error> { let create_whitelist_table = " CREATE TABLE IF NOT EXISTS whitelist ( @@ -73,6 +79,7 @@ impl Database for Mysql { Ok(()) } + /// Refer to [`databases::Database::drop_database_tables`](crate::tracker::databases::Database::drop_database_tables). fn drop_database_tables(&self) -> Result<(), Error> { let drop_whitelist_table = " DROP TABLE `whitelist`;" @@ -97,6 +104,7 @@ impl Database for Mysql { Ok(()) } + /// Refer to [`databases::Database::load_persistent_torrents`](crate::tracker::databases::Database::load_persistent_torrents). async fn load_persistent_torrents(&self) -> Result, Error> { let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -111,6 +119,7 @@ impl Database for Mysql { Ok(torrents) } + /// Refer to [`databases::Database::load_keys`](crate::tracker::databases::Database::load_keys). async fn load_keys(&self) -> Result, Error> { let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -125,6 +134,7 @@ impl Database for Mysql { Ok(keys) } + /// Refer to [`databases::Database::load_whitelist`](crate::tracker::databases::Database::load_whitelist). async fn load_whitelist(&self) -> Result, Error> { let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -135,6 +145,7 @@ impl Database for Mysql { Ok(info_hashes) } + /// Refer to [`databases::Database::save_persistent_torrent`](crate::tracker::databases::Database::save_persistent_torrent). async fn save_persistent_torrent(&self, info_hash: &InfoHash, completed: u32) -> Result<(), Error> { const COMMAND : &str = "INSERT INTO torrents (info_hash, completed) VALUES (:info_hash_str, :completed) ON DUPLICATE KEY UPDATE completed = VALUES(completed)"; @@ -147,6 +158,7 @@ impl Database for Mysql { Ok(conn.exec_drop(COMMAND, params! { info_hash_str, completed })?) } + /// Refer to [`databases::Database::get_info_hash_from_whitelist`](crate::tracker::databases::Database::get_info_hash_from_whitelist). async fn get_info_hash_from_whitelist(&self, info_hash: &InfoHash) -> Result, Error> { let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -160,6 +172,7 @@ impl Database for Mysql { Ok(info_hash) } + /// Refer to [`databases::Database::add_info_hash_to_whitelist`](crate::tracker::databases::Database::add_info_hash_to_whitelist). async fn add_info_hash_to_whitelist(&self, info_hash: InfoHash) -> Result { let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -173,6 +186,7 @@ impl Database for Mysql { Ok(1) } + /// Refer to [`databases::Database::remove_info_hash_from_whitelist`](crate::tracker::databases::Database::remove_info_hash_from_whitelist). async fn remove_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result { let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -183,6 +197,7 @@ impl Database for Mysql { Ok(1) } + /// Refer to [`databases::Database::get_key_from_keys`](crate::tracker::databases::Database::get_key_from_keys). async fn get_key_from_keys(&self, key: &Key) -> Result, Error> { let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -199,6 +214,7 @@ impl Database for Mysql { })) } + /// Refer to [`databases::Database::add_key_to_keys`](crate::tracker::databases::Database::add_key_to_keys). async fn add_key_to_keys(&self, auth_key: &auth::ExpiringKey) -> Result { let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -213,6 +229,7 @@ impl Database for Mysql { Ok(1) } + /// Refer to [`databases::Database::remove_key_from_keys`](crate::tracker::databases::Database::remove_key_from_keys). async fn remove_key_from_keys(&self, key: &Key) -> Result { let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?; diff --git a/src/tracker/databases/sqlite.rs b/src/tracker/databases/sqlite.rs index 931289183..1968ee049 100644 --- a/src/tracker/databases/sqlite.rs +++ b/src/tracker/databases/sqlite.rs @@ -1,3 +1,4 @@ +//! The `SQLite3` database driver. use std::panic::Location; use std::str::FromStr; @@ -19,6 +20,10 @@ pub struct Sqlite { #[async_trait] impl Database for Sqlite { + /// It instantiates a new `SQLite3` database driver. + /// + /// Refer to [`databases::Database::new`](crate::tracker::databases::Database::new). + /// /// # Errors /// /// Will return `r2d2::Error` if `db_path` is not able to create `SqLite` database. @@ -27,6 +32,7 @@ impl Database for Sqlite { Pool::new(cm).map_or_else(|err| Err((err, DatabaseDriver::Sqlite3).into()), |pool| Ok(Sqlite { pool })) } + /// Refer to [`databases::Database::create_database_tables`](crate::tracker::databases::Database::create_database_tables). fn create_database_tables(&self) -> Result<(), Error> { let create_whitelist_table = " CREATE TABLE IF NOT EXISTS whitelist ( @@ -60,6 +66,7 @@ impl Database for Sqlite { Ok(()) } + /// Refer to [`databases::Database::drop_database_tables`](crate::tracker::databases::Database::drop_database_tables). fn drop_database_tables(&self) -> Result<(), Error> { let drop_whitelist_table = " DROP TABLE whitelist;" @@ -82,6 +89,7 @@ impl Database for Sqlite { Ok(()) } + /// Refer to [`databases::Database::load_persistent_torrents`](crate::tracker::databases::Database::load_persistent_torrents). async fn load_persistent_torrents(&self) -> Result, Error> { let conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -102,6 +110,7 @@ impl Database for Sqlite { Ok(torrents) } + /// Refer to [`databases::Database::load_keys`](crate::tracker::databases::Database::load_keys). async fn load_keys(&self) -> Result, Error> { let conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -122,6 +131,7 @@ impl Database for Sqlite { Ok(keys) } + /// Refer to [`databases::Database::load_whitelist`](crate::tracker::databases::Database::load_whitelist). async fn load_whitelist(&self) -> Result, Error> { let conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -138,6 +148,7 @@ impl Database for Sqlite { Ok(info_hashes) } + /// Refer to [`databases::Database::save_persistent_torrent`](crate::tracker::databases::Database::save_persistent_torrent). async fn save_persistent_torrent(&self, info_hash: &InfoHash, completed: u32) -> Result<(), Error> { let conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -156,6 +167,7 @@ impl Database for Sqlite { } } + /// Refer to [`databases::Database::get_info_hash_from_whitelist`](crate::tracker::databases::Database::get_info_hash_from_whitelist). async fn get_info_hash_from_whitelist(&self, info_hash: &InfoHash) -> Result, Error> { let conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -168,6 +180,7 @@ impl Database for Sqlite { Ok(query.map(|f| InfoHash::from_str(&f.get_unwrap::<_, String>(0)).unwrap())) } + /// Refer to [`databases::Database::add_info_hash_to_whitelist`](crate::tracker::databases::Database::add_info_hash_to_whitelist). async fn add_info_hash_to_whitelist(&self, info_hash: InfoHash) -> Result { let conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -183,6 +196,7 @@ impl Database for Sqlite { } } + /// Refer to [`databases::Database::remove_info_hash_from_whitelist`](crate::tracker::databases::Database::remove_info_hash_from_whitelist). async fn remove_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result { let conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -200,6 +214,7 @@ impl Database for Sqlite { } } + /// Refer to [`databases::Database::get_key_from_keys`](crate::tracker::databases::Database::get_key_from_keys). async fn get_key_from_keys(&self, key: &Key) -> Result, Error> { let conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -219,6 +234,7 @@ impl Database for Sqlite { })) } + /// Refer to [`databases::Database::add_key_to_keys`](crate::tracker::databases::Database::add_key_to_keys). async fn add_key_to_keys(&self, auth_key: &auth::ExpiringKey) -> Result { let conn = self.pool.get().map_err(|e| (e, DRIVER))?; @@ -237,6 +253,7 @@ impl Database for Sqlite { } } + /// Refer to [`databases::Database::remove_key_from_keys`](crate::tracker::databases::Database::remove_key_from_keys). async fn remove_key_from_keys(&self, key: &Key) -> Result { let conn = self.pool.get().map_err(|e| (e, DRIVER))?; diff --git a/src/tracker/error.rs b/src/tracker/error.rs index aaf755e0d..f1e622673 100644 --- a/src/tracker/error.rs +++ b/src/tracker/error.rs @@ -1,7 +1,16 @@ +//! Error returned by the core `Tracker`. +//! +//! Error | Context | Description +//! ---|---|--- +//! `PeerKeyNotValid` | Authentication | The supplied key is not valid. It may not be registered or expired. +//! `PeerNotAuthenticated` | Authentication | The peer did not provide the authentication key. +//! `TorrentNotWhitelisted` | Authorization | The action cannot be perform on a not-whitelisted torrent (it only applies for trackers running in `listed` or `private_listed` modes). +//! use std::panic::Location; use torrust_tracker_located_error::LocatedError; +/// Authentication or authorization error returned by the core `Tracker` #[derive(thiserror::Error, Debug, Clone)] pub enum Error { // Authentication errors diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs index a89d6df2c..ce69b6125 100644 --- a/src/tracker/mod.rs +++ b/src/tracker/mod.rs @@ -1,3 +1,412 @@ +//! The core `tracker` module contains the generic `BitTorrent` tracker logic which is independent of the delivery layer. +//! +//! It contains the tracker services and their dependencies. It's a domain layer which does not +//! specify how the end user should connect to the `Tracker`. +//! +//! Typically this module is intended to be used by higher modules like: +//! +//! - A UDP tracker +//! - A HTTP tracker +//! - A tracker REST API +//! +//! ```text +//! Delivery layer Domain layer +//! +//! HTTP tracker | +//! UDP tracker |> Core tracker +//! Tracker REST API | +//! ``` +//! +//! # Table of contents +//! +//! - [Tracker](#tracker) +//! - [Announce request](#announce-request) +//! - [Scrape request](#scrape-request) +//! - [Torrents](#torrents) +//! - [Peers](#peers) +//! - [Configuration](#configuration) +//! - [Services](#services) +//! - [Authentication](#authentication) +//! - [Statistics](#statistics) +//! - [Persistence](#persistence) +//! +//! # Tracker +//! +//! The `Tracker` is the main struct in this module. `The` tracker has some groups of responsibilities: +//! +//! - **Core tracker**: it handles the information about torrents and peers. +//! - **Authentication**: it handles authentication keys which are used by HTTP trackers. +//! - **Authorization**: it handles the permission to perform requests. +//! - **Whitelist**: when the tracker runs in `listed` or `private_listed` mode all operations are restricted to whitelisted torrents. +//! - **Statistics**: it keeps and serves the tracker statistics. +//! +//! Refer to [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration) crate docs to get more information about the tracker settings. +//! +//! ## Announce request +//! +//! Handling `announce` requests is the most important task for a `BitTorrent` tracker. +//! +//! A `BitTorrent` swarm is a network of peers that are all trying to download the same torrent. +//! When a peer wants to find other peers it announces itself to the swarm via the tracker. +//! The peer sends its data to the tracker so that the tracker can add it to the swarm. +//! The tracker responds to the peer with the list of other peers in the swarm so that +//! the peer can contact them to start downloading pieces of the file from them. +//! +//! Once you have instantiated the `Tracker` you can `announce` a new [`peer`](crate::tracker::peer::Peer) with: +//! +//! ```rust,no_run +//! let info_hash = InfoHash { +//! "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::().unwrap() +//! }; +//! +//! let peer = Peer { +//! peer_id: peer::Id(*b"-qB00000000000000001"), +//! peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8081), +//! updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), +//! uploaded: NumberOfBytes(0), +//! downloaded: NumberOfBytes(0), +//! left: NumberOfBytes(0), +//! event: AnnounceEvent::Completed, +//! } +//! +//! let peer_ip = IpAddr::V4(Ipv4Addr::from_str("126.0.0.1").unwrap()); +//! +//! let announce_data = tracker.announce(&info_hash, &mut peer, &peer_ip).await; +//! ``` +//! +//! The `Tracker` returns the list of peers for the torrent with the infohash `3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0`, +//! filtering out the peer that is making the `announce` request. +//! +//! > **NOTICE**: that the peer argument is mutable because the `Tracker` can change the peer IP if the peer is using a loopback IP. +//! +//! The `peer_ip` argument is the resolved peer ip. It's a common practice that trackers ignore the peer ip in the `announce` request params, +//! and resolve the peer ip using the IP of the client making the request. As the tracker is a domain service, the peer IP must be provided +//! for the `Tracker` user, which is usually a higher component with access the the request metadata, for example, connection data, proxy headers, +//! etcetera. +//! +//! The returned struct is: +//! +//! ```rust,no_run +//! pub struct AnnounceData { +//! pub peers: Vec, +//! pub swarm_stats: SwarmStats, +//! pub interval: u32, // Option `announce_interval` from core tracker configuration +//! pub interval_min: u32, // Option `min_announce_interval` from core tracker configuration +//! } +//! +//! pub struct SwarmStats { +//! pub completed: u32, // The number of peers that have ever completed downloading +//! pub seeders: u32, // The number of active peers that have completed downloading (seeders) +//! pub leechers: u32, // The number of active peers that have not completed downloading (leechers) +//! } +//! +//! // Core tracker configuration +//! pub struct Configuration { +//! // ... +//! pub announce_interval: u32, // Interval in seconds that the client should wait between sending regular announce requests to the tracker +//! pub min_announce_interval: u32, // Minimum announce interval. Clients must not reannounce more frequently than this +//! // ... +//! } +//! ``` +//! +//! Refer to `BitTorrent` BEPs and other sites for more information about the `announce` request: +//! +//! - [BEP 3. 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 docs](https://wiki.vuze.com/w/Announce) +//! +//! ## Scrape request +//! +//! The `scrape` request allows clients to query metadata about the swarm in bulk. +//! +//! An `scrape` request includes a list of infohashes whose swarm metadata you want to collect. +//! +//! The returned struct is: +//! +//! ```rust,no_run +//! pub struct ScrapeData { +//! pub files: HashMap, +//! } +//! +//! pub struct SwarmMetadata { +//! pub complete: u32, // The number of active peers that have completed downloading (seeders) +//! pub downloaded: u32, // The number of peers that have ever completed downloading +//! pub incomplete: u32, // The number of active peers that have not completed downloading (leechers) +//! } +//! ``` +//! +//! The JSON representation of a sample `scrape` response would be like the following: +//! +//! ```json +//! { +//! 'files': { +//! 'xxxxxxxxxxxxxxxxxxxx': {'complete': 11, 'downloaded': 13772, 'incomplete': 19}, +//! 'yyyyyyyyyyyyyyyyyyyy': {'complete': 21, 'downloaded': 206, 'incomplete': 20} +//! } +//! } +//! ``` +//! +//! `xxxxxxxxxxxxxxxxxxxx` and `yyyyyyyyyyyyyyyyyyyy` are 20-byte infohash arrays. +//! There are two data structures for infohashes: byte arrays and hex strings: +//! +//! ```rust,no_run +//! let info_hash: InfoHash = [255u8; 20].into(); +//! +//! assert_eq!( +//! info_hash, +//! InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap() +//! ); +//! ``` +//! Refer to `BitTorrent` BEPs and other sites for more information about the `scrape` request: +//! +//! - [BEP 48. Tracker Protocol Extension: Scrape](https://www.bittorrent.org/beps/bep_0048.html) +//! - [BEP 15. UDP Tracker Protocol for `BitTorrent`. Scrape section](https://www.bittorrent.org/beps/bep_0015.html) +//! - [Vuze docs](https://wiki.vuze.com/w/Scrape) +//! +//! ## Torrents +//! +//! The [`torrent`](crate::tracker::torrent) module contains all the data structures stored by the `Tracker` except for peers. +//! +//! We can represent the data stored in memory internally by the `Tracker` with this JSON object: +//! +//! ```json +//! { +//! "c1277613db1d28709b034a017ab2cae4be07ae10": { +//! "completed": 0, +//! "peers": { +//! "-qB00000000000000001": { +//! "peer_id": "-qB00000000000000001", +//! "peer_addr": "2.137.87.41:1754", +//! "updated": 1672419840, +//! "uploaded": 120, +//! "downloaded": 60, +//! "left": 60, +//! "event": "started" +//! }, +//! "-qB00000000000000002": { +//! "peer_id": "-qB00000000000000002", +//! "peer_addr": "23.17.287.141:2345", +//! "updated": 1679415984, +//! "uploaded": 80, +//! "downloaded": 20, +//! "left": 40, +//! "event": "started" +//! } +//! } +//! } +//! } +//! ``` +//! +//! The `Tracker` maintains an indexed-by-info-hash list of torrents. For each torrent, it stores a torrent `Entry`. +//! The torrent entry has two attributes: +//! +//! - `completed`: which is hte number of peers that have completed downloading the torrent file/s. As they have completed downloading, +//! they have a full version of the torrent data, and they can provide the full data to other peers. That's why they are also known as "seeders". +//! - `peers`: an indexed and orderer list of peer for the torrent. Each peer contains the data received from the peer in the `announce` request. +//! +//! The [`torrent`](crate::tracker::torrent) module not only contains the original data obtained from peer via `announce` requests, it also contains +//! aggregate data that can be derived from the original data. For example: +//! +//! ```rust,no_run +//! pub struct SwarmMetadata { +//! pub complete: u32, // The number of active peers that have completed downloading (seeders) +//! pub downloaded: u32, // The number of peers that have ever completed downloading +//! pub incomplete: u32, // The number of active peers that have not completed downloading (leechers) +//! } +//! +//! pub struct SwarmStats { +//! pub completed: u32, // The number of peers that have ever completed downloading +//! pub seeders: u32, // The number of active peers that have completed downloading (seeders) +//! pub leechers: u32, // The number of active peers that have not completed downloading (leechers) +//! } +//! ``` +//! +//! > **NOTICE**: that `complete` or `completed` peers are the peers that have completed downloading, but only the active ones are considered "seeders". +//! +//! `SwarmStats` struct follows name conventions for `scrape` responses. See [BEP 48](https://www.bittorrent.org/beps/bep_0048.html), while `SwarmStats` +//! is used for the rest of cases. +//! +//! Refer to [`torrent`](crate::tracker::torrent) module for more details about these data structures. +//! +//! ## Peers +//! +//! A `Peer` is the struct used by the `Tracker` to keep peers data: +//! +//! ```rust,no_run +//! pub struct Peer { +//! pub peer_id: Id, // The peer ID +//! pub peer_addr: SocketAddr, // Peer socket address +//! pub updated: DurationSinceUnixEpoch, // Last time (timestamp) when the peer was updated +//! pub uploaded: NumberOfBytes, // Number of bytes the peer has uploaded so far +//! pub downloaded: NumberOfBytes, // Number of bytes the peer has downloaded so far +//! pub left: NumberOfBytes, // The number of bytes this peer still has to download +//! pub event: AnnounceEvent, // The event the peer has announced: `started`, `completed`, `stopped` +//! } +//! ``` +//! +//! Notice that most of the attributes are obtained from the `announce` request. +//! For example, an HTTP announce request would contain the following `GET` parameters: +//! +//! +//! +//! The `Tracker` keeps an in-memory ordered data structure with all the torrents and a list of peers for each torrent, together with some swarm metrics. +//! +//! We can represent the data stored in memory with this JSON object: +//! +//! ```json +//! { +//! "c1277613db1d28709b034a017ab2cae4be07ae10": { +//! "completed": 0, +//! "peers": { +//! "-qB00000000000000001": { +//! "peer_id": "-qB00000000000000001", +//! "peer_addr": "2.137.87.41:1754", +//! "updated": 1672419840, +//! "uploaded": 120, +//! "downloaded": 60, +//! "left": 60, +//! "event": "started" +//! }, +//! "-qB00000000000000002": { +//! "peer_id": "-qB00000000000000002", +//! "peer_addr": "23.17.287.141:2345", +//! "updated": 1679415984, +//! "uploaded": 80, +//! "downloaded": 20, +//! "left": 40, +//! "event": "started" +//! } +//! } +//! } +//! } +//! ``` +//! +//! That JSON object does not exist, it's only a representation of the `Tracker` torrents data. +//! +//! `c1277613db1d28709b034a017ab2cae4be07ae10` is the torrent infohash and `completed` contains the number of peers +//! that have a full version of the torrent data, also known as seeders. +//! +//! Refer to [`peer`](crate::tracker::peer) module for more information about peers. +//! +//! # Configuration +//! +//! You can control the behavior of this module with the module settings: +//! +//! ```toml +//! log_level = "debug" +//! mode = "public" +//! db_driver = "Sqlite3" +//! db_path = "./storage/database/data.db" +//! announce_interval = 120 +//! min_announce_interval = 120 +//! max_peer_timeout = 900 +//! on_reverse_proxy = false +//! external_ip = "2.137.87.41" +//! tracker_usage_statistics = true +//! persistent_torrent_completed_stat = true +//! inactive_peer_cleanup_interval = 600 +//! remove_peerless_torrents = false +//! ``` +//! +//! Refer to the [`configuration` module documentation](https://docs.rs/torrust-tracker-configuration) to get more information about all options. +//! +//! # Services +//! +//! Services are domain services on top of the core tracker. Right now there are two types of service: +//! +//! - For statistics +//! - For torrents +//! +//! Services usually format the data inside the tracker to make it easier to consume by other parts. +//! They also decouple the internal data structure, used by the tracker, from the way we deliver that data to the consumers. +//! The internal data structure is designed for performance or low memory consumption. And it should be changed +//! without affecting the external consumers. +//! +//! Services can include extra features like pagination, for example. +//! +//! Refer to [`services`](crate::tracker::services) module for more information about services. +//! +//! # Authentication +//! +//! One of the core `Tracker` responsibilities is to create and keep authentication keys. Auth keys are used by HTTP trackers +//! when the tracker is running in `private` or `private_listed` mode. +//! +//! HTTP tracker's clients need to obtain an auth key before starting requesting the tracker. Once the get one they have to include +//! a `PATH` param with the key in all the HTTP requests. For example, when a peer wants to `announce` itself it has to use the +//! HTTP tracker endpoint `GET /announce/:key`. +//! +//! The common way to obtain the keys is by using the tracker API directly or via other applications like the [Torrust Index](https://github.com/torrust/torrust-index). +//! +//! To learn more about tracker authentication, refer to the following modules : +//! +//! - [`auth`](crate::tracker::auth) module. +//! - [`tracker`](crate::tracker) module. +//! - [`http`](crate::servers::http) module. +//! +//! # Statistics +//! +//! The `Tracker` keeps metrics for some events: +//! +//! ```rust,no_run +//! pub struct Metrics { +//! // IP version 4 +//! +//! // HTTP tracker +//! pub tcp4_connections_handled: u64, +//! pub tcp4_announces_handled: u64, +//! pub tcp4_scrapes_handled: u64, +//! +//! // UDP tracker +//! pub udp4_connections_handled: u64, +//! pub udp4_announces_handled: u64, +//! pub udp4_scrapes_handled: u64, +//! +//! // IP version 6 +//! +//! // HTTP tracker +//! pub tcp6_connections_handled: u64, +//! pub tcp6_announces_handled: u64, +//! pub tcp6_scrapes_handled: u64, +//! +//! // UDP tracker +//! pub udp6_connections_handled: u64, +//! pub udp6_announces_handled: u64, +//! pub udp6_scrapes_handled: u64, +//! } +//! ``` +//! +//! The metrics maintained by the `Tracker` are: +//! +//! - `connections_handled`: number of connections handled by the tracker +//! - `announces_handled`: number of `announce` requests handled by the tracker +//! - `scrapes_handled`: number of `scrape` handled requests by the tracker +//! +//! > **NOTICE**: as the HTTP tracker does not have an specific `connection` request like the UDP tracker, `connections_handled` are +//! increased on every `announce` and `scrape` requests. +//! +//! The tracker exposes an event sender API that allows the tracker users to send events. When a higher application service handles a +//! `connection` , `announce` or `scrape` requests, it notifies the `Tracker` by sending statistics events. +//! +//! For example, the HTTP tracker would send an event like the following when it handles an `announce` request received from a peer using IP version 4. +//! +//! ```rust,no_run +//! tracker.send_stats_event(statistics::Event::Tcp4Announce).await +//! ``` +//! +//! Refer to [`statistics`](crate::tracker::statistics) module for more information about statistics. +//! +//! # Persistence +//! +//! Right now the `Tracker` is responsible for storing and load data into and +//! from the database, when persistence is enabled. +//! +//! There are three types of persistent object: +//! +//! - Authentication keys (only expiring keys) +//! - Torrent whitelist +//! - Torrent metrics +//! +//! Refer to [`databases`](crate::tracker::databases) module for more information about persistence. pub mod auth; pub mod databases; pub mod error; @@ -25,28 +434,45 @@ use self::torrent::{SwarmMetadata, SwarmStats}; use crate::shared::bit_torrent::info_hash::InfoHash; use crate::tracker::databases::Database; +/// The domain layer tracker service. +/// +/// Its main responsibility is to handle the `announce` and `scrape` requests. +/// But it's also a container for the `Tracker` configuration, persistence, +/// authentication and other services. +/// +/// > **NOTICE**: the `Tracker` is not responsible for handling the network layer. +/// Typically, the `Tracker` is used by a higher application service that handles +/// the network layer. pub struct Tracker { + /// `Tracker` configuration. See pub config: Arc, + /// A database driver implementation: [`Sqlite3`](crate::tracker::databases::sqlite) + /// or [`MySQL`](crate::tracker::databases::mysql) + pub database: Box, mode: TrackerMode, keys: RwLock>, whitelist: RwLock>, torrents: RwLock>, stats_event_sender: Option>, stats_repository: statistics::Repo, - pub database: Box, } +/// Structure that holds general `Tracker` torrents metrics. +/// +/// Metrics are aggregate values for all torrents. #[derive(Debug, PartialEq, Default)] pub struct TorrentsMetrics { - // code-review: consider using `SwarmStats` for - // `seeders`, `completed`, and `leechers` attributes. - // pub swarm_stats: SwarmStats; + /// Total number of seeders for all torrents pub seeders: u64, + /// Total number of peers that have ever completed downloading for all torrents. pub completed: u64, + /// Total number of leechers for all torrents. pub leechers: u64, + /// Total number of torrents. pub torrents: u64, } +/// Structure that holds the data returned by the `announce` request. #[derive(Debug, PartialEq, Default)] pub struct AnnounceData { pub peers: Vec, @@ -55,6 +481,7 @@ pub struct AnnounceData { pub interval_min: u32, } +/// Structure that holds the data returned by the `scrape` request. #[derive(Debug, PartialEq, Default)] pub struct ScrapeData { pub files: HashMap, @@ -88,9 +515,11 @@ impl ScrapeData { } impl Tracker { + /// `Tracker` constructor. + /// /// # Errors /// - /// Will return a `databases::error::Error` if unable to connect to database. + /// Will return a `databases::error::Error` if unable to connect to database. The `Tracker` is responsible for the persistence. pub fn new( config: Arc, stats_event_sender: Option>, @@ -130,6 +559,8 @@ impl Tracker { /// It handles an announce request. /// + /// # Context: Tracker + /// /// BEP 03: [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html). pub async fn announce(&self, info_hash: &InfoHash, peer: &mut Peer, remote_client_ip: &IpAddr) -> AnnounceData { // code-review: maybe instead of mutating the peer we could just return @@ -163,6 +594,8 @@ impl Tracker { /// It handles a scrape request. /// + /// # Context: Tracker + /// /// BEP 48: [Tracker Protocol Extension: Scrape](https://www.bittorrent.org/beps/bep_0048.html). pub async fn scrape(&self, info_hashes: &Vec) -> ScrapeData { let mut scrape_data = ScrapeData::empty(); @@ -178,6 +611,7 @@ impl Tracker { scrape_data } + /// It returns the data for a `scrape` response. async fn get_swarm_metadata(&self, info_hash: &InfoHash) -> SwarmMetadata { let torrents = self.get_torrents().await; match torrents.get(info_hash) { @@ -186,6 +620,168 @@ impl Tracker { } } + /// It loads the torrents from database into memory. It only loads the torrent entry list with the number of seeders for each torrent. + /// Peers data is not persisted. + /// + /// # Context: Tracker + /// + /// # Errors + /// + /// Will return a `database::Error` if unable to load the list of `persistent_torrents` from the database. + pub async fn load_torrents_from_database(&self) -> Result<(), databases::error::Error> { + let persistent_torrents = self.database.load_persistent_torrents().await?; + + let mut torrents = self.torrents.write().await; + + for (info_hash, completed) in persistent_torrents { + // Skip if torrent entry already exists + if torrents.contains_key(&info_hash) { + continue; + } + + let torrent_entry = torrent::Entry { + peers: BTreeMap::default(), + completed, + }; + + torrents.insert(info_hash, torrent_entry); + } + + Ok(()) + } + + async fn get_peers_for_peer(&self, info_hash: &InfoHash, peer: &Peer) -> Vec { + let read_lock = self.torrents.read().await; + + match read_lock.get(info_hash) { + None => vec![], + Some(entry) => entry.get_peers_for_peer(peer).into_iter().copied().collect(), + } + } + + /// # Context: Tracker + /// + /// Get all torrent peers for a given torrent + pub async fn get_all_torrent_peers(&self, info_hash: &InfoHash) -> Vec { + let read_lock = self.torrents.read().await; + + match read_lock.get(info_hash) { + None => vec![], + Some(entry) => entry.get_all_peers().into_iter().copied().collect(), + } + } + + /// It updates the torrent entry in memory, it also stores in the database + /// the torrent info data which is persistent, and finally return the data + /// needed for a `announce` request response. + /// + /// # Context: Tracker + pub async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> torrent::SwarmStats { + // code-review: consider splitting the function in two (command and query segregation). + // `update_torrent_with_peer` and `get_stats` + + let mut torrents = self.torrents.write().await; + + let torrent_entry = match torrents.entry(*info_hash) { + Entry::Vacant(vacant) => vacant.insert(torrent::Entry::new()), + Entry::Occupied(entry) => entry.into_mut(), + }; + + let stats_updated = torrent_entry.update_peer(peer); + + // todo: move this action to a separate worker + if self.config.persistent_torrent_completed_stat && stats_updated { + let _ = self + .database + .save_persistent_torrent(info_hash, torrent_entry.completed) + .await; + } + + let (seeders, completed, leechers) = torrent_entry.get_stats(); + + torrent::SwarmStats { + completed, + seeders, + leechers, + } + } + + pub async fn get_torrents(&self) -> RwLockReadGuard<'_, BTreeMap> { + self.torrents.read().await + } + + /// It calculates and returns the general `Tracker` + /// [`TorrentsMetrics`](crate::tracker::TorrentsMetrics) + /// + /// # Context: Tracker + pub async fn get_torrents_metrics(&self) -> TorrentsMetrics { + let mut torrents_metrics = TorrentsMetrics { + seeders: 0, + completed: 0, + leechers: 0, + torrents: 0, + }; + + let db = self.get_torrents().await; + + db.values().for_each(|torrent_entry| { + let (seeders, completed, leechers) = torrent_entry.get_stats(); + torrents_metrics.seeders += u64::from(seeders); + torrents_metrics.completed += u64::from(completed); + torrents_metrics.leechers += u64::from(leechers); + torrents_metrics.torrents += 1; + }); + + torrents_metrics + } + + /// Remove inactive peers and (optionally) peerless torrents + /// + /// # Context: Tracker + pub async fn cleanup_torrents(&self) { + let mut torrents_lock = self.torrents.write().await; + + // If we don't need to remove torrents we will use the faster iter + if self.config.remove_peerless_torrents { + torrents_lock.retain(|_, torrent_entry| { + torrent_entry.remove_inactive_peers(self.config.max_peer_timeout); + + if self.config.persistent_torrent_completed_stat { + torrent_entry.completed > 0 || !torrent_entry.peers.is_empty() + } else { + !torrent_entry.peers.is_empty() + } + }); + } else { + for (_, torrent_entry) in torrents_lock.iter_mut() { + torrent_entry.remove_inactive_peers(self.config.max_peer_timeout); + } + } + } + + /// It authenticates the peer `key` against the `Tracker` authentication + /// key list. + /// + /// # Errors + /// + /// Will return an error if the the authentication key cannot be verified. + /// + /// # Context: Authentication + pub async fn authenticate(&self, key: &Key) -> Result<(), auth::Error> { + if self.is_private() { + self.verify_auth_key(key).await + } else { + Ok(()) + } + } + + /// It generates a new expiring authentication key. + /// `lifetime` param is the duration in seconds for the new key. + /// The key will be no longer valid after `lifetime` seconds. + /// Authentication keys are used by HTTP trackers. + /// + /// # Context: Authentication + /// /// # Errors /// /// Will return a `database::Error` if unable to add the `auth_key` to the database. @@ -196,6 +792,10 @@ impl Tracker { Ok(auth_key) } + /// It removes an authentication key. + /// + /// # Context: Authentication + /// /// # Errors /// /// Will return a `database::Error` if unable to remove the `key` to the database. @@ -209,6 +809,10 @@ impl Tracker { Ok(()) } + /// It verifies an authentication key. + /// + /// # Context: Authentication + /// /// # Errors /// /// Will return a `key::Error` if unable to get any `auth_key`. @@ -224,6 +828,13 @@ impl Tracker { } } + /// The `Tracker` stores the authentication keys in memory and in the database. + /// In case you need to restart the `Tracker` you can load the keys from the database + /// into memory with this function. Keys are automatically stored in the database when they + /// are generated. + /// + /// # Context: Authentication + /// /// # Errors /// /// Will return a `database::Error` if unable to `load_keys` from the database. @@ -240,84 +851,10 @@ impl Tracker { Ok(()) } - /// Adding torrents is not relevant to public trackers. + /// It authenticates and authorizes a UDP tracker request. /// - /// # Errors + /// # Context: Authentication and Authorization /// - /// Will return a `database::Error` if unable to add the `info_hash` into the whitelist database. - pub async fn add_torrent_to_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { - self.add_torrent_to_database_whitelist(info_hash).await?; - self.add_torrent_to_memory_whitelist(info_hash).await; - Ok(()) - } - - /// It adds a torrent to the whitelist if it has not been whitelisted previously - async fn add_torrent_to_database_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { - let is_whitelisted = self.database.is_info_hash_whitelisted(info_hash).await?; - - if is_whitelisted { - return Ok(()); - } - - self.database.add_info_hash_to_whitelist(*info_hash).await?; - - Ok(()) - } - - pub async fn add_torrent_to_memory_whitelist(&self, info_hash: &InfoHash) -> bool { - self.whitelist.write().await.insert(*info_hash) - } - - /// Removing torrents is not relevant to public trackers. - /// - /// # Errors - /// - /// Will return a `database::Error` if unable to remove the `info_hash` from the whitelist database. - pub async fn remove_torrent_from_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { - self.remove_torrent_from_database_whitelist(info_hash).await?; - self.remove_torrent_from_memory_whitelist(info_hash).await; - Ok(()) - } - - /// # Errors - /// - /// Will return a `database::Error` if unable to remove the `info_hash` from the whitelist database. - pub async fn remove_torrent_from_database_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { - let is_whitelisted = self.database.is_info_hash_whitelisted(info_hash).await?; - - if !is_whitelisted { - return Ok(()); - } - - self.database.remove_info_hash_from_whitelist(*info_hash).await?; - - Ok(()) - } - - pub async fn remove_torrent_from_memory_whitelist(&self, info_hash: &InfoHash) -> bool { - self.whitelist.write().await.remove(info_hash) - } - - pub async fn is_info_hash_whitelisted(&self, info_hash: &InfoHash) -> bool { - self.whitelist.read().await.contains(info_hash) - } - - /// # Errors - /// - /// Will return a `database::Error` if unable to load the list whitelisted `info_hash`s from the database. - pub async fn load_whitelist_from_database(&self) -> Result<(), databases::error::Error> { - let whitelisted_torrents_from_database = self.database.load_whitelist().await?; - let mut whitelist = self.whitelist.write().await; - - whitelist.clear(); - - for info_hash in whitelisted_torrents_from_database { - let _ = whitelist.insert(info_hash); - } - - Ok(()) - } - /// # Errors /// /// Will return a `torrent::Error::PeerKeyNotValid` if the `key` is not valid. @@ -325,6 +862,7 @@ impl Tracker { /// Will return a `torrent::Error::PeerNotAuthenticated` if the `key` is `None`. /// /// Will return a `torrent::Error::TorrentNotWhitelisted` if the the Tracker is in listed mode and the `info_hash` is not whitelisted. + #[deprecated(since = "3.0.0", note = "please use `authenticate` and `authorize` instead")] pub async fn authenticate_request(&self, info_hash: &InfoHash, key: &Option) -> Result<(), Error> { // todo: this is a deprecated method. // We're splitting authentication and authorization responsibilities. @@ -369,18 +907,10 @@ impl Tracker { Ok(()) } - /// # Errors + /// Right now, there is only authorization when the `Tracker` runs in + /// `listed` or `private_listed` modes. /// - /// Will return an error if the the authentication key cannot be verified. - pub async fn authenticate(&self, key: &Key) -> Result<(), auth::Error> { - if self.is_private() { - self.verify_auth_key(key).await - } else { - Ok(()) - } - } - - /// The only authorization process is the whitelist. + /// # Context: Authorization /// /// # Errors /// @@ -401,139 +931,120 @@ impl Tracker { }); } - /// Loading the torrents from database into memory + /// It adds a torrent to the whitelist. + /// Adding torrents is not relevant to public trackers. + /// + /// # Context: Whitelist /// /// # Errors /// - /// Will return a `database::Error` if unable to load the list of `persistent_torrents` from the database. - pub async fn load_torrents_from_database(&self) -> Result<(), databases::error::Error> { - let persistent_torrents = self.database.load_persistent_torrents().await?; - - let mut torrents = self.torrents.write().await; - - for (info_hash, completed) in persistent_torrents { - // Skip if torrent entry already exists - if torrents.contains_key(&info_hash) { - continue; - } - - let torrent_entry = torrent::Entry { - peers: BTreeMap::default(), - completed, - }; - - torrents.insert(info_hash, torrent_entry); - } - + /// Will return a `database::Error` if unable to add the `info_hash` into the whitelist database. + pub async fn add_torrent_to_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { + self.add_torrent_to_database_whitelist(info_hash).await?; + self.add_torrent_to_memory_whitelist(info_hash).await; Ok(()) } - async fn get_peers_for_peer(&self, info_hash: &InfoHash, peer: &Peer) -> Vec { - let read_lock = self.torrents.read().await; + /// It adds a torrent to the whitelist if it has not been whitelisted previously + async fn add_torrent_to_database_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { + let is_whitelisted = self.database.is_info_hash_whitelisted(info_hash).await?; - match read_lock.get(info_hash) { - None => vec![], - Some(entry) => entry.get_peers_for_peer(peer).into_iter().copied().collect(), + if is_whitelisted { + return Ok(()); } - } - /// Get all torrent peers for a given torrent - pub async fn get_all_torrent_peers(&self, info_hash: &InfoHash) -> Vec { - let read_lock = self.torrents.read().await; + self.database.add_info_hash_to_whitelist(*info_hash).await?; - match read_lock.get(info_hash) { - None => vec![], - Some(entry) => entry.get_all_peers().into_iter().copied().collect(), - } + Ok(()) } - pub async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> torrent::SwarmStats { - // code-review: consider splitting the function in two (command and query segregation). - // `update_torrent_with_peer` and `get_stats` - - let mut torrents = self.torrents.write().await; + pub async fn add_torrent_to_memory_whitelist(&self, info_hash: &InfoHash) -> bool { + self.whitelist.write().await.insert(*info_hash) + } - let torrent_entry = match torrents.entry(*info_hash) { - Entry::Vacant(vacant) => vacant.insert(torrent::Entry::new()), - Entry::Occupied(entry) => entry.into_mut(), - }; + /// It removes a torrent from the whitelist. + /// Removing torrents is not relevant to public trackers. + /// + /// # Context: Whitelist + /// + /// # Errors + /// + /// Will return a `database::Error` if unable to remove the `info_hash` from the whitelist database. + pub async fn remove_torrent_from_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { + self.remove_torrent_from_database_whitelist(info_hash).await?; + self.remove_torrent_from_memory_whitelist(info_hash).await; + Ok(()) + } - let stats_updated = torrent_entry.update_peer(peer); + /// It removes a torrent from the whitelist in the database. + /// + /// # Context: Whitelist + /// + /// # Errors + /// + /// Will return a `database::Error` if unable to remove the `info_hash` from the whitelist database. + pub async fn remove_torrent_from_database_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { + let is_whitelisted = self.database.is_info_hash_whitelisted(info_hash).await?; - // todo: move this action to a separate worker - if self.config.persistent_torrent_completed_stat && stats_updated { - let _ = self - .database - .save_persistent_torrent(info_hash, torrent_entry.completed) - .await; + if !is_whitelisted { + return Ok(()); } - let (seeders, completed, leechers) = torrent_entry.get_stats(); + self.database.remove_info_hash_from_whitelist(*info_hash).await?; - torrent::SwarmStats { - completed, - seeders, - leechers, - } + Ok(()) } - pub async fn get_torrents(&self) -> RwLockReadGuard<'_, BTreeMap> { - self.torrents.read().await + /// It removes a torrent from the whitelist in memory. + /// + /// # Context: Whitelist + pub async fn remove_torrent_from_memory_whitelist(&self, info_hash: &InfoHash) -> bool { + self.whitelist.write().await.remove(info_hash) } - pub async fn get_torrents_metrics(&self) -> TorrentsMetrics { - let mut torrents_metrics = TorrentsMetrics { - seeders: 0, - completed: 0, - leechers: 0, - torrents: 0, - }; + /// It checks if a torrent is whitelisted. + /// + /// # Context: Whitelist + pub async fn is_info_hash_whitelisted(&self, info_hash: &InfoHash) -> bool { + self.whitelist.read().await.contains(info_hash) + } - let db = self.get_torrents().await; + /// It loads the whitelist from the database. + /// + /// # Context: Whitelist + /// + /// # Errors + /// + /// Will return a `database::Error` if unable to load the list whitelisted `info_hash`s from the database. + pub async fn load_whitelist_from_database(&self) -> Result<(), databases::error::Error> { + let whitelisted_torrents_from_database = self.database.load_whitelist().await?; + let mut whitelist = self.whitelist.write().await; - db.values().for_each(|torrent_entry| { - let (seeders, completed, leechers) = torrent_entry.get_stats(); - torrents_metrics.seeders += u64::from(seeders); - torrents_metrics.completed += u64::from(completed); - torrents_metrics.leechers += u64::from(leechers); - torrents_metrics.torrents += 1; - }); + whitelist.clear(); - torrents_metrics + for info_hash in whitelisted_torrents_from_database { + let _ = whitelist.insert(info_hash); + } + + Ok(()) } + /// It return the `Tracker` [`statistics::Metrics`]. + /// + /// # Context: Statistics pub async fn get_stats(&self) -> RwLockReadGuard<'_, statistics::Metrics> { self.stats_repository.get_stats().await } + /// It allows to send a statistic events which eventually will be used to update [`statistics::Metrics`]. + /// + /// # Context: Statistics pub async fn send_stats_event(&self, event: statistics::Event) -> Option>> { match &self.stats_event_sender { None => None, Some(stats_event_sender) => stats_event_sender.send_event(event).await, } } - - // Remove inactive peers and (optionally) peerless torrents - pub async fn cleanup_torrents(&self) { - let mut torrents_lock = self.torrents.write().await; - - // If we don't need to remove torrents we will use the faster iter - if self.config.remove_peerless_torrents { - torrents_lock.retain(|_, torrent_entry| { - torrent_entry.remove_inactive_peers(self.config.max_peer_timeout); - - if self.config.persistent_torrent_completed_stat { - torrent_entry.completed > 0 || !torrent_entry.peers.is_empty() - } else { - !torrent_entry.peers.is_empty() - } - }); - } else { - for (_, torrent_entry) in torrents_lock.iter_mut() { - torrent_entry.remove_inactive_peers(self.config.max_peer_timeout); - } - } - } } #[must_use] diff --git a/src/tracker/peer.rs b/src/tracker/peer.rs index 6a298c9df..3626db93d 100644 --- a/src/tracker/peer.rs +++ b/src/tracker/peer.rs @@ -1,3 +1,18 @@ +//! Peer struct used by the core `Tracker`. +//! +//! A sample peer: +//! +//! ```rust,no_run +//! peer::Peer { +//! peer_id: peer::Id(*b"-qB00000000000000000"), +//! peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), +//! updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), +//! uploaded: NumberOfBytes(0), +//! downloaded: NumberOfBytes(0), +//! left: NumberOfBytes(0), +//! event: AnnounceEvent::Started, +//! } +//! ``` use std::net::{IpAddr, SocketAddr}; use std::panic::Location; @@ -10,24 +25,49 @@ use crate::shared::bit_torrent::common::{AnnounceEventDef, NumberOfBytesDef}; use crate::shared::clock::utils::ser_unix_time_value; use crate::shared::clock::DurationSinceUnixEpoch; +/// IP version used by the peer to connect to the tracker: IPv4 or IPv6 #[derive(PartialEq, Eq, Debug)] pub enum IPVersion { + /// IPv4, + /// IPv6, } +/// Peer struct used by the core `Tracker`. +/// +/// A sample peer: +/// +/// ```rust,no_run +/// peer::Peer { +/// peer_id: peer::Id(*b"-qB00000000000000000"), +/// peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), +/// updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), +/// uploaded: NumberOfBytes(0), +/// downloaded: NumberOfBytes(0), +/// left: NumberOfBytes(0), +/// event: AnnounceEvent::Started, +/// } +/// ``` #[derive(PartialEq, Eq, Debug, Clone, Serialize, Copy)] pub struct Peer { + /// ID used by the downloader peer pub peer_id: Id, + /// The IP and port this peer is listening on pub peer_addr: SocketAddr, + /// The last time the the tracker receive an announce request from this peer (timestamp) #[serde(serialize_with = "ser_unix_time_value")] pub updated: DurationSinceUnixEpoch, + /// The total amount of bytes uploaded by this peer so far #[serde(with = "NumberOfBytesDef")] pub uploaded: NumberOfBytes, + /// The total amount of bytes downloaded by this peer so far #[serde(with = "NumberOfBytesDef")] pub downloaded: NumberOfBytes, + /// The number of bytes this peer still has to download #[serde(with = "NumberOfBytesDef")] - pub left: NumberOfBytes, // The number of bytes this peer still has to download + pub left: NumberOfBytes, + /// This is an optional key which maps to started, completed, or stopped (or empty, which is the same as not being present). #[serde(with = "AnnounceEventDef")] pub event: AnnounceEvent, } @@ -56,11 +96,24 @@ impl Peer { } } +/// Peer ID. A 20-byte array. +/// +/// A string of length 20 which this downloader uses as its id. +/// Each downloader generates its own id at random at the start of a new download. +/// +/// A sample peer ID: +/// +/// ```rust,no_run +/// let peer_id = peer::Id(*b"-qB00000000000000000"); +/// ``` #[derive(PartialEq, Eq, Hash, Clone, Debug, PartialOrd, Ord, Copy)] pub struct Id(pub [u8; 20]); const PEER_ID_BYTES_LEN: usize = 20; +/// Error returned when trying to convert an invalid peer id from another type. +/// +/// Usually because the source format does not contain 20 bytes. #[derive(Error, Debug)] pub enum IdConversionError { #[error("not enough bytes for peer id: {message} {location}")] diff --git a/src/tracker/services/mod.rs b/src/tracker/services/mod.rs index 8667f79a9..deb07a439 100644 --- a/src/tracker/services/mod.rs +++ b/src/tracker/services/mod.rs @@ -1,3 +1,9 @@ +//! Tracker domain services. Core and statistics services. +//! +//! There are two types of service: +//! +//! - [Core tracker services](crate::tracker::services::torrent): related to the tracker main functionalities like getting info about torrents. +//! - [Services for statistics](crate::tracker::services::statistics): related to tracker metrics. Aggregate data about the tracker server. pub mod statistics; pub mod torrent; @@ -7,6 +13,8 @@ use torrust_tracker_configuration::Configuration; use crate::tracker::Tracker; +/// It returns a new tracker building its dependencies. +/// /// # Panics /// /// Will panic if tracker cannot be instantiated. diff --git a/src/tracker/services/statistics/mod.rs b/src/tracker/services/statistics/mod.rs index cae4d1d69..143761420 100644 --- a/src/tracker/services/statistics/mod.rs +++ b/src/tracker/services/statistics/mod.rs @@ -1,3 +1,41 @@ +//! Statistics services. +//! +//! It includes: +//! +//! - A [`factory`](crate::tracker::services::statistics::setup::factory) function to build the structs needed to collect the tracker metrics. +//! - A [`get_metrics`](crate::tracker::services::statistics::get_metrics) service to get the [`tracker metrics`](crate::tracker::statistics::Metrics). +//! +//! Tracker metrics are collected using a Publisher-Subscribe pattern. +//! +//! The factory function builds two structs: +//! +//! - An statistics [`EventSender`](crate::tracker::statistics::EventSender) +//! - An statistics [`Repo`](crate::tracker::statistics::Repo) +//! +//! ```rust,no_run +//! let (stats_event_sender, stats_repository) = factory(tracker_usage_statistics); +//! ``` +//! +//! The statistics repository is responsible for storing the metrics in memory. +//! The statistics event sender allows sending events related to metrics. +//! There is an event listener that is receiving all the events and processing them with an event handler. +//! Then, the event handler updates the metrics depending on the received event. +//! +//! For example, if you send the event [`Event::Udp4Connect`](crate::tracker::statistics::Event::Udp4Connect): +//! +//! ```rust,no_run +//! let result = event_sender.send_event(Event::Udp4Connect).await; +//! ``` +//! +//! Eventually the counter for UDP connections from IPv4 peers will be increased. +//! +//! ```rust,no_run +//! pub struct Metrics { +//! // ... +//! pub udp4_connections_handled: u64, // This will be incremented +//! // ... +//! } +//! ``` pub mod setup; use std::sync::Arc; @@ -5,12 +43,21 @@ use std::sync::Arc; use crate::tracker::statistics::Metrics; use crate::tracker::{TorrentsMetrics, Tracker}; +/// All the metrics collected by the tracker. #[derive(Debug, PartialEq)] pub struct TrackerMetrics { + /// Domain level metrics. + /// + /// General metrics for all torrents (number of seeders, leechers, etcetera) pub torrents_metrics: TorrentsMetrics, + + /// Application level metrics. Usage statistics/metrics. + /// + /// Metrics about how the tracker is been used (number of udp announce requests, number of http scrape requests, etcetera) pub protocol_metrics: Metrics, } +/// It returns all the [`TrackerMetrics`](crate::tracker::services::statistics::TrackerMetrics) pub async fn get_metrics(tracker: Arc) -> TrackerMetrics { let torrents_metrics = tracker.get_torrents_metrics().await; let stats = tracker.get_stats().await; diff --git a/src/tracker/services/statistics/setup.rs b/src/tracker/services/statistics/setup.rs index b7cb831cb..b8d325ab4 100644 --- a/src/tracker/services/statistics/setup.rs +++ b/src/tracker/services/statistics/setup.rs @@ -1,5 +1,17 @@ +//! Setup for the tracker statistics. +//! +//! The [`factory`](crate::tracker::services::statistics::setup::factory) function builds the structs needed for handling the tracker metrics. use crate::tracker::statistics; +/// It builds the structs needed for handling the tracker metrics. +/// +/// It returns: +/// +/// - An statistics [`EventSender`](crate::tracker::statistics::EventSender) that allows you to send events related to statistics. +/// - An statistics [`Repo`](crate::tracker::statistics::Repo) which is an in-memory repository for the tracker metrics. +/// +/// When the input argument `tracker_usage_statistics`is false the setup does not run the event listeners, consequently the statistics +/// events are sent are received but not dispatched to the handler. #[must_use] pub fn factory(tracker_usage_statistics: bool) -> (Option>, statistics::Repo) { let mut stats_event_sender = None; diff --git a/src/tracker/services/torrent.rs b/src/tracker/services/torrent.rs index 30d24eb00..3610d930c 100644 --- a/src/tracker/services/torrent.rs +++ b/src/tracker/services/torrent.rs @@ -1,3 +1,9 @@ +//! Core tracker domain services. +//! +//! There are two services: +//! +//! - [`get_torrent_info`](crate::tracker::services::torrent::get_torrent_info): it returns all the data about one torrent. +//! - [`get_torrents`](crate::tracker::services::torrent::get_torrents): it returns data about some torrent in bulk excluding the peer list. use std::sync::Arc; use serde::Deserialize; @@ -6,26 +12,42 @@ use crate::shared::bit_torrent::info_hash::InfoHash; use crate::tracker::peer::Peer; use crate::tracker::Tracker; +/// It contains all the information the tracker has about a torrent #[derive(Debug, PartialEq)] pub struct Info { + /// The infohash of the torrent this data is related to pub info_hash: InfoHash, + /// The total number of seeders for this torrent. Peer that actively serving a full copy of the torrent data pub seeders: u64, + /// The total number of peers that have ever complete downloading this torrent pub completed: u64, + /// The total number of leechers for this torrent. Peers that actively downloading this torrent pub leechers: u64, + /// The swarm: the list of peers that are actively trying to download or serving this torrent pub peers: Option>, } +/// It contains only part of the information the tracker has about a torrent +/// +/// It contains the same data as [Info](crate::tracker::services::torrent::Info) but without the list of peers in the swarm. #[derive(Debug, PartialEq, Clone)] pub struct BasicInfo { + /// The infohash of the torrent this data is related to pub info_hash: InfoHash, + /// The total number of seeders for this torrent. Peer that actively serving a full copy of the torrent data pub seeders: u64, + /// The total number of peers that have ever complete downloading this torrent pub completed: u64, + /// The total number of leechers for this torrent. Peers that actively downloading this torrent pub leechers: u64, } +/// A struct to keep information about the page when results are being paginated #[derive(Deserialize)] pub struct Pagination { + /// The page number, starting at 0 pub offset: u32, + /// Page size. The number of results per page pub limit: u32, } @@ -69,6 +91,7 @@ impl Default for Pagination { } } +/// It returns all the information the tracker has about one torrent in a [Info](crate::tracker::services::torrent::Info) struct. pub async fn get_torrent_info(tracker: Arc, info_hash: &InfoHash) -> Option { let db = tracker.get_torrents().await; @@ -93,6 +116,7 @@ pub async fn get_torrent_info(tracker: Arc, info_hash: &InfoHash) -> Op }) } +/// It returns all the information the tracker has about multiple torrents in a [`BasicInfo`](crate::tracker::services::torrent::BasicInfo) struct, excluding the peer list. pub async fn get_torrents(tracker: Arc, pagination: &Pagination) -> Vec { let db = tracker.get_torrents().await; diff --git a/src/tracker/statistics.rs b/src/tracker/statistics.rs index f9079962c..03f4fc081 100644 --- a/src/tracker/statistics.rs +++ b/src/tracker/statistics.rs @@ -1,3 +1,22 @@ +//! Structs to collect and keep tracker metrics. +//! +//! The tracker collects metrics such as: +//! +//! - Number of connections handled +//! - Number of `announce` requests handled +//! - Number of `scrape` request handled +//! +//! These metrics are collected for each connection type: UDP and HTTP and +//! also for each IP version used by the peers: IPv4 and IPv6. +//! +//! > Notice: that UDP tracker have an specific `connection` request. For the HTTP metrics the counter counts one connection for each `announce` or `scrape` request. +//! +//! The data is collected by using an `event-sender -> event listener` model. +//! +//! The tracker uses an [`statistics::EventSender`](crate::tracker::statistics::EventSender) instance to send an event. +//! The [`statistics::Keeper`](crate::tracker::statistics::Keeper) listens to new events and uses the [`statistics::Repo`](crate::tracker::statistics::Repo) to upgrade and store metrics. +//! +//! See the [`statistics::Event`](crate::tracker::statistics::Event) enum to check which events are available. use std::sync::Arc; use async_trait::async_trait; @@ -9,6 +28,14 @@ use tokio::sync::{mpsc, RwLock, RwLockReadGuard}; const CHANNEL_BUFFER_SIZE: usize = 65_535; +/// An statistics event. It is used to collect tracker metrics. +/// +/// - `Tcp` prefix means the event was triggered by the HTTP tracker +/// - `Udp` prefix means the event was triggered by the UDP tracker +/// - `4` or `6` prefixes means the IP version used by the peer +/// - Finally the event suffix is the type of request: `announce`, `scrape` or `connection` +/// +/// > NOTE: HTTP trackers do not use `connection` requests. #[derive(Debug, PartialEq, Eq)] pub enum Event { // code-review: consider one single event for request type with data: Event::Announce { scheme: HTTPorUDP, ip_version: V4orV6 } @@ -25,6 +52,14 @@ pub enum Event { Udp6Scrape, } +/// Metrics collected by the tracker. +/// +/// - Number of connections handled +/// - Number of `announce` requests handled +/// - Number of `scrape` request handled +/// +/// These metrics are collected for each connection type: UDP and HTTP +/// and also for each IP version used by the peers: IPv4 and IPv6. #[derive(Debug, PartialEq, Default)] pub struct Metrics { pub tcp4_connections_handled: u64, @@ -41,6 +76,10 @@ pub struct Metrics { pub udp6_scrapes_handled: u64, } +/// The service responsible for keeping tracker metrics (listening to statistics events and handle them). +/// +/// It actively listen to new statistics events. When it receives a new event +/// it accordingly increases the counters. pub struct Keeper { pub repository: Repo, } @@ -131,12 +170,17 @@ async fn event_handler(event: Event, stats_repository: &Repo) { debug!("stats: {:?}", stats_repository.get_stats().await); } +/// A trait to allow sending statistics events #[async_trait] #[cfg_attr(test, automock)] pub trait EventSender: Sync + Send { async fn send_event(&self, event: Event) -> Option>>; } +/// An [`statistics::EventSender`](crate::tracker::statistics::EventSender) implementation. +/// +/// It uses a channel sender to send the statistic events. The channel is created by a +/// [`statistics::Keeper`](crate::tracker::statistics::Keeper) pub struct Sender { sender: mpsc::Sender, } @@ -148,6 +192,7 @@ impl EventSender for Sender { } } +/// A repository for the tracker metrics. #[derive(Clone)] pub struct Repo { pub stats: Arc>, diff --git a/src/tracker/torrent.rs b/src/tracker/torrent.rs index 882e52ff1..8eb557f1e 100644 --- a/src/tracker/torrent.rs +++ b/src/tracker/torrent.rs @@ -1,3 +1,33 @@ +//! Structs to store the swarm data. +//! +//! There are to main data structures: +//! +//! - A torrent [`Entry`](crate::tracker::torrent::Entry): it contains all the information stored by the tracker for one torrent. +//! - The [`SwarmMetadata`](crate::tracker::torrent::SwarmMetadata): it contains aggregate information that can me derived from the torrent entries. +//! +//! A "swarm" is a network of peers that are trying to download the same torrent. +//! +//! The torrent entry contains the "swarm" data, which is basically the list of peers in the swarm. +//! That's the most valuable information the peer want to get from the tracker, because it allows them to +//! start downloading torrent from those peers. +//! +//! > **NOTICE**: that both swarm data (torrent entries) and swarm metadata (aggregate counters) are related to only one torrent. +//! +//! The "swarm metadata" contains aggregate data derived from the torrent entries. There two types of data: +//! +//! - For **active peers**: metrics related to the current active peers in the swarm. +//! - **Historical data**: since the tracker started running. +//! +//! The tracker collects metrics for: +//! +//! - The number of peers that have completed downloading the torrent since the tracker started collecting metrics. +//! - The number of peers that have completed downloading the torrent and are still active, that means they are actively participating in the network, +//! by announcing themselves periodically to the tracker. Since they have completed downloading they have a full copy of the torrent data. Peers with a +//! full copy of the data are called "seeders". +//! - The number of peers that have NOT completed downloading the torrent and are still active, that means they are actively participating in the network. +//! Peer that don not have a full copy of the torrent data are called "leechers". +//! +//! > **NOTICE**: that both [`SwarmMetadata`](crate::tracker::torrent::SwarmMetadata) and [`SwarmStats`](crate::tracker::torrent::SwarmStats) contain the same information. [`SwarmMetadata`](crate::tracker::torrent::SwarmMetadata) is using the names used on [BEP 48: Tracker Protocol Extension: Scrape](https://www.bittorrent.org/beps/bep_0048.html). use std::time::Duration; use aquatic_udp_protocol::AnnounceEvent; @@ -7,21 +37,33 @@ use super::peer::{self, Peer}; use crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS; use crate::shared::clock::{Current, TimeNow}; +/// A data structure containing all the information about a torrent in the tracker. +/// +/// This is the tracker entry for a given torrent and contains the swarm data, +/// that's the list of all the peers trying to download the same torrent. +/// The tracker keeps one entry like this for every torrent. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Entry { + /// The swarm: a network of peers that are all trying to download the torrent associated to this entry #[serde(skip)] pub peers: std::collections::BTreeMap, + /// The number of peers that have ever completed downloading the torrent associated to this entry pub completed: u32, } /// Swarm statistics for one torrent. /// Swarm metadata dictionary in the scrape response. -/// BEP 48: +/// +/// See [BEP 48: Tracker Protocol Extension: Scrape](https://www.bittorrent.org/beps/bep_0048.html) #[derive(Debug, PartialEq, Default)] pub struct SwarmMetadata { - pub complete: u32, // The number of active peers that have completed downloading (seeders) - pub downloaded: u32, // The number of peers that have ever completed downloading - pub incomplete: u32, // The number of active peers that have not completed downloading (leechers) + /// 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) + pub incomplete: u32, } impl SwarmMetadata { @@ -32,12 +74,17 @@ impl SwarmMetadata { } /// Swarm statistics for one torrent. -/// Alternative struct for swarm metadata in scrape response. +/// +/// See [BEP 48: Tracker Protocol Extension: Scrape](https://www.bittorrent.org/beps/bep_0048.html) #[derive(Debug, PartialEq, Default)] pub struct SwarmStats { - pub completed: u32, // The number of peers that have ever completed downloading - pub seeders: u32, // The number of active peers that have completed downloading (seeders) - pub leechers: u32, // The number of active peers that have not completed downloading (leechers) + /// 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) + pub leechers: u32, } impl Entry { @@ -49,7 +96,10 @@ impl Entry { } } - // Update peer and return completed (times torrent has been downloaded) + /// It updates a peer and returns true if the number of complete downloads have increased. + /// + /// The number of peers that have complete downloading is synchronously updated when peers are updated. + /// That's the total torrent downloads counter. pub fn update_peer(&mut self, peer: &peer::Peer) -> bool { let mut did_torrent_stats_change: bool = false; @@ -73,14 +123,15 @@ impl Entry { did_torrent_stats_change } - /// Get all peers, limiting the result to the maximum number of scrape torrents. + /// Get all swarm peers, limiting the result to the maximum number of scrape torrents. #[must_use] pub fn get_all_peers(&self) -> Vec<&peer::Peer> { self.peers.values().take(MAX_SCRAPE_TORRENTS as usize).collect() } - /// Returns the list of peers for a given client. - /// It filters out the input peer. + /// It returns the list of peers for a given peer client. + /// + /// It filters out the input peer, typically because we want to return this list of peers to that client peer. #[must_use] pub fn get_peers_for_peer(&self, client: &Peer) -> Vec<&peer::Peer> { self.peers @@ -92,6 +143,7 @@ impl Entry { .collect() } + /// It returns the swarm metadata (statistics) as a tuple `(seeders, completed, leechers)` #[allow(clippy::cast_possible_truncation)] #[must_use] pub fn get_stats(&self) -> (u32, u32, u32) { @@ -100,6 +152,7 @@ impl Entry { (seeders, self.completed, leechers) } + /// It returns the swarm metadata (statistics) as an struct #[must_use] pub fn get_swarm_metadata(&self) -> SwarmMetadata { // code-review: consider using always this function instead of `get_stats`. @@ -111,6 +164,7 @@ impl Entry { } } + /// It removes peer from the swarm that have not been updated for more than `max_peer_timeout` seconds pub fn remove_inactive_peers(&mut self, max_peer_timeout: u32) { let current_cutoff = Current::sub(&Duration::from_secs(u64::from(max_peer_timeout))).unwrap_or_default(); self.peers.retain(|_, peer| peer.updated > current_cutoff); From eab4ebeb6b1a251fddf89047c1ca072785359e1a Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Tue, 28 Mar 2023 11:56:07 +0200 Subject: [PATCH 05/28] feat: add lint for rust-documentation --- .github/workflows/test_build_release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_build_release.yml b/.github/workflows/test_build_release.yml index 3b9a9a44a..d8c25bd56 100644 --- a/.github/workflows/test_build_release.yml +++ b/.github/workflows/test_build_release.yml @@ -34,6 +34,8 @@ jobs: run: cargo check --all-targets - name: Clippy Rust Code run: cargo clippy --all-targets -- -D clippy::pedantic + - name: Test Documentation + run: cargo test --doc - uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@nextest - name: Run Tests From e33cb6df85f132930c7dd2cc6598a73defebb162 Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Tue, 28 Mar 2023 12:08:07 +0200 Subject: [PATCH 06/28] doc: fix basic doc linting errors --- src/tracker/auth.rs | 16 +++++++---- src/tracker/databases/driver.rs | 14 +++++++--- src/tracker/mod.rs | 37 +++++++++++++++++++++----- src/tracker/peer.rs | 20 ++++++++++++-- src/tracker/services/statistics/mod.rs | 4 +-- 5 files changed, 71 insertions(+), 20 deletions(-) diff --git a/src/tracker/auth.rs b/src/tracker/auth.rs index 9068a94f0..9fe111e5e 100644 --- a/src/tracker/auth.rs +++ b/src/tracker/auth.rs @@ -12,6 +12,9 @@ //! Keys are stored in this struct: //! //! ```rust,no_run +//! use torrust_tracker::tracker::auth::Key; +//! use torrust_tracker::shared::clock::DurationSinceUnixEpoch; +//! //! pub struct ExpiringKey { //! /// Random 32-char string. For example: `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ` //! pub key: Key, @@ -23,16 +26,16 @@ //! You can generate a new key valid for `9999` seconds and `0` nanoseconds from the current time with the following: //! //! ```rust,no_run -//! let expiring_key = auth::generate(Duration::new(9999, 0)); +//! use torrust_tracker::tracker::auth; +//! use std::time::Duration; //! -//! assert!(auth::verify(&expiring_key).is_ok()); -//! ``` +//! let expiring_key = auth::generate(Duration::new(9999, 0)); //! -//! And you can later verify it with: +//! // And you can later verify it with: //! -//! ```rust,no_run //! assert!(auth::verify(&expiring_key).is_ok()); //! ``` + use std::panic::Location; use std::str::FromStr; use std::sync::Arc; @@ -135,6 +138,9 @@ pub struct Key(String); /// Error returned when a key cannot be parsed from a string. /// /// ```rust,no_run +/// use torrust_tracker::tracker::auth::Key; +/// use std::str::FromStr; +/// /// let key_string = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ"; /// let key = Key::from_str(key_string); /// diff --git a/src/tracker/databases/driver.rs b/src/tracker/databases/driver.rs index ef9a4eb07..7115bae8e 100644 --- a/src/tracker/databases/driver.rs +++ b/src/tracker/databases/driver.rs @@ -14,17 +14,23 @@ use super::{Builder, Database}; /// Example for `SQLite3`: /// /// ```rust,no_run -/// let db_driver = "Sqlite3".to_string(); +/// use torrust_tracker::tracker::databases; +/// use torrust_tracker_primitives::DatabaseDriver; +/// +/// let db_driver = DatabaseDriver::Sqlite3; /// let db_path = "./storage/database/data.db".to_string(); -/// let database = databases::driver::build(&db_driver, &db_path)?; +/// let database = databases::driver::build(&db_driver, &db_path); /// ``` /// /// Example for `MySQL`: /// /// ```rust,no_run -/// let db_driver = "MySQL".to_string(); +/// use torrust_tracker::tracker::databases; +/// use torrust_tracker_primitives::DatabaseDriver; +/// +/// let db_driver = DatabaseDriver::MySQL; /// let db_path = "mysql://db_user:db_user_secret_password@mysql:3306/torrust_tracker".to_string(); -/// let database = databases::driver::build(&db_driver, &db_path)?; +/// let database = databases::driver::build(&db_driver, &db_path); /// ``` /// /// Refer to the [configuration documentation](https://docs.rs/torrust-tracker-configuration) diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs index ce69b6125..faabbe095 100644 --- a/src/tracker/mod.rs +++ b/src/tracker/mod.rs @@ -55,11 +55,19 @@ //! Once you have instantiated the `Tracker` you can `announce` a new [`peer`](crate::tracker::peer::Peer) with: //! //! ```rust,no_run -//! let info_hash = InfoHash { -//! "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::().unwrap() -//! }; +//! use torrust_tracker::tracker::peer; +//! use torrust_tracker::shared::bit_torrent::info_hash::InfoHash; +//! use torrust_tracker::shared::clock::DurationSinceUnixEpoch; +//! use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; +//! use std::net::SocketAddr; +//! use std::net::IpAddr; +//! use std::net::Ipv4Addr; +//! use std::str::FromStr; +//! +//! +//! let info_hash = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(); //! -//! let peer = Peer { +//! let peer = peer::Peer { //! peer_id: peer::Id(*b"-qB00000000000000001"), //! peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8081), //! updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0), @@ -67,10 +75,11 @@ //! downloaded: NumberOfBytes(0), //! left: NumberOfBytes(0), //! event: AnnounceEvent::Completed, -//! } +//! }; //! //! let peer_ip = IpAddr::V4(Ipv4Addr::from_str("126.0.0.1").unwrap()); -//! +//! ``` +//! ```rust,ignore //! let announce_data = tracker.announce(&info_hash, &mut peer, &peer_ip).await; //! ``` //! @@ -87,6 +96,8 @@ //! The returned struct is: //! //! ```rust,no_run +//! use torrust_tracker::tracker::peer::Peer; +//! //! pub struct AnnounceData { //! pub peers: Vec, //! pub swarm_stats: SwarmStats, @@ -124,6 +135,9 @@ //! The returned struct is: //! //! ```rust,no_run +//! use torrust_tracker::shared::bit_torrent::info_hash::InfoHash; +//! use std::collections::HashMap; +//! //! pub struct ScrapeData { //! pub files: HashMap, //! } @@ -150,6 +164,9 @@ //! There are two data structures for infohashes: byte arrays and hex strings: //! //! ```rust,no_run +//! use torrust_tracker::shared::bit_torrent::info_hash::InfoHash; +//! use std::str::FromStr; +//! //! let info_hash: InfoHash = [255u8; 20].into(); //! //! assert_eq!( @@ -233,6 +250,12 @@ //! A `Peer` is the struct used by the `Tracker` to keep peers data: //! //! ```rust,no_run +//! use torrust_tracker::tracker::peer::Id; +//! use std::net::SocketAddr; +//! use torrust_tracker::shared::clock::DurationSinceUnixEpoch; +//! use aquatic_udp_protocol::NumberOfBytes; +//! use aquatic_udp_protocol::AnnounceEvent; +//! //! pub struct Peer { //! pub peer_id: Id, // The peer ID //! pub peer_addr: SocketAddr, // Peer socket address @@ -389,7 +412,7 @@ //! //! For example, the HTTP tracker would send an event like the following when it handles an `announce` request received from a peer using IP version 4. //! -//! ```rust,no_run +//! ```rust,ignore //! tracker.send_stats_event(statistics::Event::Tcp4Announce).await //! ``` //! diff --git a/src/tracker/peer.rs b/src/tracker/peer.rs index 3626db93d..a54346280 100644 --- a/src/tracker/peer.rs +++ b/src/tracker/peer.rs @@ -3,6 +3,13 @@ //! A sample peer: //! //! ```rust,no_run +//! use torrust_tracker::tracker::peer; +//! use std::net::SocketAddr; +//! use std::net::IpAddr; +//! use std::net::Ipv4Addr; +//! use torrust_tracker::shared::clock::DurationSinceUnixEpoch; +//! use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; +//! //! peer::Peer { //! peer_id: peer::Id(*b"-qB00000000000000000"), //! peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), @@ -11,7 +18,7 @@ //! downloaded: NumberOfBytes(0), //! left: NumberOfBytes(0), //! event: AnnounceEvent::Started, -//! } +//! }; //! ``` use std::net::{IpAddr, SocketAddr}; use std::panic::Location; @@ -39,6 +46,13 @@ pub enum IPVersion { /// A sample peer: /// /// ```rust,no_run +/// use torrust_tracker::tracker::peer; +/// use std::net::SocketAddr; +/// use std::net::IpAddr; +/// use std::net::Ipv4Addr; +/// use torrust_tracker::shared::clock::DurationSinceUnixEpoch; +/// use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; +/// /// peer::Peer { /// peer_id: peer::Id(*b"-qB00000000000000000"), /// peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)), 8080), @@ -47,7 +61,7 @@ pub enum IPVersion { /// downloaded: NumberOfBytes(0), /// left: NumberOfBytes(0), /// event: AnnounceEvent::Started, -/// } +/// }; /// ``` #[derive(PartialEq, Eq, Debug, Clone, Serialize, Copy)] pub struct Peer { @@ -104,6 +118,8 @@ impl Peer { /// A sample peer ID: /// /// ```rust,no_run +/// use torrust_tracker::tracker::peer; +/// /// let peer_id = peer::Id(*b"-qB00000000000000000"); /// ``` #[derive(PartialEq, Eq, Hash, Clone, Debug, PartialOrd, Ord, Copy)] diff --git a/src/tracker/services/statistics/mod.rs b/src/tracker/services/statistics/mod.rs index 143761420..ac3ba510e 100644 --- a/src/tracker/services/statistics/mod.rs +++ b/src/tracker/services/statistics/mod.rs @@ -12,7 +12,7 @@ //! - An statistics [`EventSender`](crate::tracker::statistics::EventSender) //! - An statistics [`Repo`](crate::tracker::statistics::Repo) //! -//! ```rust,no_run +//! ```rust,ignore //! let (stats_event_sender, stats_repository) = factory(tracker_usage_statistics); //! ``` //! @@ -23,7 +23,7 @@ //! //! For example, if you send the event [`Event::Udp4Connect`](crate::tracker::statistics::Event::Udp4Connect): //! -//! ```rust,no_run +//! ```rust,ignore //! let result = event_sender.send_event(Event::Udp4Connect).await; //! ``` //! From b7e78ab4eab5d7784c7d65aee8bbcb59f803f74e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Fri, 24 Mar 2023 17:25:30 +0000 Subject: [PATCH 07/28] docs: [#260] crate docs for shared mod --- cSpell.json | 1 + ...ndelbrot_2048x2048_infohash_v1.png.torrent | Bin 0 -> 375 bytes ...rot_2048x2048_infohash_v1.png.torrent.json | 10 ++ src/shared/bit_torrent/common.rs | 31 +++- src/shared/bit_torrent/info_hash.rs | 140 +++++++++++++++++- src/shared/bit_torrent/mod.rs | 1 + src/shared/clock/mod.rs | 67 +++++++++ src/shared/clock/static_time.rs | 3 + src/shared/clock/time_extent.rs | 110 ++++++++++++++ src/shared/clock/utils.rs | 2 + src/shared/crypto/ephemeral_instance_keys.rs | 5 + src/shared/crypto/keys.rs | 17 +++ src/shared/crypto/mod.rs | 1 + src/shared/mod.rs | 5 + src/tracker/torrent.rs | 13 +- 15 files changed, 400 insertions(+), 6 deletions(-) create mode 100644 docs/media/mandelbrot_2048x2048_infohash_v1.png.torrent create mode 100644 docs/media/mandelbrot_2048x2048_infohash_v1.png.torrent.json diff --git a/cSpell.json b/cSpell.json index 88794b2ad..b0ad4caf7 100644 --- a/cSpell.json +++ b/cSpell.json @@ -37,6 +37,7 @@ "leechers", "libtorrent", "Lphant", + "metainfo", "middlewares", "mockall", "multimap", diff --git a/docs/media/mandelbrot_2048x2048_infohash_v1.png.torrent b/docs/media/mandelbrot_2048x2048_infohash_v1.png.torrent new file mode 100644 index 0000000000000000000000000000000000000000..1a08a811bfc6bfe1f10e2362b951343b11c09dcf GIT binary patch literal 375 zcmYc>G_Xo8N=+V(0!*Kov4u&h8APxcXogko#6sS=zPmbRhbV+xRV(U^YO{Z+ zc)$Gk_uV<=(-hNGEsowiySkg>eUHlQO<#|k)w<7pfak;7`$ZF;Zgcce&T*gJH{Wd) z4{zVa>HptlbjO=4Zogi5blJ~7y=8_qdq4bqr2O+t!}<^gmY=PeGUc5I%lxjb+Ml@W zP{YjC+u{@S9agCBGg@prmz}3FW5$)a$)4*KBGT0IXJqFZnVhZV5RH?%z`z;4+Q3>N z;ou#i_G_P?yZRTTbD9~3ep2|Bvs5%+r*mqiSa4&l;m2)z&7IB&a&P*h?YXpd+n=%$ Im&vKA0ABB%Bme*a literal 0 HcmV?d00001 diff --git a/docs/media/mandelbrot_2048x2048_infohash_v1.png.torrent.json b/docs/media/mandelbrot_2048x2048_infohash_v1.png.torrent.json new file mode 100644 index 000000000..caaa1a417 --- /dev/null +++ b/docs/media/mandelbrot_2048x2048_infohash_v1.png.torrent.json @@ -0,0 +1,10 @@ +{ + "created by": "qBittorrent v4.4.1", + "creation date": 1679674628, + "info": { + "length": 172204, + "name": "mandelbrot_2048x2048.png", + "piece length": 16384, + "pieces": "7D 91 71 0D 9D 4D BA 88 9B 54 20 54 D5 26 72 8D 5A 86 3F E1 21 DF 77 C7 F7 BB 6C 77 96 21 66 25 38 C5 D9 CD AB 8B 08 EF 8C 24 9B B2 F5 C4 CD 2A DF 0B C0 0C F0 AD DF 72 90 E5 B6 41 4C 23 6C 47 9B 8E 9F 46 AA 0C 0D 8E D1 97 FF EE 68 8B 5F 34 A3 87 D7 71 C5 A6 F9 8E 2E A6 31 7C BD F0 F9 E2 23 F9 CC 80 AF 54 00 04 F9 85 69 1C 77 89 C1 76 4E D6 AA BF 61 A6 C2 80 99 AB B6 5F 60 2F 40 A8 25 BE 32 A3 3D 9D 07 0C 79 68 98 D4 9D 63 49 AF 20 58 66 26 6F 98 6B 6D 32 34 CD 7D 08 15 5E 1A D0 00 09 57 AB 30 3B 20 60 C1 DC 12 87 D6 F3 E7 45 4F 70 67 09 36 31 55 F2 20 F6 6C A5 15 6F 2C 89 95 69 16 53 81 7D 31 F1 B6 BD 37 42 CC 11 0B B2 FC 2B 49 A5 85 B6 FC 76 74 44 93" + } +} \ No newline at end of file diff --git a/src/shared/bit_torrent/common.rs b/src/shared/bit_torrent/common.rs index 527ae9ebc..fd52e098c 100644 --- a/src/shared/bit_torrent/common.rs +++ b/src/shared/bit_torrent/common.rs @@ -1,27 +1,56 @@ +//! `BitTorrent` protocol primitive types +//! +//! [BEP 3. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; use serde::{Deserialize, Serialize}; +/// The maximum number of torrents that can be returned in an `scrape` response. +/// It's also the maximum number of peers returned in an `announce` response. +/// +/// The [BEP 15. UDP Tracker Protocol for `BitTorrent`](https://www.bittorrent.org/beps/bep_0015.html) +/// defines this limit: +/// +/// "Up to about 74 torrents can be scraped at once. A full scrape can't be done +/// with this protocol." +/// +/// The [BEP 48. Tracker Protocol Extension: Scrape](https://www.bittorrent.org/beps/bep_0048.html) +/// does not specifically mention this limit, but the limit is being used for +/// both the UDP and HTTP trackers since it's applied at the domain level. pub const MAX_SCRAPE_TORRENTS: u8 = 74; + +/// HTTP tracker authentication key length. +/// +/// See function to [`generate`](crate::tracker::auth::generate) the +/// [`ExpiringKeys`](crate::tracker::auth::ExpiringKey) for more information. pub const AUTH_KEY_LENGTH: usize = 32; #[repr(u32)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub enum Actions { +enum Actions { + // todo: it seems this enum is not used anywhere. Values match the ones in + // aquatic_udp_protocol::request::Request::from_bytes. Connect = 0, Announce = 1, Scrape = 2, Error = 3, } +/// Announce events. Described on the +/// [BEP 3. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) #[derive(Serialize, Deserialize)] #[serde(remote = "AnnounceEvent")] pub enum AnnounceEventDef { + /// The peer has started downloading the torrent. Started, + /// The peer has ceased downloading the torrent. Stopped, + /// The peer has completed downloading the torrent. Completed, + /// This is one of the announcements done at regular intervals. None, } +/// Number of bytes downloaded, uploaded or pending to download (left) by the peer. #[derive(Serialize, Deserialize)] #[serde(remote = "NumberOfBytes")] pub struct NumberOfBytesDef(pub i64); diff --git a/src/shared/bit_torrent/info_hash.rs b/src/shared/bit_torrent/info_hash.rs index fd7602cdd..7392c791d 100644 --- a/src/shared/bit_torrent/info_hash.rs +++ b/src/shared/bit_torrent/info_hash.rs @@ -1,13 +1,147 @@ +//! A `BitTorrent` `InfoHash`. It's a unique identifier for a `BitTorrent` torrent. +//! +//! "The 20-byte sha1 hash of the bencoded form of the info value +//! from the metainfo file." +//! +//! See [BEP 3. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html) +//! for the official specification. +//! +//! This modules provides a type that can be used to represent infohashes. +//! +//! > **NOTICE**: It only supports Info Hash v1. +//! +//! Typically infohashes are represented as hex strings, but internally they are +//! a 20-byte array. +//! +//! # Calculating the info-hash of a torrent file +//! +//! A sample torrent: +//! +//! - Torrent file: `mandelbrot_2048x2048_infohash_v1.png.torrent` +//! - File: `mandelbrot_2048x2048.png` +//! - Info Hash v1: `5452869be36f9f3350ccee6b4544e7e76caaadab` +//! - Sha1 hash of the info dictionary: `5452869BE36F9F3350CCEE6B4544E7E76CAAADAB` +//! +//! A torrent file is a binary file encoded with [Bencode encoding](https://en.wikipedia.org/wiki/Bencode): +//! +//! ```text +//! 0000000: 6431 303a 6372 6561 7465 6420 6279 3138 d10:created by18 +//! 0000010: 3a71 4269 7474 6f72 7265 6e74 2076 342e :qBittorrent v4. +//! 0000020: 342e 3131 333a 6372 6561 7469 6f6e 2064 4.113:creation d +//! 0000030: 6174 6569 3136 3739 3637 3436 3238 6534 atei1679674628e4 +//! 0000040: 3a69 6e66 6f64 363a 6c65 6e67 7468 6931 :infod6:lengthi1 +//! 0000050: 3732 3230 3465 343a 6e61 6d65 3234 3a6d 72204e4:name24:m +//! 0000060: 616e 6465 6c62 726f 745f 3230 3438 7832 andelbrot_2048x2 +//! 0000070: 3034 382e 706e 6731 323a 7069 6563 6520 048.png12:piece +//! 0000080: 6c65 6e67 7468 6931 3633 3834 6536 3a70 lengthi16384e6:p +//! 0000090: 6965 6365 7332 3230 3a7d 9171 0d9d 4dba ieces220:}.q..M. +//! 00000a0: 889b 5420 54d5 2672 8d5a 863f e121 df77 ..T T.&r.Z.?.!.w +//! 00000b0: c7f7 bb6c 7796 2166 2538 c5d9 cdab 8b08 ...lw.!f%8...... +//! 00000c0: ef8c 249b b2f5 c4cd 2adf 0bc0 0cf0 addf ..$.....*....... +//! 00000d0: 7290 e5b6 414c 236c 479b 8e9f 46aa 0c0d r...AL#lG...F... +//! 00000e0: 8ed1 97ff ee68 8b5f 34a3 87d7 71c5 a6f9 .....h._4...q... +//! 00000f0: 8e2e a631 7cbd f0f9 e223 f9cc 80af 5400 ...1|....#....T. +//! 0000100: 04f9 8569 1c77 89c1 764e d6aa bf61 a6c2 ...i.w..vN...a.. +//! 0000110: 8099 abb6 5f60 2f40 a825 be32 a33d 9d07 ...._`/@.%.2.=.. +//! 0000120: 0c79 6898 d49d 6349 af20 5866 266f 986b .yh...cI. Xf&o.k +//! 0000130: 6d32 34cd 7d08 155e 1ad0 0009 57ab 303b m24.}..^....W.0; +//! 0000140: 2060 c1dc 1287 d6f3 e745 4f70 6709 3631 `.......EOpg.61 +//! 0000150: 55f2 20f6 6ca5 156f 2c89 9569 1653 817d U. .l..o,..i.S.} +//! 0000160: 31f1 b6bd 3742 cc11 0bb2 fc2b 49a5 85b6 1...7B.....+I... +//! 0000170: fc76 7444 9365 65 .vtD.ee +//! ``` +//! +//! You can generate that output with the command: +//! +//! ```text +//! xxd mandelbrot_2048x2048_infohash_v1.png.torrent +//! ``` +//! +//! And you can show only the bytes (hexadecimal): +//! +//! ```text +//! 6431303a6372656174656420627931383a71426974746f7272656e742076 +//! 342e342e3131333a6372656174696f6e2064617465693136373936373436 +//! 323865343a696e666f64363a6c656e6774686931373232303465343a6e61 +//! 6d6532343a6d616e64656c62726f745f3230343878323034382e706e6731 +//! 323a7069656365206c656e67746869313633383465363a70696563657332 +//! 32303a7d91710d9d4dba889b542054d526728d5a863fe121df77c7f7bb6c +//! 779621662538c5d9cdab8b08ef8c249bb2f5c4cd2adf0bc00cf0addf7290 +//! e5b6414c236c479b8e9f46aa0c0d8ed197ffee688b5f34a387d771c5a6f9 +//! 8e2ea6317cbdf0f9e223f9cc80af540004f985691c7789c1764ed6aabf61 +//! a6c28099abb65f602f40a825be32a33d9d070c796898d49d6349af205866 +//! 266f986b6d3234cd7d08155e1ad0000957ab303b2060c1dc1287d6f3e745 +//! 4f706709363155f220f66ca5156f2c8995691653817d31f1b6bd3742cc11 +//! 0bb2fc2b49a585b6fc767444936565 +//! ``` +//! +//! You can generate that output with the command: +//! +//! ```text +//! `xxd -ps mandelbrot_2048x2048_infohash_v1.png.torrent`. +//! ``` +//! +//! The same data can be represented in a JSON format: +//! +//! ```json +//! { +//! "created by": "qBittorrent v4.4.1", +//! "creation date": 1679674628, +//! "info": { +//! "length": 172204, +//! "name": "mandelbrot_2048x2048.png", +//! "piece length": 16384, +//! "pieces": "7D 91 71 0D 9D 4D BA 88 9B 54 20 54 D5 26 72 8D 5A 86 3F E1 21 DF 77 C7 F7 BB 6C 77 96 21 66 25 38 C5 D9 CD AB 8B 08 EF 8C 24 9B B2 F5 C4 CD 2A DF 0B C0 0C F0 AD DF 72 90 E5 B6 41 4C 23 6C 47 9B 8E 9F 46 AA 0C 0D 8E D1 97 FF EE 68 8B 5F 34 A3 87 D7 71 C5 A6 F9 8E 2E A6 31 7C BD F0 F9 E2 23 F9 CC 80 AF 54 00 04 F9 85 69 1C 77 89 C1 76 4E D6 AA BF 61 A6 C2 80 99 AB B6 5F 60 2F 40 A8 25 BE 32 A3 3D 9D 07 0C 79 68 98 D4 9D 63 49 AF 20 58 66 26 6F 98 6B 6D 32 34 CD 7D 08 15 5E 1A D0 00 09 57 AB 30 3B 20 60 C1 DC 12 87 D6 F3 E7 45 4F 70 67 09 36 31 55 F2 20 F6 6C A5 15 6F 2C 89 95 69 16 53 81 7D 31 F1 B6 BD 37 42 CC 11 0B B2 FC 2B 49 A5 85 B6 FC 76 74 44 93" +//! } +//! } +//! ``` +//! +//! The JSON object was generated with: +//! +//! As you can see, there is a `info` attribute: +//! +//! ```json +//! { +//! "length": 172204, +//! "name": "mandelbrot_2048x2048.png", +//! "piece length": 16384, +//! "pieces": "7D 91 71 0D 9D 4D BA 88 9B 54 20 54 D5 26 72 8D 5A 86 3F E1 21 DF 77 C7 F7 BB 6C 77 96 21 66 25 38 C5 D9 CD AB 8B 08 EF 8C 24 9B B2 F5 C4 CD 2A DF 0B C0 0C F0 AD DF 72 90 E5 B6 41 4C 23 6C 47 9B 8E 9F 46 AA 0C 0D 8E D1 97 FF EE 68 8B 5F 34 A3 87 D7 71 C5 A6 F9 8E 2E A6 31 7C BD F0 F9 E2 23 F9 CC 80 AF 54 00 04 F9 85 69 1C 77 89 C1 76 4E D6 AA BF 61 A6 C2 80 99 AB B6 5F 60 2F 40 A8 25 BE 32 A3 3D 9D 07 0C 79 68 98 D4 9D 63 49 AF 20 58 66 26 6F 98 6B 6D 32 34 CD 7D 08 15 5E 1A D0 00 09 57 AB 30 3B 20 60 C1 DC 12 87 D6 F3 E7 45 4F 70 67 09 36 31 55 F2 20 F6 6C A5 15 6F 2C 89 95 69 16 53 81 7D 31 F1 B6 BD 37 42 CC 11 0B B2 FC 2B 49 A5 85 B6 FC 76 74 44 93" +//! } +//! ``` +//! +//! The infohash is the [SHA1](https://en.wikipedia.org/wiki/SHA-1) hash +//! of the `info` attribute. That is, the SHA1 hash of: +//! +//! ```text +//! 64363a6c656e6774686931373232303465343a6e61 +//! d6532343a6d616e64656c62726f745f3230343878323034382e706e6731 +//! 23a7069656365206c656e67746869313633383465363a70696563657332 +//! 2303a7d91710d9d4dba889b542054d526728d5a863fe121df77c7f7bb6c +//! 79621662538c5d9cdab8b08ef8c249bb2f5c4cd2adf0bc00cf0addf7290 +//! 5b6414c236c479b8e9f46aa0c0d8ed197ffee688b5f34a387d771c5a6f9 +//! e2ea6317cbdf0f9e223f9cc80af540004f985691c7789c1764ed6aabf61 +//! 6c28099abb65f602f40a825be32a33d9d070c796898d49d6349af205866 +//! 66f986b6d3234cd7d08155e1ad0000957ab303b2060c1dc1287d6f3e745 +//! f706709363155f220f66ca5156f2c8995691653817d31f1b6bd3742cc11 +//! bb2fc2b49a585b6fc7674449365 +//! ``` +//! +//! You can hash that byte string with +//! +//! The result is a 20-char string: `5452869BE36F9F3350CCEE6B4544E7E76CAAADAB` use std::panic::Location; use thiserror::Error; +/// `BitTorrent` Info Hash v1 #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] pub struct InfoHash(pub [u8; 20]); const INFO_HASH_BYTES_LEN: usize = 20; impl InfoHash { + /// Create a new `InfoHash` from a byte slice. + /// /// # Panics /// /// Will panic if byte slice does not contains the exact amount of bytes need for the `InfoHash`. @@ -19,12 +153,13 @@ impl InfoHash { ret } - /// For readability, when accessing the bytes array + /// Returns the `InfoHash` internal byte array. #[must_use] pub fn bytes(&self) -> [u8; 20] { self.0 } + /// Returns the `InfoHash` as a hex string. #[must_use] pub fn to_hex_string(&self) -> String { self.to_string() @@ -79,13 +214,16 @@ impl std::convert::From<[u8; 20]> for InfoHash { } } +/// Errors that can occur when converting from a `Vec` to an `InfoHash`. #[derive(Error, Debug)] pub enum ConversionError { + /// Not enough bytes for infohash. An infohash is 20 bytes. #[error("not enough bytes for infohash: {message} {location}")] NotEnoughBytes { location: &'static Location<'static>, message: String, }, + /// Too many bytes for infohash. An infohash is 20 bytes. #[error("too many bytes for infohash: {message} {location}")] TooManyBytes { location: &'static Location<'static>, diff --git a/src/shared/bit_torrent/mod.rs b/src/shared/bit_torrent/mod.rs index 7579a0780..0e5d7e7f2 100644 --- a/src/shared/bit_torrent/mod.rs +++ b/src/shared/bit_torrent/mod.rs @@ -1,2 +1,3 @@ +//! Common code for the `BitTorrent` protocol. pub mod common; pub mod info_hash; diff --git a/src/shared/clock/mod.rs b/src/shared/clock/mod.rs index b5001e10e..df638f835 100644 --- a/src/shared/clock/mod.rs +++ b/src/shared/clock/mod.rs @@ -1,3 +1,27 @@ +//! Time related functions and types. +//! +//! It's usually a good idea to control where the time comes from +//! in an application so that it can be mocked for testing and it can be +//! controlled in production so we get the intended behavior without +//! relying on the specific time zone for the underlying system. +//! +//! Clocks use the type `DurationSinceUnixEpoch` which is a +//! `std::time::Duration` since the Unix Epoch (timestamp). +//! +//! ```text +//! Local time: lun 2023-03-27 16:12:00 WEST +//! Universal time: lun 2023-03-27 15:12:00 UTC +//! Time zone: Atlantic/Canary (WEST, +0100) +//! Timestamp: 1679929914 +//! Duration: 1679929914.10167426 +//! ``` +//! +//! > **NOTICE**: internally the `Duration` is stores it's main unit as seconds in a `u64` and it will +//! overflow in 584.9 billion years. +//! +//! > **NOTICE**: the timestamp does not depend on the time zone. That gives you +//! the ability to use the clock regardless of the underlying system time zone +//! configuration. See [Unix time Wikipedia entry](https://en.wikipedia.org/wiki/Unix_time). pub mod static_time; pub mod time_extent; pub mod utils; @@ -8,30 +32,47 @@ use std::time::Duration; use chrono::{DateTime, NaiveDateTime, Utc}; +/// Duration since the Unix Epoch. pub type DurationSinceUnixEpoch = Duration; +/// Clock types. #[derive(Debug)] pub enum Type { + /// Clock that returns the current time. WorkingClock, + /// Clock that returns always the same fixed time. StoppedClock, } +/// A generic structure that represents a clock. +/// +/// It can be either the working clock (production) or the stopped clock +/// (testing). It implements the `Time` trait, which gives you the current time. #[derive(Debug)] pub struct Clock; +/// The working clock. It returns the current time. pub type Working = Clock<{ Type::WorkingClock as usize }>; +/// The stopped clock. It returns always the same fixed time. pub type Stopped = Clock<{ Type::StoppedClock as usize }>; +/// The current clock. Defined at compilation time. +/// It can be either the working clock (production) or the stopped clock (testing). #[cfg(not(test))] pub type Current = Working; +/// The current clock. Defined at compilation time. +/// It can be either the working clock (production) or the stopped clock (testing). #[cfg(test)] pub type Current = Stopped; +/// Trait for types that can be used as a timestamp clock. pub trait Time: Sized { fn now() -> DurationSinceUnixEpoch; } +/// Trait for types that can be manipulate the current time in order to +/// get time in the future or in the past after or before a duration of time. pub trait TimeNow: Time { #[must_use] fn add(add_time: &Duration) -> Option { @@ -43,6 +84,10 @@ pub trait TimeNow: Time { } } +/// It converts a string in ISO 8601 format to a timestamp. +/// For example, the string `1970-01-01T00:00:00.000Z` which is the Unix Epoch +/// will be converted to a timestamp of 0: `DurationSinceUnixEpoch::ZERO`. +/// /// # Panics /// /// Will panic if the input time cannot be converted to `DateTime::`. @@ -52,6 +97,10 @@ pub fn convert_from_iso_8601_to_timestamp(iso_8601: &str) -> DurationSinceUnixEp convert_from_datetime_utc_to_timestamp(&DateTime::::from_str(iso_8601).unwrap()) } +/// It converts a `DateTime::` to a timestamp. +/// For example, the `DateTime::` of the Unix Epoch will be converted to a +/// timestamp of 0: `DurationSinceUnixEpoch::ZERO`. +/// /// # Panics /// /// Will panic if the input time overflows the u64 type. @@ -61,6 +110,10 @@ pub fn convert_from_datetime_utc_to_timestamp(datetime_utc: &DateTime) -> D DurationSinceUnixEpoch::from_secs(u64::try_from(datetime_utc.timestamp()).expect("Overflow of u64 seconds, very future!")) } +/// It converts a timestamp to a `DateTime::`. +/// For example, the timestamp of 0: `DurationSinceUnixEpoch::ZERO` will be +/// converted to the `DateTime::` of the Unix Epoch. +/// /// # Panics /// /// Will panic if the input time overflows the i64 type. @@ -144,23 +197,37 @@ mod working_clock { impl TimeNow for Working {} } +/// Trait for types that can be used as a timestamp clock stopped +/// at a given time. pub trait StoppedTime: TimeNow { + /// It sets the clock to a given time. fn local_set(unix_time: &DurationSinceUnixEpoch); + + /// It sets the clock to the Unix Epoch. fn local_set_to_unix_epoch() { Self::local_set(&DurationSinceUnixEpoch::ZERO); } + + /// It sets the clock to the time the application started. fn local_set_to_app_start_time(); + + /// It sets the clock to the current system time. fn local_set_to_system_time_now(); + /// It adds a `Duration` to the clock. + /// /// # Errors /// /// Will return `IntErrorKind` if `duration` would overflow the internal `Duration`. fn local_add(duration: &Duration) -> Result<(), IntErrorKind>; + /// It subtracts a `Duration` from the clock. /// # Errors /// /// Will return `IntErrorKind` if `duration` would underflow the internal `Duration`. fn local_sub(duration: &Duration) -> Result<(), IntErrorKind>; + + /// It resets the clock to default fixed time that is application start time (or the unix epoch when testing). fn local_reset(); } diff --git a/src/shared/clock/static_time.rs b/src/shared/clock/static_time.rs index f916cec9c..79557b3c4 100644 --- a/src/shared/clock/static_time.rs +++ b/src/shared/clock/static_time.rs @@ -1,5 +1,8 @@ +//! It contains a static variable that is set to the time at which +//! the application started. use std::time::SystemTime; lazy_static! { + /// The time at which the application started. pub static ref TIME_AT_APP_START: SystemTime = SystemTime::now(); } diff --git a/src/shared/clock/time_extent.rs b/src/shared/clock/time_extent.rs index 64142c404..2f9e003be 100644 --- a/src/shared/clock/time_extent.rs +++ b/src/shared/clock/time_extent.rs @@ -1,43 +1,124 @@ +//! It includes functionality to handle time extents. +//! +//! Time extents are used to represent a duration of time which contains +//! N times intervals of the same duration. +//! +//! Given a duration of: 60 seconds. +//! +//! ```text +//! |------------------------------------------------------------| +//! ``` +//! +//! If we define a **base** duration of `10` seconds, we would have `6` intervals. +//! +//! ```text +//! |----------|----------|----------|----------|----------|----------| +//! ^--- 10 seconds +//! ``` +//! +//! Then, You can represent half of the duration (`30` seconds) as: +//! +//! ```text +//! |----------|----------|----------|----------|----------|----------| +//! ^--- 30 seconds +//! ``` +//! +//! `3` times (**multiplier**) the **base** interval (3*10 = 30 seconds): +//! +//! ```text +//! |----------|----------|----------|----------|----------|----------| +//! ^--- 30 seconds (3 units of 10 seconds) +//! ``` +//! +//! Time extents are a way to measure time duration using only one unit of time +//! (**base** duration) repeated `N` times (**multiplier**). +//! +//! Time extents are not clocks in a sense that they do not have a start time. +//! They are not synchronized with the real time. In order to measure time, +//! you need to define a start time for the intervals. +//! +//! For example, we could measure time is "lustrums" (5 years) since the start +//! of the 21st century. The time extent would contains a base 5-year duration +//! and the multiplier. The current "lustrum" (2023) would be 5th one if we +//! start counting "lustrums" at 1. +//! +//! ```text +//! Lustrum 1: 2000-2004 +//! Lustrum 2: 2005-2009 +//! Lustrum 3: 2010-2014 +//! Lustrum 4: 2015-2019 +//! Lustrum 5: 2020-2024 +//! ``` +//! +//! More practically time extents are used to represent number of time intervals +//! since the Unix Epoch. Each interval is typically an amount of seconds. +//! It's specially useful to check expiring dates. For example, you can have an +//! authentication token that expires after 120 seconds. If you divide the +//! current timestamp by 120 you get the number of 2-minute intervals since the +//! Unix Epoch, you can hash that value with a secret key and send it to a +//! client. The client can authenticate by sending the hashed value back to the +//! server. The server can build the same hash and compare it with the one sent +//! by the client. The hash would be the same during the 2-minute interval, but +//! it would change after that. This method is one of the methods used by UDP +//! trackers to generate and verify a connection ID, which a a token sent to +//! the client to identify the connection. use std::num::{IntErrorKind, TryFromIntError}; use std::time::Duration; use super::{Stopped, TimeNow, Type, Working}; +/// This trait defines the operations that can be performed on a `TimeExtent`. pub trait Extent: Sized + Default { type Base; type Multiplier; type Product; + /// It creates a new `TimeExtent`. fn new(unit: &Self::Base, count: &Self::Multiplier) -> Self; + /// It increases the `TimeExtent` by a multiplier. + /// /// # Errors /// /// Will return `IntErrorKind` if `add` would overflow the internal `Duration`. fn increase(&self, add: Self::Multiplier) -> Result; + /// It decreases the `TimeExtent` by a multiplier. + /// /// # Errors /// /// Will return `IntErrorKind` if `sub` would underflow the internal `Duration`. fn decrease(&self, sub: Self::Multiplier) -> Result; + /// It returns the total `Duration` of the `TimeExtent`. fn total(&self) -> Option>; + + /// It returns the total `Duration` of the `TimeExtent` plus one increment. fn total_next(&self) -> Option>; } +/// The `TimeExtent` base `Duration`, which is the duration of a single interval. pub type Base = Duration; +/// The `TimeExtent` `Multiplier`, which is the number of `Base` duration intervals. pub type Multiplier = u64; +/// The `TimeExtent` product, which is the total duration of the `TimeExtent`. pub type Product = Base; +/// A `TimeExtent` is a duration of time which contains N times intervals +/// of the same duration. #[derive(Debug, Default, Hash, PartialEq, Eq)] pub struct TimeExtent { pub increment: Base, pub amount: Multiplier, } +/// A zero time extent. It's the additive identity for a `TimeExtent`. pub const ZERO: TimeExtent = TimeExtent { increment: Base::ZERO, amount: Multiplier::MIN, }; + +/// The maximum value for a `TimeExtent`. pub const MAX: TimeExtent = TimeExtent { increment: Base::MAX, amount: Multiplier::MAX, @@ -114,10 +195,23 @@ impl Extent for TimeExtent { } } +/// A `TimeExtent` maker. It's a clock base on time extents. +/// It gives you the time in time extents. pub trait Make: Sized where Clock: TimeNow, { + /// It gives you the current time extent (with a certain increment) for + /// the current time. It gets the current timestamp front he `Clock`. + /// + /// For example: + /// + /// - If the base increment is `1` second, it will return a time extent + /// whose duration is `1 second` and whose multiplier is the the number + /// of seconds since the Unix Epoch (time extent). + /// - If the base increment is `1` minute, it will return a time extent + /// whose duration is `60 seconds` and whose multiplier is the number of + /// minutes since the Unix Epoch (time extent). #[must_use] fn now(increment: &Base) -> Option> { Clock::now() @@ -129,6 +223,9 @@ where }) } + /// Same as [`now`](crate::shared::clock::time_extent::Make::now), but it + /// will add an extra duration to the current time before calculating the + /// time extent. It gives you a time extent for a time in the future. #[must_use] fn now_after(increment: &Base, add_time: &Duration) -> Option> { match Clock::add(add_time) { @@ -143,6 +240,9 @@ where } } + /// Same as [`now`](crate::shared::clock::time_extent::Make::now), but it + /// will subtract a duration to the current time before calculating the + /// time extent. It gives you a time extent for a time in the past. #[must_use] fn now_before(increment: &Base, sub_time: &Duration) -> Option> { match Clock::sub(sub_time) { @@ -158,18 +258,28 @@ where } } +/// A `TimeExtent` maker which makes `TimeExtents`. +/// +/// It's a clock which measures time in `TimeExtents`. #[derive(Debug)] pub struct Maker {} +/// A `TimeExtent` maker which makes `TimeExtents` from the `Working` clock. pub type WorkingTimeExtentMaker = Maker<{ Type::WorkingClock as usize }>; + +/// A `TimeExtent` maker which makes `TimeExtents` from the `Stopped` clock. pub type StoppedTimeExtentMaker = Maker<{ Type::StoppedClock as usize }>; impl Make for WorkingTimeExtentMaker {} impl Make for StoppedTimeExtentMaker {} +/// The default `TimeExtent` maker. It is `WorkingTimeExtentMaker` in production +/// and `StoppedTimeExtentMaker` in tests. #[cfg(not(test))] pub type DefaultTimeExtentMaker = WorkingTimeExtentMaker; +/// The default `TimeExtent` maker. It is `WorkingTimeExtentMaker` in production +/// and `StoppedTimeExtentMaker` in tests. #[cfg(test)] pub type DefaultTimeExtentMaker = StoppedTimeExtentMaker; diff --git a/src/shared/clock/utils.rs b/src/shared/clock/utils.rs index 9127f97b1..94d88d288 100644 --- a/src/shared/clock/utils.rs +++ b/src/shared/clock/utils.rs @@ -1,5 +1,7 @@ +//! It contains helper functions related to time. use super::DurationSinceUnixEpoch; +/// Serializes a `DurationSinceUnixEpoch` as a Unix timestamp in milliseconds. /// # Errors /// /// Will return `serde::Serializer::Error` if unable to serialize the `unix_time_value`. diff --git a/src/shared/crypto/ephemeral_instance_keys.rs b/src/shared/crypto/ephemeral_instance_keys.rs index 635d10fbd..44283365a 100644 --- a/src/shared/crypto/ephemeral_instance_keys.rs +++ b/src/shared/crypto/ephemeral_instance_keys.rs @@ -1,8 +1,13 @@ +//! This module contains the ephemeral instance keys used by the application. +//! +//! They are ephemeral because they are generated at runtime when the +//! application starts and are not persisted anywhere. use rand::rngs::ThreadRng; use rand::Rng; pub type Seed = [u8; 32]; lazy_static! { + /// The random static seed. pub static ref RANDOM_SEED: Seed = Rng::gen(&mut ThreadRng::default()); } diff --git a/src/shared/crypto/keys.rs b/src/shared/crypto/keys.rs index 5e04eb551..92e180996 100644 --- a/src/shared/crypto/keys.rs +++ b/src/shared/crypto/keys.rs @@ -1,13 +1,30 @@ +//! This module contains logic related to cryptographic keys. pub mod seeds { + //! This module contains logic related to cryptographic seeds. + //! + //! Specifically, it contains the logic for storing the seed and providing + //! it to other modules. + //! + //! A **seed** is a pseudo-random number that is used as a secret key for + //! cryptographic operations. use self::detail::CURRENT_SEED; use crate::shared::crypto::ephemeral_instance_keys::{Seed, RANDOM_SEED}; + /// This trait is for structures that can keep and provide a seed. pub trait Keeper { type Seed: Sized + Default + AsMut<[u8]>; + + /// It returns a reference to the seed that is keeping. fn get_seed() -> &'static Self::Seed; } + /// The seed keeper for the instance. When the application is running + /// in production, this will be the seed keeper that is used. pub struct Instance; + + /// The seed keeper for the current execution. It's a facade at compilation + /// time that will either be the instance seed keeper (with a randomly + /// generated key for production) or the zeroed seed keeper. pub struct Current; impl Keeper for Instance { diff --git a/src/shared/crypto/mod.rs b/src/shared/crypto/mod.rs index 066eb0f46..3c7c287b5 100644 --- a/src/shared/crypto/mod.rs +++ b/src/shared/crypto/mod.rs @@ -1,2 +1,3 @@ +//! Cryptographic primitives. pub mod ephemeral_instance_keys; pub mod keys; diff --git a/src/shared/mod.rs b/src/shared/mod.rs index 4b0d9138e..f016ba913 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -1,3 +1,8 @@ +//! Modules with generic logic used by several modules. +//! +//! - [`bit_torrent`]: `BitTorrent` protocol related logic. +//! - [`clock`]: Times services. +//! - [`crypto`]: Encryption related logic. pub mod bit_torrent; pub mod clock; pub mod crypto; diff --git a/src/tracker/torrent.rs b/src/tracker/torrent.rs index 8eb557f1e..1e78cd909 100644 --- a/src/tracker/torrent.rs +++ b/src/tracker/torrent.rs @@ -123,15 +123,18 @@ impl Entry { did_torrent_stats_change } - /// Get all swarm peers, limiting the result to the maximum number of scrape torrents. + /// Get all swarm peers, limiting the result to the maximum number of scrape + /// torrents. #[must_use] pub fn get_all_peers(&self) -> Vec<&peer::Peer> { self.peers.values().take(MAX_SCRAPE_TORRENTS as usize).collect() } - /// It returns the list of peers for a given peer client. + /// It returns the list of peers for a given peer client, limiting the + /// result to the maximum number of scrape torrents. /// - /// It filters out the input peer, typically because we want to return this list of peers to that client peer. + /// It filters out the input peer, typically because we want to return this + /// list of peers to that client peer. #[must_use] pub fn get_peers_for_peer(&self, client: &Peer) -> Vec<&peer::Peer> { self.peers @@ -143,7 +146,9 @@ impl Entry { .collect() } - /// It returns the swarm metadata (statistics) as a tuple `(seeders, completed, leechers)` + /// It returns the swarm metadata (statistics) as a tuple: + /// + /// `(seeders, completed, leechers)` #[allow(clippy::cast_possible_truncation)] #[must_use] pub fn get_stats(&self) -> (u32, u32, u32) { From ddf4dc639bfd591e1862b43fb312b11240f50d51 Mon Sep 17 00:00:00 2001 From: Cameron Garnham Date: Tue, 28 Mar 2023 16:18:01 +0200 Subject: [PATCH 08/28] fix: fix doc for DateTime overflow panic --- src/shared/clock/mod.rs | 12 ++++++------ src/tracker/auth.rs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/shared/clock/mod.rs b/src/shared/clock/mod.rs index df638f835..7a5290f49 100644 --- a/src/shared/clock/mod.rs +++ b/src/shared/clock/mod.rs @@ -90,8 +90,8 @@ pub trait TimeNow: Time { /// /// # Panics /// -/// Will panic if the input time cannot be converted to `DateTime::`. -/// +/// Will panic if the input time cannot be converted to `DateTime::`, internally using the `i64` type. +/// (this will naturally happen in 292.5 billion years) #[must_use] pub fn convert_from_iso_8601_to_timestamp(iso_8601: &str) -> DurationSinceUnixEpoch { convert_from_datetime_utc_to_timestamp(&DateTime::::from_str(iso_8601).unwrap()) @@ -103,8 +103,8 @@ pub fn convert_from_iso_8601_to_timestamp(iso_8601: &str) -> DurationSinceUnixEp /// /// # Panics /// -/// Will panic if the input time overflows the u64 type. -/// +/// Will panic if the input time overflows the `u64` type. +/// (this will naturally happen in 584.9 billion years) #[must_use] pub fn convert_from_datetime_utc_to_timestamp(datetime_utc: &DateTime) -> DurationSinceUnixEpoch { DurationSinceUnixEpoch::from_secs(u64::try_from(datetime_utc.timestamp()).expect("Overflow of u64 seconds, very future!")) @@ -116,8 +116,8 @@ pub fn convert_from_datetime_utc_to_timestamp(datetime_utc: &DateTime) -> D /// /// # Panics /// -/// Will panic if the input time overflows the i64 type. -/// +/// Will panic if the input time overflows the `u64` seconds overflows the `i64` type. +/// (this will naturally happen in 292.5 billion years) #[must_use] pub fn convert_from_timestamp_to_datetime_utc(duration: DurationSinceUnixEpoch) -> DateTime { DateTime::::from_utc( diff --git a/src/tracker/auth.rs b/src/tracker/auth.rs index 9fe111e5e..466187af5 100644 --- a/src/tracker/auth.rs +++ b/src/tracker/auth.rs @@ -120,8 +120,8 @@ impl ExpiringKey { /// /// # Panics /// - /// Will panic when the key timestamp overflows the ui64 type. - /// + /// Will panic when the key timestamp overflows the internal i64 type. + /// (this will naturally happen in 292.5 billion years) #[must_use] pub fn expiry_time(&self) -> chrono::DateTime { convert_from_timestamp_to_datetime_utc(self.valid_until) From 81c4d7072103144e5bd55bbf4147b47190e0951e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 28 Mar 2023 11:26:44 +0100 Subject: [PATCH 09/28] docs: [#263] crate docs for apis mod --- cSpell.json | 1 + src/lib.rs | 4 +- src/servers/apis/mod.rs | 166 ++++++++++++++++++ src/servers/apis/routes.rs | 14 +- src/servers/apis/server.rs | 50 ++++++ .../apis/v1/context/auth_key/handlers.rs | 50 ++++++ src/servers/apis/v1/context/auth_key/mod.rs | 121 +++++++++++++ .../apis/v1/context/auth_key/resources.rs | 6 + .../apis/v1/context/auth_key/responses.rs | 7 + .../apis/v1/context/auth_key/routes.rs | 9 + src/servers/apis/v1/context/mod.rs | 4 + src/servers/apis/v1/context/stats/handlers.rs | 8 + src/servers/apis/v1/context/stats/mod.rs | 48 +++++ .../apis/v1/context/stats/resources.rs | 24 +++ .../apis/v1/context/stats/responses.rs | 3 + src/servers/apis/v1/context/stats/routes.rs | 6 + .../apis/v1/context/torrent/handlers.rs | 29 ++- src/servers/apis/v1/context/torrent/mod.rs | 109 ++++++++++++ .../apis/v1/context/torrent/resources/mod.rs | 2 + .../apis/v1/context/torrent/resources/peer.rs | 14 ++ .../v1/context/torrent/resources/torrent.rs | 32 +++- .../apis/v1/context/torrent/responses.rs | 9 + src/servers/apis/v1/context/torrent/routes.rs | 7 + .../apis/v1/context/whitelist/handlers.rs | 31 ++++ src/servers/apis/v1/context/whitelist/mod.rs | 95 ++++++++++ .../apis/v1/context/whitelist/responses.rs | 5 + .../apis/v1/context/whitelist/routes.rs | 8 + src/servers/apis/v1/middlewares/auth.rs | 30 +++- src/servers/apis/v1/middlewares/mod.rs | 1 + src/servers/apis/v1/mod.rs | 18 ++ src/servers/apis/v1/responses.rs | 3 + src/servers/apis/v1/routes.rs | 7 + src/servers/http/mod.rs | 2 +- src/tracker/mod.rs | 5 +- src/tracker/services/statistics/mod.rs | 4 +- src/tracker/statistics.rs | 14 ++ 36 files changed, 933 insertions(+), 13 deletions(-) diff --git a/cSpell.json b/cSpell.json index b0ad4caf7..e7c0166f8 100644 --- a/cSpell.json +++ b/cSpell.json @@ -16,6 +16,7 @@ "byteorder", "canonicalize", "canonicalized", + "certbot", "chrono", "clippy", "completei", diff --git a/src/lib.rs b/src/lib.rs index a460a28b8..3b9777b36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -315,7 +315,7 @@ //! Using `curl` you can create a 2-minute valid auth key: //! //! ```text -//! $ curl -X POST http://127.0.0.1:1212/api/v1/key/120?token=MyAccessToken +//! $ curl -X POST "http://127.0.0.1:1212/api/v1/key/120?token=MyAccessToken" //! ``` //! //! Response: @@ -329,7 +329,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 API. +//! the Index will create the keys by using the tracker [API](crate::servers::apis). //! //! ## UDP tracker usage //! diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index 1bc257916..203f1d146 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -1,8 +1,174 @@ +//! The tracker REST API with all its versions. +//! +//! > **NOTICE**: This API should not be exposed directly to the internet, it is +//! intended for internal use only. +//! +//! Endpoints for the latest API: [v1](crate::servers::apis::v1). +//! +//! All endpoints require an authorization token which must be set in the +//! configuration before running the tracker. The default configuration uses +//! `?token=MyAccessToken`. Refer to [Authentication](#authentication) for more +//! information. +//! +//! # Table of contents +//! +//! - [Configuration](#configuration) +//! - [Authentication](#authentication) +//! - [Versioning](#versioning) +//! - [Endpoints](#endpoints) +//! - [Documentation](#documentation) +//! +//! # Configuration +//! +//! The configuration file has a [`[http_api]`](torrust_tracker_configuration::HttpApi) +//! section that can be used to enable the API. +//! +//! ```toml +//! [http_api] +//! enabled = true +//! bind_address = "0.0.0.0:1212" +//! ssl_enabled = false +//! ssl_cert_path = "./storage/ssl_certificates/localhost.crt" +//! ssl_key_path = "./storage/ssl_certificates/localhost.key" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! ``` +//! +//! Refer to [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration>) +//! for more information about the API configuration. +//! +//! When you run the tracker with enabled API, you will see the following message: +//! +//! ```text +//! Loading configuration from config file ./config.toml +//! 023-03-28T12:19:24.963054069+01:00 [torrust_tracker::bootstrap::logging][INFO] logging initialized. +//! ... +//! 023-03-28T12:19:24.964138723+01:00 [torrust_tracker::bootstrap::jobs::tracker_apis][INFO] Starting Torrust APIs server on: http://0.0.0.0:1212 +//! ``` +//! +//! The API server will be available on the address specified in the configuration. +//! +//! You can test the API by loading the following URL on a browser: +//! +//! +//! +//! Or using `curl`: +//! +//! ```bash +//! $ curl -s "http://0.0.0.0:1212/api/v1/stats?token=MyAccessToken" +//! ``` +//! +//! The response will be a JSON object. For example, the [tracker statistics +//! endpoint](crate::servers::apis::v1::context::stats#get-tracker-statistics): +//! +//! ```json +//! { +//! "torrents": 0, +//! "seeders": 0, +//! "completed": 0, +//! "leechers": 0, +//! "tcp4_connections_handled": 0, +//! "tcp4_announces_handled": 0, +//! "tcp4_scrapes_handled": 0, +//! "tcp6_connections_handled": 0, +//! "tcp6_announces_handled": 0, +//! "tcp6_scrapes_handled": 0, +//! "udp4_connections_handled": 0, +//! "udp4_announces_handled": 0, +//! "udp4_scrapes_handled": 0, +//! "udp6_connections_handled": 0, +//! "udp6_announces_handled": 0, +//! "udp6_scrapes_handled": 0 +//! } +//! ``` +//! +//! # Authentication +//! +//! The API supports authentication using a GET parameter token. +//! +//! +//! +//! You can set as many tokens as you want in the configuration file: +//! +//! ```toml +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! ``` +//! +//! The token label is used to identify the token. All tokens have full access +//! to the API. +//! +//! Refer to [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration>) +//! for more information about the API configuration and to the +//! [`auth`](crate::servers::apis::v1::middlewares::auth) middleware for more +//! information about the authentication process. +//! +//! # Setup SSL (optional) +//! +//! The API server supports SSL. You can enable it by setting the +//! [`ssl_enabled`](torrust_tracker_configuration::HttpApi::ssl_enabled) option +//! to `true` in the configuration file +//! ([`http_api`](torrust_tracker_configuration::HttpApi) section). +//! +//! ```toml +//! [http_api] +//! enabled = true +//! bind_address = "0.0.0.0:1212" +//! ssl_enabled = true +//! ssl_cert_path = "./storage/ssl_certificates/localhost.crt" +//! ssl_key_path = "./storage/ssl_certificates/localhost.key" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! ``` +//! +//! > **NOTICE**: If you are using a reverse proxy like NGINX, you can skip this +//! step and use NGINX for the SSL instead. See +//! [other alternatives to Nginx/certbot](https://github.com/torrust/torrust-tracker/discussions/131) +//! +//! > **NOTICE**: You can generate a self-signed certificate for localhost using +//! OpenSSL. See [Let's Encrypt](https://letsencrypt.org/docs/certificates-for-localhost/). +//! That's particularly useful for testing purposes. Once you have the certificate +//! you need to set the [`ssl_cert_path`](torrust_tracker_configuration::HttpApi::ssl_cert_path) +//! and [`ssl_key_path`](torrust_tracker_configuration::HttpApi::ssl_key_path) +//! options in the configuration file with the paths to the certificate +//! (`localhost.crt`) and key (`localhost.key`) files. +//! +//! # Versioning +//! +//! The API is versioned and each version has its own module. +//! The API server runs all the API versions on the same server using +//! the same port. Currently there is only one API version: [v1](crate::servers::apis::v1) +//! but a version [`v2`](https://github.com/torrust/torrust-tracker/issues/144) +//! is planned. +//! +//! # Endpoints +//! +//! Refer to the [v1](crate::servers::apis::v1) module for the list of available +//! API endpoints. +//! +//! # Documentation +//! +//! If you want to contribute to this documentation you can [open a new pull request](https://github.com/torrust/torrust-tracker/pulls). +//! +//! > **NOTICE**: we are using [curl](https://curl.se/) in the API examples. +//! And you have to use quotes around the URL in order to avoid unexpected +//! errors. For example: `curl "http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken"`. pub mod routes; pub mod server; pub mod v1; use serde::Deserialize; +/// The info hash URL path parameter. +/// +/// Some API endpoints require an info hash as a path parameter. +/// +/// For example: `http://localhost:1212/api/v1/torrent/{info_hash}`. +/// +/// The info hash represents teh value collected from the URL path parameter. +/// It does not include validation as this is done by the API endpoint handler, +/// in order to provide a more specific error message. #[derive(Deserialize)] pub struct InfoHashParam(pub String); diff --git a/src/servers/apis/routes.rs b/src/servers/apis/routes.rs index 2545d6b88..a4c4642c7 100644 --- a/src/servers/apis/routes.rs +++ b/src/servers/apis/routes.rs @@ -1,11 +1,18 @@ +//! API routes. +//! +//! It loads all the API routes for all API versions and adds the authentication +//! middleware to them. +//! +//! All the API routes have the `/api` prefix and the version number as the +//! first path segment. For example: `/api/v1/torrents`. use std::sync::Arc; use axum::{middleware, Router}; use super::v1; -use super::v1::middlewares::auth::auth; use crate::tracker::Tracker; +/// Add all API routes to the router. #[allow(clippy::needless_pass_by_value)] pub fn router(tracker: Arc) -> Router { let router = Router::new(); @@ -14,5 +21,8 @@ pub fn router(tracker: Arc) -> Router { let router = v1::routes::add(prefix, router, tracker.clone()); - router.layer(middleware::from_fn_with_state(tracker.config.clone(), auth)) + router.layer(middleware::from_fn_with_state( + tracker.config.clone(), + v1::middlewares::auth::auth, + )) } diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index e4714cd9a..76396cc51 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -1,3 +1,28 @@ +//! Logic to run the HTTP API server. +//! +//! It contains two main structs: `ApiServer` and `Launcher`, +//! and two main functions: `start` and `start_tls`. +//! +//! The `ApiServer` struct is responsible for: +//! - Starting and stopping the server. +//! - Storing the configuration. +//! +//! `ApiServer` relies on a launcher to start the actual server. +/// +/// 1. `ApiServer::start` -> spawns new asynchronous task. +/// 2. `Launcher::start` -> starts the server on the spawned task. +/// +/// The `Launcher` struct is responsible for: +/// +/// - Knowing how to start the server with graceful shutdown. +/// +/// For the time being the `ApiServer` and `Launcher` are only used in tests +/// where we need to start and stop the server multiple times. In production +/// code and the main application uses the `start` and `start_tls` functions +/// to start the servers directly since we do not need to control the server +/// when it's running. In the future we might need to control the server, +/// for example, to restart it to apply new configuration changes, to remotely +/// shutdown the server, etc. use std::net::SocketAddr; use std::str::FromStr; use std::sync::Arc; @@ -12,24 +37,35 @@ use super::routes::router; use crate::servers::signals::shutdown_signal; use crate::tracker::Tracker; +/// Errors that can occur when starting or stopping the API server. #[derive(Debug)] pub enum Error { Error(String), } +/// An alias for the `ApiServer` struct with the `Stopped` state. #[allow(clippy::module_name_repetitions)] pub type StoppedApiServer = ApiServer; + +/// An alias for the `ApiServer` struct with the `Running` state. #[allow(clippy::module_name_repetitions)] pub type RunningApiServer = ApiServer; +/// A struct responsible for starting and stopping an API server with a +/// specific configuration and keeping track of the started server. +/// +/// It's a state machine that can be in one of two +/// states: `Stopped` or `Running`. #[allow(clippy::module_name_repetitions)] pub struct ApiServer { pub cfg: torrust_tracker_configuration::HttpApi, pub state: S, } +/// The `Stopped` state of the `ApiServer` struct. pub struct Stopped; +/// The `Running` state of the `ApiServer` struct. pub struct Running { pub bind_addr: SocketAddr, task_killer: tokio::sync::oneshot::Sender, @@ -42,6 +78,8 @@ impl ApiServer { Self { cfg, state: Stopped {} } } + /// Starts the API server with the given configuration. + /// /// # Errors /// /// It would return an error if no `SocketAddr` is returned after launching the server. @@ -75,6 +113,8 @@ impl ApiServer { } impl ApiServer { + /// Stops the API server. + /// /// # Errors /// /// It would return an error if the channel for the task killer signal was closed. @@ -93,9 +133,15 @@ impl ApiServer { } } +/// A struct responsible for starting the API server. struct Launcher; impl Launcher { + /// Starts the API server with graceful shutdown. + /// + /// If TLS is enabled in the configuration, it will start the server with + /// TLS. See [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration>) + /// for more information about configuration. pub fn start( cfg: &torrust_tracker_configuration::HttpApi, tracker: Arc, @@ -126,6 +172,7 @@ impl Launcher { } } + /// Starts the API server with graceful shutdown. pub fn start_with_graceful_shutdown( tcp_listener: std::net::TcpListener, tracker: Arc, @@ -146,6 +193,7 @@ impl Launcher { }) } + /// Starts the API server with graceful shutdown and TLS. pub fn start_tls_with_graceful_shutdown( tcp_listener: std::net::TcpListener, (ssl_cert_path, ssl_key_path): (String, String), @@ -180,6 +228,7 @@ impl Launcher { } } +/// Starts the API server with graceful shutdown on the current thread. pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl Future> { let app = router(tracker); @@ -191,6 +240,7 @@ pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl Future>, Path(seconds_valid_or_key): Path) -> Response { let seconds_valid = seconds_valid_or_key; match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await { @@ -22,9 +34,35 @@ pub async fn generate_auth_key_handler(State(tracker): State>, Path } } +/// A container for the `key` parameter extracted from the URL PATH. +/// +/// It does not perform any validation, it just stores the value. +/// +/// In the current API version, the `key` parameter can be either a valid key +/// like `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6` or the number of seconds the +/// key will be valid, for example two minutes `120`. +/// +/// For example, the `key` is used in the following requests: +/// +/// - `POST /api/v1/key/120`. It will generate a new key valid for two minutes. +/// - `DELETE /api/v1/key/xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. It will delete the +/// key `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. +/// +/// > **NOTICE**: this may change in the future, in the [API v2](https://github.com/torrust/torrust-tracker/issues/144). #[derive(Deserialize)] pub struct KeyParam(String); +/// It handles the request to delete an authentication key. +/// +/// It returns two types of responses: +/// +/// - `200` with an json [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) +/// response. If the key was deleted successfully. +/// - `500` with serialized error in debug format. If the key couldn't be +/// deleted. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key#delete-an-authentication-key) +/// for more information about this endpoint. pub async fn delete_auth_key_handler( State(tracker): State>, Path(seconds_valid_or_key): Path, @@ -38,6 +76,18 @@ pub async fn delete_auth_key_handler( } } +/// It handles the request to reload the authentication keys from the database +/// into memory. +/// +/// It returns two types of responses: +/// +/// - `200` with an json [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) +/// response. If the keys were successfully reloaded. +/// - `500` with serialized error in debug format. If the they couldn't be +/// reloaded. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key#reload-authentication-keys) +/// for more information about this endpoint. pub async fn reload_keys_handler(State(tracker): State>) -> Response { match tracker.load_keys_from_database().await { Ok(_) => ok_response(), diff --git a/src/servers/apis/v1/context/auth_key/mod.rs b/src/servers/apis/v1/context/auth_key/mod.rs index 746a2f064..11bc8a43f 100644 --- a/src/servers/apis/v1/context/auth_key/mod.rs +++ b/src/servers/apis/v1/context/auth_key/mod.rs @@ -1,3 +1,124 @@ +//! Authentication keys API context. +//! +//! Authentication keys are used to authenticate HTTP tracker `announce` and +//! `scrape` requests. +//! +//! When the tracker is running in `private` or `private_listed` mode, the +//! authentication keys are required to announce and scrape torrents. +//! +//! A sample `announce` request **without** authentication key: +//! +//! +//! +//! A sample `announce` request **with** authentication key: +//! +//! +//! +//! # Endpoints +//! +//! - [Generate a new authentication key](#generate-a-new-authentication-key) +//! - [Delete an authentication key](#delete-an-authentication-key) +//! - [Reload authentication keys](#reload-authentication-keys) +//! +//! # Generate a new authentication key +//! +//! `POST /key/:seconds_valid` +//! +//! It generates a new authentication key. +//! +//! > **NOTICE**: keys expire after a certain amount of time. +//! +//! **Path parameters** +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `seconds_valid` | positive integer | The number of seconds the key will be valid. | Yes | `3600` +//! +//! **Example request** +//! +//! ```bash +//! curl -X POST "http://127.0.0.1:1212/api/v1/key/120?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "key": "xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6", +//! "valid_until": 1680009900, +//! "expiry_time": "2023-03-28 13:25:00.058085050 UTC" +//! } +//! ``` +//! +//! > **NOTICE**: `valid_until` and `expiry_time` represent the same time. +//! `valid_until` is the number of seconds since the Unix epoch +//! ([timestamp](https://en.wikipedia.org/wiki/Timestamp)), while `expiry_time` +//! is the human-readable time ([ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html)). +//! +//! **Resource** +//! +//! Refer to the API [`AuthKey`](crate::servers::apis::v1::context::auth_key::resources::AuthKey) +//! resource for more information about the response attributes. +//! +//! # Delete an authentication key +//! +//! `DELETE /key/:key` +//! +//! It deletes a previously generated authentication key. +//! +//! **Path parameters** +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `key` | 40-char string | The `key` to remove. | Yes | `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6` +//! +//! **Example request** +//! +//! ```bash +//! curl -X DELETE "http://127.0.0.1:1212/api/v1/key/xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "status": "ok" +//! } +//! ``` +//! +//! It you try to delete a non-existent key, the response will be an error with +//! a `500` status code. +//! +//! **Example error response** `500` +//! +//! ```text +//! Unhandled rejection: Err { reason: "failed to delete key: Failed to remove record from Sqlite3 database, error-code: 0, src/tracker/databases/sqlite.rs:267:27" } +//! ``` +//! +//! > **NOTICE**: a `500` status code will be returned and the body is not a +//! valid JSON. It's a text body containing the serialized-to-display error +//! message. +//! +//! # Reload authentication keys +//! +//! `GET /keys/reload` +//! +//! The tracker persists the authentication keys in a database. This endpoint +//! reloads the keys from the database. +//! +//! **Example request** +//! +//! ```bash +//! curl "http://127.0.0.1:1212/api/v1/keys/reload?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "status": "ok" +//! } +//! ``` pub mod handlers; pub mod resources; pub mod responses; diff --git a/src/servers/apis/v1/context/auth_key/resources.rs b/src/servers/apis/v1/context/auth_key/resources.rs index 400b34eb7..3eeafbda0 100644 --- a/src/servers/apis/v1/context/auth_key/resources.rs +++ b/src/servers/apis/v1/context/auth_key/resources.rs @@ -1,3 +1,4 @@ +//! API resources for the [`auth_key`](crate::servers::apis::v1::context::auth_key) API context. use std::convert::From; use serde::{Deserialize, Serialize}; @@ -5,10 +6,15 @@ use serde::{Deserialize, Serialize}; use crate::shared::clock::convert_from_iso_8601_to_timestamp; use crate::tracker::auth::{self, Key}; +/// A resource that represents an authentication key. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct AuthKey { + /// The authentication key. pub key: String, + /// The timestamp when the key will expire. + #[deprecated(since = "3.0.0", note = "please use `expiry_time` instead")] pub valid_until: u64, // todo: remove when the torrust-index-backend starts using the `expiry_time` attribute. + /// The ISO 8601 timestamp when the key will expire. pub expiry_time: String, } diff --git a/src/servers/apis/v1/context/auth_key/responses.rs b/src/servers/apis/v1/context/auth_key/responses.rs index 4e3b0c711..51be162c5 100644 --- a/src/servers/apis/v1/context/auth_key/responses.rs +++ b/src/servers/apis/v1/context/auth_key/responses.rs @@ -1,3 +1,4 @@ +//! API responses for the [`auth_key`](crate::servers::apis::v1::context::auth_key) API context. use std::error::Error; use axum::http::{header, StatusCode}; @@ -6,6 +7,8 @@ use axum::response::{IntoResponse, Response}; use crate::servers::apis::v1::context::auth_key::resources::AuthKey; use crate::servers::apis::v1::responses::unhandled_rejection_response; +/// `200` response that contains the `AuthKey` resource as json. +/// /// # Panics /// /// Will panic if it can't convert the `AuthKey` resource to json @@ -19,16 +22,20 @@ pub fn auth_key_response(auth_key: &AuthKey) -> Response { .into_response() } +/// `500` error response when a new authentication key cannot be generated. #[must_use] pub fn failed_to_generate_key_response(e: E) -> Response { unhandled_rejection_response(format!("failed to generate key: {e}")) } +/// `500` error response when an authentication key cannot be deleted. #[must_use] pub fn failed_to_delete_key_response(e: E) -> Response { unhandled_rejection_response(format!("failed to delete key: {e}")) } +/// `500` error response when the authentication keys cannot be reloaded from +/// the database into memory. #[must_use] pub fn failed_to_reload_keys_response(e: E) -> Response { unhandled_rejection_response(format!("failed to reload keys: {e}")) diff --git a/src/servers/apis/v1/context/auth_key/routes.rs b/src/servers/apis/v1/context/auth_key/routes.rs index 9b155c2a5..76c634e21 100644 --- a/src/servers/apis/v1/context/auth_key/routes.rs +++ b/src/servers/apis/v1/context/auth_key/routes.rs @@ -1,3 +1,11 @@ +//! API routes for the [`auth_key`](crate::servers::apis::v1::context::auth_key) +//! API context. +//! +//! - `POST /key/:seconds_valid` +//! - `DELETE /key/:key` +//! - `GET /keys/reload` +//! +//! Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key). use std::sync::Arc; use axum::routing::{get, post}; @@ -6,6 +14,7 @@ use axum::Router; use super::handlers::{delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler}; use crate::tracker::Tracker; +/// It adds the routes to the router for the [`auth_key`](crate::servers::apis::v1::context::auth_key) API context. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { // Keys router diff --git a/src/servers/apis/v1/context/mod.rs b/src/servers/apis/v1/context/mod.rs index 6d3fb7566..5e268a429 100644 --- a/src/servers/apis/v1/context/mod.rs +++ b/src/servers/apis/v1/context/mod.rs @@ -1,3 +1,7 @@ +//! API is organized in resource groups called contexts. +//! +//! Each context is a module that contains the API endpoints related to a +//! specific resource group. pub mod auth_key; pub mod stats; pub mod torrent; diff --git a/src/servers/apis/v1/context/stats/handlers.rs b/src/servers/apis/v1/context/stats/handlers.rs index e93e65996..dfb983f77 100644 --- a/src/servers/apis/v1/context/stats/handlers.rs +++ b/src/servers/apis/v1/context/stats/handlers.rs @@ -1,3 +1,5 @@ +//! API handlers for the [`stats`](crate::servers::apis::v1::context::stats) +//! API context. use std::sync::Arc; use axum::extract::State; @@ -8,6 +10,12 @@ use super::responses::stats_response; use crate::tracker::services::statistics::get_metrics; use crate::tracker::Tracker; +/// It handles the request to get the tracker statistics. +/// +/// It returns a `200` response with a json [`Stats`](crate::servers::apis::v1::context::stats::resources::Stats) +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::stats#get-tracker-statistics) +/// for more information about this endpoint. pub async fn get_stats_handler(State(tracker): State>) -> Json { stats_response(get_metrics(tracker.clone()).await) } diff --git a/src/servers/apis/v1/context/stats/mod.rs b/src/servers/apis/v1/context/stats/mod.rs index 746a2f064..80f37f73f 100644 --- a/src/servers/apis/v1/context/stats/mod.rs +++ b/src/servers/apis/v1/context/stats/mod.rs @@ -1,3 +1,51 @@ +//! Tracker statistics API context. +//! +//! The tracker collects statistics about the number of torrents, seeders, +//! leechers, completed downloads, and the number of requests handled. +//! +//! # Endpoints +//! +//! - [Get tracker statistics](#get-tracker-statistics) +//! +//! # Get tracker statistics +//! +//! `GET /stats` +//! +//! Returns the tracker statistics. +//! +//! **Example request** +//! +//! ```bash +//! curl "http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "torrents": 0, +//! "seeders": 0, +//! "completed": 0, +//! "leechers": 0, +//! "tcp4_connections_handled": 0, +//! "tcp4_announces_handled": 0, +//! "tcp4_scrapes_handled": 0, +//! "tcp6_connections_handled": 0, +//! "tcp6_announces_handled": 0, +//! "tcp6_scrapes_handled": 0, +//! "udp4_connections_handled": 0, +//! "udp4_announces_handled": 0, +//! "udp4_scrapes_handled": 0, +//! "udp6_connections_handled": 0, +//! "udp6_announces_handled": 0, +//! "udp6_scrapes_handled": 0 +//! } +//! ``` +//! +//! **Resource** +//! +//! Refer to the API [`Stats`](crate::servers::apis::v1::context::stats::resources::Stats) +//! resource for more information about the response attributes. pub mod handlers; pub mod resources; pub mod responses; diff --git a/src/servers/apis/v1/context/stats/resources.rs b/src/servers/apis/v1/context/stats/resources.rs index 44ac814dc..355a1e448 100644 --- a/src/servers/apis/v1/context/stats/resources.rs +++ b/src/servers/apis/v1/context/stats/resources.rs @@ -1,24 +1,48 @@ +//! API resources for the [`stats`](crate::servers::apis::v1::context::stats) +//! API context. use serde::{Deserialize, Serialize}; use crate::tracker::services::statistics::TrackerMetrics; +/// It contains all the statistics generated by the tracker. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Stats { + // Torrent metrics + /// Total number of torrents. pub torrents: u64, + /// Total number of seeders for all torrents. pub seeders: u64, + /// Total number of peers that have ever completed downloading for all torrents. pub completed: u64, + /// Total number of leechers for all torrents. pub leechers: u64, + + // Protocol metrics + /// Total number of TCP (HTTP tracker) connections from IPv4 peers. + /// Since the HTTP tracker spec does not require a handshake, this metric + /// increases for every HTTP request. pub tcp4_connections_handled: u64, + /// Total number of TCP (HTTP tracker) `announce` requests from IPv4 peers. pub tcp4_announces_handled: u64, + /// Total number of TCP (HTTP tracker) `scrape` requests from IPv4 peers. pub tcp4_scrapes_handled: u64, + /// Total number of TCP (HTTP tracker) connections from IPv6 peers. pub tcp6_connections_handled: u64, + /// Total number of TCP (HTTP tracker) `announce` requests from IPv6 peers. pub tcp6_announces_handled: u64, + /// Total number of TCP (HTTP tracker) `scrape` requests from IPv6 peers. pub tcp6_scrapes_handled: u64, + /// Total number of UDP (UDP tracker) connections from IPv4 peers. pub udp4_connections_handled: u64, + /// Total number of UDP (UDP tracker) `announce` requests from IPv4 peers. pub udp4_announces_handled: u64, + /// Total number of UDP (UDP tracker) `scrape` requests from IPv4 peers. pub udp4_scrapes_handled: u64, + /// Total number of UDP (UDP tracker) `connection` requests from IPv6 peers. pub udp6_connections_handled: u64, + /// Total number of UDP (UDP tracker) `announce` requests from IPv6 peers. pub udp6_announces_handled: u64, + /// Total number of UDP (UDP tracker) `scrape` requests from IPv6 peers. pub udp6_scrapes_handled: u64, } diff --git a/src/servers/apis/v1/context/stats/responses.rs b/src/servers/apis/v1/context/stats/responses.rs index ea9a2480a..a4dad77e4 100644 --- a/src/servers/apis/v1/context/stats/responses.rs +++ b/src/servers/apis/v1/context/stats/responses.rs @@ -1,8 +1,11 @@ +//! API responses for the [`stats`](crate::servers::apis::v1::context::stats) +//! API context. use axum::response::Json; use super::resources::Stats; use crate::tracker::services::statistics::TrackerMetrics; +/// `200` response that contains the [`Stats`](crate::servers::apis::v1::context::stats::resources::Stats) resource as json. pub fn stats_response(tracker_metrics: TrackerMetrics) -> Json { Json(Stats::from(tracker_metrics)) } diff --git a/src/servers/apis/v1/context/stats/routes.rs b/src/servers/apis/v1/context/stats/routes.rs index 07f88aa70..9198562dd 100644 --- a/src/servers/apis/v1/context/stats/routes.rs +++ b/src/servers/apis/v1/context/stats/routes.rs @@ -1,3 +1,8 @@ +//! API routes for the [`stats`](crate::servers::apis::v1::context::stats) API context. +//! +//! - `GET /stats` +//! +//! Refer to the [API endpoint documentation](crate::servers::apis::v1::context::stats). use std::sync::Arc; use axum::routing::get; @@ -6,6 +11,7 @@ use axum::Router; use super::handlers::get_stats_handler; use crate::tracker::Tracker; +/// It adds the routes to the router for the [`stats`](crate::servers::apis::v1::context::stats) API context. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { router.route(&format!("{prefix}/stats"), get(get_stats_handler).with_state(tracker)) } diff --git a/src/servers/apis/v1/context/torrent/handlers.rs b/src/servers/apis/v1/context/torrent/handlers.rs index 4032f2e9a..002d4356e 100644 --- a/src/servers/apis/v1/context/torrent/handlers.rs +++ b/src/servers/apis/v1/context/torrent/handlers.rs @@ -1,9 +1,12 @@ +//! API handlers for the [`torrent`](crate::servers::apis::v1::context::torrent) +//! API context. use std::fmt; use std::str::FromStr; use std::sync::Arc; use axum::extract::{Path, Query, State}; use axum::response::{IntoResponse, Json, Response}; +use log::debug; use serde::{de, Deserialize, Deserializer}; use super::resources::torrent::ListItem; @@ -14,6 +17,15 @@ use crate::shared::bit_torrent::info_hash::InfoHash; use crate::tracker::services::torrent::{get_torrent_info, get_torrents, Pagination}; use crate::tracker::Tracker; +/// It handles the request to get the torrent data. +/// +/// It returns: +/// +/// - `200` response with a json [`Torrent`](crate::servers::apis::v1::context::torrent::resources::torrent::Torrent). +/// - `500` with serialized error in debug format if the torrent is not known. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::torrent#get-a-torrent) +/// for more information about this endpoint. pub async fn get_torrent_handler(State(tracker): State>, Path(info_hash): Path) -> Response { match InfoHash::from_str(&info_hash.0) { Err(_) => invalid_info_hash_param_response(&info_hash.0), @@ -24,17 +36,32 @@ pub async fn get_torrent_handler(State(tracker): State>, Path(info_ } } -#[derive(Deserialize)] +/// A container for the optional URL query pagination parameters: +/// `offset` and `limit`. +#[derive(Deserialize, Debug)] pub struct PaginationParams { + /// The offset of the first page to return. Starts at 0. #[serde(default, deserialize_with = "empty_string_as_none")] pub offset: Option, + /// The maximum number of items to return per page + #[serde(default, deserialize_with = "empty_string_as_none")] pub limit: Option, } +/// It handles the request to get a list of torrents. +/// +/// It returns a `200` response with a json array with +/// [`ListItem`](crate::servers::apis::v1::context::torrent::resources::torrent::ListItem) +/// resources. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::torrent#list-torrents) +/// for more information about this endpoint. pub async fn get_torrents_handler( State(tracker): State>, pagination: Query, ) -> Json> { + debug!("pagination: {:?}", pagination); + torrent_list_response( &get_torrents( tracker.clone(), diff --git a/src/servers/apis/v1/context/torrent/mod.rs b/src/servers/apis/v1/context/torrent/mod.rs index 746a2f064..1658e1748 100644 --- a/src/servers/apis/v1/context/torrent/mod.rs +++ b/src/servers/apis/v1/context/torrent/mod.rs @@ -1,3 +1,112 @@ +//! Torrents API context. +//! +//! This API context is responsible for handling all the requests related to +//! the torrents data stored by the tracker. +//! +//! # Endpoints +//! +//! - [Get a torrent](#get-a-torrent) +//! - [List torrents](#list-torrents) +//! +//! # Get a torrent +//! +//! `GET /torrent/:info_hash` +//! +//! Returns all the information about a torrent. +//! +//! **Path parameters** +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `info_hash` | 40-char string | The Info Hash v1 | Yes | `5452869be36f9f3350ccee6b4544e7e76caaadab` +//! +//! **Example request** +//! +//! ```bash +//! curl "http://127.0.0.1:1212/api/v1/torrent/5452869be36f9f3350ccee6b4544e7e76caaadab?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "info_hash": "5452869be36f9f3350ccee6b4544e7e76caaadab", +//! "seeders": 1, +//! "completed": 0, +//! "leechers": 0, +//! "peers": [ +//! { +//! "peer_id": { +//! "id": "0x2d7142343431302d2a64465a3844484944704579", +//! "client": "qBittorrent" +//! }, +//! "peer_addr": "192.168.1.88:17548", +//! "updated": 1680082693001, +//! "updated_milliseconds_ago": 1680082693001, +//! "uploaded": 0, +//! "downloaded": 0, +//! "left": 0, +//! "event": "None" +//! } +//! ] +//! } +//! ``` +//! +//! **Not Found response** `200` +//! +//! This response is returned when the tracker does not have the torrent. +//! +//! ```json +//! "torrent not known" +//! ``` +//! +//! **Resource** +//! +//! Refer to the API [`Torrent`](crate::servers::apis::v1::context::torrent::resources::torrent::Torrent) +//! resource for more information about the response attributes. +//! +//! # List torrents +//! +//! `GET /torrents` +//! +//! Returns basic information (no peer list) for all torrents. +//! +//! **Query parameters** +//! +//! The endpoint supports pagination. +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `offset` | positive integer | The page number, starting at 0 | No | `1` +//! `limit` | positive integer | Page size. The number of results per page | No | `10` +//! +//! **Example request** +//! +//! ```bash +//! curl "http://127.0.0.1:1212/api/v1/torrents?token=MyAccessToken&offset=1&limit=1" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! [ +//! { +//! "info_hash": "5452869be36f9f3350ccee6b4544e7e76caaadab", +//! "seeders": 1, +//! "completed": 0, +//! "leechers": 0, +//! "peers": null +//! } +//! ] +//! ``` +//! +//! **Resource** +//! +//! Refer to the API [`ListItem`](crate::servers::apis::v1::context::torrent::resources::torrent::ListItem) +//! resource for more information about the attributes for a single item in the +//! response. +//! +//! > **NOTICE**: this endpoint does not include the `peers` list. pub mod handlers; pub mod resources; pub mod responses; diff --git a/src/servers/apis/v1/context/torrent/resources/mod.rs b/src/servers/apis/v1/context/torrent/resources/mod.rs index 46d62aac5..a6dbff726 100644 --- a/src/servers/apis/v1/context/torrent/resources/mod.rs +++ b/src/servers/apis/v1/context/torrent/resources/mod.rs @@ -1,2 +1,4 @@ +//! API resources for the [`torrent`](crate::servers::apis::v1::context::torrent) +//! API context. pub mod peer; pub mod torrent; diff --git a/src/servers/apis/v1/context/torrent/resources/peer.rs b/src/servers/apis/v1/context/torrent/resources/peer.rs index 5284d26f6..539637b35 100644 --- a/src/servers/apis/v1/context/torrent/resources/peer.rs +++ b/src/servers/apis/v1/context/torrent/resources/peer.rs @@ -1,23 +1,37 @@ +//! `Peer` and Peer `Id` API resources. use serde::{Deserialize, Serialize}; use crate::tracker; +/// `Peer` API resource. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Peer { + /// The peer's ID. See [`Id`](crate::servers::apis::v1::context::torrent::resources::peer::Id). pub peer_id: Id, + /// The peer's socket address. For example: `192.168.1.88:17548`. pub peer_addr: String, + /// The peer's last update time in milliseconds. #[deprecated(since = "2.0.0", note = "please use `updated_milliseconds_ago` instead")] pub updated: u128, + /// The peer's last update time in milliseconds. pub updated_milliseconds_ago: u128, + /// The peer's uploaded bytes. pub uploaded: i64, + /// The peer's downloaded bytes. pub downloaded: i64, + /// The peer's left bytes (pending to download). pub left: i64, + /// The peer's event: `started`, `stopped`, `completed`. + /// See [`AnnounceEventDef`](crate::shared::bit_torrent::common::AnnounceEventDef). pub event: String, } +/// Peer `Id` API resource. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Id { + /// The peer's ID in hex format. For example: `0x2d7142343431302d2a64465a3844484944704579`. pub id: Option, + /// The peer's client name. For example: `qBittorrent`. pub client: Option, } diff --git a/src/servers/apis/v1/context/torrent/resources/torrent.rs b/src/servers/apis/v1/context/torrent/resources/torrent.rs index e328f80c4..c9dbd1c02 100644 --- a/src/servers/apis/v1/context/torrent/resources/torrent.rs +++ b/src/servers/apis/v1/context/torrent/resources/torrent.rs @@ -1,26 +1,52 @@ +//! `Torrent` and `ListItem` API resources. +//! +//! - `Torrent` is the full torrent resource. +//! - `ListItem` is a list item resource on a torrent list. `ListItem` does +//! include a `peers` field but it is always `None` in the struct and `null` in +//! the JSON response. use serde::{Deserialize, Serialize}; use super::peer; use crate::tracker::services::torrent::{BasicInfo, Info}; +/// `Torrent` API resource. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Torrent { + /// The torrent's info hash v1. pub info_hash: String, + /// The torrent's seeders counter. Active peers with a full copy of the + /// torrent. pub seeders: u64, + /// The torrent's completed counter. Peers that have ever completed the + /// download. pub completed: u64, + /// The torrent's leechers counter. Active peers that are downloading the + /// torrent. pub leechers: u64, + /// The torrent's peers. See [`Peer`](crate::servers::apis::v1::context::torrent::resources::peer::Peer). #[serde(skip_serializing_if = "Option::is_none")] pub peers: Option>, } +/// `ListItem` API resource. A list item on a torrent list. +/// `ListItem` does include a `peers` field but it is always `None` in the +/// struct and `null` in the JSON response. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct ListItem { + /// The torrent's info hash v1. pub info_hash: String, + /// The torrent's seeders counter. Active peers with a full copy of the + /// torrent. pub seeders: u64, + /// The torrent's completed counter. Peers that have ever completed the + /// download. pub completed: u64, + /// The torrent's leechers counter. Active peers that are downloading the + /// torrent. pub leechers: u64, - // todo: this is always None. Remove field from endpoint? - pub peers: Option>, + /// The torrent's peers. It's always `None` in the struct and `null` in the + /// JSON response. + pub peers: Option>, // todo: this is always None. Remove field from endpoint? } impl ListItem { @@ -33,6 +59,8 @@ impl ListItem { } } +/// Maps an array of the domain type [`BasicInfo`](crate::tracker::services::torrent::BasicInfo) +/// to the API resource type [`ListItem`](crate::servers::apis::v1::context::torrent::resources::torrent::ListItem). #[must_use] pub fn to_resource(basic_info_vec: &[BasicInfo]) -> Vec { basic_info_vec diff --git a/src/servers/apis/v1/context/torrent/responses.rs b/src/servers/apis/v1/context/torrent/responses.rs index 48e3c6e7f..d3be092eb 100644 --- a/src/servers/apis/v1/context/torrent/responses.rs +++ b/src/servers/apis/v1/context/torrent/responses.rs @@ -1,17 +1,26 @@ +//! API responses for the [`torrent`](crate::servers::apis::v1::context::torrent) +//! API context. use axum::response::{IntoResponse, Json, Response}; use serde_json::json; use super::resources::torrent::{ListItem, Torrent}; use crate::tracker::services::torrent::{BasicInfo, Info}; +/// `200` response that contains an array of +/// [`ListItem`](crate::servers::apis::v1::context::torrent::resources::torrent::ListItem) +/// resources as json. pub fn torrent_list_response(basic_infos: &[BasicInfo]) -> Json> { Json(ListItem::new_vec(basic_infos)) } +/// `200` response that contains a +/// [`Torrent`](crate::servers::apis::v1::context::torrent::resources::torrent::Torrent) +/// resources as json. pub fn torrent_info_response(info: Info) -> Json { Json(Torrent::from(info)) } +/// `500` error response in plain text returned when a torrent is not found. #[must_use] pub fn torrent_not_known_response() -> Response { Json(json!("torrent not known")).into_response() diff --git a/src/servers/apis/v1/context/torrent/routes.rs b/src/servers/apis/v1/context/torrent/routes.rs index 00faa9665..18295f2a2 100644 --- a/src/servers/apis/v1/context/torrent/routes.rs +++ b/src/servers/apis/v1/context/torrent/routes.rs @@ -1,3 +1,9 @@ +//! API routes for the [`torrent`](crate::servers::apis::v1::context::torrent) API context. +//! +//! - `GET /torrent/:info_hash` +//! - `GET /torrents` +//! +//! Refer to the [API endpoint documentation](crate::servers::apis::v1::context::torrent). use std::sync::Arc; use axum::routing::get; @@ -6,6 +12,7 @@ use axum::Router; use super::handlers::{get_torrent_handler, get_torrents_handler}; use crate::tracker::Tracker; +/// It adds the routes to the router for the [`torrent`](crate::servers::apis::v1::context::torrent) API context. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { // Torrents router diff --git a/src/servers/apis/v1/context/whitelist/handlers.rs b/src/servers/apis/v1/context/whitelist/handlers.rs index 25e285c0b..8e8c20b50 100644 --- a/src/servers/apis/v1/context/whitelist/handlers.rs +++ b/src/servers/apis/v1/context/whitelist/handlers.rs @@ -1,3 +1,5 @@ +//! API handlers for the [`whitelist`](crate::servers::apis::v1::context::whitelist) +//! API context. use std::str::FromStr; use std::sync::Arc; @@ -12,6 +14,15 @@ use crate::servers::apis::InfoHashParam; use crate::shared::bit_torrent::info_hash::InfoHash; use crate::tracker::Tracker; +/// It handles the request to add a torrent to the whitelist. +/// +/// It returns: +/// +/// - `200` response with a [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) in json. +/// - `500` with serialized error in debug format if the torrent couldn't be whitelisted. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::whitelist#add-a-torrent-to-the-whitelist) +/// for more information about this endpoint. pub async fn add_torrent_to_whitelist_handler( State(tracker): State>, Path(info_hash): Path, @@ -25,6 +36,16 @@ pub async fn add_torrent_to_whitelist_handler( } } +/// It handles the request to remove a torrent to the whitelist. +/// +/// It returns: +/// +/// - `200` response with a [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) in json. +/// - `500` with serialized error in debug format if the torrent couldn't be +/// removed from the whitelisted. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::whitelist#remove-a-torrent-from-the-whitelist) +/// for more information about this endpoint. pub async fn remove_torrent_from_whitelist_handler( State(tracker): State>, Path(info_hash): Path, @@ -38,6 +59,16 @@ pub async fn remove_torrent_from_whitelist_handler( } } +/// It handles the request to reload the torrent whitelist from the database. +/// +/// It returns: +/// +/// - `200` response with a [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) in json. +/// - `500` with serialized error in debug format if the torrent whitelist +/// couldn't be reloaded from the database. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::whitelist#reload-the-whitelist) +/// for more information about this endpoint. pub async fn reload_whitelist_handler(State(tracker): State>) -> Response { match tracker.load_whitelist_from_database().await { Ok(_) => ok_response(), diff --git a/src/servers/apis/v1/context/whitelist/mod.rs b/src/servers/apis/v1/context/whitelist/mod.rs index f6f000f34..2bb35ef65 100644 --- a/src/servers/apis/v1/context/whitelist/mod.rs +++ b/src/servers/apis/v1/context/whitelist/mod.rs @@ -1,3 +1,98 @@ +//! Whitelist API context. +//! +//! This API context is responsible for handling all the requests related to +//! the torrent whitelist. +//! +//! A torrent whitelist is a list of Info Hashes that are allowed to be tracked +//! by the tracker. This is useful when you want to limit the torrents that are +//! tracked by the tracker. +//! +//! Common tracker requests like `announce` and `scrape` are limited to the +//! torrents in the whitelist. The whitelist can be updated using the API. +//! +//! > **NOTICE**: the whitelist is only used when the tracker is configured to +//! in `listed` or `private_listed` modes. Refer to the +//! [configuration crate documentation](https://docs.rs/torrust-tracker-configuration) +//! to know how to enable the those modes. +//! +//! > **NOTICE**: if the tracker is not running in `listed` or `private_listed` +//! modes the requests to the whitelist API will be ignored. +//! +//! # Endpoints +//! +//! - [Add a torrent to the whitelist](#add-a-torrent-to-the-whitelist) +//! - [Remove a torrent from the whitelist](#remove-a-torrent-from-the-whitelist) +//! - [Reload the whitelist](#reload-the-whitelist) +//! +//! # Add a torrent to the whitelist +//! +//! `POST /whitelist/:info_hash` +//! +//! It adds a torrent infohash to the whitelist. +//! +//! **Path parameters** +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `info_hash` | 40-char string | The Info Hash v1 | Yes | `5452869be36f9f3350ccee6b4544e7e76caaadab` +//! +//! **Example request** +//! +//! ```bash +//! curl -X POST "http://127.0.0.1:1212/api/v1/whitelist/5452869be36f9f3350ccee6b4544e7e76caaadab?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "status": "ok" +//! } +//! ``` +//! +//! # Remove a torrent from the whitelist +//! +//! `DELETE /whitelist/:info_hash` +//! +//! It removes a torrent infohash to the whitelist. +//! +//! **Path parameters** +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `info_hash` | 40-char string | The Info Hash v1 | Yes | `5452869be36f9f3350ccee6b4544e7e76caaadab` +//! +//! **Example request** +//! +//! ```bash +//! curl -X DELETE "http://127.0.0.1:1212/api/v1/whitelist/5452869be36f9f3350ccee6b4544e7e76caaadab?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "status": "ok" +//! } +//! ``` +//! +//! # Reload the whitelist +//! +//! It reloads the whitelist from the database. +//! +//! **Example request** +//! +//! ```bash +//! curl "http://127.0.0.1:1212/api/v1/whitelist/reload?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "status": "ok" +//! } +//! ``` pub mod handlers; pub mod responses; pub mod routes; diff --git a/src/servers/apis/v1/context/whitelist/responses.rs b/src/servers/apis/v1/context/whitelist/responses.rs index 06d4a9448..ce901c2f0 100644 --- a/src/servers/apis/v1/context/whitelist/responses.rs +++ b/src/servers/apis/v1/context/whitelist/responses.rs @@ -1,19 +1,24 @@ +//! API responses for the [`whitelist`](crate::servers::apis::v1::context::whitelist) +//! API context. use std::error::Error; use axum::response::Response; use crate::servers::apis::v1::responses::unhandled_rejection_response; +/// `500` error response when a torrent cannot be removed from the whitelist. #[must_use] pub fn failed_to_remove_torrent_from_whitelist_response(e: E) -> Response { unhandled_rejection_response(format!("failed to remove torrent from whitelist: {e}")) } +/// `500` error response when a torrent cannot be added to the whitelist. #[must_use] pub fn failed_to_whitelist_torrent_response(e: E) -> Response { unhandled_rejection_response(format!("failed to whitelist torrent: {e}")) } +/// `500` error response when the whitelist cannot be reloaded from the database. #[must_use] pub fn failed_to_reload_whitelist_response(e: E) -> Response { unhandled_rejection_response(format!("failed to reload whitelist: {e}")) diff --git a/src/servers/apis/v1/context/whitelist/routes.rs b/src/servers/apis/v1/context/whitelist/routes.rs index 06011b462..65d511341 100644 --- a/src/servers/apis/v1/context/whitelist/routes.rs +++ b/src/servers/apis/v1/context/whitelist/routes.rs @@ -1,3 +1,10 @@ +//! API routes for the [`whitelist`](crate::servers::apis::v1::context::whitelist) API context. +//! +//! - `POST /whitelist/:info_hash` +//! - `DELETE /whitelist/:info_hash` +//! - `GET /whitelist/reload` +//! +//! Refer to the [API endpoint documentation](crate::servers::apis::v1::context::torrent). use std::sync::Arc; use axum::routing::{delete, get, post}; @@ -6,6 +13,7 @@ use axum::Router; use super::handlers::{add_torrent_to_whitelist_handler, reload_whitelist_handler, remove_torrent_from_whitelist_handler}; use crate::tracker::Tracker; +/// It adds the routes to the router for the [`whitelist`](crate::servers::apis::v1::context::whitelist) API context. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { let prefix = format!("{prefix}/whitelist"); diff --git a/src/servers/apis/v1/middlewares/auth.rs b/src/servers/apis/v1/middlewares/auth.rs index f0c63250b..608a1b7d2 100644 --- a/src/servers/apis/v1/middlewares/auth.rs +++ b/src/servers/apis/v1/middlewares/auth.rs @@ -1,3 +1,26 @@ +//! Authentication middleware for the API. +//! +//! It uses a "token" GET param to authenticate the user. URLs must be of the +//! form: +//! +//! `http://:/api/v1/?token=`. +//! +//! > **NOTICE**: the token can be at any position in the URL, not just at the +//! > beginning or at the end. +//! +//! The token must be one of the `access_tokens` in the tracker +//! [HTTP API configuration](torrust_tracker_configuration::HttpApi). +//! +//! The configuration file `config.toml` contains a list of tokens: +//! +//! ```toml +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! ``` +//! +//! All the tokes have the same permissions, so it is not possible to have +//! different permissions for different tokens. The label is only used to +//! identify the token. use std::sync::Arc; use axum::extract::{Query, State}; @@ -9,13 +32,14 @@ use torrust_tracker_configuration::{Configuration, HttpApi}; use crate::servers::apis::v1::responses::unhandled_rejection_response; +/// Container for the `token` extracted from the query params. #[derive(Deserialize, Debug)] pub struct QueryParams { pub token: Option, } /// Middleware for authentication using a "token" GET param. -/// The token must be one of the tokens in the tracker HTTP API configuration. +/// The token must be one of the tokens in the tracker [HTTP API configuration](torrust_tracker_configuration::HttpApi). pub async fn auth( State(config): State>, Query(params): Query, @@ -35,7 +59,9 @@ where } enum AuthError { + /// Missing token for authentication. Unauthorized, + /// Token was provided but it is not valid. TokenNotValid, } @@ -52,11 +78,13 @@ fn authenticate(token: &str, http_api_config: &HttpApi) -> bool { http_api_config.contains_token(token) } +/// `500` error response returned when the token is missing. #[must_use] pub fn unauthorized_response() -> Response { unhandled_rejection_response("unauthorized".to_string()) } +/// `500` error response when the provided token is not valid. #[must_use] pub fn token_not_valid_response() -> Response { unhandled_rejection_response("token not valid".to_string()) diff --git a/src/servers/apis/v1/middlewares/mod.rs b/src/servers/apis/v1/middlewares/mod.rs index 0e4a05d59..141e3038a 100644 --- a/src/servers/apis/v1/middlewares/mod.rs +++ b/src/servers/apis/v1/middlewares/mod.rs @@ -1 +1,2 @@ +//! API middlewares. See [Axum middlewares](axum::middleware). pub mod auth; diff --git a/src/servers/apis/v1/mod.rs b/src/servers/apis/v1/mod.rs index e87984b8e..213ee9335 100644 --- a/src/servers/apis/v1/mod.rs +++ b/src/servers/apis/v1/mod.rs @@ -1,3 +1,21 @@ +//! The API version `v1`. +//! +//! The API is organized in the following contexts: +//! +//! Context | Description | Version +//! ---|---|--- +//! `Stats` | Tracker statistics | [`v1`](crate::servers::apis::v1::context::stats) +//! `Torrents` | Torrents | [`v1`](crate::servers::apis::v1::context::torrent) +//! `Whitelist` | Torrents whitelist | [`v1`](crate::servers::apis::v1::context::whitelist) +//! `Authentication keys` | Authentication keys | [`v1`](crate::servers::apis::v1::context::auth_key) +//! +//! > **NOTICE**: +//! - The authentication keys are only used by the HTTP tracker. +//! - The whitelist is only used when the tracker is running in `listed` or +//! `private_listed` mode. +//! +//! Refer to the [authentication middleware](crate::servers::apis::v1::middlewares::auth) +//! for more information about the authentication process. pub mod context; pub mod middlewares; pub mod responses; diff --git a/src/servers/apis/v1/responses.rs b/src/servers/apis/v1/responses.rs index 4a9c39bf9..ecaf90098 100644 --- a/src/servers/apis/v1/responses.rs +++ b/src/servers/apis/v1/responses.rs @@ -1,3 +1,4 @@ +//! Common responses for the API v1 shared by all the contexts. use axum::http::{header, StatusCode}; use axum::response::{IntoResponse, Response}; use serde::Serialize; @@ -22,6 +23,8 @@ use serde::Serialize; We can put the second level of validation in the application and domain services. */ +/// Response status used when requests have only two possible results +/// `Ok` or `Error` and no data is returned. #[derive(Serialize, Debug)] #[serde(tag = "status", rename_all = "snake_case")] pub enum ActionStatus<'a> { diff --git a/src/servers/apis/v1/routes.rs b/src/servers/apis/v1/routes.rs index d45319c4b..7b792f8a8 100644 --- a/src/servers/apis/v1/routes.rs +++ b/src/servers/apis/v1/routes.rs @@ -1,3 +1,4 @@ +//! Route initialization for the v1 API. use std::sync::Arc; use axum::Router; @@ -5,6 +6,12 @@ use axum::Router; use super::context::{auth_key, stats, torrent, whitelist}; use crate::tracker::Tracker; +/// Add the routes for the v1 API. +/// +/// > **NOTICE**: the old API endpoints without `v1` prefix are kept for +/// backward compatibility. For example, the `GET /api/stats` endpoint is +/// still available, but it is deprecated and will be removed in the future. +/// You should use the `GET /api/v1/stats` endpoint instead. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { // Without `v1` prefix. // We keep the old API endpoints without `v1` prefix for backward compatibility. diff --git a/src/servers/http/mod.rs b/src/servers/http/mod.rs index b8aa6b19f..4212f86c4 100644 --- a/src/servers/http/mod.rs +++ b/src/servers/http/mod.rs @@ -1,4 +1,4 @@ -//! Tracker HTTP/HTTPS Protocol: +//! Tracker HTTP/HTTPS Protocol. //! //! Original specification in BEP 3 (section "Trackers"): //! diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs index faabbe095..dd2e94660 100644 --- a/src/tracker/mod.rs +++ b/src/tracker/mod.rs @@ -79,7 +79,8 @@ //! //! let peer_ip = IpAddr::V4(Ipv4Addr::from_str("126.0.0.1").unwrap()); //! ``` -//! ```rust,ignore +//! +//! ```text //! let announce_data = tracker.announce(&info_hash, &mut peer, &peer_ip).await; //! ``` //! @@ -412,7 +413,7 @@ //! //! For example, the HTTP tracker would send an event like the following when it handles an `announce` request received from a peer using IP version 4. //! -//! ```rust,ignore +//! ```text //! tracker.send_stats_event(statistics::Event::Tcp4Announce).await //! ``` //! diff --git a/src/tracker/services/statistics/mod.rs b/src/tracker/services/statistics/mod.rs index ac3ba510e..3761e38de 100644 --- a/src/tracker/services/statistics/mod.rs +++ b/src/tracker/services/statistics/mod.rs @@ -12,7 +12,7 @@ //! - An statistics [`EventSender`](crate::tracker::statistics::EventSender) //! - An statistics [`Repo`](crate::tracker::statistics::Repo) //! -//! ```rust,ignore +//! ```text //! let (stats_event_sender, stats_repository) = factory(tracker_usage_statistics); //! ``` //! @@ -23,7 +23,7 @@ //! //! For example, if you send the event [`Event::Udp4Connect`](crate::tracker::statistics::Event::Udp4Connect): //! -//! ```rust,ignore +//! ```text //! let result = event_sender.send_event(Event::Udp4Connect).await; //! ``` //! diff --git a/src/tracker/statistics.rs b/src/tracker/statistics.rs index 03f4fc081..85cc4f255 100644 --- a/src/tracker/statistics.rs +++ b/src/tracker/statistics.rs @@ -62,17 +62,31 @@ pub enum Event { /// and also for each IP version used by the peers: IPv4 and IPv6. #[derive(Debug, PartialEq, Default)] pub struct Metrics { + /// Total number of TCP (HTTP tracker) connections from IPv4 peers. + /// Since the HTTP tracker spec does not require a handshake, this metric + /// increases for every HTTP request. pub tcp4_connections_handled: u64, + /// Total number of TCP (HTTP tracker) `announce` requests from IPv4 peers. pub tcp4_announces_handled: u64, + /// Total number of TCP (HTTP tracker) `scrape` requests from IPv4 peers. pub tcp4_scrapes_handled: u64, + /// Total number of TCP (HTTP tracker) connections from IPv6 peers. pub tcp6_connections_handled: u64, + /// Total number of TCP (HTTP tracker) `announce` requests from IPv6 peers. pub tcp6_announces_handled: u64, + /// Total number of TCP (HTTP tracker) `scrape` requests from IPv6 peers. pub tcp6_scrapes_handled: u64, + /// Total number of UDP (UDP tracker) connections from IPv4 peers. pub udp4_connections_handled: u64, + /// Total number of UDP (UDP tracker) `announce` requests from IPv4 peers. pub udp4_announces_handled: u64, + /// Total number of UDP (UDP tracker) `scrape` requests from IPv4 peers. pub udp4_scrapes_handled: u64, + /// Total number of UDP (UDP tracker) `connection` requests from IPv6 peers. pub udp6_connections_handled: u64, + /// Total number of UDP (UDP tracker) `announce` requests from IPv6 peers. pub udp6_announces_handled: u64, + /// Total number of UDP (UDP tracker) `scrape` requests from IPv6 peers. pub udp6_scrapes_handled: u64, } From 11d87317b3362ce41102b1b11ee8de9943b50d22 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 30 Mar 2023 20:02:00 +0100 Subject: [PATCH 10/28] docs: [#266] crate docs for servers::http mod --- cSpell.json | 2 + packages/configuration/src/lib.rs | 24 +- src/lib.rs | 19 +- src/servers/http/mod.rs | 309 +++++++++++++++++- src/servers/http/percent_encoding.rs | 63 +++- src/servers/http/server.rs | 50 ++- .../http/v1/extractors/announce_request.rs | 31 ++ .../http/v1/extractors/authentication_key.rs | 46 ++- .../http/v1/extractors/client_ip_sources.rs | 41 ++- src/servers/http/v1/extractors/mod.rs | 4 + .../http/v1/extractors/scrape_request.rs | 31 ++ src/servers/http/v1/handlers/announce.rs | 18 + src/servers/http/v1/handlers/common/auth.rs | 8 + src/servers/http/v1/handlers/common/mod.rs | 1 + .../http/v1/handlers/common/peer_ip.rs | 6 + src/servers/http/v1/handlers/mod.rs | 4 + src/servers/http/v1/handlers/scrape.rs | 14 + src/servers/http/v1/launcher.rs | 1 + src/servers/http/v1/mod.rs | 4 + src/servers/http/v1/query.rs | 60 +++- src/servers/http/v1/requests/announce.rs | 81 +++++ src/servers/http/v1/requests/mod.rs | 4 + src/servers/http/v1/requests/scrape.rs | 3 + src/servers/http/v1/responses/announce.rs | 191 ++++++++++- src/servers/http/v1/responses/error.rs | 31 +- src/servers/http/v1/responses/mod.rs | 4 + src/servers/http/v1/responses/scrape.rs | 37 +++ src/servers/http/v1/routes.rs | 5 + src/servers/http/v1/services/announce.rs | 20 ++ src/servers/http/v1/services/mod.rs | 7 + .../http/v1/services/peer_ip_resolver.rs | 75 ++++- src/servers/http/v1/services/scrape.rs | 24 ++ src/servers/mod.rs | 1 + src/servers/signals.rs | 1 + src/shared/bit_torrent/mod.rs | 68 ++++ src/tracker/mod.rs | 17 + src/tracker/torrent.rs | 2 - 37 files changed, 1269 insertions(+), 38 deletions(-) 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) From 7014a4645d0c71e503e5d789373f6e6bfc04d310 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 5 Apr 2023 13:48:20 +0100 Subject: [PATCH 11/28] docs: [#271] crate docs for servers::udp mod --- cSpell.json | 5 + src/lib.rs | 1 - src/servers/http/mod.rs | 4 +- src/servers/http/percent_encoding.rs | 2 +- src/servers/http/server.rs | 8 +- src/servers/http/v1/requests/announce.rs | 2 +- src/servers/signals.rs | 2 +- src/servers/udp/connection_cookie.rs | 73 +++ src/servers/udp/error.rs | 6 + src/servers/udp/handlers.rs | 68 ++- src/servers/udp/mod.rs | 647 +++++++++++++++++++++++ src/servers/udp/peer_builder.rs | 9 + src/servers/udp/request.rs | 11 + src/servers/udp/server.rs | 71 ++- tests/servers/api/v1/asserts.rs | 2 +- 15 files changed, 886 insertions(+), 25 deletions(-) diff --git a/cSpell.json b/cSpell.json index af0de7101..f07e2bfb4 100644 --- a/cSpell.json +++ b/cSpell.json @@ -1,6 +1,7 @@ { "words": [ "appuser", + "Arvid", "AUTOINCREMENT", "automock", "Avicora", @@ -21,6 +22,7 @@ "chrono", "clippy", "completei", + "connectionless", "dockerhub", "downloadedi", "filesd", @@ -47,6 +49,7 @@ "nanos", "nextest", "nocapture", + "Norberg", "numwant", "oneshot", "ostr", @@ -59,6 +62,7 @@ "reqwest", "rerequests", "rngs", + "routable", "rusqlite", "rustfmt", "Rustls", @@ -82,6 +86,7 @@ "Vagaa", "Vuze", "whitespaces", + "XBTT", "Xtorrent", "Xunlei", "xxxxxxxxxxxxxxxxxxxxd", diff --git a/src/lib.rs b/src/lib.rs index 36d1792d3..adcf3d1f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -428,7 +428,6 @@ //! - [BEP 15](https://www.bittorrent.org/beps/bep_0015.html): UDP Tracker Protocol for `BitTorrent` //! - [BEP 23](https://www.bittorrent.org/beps/bep_0023.html): Tracker Returns Compact Peer Lists //! - [BEP 27](https://www.bittorrent.org/beps/bep_0027.html): Private Torrents -//! - [BEP 41](https://www.bittorrent.org/beps/bep_0041.html): UDP Tracker Protocol Extensions //! - [BEP 48](https://www.bittorrent.org/beps/bep_0048.html): Tracker Protocol Extension: Scrape //! //! # Contributing diff --git a/src/servers/http/mod.rs b/src/servers/http/mod.rs index 78c086892..067e88fdd 100644 --- a/src/servers/http/mod.rs +++ b/src/servers/http/mod.rs @@ -43,7 +43,7 @@ //! //! 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` +//! [`info_hash`](crate::servers::http::v1::requests::announce::Announce::info_hash) | percent encoded of 20-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` @@ -220,7 +220,7 @@ //! //! 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` +//! [`info_hash`](crate::servers::http::v1::requests::scrape::Scrape::info_hashes) | percent encoded of 20-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. diff --git a/src/servers/http/percent_encoding.rs b/src/servers/http/percent_encoding.rs index b807e74c9..c8f0f7f12 100644 --- a/src/servers/http/percent_encoding.rs +++ b/src/servers/http/percent_encoding.rs @@ -19,7 +19,7 @@ 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. +/// [`InfoHash`](crate::shared::bit_torrent::info_hash::InfoHash) is a 20-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`. diff --git a/src/servers/http/server.rs b/src/servers/http/server.rs index 3008771ee..6a46b81df 100644 --- a/src/servers/http/server.rs +++ b/src/servers/http/server.rs @@ -44,15 +44,15 @@ pub enum Error { Error(String), // todo: refactor to use thiserror and add more variants for specific errors. } -/// A stopped HTTP server. +/// A HTTP server instance controller with no HTTP instance running. #[allow(clippy::module_name_repetitions)] pub type StoppedHttpServer = HttpServer>; -/// A running HTTP server. +/// A HTTP server instance controller with a running HTTP instance. #[allow(clippy::module_name_repetitions)] pub type RunningHttpServer = HttpServer>; -/// A HTTP running server controller. +/// A HTTP server instance controller. /// /// It's responsible for: /// @@ -83,12 +83,14 @@ pub struct Stopped { /// A running HTTP server state. pub struct Running { + /// The address where the server is bound. pub bind_addr: SocketAddr, task_killer: tokio::sync::oneshot::Sender, task: tokio::task::JoinHandle, } impl HttpServer> { + /// It creates a new `HttpServer` controller in `stopped` state. pub fn new(cfg: torrust_tracker_configuration::HttpTracker, launcher: I) -> Self { Self { cfg, diff --git a/src/servers/http/v1/requests/announce.rs b/src/servers/http/v1/requests/announce.rs index 3725ee1df..1cf632eb5 100644 --- a/src/servers/http/v1/requests/announce.rs +++ b/src/servers/http/v1/requests/announce.rs @@ -88,7 +88,7 @@ pub struct Announce { /// 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 +/// binary data. The `info_hash` is a 20-byte SHA1 hash and the `peer_id` is a /// 20-byte array. #[derive(Error, Debug)] pub enum ParseAnnounceQueryError { diff --git a/src/servers/signals.rs b/src/servers/signals.rs index 879a82d5e..f0312b886 100644 --- a/src/servers/signals.rs +++ b/src/servers/signals.rs @@ -1,4 +1,4 @@ -/// This module contains functions to handle signals. +//! This module contains functions to handle signals. use log::info; /// Resolves on `ctrl_c` or the `terminate` signal. diff --git a/src/servers/udp/connection_cookie.rs b/src/servers/udp/connection_cookie.rs index 4a75145c1..a389388a7 100644 --- a/src/servers/udp/connection_cookie.rs +++ b/src/servers/udp/connection_cookie.rs @@ -1,3 +1,71 @@ +//! Logic for generating and verifying connection IDs. +//! +//! The UDP tracker requires the client to connect to the server before it can +//! send any data. The server responds with a random 64-bit integer that the +//! client must use to identify itself. +//! +//! This connection ID is used to avoid spoofing attacks. The client must send +//! the connection ID in all requests to the server. The server will ignore any +//! requests that do not contain the correct connection ID. +//! +//! The simplest way to implement this would be to generate a random number when +//! the client connects and store it in a hash table. However, this would +//! require the server to store a large number of connection IDs, which would be +//! a waste of memory. Instead, the server generates a connection ID based on +//! the client's IP address and the current time. This allows the server to +//! verify the connection ID without storing it. +//! +//! This module implements this method of generating connection IDs. It's the +//! most common way to generate connection IDs. The connection ID is generated +//! using a time based algorithm and it is valid for a certain amount of time +//! (usually two minutes). The connection ID is generated using the following: +//! +//! ```text +//! connection ID = hash(client IP + current time slot + secret seed) +//! ``` +//! +//! Time slots are two minute intervals since the Unix epoch. The secret seed is +//! a random number that is generated when the server starts. And the client IP +//! is used in order generate a unique connection ID for each client. +//! +//! The BEP-15 recommends a two-minute time slot. +//! +//! ```text +//! Timestamp (seconds from Unix epoch): +//! |------------|------------|------------|------------| +//! 0 120 240 360 480 +//! Time slots (two-minutes time extents from Unix epoch): +//! |------------|------------|------------|------------| +//! 0 1 2 3 4 +//! Peer connections: +//! Peer A |-------------------------| +//! Peer B |-------------------------| +//! Peer C |------------------| +//! Peer A connects at timestamp 120 slot 1 -> connection ID will be valid from timestamp 120 to 360 +//! Peer B connects at timestamp 240 slot 2 -> connection ID will be valid from timestamp 240 to 480 +//! Peer C connects at timestamp 180 slot 1 -> connection ID will be valid from timestamp 180 to 360 +//! ``` +//! > **NOTICE**: connection ID is always the same for a given peer +//! (socket address) and time slot. +//! +//! > **NOTICE**: connection ID will be valid for two time extents, **not two +//! minutes**. It'll be valid for the the current time extent and the next one. +//! +//! Refer to [`Connect`](crate::servers::udp#connect) for more information about +//! the connection process. +//! +//! ## Advantages +//! +//! - It consumes less memory than storing a hash table of connection IDs. +//! - It's easy to implement. +//! - It's fast. +//! +//! ## Disadvantages +//! +//! - It's not very flexible. The connection ID is only valid for a certain +//! amount of time. +//! - It's not very accurate. The connection ID is valid for more than two +//! minutes. use std::net::SocketAddr; use std::panic::Location; @@ -12,16 +80,19 @@ pub type SinceUnixEpochTimeExtent = TimeExtent; pub const COOKIE_LIFETIME: TimeExtent = TimeExtent::from_sec(2, &60); +/// Converts a connection ID into a connection cookie. #[must_use] pub fn from_connection_id(connection_id: &ConnectionId) -> Cookie { connection_id.0.to_le_bytes() } +/// Converts a connection cookie into a connection ID. #[must_use] pub fn into_connection_id(connection_cookie: &Cookie) -> ConnectionId { ConnectionId(i64::from_le_bytes(*connection_cookie)) } +/// Generates a new connection cookie. #[must_use] pub fn make(remote_address: &SocketAddr) -> Cookie { let time_extent = cookie_builder::get_last_time_extent(); @@ -30,6 +101,8 @@ pub fn make(remote_address: &SocketAddr) -> Cookie { cookie_builder::build(remote_address, &time_extent) } +/// Checks if the supplied `connection_cookie` is valid. +/// /// # Panics /// /// It would panic if the `COOKIE_LIFETIME` constant would be an unreasonably large number. diff --git a/src/servers/udp/error.rs b/src/servers/udp/error.rs index a6381cc78..ce59cd015 100644 --- a/src/servers/udp/error.rs +++ b/src/servers/udp/error.rs @@ -1,24 +1,30 @@ +//! Error types for the UDP server. use std::panic::Location; use thiserror::Error; use torrust_tracker_located_error::LocatedError; +/// Error returned by the UDP server. #[derive(Error, Debug)] pub enum Error { + /// Error returned when the domain tracker returns an error. #[error("tracker server error: {source}")] TrackerError { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, }, + /// Error returned from a third-party library (aquatic_udp_protocol). #[error("internal server error: {message}, {location}")] InternalServer { location: &'static Location<'static>, message: String, }, + /// Error returned when the connection id could not be verified. #[error("connection id could not be verified")] InvalidConnectionId { location: &'static Location<'static> }, + /// Error returned when the request is invalid. #[error("bad request: {source}")] BadRequest { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, diff --git a/src/servers/udp/handlers.rs b/src/servers/udp/handlers.rs index e00203cfc..e94e0292f 100644 --- a/src/servers/udp/handlers.rs +++ b/src/servers/udp/handlers.rs @@ -1,3 +1,4 @@ +//! Handlers for the UDP server. use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::panic::Location; use std::sync::Arc; @@ -16,6 +17,15 @@ use crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS; use crate::shared::bit_torrent::info_hash::InfoHash; use crate::tracker::{statistics, Tracker}; +/// It handles the incoming UDP packets. +/// +/// It's responsible for: +/// +/// - Parsing the incoming packet. +/// - Delegating the request to the correct handler depending on the request +/// type. +/// +/// It will return an `Error` response if the request is invalid. pub async fn handle_packet(remote_addr: SocketAddr, payload: Vec, tracker: &Tracker) -> Response { match Request::from_bytes(&payload[..payload.len()], MAX_SCRAPE_TORRENTS).map_err(|e| Error::InternalServer { message: format!("{e:?}"), @@ -43,6 +53,8 @@ pub async fn handle_packet(remote_addr: SocketAddr, payload: Vec, tracker: & } } +/// It dispatches the request to the correct handler. +/// /// # Errors /// /// If a error happens in the `handle_request` function, it will just return the `ServerError`. @@ -54,17 +66,24 @@ pub async fn handle_request(request: Request, remote_addr: SocketAddr, tracker: } } +/// It handles the `Connect` request. Refer to [`Connect`](crate::servers::udp#connect) +/// request for more information. +/// /// # Errors /// -/// This function dose not ever return an error. +/// This function does not ever return an error. pub async fn handle_connect(remote_addr: SocketAddr, request: &ConnectRequest, tracker: &Tracker) -> Result { + debug!("udp connect request: {:#?}", request); + let connection_cookie = make(&remote_addr); let connection_id = into_connection_id(&connection_cookie); - let response = Response::from(ConnectResponse { + let response = ConnectResponse { transaction_id: request.transaction_id, connection_id, - }); + }; + + debug!("udp connect response: {:#?}", response); // send stats event match remote_addr { @@ -76,9 +95,12 @@ pub async fn handle_connect(remote_addr: SocketAddr, request: &ConnectRequest, t } } - Ok(response) + Ok(Response::from(response)) } +/// It authenticates the request. It returns an error if the peer is not allowed +/// to make the request. +/// /// # Errors /// /// Will return `Error` if unable to `authenticate_request`. @@ -91,6 +113,9 @@ pub async fn authenticate(info_hash: &InfoHash, tracker: &Tracker) -> Result<(), }) } +/// It handles the `Announce` request. Refer to [`Announce`](crate::servers::udp#announce) +/// request for more information. +/// /// # Errors /// /// If a error happens in the `handle_announce` function, it will just return the `ServerError`. @@ -124,8 +149,8 @@ pub async fn handle_announce( } #[allow(clippy::cast_possible_truncation)] - let announce_response = if remote_addr.is_ipv4() { - Response::from(AnnounceResponse { + if remote_addr.is_ipv4() { + let announce_response = AnnounceResponse { transaction_id: wrapped_announce_request.announce_request.transaction_id, announce_interval: AnnounceInterval(i64::from(tracker.config.announce_interval) as i32), leechers: NumberOfPeers(i64::from(response.swarm_stats.leechers) as i32), @@ -144,9 +169,13 @@ pub async fn handle_announce( } }) .collect(), - }) + }; + + debug!("udp announce response: {:#?}", announce_response); + + Ok(Response::from(announce_response)) } else { - Response::from(AnnounceResponse { + let announce_response = AnnounceResponse { transaction_id: wrapped_announce_request.announce_request.transaction_id, announce_interval: AnnounceInterval(i64::from(tracker.config.announce_interval) as i32), leechers: NumberOfPeers(i64::from(response.swarm_stats.leechers) as i32), @@ -165,16 +194,23 @@ pub async fn handle_announce( } }) .collect(), - }) - }; + }; + + debug!("udp announce response: {:#?}", announce_response); - Ok(announce_response) + Ok(Response::from(announce_response)) + } } +/// It handles the `Scrape` request. Refer to [`Scrape`](crate::servers::udp#scrape) +/// request for more information. +/// /// # Errors /// -/// This function dose not ever return an error. +/// This function does not ever return an error. pub async fn handle_scrape(remote_addr: SocketAddr, request: &ScrapeRequest, tracker: &Tracker) -> Result { + debug!("udp scrape request: {:#?}", request); + // Convert from aquatic infohashes let mut info_hashes = vec![]; for info_hash in &request.info_hashes { @@ -217,10 +253,14 @@ pub async fn handle_scrape(remote_addr: SocketAddr, request: &ScrapeRequest, tra } } - Ok(Response::from(ScrapeResponse { + let response = ScrapeResponse { transaction_id: request.transaction_id, torrent_stats, - })) + }; + + debug!("udp scrape response: {:#?}", response); + + Ok(Response::from(response)) } fn handle_error(e: &Error, transaction_id: TransactionId) -> Response { diff --git a/src/servers/udp/mod.rs b/src/servers/udp/mod.rs index 7b755a20b..edbfd77d2 100644 --- a/src/servers/udp/mod.rs +++ b/src/servers/udp/mod.rs @@ -1,3 +1,643 @@ +//! UDP Tracker. +//! +//! This module contains the UDP tracker implementation. +//! +//! The UDP tracker is a simple UDP server that responds to these requests: +//! +//! - `Connect`: used to get a connection ID which must be provided on each +//! request in order to avoid spoofing the source address of the UDP packets. +//! - `Announce`: used to announce the presence of a peer to the tracker. +//! - `Scrape`: used to get information about a torrent. +//! +//! It was introduced in [BEP 15. UDP Tracker Protocol for `BitTorrent`](https://www.bittorrent.org/beps/bep_0015.html) +//! as an alternative to the [HTTP tracker](https://www.bittorrent.org/beps/bep_0003.html). +//! The UDP tracker is more efficient than the HTTP tracker because it uses UDP +//! instead of TCP. +//! +//! Refer to the [`bit_torrent`](crate::shared::bit_torrent) module for more +//! information about the `BitTorrent` protocol. +//! +//! Refer to [BEP 15. UDP Tracker Protocol for `BitTorrent`](https://www.bittorrent.org/beps/bep_0015.html) +//! and to [BEP 41. UDP Tracker Protocol Extensions](https://www.bittorrent.org/beps/bep_0041.html) +//! for more information about the UDP tracker protocol. +//! +//! > **NOTICE**: [BEP-41](https://www.bittorrent.org/beps/bep_0041.html) is not +//! implemented yet. +//! +//! > **NOTICE**: we are using the [`aquatic_udp_protocol`](https://crates.io/crates/aquatic_udp_protocol) +//! crate so requests and responses are handled by it. +//! +//! > **NOTICE**: all values are send in network byte order ([big endian](https://en.wikipedia.org/wiki/Endianness)). +//! +//! ## Table of Contents +//! +//! - [Actions](#actions) +//! - [Connect](#connect) +//! - [Connect Request](#connect-request) +//! - [Connect Response](#connect-response) +//! - [Announce](#announce) +//! - [Announce Request](#announce-request) +//! - [Announce Response](#announce-response) +//! - [Scrape](#scrape) +//! - [Scrape Request](#scrape-request) +//! - [Scrape Response](#scrape-response) +//! - [Errors](#errors) +//! - [Extensions](#extensions) +//! - [Links](#links) +//! - [Credits](#credits) +//! +//! ## Actions +//! +//! Requests are sent to the tracker using UDP packets. The UDP tracker protocol +//! is designed to be as simple as possible. It uses a single UDP port and +//! supports only three types of requests: `Connect`, `Announce` and `Scrape`. +//! +//! Request are parsed from UDP packets using the [`aquatic_udp_protocol`](https://crates.io/crates/aquatic_udp_protocol) +//! crate and then handled by the [`Tracker`](crate::tracker::Tracker) struct. +//! And then the response is also build using the [`aquatic_udp_protocol`](https://crates.io/crates/aquatic_udp_protocol) +//! and converted to a UDP packet. +//! +//! ```text +//! UDP packet -> Aquatic Struct Request -> [Torrust Struct Request] -> Tracker -> Aquatic Struct Response -> UDP packet +//! ``` +//! +//! For the `Announce` request there is a wrapper struct [`AnnounceWrapper`](crate::servers::udp::request::AnnounceWrapper). +//! It was added to add an extra field with the internal [`InfoHash`](crate::shared::bit_torrent::info_hash::InfoHash) struct. +//! +//! ### Connect +//! +//! `Connect` requests are used to get a connection ID which must be provided on +//! each request in order to avoid spoofing the source address of the UDP. +//! +//! The connection ID is a random 64-bit integer that is used to identify the +//! client. It is used to prevent spoofing of the source address of the UDP +//! packets. Before announcing or scraping, you have to obtain a connection ID. +//! +//! The connection ID is generated by the tracker and sent back to the client's +//! IP address. Only the client using that IP can receive the response, so the +//! tracker can be sure that the client is the one who sent the request. If the +//! client's IP was spoofed the tracker will send the response to the wrong +//! client and the client will not receive it. +//! +//! The reason why the UDP tracker protocol needs a connection ID to avoid IP +//! spoofing can be explained as follows: +//! +//! 1. No connection state: Unlike TCP, UDP is a connectionless protocol, +//! meaning that it does not establish a connection between two endpoints before +//! exchanging data. As a result, it is more susceptible to IP spoofing, where +//! an attacker sends packets with a forged source IP address, tricking the +//! receiver into believing that they are coming from a legitimate source. +//! +//! 2. Mitigating IP spoofing: To mitigate IP spoofing in the UDP tracker +//! protocol, a connection ID is used. When a client wants to interact with a +//! tracker, it sends a "connect" request to the tracker, which, in turn, +//! responds with a unique connection ID. This connection ID must be included in +//! all subsequent requests from the client to the tracker. +//! +//! 3. Validating requests: By requiring the connection ID, the tracker can +//! verify that the requests are coming from the same client that initially sent +//! the "connect" request. If an attacker attempts to spoof the client's IP +//! address, they would also need to know the valid connection ID to be accepted +//! by the tracker. This makes it significantly more challenging for an attacker +//! to spoof IP addresses and disrupt the P2P network. +//! +//! There are different ways to generate a connection ID. The most common way is +//! to generate a time bound secret. The secret is generated using a time based +//! algorithm and it is valid for a certain amount of time. +//! +//! ```text +//! connection ID = hash(client IP + current time slot + secret seed) +//! ``` +//! +//! The BEP-15 recommends a two-minute time slot. Refer to [`connection_cookie`](crate::servers::udp::connection_cookie) +//! for more information about the connection ID generation with this method. +//! +//! #### Connect Request +//! +//! **Connect request (UDP packet)** +//! +//! Offset | Type/Size | Name | Description | Hex | Decimal +//! -------|-------------------|------------------|-------------------------------------------------|-----------------------------|----------------- +//! 0 | [`i64`](std::i64) | `protocol_id` | Magic constant that will identify the protocol. | `0x00_00_04_17_27_10_19_80` | `4497486125440` +//! 8 | [`i32`](std::i32) | `action` | Action identifying the connect request. | `0x00_00_00_00` | `0` +//! 12 | [`i32`](std::i32) | `transaction_id` | Randomly generated by the client. | `0x34_FA_A1_F9` | `-888840697` +//! +//! **Sample connect request (UDP packet)** +//! +//! UDP packet bytes: +//! +//! ```text +//! Offset: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] +//! Decimal: [ 0, 0, 4, 23, 39, 16, 25, 128, 0, 0, 0, 0, 203, 5, 94, 7] +//! Hex: [0x00, 0x00, 0x04, 0x17, 0x27, 0x10, 0x19, 0x80, 0x00, 0x00, 0x00, 0x00, 0xCB, 0x05, 0x5E, 0x07] +//! Param: [<------------- protocol_id ------------------>,<------- action ------>,<--- transaction_id -->] +//! ``` +//! +//! UDP packet fields: +//! +//! Offset | Type/Size | Name | Bytes Dec (Big Endian) | Hex | Decimal +//! -------|-------------------|------------------|--------------------------------|-----------------------------|---------------- +//! 0 | [`i64`](std::i64) | `protocol_id` | [0, 0, 4, 23, 39, 16, 25, 128] | `0x00_00_04_17_27_10_19_80` | `4497486125440` +//! 4 | [`i32`](std::i32) | `action` | [0, 0, 0, 0] | `0x00_00_00_00` | `0` +//! 8 | [`i32`](std::i32) | `transaction_id` | [35, 63, 226, 1] | `0xCB_05_5E_07` | `-888840697` +//! +//! **Connect request (parsed struct)** +//! +//! After parsing the UDP packet, the [`ConnectRequest`](aquatic_udp_protocol::request::ConnectRequest) +//! request struct will look like this: +//! +//! Field | Type | Example +//! -----------------|----------------------------------------------------------------|------------- +//! `transaction_id` | [`TransactionId`](aquatic_udp_protocol::common::TransactionId) | `1950635409` +//! +//! #### Connect Response +//! +//! **Connect response (UDP packet)** +//! +//! Offset | Type/Size | Name | Description | Hex | Decimal +//! -------|-------------------|------------------|-------------------------------------------------------|-----------------------------|----------------------- +//! 0 | [`i64`](std::i32) | `action` | Action identifying the connect request | `0x00_00_00_00` | `0` +//! 4 | [`i32`](std::i32) | `transaction_id` | Must match the `transaction_id` sent from the client. | `0xCB_05_5E_07` | `-888840697` +//! 8 | [`i32`](std::i64) | `connection_id` | Generated by the tracker to authenticate the client. | `0xC5_58_7C_09_08_48_D8_37` | `-4226491872051668937` +//! +//! > **NOTICE**: the `connection_id` is used when further information is +//! exchanged with the tracker, to identify the client. This `connection_id` can +//! be reused for multiple requests, but if it's cached for too long, it will +//! not be valid anymore. +//! +//! > **NOTICE**: `Hex` column is a signed 2's complement. +//! +//! **Sample connect response (UDP packet)** +//! +//! UDP packet bytes: +//! +//! ```text +//! Offset: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] +//! Decimal: [ 0, 0, 0, 0, 203, 5, 94, 7, 197, 88, 124, 9, 8, 72, 216, 55] +//! Hex: [0x00, 0x00, 0x00, 0x00, 0xCB, 0x05, 0x5E, 0x07, 0xC5, 0x58, 0x7C, 0x09, 0x08, 0x48, 0xD8, 0x37] +//! Param: [<------ action ------>,<-- transaction_id --->,<--------------- connection_id --------------->] +//! ``` +//! +//! UDP packet fields: +//! +//! Offset | Type/Size | Name | Bytes (Big Endian) | Hex | Decimal +//! -------|-------------------|------------------|-----------------------------------|------------------------------|----------------------- +//! 0 | [`i64`](std::i32) | `action` | [0, 0, 0, 0] | `0x00_00_00_00` | `0` +//! 4 | [`i64`](std::i32) | `transaction_id` | [203, 5, 94, 7] | `0xCB_05_5E_07` | `-888840697` +//! 8 | [`i64`](std::i64) | `connection_id` | [197, 88, 124, 9, 8, 72, 216, 55] | `0xC5_58_7C_09_08_48_D8_37` | `-4226491872051668937` +//! +//! > **NOTICE**: `Hex` column is a signed 2's complement. +//! +//! **Connect response (struct)** +//! +//! Before building the UDP packet, the [`ConnectResponse`](aquatic_udp_protocol::response::ConnectResponse) +//! struct will look like this: +//! +//! Field | Type | Example +//! -----------------|----------------------------------------------------------------|------------------------- +//! `connection_id` | [`ConnectionId`](aquatic_udp_protocol::common::ConnectionId) | `-4226491872051668937` +//! `transaction_id` | [`TransactionId`](aquatic_udp_protocol::common::TransactionId) | `-888840697` +//! +//! **Connect specification** +//! +//! Original specification in [BEP 15. UDP Tracker Protocol for `BitTorrent`](https://www.bittorrent.org/beps/bep_0015.html). +//! +//! ### 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 Request +//! +//! **Announce request (UDP packet)** +//! +//! Offset | Type/Size | Name | Description | Hex | Decimal +//! -------|-------------------|------------------|--------------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------------- +//! 0 | [`i64`](std::i64) | `connection_id` | The connection id acquired from establishing the connection. | `0xC5_58_7C_09_08_48_D8_37` | `-4226491872051668937` +//! 8 | [`i32`](std::i32) | `action` | Action for announce request. | `0x00_00_00_01` | `1` +//! 12 | [`i32`](std::i32) | `transaction_id` | Randomly generated by the client. | `0xA2_F9_54_48` | `-1560718264` +//! 16 | 20-byte | `info_hash` | The infohash of the torrent being announced. | `0x03_84_05_48_64_3A_F2_A7_B6_3A_9F_5C_BC_A3_48_BC_71_50_CA_3A` | `20071130873666512363095721859061691407221705274` +//! 36 | 20-byte | `peer_id` | The ID of the peer announcing the torrent. | `0x2D_71_42_34_34_31_30_2D_29_53_64_7E_64_65_34_78_4D_70_36_44` | `259430336069436570531165609119312093997849130564` +//! 56 | [`i64`](std::i64) | `downloaded` | The number of bytes the peer has downloaded so far. | `0x00_00_00_00_00_00_00_00` | `0` +//! 64 | [`i64`](std::i64) | `left` | The number of bytes left to download by the peer. | `0x00_00_00_00_00_00_00_00` | `0` +//! 72 | [`i64`](std::i64) | `uploaded` | The number of bytes the peer has uploaded so far. | `0x00_00_00_00_00_00_00_00` | `0` +//! 80 | [`i32`](std::i32) | `event` | The event the peer is reporting to the tracker. | `0x0`, `0x1`, `0x2`, `0x3` | `0`: none; `1`: completed; `2`: started; `3`: stopped +//! 84 | [`i32`](std::i32) | `IP address` | The peer IP. Ignored by the tracker. It uses the Sender's IP.| `0x00_00_00_00` | `0` +//! 88 | [`i32`](std::i32) | `key` | A unique key that is randomized by the client. | `0xEF_34_95_D6` | `-281766442` +//! 92 | [`i32`](std::i32) | `num_want` | The maximum number of peers the peer wants in the response. | `0x00_00_00_C8` | `200` +//! 96 | [`i16`](std::i16) | `port` | The port the peer is listening on. | `0x44_8C` | `17548` +//! +//! **Peer IP address** +//! +//! The peer IP address is always ignored by the tracker. It uses the sender's +//! IP address. +//! +//! _"Do note that most trackers will only honor the IP address field under +//! limited circumstances."_ ([BEP 15](https://www.bittorrent.org/beps/bep_0015.html)). +//! +//! Although not supported by this tracker a UDP tracker can use the IP address +//! provided by the peer in the announce request under specific circumstances +//! when it cannot rely on the source IP address of the incoming request. These +//! circumstances might include: +//! +//! 1. Network Address Translation (NAT): In cases where a peer is behind a NAT, +//! the private IP address of the peer is not directly routable over the +//! internet. The NAT device translates the private IP address to a public one +//! when sending packets to the tracker. The public IP address is what the +//! tracker sees as the source IP of the incoming request. However, if the peer +//! provides its private IP address in the announce request, the tracker can use +//! this information to facilitate communication between peers in the same +//! private network. +//! +//! 2. Proxy or VPN usage: If a peer uses a proxy or VPN service to connect to +//! the tracker, the source IP address seen by the tracker will be the one +//! assigned by the proxy or VPN server. In this case, if the peer provides its +//! actual IP address in the announce request, the tracker can use it to +//! establish a direct connection with other peers, bypassing the proxy or VPN +//! server. This might improve performance or help in cases where some peers +//! cannot connect to the proxy or VPN server. +//! +//! 3. Tracker is behind a NAT, firewall, proxy, VPN, or load balancer: In cases +//! where the tracker is behind a NAT, firewall, proxy, VPN, or load balancer, +//! the source IP address of the incoming request will be the public IP address +//! of the NAT, firewall, proxy, VPN, or load balancer. If the peer provides its +//! private IP address in the announce request, the tracker can use this +//! information to establish a direct connection with the peer. +//! +//! It's important to note that using the provided IP address can pose security +//! risks, as malicious peers might spoof their IP addresses in the announce +//! request to perform various types of attacks. +//! +//! > **NOTICE**: The current tracker behavior is to ignore the IP address +//! provided by the peer, and use the source IP address of the incoming request, +//! when the tracker is not running behind a proxy, and to use the right-most IP +//! address in the `X-Forwarded-For` header when the tracker is running behind a +//! proxy. +//! +//! > **NOTICE**: The tracker also changes the peer IP address to the tracker +//! external IP when the peer is using a loopback IP address. +//! +//! **Sample announce request (UDP packet)** +//! +//! Some values used in the sample request: +//! +//! - Infohash: `0x03840548643AF2A7B63A9F5CBCA348BC7150CA3A` +//! - Peer ID: `0x2D7142343431302D2953647E646534784D703644` +//! +//! UDP packet bytes: +//! +//! ```text +//! Offset: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100] +//! Decimal: [ 197, 88, 124, 9, 8, 72, 216, 55, 0, 0, 0, 1, 162, 249, 84, 72, 3, 132, 5, 72, 100, 58, 242, 167, 182, 58, 159, 92, 188, 163, 72, 188, 113, 80, 202, 58, 45, 113, 66, 52, 52, 49, 48, 45, 41, 83, 100, 126, 100, 101, 52, 120, 77, 112, 54, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 239, 52, 149, 214, 0, 0, 0, 200, 68, 140, 2, 1, 47] +//! Hex: [ 0xC5, 0x58, 0x7C, 0x09, 0x08, 0x48, 0xD8, 0x37, 0x00, 0x00, 0x00, 0x01, 0xA2, 0xF9, 0x54, 0x48, 0x03, 0x84, 0x05, 0x48, 0x64, 0x3A, 0xF2, 0xA7, 0xB6, 0x3A, 0x9F, 0x5C, 0xBC, 0xA3, 0x48, 0xBC, 0x71, 0x50, 0xCA, 0x3A, 0x2D, 0x71, 0x42, 0x34, 0x34, 0x31, 0x30, 0x2D, 0x29, 0x53, 0x64, 0x7E, 0x64, 0x65, 0x34, 0x78, 0x4D, 0x70, 0x36, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0xEF, 0x34, 0x95, 0xD6, 0x00, 0x00, 0x00, 0xC8, 0x44, 0x8C, 0x02, 0x01, 0x2F] +//! Param: [<--------------- connection_id --------------->,<--------- action ---->,<-- transaction_id --->,<--------------------------------------------------------- info_hash ------------------------------------------------->,<---------------------------------------------- peer_id -------------------------------------------------------------->,<------------------- downloaded -------------->,<-------------------- left ------------------->,<---------------- uploaded ------------------->,<-------- event ------>,<----- IP address ---->,<--------- key ------->,<------ num_want ----->,<-- port --><---- BEP 41 --->] +//! ``` +//! +//! UDP packet fields: +//! +//! Offset | Type/Size | Name | Bytes Dec (Big Endian) | Hex | Decimal +//! -------|-------------------|-------------------|--------------------------------------------------------------------------|-----------------------------------------------------------------|---------------------------------------------------- +//! 0 | [`i64`](std::i64) | `connection_id` | `[197,88,124,9,8,72,216,55]` | `0xC5_58_7C_09_08_48_D8_37` | `-4226491872051668937` +//! 8 | [`i32`](std::i32) | `action` | `[0,0,0,1]` | `0x00_00_00_01` | `1` +//! 12 | [`i32`](std::i32) | `transaction_id` | `[162,249,84,72]` | `0xA2_F9_54_48` | `-1560718264` +//! 16 | 20 bytes | `info_hash` | `[3,132,5,72,100,58,242,167,182,58,159,92,188,163,72,188,113,80,202,58]` | `0x03_84_05_48_64_3A_F2_A7_B6_3A_9F_5C_BC_A3_48_BC_71_50_CA_3A` | `20071130873666512363095721859061691407221705274` +//! 36 | 20 bytes | `peer_id` | `[45,113,66,52,52,49,48,45,41,83,100,126,100,101,52,120,77,112,54,68]` | `0x2D_71_42_34_34_31_30_2D_29_53_64_7E_64_65_34_78_4D_70_36_44` | `259430336069436570531165609119312093997849130564` +//! 56 | [`i64`](std::i64) | `downloaded` | `[0,0,0,0,0,0,0,0]` | `0x00_00_00_00_00_00_00_00` | `0` +//! 64 | [`i64`](std::i64) | `left` | `[0,0,0,0,0,0,0,0]` | `0x00_00_00_00_00_00_00_00` | `0` +//! 72 | [`i64`](std::i64) | `uploaded` | `[0,0,0,0,0,0,0,0]` | `0x00_00_00_00_00_00_00_00` | `0` +//! 80 | [`i32`](std::i32) | `event` | `[0,0,0,2]` | `0x00_00_00_02` | `2` (`Started`) +//! 84 | [`i32`](std::i32) | `IP address` | `[0,0,0,0]` | `0x00_00_00_00` | `0` +//! 88 | [`i32`](std::i32) | `key` | `[239,52,149,214]` | `0xEF_34_95_D6` | `-281766442` +//! 92 | [`i32`](std::i32) | `num_want` | `[0,0,0,200]` | `0x00_00_00_C8` | `200` +//! 96 | [`i16`](std::i16) | `port` | `[8,140]` | `0x44_8C` | `17548` +//! 98 | 1 byte | `Option-Type` | `[2]` | `0x02` | `2` +//! 99 | 2 byte | `Length Byte` | `[1,47]` | `0x01_2F` | `303` +//! 101 | N bytes | | | | +//! +//! > **NOTICE**: bytes after offset 98 are part of the [BEP-41. UDP Tracker Protocol Extensions](https://www.bittorrent.org/beps/bep_0041.html). +//! There are three options defined for byte 98: `0x0` (`EndOfOptions`), `0x1` (`NOP`) and `0x2` (`URLData`). +//! +//! > **NOTICE**: `num_want` is being ignored by the tracker. Refer to +//! [issue 262](https://github.com/torrust/torrust-tracker/issues/262) for more +//! information. +//! +//! **Announce request (parsed struct)** +//! +//! After parsing the UDP packet, the [`AnnounceRequest`](aquatic_udp_protocol::request::AnnounceRequest) +//! struct will contain the following fields: +//! +//! Field | Type | Example +//! -------------------|---------------------------------------------------------------- |-------------- +//! `connection_id` | [`ConnectionId`](aquatic_udp_protocol::common::ConnectionId) | `-4226491872051668937` +//! `transaction_id` | [`TransactionId`](aquatic_udp_protocol::common::TransactionId) | `-1560718264` +//! `info_hash` | [`InfoHash`](aquatic_udp_protocol::common::InfoHash) | `[3,132,5,72,100,58,242,167,182,58,159,92,188,163,72,188,113,80,202,58]` +//! `peer_id` | [`PeerId`](aquatic_udp_protocol::common::PeerId) | `[45,113,66,52,52,49,48,45,41,83,100,126,100,101,52,120,77,112,54,68]` +//! `bytes_downloaded` | [`NumberOfBytes`](aquatic_udp_protocol::common::NumberOfBytes) | `0` +//! `bytes_uploaded` | [`TransactionId`](aquatic_udp_protocol::common::NumberOfBytes) | `0` +//! `event` | [`AnnounceEvent`](aquatic_udp_protocol::request::AnnounceEvent) | `Started` +//! `ip_address` | [`Ipv4Addr`](aquatic_udp_protocol::common::ConnectionId) | `None` +//! `peers_wanted` | [`NumberOfPeers`](aquatic_udp_protocol::common::NumberOfPeers) | `200` +//! `port` | [`Port`](aquatic_udp_protocol::common::Port) | `17548` +//! +//! > **NOTICE**: the `peers_wanted` field is the `num_want` field in the UDP +//! packet. +//! +//! We are using a wrapper struct for the aquatic [`AnnounceRequest`](aquatic_udp_protocol::request::AnnounceRequest) +//! struct, because we have our internal [`InfoHash`](crate::shared::bit_torrent::info_hash::InfoHash) +//! struct. +//! +//! ```text +//! pub struct AnnounceWrapper { +//! pub announce_request: AnnounceRequest, // aquatic +//! pub info_hash: InfoHash, // our own +//! } +//! ``` +//! +//! #### Announce Response +//! +//! **Announce response (UDP packet)** +//! +//! Offset | Type/Size | Name | Description | Hex | Decimal +//! -----------|-------------------|------------------|---------------------------------------------------------------------------------|-----------------|---------------------------- +//! 0 | [`i32`](std::i32) | `action` | The action this is a reply to. | `0x00_00_00_01` | `1`: announce; `3`: error +//! 4 | [`i32`](std::i32) | `transaction_id` | Must match the `transaction_id` sent in the announce request. | `0x00_00_00_00` | `0` +//! 8 | [`i32`](std::i32) | `interval` | The number of seconds the peer should wait until re-announcing itself. | `0x00_00_00_00` | `0` +//! 12 | [`i32`](std::i32) | `leechers` | The number of peers in the swarm that has not finished downloading. | `0x00_00_00_00` | `0` +//! 16 | [`i32`](std::i32) | `seeders` | The number of peers in the swarm that has finished downloading and are seeding. | `0x00_00_00_00` | `0` +//! | | | | | +//! 20 + 6 * n | [`i32`](std::i32) | `IP address` | The IP of a peer in the swarm. | `0x69_69_69_69` | `1768515945` +//! 24 + 6 * n | [`i16`](std::i16) | `TCP port` | The peer's listen port. | `0x44_8C` | `17548` +//! 20 + 6 * N | | | | | +//! +//! > **NOTICE**: `Hex` column is a signed 2's complement. +//! +//! > **NOTICE**: `IP address` should always be set to 0 when the peer is using +//! `IPv6`. +//! +//! **Sample announce response (UDP packet)** +//! +//! UDP packet bytes (fixed part): +//! +//! ```text +//! Offset: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] +//! Decimal: [ 0, 0, 0, 1, 162, 249, 84, 72, 0, 0, 0, 120, 0, 0, 0, 0, 0, 0, 0, 1] +//! Hex: [ 0x00, 0x00, 0x00, 0x01, 0xA2, 0xF9, 0x54, 0x48, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01] +//! Param: [<------- action ------>,<-- transaction_id --->,<----- interval ------>,<----- leechers ------>,<------ seeders ------>] +//! ``` +//! +//! UDP packet fields (fixed part): +//! +//! Offset | Type/Size | Name | Bytes (Big Endian) | Hex | Decimal +//! -----------|-------------------|------------------|---------------------|-----------------|---------------------------- +//! 0 | [`i32`](std::i32) | `action` | `[0, 0, 0, 0]` | `0x00_00_00_01` | `1`: announce; `3`: error +//! 4 | [`i32`](std::i32) | `transaction_id` | `[162,249,84,72]` | `0xA2_F9_54_48` | `-1560718264` +//! 8 | [`i32`](std::i32) | `interval` | `[0,0,0,120]` | `0x00_00_00_78` | `120` +//! 12 | [`i32`](std::i32) | `leechers` | `[0, 0, 0, 0]` | `0x00_00_00_00` | `0` +//! 16 | [`i32`](std::i32) | `seeders` | `[0, 0, 0, 1]` | `0x00_00_00_01` | `1` +//! +//! This is the fixed part of the packet. After the fixed part there is +//! dynamically generated data with the list of peers in the swarm. The list may +//! include `IPv4` or `IPv6` peers, depending on the address family of the +//! underlying UDP packet. I.e. packets from a v4 address use the v4 format, +//! those from a v6 address use the v6 format. +//! +//! UDP packet bytes (`IPv4` peer list): +//! +//! ```text +//! Offset: [ 20, 21, 22, 23, 24, 25] +//! Decimal: [ 105, 105, 105, 105, 08, 140] +//! Hex: [ 0x69, 0x69, 0x69, 0x69, 0x44, 0x8C] +//! Param: [<----- IP address ---->,<-TCP port>] +//! ``` +//! +//! > **NOTICE**: there are 6 bytes per peer (4 bytes for the `IPv4` address and +//! 2 bytes for the TCP port). +//! +//! UDP packet fields (`IPv4` peer list): +//! +//! Offset | Type/Size | Name | Bytes (Big Endian) | Hex | Decimal +//! ---------|-------------------|--------------|---------------------|-----------------|---------------------------- +//! 20 + 6*n | [`i32`](std::i32) | `IP address` | `[105,105,105,105]` | `0x69_69_69_69` | `1768515945` +//! 24 + 6*n | [`i16`](std::i16) | `TCP port` | `[8,140]` | `0x44_8C` | `17548` +//! 20 + 6*N | | | | | +//! +//! UDP packet bytes (`IPv6` peer list): +//! +//! ```text +//! Offset: [ 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37] +//! Decimal: [ 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 105, 08, 140] +//! Hex: [ 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x69, 0x44, 0x8C] +//! Param: [<-------------------------------------------- IP address ------------------------------------->,<-TCP port>] +//! ``` +//! +//! > **NOTICE**: there are 18 bytes per peer (16 bytes for the `IPv6` address and +//! 2 bytes for the TCP port). +//! +//! UDP packet fields (`IPv6` peer list): +//! +//! Offset | Type/Size | Name | Bytes (Big Endian) | Hex | Decimal +//! ----------|---------------------|--------------|---------------------------------------------------------------------|-----------------------------------------------------|------------------------------------------- +//! 20 + 18*n | [`i128`](std::i128) | `IP address` | `[105,105,105,105,105,105,105,105,105,105,105,105,105,105,105,105]` | `0x69_69_69_69_69_69_69_69_69_69_69_69_69_69_69_69` | `140116268732151132014330720707198675305` +//! 24 + 18*n | [`i16`](std::i16) | `TCP port` | `[8,140]` | `0x44_8C` | `17548` +//! 20 + 18*N | | | | | +//! +//! > **NOTICE**: `Hex` column is a signed 2's complement. +//! +//! > **NOTICE**: the peer list does not include the peer that sent the announce +//! request. +//! +//! **Announce response (struct)** +//! +//! The [`AnnounceResponse`](aquatic_udp_protocol::response::AnnounceResponse) +//! struct will have the following fields: +//! +//! Field | Type | Example +//! --------------------|------------------------------------------------------------------------|-------------- +//! `transaction_id` | [`TransactionId`](aquatic_udp_protocol::common::TransactionId) | `-1560718264` +//! `announce_interval` | [`AnnounceInterval`](aquatic_udp_protocol::common::AnnounceInterval) | `120` +//! `leechers` | [`NumberOfPeers`](aquatic_udp_protocol::common::NumberOfPeers) | `0` +//! `seeders` | [`NumberOfPeers`](aquatic_udp_protocol::common::NumberOfPeers) | `1` +//! `peers` | Vector of [`ResponsePeer`](aquatic_udp_protocol::common::ResponsePeer) | `[]` +//! +//! **Announce specification** +//! +//! Original specification in [BEP 15. UDP Tracker Protocol for `BitTorrent`](https://www.bittorrent.org/beps/bep_0015.html). +//! +//! ### 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) +//! +//! > **NOTICE**: up to about 74 torrents can be scraped at once. A full scrape +//! can't be done with this protocol. This is a limitation of the UDP protocol. +//! 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. +//! +//! #### Scrape Request +//! +//! **Scrape request (UDP packet)** +//! +//! Offset | Type/Size | Name | Description | Hex | Decimal +//! ----------|-------------------|------------------|------------------------------------------------------------------------|-----------------------------------------------------------------|-------------------------------------------------- +//! 0 | [`i64`](std::i64) | `connection_id` | The `connection_id` retrieved from the establishing of the connection. | `0xC5_58_7C_09_08_48_D8_37` | `-4226491872051668937` +//! 8 | [`i32`](std::i32) | `action` | Action identifying the scrape request | `0x00_00_00_02` | `2` (`Scrape`) +//! 12 | [`i32`](std::i32) | `transaction_id` | Randomly generated by the client. | `0xA2_F9_54_48` | `-1560718264` +//! 16 + 20*n | 20 bytes | `info_hash` | The infohash of the torrent being scraped. | `0x03_84_05_48_64_3A_F2_A7_B6_3A_9F_5C_BC_A3_48_BC_71_50_CA_3A` | `20071130873666512363095721859061691407221705274` +//! 16 + 20*N | | | | +//! +//! The last field (`info_hash`) is repeated for each torrent being scraped. +//! +//! Dynamic part of the UDP packet: +//! +//! Offset | Type/Size | Name | Description | Hex | Decimal +//! ----------|-------------------|-------------|--------------------------------------------|-----------------------------------------------------------------|--------------------------------------------------- +//! 16 + 20*n | 20 bytes | `info_hash` | The infohash of the torrent being scraped. | `0x03_84_05_48_64_3A_F2_A7_B6_3A_9F_5C_BC_A3_48_BC_71_50_CA_3A` | `20071130873666512363095721859061691407221705274` +//! +//! **Sample scrape request (UDP packet)** +//! +//! UDP packet bytes (fixed part): +//! +//! ```text +//! Offset: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35] +//! Decimal: [ 197, 88, 124, 9, 8, 72, 216, 55, 0, 0, 0, 2, 162, 249, 84, 72, 3, 132, 5, 72, 100, 58, 242, 167, 182, 58, 159, 92, 188, 163, 72, 188, 113, 80, 202, 58] +//! Hex: [ 0xC5, 0x58, 0x7C, 0x09, 0x08, 0x48, 0xD8, 0x37, 0x00, 0x00, 0x00, 0x02, 0xA2, 0xF9, 0x54, 0x48, 0x03, 0x84, 0x05, 0x48, 0x64, 0x3A, 0xF2, 0xA7, 0xB6, 0x3A, 0x9F, 0x5C, 0xBC, 0xA3, 0x48, 0xBC, 0x71, 0x50, 0xCA, 0x3A] +//! Param: [<--------------- connection_id --------------->,<--------- action ---->,<-- transaction_id --->,<--------------------------------------------------------- info_hash ------------------------------------------------->] +//! ``` +//! +//! UDP packet bytes (infohash list): +//! +//! ```text +//! Offset: [ 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35] +//! Decimal: [ 3, 132, 5, 72, 100, 58, 242, 167, 182, 58, 159, 92, 188, 163, 72, 188, 113, 80, 202, 58] +//! Hex: [ 0x03, 0x84, 0x05, 0x48, 0x64, 0x3A, 0xF2, 0xA7, 0xB6, 0x3A, 0x9F, 0x5C, 0xBC, 0xA3, 0x48, 0xBC, 0x71, 0x50, 0xCA, 0x3A] +//! Param: [<--------------------------------------------------------- info_hash ------------------------------------------------->] +//! ``` +//! +//! UDP packet fields: +//! +//! Offset | Type/Size | Name | Bytes Dec (Big Endian) | Hex | Decimal +//! -------|-------------------|------------------|--------------------------------------------------------------------------|-----------------------------------------------------------------|-------------------------------------------------- +//! 0 | [`i64`](std::i64) | `connection_id` | `[197,88,124,9,8,72,216,55]` | `0xC5_58_7C_09_08_48_D8_37` | `-4226491872051668937` +//! 4 | [`i32`](std::i32) | `action` | `[0, 0, 0, 2]` | `0x00_00_00_02` | `2` (`Scrape`) +//! 8 | [`i32`](std::i32) | `transaction_id` | `[162,249,84,72]` | `0xA2_F9_54_48` | `-1560718264` +//! 8 | 20 bytes | `info_hash` | `[3,132,5,72,100,58,242,167,182,58,159,92,188,163,72,188,113,80,202,58]` | `0x03_84_05_48_64_3A_F2_A7_B6_3A_9F_5C_BC_A3_48_BC_71_50_CA_3A` | `20071130873666512363095721859061691407221705274` +//! +//! **Scrape request (parsed struct)** +//! +//! After parsing the UDP packet, the [`ScrapeRequest`](aquatic_udp_protocol::request::ScrapeRequest) +//! struct will look like this: +//! +//! Field | Type | Example +//! -----------------|----------------------------------------------------------------|---------------------------------------------------------------------------- +//! `connection_id` | [`ConnectionId`](aquatic_udp_protocol::common::ConnectionId) | `-4226491872051668937` +//! `transaction_id` | [`TransactionId`](aquatic_udp_protocol::common::TransactionId) | `-1560718264` +//! `info_hashes` | Vector of [`InfoHash`](aquatic_udp_protocol::common::InfoHash) | `[[3,132,5,72,100,58,242,167,182,58,159,92,188,163,72,188,113,80,202,58]]` +//! +//! #### Scrape Response +//! +//! **Scrape response (UDP packet)** +//! +//! Offset | Type/Size | Name (BEP15 or libtorrent) | Description | Hex | Decimal +//! ----------|-------------------|-----------------------------|-------------------------------------------------------|-----------------|----------------- +//! 0 | [`i32`](std::i32) | `action` | Action identifying the connect request | `0x00_00_00_00` | `2` (`Scrape`) +//! 4 | [`i32`](std::i32) | `transaction_id` | Must match the `transaction_id` sent from the client. | `0xA2_F9_54_48` | `-1560718264` +//! 8 + 12*n | [`i32`](std::i32) | `seeders` or `complete` | The current number of connected seeds. | `0x00_00_00_00` | `0` +//! 12 + 12*n | [`i32`](std::i32) | `completed` or `downloaded` | The number of times this torrent has been downloaded. | `0x00_00_00_00` | `0` +//! 16 + 12*n | [`i32`](std::i32) | `leechers` or `incomplete` | The current number of connected leechers. | `0x00_00_00_00` | `0` +//! 8 + 12*N | | | | | +//! +//! > **NOTICE**: `Hex` column is a signed 2's complement. +//! +//! Dynamic part of the UDP packet: +//! +//! Offset | Type/Size | Name (BEP15 or libtorrent) | Description | Hex | Decimal +//! ----------|-------------------|-----------------------------|-------------------------------------------------------|-----------------|----------------- +//! 8 + 12*n | [`i32`](std::i32) | `seeders` or `complete` | The current number of connected seeds. | `0x00_00_00_00` | `0` +//! 12 + 12*n | [`i32`](std::i32) | `completed` or `downloaded` | The number of times this torrent has been downloaded. | `0x00_00_00_00` | `0` +//! 16 + 12*n | [`i32`](std::i32) | `leechers` or `incomplete` | The current number of connected leechers. | `0x00_00_00_00` | `0` +//! 8 + 12*N | | | | | +//! +//! For each info hash in the request there will be 3 32-bit integers (12 bytes) +//! in the response with the number of seeders, leechers and downloads. +//! +//! **Sample scrape response (UDP packet)** +//! +//! UDP packet bytes: +//! +//! ```text +//! Offset: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] +//! Decimal: [ 0, 0, 0, 0, 203, 5, 94, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +//! Hex: [0x00, 0x00, 0x00, 0x00, 0xCB, 0x05, 0x5E, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] +//! Param: [<------ action ------>,<-- transaction_id --->,<------ seeders ------>,<----- completed ----->,<------ leechers ----->] +//! ``` +//! +//! UDP packet fields: +//! +//! Offset | Type/Size | Name | Bytes (Big Endian) | Hex | Decimal +//! -------|-------------------|------------------|--------------------|------------------|---------------- +//! 0 | [`i32`](std::i32) | `action` | [0, 0, 0, 2] | `0x00_00_00_02` | `2` (`Scrape`) +//! 4 | [`i32`](std::i32) | `transaction_id` | [203, 5, 94, 7] | `0xA2_F9_54_48` | `-1560718264` +//! 8 | [`i32`](std::i32) | `seeders` | [0, 0, 0, 0] | `0x00_00_00_00` | `0` +//! 12 | [`i32`](std::i32) | `completed` | [0, 0, 0, 0] | `0x00_00_00_00` | `0` +//! 16 | [`i32`](std::i32) | `leechers` | [0, 0, 0, 0] | `0x00_00_00_00` | `0` +//! +//! > **NOTICE**: `Hex` column is a signed 2's complement. +//! +//! **Scrape response (struct)** +//! +//! Before building the UDP packet, the [`ScrapeResponse`](aquatic_udp_protocol::response::ScrapeResponse) +//! struct will look like this: +//! +//! Field | Type | Example +//! -----------------|-------------------------------------------------------------------------------------------------|--------------- +//! `transaction_id` | [`TransactionId`](aquatic_udp_protocol::common::TransactionId) | `-1560718264` +//! `torrent_stats` | Vector of [`TorrentScrapeStatistics`](aquatic_udp_protocol::response::TorrentScrapeStatistics) | `[]` +//! +//! **Scrape specification** +//! +//! Original specification in [BEP 15. UDP Tracker Protocol for `BitTorrent`](https://www.bittorrent.org/beps/bep_0015.html). +//! +//! ## Errors +//! +//! ### Error Response +//! +//! **Error response (UDP packet)** +//! +//! Offset | Type/Size | Name | Description | Hex | Decimal +//! -------|-------------------|------------------|-------------------------------------------------------|-----------------------------|----------------------- +//! 0 | [`i32`](std::i32) | `action` | Action identifying the error response. | `0x00_00_00_03` | `3` +//! 4 | [`i32`](std::i32) | `transaction_id` | Must match the `transaction_id` sent from the client. | `0xCB_05_5E_07` | `-888840697` +//! 8 | N Bytes | `error_string` | Error description. | | +//! +//! ## Extensions +//! +//! Extensions described in [BEP 41. UDP Tracker Protocol Extensions](https://www.bittorrent.org/beps/bep_0041.html) +//! are not supported yet. +//! +//! ## Links +//! +//! - [BEP 15. UDP Tracker Protocol for `BitTorrent`](https://www.bittorrent.org/beps/bep_0015.html). +//! - [BEP 41. UDP Tracker Protocol Extensions](https://www.bittorrent.org/beps/bep_0041.html). +//! - [libtorrent - Bittorrent UDP-tracker protocol extension](https://www.rasterbar.com/products/libtorrent/udp_tracker_protocol.html). +//! - [XBTT Tracker. UDP tracker protocol](https://xbtt.sourceforge.net/udp_tracker_protocol.html). +//! - [Wikipedia: UDP tracker](https://en.wikipedia.org/wiki/UDP_tracker). +//! +//! ## Credits +//! +//! [Bittorrent UDP-tracker protocol extension](https://www.rasterbar.com/products/libtorrent/udp_tracker_protocol.html) +//! documentation by [Arvid Norberg](https://github.com/arvidn) was very +//! supportive in the development of this documentation. Some descriptions were +//! taken from the [libtorrent](https://www.rasterbar.com/products/libtorrent/udp_tracker_protocol.html). pub mod connection_cookie; pub mod error; pub mod handlers; @@ -5,9 +645,16 @@ pub mod peer_builder; pub mod request; pub mod server; +/// Number of bytes. pub type Bytes = u64; +/// The port the peer is listening on. pub type Port = u16; +/// The transaction id. A random number generated byt the peer that is used to +/// match requests and responses. pub type TransactionId = i64; +/// The maximum number of bytes in a UDP packet. pub const MAX_PACKET_SIZE: usize = 1496; +/// A magic 64-bit integer constant defined in the protocol that is used to +/// identify the protocol. pub const PROTOCOL_ID: i64 = 0x0417_2710_1980; diff --git a/src/servers/udp/peer_builder.rs b/src/servers/udp/peer_builder.rs index 8d8852dc7..ac62a7ecd 100644 --- a/src/servers/udp/peer_builder.rs +++ b/src/servers/udp/peer_builder.rs @@ -1,9 +1,18 @@ +//! Logic to extract the peer info from the announce request. use std::net::{IpAddr, SocketAddr}; use super::request::AnnounceWrapper; use crate::shared::clock::{Current, Time}; use crate::tracker::peer::{Id, Peer}; +/// Extracts the [`Peer`](crate::tracker::peer::Peer) info from the +/// announce request. +/// +/// # Arguments +/// +/// * `announce_wrapper` - The announce request to extract the peer info from. +/// * `peer_ip` - The real IP address of the peer, not the one in the announce +/// request. #[must_use] pub fn from_request(announce_wrapper: &AnnounceWrapper, peer_ip: &IpAddr) -> Peer { Peer { diff --git a/src/servers/udp/request.rs b/src/servers/udp/request.rs index 4be99e6d0..0afa02806 100644 --- a/src/servers/udp/request.rs +++ b/src/servers/udp/request.rs @@ -1,13 +1,24 @@ +//! UDP request types. +//! +//! Torrust Tracker uses the [`aquatic_udp_protocol`](https://crates.io/crates/aquatic_udp_protocol) +//! crate to parse and serialize UDP requests. +//! +//! Some of the type in this module are wrappers around the types in the +//! `aquatic_udp_protocol` crate. use aquatic_udp_protocol::AnnounceRequest; use crate::shared::bit_torrent::info_hash::InfoHash; +/// Wrapper around [`AnnounceRequest`](aquatic_udp_protocol::request::AnnounceRequest). pub struct AnnounceWrapper { + /// [`AnnounceRequest`](aquatic_udp_protocol::request::AnnounceRequest) to wrap. pub announce_request: AnnounceRequest, + /// Info hash of the torrent. pub info_hash: InfoHash, } impl AnnounceWrapper { + /// Creates a new [`AnnounceWrapper`] from an [`AnnounceRequest`]. #[must_use] pub fn new(announce_request: &AnnounceRequest) -> Self { AnnounceWrapper { diff --git a/src/servers/udp/server.rs b/src/servers/udp/server.rs index 9eb9836fe..a4f1faae8 100644 --- a/src/servers/udp/server.rs +++ b/src/servers/udp/server.rs @@ -1,3 +1,22 @@ +//! Module to handle the UDP server instances. +//! +//! There are two main types in this module: +//! +//! - [`UdpServer`](crate::servers::udp::server::UdpServer): a controller to +//! start and stop the server. +//! - [`Udp`](crate::servers::udp::server::Udp): the server launcher. +//! +//! The `UdpServer` is an state machine for a given configuration. This struct +//! represents concrete configuration and state. It allows to start and +//! stop the server but always keeping the same configuration. +//! +//! The `Udp` is the server launcher. It's responsible for launching the UDP +//! but without keeping any state. +//! +//! For the time being, the `UdpServer` is only used for testing purposes, +//! because we want to be able to start and stop the server multiple times, and +//! we want to know the bound address and the current state of the server. +//! In production, the `Udp` launcher is used directly. use std::future::Future; use std::io::Cursor; use std::net::SocketAddr; @@ -14,36 +33,76 @@ use crate::servers::udp::handlers::handle_packet; use crate::servers::udp::MAX_PACKET_SIZE; use crate::tracker::Tracker; +/// Error that can occur when starting or stopping the UDP server. +/// +/// Some errors triggered while starting the server are: +/// +/// - The server cannot bind to the given address. +/// - It cannot get the bound address. +/// +/// Some errors triggered while stopping the server are: +/// +/// - The [`UdpServer`](crate::servers::udp::server::UdpServer) cannot send the +/// shutdown signal to the spawned UDP service thread. #[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 UDP server instance controller with no UDP instance running. #[allow(clippy::module_name_repetitions)] pub type StoppedUdpServer = UdpServer; + +/// A UDP server instance controller with a running UDP instance. #[allow(clippy::module_name_repetitions)] pub type RunningUdpServer = UdpServer; +/// A UDP server instance 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 UdpServer { + /// The configuration of the server that will be used every time the server + /// is started. pub cfg: torrust_tracker_configuration::UdpTracker, + /// The state of the server: `running` or `stopped`. pub state: S, } +/// A stopped UDP server state. pub struct Stopped; +/// A running UDP server state. pub struct Running { + /// The address where the server is bound. pub bind_address: SocketAddr, stop_job_sender: tokio::sync::oneshot::Sender, job: JoinHandle<()>, } impl UdpServer { + /// Creates a new `UdpServer` instance in `stopped`state. #[must_use] pub fn new(cfg: torrust_tracker_configuration::UdpTracker) -> Self { Self { cfg, state: Stopped {} } } + /// It starts the server and returns a `UdpServer` controller in `running` + /// state. + /// /// # Errors /// /// Will return `Err` if UDP can't bind to given bind address. @@ -74,6 +133,9 @@ impl UdpServer { } impl UdpServer { + /// It stops the server and returns a `UdpServer` controller in `stopped` + /// state. + /// /// # Errors /// /// Will return `Err` if the oneshot channel to send the stop signal @@ -92,11 +154,14 @@ impl UdpServer { } } +/// A UDP server instance launcher. pub struct Udp { socket: Arc, } impl Udp { + /// Creates a new `Udp` instance. + /// /// # Errors /// /// Will return `Err` unable to bind to the supplied `bind_address`. @@ -108,6 +173,8 @@ impl Udp { }) } + /// It starts the UDP server instance. + /// /// # Panics /// /// It would panic if unable to resolve the `local_addr` from the supplied ´socket´. @@ -136,6 +203,8 @@ impl Udp { } } + /// It starts the UDP server instance with graceful shutdown. + /// /// # Panics /// /// It would panic if unable to resolve the `local_addr` from the supplied ´socket´. diff --git a/tests/servers/api/v1/asserts.rs b/tests/servers/api/v1/asserts.rs index 1b1f204a2..955293db1 100644 --- a/tests/servers/api/v1/asserts.rs +++ b/tests/servers/api/v1/asserts.rs @@ -134,6 +134,6 @@ async fn assert_unhandled_rejection(response: Response, reason: &str) { let response_text = response.text().await.unwrap(); assert!( response_text.contains(&reason_text), - ":\n response: `\"{response_text}\"`\n dose not contain: `\"{reason_text}\"`." + ":\n response: `\"{response_text}\"`\n does not contain: `\"{reason_text}\"`." ); } From ae5f6286a3486e9186ea27a54bee69503a5421a7 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 11 Apr 2023 18:26:43 +0100 Subject: [PATCH 12/28] docs: [#275] crate docs for configuration package --- packages/configuration/src/lib.rs | 322 +++++++++++++++++++++++++++++- src/servers/apis/mod.rs | 4 +- src/servers/apis/server.rs | 2 +- src/tracker/mod.rs | 2 +- 4 files changed, 325 insertions(+), 5 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index d5beca236..6b051b572 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -1,3 +1,231 @@ +//! Configuration data structures for [Torrust Tracker](https://docs.rs/torrust-tracker). +//! +//! This module contains the configuration data structures for the +//! Torrust Tracker, which is a `BitTorrent` tracker server. +//! +//! The configuration is loaded from a [TOML](https://toml.io/en/) file +//! `config.toml` in the project root folder or from an environment variable +//! with the same content as the file. +//! +//! When you run the tracker without a configuration file, a new one will be +//! created with the default values, but the tracker immediately exits. You can +//! then edit the configuration file and run the tracker again. +//! +//! Configuration can not only be loaded from a file, but also from environment +//! variable `TORRUST_TRACKER_CONFIG`. This is useful when running the tracker +//! in a Docker container or environments where you do not have a persistent +//! storage or you cannot inject a configuration file. Refer to +//! [`Torrust Tracker documentation`](https://docs.rs/torrust-tracker) for more +//! information about how to pass configuration to the tracker. +//! +//! # Table of contents +//! +//! - [Sections](#sections) +//! - [Port binding](#port-binding) +//! - [TSL support](#tsl-support) +//! - [Generating self-signed certificates](#generating-self-signed-certificates) +//! - [Default configuration](#default-configuration) +//! +//! ## Sections +//! +//! Each section in the toml structure is mapped to a data structure. For +//! example, the `[http_api]` section (configuration for the tracker HTTP API) +//! is mapped to the [`HttpApi`](HttpApi) structure. +//! +//! > **NOTICE**: some sections are arrays of structures. For example, the +//! > `[[udp_trackers]]` section is an array of [`UdpTracker`](UdpTracker) since +//! > you can have multiple running UDP trackers bound to different ports. +//! +//! Please refer to the documentation of each structure for more information +//! about each section. +//! +//! - [`Core configuration`](crate::Configuration) +//! - [`HTTP API configuration`](crate::HttpApi) +//! - [`HTTP Tracker configuration`](crate::HttpTracker) +//! - [`UDP Tracker configuration`](crate::UdpTracker) +//! +//! ## Port binding +//! +//! For the API, HTTP and UDP trackers you can bind to a random port by using +//! port `0`. For example, if you want to bind to a random port on all +//! interfaces, use `0.0.0.0:0`. The OS will choose a random port but the +//! tracker will not print the port it is listening to when it starts. It just +//! says `Starting Torrust HTTP tracker server on: http://0.0.0.0:0`. It shows +//! the port used in the configuration file, and not the port the +//! tracker is actually listening to. This is a planned feature, see issue +//! [186](https://github.com/torrust/torrust-tracker/issues/186) for more +//! information. +//! +//! ## TSL support +//! +//! For the API and HTTP tracker you can enable TSL by setting `ssl_enabled` to +//! `true` and setting the paths to the certificate and key files. +//! +//! Typically, you will have a directory structure like this: +//! +//! ```text +//! storage/ +//! ├── database +//! │ └── data.db +//! └── ssl_certificates +//! ├── localhost.crt +//! └── localhost.key +//! ``` +//! +//! where you can store all the persistent data. +//! +//! Alternatively, you could setup a reverse proxy like Nginx or Apache to +//! handle the SSL/TLS part and forward the requests to the tracker. If you do +//! that, you should set [`on_reverse_proxy`](crate::Configuration::on_reverse_proxy) +//! to `true` in the configuration file. It's out of scope for this +//! documentation to explain in detail how to setup a reverse proxy, but the +//! configuration file should be something like this: +//! +//! For [NGINX](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/): +//! +//! ```text +//! # HTTPS only (with SSL - force redirect to HTTPS) +//! +//! server { +//! listen 80; +//! server_name tracker.torrust.com; +//! +//! return 301 https://$host$request_uri; +//! } +//! +//! server { +//! listen 443; +//! server_name tracker.torrust.com; +//! +//! ssl_certificate CERT_PATH +//! ssl_certificate_key CERT_KEY_PATH; +//! +//! location / { +//! proxy_set_header X-Forwarded-For $remote_addr; +//! proxy_pass http://127.0.0.1:6969; +//! } +//! } +//! ``` +//! +//! For [Apache](https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html): +//! +//! ```text +//! # HTTPS only (with SSL - force redirect to HTTPS) +//! +//! +//! ServerAdmin webmaster@tracker.torrust.com +//! ServerName tracker.torrust.com +//! +//! +//! RewriteEngine on +//! RewriteCond %{HTTPS} off +//! RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] +//! +//! +//! +//! +//! +//! ServerAdmin webmaster@tracker.torrust.com +//! ServerName tracker.torrust.com +//! +//! +//! Order allow,deny +//! Allow from all +//! +//! +//! ProxyPreserveHost On +//! ProxyRequests Off +//! AllowEncodedSlashes NoDecode +//! +//! ProxyPass / http://localhost:3000/ +//! ProxyPassReverse / http://localhost:3000/ +//! ProxyPassReverse / http://tracker.torrust.com/ +//! +//! RequestHeader set X-Forwarded-Proto "https" +//! RequestHeader set X-Forwarded-Port "443" +//! +//! ErrorLog ${APACHE_LOG_DIR}/tracker.torrust.com-error.log +//! CustomLog ${APACHE_LOG_DIR}/tracker.torrust.com-access.log combined +//! +//! SSLCertificateFile CERT_PATH +//! SSLCertificateKeyFile CERT_KEY_PATH +//! +//! +//! ``` +//! +//! ## Generating self-signed certificates +//! +//! For testing purposes, you can use self-signed certificates. +//! +//! Refer to [Let's Encrypt - Certificates for localhost](https://letsencrypt.org/docs/certificates-for-localhost/) +//! for more information. +//! +//! Running the following command will generate a certificate (`localhost.crt`) +//! and key (`localhost.key`) file in your current directory: +//! +//! ```s +//! openssl req -x509 -out localhost.crt -keyout localhost.key \ +//! -newkey rsa:2048 -nodes -sha256 \ +//! -subj '/CN=localhost' -extensions EXT -config <( \ +//! printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") +//! ``` +//! +//! You can then use the generated files in the configuration file: +//! +//! ```s +//! [[http_trackers]] +//! enabled = true +//! ... +//! ssl_cert_path = "./storage/ssl_certificates/localhost.crt" +//! ssl_key_path = "./storage/ssl_certificates/localhost.key" +//! +//! [http_api] +//! enabled = true +//! ... +//! ssl_cert_path = "./storage/ssl_certificates/localhost.crt" +//! ssl_key_path = "./storage/ssl_certificates/localhost.key" +//! ``` +//! +//! ## Default configuration +//! +//! The default configuration is: +//! +//! ```toml +//! log_level = "info" +//! mode = "public" +//! db_driver = "Sqlite3" +//! db_path = "./storage/database/data.db" +//! announce_interval = 120 +//! min_announce_interval = 120 +//! max_peer_timeout = 900 +//! on_reverse_proxy = false +//! external_ip = "0.0.0.0" +//! tracker_usage_statistics = true +//! persistent_torrent_completed_stat = false +//! inactive_peer_cleanup_interval = 600 +//! remove_peerless_torrents = true +//! +//! [[udp_trackers]] +//! enabled = false +//! bind_address = "0.0.0.0:6969" +//! +//! [[http_trackers]] +//! enabled = false +//! bind_address = "0.0.0.0:7070" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! +//! [http_api] +//! enabled = true +//! bind_address = "127.0.0.1:1212" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//!``` use std::collections::{HashMap, HashSet}; use std::net::IpAddr; use std::panic::Location; @@ -14,38 +242,67 @@ use thiserror::Error; use torrust_tracker_located_error::{Located, LocatedError}; use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; +/// Configuration for each UDP tracker. #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct UdpTracker { + /// Weather the UDP tracker is enabled or not. pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. pub bind_address: String, } +/// Configuration for each HTTP tracker. #[serde_as] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpTracker { + /// Weather the HTTP tracker is enabled or not. pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. pub bind_address: String, + /// Weather the HTTP tracker will use SSL or not. pub ssl_enabled: bool, + /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. #[serde_as(as = "NoneAsEmptyString")] pub ssl_cert_path: Option, + /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. #[serde_as(as = "NoneAsEmptyString")] pub ssl_key_path: Option, } +/// Configuration for the HTTP API. #[serde_as] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] pub struct HttpApi { + /// Weather the HTTP API is enabled or not. pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. pub bind_address: String, + /// Weather the HTTP API will use SSL or not. pub ssl_enabled: bool, + /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. #[serde_as(as = "NoneAsEmptyString")] pub ssl_cert_path: Option, + /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. #[serde_as(as = "NoneAsEmptyString")] pub ssl_key_path: Option, + /// Access tokens for the HTTP API. The key is a label identifying the + /// token and the value is the token itself. The token is used to + /// authenticate the user. All tokens are valid for all endpoints and have + /// the all permissions. pub access_tokens: HashMap, } impl HttpApi { + /// Checks if the given token is one of the token in the configuration. #[must_use] pub fn contains_token(&self, token: &str) -> bool { let tokens: HashMap = self.access_tokens.clone(); @@ -54,14 +311,24 @@ impl HttpApi { } } +/// Core configuration for the tracker. #[allow(clippy::struct_excessive_bools)] #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] pub struct Configuration { + /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, + /// `Debug` and `Trace`. Default is `Info`. pub log_level: Option, + /// Tracker mode. See [`TrackerMode`](torrust_tracker_primitives::TrackerMode) for more information. pub mode: TrackerMode, // Database configuration + /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. pub db_driver: DatabaseDriver, + /// Database connection string. The format depends on the database driver. + /// For `Sqlite3`, the format is `path/to/database.db`, for example: + /// `./storage/database/data.db`. + /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for + /// example: `root:password@localhost:3306/torrust`. pub db_path: String, /// Interval in seconds that the client should wait between sending regular @@ -88,35 +355,77 @@ pub struct Configuration { /// 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, + /// Weather the tracker is behind a reverse proxy or not. + /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header + /// sent from the proxy will be used to get the client's IP address. pub on_reverse_proxy: bool, + /// The external IP address of the tracker. If the client is using a + /// loopback IP address, this IP address will be used instead. If the peer + /// is using a loopback IP address, the tracker assumes that the peer is + /// in the same network as the tracker and will use the tracker's IP + /// address instead. pub external_ip: Option, + /// Weather the tracker should collect statistics about tracker usage. + /// If enabled, the tracker will collect statistics like the number of + /// connections handled, the number of announce requests handled, etc. + /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more + /// information about the collected metrics. pub tracker_usage_statistics: bool, + /// If enabled the tracker will persist the number of completed downloads. + /// That's how many times a torrent has been downloaded completely. pub persistent_torrent_completed_stat: bool, // Cleanup job configuration + /// Maximum time in seconds that a peer can be inactive before being + /// considered an inactive peer. If a peer is inactive for more than this + /// time, it will be removed from the torrent peer list. pub max_peer_timeout: u32, + /// Interval in seconds that the cleanup job will run to remove inactive + /// peers from the torrent peer list. pub inactive_peer_cleanup_interval: u64, + /// If enabled, the tracker will remove torrents that have no peers. + /// THe clean up torrent job runs every `inactive_peer_cleanup_interval` + /// seconds and it removes inactive peers. Eventually, the peer list of a + /// torrent could be empty and the torrent will be removed if this option is + /// enabled. pub remove_peerless_torrents: bool, // Server jobs configuration + /// The list of UDP trackers the tracker is running. Each UDP tracker + /// represents a UDP server that the tracker is running and it has its own + /// configuration. pub udp_trackers: Vec, + /// The list of HTTP trackers the tracker is running. Each HTTP tracker + /// represents a HTTP server that the tracker is running and it has its own + /// configuration. pub http_trackers: Vec, + /// The HTTP API configuration. pub http_api: HttpApi, } +/// Errors that can occur when loading the configuration. #[derive(Error, Debug)] pub enum Error { + /// Unable to load the configuration from the environment variable. + /// This error only occurs if there is no configuration file and the + /// `TORRUST_TRACKER_CONFIG` environment variable is not set. #[error("Unable to load from Environmental Variable: {source}")] UnableToLoadFromEnvironmentVariable { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, }, + /// If you run the tracker without providing the configuration (via the + /// `TORRUST_TRACKER_CONFIG` environment variable or configuration file), + /// the tracker will create a default configuration file but it will not + /// load it. It will return this error instead and you have to restart the + /// it. #[error("Default configuration created at: `{path}`, please review and reload tracker, {location}")] CreatedNewConfigHalt { location: &'static Location<'static>, path: String, }, + /// Unable to load the configuration from the configuration file. #[error("Failed processing the configuration: {source}")] ConfigError { source: LocatedError<'static, ConfigError> }, } @@ -176,6 +485,8 @@ impl Default for Configuration { } impl Configuration { + /// Returns the tracker public IP address id defined in the configuration, + /// and `None` otherwise. #[must_use] pub fn get_ext_ip(&self) -> Option { match &self.external_ip { @@ -187,6 +498,8 @@ impl Configuration { } } + /// Loads the configuration from the configuration file. + /// /// # Errors /// /// Will return `Err` if `path` does not exist or has a bad configuration. @@ -214,6 +527,10 @@ impl Configuration { Ok(torrust_config) } + /// Loads the configuration from the environment variable. The whole + /// configuration must be in the environment variable. It contains the same + /// configuration as the configuration file with the same format. + /// /// # Errors /// /// Will return `Err` if the environment variable does not exist or has a bad configuration. @@ -232,10 +549,13 @@ impl Configuration { } } + /// Saves the configuration to the configuration file. + /// /// # Errors /// /// Will return `Err` if `filename` does not exist or the user does not have - /// permission to read it. + /// permission to read it. Will also return `Err` if the configuration is + /// not valid or cannot be encoded to TOML. pub fn save_to_file(&self, path: &str) -> Result<(), Error> { let toml_string = toml::to_string(self).expect("Could not encode TOML value"); fs::write(path, toml_string).expect("Could not write to file!"); diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index 203f1d146..eb278bf3c 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -35,7 +35,7 @@ //! admin = "MyAccessToken" //! ``` //! -//! Refer to [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration>) +//! Refer to [`torrust-tracker-configuration`](torrust_tracker_configuration) //! for more information about the API configuration. //! //! When you run the tracker with enabled API, you will see the following message: @@ -99,7 +99,7 @@ //! The token label is used to identify the token. All tokens have full access //! to the API. //! -//! Refer to [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration>) +//! Refer to [`torrust-tracker-configuration`](torrust_tracker_configuration) //! for more information about the API configuration and to the //! [`auth`](crate::servers::apis::v1::middlewares::auth) middleware for more //! information about the authentication process. diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index 76396cc51..91821aa79 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -140,7 +140,7 @@ impl Launcher { /// Starts the API server with graceful shutdown. /// /// If TLS is enabled in the configuration, it will start the server with - /// TLS. See [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration>) + /// TLS. See [`torrust-tracker-configuration`](torrust_tracker_configuration) /// for more information about configuration. pub fn start( cfg: &torrust_tracker_configuration::HttpApi, diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs index 03853e1aa..2cabd5a82 100644 --- a/src/tracker/mod.rs +++ b/src/tracker/mod.rs @@ -468,7 +468,7 @@ use crate::tracker::databases::Database; /// Typically, the `Tracker` is used by a higher application service that handles /// the network layer. pub struct Tracker { - /// `Tracker` configuration. See + /// `Tracker` configuration. See [`torrust-tracker-configuration`](torrust_tracker_configuration) pub config: Arc, /// A database driver implementation: [`Sqlite3`](crate::tracker::databases::sqlite) /// or [`MySQL`](crate::tracker::databases::mysql) From a109168cd30a05648bec5d6be643e9525d76032f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 12 Apr 2023 07:00:27 +0100 Subject: [PATCH 13/28] feat: dependabot workflow To open pull requests automatically to keep the dependencies up-to-date . --- .github/dependabot.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..8f36cb692 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + target-branch: "develop" + + - package-ecosystem: cargo + directory: / + schedule: + interval: daily + target-branch: "develop" From 117dc0bbfe59ecac7ecf94751dd01d962c785c99 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 11 Apr 2023 19:34:53 +0100 Subject: [PATCH 14/28] docs: [#275] crate docs for locate-error package --- packages/located-error/src/lib.rs | 39 ++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/located-error/src/lib.rs b/packages/located-error/src/lib.rs index d45517e5a..67c432528 100644 --- a/packages/located-error/src/lib.rs +++ b/packages/located-error/src/lib.rs @@ -1,11 +1,44 @@ -// https://stackoverflow.com/questions/74336993/getting-line-numbers-with-when-using-boxdyn-stderrorerror - +//! This crate provides a wrapper around an error that includes the location of +//! the error. +//! +//! ```rust +//! use std::error::Error; +//! use std::panic::Location; +//! use std::sync::Arc; +//! use torrust_tracker_located_error::{Located, LocatedError}; +//! +//! #[derive(thiserror::Error, Debug)] +//! enum TestError { +//! #[error("Test")] +//! Test, +//! } +//! +//! #[track_caller] +//! fn get_caller_location() -> Location<'static> { +//! *Location::caller() +//! } +//! +//! let e = TestError::Test; +//! +//! let b: LocatedError = Located(e).into(); +//! let l = get_caller_location(); +//! +//! assert!(b.to_string().contains("Test, src/lib.rs")); +//! ``` +//! +//! # Credits +//! +//! use std::error::Error; use std::panic::Location; use std::sync::Arc; +/// A generic wrapper around an error. +/// +/// Where `E` is the inner error (source error). pub struct Located(pub E); +/// A wrapper around an error that includes the location of the error. #[derive(Debug)] pub struct LocatedError<'a, E> where @@ -78,7 +111,7 @@ mod tests { use std::panic::Location; use super::LocatedError; - use crate::located_error::Located; + use crate::Located; #[derive(thiserror::Error, Debug)] enum TestError { From cb6fc79e316528e2f1ffd807bdb3e46c255d78ee Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 12 Apr 2023 12:10:05 +0100 Subject: [PATCH 15/28] fix: update dependencies --- Cargo.lock | 601 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 337 insertions(+), 264 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3bc78bd67..d7053d618 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,13 +81,13 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-trait" -version = "0.1.66" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] @@ -98,9 +98,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.10" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8582122b8edba2af43eaf6b80dbfd33f421b5a0eb3a3113d21bc096ac5b44faf" +checksum = "3b32c5ea3aabaf4deb5f5ced2d688ec0844c881c9e6c696a8b769a05fc691e62" dependencies = [ "async-trait", "axum-core", @@ -124,7 +124,6 @@ dependencies = [ "sync_wrapper", "tokio", "tower", - "tower-http", "tower-layer", "tower-service", ] @@ -142,9 +141,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f958c80c248b34b9a877a643811be8dbca03ca5ba827f2b63baf3a81e5fc4e" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", "bytes", @@ -159,9 +158,9 @@ dependencies = [ [[package]] name = "axum-server" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e4a990e1593e286b1b96e6df76da9dbcb84945a810287ca8101f1a4f000f61" +checksum = "bace45b270e36e3c27a190c65883de6dfc9f1d18c829907c127464815dc67b24" dependencies = [ "arc-swap", "bytes", @@ -278,9 +277,9 @@ dependencies = [ [[package]] name = "borsh" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40f9ca3698b2e4cb7c15571db0abc5551dca417a21ae8140460b50309bb2cc62" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", "hashbrown 0.13.2", @@ -288,37 +287,37 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598b3eacc6db9c3ee57b22707ad8f6a8d2f6d442bfe24ffeb8cbb70ca59e6a35" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" dependencies = [ "borsh-derive-internal", "borsh-schema-derive-internal", "proc-macro-crate", "proc-macro2", - "syn", + "syn 1.0.109", ] [[package]] name = "borsh-derive-internal" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186b734fa1c9f6743e90c95d7233c9faab6360d1a96d4ffa19d9cfd1e9350f8a" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "borsh-schema-derive-internal" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b7ff1008316626f485991b960ade129253d4034014616b94f309a15366cc49" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -352,7 +351,7 @@ checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -390,9 +389,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" dependencies = [ "iana-time-zone", "js-sys", @@ -406,9 +405,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ed9a53e5d4d9c573ae844bfac6872b159cb1d1585a83b29e7a64b7eef7332a" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" dependencies = [ "glob", "libc", @@ -417,9 +416,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.49" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" dependencies = [ "cc", ] @@ -471,15 +470,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" dependencies = [ "libc", ] @@ -509,9 +508,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", @@ -572,9 +571,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a140f260e6f3f79013b8bfc65e7ce630c9ab4388c6a89c71e07226f49487b72" +checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" dependencies = [ "cc", "cxxbridge-flags", @@ -584,9 +583,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da6383f459341ea689374bf0a42979739dc421874f112ff26f829b8040b8e613" +checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" dependencies = [ "cc", "codespan-reporting", @@ -594,24 +593,24 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 2.0.14", ] [[package]] name = "cxxbridge-flags" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90201c1a650e95ccff1c8c0bb5a343213bdd317c6e600a93075bca2eff54ec97" +checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" [[package]] name = "cxxbridge-macro" -version = "1.0.92" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56" +checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] @@ -635,7 +634,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 1.0.109", ] [[package]] @@ -646,7 +645,7 @@ checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -659,18 +658,18 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.109", ] [[package]] name = "derive_utils" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7590f99468735a318c254ca9158d0c065aa9b5312896b5a043b5e39bc96f5fa2" +checksum = "dff8f6a793f528719e1ad4425a52a213ac1214ac7158c5fb97a7f50a64bfc96d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] @@ -718,13 +717,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.2.8" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "winapi", + "windows-sys 0.48.0", ] [[package]] @@ -769,9 +768,9 @@ dependencies = [ [[package]] name = "fern" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd7b0849075e79ee9a1836df22c717d1eba30451796fdc631b04565dd11e2a" +checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" dependencies = [ "log", ] @@ -867,7 +866,7 @@ checksum = "b83164912bb4c97cfe0772913c7af7387ee2e00cb6d4636fb65a35b3d0c8f173" dependencies = [ "frunk_proc_macro_helpers", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -879,7 +878,7 @@ dependencies = [ "frunk_core", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -903,7 +902,7 @@ dependencies = [ "frunk_proc_macro_helpers", "proc-macro-hack", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -914,9 +913,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -929,9 +928,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -939,15 +938,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -956,38 +955,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] name = "futures-sink" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -1003,9 +1002,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1013,9 +1012,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -1089,6 +1088,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -1117,12 +1122,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" - [[package]] name = "httparse" version = "1.8.0" @@ -1137,9 +1136,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.24" +version = "0.14.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" +checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" dependencies = [ "bytes", "futures-channel", @@ -1174,16 +1173,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows", ] [[package]] @@ -1214,9 +1213,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", @@ -1234,30 +1233,30 @@ dependencies = [ [[package]] name = "io-enum" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b0d47a958cb166282b4dc4840a35783e861c2b39080af846e6481ebe145eee" +checksum = "01c662c349c9c9f542e7bfd9134143beb27da4b20dfbc3b3ef5b2a5b507dafbd" dependencies = [ "derive_utils", - "quote", - "syn", + "syn 2.0.14", ] [[package]] name = "io-lifetimes" -version = "1.0.6" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ + "hermit-abi 0.3.1", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "ipnet" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" +checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" [[package]] name = "itertools" @@ -1270,9 +1269,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" @@ -1381,9 +1380,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.140" +version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" [[package]] name = "libloading" @@ -1434,9 +1433,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" [[package]] name = "local-ip-address" @@ -1501,9 +1500,9 @@ dependencies = [ [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -1534,9 +1533,9 @@ dependencies = [ [[package]] name = "mockall" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e4a1c770583dac7ab5e2f6c139153b783a53a1bbee9729613f193e59828326" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" dependencies = [ "cfg-if", "downcast", @@ -1549,14 +1548,14 @@ dependencies = [ [[package]] name = "mockall_derive" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "832663583d5fa284ca8810bf7015e46c9fff9622d3cf34bd1eea5003fec06dd0" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1726,7 +1725,7 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -1747,9 +1746,9 @@ checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openssl" -version = "0.10.45" +version = "0.10.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +checksum = "7e30d8bc91859781f0a943411186324d580f2bbeb71b452fe91ae344806af3f1" dependencies = [ "bitflags", "cfg-if", @@ -1762,13 +1761,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] @@ -1779,20 +1778,19 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.25.1+1.1.1t" +version = "111.25.2+1.1.1t" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef9a9cc6ea7d9d5e7c4a913dc4b48d0e359eddf01af1dfec96ba7064b4aba10" +checksum = "320708a054ad9b3bf314688b5db87cf4d6683d64cfc835e2337924ae62bf4431" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.80" +version = "0.9.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +checksum = "0d3d193fb1488ad46ffe3aaabc912cc931d02ee8518fe2959aea8ef52718b0c0" dependencies = [ - "autocfg", "cc", "libc", "openssl-src", @@ -1828,7 +1826,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "windows-sys 0.45.0", ] @@ -1862,9 +1860,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.5.6" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cbd939b234e95d72bc393d51788aec68aeeb5d51e748ca08ff3aad58cb722f7" +checksum = "7b1403e8401ad5dedea73c626b99758535b342502f8d1e361f4a2dd952749122" dependencies = [ "thiserror", "ucd-trie", @@ -1872,9 +1870,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.5.6" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a81186863f3d0a27340815be8f2078dd8050b14cd71913db9fbda795e5f707d7" +checksum = "be99c4c1d2fc2769b1d00239431d711d08f6efedcecb8b6e30707160aee99c15" dependencies = [ "pest", "pest_generator", @@ -1882,22 +1880,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.5.6" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a1ef20bf3193c15ac345acb32e26b3dc3223aff4d77ae4fc5359567683796b" +checksum = "e56094789873daa36164de2e822b3888c6ae4b4f9da555a1103587658c805b1e" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] name = "pest_meta" -version = "2.5.6" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e3b284b1f13a20dc5ebc90aff59a51b8d7137c221131b52a7260c08cbc1cc80" +checksum = "6733073c7cff3d8459fda0e42f13a047870242aed8b509fe98000928975f359e" dependencies = [ "once_cell", "pest", @@ -1921,7 +1919,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1964,15 +1962,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" [[package]] name = "predicates-tree" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" dependencies = [ "predicates-core", "termtree", @@ -1995,9 +1993,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -2019,14 +2017,14 @@ checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -2107,11 +2105,20 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" -version = "1.7.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", @@ -2120,9 +2127,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "rend" @@ -2135,9 +2142,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.14" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" +checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" dependencies = [ "base64 0.21.0", "bytes", @@ -2187,9 +2194,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.40" +version = "0.7.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c30f1d45d9aa61cbc8cd1eb87705470892289bb2d01943e7803b873a57404dc3" +checksum = "21499ed91807f07ae081880aabb2ccc0235e9d88011867d984525e9a4c3cfa3e" dependencies = [ "bytecheck", "hashbrown 0.12.3", @@ -2201,13 +2208,13 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.40" +version = "0.7.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff26ed6c7c4dfc2aa9480b86a60e3c7233543a270a680e10758a507c5a4ce476" +checksum = "ac1c672430eb41556291981f45ca900a0239ad007242d1cb4b4167af842db666" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2247,9 +2254,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.28.1" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13cf35f7140155d02ba4ec3294373d513a3c7baa8364c162b030e33c61520a8" +checksum = "26bd36b60561ee1fb5ec2817f198b6fd09fa571c897a5e86d1487cfc2b096dfc" dependencies = [ "arrayvec", "borsh", @@ -2265,9 +2272,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" [[package]] name = "rustc-hash" @@ -2286,16 +2293,16 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.9" +version = "0.37.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" +checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2408,15 +2415,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.154" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" dependencies = [ "serde_derive", ] @@ -2442,20 +2449,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.154" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" dependencies = [ "itoa", "ryu", @@ -2464,22 +2471,22 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0969fff533976baadd92e08b1d102c5a3d8a8049eadfd69d4d1e3c5b2ed189" +checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" dependencies = [ "serde", ] [[package]] name = "serde_repr" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395627de918015623b32e7669714206363a7fc00382bf477e72c1f7533e8eafc" +checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] @@ -2505,9 +2512,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "2.3.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ea48c9627169d206b35905699f513f513c303ab9d964a59b44fdcf66c1d1ab7" +checksum = "331bb8c3bf9b92457ab7abecf07078c13f7d270ba490103e84e8b014490cd0b0" dependencies = [ "base64 0.13.1", "chrono", @@ -2521,14 +2528,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "2.3.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6b7e52858f9f06c25e1c566bbb4ab428200cb3b30053ea09dc50837de7538b" +checksum = "859011bddcc11f289f07f467cc1fe01c7a941daa4d8f6c40d4d1c92eb6d9319c" dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2638,6 +2645,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sync_wrapper" version = "0.1.2" @@ -2652,15 +2670,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -2674,28 +2692,28 @@ dependencies = [ [[package]] name = "termtree" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] @@ -2753,14 +2771,13 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.26.0" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "num_cpus", "pin-project-lite", @@ -2772,13 +2789,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.14", ] [[package]] @@ -2827,9 +2844,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" dependencies = [ "serde", "serde_spanned", @@ -2848,9 +2865,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.4" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" dependencies = [ "indexmap", "serde", @@ -2898,7 +2915,7 @@ dependencies = [ "serde_with", "thiserror", "tokio", - "toml 0.7.2", + "toml 0.7.3", "torrust-tracker-configuration", "torrust-tracker-located-error", "torrust-tracker-primitives", @@ -2964,25 +2981,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower-http" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-range-header", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" version = "0.3.2" @@ -3047,9 +3045,9 @@ checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "unicode-bidi" -version = "0.3.11" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" @@ -3091,9 +3089,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" dependencies = [ "getrandom", ] @@ -3153,7 +3151,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-shared", ] @@ -3187,7 +3185,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3249,19 +3247,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + [[package]] name = "windows-sys" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -3270,71 +3277,137 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", ] [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee7b2c67f962bf5042bfd8b6a916178df33a26eec343ae064cb8e069f638fa6f" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" dependencies = [ "memchr", ] From 776d7d9801f4b57d4d0bac25f186134695594ca1 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 12 Apr 2023 12:14:42 +0100 Subject: [PATCH 16/28] Create SECURITY.md --- SECURITY.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..b36d27978 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,27 @@ +# Security Policy + +Thanks for helping make Torrust Tracker safe for everyone. + +## Security + +[Torrust](https://github.com/torrust) takes the security of our software products and services seriously. + +## Reporting Security Issues + +If you believe you have found a security vulnerability in any of our repositories, please report it to us through coordinated disclosure. + +**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** + +Instead, please send an email to info[@]nautilus-cyberneering.de. + +Please include as much of the information listed below as you can to help us better understand and resolve the issue: + +- The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting) +- Full paths of source file(s) related to the manifestation of the issue +- The location of the affected source code (tag/branch/commit or direct URL) +- Any special configuration required to reproduce the issue +- Step-by-step instructions to reproduce the issue +- Proof-of-concept or exploit code (if possible) +- Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. From f78638aa93384ba6f1674574001e210e2874289e Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 12 Apr 2023 13:20:07 +0100 Subject: [PATCH 17/28] docs: [#279] crate docs for locate-error package --- packages/primitives/src/lib.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/primitives/src/lib.rs b/packages/primitives/src/lib.rs index bcd48145f..e6f8cb93b 100644 --- a/packages/primitives/src/lib.rs +++ b/packages/primitives/src/lib.rs @@ -1,27 +1,47 @@ +//! Primitive types for [Torrust Tracker](https://docs.rs/torrust-tracker). +//! +//! This module contains the basic data structures for the [Torrust Tracker](https://docs.rs/torrust-tracker), +//! which is a `BitTorrent` tracker server. These structures are used not only +//! by the tracker server crate, but also by other crates in the Torrust +//! ecosystem. use serde::{Deserialize, Serialize}; -// TODO: Move to the database crate once that gets its own crate. +/// The database management system used by the tracker. +/// +/// Refer to: +/// +/// - [Torrust Tracker Configuration](https://docs.rs/torrust-tracker-configuration). +/// - [Torrust Tracker](https://docs.rs/torrust-tracker). +/// +/// For more information about persistence. #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, derive_more::Display, Clone)] pub enum DatabaseDriver { + // TODO: Move to the database crate once that gets its own crate. + /// The Sqlite3 database driver. Sqlite3, + /// The MySQL database driver. MySQL, } +/// The mode the tracker will run in. +/// +/// Refer to [Torrust Tracker Configuration](https://docs.rs/torrust-tracker-configuration) +/// to know how to configure the tracker to run in each mode. #[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)] pub enum TrackerMode { - // Will track every new info hash and serve every peer. + /// Will track every new info hash and serve every peer. #[serde(rename = "public")] Public, - // Will only track whitelisted info hashes. + /// Will only track whitelisted info hashes. #[serde(rename = "listed")] Listed, - // Will only serve authenticated peers + /// Will only serve authenticated peers #[serde(rename = "private")] Private, - // Will only track whitelisted info hashes and serve authenticated peers + /// Will only track whitelisted info hashes and serve authenticated peers #[serde(rename = "private_listed")] PrivateListed, } From 9e6e6085725e34762d176c08da07cff05cd58a60 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 12 Apr 2023 13:47:08 +0100 Subject: [PATCH 18/28] docs: [#280] crate docs for test-helpers package --- packages/test-helpers/src/configuration.rs | 23 ++++++++++++++++++++-- packages/test-helpers/src/lib.rs | 3 +++ packages/test-helpers/src/random.rs | 3 +++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/test-helpers/src/configuration.rs b/packages/test-helpers/src/configuration.rs index 0b7a269ff..437475ee2 100644 --- a/packages/test-helpers/src/configuration.rs +++ b/packages/test-helpers/src/configuration.rs @@ -1,3 +1,4 @@ +//! Tracker configuration factories for testing. use std::env; use std::net::IpAddr; @@ -6,8 +7,17 @@ use torrust_tracker_primitives::TrackerMode; use crate::random; -/// This configuration is used for testing. It generates random config values so they do not collide -/// if you run more than one tracker at the same time. +/// This configuration is used for testing. It generates random config values +/// so they do not collide if you run more than one tracker at the same time. +/// +/// > **NOTICE**: This configuration is not meant to be used in production. +/// +/// > **NOTICE**: Port 0 is used for ephemeral ports, which means that the OS +/// will assign a random free port for the tracker to use. +/// +/// > **NOTICE**: You can change the log level to `debug` to see the logs of the +/// tracker while running the tests. That can be particularly useful when +/// debugging tests. /// /// # Panics /// @@ -46,6 +56,7 @@ pub fn ephemeral() -> Configuration { config } +/// Ephemeral configuration with reverse proxy enabled. #[must_use] pub fn ephemeral_with_reverse_proxy() -> Configuration { let mut cfg = ephemeral(); @@ -55,6 +66,7 @@ pub fn ephemeral_with_reverse_proxy() -> Configuration { cfg } +/// Ephemeral configuration with reverse proxy disabled. #[must_use] pub fn ephemeral_without_reverse_proxy() -> Configuration { let mut cfg = ephemeral(); @@ -64,6 +76,7 @@ pub fn ephemeral_without_reverse_proxy() -> Configuration { cfg } +/// Ephemeral configuration with `public` mode. #[must_use] pub fn ephemeral_mode_public() -> Configuration { let mut cfg = ephemeral(); @@ -73,6 +86,7 @@ pub fn ephemeral_mode_public() -> Configuration { cfg } +/// Ephemeral configuration with `private` mode. #[must_use] pub fn ephemeral_mode_private() -> Configuration { let mut cfg = ephemeral(); @@ -82,6 +96,7 @@ pub fn ephemeral_mode_private() -> Configuration { cfg } +/// Ephemeral configuration with `listed` mode. #[must_use] pub fn ephemeral_mode_whitelisted() -> Configuration { let mut cfg = ephemeral(); @@ -91,6 +106,7 @@ pub fn ephemeral_mode_whitelisted() -> Configuration { cfg } +/// Ephemeral configuration with `private_listed` mode. #[must_use] pub fn ephemeral_mode_private_whitelisted() -> Configuration { let mut cfg = ephemeral(); @@ -100,6 +116,7 @@ pub fn ephemeral_mode_private_whitelisted() -> Configuration { cfg } +/// Ephemeral configuration with a custom external (public) IP for the tracker. #[must_use] pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { let mut cfg = ephemeral(); @@ -109,6 +126,8 @@ pub fn ephemeral_with_external_ip(ip: IpAddr) -> Configuration { cfg } +/// Ephemeral configuration using a wildcard IPv6 for the UDP, HTTP and API +/// services. #[must_use] pub fn ephemeral_ipv6() -> Configuration { let mut cfg = ephemeral(); diff --git a/packages/test-helpers/src/lib.rs b/packages/test-helpers/src/lib.rs index e0f350131..e66ea2adc 100644 --- a/packages/test-helpers/src/lib.rs +++ b/packages/test-helpers/src/lib.rs @@ -1,2 +1,5 @@ +//! Testing helpers for [Torrust Tracker](https://docs.rs/torrust-tracker). +//! +//! A collection of functions and types to help with testing the tracker server. pub mod configuration; pub mod random; diff --git a/packages/test-helpers/src/random.rs b/packages/test-helpers/src/random.rs index ffb2ccd6f..2133dcd29 100644 --- a/packages/test-helpers/src/random.rs +++ b/packages/test-helpers/src/random.rs @@ -1,7 +1,10 @@ +//! Random data generators for testing. use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; /// Returns a random alphanumeric string of a certain size. +/// +/// It is useful for generating random names, IDs, etc for testing. pub fn string(size: usize) -> String { thread_rng().sample_iter(&Alphanumeric).take(size).map(char::from).collect() } From f555e6888082386068bfd5f8eeb8d214f1105bf7 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Wed, 12 Apr 2023 16:10:05 +0100 Subject: [PATCH 19/28] docs: update README --- README.md | 154 ++++++++++++++++++++++------------------------------ cSpell.json | 2 + 2 files changed, 68 insertions(+), 88 deletions(-) diff --git a/README.md b/README.md index 4e464dd68..5d74ef130 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,80 @@ # Torrust Tracker -![Test](https://github.com/torrust/torrust-tracker/actions/workflows/test_build_release.yml/badge.svg) - -## Project Description -Torrust Tracker is a lightweight but incredibly powerful and feature-rich BitTorrent tracker made using Rust. - - -### Features -* [X] Multiple UDP server and HTTP(S) server blocks for socket binding possible -* [X] Full IPv4 and IPv6 support for both UDP and HTTP(S) -* [X] Private & Whitelisted mode -* [X] Built-in API -* [X] Torrent whitelisting -* [X] Peer authentication using time-bound keys -* [X] newTrackon check supported for both HTTP, UDP, where IPv4 and IPv6 is properly handled -* [X] SQLite3 Persistent loading and saving of the torrent hashes and completed count -* [X] MySQL support added as engine option -* [X] Periodically saving added, interval can be configured - -### Implemented BEPs -* [BEP 3](https://www.bittorrent.org/beps/bep_0003.html): The BitTorrent Protocol -* [BEP 7](https://www.bittorrent.org/beps/bep_0007.html): IPv6 Support -* [BEP 15](http://www.bittorrent.org/beps/bep_0015.html): UDP Tracker Protocol for BitTorrent -* [BEP 23](http://bittorrent.org/beps/bep_0023.html): Tracker Returns Compact Peer Lists -* [BEP 27](http://bittorrent.org/beps/bep_0027.html): Private Torrents -* [BEP 41](http://bittorrent.org/beps/bep_0041.html): UDP Tracker Protocol Extensions -* [BEP 48](http://bittorrent.org/beps/bep_0048.html): Tracker Protocol Extension: Scrape + +[![Build & Release](https://github.com/torrust/torrust-tracker/actions/workflows/build_release.yml/badge.svg)](https://github.com/torrust/torrust-tracker/actions/workflows/build_release.yml) [![CI](https://github.com/torrust/torrust-tracker/actions/workflows/test_build_release.yml/badge.svg)](https://github.com/torrust/torrust-tracker/actions/workflows/test_build_release.yml) [![Publish crate](https://github.com/torrust/torrust-tracker/actions/workflows/publish_crate.yml/badge.svg)](https://github.com/torrust/torrust-tracker/actions/workflows/publish_crate.yml) [![Publish docker image](https://github.com/torrust/torrust-tracker/actions/workflows/publish_docker_image.yml/badge.svg)](https://github.com/torrust/torrust-tracker/actions/workflows/publish_docker_image.yml) [![Test](https://github.com/torrust/torrust-tracker/actions/workflows/test.yml/badge.svg)](https://github.com/torrust/torrust-tracker/actions/workflows/test.yml) [![Test docker build](https://github.com/torrust/torrust-tracker/actions/workflows/test_docker.yml/badge.svg)](https://github.com/torrust/torrust-tracker/actions/workflows/test_docker.yml) [![Upload code coverage](https://github.com/torrust/torrust-tracker/actions/workflows/codecov.yml/badge.svg)](https://github.com/torrust/torrust-tracker/actions/workflows/codecov.yml) + +Torrust Tracker is a lightweight but incredibly high-performance and feature-rich BitTorrent tracker written in [Rust](https://www.rust-lang.org/). + +It aims to provide a reliable and efficient solution for serving torrents to a vast number of peers while maintaining a high level of performance, robustness, extensibility, security, usability and with community-driven development. + +## Key Features + +* [X] Multiple UDP server and HTTP(S) server blocks for socket binding are possible. +* [X] Full IPv4 and IPv6 support for both UDP and HTTP(S). +* [X] Private & Whitelisted mode. +* [X] Built-in API. +* [X] Torrent whitelisting. +* [X] Peer authentication using time-bound keys. +* [X] [newTrackon](https://newtrackon.com/) check is supported for both HTTP and UDP, where IPv4 and IPv6 are properly handled. +* [X] SQLite3 and MySQL persistence, loading and saving of the torrent hashes and downloads completed count. +* [X] Comprehensive documentation. +* [X] A complete suite of tests. See [code coverage](https://app.codecov.io/gh/torrust/torrust-tracker) report. + +## Implemented BEPs + +* [BEP 3](https://www.bittorrent.org/beps/bep_0003.html): The BitTorrent Protocol. +* [BEP 7](https://www.bittorrent.org/beps/bep_0007.html): IPv6 Support. +* [BEP 15](http://www.bittorrent.org/beps/bep_0015.html): UDP Tracker Protocol for BitTorrent. +* [BEP 23](http://bittorrent.org/beps/bep_0023.html): Tracker Returns Compact Peer Lists. +* [BEP 27](http://bittorrent.org/beps/bep_0027.html): Private Torrents. +* [BEP 48](http://bittorrent.org/beps/bep_0048.html): Tracker Protocol Extension: Scrape. ## Getting Started -You can get the latest binaries from [releases](https://github.com/torrust/torrust-tracker/releases) or follow the install from scratch instructions below. -### Install From Scratch -1. Clone the repo. -```bash -git clone https://github.com/torrust/torrust-tracker.git -cd torrust-tracker -``` +Requirements: -2. Build the source code. -```bash -cargo build --release -``` +* Rust Stable `1.68` +* You might have problems compiling with a machine or docker container with low resources. It has been tested with docker containers with 6 CPUs, 7.5 GM of memory and 2GB of swap. + +You can follow the [documentation](https://docs.rs/torrust-tracker/) to install and use Torrust Tracker in different ways, but if you want to give it a quick try, you can use the following commands: -### Usage -* Run the torrust-tracker once to create the `config.toml` file: -```bash -./target/release/torrust-tracker +```s +git clone https://github.com/torrust/torrust-tracker.git \ + && cd torrust-tracker \ + && cargo build --release \ + && mkdir -p ./storage/database \ + && mkdir -p ./storage/ssl_certificates ``` +And then run `cargo run` twice. The first time to generate the `config.toml` file and the second time to run the tracker with the default configuration. -* Edit the newly created config.toml file according to your liking, see [configuration documentation](https://torrust.github.io/torrust-documentation/torrust-tracker/config/). Eg: -```toml -log_level = "info" -mode = "public" -db_driver = "Sqlite3" -db_path = "data.db" -announce_interval = 120 -min_announce_interval = 120 -max_peer_timeout = 900 -on_reverse_proxy = false -external_ip = "0.0.0.0" -tracker_usage_statistics = true -persistent_torrent_completed_stat = false -inactive_peer_cleanup_interval = 600 -remove_peerless_torrents = true - -[[udp_trackers]] -enabled = false -bind_address = "0.0.0.0:6969" - -[[http_trackers]] -enabled = true -bind_address = "0.0.0.0:7070" -ssl_enabled = false -ssl_cert_path = "" -ssl_key_path = "" - -[http_api] -enabled = true -bind_address = "127.0.0.1:1212" - -[http_api.access_tokens] -admin = "MyAccessToken" -``` +After running the tracker these services will be available: +* UDP tracker: `udp://127.0.0.1:6969/announce`. +* HTTP tracker: `http://127.0.0.1:6969/announce`. +* API: `http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken`. -* Run the torrust-tracker again: -```bash -./target/release/torrust-tracker -``` +## Documentation + +* [Crate documentation](https://docs.rs/torrust-tracker/). +* [API documentation](https://torrust.github.io/torrust-documentation/torrust-tracker/api/). + +## Contributing + +We welcome contributions from the community! + +How can you contribute? + +* Bug reports and feature requests. +* Code contributions. You can start by looking at the issues labeled ["good first issues"](https://github.com/torrust/torrust-tracker/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). +* Documentation improvements. Check the [documentation](https://docs.rs/torrust-tracker/) and [API documentation](https://torrust.github.io/torrust-documentation/torrust-tracker/api/) for typos, errors, or missing information. +* Participation in the community. You can help by answering questions in the [discussions](https://github.com/torrust/torrust-tracker/discussions). + +## License + +The project is licensed under the terms of the [GNU AFFERO GENERAL PUBLIC LICENSE](./LICENSE). -### Tracker URL -Your tracker announce URL will be **udp://{tracker-ip:port}** and/or **http://{tracker-ip:port}/announce** and/or **https://{tracker-ip:port}/announce** depending on your bindings. -In private & private_listed mode, tracker keys are added after the tracker URL like: **https://{tracker-ip:port}/announce/{key}**. +There is an ongoing discussion about the license of the project. You can follow the discussion [here](https://github.com/torrust/torrust-tracker/pull/251). -### Built-in API -Read the API documentation [here](https://torrust.github.io/torrust-documentation/torrust-tracker/api/). +## Acknowledgments -### Credits -This project was a joint effort by [Nautilus Cyberneering GmbH](https://nautilus-cyberneering.de/) and [Dutch Bits](https://dutchbits.nl). -Also thanks to [Naim A.](https://github.com/naim94a/udpt) and [greatest-ape](https://github.com/greatest-ape/aquatic) for some parts of the code. -Further added features and functions thanks to [Power2All](https://github.com/power2all). +This project was a joint effort by [Nautilus Cyberneering GmbH](https://nautilus-cyberneering.de/) and [Dutch Bits](https://dutchbits.nl). Also thanks to [Naim A.](https://github.com/naim94a/udpt) and [greatest-ape](https://github.com/greatest-ape/aquatic) for some parts of the code. Further added features and functions thanks to [Power2All](https://github.com/power2all). diff --git a/cSpell.json b/cSpell.json index f07e2bfb4..2fa80b58a 100644 --- a/cSpell.json +++ b/cSpell.json @@ -46,6 +46,7 @@ "mockall", "multimap", "myacicontext", + "Naim", "nanos", "nextest", "nocapture", @@ -79,6 +80,7 @@ "torrust", "torrustracker", "trackerid", + "Trackon", "typenum", "Unamed", "untuple", From 09da301d1612af8fff27d629fa59797bf182f937 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 13 Apr 2023 08:24:29 +0100 Subject: [PATCH 20/28] docs: [#295] add README to packages --- README.md | 6 +- packages/configuration/LICENSE | 661 +++++++++++++++++++++++++++++++ packages/configuration/README.md | 11 + packages/located-error/LICENSE | 661 +++++++++++++++++++++++++++++++ packages/located-error/README.md | 11 + packages/primitives/LICENSE | 661 +++++++++++++++++++++++++++++++ packages/primitives/README.md | 11 + packages/test-helpers/LICENSE | 661 +++++++++++++++++++++++++++++++ packages/test-helpers/README.md | 11 + 9 files changed, 2692 insertions(+), 2 deletions(-) create mode 100644 packages/configuration/LICENSE create mode 100644 packages/configuration/README.md create mode 100644 packages/located-error/LICENSE create mode 100644 packages/located-error/README.md create mode 100644 packages/primitives/LICENSE create mode 100644 packages/primitives/README.md create mode 100644 packages/test-helpers/LICENSE create mode 100644 packages/test-helpers/README.md diff --git a/README.md b/README.md index 5d74ef130..666868a87 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,9 @@ After running the tracker these services will be available: ## Documentation * [Crate documentation](https://docs.rs/torrust-tracker/). -* [API documentation](https://torrust.github.io/torrust-documentation/torrust-tracker/api/). +* [API `v1`](https://docs.rs/torrust-tracker/3.0.0-alpha.1/torrust_tracker/servers/apis/v1). +* [HTTP Tracker](https://docs.rs/torrust-tracker/3.0.0-alpha.1/torrust_tracker/servers/http). +* [UDP Tracker](https://docs.rs/torrust-tracker/3.0.0-alpha.1/torrust_tracker/servers/udp). ## Contributing @@ -66,7 +68,7 @@ How can you contribute? * Bug reports and feature requests. * Code contributions. You can start by looking at the issues labeled ["good first issues"](https://github.com/torrust/torrust-tracker/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). -* Documentation improvements. Check the [documentation](https://docs.rs/torrust-tracker/) and [API documentation](https://torrust.github.io/torrust-documentation/torrust-tracker/api/) for typos, errors, or missing information. +* Documentation improvements. Check the [documentation](https://docs.rs/torrust-tracker/) and [API documentation](https://docs.rs/torrust-tracker/3.0.0-alpha.1/torrust_tracker/servers/apis/v1) for typos, errors, or missing information. * Participation in the community. You can help by answering questions in the [discussions](https://github.com/torrust/torrust-tracker/discussions). ## License diff --git a/packages/configuration/LICENSE b/packages/configuration/LICENSE new file mode 100644 index 000000000..0ad25db4b --- /dev/null +++ b/packages/configuration/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/configuration/README.md b/packages/configuration/README.md new file mode 100644 index 000000000..ccae51d70 --- /dev/null +++ b/packages/configuration/README.md @@ -0,0 +1,11 @@ +# Torrust Tracker Configuration + +A library to provide configuration to the [Torrust Tracker](https://github.com/torrust/torrust-tracker). + +## Documentation + +[Crate documentation](https://docs.rs/torrust-tracker-configuration). + +## License + +The project is licensed under the terms of the [GNU AFFERO GENERAL PUBLIC LICENSE](./LICENSE). diff --git a/packages/located-error/LICENSE b/packages/located-error/LICENSE new file mode 100644 index 000000000..0ad25db4b --- /dev/null +++ b/packages/located-error/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/located-error/README.md b/packages/located-error/README.md new file mode 100644 index 000000000..c3c18fa49 --- /dev/null +++ b/packages/located-error/README.md @@ -0,0 +1,11 @@ +# Torrust Tracker Located Error + +A library to provide an error decorator with the location and the source of the original error. + +## Documentation + +[Crate documentation](https://docs.rs/torrust-tracker-located-error). + +## License + +The project is licensed under the terms of the [GNU AFFERO GENERAL PUBLIC LICENSE](./LICENSE). diff --git a/packages/primitives/LICENSE b/packages/primitives/LICENSE new file mode 100644 index 000000000..0ad25db4b --- /dev/null +++ b/packages/primitives/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/primitives/README.md b/packages/primitives/README.md new file mode 100644 index 000000000..791955859 --- /dev/null +++ b/packages/primitives/README.md @@ -0,0 +1,11 @@ +# Torrust Tracker Primitives + +A library with the primitive types shared by the [Torrust Tracker](https://github.com/torrust/torrust-tracker) packages. + +## Documentation + +[Crate documentation](https://docs.rs/torrust-tracker-primitives). + +## License + +The project is licensed under the terms of the [GNU AFFERO GENERAL PUBLIC LICENSE](./LICENSE). diff --git a/packages/test-helpers/LICENSE b/packages/test-helpers/LICENSE new file mode 100644 index 000000000..0ad25db4b --- /dev/null +++ b/packages/test-helpers/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/packages/test-helpers/README.md b/packages/test-helpers/README.md new file mode 100644 index 000000000..7389dce11 --- /dev/null +++ b/packages/test-helpers/README.md @@ -0,0 +1,11 @@ +# Torrust Tracker Configuration + +A library providing helpers for testing the [Torrust Tracker](https://github.com/torrust/torrust-tracker). + +## Documentation + +[Crate documentation](https://docs.rs/torrust-tracker-test-helpers). + +## License + +The project is licensed under the terms of the [GNU AFFERO GENERAL PUBLIC LICENSE](./LICENSE). From aec3a4ce4967082a79036e9b147c82091d89b925 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 13 Apr 2023 09:04:32 +0100 Subject: [PATCH 21/28] chore(api)!: remove API endpoint without version prefix BREAKING CHANGE: API endpoints without version prefixes are no longer available. For example, `/api/stats` use `/api/v1/stats` instead. --- src/servers/apis/v1/routes.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/servers/apis/v1/routes.rs b/src/servers/apis/v1/routes.rs index 7b792f8a8..74778ca14 100644 --- a/src/servers/apis/v1/routes.rs +++ b/src/servers/apis/v1/routes.rs @@ -7,21 +7,7 @@ use super::context::{auth_key, stats, torrent, whitelist}; use crate::tracker::Tracker; /// Add the routes for the v1 API. -/// -/// > **NOTICE**: the old API endpoints without `v1` prefix are kept for -/// backward compatibility. For example, the `GET /api/stats` endpoint is -/// still available, but it is deprecated and will be removed in the future. -/// You should use the `GET /api/v1/stats` endpoint instead. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { - // Without `v1` prefix. - // We keep the old API endpoints without `v1` prefix for backward compatibility. - // todo: remove when the torrust index backend is using the `v1` prefix. - let router = auth_key::routes::add(prefix, router, tracker.clone()); - let router = stats::routes::add(prefix, router, tracker.clone()); - let router = whitelist::routes::add(prefix, router, tracker.clone()); - let router = torrent::routes::add(prefix, router, tracker.clone()); - - // With `v1` prefix let v1_prefix = format!("{prefix}/v1"); let router = auth_key::routes::add(&v1_prefix, router, tracker.clone()); let router = stats::routes::add(&v1_prefix, router, tracker.clone()); From feaa1737b29336fc8348cd5cc238d53c69d98ec6 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 13 Apr 2023 10:27:33 +0100 Subject: [PATCH 22/28] fix: test for configuration Fields were reorganized and package test were not being executed on the CI. --- packages/configuration/src/lib.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index 6b051b572..e48355757 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -557,10 +557,14 @@ impl Configuration { /// permission to read it. Will also return `Err` if the configuration is /// not valid or cannot be encoded to TOML. pub fn save_to_file(&self, path: &str) -> Result<(), Error> { - let toml_string = toml::to_string(self).expect("Could not encode TOML value"); - fs::write(path, toml_string).expect("Could not write to file!"); + fs::write(path, self.to_toml()).expect("Could not write to file!"); Ok(()) } + + /// Encodes the configuration to TOML. + fn to_toml(&self) -> String { + toml::to_string(self).expect("Could not encode TOML value") + } } #[cfg(test)] @@ -575,11 +579,11 @@ mod tests { db_path = "./storage/database/data.db" announce_interval = 120 min_announce_interval = 120 - max_peer_timeout = 900 on_reverse_proxy = false external_ip = "0.0.0.0" tracker_usage_statistics = true persistent_torrent_completed_stat = false + max_peer_timeout = 900 inactive_peer_cleanup_interval = 600 remove_peerless_torrents = true From a933a6929c9ebb67bf1abd7372840be1216a2267 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 13 Apr 2023 10:28:45 +0100 Subject: [PATCH 23/28] ci: run tests for workspace packages Test for packages were not being executed. We need to add the `--workpspace` option: ``` cargo test --workspace ``` --- .github/workflows/test_build_release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_build_release.yml b/.github/workflows/test_build_release.yml index d8c25bd56..f1bc295ce 100644 --- a/.github/workflows/test_build_release.yml +++ b/.github/workflows/test_build_release.yml @@ -36,9 +36,11 @@ jobs: run: cargo clippy --all-targets -- -D clippy::pedantic - name: Test Documentation run: cargo test --doc + - name: Run Tests + run: cargo test --workspace - uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@nextest - - name: Run Tests + - name: Show coverage run: cargo llvm-cov nextest build: From 823537e9c2e8d004de72fae208603e19f95a4ab8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 13 Apr 2023 10:45:40 +0100 Subject: [PATCH 24/28] fix: [#234] remove unused dependencies And move to `[dev-dependencies]` the ones that are only used for testing. --- Cargo.lock | 61 ++----------------------------- Cargo.toml | 2 - packages/configuration/Cargo.toml | 2 + packages/located-error/Cargo.toml | 3 ++ packages/test-helpers/Cargo.toml | 1 - 5 files changed, 8 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7053d618..ae256a5a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,7 +448,7 @@ dependencies = [ "rust-ini", "serde", "serde_json", - "toml 0.5.11", + "toml", "yaml-rust", ] @@ -1982,7 +1982,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml 0.5.11", + "toml", ] [[package]] @@ -2489,15 +2489,6 @@ dependencies = [ "syn 2.0.14", ] -[[package]] -name = "serde_spanned" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2842,40 +2833,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.19.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - [[package]] name = "torrust-tracker" version = "3.0.0-alpha.1" @@ -2892,7 +2849,6 @@ dependencies = [ "derive_more", "fern", "futures", - "hex", "hyper", "lazy_static", "local-ip-address", @@ -2915,7 +2871,6 @@ dependencies = [ "serde_with", "thiserror", "tokio", - "toml 0.7.3", "torrust-tracker-configuration", "torrust-tracker-located-error", "torrust-tracker-primitives", @@ -2932,7 +2887,7 @@ dependencies = [ "serde", "serde_with", "thiserror", - "toml 0.5.11", + "toml", "torrust-tracker-located-error", "torrust-tracker-primitives", "uuid", @@ -2960,7 +2915,6 @@ version = "3.0.0-alpha.1" dependencies = [ "lazy_static", "rand", - "tokio", "torrust-tracker-configuration", "torrust-tracker-primitives", ] @@ -3403,15 +3357,6 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" -[[package]] -name = "winnow" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 4b6bcb323..13dd8d572 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,13 +18,11 @@ serde = { version = "1.0", features = ["derive"] } serde_bencode = "^0.2" serde_json = "1.0" serde_with = "2.0" -hex = "0.4" percent-encoding = "2.2" binascii = "0.1" lazy_static = "1.4" openssl = { version = "0.10", features = ["vendored"] } config = "0.13" -toml = "0.7" log = { version = "0.4", features = ["release_max_level_info"] } fern = "0.6" chrono = "0.4" diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index aade6272d..36fb6d665 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -15,4 +15,6 @@ log = { version = "0.4", features = ["release_max_level_info"] } thiserror = "1.0" torrust-tracker-primitives = { version = "3.0.0-alpha.1", path = "../primitives" } torrust-tracker-located-error = { version = "3.0.0-alpha.1", path = "../located-error" } + +[dev-dependencies] uuid = { version = "1", features = ["v4"] } diff --git a/packages/located-error/Cargo.toml b/packages/located-error/Cargo.toml index f67ef340f..acd13def3 100644 --- a/packages/located-error/Cargo.toml +++ b/packages/located-error/Cargo.toml @@ -8,4 +8,7 @@ edition.workspace = true [dependencies] log = { version = "0.4", features = ["release_max_level_info"] } + +[dev-dependencies] thiserror = "1.0" + diff --git a/packages/test-helpers/Cargo.toml b/packages/test-helpers/Cargo.toml index 4483f8f4d..2f876470b 100644 --- a/packages/test-helpers/Cargo.toml +++ b/packages/test-helpers/Cargo.toml @@ -7,7 +7,6 @@ authors.workspace = true edition.workspace = true [dependencies] -tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "macros", "signal"] } lazy_static = "1.4" rand = "0.8.5" torrust-tracker-configuration = { version = "3.0.0-alpha.1", path = "../configuration"} From 09e97a1e9a7184c3bf2f10fde1ed47ec9861b6c4 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 13 Apr 2023 11:30:33 +0100 Subject: [PATCH 25/28] feat: [#267] remove always restart configutation for MySQL from docker compose Since the configuration is intended for development, it does not make sense. --- compose.yaml | 1 - docker/README.md | 17 ++++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/compose.yaml b/compose.yaml index d11f9c8ae..49f3055a8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -22,7 +22,6 @@ services: mysql: image: mysql:8.0 command: '--default-authentication-plugin=mysql_native_password' - restart: always healthcheck: test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 --password="$$(cat /run/secrets/db-password)" --silent'] interval: 3s diff --git a/docker/README.md b/docker/README.md index e5b4dfe74..e0fee61e7 100644 --- a/docker/README.md +++ b/docker/README.md @@ -19,6 +19,17 @@ storage/ ## Dev environment +When using docker you have to bind the exposed ports to the wildcard address `0.0.0.0`, so you can access the application from the host machine. + +The default API configuration uses `127.0.0.1`, so you have to change it to: + +```toml +[http_api] +bind_address = "0.0.0.0:1212" +``` + +Otherwise the API will be only accessible from inside the container. + ### With docker Build and run locally: @@ -78,7 +89,7 @@ CONTAINER ID IMAGE COMMAND CREATED STATU And you should be able to use the application, for example making a request to the API: - + You can stop the containers with: @@ -169,7 +180,7 @@ ssl_key_path = "./storage/ssl_certificates/localhost.key" If you enable the SSL certificate for the API, for example, you can load the API with this URL: - + ## Prod environment @@ -232,7 +243,7 @@ CONTAINER ID IMAGE intelligent-hawking registry.hub.docker.com/torrust/tracker:latest Running 4.236.213.57:6969->6969/udp, 4.236.213.57:1212->1212/tcp ``` -After a while, you can use the tracker API `http://4.236.213.57:1212/api/stats?token=MyAccessToken` and the UDP tracker with your BitTorrent client using this tracker announce URL `udp://4.236.213.57:6969`. +After a while, you can use the tracker API `http://4.236.213.57:1212/api/v1/stats?token=MyAccessToken` and the UDP tracker with your BitTorrent client using this tracker announce URL `udp://4.236.213.57:6969`. > NOTES: > From 77a5e233d3cc7c70afb7bb1114baa9b413fa252f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 13 Apr 2023 13:35:26 +0100 Subject: [PATCH 26/28] chore(deps): [#297] update workflow actions --- .github/workflows/publish_docker_image.yml | 2 +- .github/workflows/test_build_release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish_docker_image.yml b/.github/workflows/publish_docker_image.yml index 20152a727..1dd65e3a7 100644 --- a/.github/workflows/publish_docker_image.yml +++ b/.github/workflows/publish_docker_image.yml @@ -73,7 +73,7 @@ jobs: uses: docker/setup-buildx-action@v2 - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile diff --git a/.github/workflows/test_build_release.yml b/.github/workflows/test_build_release.yml index f1bc295ce..88234b97a 100644 --- a/.github/workflows/test_build_release.yml +++ b/.github/workflows/test_build_release.yml @@ -61,7 +61,7 @@ jobs: - name: Build Torrust Tracker run: cargo build --release - name: Upload Build Artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: torrust-tracker path: ./target/release/torrust-tracker @@ -71,7 +71,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download Build Artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: torrust-tracker - name: Release From dfcc4b36356fc4dce87b74e35d8eabf3f1e5a741 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 13 Apr 2023 13:50:23 +0100 Subject: [PATCH 27/28] chore(deps): [#297] update cargo dependencies --- Cargo.lock | 70 +++++++++++++++++++++++++++---- Cargo.toml | 4 +- packages/configuration/Cargo.toml | 2 +- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae256a5a9..995d051dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,9 +59,9 @@ dependencies = [ [[package]] name = "aquatic_udp_protocol" -version = "0.2.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16149f27924d42b337a637cd90a8ee2a8973bbccf32aabebce2b3c66913f947f" +checksum = "b2919b480121f7d20d247524da62bad1b6b7928bc3f50898f624b5c592727341" dependencies = [ "byteorder", "either", @@ -448,7 +448,7 @@ dependencies = [ "rust-ini", "serde", "serde_json", - "toml", + "toml 0.5.11", "yaml-rust", ] @@ -1560,9 +1560,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +checksum = "70db9248a93dc36a36d9a47898caa007a32755c7ad140ec64eeeb50d5a730631" dependencies = [ "serde", ] @@ -1982,7 +1982,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] @@ -2460,9 +2460,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -2489,6 +2489,15 @@ dependencies = [ "syn 2.0.14", ] +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2833,6 +2842,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "torrust-tracker" version = "3.0.0-alpha.1" @@ -2887,7 +2930,7 @@ dependencies = [ "serde", "serde_with", "thiserror", - "toml", + "toml 0.7.3", "torrust-tracker-located-error", "torrust-tracker-primitives", "uuid", @@ -3357,6 +3400,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 13dd8d572..f8a59bf4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ derive_more = "0.99" thiserror = "1.0" futures = "0.3" async-trait = "0.1" -aquatic_udp_protocol = "0.2" +aquatic_udp_protocol = "0.8" uuid = { version = "1", features = ["v4"] } axum = "0.6.10" axum-server = { version = "0.4", features = ["tls-rustls"] } @@ -43,7 +43,7 @@ bip_bencode = "0.4" torrust-tracker-primitives = { version = "3.0.0-alpha.1", path = "packages/primitives" } torrust-tracker-configuration = { version = "3.0.0-alpha.1", path = "packages/configuration" } torrust-tracker-located-error = { version = "3.0.0-alpha.1", path = "packages/located-error" } -multimap = "0.8" +multimap = "0.9" hyper = "0.14" [dev-dependencies] diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index 36fb6d665..8ad5aa3fb 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -10,7 +10,7 @@ edition.workspace = true serde = { version = "1.0", features = ["derive"] } serde_with = "2.0" config = "0.13" -toml = "0.5" +toml = "0.7" log = { version = "0.4", features = ["release_max_level_info"] } thiserror = "1.0" torrust-tracker-primitives = { version = "3.0.0-alpha.1", path = "../primitives" } From 5e2356effba1b24fe8a640b6b57b63efcab41886 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Thu, 13 Apr 2023 15:28:36 +0100 Subject: [PATCH 28/28] feat: release 3.0.0-alpha.2 --- Cargo.lock | 10 +++++----- Cargo.toml | 10 +++++----- README.md | 8 ++++---- packages/configuration/Cargo.toml | 4 ++-- packages/test-helpers/Cargo.toml | 4 ++-- src/lib.rs | 4 ++-- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 995d051dd..899e8855f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2878,7 +2878,7 @@ dependencies = [ [[package]] name = "torrust-tracker" -version = "3.0.0-alpha.1" +version = "3.0.0-alpha.2" dependencies = [ "aquatic_udp_protocol", "async-trait", @@ -2923,7 +2923,7 @@ dependencies = [ [[package]] name = "torrust-tracker-configuration" -version = "3.0.0-alpha.1" +version = "3.0.0-alpha.2" dependencies = [ "config", "log", @@ -2938,7 +2938,7 @@ dependencies = [ [[package]] name = "torrust-tracker-located-error" -version = "3.0.0-alpha.1" +version = "3.0.0-alpha.2" dependencies = [ "log", "thiserror", @@ -2946,7 +2946,7 @@ dependencies = [ [[package]] name = "torrust-tracker-primitives" -version = "3.0.0-alpha.1" +version = "3.0.0-alpha.2" dependencies = [ "derive_more", "serde", @@ -2954,7 +2954,7 @@ dependencies = [ [[package]] name = "torrust-tracker-test-helpers" -version = "3.0.0-alpha.1" +version = "3.0.0-alpha.2" dependencies = [ "lazy_static", "rand", diff --git a/Cargo.toml b/Cargo.toml index f8a59bf4c..51c191275 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ version.workspace = true authors = ["Nautilus Cyberneering , Mick van Dijke "] edition = "2021" repository = "https://github.com/torrust/torrust-tracker" -version = "3.0.0-alpha.1" +version = "3.0.0-alpha.2" [dependencies] tokio = { version = "1.26", features = ["rt-multi-thread", "net", "sync", "macros", "signal"] } @@ -40,9 +40,9 @@ axum = "0.6.10" axum-server = { version = "0.4", features = ["tls-rustls"] } axum-client-ip = "0.4" bip_bencode = "0.4" -torrust-tracker-primitives = { version = "3.0.0-alpha.1", path = "packages/primitives" } -torrust-tracker-configuration = { version = "3.0.0-alpha.1", path = "packages/configuration" } -torrust-tracker-located-error = { version = "3.0.0-alpha.1", path = "packages/located-error" } +torrust-tracker-primitives = { version = "3.0.0-alpha.2", path = "packages/primitives" } +torrust-tracker-configuration = { version = "3.0.0-alpha.2", path = "packages/configuration" } +torrust-tracker-located-error = { version = "3.0.0-alpha.2", path = "packages/located-error" } multimap = "0.9" hyper = "0.14" @@ -53,7 +53,7 @@ serde_urlencoded = "0.7" serde_repr = "0.1" serde_bytes = "0.11" local-ip-address = "0.5" -torrust-tracker-test-helpers = { version = "3.0.0-alpha.1", path = "packages/test-helpers" } +torrust-tracker-test-helpers = { version = "3.0.0-alpha.2", path = "packages/test-helpers" } [workspace] members = [ diff --git a/README.md b/README.md index 666868a87..c3d0a127b 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,9 @@ After running the tracker these services will be available: ## Documentation * [Crate documentation](https://docs.rs/torrust-tracker/). -* [API `v1`](https://docs.rs/torrust-tracker/3.0.0-alpha.1/torrust_tracker/servers/apis/v1). -* [HTTP Tracker](https://docs.rs/torrust-tracker/3.0.0-alpha.1/torrust_tracker/servers/http). -* [UDP Tracker](https://docs.rs/torrust-tracker/3.0.0-alpha.1/torrust_tracker/servers/udp). +* [API `v1`](https://docs.rs/torrust-tracker/3.0.0-alpha.2/torrust_tracker/servers/apis/v1). +* [HTTP Tracker](https://docs.rs/torrust-tracker/3.0.0-alpha.2/torrust_tracker/servers/http). +* [UDP Tracker](https://docs.rs/torrust-tracker/3.0.0-alpha.2/torrust_tracker/servers/udp). ## Contributing @@ -68,7 +68,7 @@ How can you contribute? * Bug reports and feature requests. * Code contributions. You can start by looking at the issues labeled ["good first issues"](https://github.com/torrust/torrust-tracker/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). -* Documentation improvements. Check the [documentation](https://docs.rs/torrust-tracker/) and [API documentation](https://docs.rs/torrust-tracker/3.0.0-alpha.1/torrust_tracker/servers/apis/v1) for typos, errors, or missing information. +* Documentation improvements. Check the [documentation](https://docs.rs/torrust-tracker/) and [API documentation](https://docs.rs/torrust-tracker/3.0.0-alpha.2/torrust_tracker/servers/apis/v1) for typos, errors, or missing information. * Participation in the community. You can help by answering questions in the [discussions](https://github.com/torrust/torrust-tracker/discussions). ## License diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index 8ad5aa3fb..6f9e5cbc5 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -13,8 +13,8 @@ config = "0.13" toml = "0.7" log = { version = "0.4", features = ["release_max_level_info"] } thiserror = "1.0" -torrust-tracker-primitives = { version = "3.0.0-alpha.1", path = "../primitives" } -torrust-tracker-located-error = { version = "3.0.0-alpha.1", path = "../located-error" } +torrust-tracker-primitives = { version = "3.0.0-alpha.2", path = "../primitives" } +torrust-tracker-located-error = { version = "3.0.0-alpha.2", path = "../located-error" } [dev-dependencies] uuid = { version = "1", features = ["v4"] } diff --git a/packages/test-helpers/Cargo.toml b/packages/test-helpers/Cargo.toml index 2f876470b..5d360d101 100644 --- a/packages/test-helpers/Cargo.toml +++ b/packages/test-helpers/Cargo.toml @@ -9,5 +9,5 @@ edition.workspace = true [dependencies] lazy_static = "1.4" rand = "0.8.5" -torrust-tracker-configuration = { version = "3.0.0-alpha.1", path = "../configuration"} -torrust-tracker-primitives = { version = "3.0.0-alpha.1", path = "../primitives"} +torrust-tracker-configuration = { version = "3.0.0-alpha.2", path = "../configuration"} +torrust-tracker-primitives = { version = "3.0.0-alpha.2", path = "../primitives"} diff --git a/src/lib.rs b/src/lib.rs index adcf3d1f2..e219e59e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,7 +110,7 @@ //! --publish 7070:7070/tcp \ //! --publish 1212:1212/tcp \ //! --volume "$(pwd)/storage":"/app/storage" \ -//! torrust/tracker:3.0.0-alpha.1 +//! torrust/tracker:3.0.0-alpha.2 //! ``` //! //! For more information about using docker visit the [tracker docker documentation](https://github.com/torrust/torrust-tracker/tree/develop/docker). @@ -162,7 +162,7 @@ //! //! The default configuration includes one disabled UDP server, one disabled HTTP server and the enabled API. //! -//! For more information about each service and options you can visit the documentation for the [torrust-tracker-configuration crate](https://docs.rs/torrust-tracker-configuration/3.0.0-alpha.1/torrust_tracker_configuration/). +//! For more information about each service and options you can visit the documentation for the [torrust-tracker-configuration crate](https://docs.rs/torrust-tracker-configuration). //! //! Alternatively to the `config.toml` file you can use one environment variable `TORRUST_TRACKER_CONFIG` to pass the configuration to the tracker: //!