diff --git a/packages/http-protocol/src/v1/query.rs b/packages/http-protocol/src/v1/query.rs index f77145cb..66afddf6 100644 --- a/packages/http-protocol/src/v1/query.rs +++ b/packages/http-protocol/src/v1/query.rs @@ -249,6 +249,13 @@ mod tests { assert_eq!(query.get_param("param2"), Some("value2".to_string())); } + #[test] + fn should_ignore_duplicate_param_values_when_asked_to_return_only_one_value() { + let query = Query::from(vec![("param1", "value1"), ("param1", "value2")]); + + assert_eq!(query.get_param("param1"), Some("value1".to_string())); + } + #[test] fn should_fail_parsing_an_invalid_query_string() { let invalid_raw_query = "name=value=value"; diff --git a/packages/tracker-core/src/announce_handler.rs b/packages/tracker-core/src/announce_handler.rs index cd0a9b86..6707f191 100644 --- a/packages/tracker-core/src/announce_handler.rs +++ b/packages/tracker-core/src/announce_handler.rs @@ -1,3 +1,95 @@ +//! Announce handler. +//! +//! 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 `AnnounceHandler` you can `announce` a new [`peer::Peer`](torrust_tracker_primitives) with: +//! +//! ```rust,no_run +//! use std::net::SocketAddr; +//! use std::net::IpAddr; +//! use std::net::Ipv4Addr; +//! use std::str::FromStr; +//! +//! use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes, PeerId}; +//! use torrust_tracker_primitives::DurationSinceUnixEpoch; +//! use torrust_tracker_primitives::peer; +//! use bittorrent_primitives::info_hash::InfoHash; +//! +//! let info_hash = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(); +//! +//! let peer = peer::Peer { +//! peer_id: PeerId(*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::new(0), +//! downloaded: NumberOfBytes::new(0), +//! left: NumberOfBytes::new(0), +//! event: AnnounceEvent::Completed, +//! }; +//! +//! let peer_ip = IpAddr::V4(Ipv4Addr::from_str("126.0.0.1").unwrap()); +//! ``` +//! +//! ```text +//! let announce_data = announce_handler.announce(&info_hash, &mut peer, &peer_ip).await; +//! ``` +//! +//! The handler 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 handler 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 handler 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 +//! use torrust_tracker_primitives::peer; +//! use torrust_tracker_configuration::AnnouncePolicy; +//! +//! pub struct AnnounceData { +//! pub peers: Vec, +//! pub swarm_stats: SwarmMetadata, +//! pub policy: AnnouncePolicy, // the tracker announce policy. +//! } +//! +//! pub struct SwarmMetadata { +//! 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 AnnounceInterval { +//! // ... +//! pub interval: u32, // Interval in seconds that the client should wait between sending regular announce requests to the tracker +//! pub interval_min: u32, // Minimum announce interval. Clients must not reannounce more frequently than this +//! // ... +//! } +//! ``` +//! +//! ## Related BEPs: +//! +//! 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) use std::net::IpAddr; use std::sync::Arc; @@ -10,18 +102,20 @@ use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use super::torrent::repository::in_memory::InMemoryTorrentRepository; use super::torrent::repository::persisted::DatabasePersistentTorrentRepository; +/// Handles `announce` requests from `BitTorrent` clients. pub struct AnnounceHandler { /// The tracker configuration. config: Core, - /// The in-memory torrents repository. + /// Repository for in-memory torrent data. in_memory_torrent_repository: Arc, - /// The persistent torrents repository. + /// Repository for persistent torrent data (database). db_torrent_repository: Arc, } impl AnnounceHandler { + /// Creates a new `AnnounceHandler`. #[must_use] pub fn new( config: &Core, @@ -35,9 +129,20 @@ impl AnnounceHandler { } } - /// It handles an announce request. + /// Processes an announce request from a peer. /// /// BEP 03: [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html). + /// + /// # Parameters + /// + /// - `info_hash`: The unique identifier of the torrent. + /// - `peer`: The peer announcing itself (may be updated if IP is adjusted). + /// - `remote_client_ip`: The IP address of the client making the request. + /// - `peers_wanted`: Specifies how many peers the client wants in the response. + /// + /// # Returns + /// + /// An `AnnounceData` struct containing the list of peers, swarm statistics, and tracker policy. pub fn announce( &self, info_hash: &InfoHash, @@ -77,9 +182,8 @@ impl AnnounceHandler { } } - /// 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. + /// Updates the torrent data in memory, persists statistics if needed, and + /// returns the updated swarm stats. #[must_use] fn upsert_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> SwarmMetadata { let swarm_metadata_before = self.in_memory_torrent_repository.get_swarm_metadata(info_hash); @@ -95,7 +199,7 @@ impl AnnounceHandler { swarm_metadata_after } - /// It stores the torrents stats into the database (if persistency is enabled). + /// Persists torrent statistics to the database if persistence is enabled. fn persist_stats(&self, info_hash: &InfoHash, swarm_metadata: &SwarmMetadata) { if self.config.tracker_policy.persistent_torrent_completed_stat { let completed = swarm_metadata.downloaded; @@ -106,22 +210,25 @@ impl AnnounceHandler { } } -/// How many peers the peer announcing wants in the announce response. +/// Specifies how many peers a client wants in the announce response. #[derive(Clone, Debug, PartialEq, Default)] pub enum PeersWanted { - /// The peer wants as many peers as possible in the announce response. + /// Request as many peers as possible (default behavior). #[default] AsManyAsPossible, - /// The peer only wants a certain amount of peers in the announce response. + + /// Request a specific number of peers. Only { amount: usize }, } impl PeersWanted { + /// Request a specific number of peers. #[must_use] pub fn only(limit: u32) -> Self { limit.into() } + /// Returns the maximum number of peers allowed based on the request and tracker limit. fn limit(&self) -> usize { match self { PeersWanted::AsManyAsPossible => TORRENT_PEERS_LIMIT, @@ -159,6 +266,10 @@ impl From for PeersWanted { } } +/// Assigns the correct IP address to a peer based on tracker settings. +/// +/// If the client IP is a loopback address and the tracker has an external IP +/// configured, the external IP will be assigned to the peer. #[must_use] fn assign_ip_address_to_peer(remote_client_ip: &IpAddr, tracker_external_ip: Option) -> IpAddr { if let Some(host_ip) = tracker_external_ip.filter(|_| remote_client_ip.is_loopback()) { diff --git a/packages/tracker-core/src/authentication/handler.rs b/packages/tracker-core/src/authentication/handler.rs index 13606091..178895b8 100644 --- a/packages/tracker-core/src/authentication/handler.rs +++ b/packages/tracker-core/src/authentication/handler.rs @@ -1,3 +1,11 @@ +//! This module implements the `KeysHandler` service +//! +//! It's responsible for managing authentication keys for the `BitTorrent` tracker. +//! +//! The service handles both persistent and in-memory storage of peer keys, and +//! supports adding new keys (either pre-generated or randomly created), +//! removing keys, and loading keys from the database into memory. Keys can be +//! either permanent or expire after a configurable duration per key. use std::sync::Arc; use std::time::Duration; @@ -11,29 +19,44 @@ use super::{key, CurrentClock, Key, PeerKey}; use crate::databases; use crate::error::PeerKeyError; -/// This type contains the info needed to add a new tracker key. +/// Contains the information needed to add a new tracker key. /// -/// You can upload a pre-generated key or let the app to generate a new one. -/// You can also set an expiration date or leave it empty (`None`) if you want -/// to create a permanent key that does not expire. +/// A new key can either be a pre-generated key provided by the user or can be +/// randomly generated by the application. Additionally, the key may be set to +/// expire after a certain number of seconds, or be permanent (if no expiration +/// is specified). #[derive(Debug)] pub struct AddKeyRequest { - /// The pre-generated key. Use `None` to generate a random key. + /// The pre-generated key as a string. If `None` the service will generate a + /// random key. pub opt_key: Option, - /// How long the key will be valid in seconds. Use `None` for permanent keys. + /// The duration (in seconds) for which the key is valid. Use `None` for + /// permanent keys. pub opt_seconds_valid: Option, } +/// The `KeysHandler` service manages the creation, addition, removal, and loading +/// of authentication keys for the tracker. +/// +/// It uses both a persistent (database) repository and an in-memory repository +/// to manage keys. pub struct KeysHandler { - /// The database repository for the authentication keys. + /// The database repository for storing authentication keys persistently. db_key_repository: Arc, - /// In-memory implementation of the authentication key repository. + /// The in-memory repository for caching authentication keys. in_memory_key_repository: Arc, } impl KeysHandler { + /// Creates a new instance of the `KeysHandler` service. + /// + /// # Parameters + /// + /// - `db_key_repository`: A shared reference to the database key repository. + /// - `in_memory_key_repository`: A shared reference to the in-memory key + /// repository. #[must_use] pub fn new(db_key_repository: &Arc, in_memory_key_repository: &Arc) -> Self { Self { @@ -42,18 +65,24 @@ impl KeysHandler { } } - /// Adds new peer keys to the tracker. + /// Adds a new peer key to the tracker. + /// + /// The key may be pre-generated or generated on-the-fly. + /// + /// Depending on whether an expiration duration is specified, the key will + /// be either expiring or permanent. /// - /// Keys can be pre-generated or randomly created. They can also be - /// permanent or expire. + /// # Parameters + /// + /// - `add_key_req`: The request containing options for key creation. /// /// # Errors /// - /// Will return an error if: + /// Returns an error if: /// - /// - The key duration overflows the duration type maximum value. + /// - The provided key duration exceeds the maximum allowed value. /// - The provided pre-generated key is invalid. - /// - The key could not been persisted due to database issues. + /// - There is an error persisting the key in the database. pub async fn add_peer_key(&self, add_key_req: AddKeyRequest) -> Result { if let Some(pre_existing_key) = add_key_req.opt_key { // Pre-generated key @@ -125,29 +154,31 @@ impl KeysHandler { } } - /// It generates a new permanent authentication key. + /// Generates a new permanent authentication key. /// - /// Authentication keys are used by HTTP trackers. + /// Permanent keys do not expire. /// /// # Errors /// - /// Will return a `database::Error` if unable to add the `auth_key` to the database. + /// Returns a `databases::error::Error` if the key cannot be persisted in + /// the database. pub(crate) async fn generate_permanent_peer_key(&self) -> Result { self.generate_expiring_peer_key(None).await } - /// It generates a new expiring authentication key. + /// Generates a new authentication key with an optional expiration lifetime. /// - /// Authentication keys are used by HTTP trackers. + /// If a `lifetime` is provided, the generated key will expire after that + /// duration. The new key is stored both in the database and in memory. /// - /// # Errors + /// # Parameters /// - /// Will return a `database::Error` if unable to add the `auth_key` to the database. + /// - `lifetime`: An optional duration specifying how long the key is valid. /// - /// # Arguments + /// # Errors /// - /// * `lifetime` - The duration in seconds for the new key. The key will be - /// no longer valid after `lifetime` seconds. + /// Returns a `databases::error::Error` if there is an issue adding the key + /// to the database. pub async fn generate_expiring_peer_key(&self, lifetime: Option) -> Result { let peer_key = key::generate_key(lifetime); @@ -158,36 +189,36 @@ impl KeysHandler { Ok(peer_key) } - /// It adds a pre-generated permanent authentication key. + /// Adds a pre-generated permanent authentication key. /// - /// Authentication keys are used by HTTP trackers. + /// Internally, this calls `add_expiring_peer_key` with no expiration. /// - /// # Errors + /// # Parameters /// - /// Will return a `database::Error` if unable to add the `auth_key` to the - /// database. For example, if the key already exist. + /// - `key`: The pre-generated key. /// - /// # Arguments + /// # Errors /// - /// * `key` - The pre-generated key. + /// Returns a `databases::error::Error` if there is an issue persisting the + /// key. pub(crate) async fn add_permanent_peer_key(&self, key: Key) -> Result { self.add_expiring_peer_key(key, None).await } - /// It adds a pre-generated authentication key. + /// Adds a pre-generated authentication key with an optional expiration. /// - /// Authentication keys are used by HTTP trackers. + /// The key is stored in both the database and the in-memory repository. /// - /// # Errors + /// # Parameters /// - /// Will return a `database::Error` if unable to add the `auth_key` to the - /// database. For example, if the key already exist. + /// - `key`: The pre-generated key. + /// - `valid_until`: An optional timestamp (as a duration since the Unix + /// epoch) after which the key expires. /// - /// # Arguments + /// # Errors /// - /// * `key` - The pre-generated key. - /// * `lifetime` - The duration in seconds for the new key. The key will be - /// no longer valid after `lifetime` seconds. + /// Returns a `databases::error::Error` if there is an issue adding the key + /// to the database. pub(crate) async fn add_expiring_peer_key( &self, key: Key, @@ -205,11 +236,18 @@ impl KeysHandler { Ok(peer_key) } - /// It removes an authentication key. + /// Removes an authentication key. + /// + /// The key is removed from both the database and the in-memory repository. + /// + /// # Parameters + /// + /// - `key`: A reference to the key to be removed. /// /// # Errors /// - /// Will return a `database::Error` if unable to remove the `key` to the database. + /// Returns a `databases::error::Error` if the key cannot be removed from + /// the database. pub async fn remove_peer_key(&self, key: &Key) -> Result<(), databases::error::Error> { self.db_key_repository.remove(key)?; @@ -218,19 +256,26 @@ impl KeysHandler { Ok(()) } - /// It removes an authentication key from memory. + /// Removes an authentication key from the in-memory repository. + /// + /// This function does not interact with the database. + /// + /// # Parameters + /// + /// - `key`: A reference to the key to be removed. pub(crate) async fn remove_in_memory_auth_key(&self, key: &Key) { self.in_memory_key_repository.remove(key).await; } - /// 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. + /// Loads all authentication keys from the database into the in-memory + /// repository. + /// + /// This is useful during tracker startup to ensure that all persisted keys + /// are available in memory. /// /// # Errors /// - /// Will return a `database::Error` if unable to `load_keys` from the database. + /// Returns a `databases::error::Error` if there is an issue loading the keys from the database. pub async fn load_peer_keys_from_database(&self) -> Result<(), databases::error::Error> { let keys_from_database = self.db_key_repository.load_keys()?; diff --git a/packages/tracker-core/src/authentication/key/mod.rs b/packages/tracker-core/src/authentication/key/mod.rs index fce18c0d..64814392 100644 --- a/packages/tracker-core/src/authentication/key/mod.rs +++ b/packages/tracker-core/src/authentication/key/mod.rs @@ -1,42 +1,45 @@ -//! Tracker authentication services and structs. +//! Tracker authentication services and types. //! -//! 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. +//! This module provides functions and data structures for handling tracker keys. +//! Tracker keys are tokens used to authenticate tracker clients when the +//! tracker is running in `private` mode. //! -//! There are services to [`generate_key`] and [`verify_key_expiration`] authentication keys. +//! Authentication keys are used exclusively by HTTP trackers. Every key has an +//! expiration time, meaning that it is only valid for a predetermined period. +//! Once the expiration time is reached, an expiring key will be rejected. //! -//! 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. +//! The primary key structure is [`PeerKey`], which couples a randomly generated +//! [`Key`] (a 32-character alphanumeric string) with an optional expiration +//! timestamp. //! -//! Keys are stored in this struct: +//! # Examples //! -//! ```rust,no_run +//! Generating a new key valid for `9999` seconds: +//! +//! ```rust +//! use bittorrent_tracker_core::authentication; +//! use std::time::Duration; +//! +//! let expiring_key = authentication::key::generate_key(Some(Duration::new(9999, 0))); +//! +//! // Later, verify that the key is still valid. +//! assert!(authentication::key::verify_key_expiration(&expiring_key).is_ok()); +//! ``` +//! +//! The core key types are defined as follows: +//! +//! ```rust //! use bittorrent_tracker_core::authentication::Key; //! use torrust_tracker_primitives::DurationSinceUnixEpoch; //! //! pub struct PeerKey { -//! /// Random 32-char string. For example: `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ` +//! /// A random 32-character authentication token (e.g., `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ`) //! pub key: Key, //! -//! /// Timestamp, the key will be no longer valid after this timestamp. -//! /// If `None` the keys will not expire (permanent key). +//! /// The timestamp after which the key expires. If `None`, the key is permanent. //! pub valid_until: Option, //! } //! ``` -//! -//! You can generate a new key valid for `9999` seconds and `0` nanoseconds from the current time with the following: -//! -//! ```rust,no_run -//! use bittorrent_tracker_core::authentication; -//! use std::time::Duration; -//! -//! let expiring_key = authentication::key::generate_key(Some(Duration::new(9999, 0))); -//! -//! // And you can later verify it with: -//! -//! assert!(authentication::key::verify_key_expiration(&expiring_key).is_ok()); -//! ``` pub mod peer_key; pub mod repository; @@ -75,17 +78,33 @@ pub(crate) fn generate_expiring_key(lifetime: Duration) -> PeerKey { generate_key(Some(lifetime)) } -/// It generates a new random 32-char authentication [`PeerKey`]. +/// Generates a new random 32-character authentication key (`PeerKey`). /// -/// It can be an expiring or permanent key. +/// If a lifetime is provided, the generated key will expire after the specified +/// duration; otherwise, the key is permanent (i.e., it never expires). /// /// # Panics /// -/// It would panic if the `lifetime: Duration` + Duration is more than `Duration::MAX`. +/// Panics if the addition of the lifetime to the current time overflows +/// (an extremely unlikely event). /// /// # Arguments /// -/// * `lifetime`: if `None` the key will be permanent. +/// * `lifetime`: An optional duration specifying how long the key is valid. +/// If `None`, the key is permanent. +/// +/// # Examples +/// +/// ```rust +/// use bittorrent_tracker_core::authentication::key; +/// use std::time::Duration; +/// +/// // Generate an expiring key valid for 3600 seconds. +/// let expiring_key = key::generate_key(Some(Duration::from_secs(3600))); +/// +/// // Generate a permanent key. +/// let permanent_key = key::generate_key(None); +/// ``` #[must_use] pub fn generate_key(lifetime: Option) -> PeerKey { let random_key = Key::random(); @@ -107,13 +126,27 @@ pub fn generate_key(lifetime: Option) -> PeerKey { } } -/// It verifies an [`PeerKey`]. It checks if the expiration date has passed. -/// Permanent keys without duration (`None`) do not expire. +/// Verifies whether a given authentication key (`PeerKey`) is still valid. +/// +/// For expiring keys, this function compares the key's expiration timestamp +/// against the current time. Permanent keys (with `None` as their expiration) +/// are always valid. /// /// # Errors /// -/// Will return a verification error [`crate::authentication::key::Error`] if -/// it cannot verify the key. +/// Returns a verification error of type [`enum@Error`] if the key has expired. +/// +/// # Examples +/// +/// ```rust +/// use bittorrent_tracker_core::authentication::key; +/// use std::time::Duration; +/// +/// let expiring_key = key::generate_key(Some(Duration::from_secs(100))); +/// +/// // If the key's expiration time has passed, the verification will fail. +/// assert!(key::verify_key_expiration(&expiring_key).is_ok()); +/// ``` pub fn verify_key_expiration(auth_key: &PeerKey) -> Result<(), Error> { let current_time: DurationSinceUnixEpoch = CurrentClock::now(); @@ -136,17 +169,20 @@ pub fn verify_key_expiration(auth_key: &PeerKey) -> Result<(), Error> { #[derive(Debug, Error)] #[allow(dead_code)] pub enum Error { + /// Wraps an underlying error encountered during key verification. #[error("Key could not be verified: {source}")] KeyVerificationError { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, }, + /// Indicates that the key could not be read or found. #[error("Failed to read key: {key}, {location}")] UnableToReadKey { location: &'static Location<'static>, key: Box, }, + /// Indicates that the key has expired. #[error("Key has expired, {location}")] KeyExpired { location: &'static Location<'static> }, } diff --git a/packages/tracker-core/src/authentication/key/peer_key.rs b/packages/tracker-core/src/authentication/key/peer_key.rs index 1d2b1fad..41aba950 100644 --- a/packages/tracker-core/src/authentication/key/peer_key.rs +++ b/packages/tracker-core/src/authentication/key/peer_key.rs @@ -1,3 +1,13 @@ +//! Authentication keys for private trackers. +//! +//! This module defines the types and functionality for managing authentication +//! keys used by the tracker. These keys, represented by the `Key` and `PeerKey` +//! types, are essential for authenticating peers in private tracker +//! environments. +//! +//! A `Key` is a 32-character alphanumeric token, while a `PeerKey` couples a +//! `Key` with an optional expiration timestamp. If the expiration is set (via +//! `valid_until`), the key will become invalid after that time. use std::str::FromStr; use std::time::Duration; @@ -11,22 +21,42 @@ use torrust_tracker_primitives::DurationSinceUnixEpoch; use super::AUTH_KEY_LENGTH; -/// An authentication key which can potentially have an expiration time. -/// After that time is will automatically become invalid. +/// A peer authentication key with an optional expiration time. +/// +/// A `PeerKey` associates a generated `Key` (a 32-character alphanumeric string) +/// with an optional expiration timestamp (`valid_until`). If `valid_until` is +/// `None`, the key is considered permanent. +/// +/// # Example +/// +/// ```rust +/// use std::time::Duration; +/// use bittorrent_tracker_core::authentication::key::peer_key::{Key, PeerKey}; +/// +/// let expiring_key = PeerKey { +/// key: Key::random(), +/// valid_until: Some(Duration::from_secs(3600)), // Expires in 1 hour +/// }; +/// +/// let permanent_key = PeerKey { +/// key: Key::random(), +/// valid_until: None, +/// }; +/// ``` #[derive(Serialize, Deserialize, Debug, Clone)] pub struct PeerKey { - /// Random 32-char string. For example: `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ` + /// A 32-character authentication key. For example: `YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ` pub key: Key, - /// Timestamp, the key will be no longer valid after this timestamp. - /// If `None` the keys will not expire (permanent key). + /// An optional expiration timestamp. If set, the key becomes invalid after + /// this time. A value of `None` indicates a permanent key. pub valid_until: Option, } impl PartialEq for PeerKey { fn eq(&self, other: &Self) -> bool { - // We ignore the fractions of seconds when comparing the timestamps - // because we only store the seconds in the database. + // When comparing two PeerKeys, ignore fractions of seconds since only + // whole seconds are stored in the database. self.key == other.key && match (&self.valid_until, &other.valid_until) { (Some(a), Some(b)) => a.as_secs() == b.as_secs(), @@ -53,14 +83,17 @@ impl PeerKey { self.key.clone() } - /// It returns the expiry time. For example, for the starting time for Unix Epoch - /// (timestamp 0) it will return a `DateTime` whose string representation is - /// `1970-01-01 00:00:00 UTC`. + /// Computes and returns the expiration time as a UTC `DateTime`, if one + /// exists. + /// + /// The returned time is derived from the stored seconds since the Unix + /// epoch. Note that any fractional seconds are discarded since only whole + /// seconds are stored in the database. /// /// # Panics /// - /// Will panic when the key timestamp overflows the internal i64 type. - /// (this will naturally happen in 292.5 billion years) + /// Panics if the key's timestamp overflows the internal `i64` type (this is + /// extremely unlikely, happening roughly 292.5 billion years from now). #[must_use] pub fn expiry_time(&self) -> Option> { // We remove the fractions of seconds because we only store the seconds @@ -72,17 +105,37 @@ impl PeerKey { /// A token used for authentication. /// -/// - It contains only ascii alphanumeric chars: lower and uppercase letters and -/// numbers. -/// - It's a 32-char string. +/// The `Key` type encapsulates a 32-character string that must consist solely +/// of ASCII alphanumeric characters (0-9, a-z, A-Z). This key is used by the +/// tracker to authenticate peers. +/// +/// # Examples +/// +/// Creating a key from a valid string: +/// +/// ``` +/// use bittorrent_tracker_core::authentication::key::peer_key::Key; +/// let key = Key::new("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap(); +/// ``` +/// +/// Generating a random key: +/// +/// ``` +/// use bittorrent_tracker_core::authentication::key::peer_key::Key; +/// let random_key = Key::random(); +/// ``` #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Display, Hash)] pub struct Key(String); impl Key { + /// Constructs a new `Key` from the given string. + /// /// # Errors /// - /// Will return an error is the string represents an invalid key. - /// Valid keys can only contain 32 chars including 0-9, a-z and A-Z. + /// Returns a `ParseKeyError` if: + /// + /// - The input string does not have exactly 32 characters. + /// - The input string contains characters that are not ASCII alphanumeric. pub fn new(value: &str) -> Result { if value.len() != AUTH_KEY_LENGTH { return Err(ParseKeyError::InvalidKeyLength); @@ -95,11 +148,14 @@ impl Key { Ok(Self(value.to_owned())) } - /// It generates a random key. + /// Generates a new random authentication key. + /// + /// The random key is generated by sampling 32 ASCII alphanumeric characters. /// /// # Panics /// - /// Will panic if the random number generator fails to generate a valid key. + /// Panics if the random number generator fails to produce a valid key + /// (extremely unlikely). pub fn random() -> Self { let random_id: String = rng() .sample_iter(&Alphanumeric) @@ -115,9 +171,11 @@ impl Key { } } -/// Error returned when a key cannot be parsed from a string. +/// Errors that can occur when parsing a string into a `Key`. +/// +/// # Examples /// -/// ```text +/// ```rust /// use bittorrent_tracker_core::authentication::Key; /// use std::str::FromStr; /// @@ -132,9 +190,12 @@ impl Key { /// this error. #[derive(Debug, Error)] pub enum ParseKeyError { + /// The provided key does not have exactly 32 characters. #[error("Invalid key length. Key must be have 32 chars")] InvalidKeyLength, + /// The provided key contains invalid characters. Only ASCII alphanumeric + /// characters are allowed. #[error("Invalid chars for key. Key can only alphanumeric chars (0-9, a-z, A-Z)")] InvalidChars, } diff --git a/packages/tracker-core/src/authentication/key/repository/in_memory.rs b/packages/tracker-core/src/authentication/key/repository/in_memory.rs index 13664e27..5911771d 100644 --- a/packages/tracker-core/src/authentication/key/repository/in_memory.rs +++ b/packages/tracker-core/src/authentication/key/repository/in_memory.rs @@ -1,6 +1,11 @@ +//! In-memory implementation of the authentication key repository. use crate::authentication::key::{Key, PeerKey}; -/// In-memory implementation of the authentication key repository. +/// An in-memory repository for storing authentication keys. +/// +/// This repository maintains a mapping between a peer's [`Key`] and its +/// corresponding [`PeerKey`]. It is designed for use in private tracker +/// environments where keys are maintained in memory. #[derive(Debug, Default)] pub struct InMemoryKeyRepository { /// Tracker users' keys. Only for private trackers. @@ -8,28 +13,66 @@ pub struct InMemoryKeyRepository { } impl InMemoryKeyRepository { - /// It adds a new authentication key. + /// Inserts a new authentication key into the repository. + /// + /// This function acquires a write lock on the internal storage and inserts + /// the provided [`PeerKey`], using its inner [`Key`] as the map key. + /// + /// # Arguments + /// + /// * `auth_key` - A reference to the [`PeerKey`] to be inserted. pub(crate) async fn insert(&self, auth_key: &PeerKey) { self.keys.write().await.insert(auth_key.key.clone(), auth_key.clone()); } - /// It removes an authentication key. + /// Removes an authentication key from the repository. + /// + /// This function acquires a write lock on the internal storage and removes + /// the key that matches the provided [`Key`]. + /// + /// # Arguments + /// + /// * `key` - A reference to the [`Key`] corresponding to the key to be removed. pub(crate) async fn remove(&self, key: &Key) { self.keys.write().await.remove(key); } + /// Retrieves an authentication key from the repository. + /// + /// This function acquires a read lock on the internal storage and returns a + /// cloned [`PeerKey`] if the provided [`Key`] exists. + /// + /// # Arguments + /// + /// * `key` - A reference to the [`Key`] to look up. + /// + /// # Returns + /// + /// An `Option` containing the matching key if found, or `None` + /// otherwise. pub(crate) async fn get(&self, key: &Key) -> Option { self.keys.read().await.get(key).cloned() } - /// It clears all the authentication keys. + /// Clears all authentication keys from the repository. + /// + /// This function acquires a write lock on the internal storage and removes + /// all entries. #[allow(dead_code)] pub(crate) async fn clear(&self) { let mut keys = self.keys.write().await; keys.clear(); } - /// It resets the authentication keys with a new list of keys. + /// Resets the repository with a new list of authentication keys. + /// + /// This function clears all existing keys and then inserts each key from + /// the provided vector. + /// + /// # Arguments + /// + /// * `peer_keys` - A vector of [`PeerKey`] instances that will replace the + /// current set of keys. pub async fn reset_with(&self, peer_keys: Vec) { let mut keys_lock = self.keys.write().await; diff --git a/packages/tracker-core/src/authentication/key/repository/mod.rs b/packages/tracker-core/src/authentication/key/repository/mod.rs index 51723b68..3df78362 100644 --- a/packages/tracker-core/src/authentication/key/repository/mod.rs +++ b/packages/tracker-core/src/authentication/key/repository/mod.rs @@ -1,2 +1,3 @@ +//! Key repository implementations. pub mod in_memory; pub mod persisted; diff --git a/packages/tracker-core/src/authentication/key/repository/persisted.rs b/packages/tracker-core/src/authentication/key/repository/persisted.rs index 95a3b874..e84a23c9 100644 --- a/packages/tracker-core/src/authentication/key/repository/persisted.rs +++ b/packages/tracker-core/src/authentication/key/repository/persisted.rs @@ -1,14 +1,28 @@ +//! The database repository for the authentication keys. use std::sync::Arc; use crate::authentication::key::{Key, PeerKey}; use crate::databases::{self, Database}; -/// The database repository for the authentication keys. +/// A repository for storing authentication keys in a persistent database. +/// +/// This repository provides methods to add, remove, and load authentication +/// keys from the underlying database. It wraps an instance of a type +/// implementing the [`Database`] trait. pub struct DatabaseKeyRepository { database: Arc>, } impl DatabaseKeyRepository { + /// Creates a new `DatabaseKeyRepository` instance. + /// + /// # Arguments + /// + /// * `database` - A shared reference to a boxed database implementation. + /// + /// # Returns + /// + /// A new instance of `DatabaseKeyRepository` #[must_use] pub fn new(database: &Arc>) -> Self { Self { @@ -16,31 +30,43 @@ impl DatabaseKeyRepository { } } - /// It adds a new key to the database. + /// Adds a new authentication key to the database. + /// + /// # Arguments + /// + /// * `peer_key` - A reference to the [`PeerKey`] to be persisted. /// /// # Errors /// - /// Will return a `databases::error::Error` if unable to add the `auth_key` to the database. + /// Returns a [`databases::error::Error`] if the key cannot be added. pub(crate) fn add(&self, peer_key: &PeerKey) -> Result<(), databases::error::Error> { self.database.add_key_to_keys(peer_key)?; Ok(()) } - /// It removes an key from the database. + /// Removes an authentication key from the database. + /// + /// # Arguments + /// + /// * `key` - A reference to the [`Key`] corresponding to the key to remove. /// /// # Errors /// - /// Will return a `database::Error` if unable to remove the `key` from the database. + /// Returns a [`databases::error::Error`] if the key cannot be removed. pub(crate) fn remove(&self, key: &Key) -> Result<(), databases::error::Error> { self.database.remove_key_from_keys(key)?; Ok(()) } - /// It loads all keys from the database. + /// Loads all authentication keys from the database. /// /// # Errors /// - /// Will return a `database::Error` if unable to load the keys from the database. + /// Returns a [`databases::error::Error`] if the keys cannot be loaded. + /// + /// # Returns + /// + /// A vector containing all persisted [`PeerKey`] entries. pub(crate) fn load_keys(&self) -> Result, databases::error::Error> { let keys = self.database.load_keys()?; Ok(keys) diff --git a/packages/tracker-core/src/authentication/mod.rs b/packages/tracker-core/src/authentication/mod.rs index 52138d26..12b742b8 100644 --- a/packages/tracker-core/src/authentication/mod.rs +++ b/packages/tracker-core/src/authentication/mod.rs @@ -1,3 +1,18 @@ +//! Tracker authentication services and structs. +//! +//! One of the crate responsibilities is to create and keep authentication keys. +//! Auth keys are used by HTTP trackers when the tracker is running in `private` +//! mode. +//! +//! HTTP tracker's clients need to obtain an authentication key before starting +//! requesting the tracker. Once they 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). use crate::CurrentClock; pub mod handler; diff --git a/packages/tracker-core/src/authentication/service.rs b/packages/tracker-core/src/authentication/service.rs index 5ca0a09e..75b28944 100644 --- a/packages/tracker-core/src/authentication/service.rs +++ b/packages/tracker-core/src/authentication/service.rs @@ -1,3 +1,4 @@ +//! Authentication service. use std::panic::Location; use std::sync::Arc; @@ -6,6 +7,11 @@ use torrust_tracker_configuration::Core; use super::key::repository::in_memory::InMemoryKeyRepository; use super::{key, Error, Key}; +/// The authentication service responsible for validating peer keys. +/// +/// The service uses an in-memory key repository along with the tracker +/// configuration to determine whether a given peer key is valid. In a private +/// tracker, only registered keys (and optionally unexpired keys) are allowed. #[derive(Debug)] pub struct AuthenticationService { /// The tracker configuration. @@ -16,6 +22,18 @@ pub struct AuthenticationService { } impl AuthenticationService { + /// Creates a new instance of the `AuthenticationService`. + /// + /// # Parameters + /// + /// - `config`: A reference to the tracker core configuration. + /// - `in_memory_key_repository`: A shared reference to an in-memory key + /// repository. + /// + /// # Returns + /// + /// An `AuthenticationService` instance initialized with the given + /// configuration and repository. #[must_use] pub fn new(config: &Core, in_memory_key_repository: &Arc) -> Self { Self { @@ -24,12 +42,23 @@ impl AuthenticationService { } } - /// It authenticates the peer `key` against the `Tracker` authentication - /// key list. + /// Authenticates a peer key against the tracker's authentication key list. + /// + /// For private trackers, the key must be registered (and optionally not + /// expired) to be considered valid. For public trackers, authentication + /// always succeeds. + /// + /// # Parameters + /// + /// - `key`: A reference to the peer key that needs to be authenticated. /// /// # Errors /// - /// Will return an error if the the authentication key cannot be verified. + /// Returns an error if: + /// + /// - The tracker is in private mode and the key cannot be found in the + /// repository. + /// - The key is found but fails the expiration check (if expiration is enforced). pub async fn authenticate(&self, key: &Key) -> Result<(), Error> { if self.tracker_is_private() { self.verify_auth_key(key).await @@ -44,11 +73,25 @@ impl AuthenticationService { self.config.private } - /// It verifies an authentication key. + /// Verifies the authentication key against the in-memory repository. + /// + /// This function retrieves the key from the repository. If the key is not + /// found, it returns an error with the caller's location. If the key is + /// found, the function then checks the key's expiration based on the + /// tracker configuration. The behavior differs depending on whether a + /// `private` configuration is provided and whether key expiration checking + /// is enabled. + /// + /// # Parameters + /// + /// - `key`: A reference to the peer key that needs to be verified. /// /// # Errors /// - /// Will return a `key::Error` if unable to get any `auth_key`. + /// Returns an error if: + /// + /// - The key is not found in the repository. + /// - The key fails the expiration check when such verification is required. async fn verify_auth_key(&self, key: &Key) -> Result<(), Error> { match self.in_memory_key_repository.get(key).await { None => Err(Error::UnableToReadKey { diff --git a/packages/tracker-core/src/databases/driver/mod.rs b/packages/tracker-core/src/databases/driver/mod.rs index 2bc6a1e3..06e912f7 100644 --- a/packages/tracker-core/src/databases/driver/mod.rs +++ b/packages/tracker-core/src/databases/driver/mod.rs @@ -1,7 +1,4 @@ //! Database driver factory. -//! -//! See [`databases::driver::build`](crate::core::databases::driver::build) -//! function for more information. use mysql::Mysql; use serde::{Deserialize, Serialize}; use sqlite::Sqlite; diff --git a/packages/tracker-core/src/databases/driver/mysql.rs b/packages/tracker-core/src/databases/driver/mysql.rs index 365bd0ad..624e34c9 100644 --- a/packages/tracker-core/src/databases/driver/mysql.rs +++ b/packages/tracker-core/src/databases/driver/mysql.rs @@ -1,4 +1,10 @@ //! The `MySQL` database driver. +//! +//! This module provides an implementation of the [`Database`] trait for `MySQL` +//! using the `r2d2_mysql` connection pool. It configures the MySQL connection +//! based on a URL, creates the necessary tables (for torrent metrics, torrent +//! whitelist, and authentication keys), and implements all CRUD operations +//! required by the persistence layer. use std::str::FromStr; use std::time::Duration; @@ -15,6 +21,11 @@ use crate::authentication::{self, Key}; const DRIVER: Driver = Driver::MySQL; +/// `MySQL` driver implementation. +/// +/// This struct encapsulates a connection pool for `MySQL`, built using the +/// `r2d2_mysql` connection manager. It implements the [`Database`] trait to +/// provide persistence operations. pub(crate) struct Mysql { pool: Pool, } diff --git a/packages/tracker-core/src/databases/driver/sqlite.rs b/packages/tracker-core/src/databases/driver/sqlite.rs index 36ca4eab..bab2fb6a 100644 --- a/packages/tracker-core/src/databases/driver/sqlite.rs +++ b/packages/tracker-core/src/databases/driver/sqlite.rs @@ -1,4 +1,10 @@ //! The `SQLite3` database driver. +//! +//! This module provides an implementation of the [`Database`] trait for +//! `SQLite3` using the `r2d2_sqlite` connection pool. It defines the schema for +//! whitelist, torrent metrics, and authentication keys, and provides methods +//! to create and drop tables as well as perform CRUD operations on these +//! persistent objects. use std::panic::Location; use std::str::FromStr; @@ -14,18 +20,29 @@ use crate::authentication::{self, Key}; const DRIVER: Driver = Driver::Sqlite3; +/// `SQLite` driver implementation. +/// +/// This struct encapsulates a connection pool for `SQLite` using the `r2d2_sqlite` +/// connection manager. pub(crate) struct Sqlite { pool: Pool, } impl Sqlite { - /// It instantiates a new `SQLite3` database driver. + /// Instantiates a new `SQLite3` database driver. /// - /// Refer to [`databases::Database::new`](crate::core::databases::Database::new). + /// This function creates a connection manager for the `SQLite` database + /// located at `db_path` and then builds a connection pool using `r2d2`. If + /// the pool cannot be created, an error is returned (wrapped with the + /// appropriate driver information). + /// + /// # Arguments + /// + /// * `db_path` - A string slice representing the file path to the `SQLite` database. /// /// # Errors /// - /// Will return `r2d2::Error` if `db_path` is not able to create `SqLite` database. + /// Returns an [`Error`] if the connection pool cannot be built. pub fn new(db_path: &str) -> Result { let manager = SqliteConnectionManager::file(db_path); let pool = r2d2::Pool::builder().build(manager).map_err(|e| (e, DRIVER))?; diff --git a/packages/tracker-core/src/databases/error.rs b/packages/tracker-core/src/databases/error.rs index 0f320758..fd9adfc2 100644 --- a/packages/tracker-core/src/databases/error.rs +++ b/packages/tracker-core/src/databases/error.rs @@ -1,6 +1,13 @@ //! Database errors. //! -//! This module contains the [Database errors](crate::core::databases::error::Error). +//! This module defines the [`Error`] enum used to represent errors that occur +//! during database operations. These errors encapsulate issues such as missing +//! query results, malformed queries, connection failures, and connection pool +//! creation errors. Each error variant includes contextual information such as +//! the associated database driver and, when applicable, the source error. +//! +//! External errors from database libraries (e.g., `rusqlite`, `mysql`) are +//! converted into this error type using the provided `From` implementations. use std::panic::Location; use std::sync::Arc; @@ -9,30 +16,43 @@ use torrust_tracker_located_error::{DynError, Located, LocatedError}; use super::driver::Driver; +/// Database error type that encapsulates various failures encountered during +/// database operations. #[derive(thiserror::Error, Debug, Clone)] pub enum Error { - /// The query unexpectedly returned nothing. + /// Indicates that a query unexpectedly returned no rows. + /// + /// This error variant is used when a query that is expected to return a + /// result does not. #[error("The {driver} query unexpectedly returned nothing: {source}")] QueryReturnedNoRows { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, driver: Driver, }, - /// The query was malformed. + /// Indicates that the query was malformed. + /// + /// This error variant is used when the SQL query itself is invalid or + /// improperly formatted. #[error("The {driver} query was malformed: {source}")] InvalidQuery { source: LocatedError<'static, dyn std::error::Error + Send + Sync>, driver: Driver, }, - /// Unable to insert a record into the database + /// Indicates a failure to insert a record into the database. + /// + /// This error is raised when an insertion operation fails. #[error("Unable to insert record into {driver} database, {location}")] InsertFailed { location: &'static Location<'static>, driver: Driver, }, - /// Unable to delete a record into the database + /// Indicates a failure to delete a record from the database. + /// + /// This error includes an error code that may be returned by the database + /// driver. #[error("Failed to remove record from {driver} database, error-code: {error_code}, {location}")] DeleteFailed { location: &'static Location<'static>, @@ -40,14 +60,18 @@ pub enum Error { driver: Driver, }, - /// Unable to connect to the database + /// Indicates a failure to connect to the database. + /// + /// This error variant wraps connection-related errors, such as those caused by an invalid URL. #[error("Failed to connect to {driver} database: {source}")] ConnectionError { source: LocatedError<'static, UrlError>, driver: Driver, }, - /// Unable to create a connection pool + /// Indicates a failure to create a connection pool. + /// + /// This error variant is used when the connection pool creation (using r2d2) fails. #[error("Failed to create r2d2 {driver} connection pool: {source}")] ConnectionPool { source: LocatedError<'static, r2d2::Error>, diff --git a/packages/tracker-core/src/databases/mod.rs b/packages/tracker-core/src/databases/mod.rs index 01025213..33a7e3c6 100644 --- a/packages/tracker-core/src/databases/mod.rs +++ b/packages/tracker-core/src/databases/mod.rs @@ -1,48 +1,51 @@ //! The persistence module. //! -//! Persistence is currently implemented with one [`Database`] trait. +//! Persistence is currently implemented using a single [`Database`] trait. //! //! There are two implementations of the trait (two drivers): //! -//! - [`Mysql`](crate::core::databases::mysql::Mysql) -//! - [`Sqlite`](crate::core::databases::sqlite::Sqlite) +//! - **`MySQL`** +//! - **`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. +//! > **NOTICE**: There are no database migrations at this time. If schema +//! > changes occur, either migration functionality will be implemented or a +//! > script will be provided to migrate to the new schema. //! -//! The persistent objects are: +//! The persistent objects handled by this module include: //! -//! - [Torrent metrics](#torrent-metrics) -//! - [Torrent whitelist](torrent-whitelist) -//! - [Authentication keys](authentication-keys) +//! - **Torrent metrics**: Metrics such as the number of completed downloads for +//! each torrent. +//! - **Torrent whitelist**: A list of torrents (by infohash) that are allowed. +//! - **Authentication keys**: Expiring authentication keys used to secure +//! access to private trackers. //! -//! # Torrent metrics +//! # 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`](torrust_tracker_torrent_repository::entry::Entry) for more information. +//! | Field | Sample data | Description | +//! |-------------|--------------------------------------------|-----------------------------------------------------------------------------| +//! | `id` | 1 | Auto-increment id | +//! | `info_hash` | `c1277613db1d28709b034a017ab2cae4be07ae10` | `BitTorrent` infohash V1 | +//! | `completed` | 20 | The number of peers that have completed downloading the associated torrent. | //! -//! > **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. +//! > **NOTICE**: The peer list for a torrent is not persisted. Because peers re-announce at +//! > intervals, the peer list is regenerated periodically. //! -//! # Torrent whitelist +//! # Torrent Whitelist //! -//! Field | Sample data | Description -//! ---|---|--- -//! `id` | 1 | Autoincrement id -//! `info_hash` | `c1277613db1d28709b034a017ab2cae4be07ae10` | `BitTorrent` infohash V1 +//! | Field | Sample data | Description | +//! |-------------|--------------------------------------------|--------------------------------| +//! | `id` | 1 | Auto-increment id | +//! | `info_hash` | `c1277613db1d28709b034a017ab2cae4be07ae10` | `BitTorrent` infohash V1 | //! -//! # Authentication keys +//! # Authentication Keys //! -//! Field | Sample data | Description -//! ---|---|--- -//! `id` | 1 | Autoincrement id -//! `key` | `IrweYtVuQPGbG9Jzx1DihcPmJGGpVy82` | Token -//! `valid_until` | 1672419840 | Timestamp for the expiring date +//! | Field | Sample data | Description | +//! |---------------|------------------------------------|--------------------------------------| +//! | `id` | 1 | Auto-increment id | +//! | `key` | `IrweYtVuQPGbG9Jzx1DihcPmJGGpVy82` | Authentication token (32 chars) | +//! | `valid_until` | 1672419840 | Timestamp indicating expiration time | //! -//! > **NOTICE**: All keys must have an expiration date. +//! > **NOTICE**: All authentication keys must have an expiration date. pub mod driver; pub mod error; pub mod setup; @@ -54,143 +57,159 @@ use torrust_tracker_primitives::PersistentTorrents; use self::error::Error; use crate::authentication::{self, Key}; -/// The persistence trait. It contains all the methods to interact with the database. +/// The persistence trait. +/// +/// This trait defines all the methods required to interact with the database, +/// including creating and dropping schema tables, and CRUD operations for +/// torrent metrics, whitelists, and authentication keys. Implementations of +/// this trait must ensure that operations are safe, consistent, and report +/// errors using the [`Error`] type. #[automock] pub trait Database: Sync + Send { - /// It generates the database tables. SQL queries are hardcoded in the trait - /// implementation. + /// Creates the necessary database tables. + /// + /// The SQL queries for table creation are hardcoded in the trait implementation. /// /// # Context: Schema /// /// # Errors /// - /// Will return `Error` if unable to create own tables. + /// Returns an [`Error`] if the tables cannot be created. fn create_database_tables(&self) -> Result<(), Error>; - /// It drops the database tables. + /// Drops the database tables. + /// + /// This operation removes the persistent schema. /// /// # Context: Schema /// /// # Errors /// - /// Will return `Err` if unable to drop tables. + /// Returns an [`Error`] if the tables cannot be dropped. fn drop_database_tables(&self) -> Result<(), Error>; // Torrent Metrics - /// It loads the torrent metrics data from the database. + /// Loads torrent metrics data from the database. /// - /// It returns an array of tuples with the torrent - /// [`InfoHash`] and the - /// [`downloaded`](torrust_tracker_torrent_repository::entry::Torrent::downloaded) counter - /// which is the number of times the torrent has been downloaded. - /// See [`Entry::downloaded`](torrust_tracker_torrent_repository::entry::Torrent::downloaded). + /// This function returns the persistent torrent metrics as a collection of + /// tuples, where each tuple contains an [`InfoHash`] and the `downloaded` + /// counter (i.e. the number of times the torrent has been downloaded). /// /// # Context: Torrent Metrics /// /// # Errors /// - /// Will return `Err` if unable to load. + /// Returns an [`Error`] if the metrics cannot be loaded. fn load_persistent_torrents(&self) -> Result; - /// It saves the torrent metrics data into the database. + /// Saves torrent metrics data into the database. + /// + /// # Arguments + /// + /// * `info_hash` - A reference to the torrent's info hash. + /// * `downloaded` - The number of times the torrent has been downloaded. /// /// # Context: Torrent Metrics /// /// # Errors /// - /// Will return `Err` if unable to save. + /// Returns an [`Error`] if the metrics cannot be saved. fn save_persistent_torrent(&self, info_hash: &InfoHash, downloaded: u32) -> Result<(), Error>; // Whitelist - /// It loads the whitelisted torrents from the database. + /// Loads the whitelisted torrents from the database. /// /// # Context: Whitelist /// /// # Errors /// - /// Will return `Err` if unable to load. + /// Returns an [`Error`] if the whitelist cannot be loaded. fn load_whitelist(&self) -> Result, Error>; - /// It checks if the torrent is whitelisted. + /// Retrieves a whitelisted torrent from the database. /// - /// It returns `Some(InfoHash)` if the torrent is whitelisted, `None` otherwise. + /// Returns `Some(InfoHash)` if the torrent is in the whitelist, or `None` + /// otherwise. /// /// # Context: Whitelist /// /// # Errors /// - /// Will return `Err` if unable to load. + /// Returns an [`Error`] if the whitelist cannot be queried. fn get_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result, Error>; - /// It adds the torrent to the whitelist. + /// Adds a torrent to the whitelist. /// /// # Context: Whitelist /// /// # Errors /// - /// Will return `Err` if unable to save. + /// Returns an [`Error`] if the torrent cannot be added to the whitelist. fn add_info_hash_to_whitelist(&self, info_hash: InfoHash) -> Result; - /// It checks if the torrent is whitelisted. + /// Checks whether a torrent is whitelisted. + /// + /// This default implementation returns `true` if the infohash is included + /// in the whitelist, or `false` otherwise. /// /// # Context: Whitelist /// /// # Errors /// - /// Will return `Err` if unable to load. + /// Returns an [`Error`] if the whitelist cannot be queried. fn is_info_hash_whitelisted(&self, info_hash: InfoHash) -> Result { Ok(self.get_info_hash_from_whitelist(info_hash)?.is_some()) } - /// It removes the torrent from the whitelist. + /// Removes a torrent from the whitelist. /// /// # Context: Whitelist /// /// # Errors /// - /// Will return `Err` if unable to save. + /// Returns an [`Error`] if the torrent cannot be removed from the whitelist. fn remove_info_hash_from_whitelist(&self, info_hash: InfoHash) -> Result; // Authentication keys - /// It loads the expiring authentication keys from the database. + /// Loads all authentication keys from the database. /// /// # Context: Authentication Keys /// /// # Errors /// - /// Will return `Err` if unable to load. + /// Returns an [`Error`] if the keys cannot be loaded. fn load_keys(&self) -> Result, Error>; - /// It gets an expiring authentication key from the database. + /// Retrieves a specific authentication key from the database. /// - /// It returns `Some(PeerKey)` if a [`PeerKey`](crate::authentication::PeerKey) - /// with the input [`Key`] exists, `None` otherwise. + /// Returns `Some(PeerKey)` if a key corresponding to the provided [`Key`] + /// exists, or `None` otherwise. /// /// # Context: Authentication Keys /// /// # Errors /// - /// Will return `Err` if unable to load. + /// Returns an [`Error`] if the key cannot be queried. fn get_key_from_keys(&self, key: &Key) -> Result, Error>; - /// It adds an expiring authentication key to the database. + /// Adds an authentication key to the database. /// /// # Context: Authentication Keys /// /// # Errors /// - /// Will return `Err` if unable to save. + /// Returns an [`Error`] if the key cannot be saved. fn add_key_to_keys(&self, auth_key: &authentication::PeerKey) -> Result; - /// It removes an expiring authentication key from the database. + /// Removes an authentication key from the database. /// /// # Context: Authentication Keys /// /// # Errors /// - /// Will return `Err` if unable to load. + /// Returns an [`Error`] if the key cannot be removed. fn remove_key_from_keys(&self, key: &Key) -> Result; } diff --git a/packages/tracker-core/src/databases/setup.rs b/packages/tracker-core/src/databases/setup.rs index 73ff23fe..6ba9f2a6 100644 --- a/packages/tracker-core/src/databases/setup.rs +++ b/packages/tracker-core/src/databases/setup.rs @@ -1,3 +1,4 @@ +//! This module provides functionality for setting up databases. use std::sync::Arc; use torrust_tracker_configuration::Core; @@ -5,9 +6,38 @@ use torrust_tracker_configuration::Core; use super::driver::{self, Driver}; use super::Database; +/// Initializes and returns a database instance based on the provided configuration. +/// +/// This function creates a new database instance according to the settings +/// defined in the [`Core`] configuration. It selects the appropriate driver +/// (either `Sqlite3` or `MySQL`) as specified in `config.database.driver` and +/// attempts to build the database connection using the path defined in +/// `config.database.path`. +/// +/// The resulting database instance is wrapped in a shared pointer (`Arc`) to a +/// boxed trait object, allowing safe sharing of the database connection across +/// multiple threads. +/// /// # Panics /// -/// Will panic if database cannot be initialized. +/// This function will panic if the database cannot be initialized (i.e., if the +/// driver fails to build the connection). This is enforced by the use of +/// [`expect`](std::result::Result::expect) in the implementation. +/// +/// # Example +/// +/// ```rust,no_run +/// use torrust_tracker_configuration::Core; +/// use bittorrent_tracker_core::databases::setup::initialize_database; +/// +/// // Create a default configuration (ensure it is properly set up for your environment) +/// let config = Core::default(); +/// +/// // Initialize the database; this will panic if initialization fails. +/// let database = initialize_database(&config); +/// +/// // The returned database instance can now be used for persistence operations. +/// ``` #[must_use] pub fn initialize_database(config: &Core) -> Arc> { let driver = match config.database.driver { @@ -17,3 +47,15 @@ pub fn initialize_database(config: &Core) -> Arc> { Arc::new(driver::build(&driver, &config.database.path).expect("Database driver build failed.")) } + +#[cfg(test)] +mod tests { + use super::initialize_database; + use crate::test_helpers::tests::ephemeral_configuration; + + #[test] + fn it_should_initialize_the_sqlite_database() { + let config = ephemeral_configuration(); + let _database = initialize_database(&config); + } +} diff --git a/packages/tracker-core/src/error.rs b/packages/tracker-core/src/error.rs index dcdd8966..99ac48ed 100644 --- a/packages/tracker-core/src/error.rs +++ b/packages/tracker-core/src/error.rs @@ -1,4 +1,12 @@ -//! Errors returned by the core tracker. +//! Core tracker errors. +//! +//! This module defines the error types used internally by the `BitTorrent` +//! tracker core. +//! +//! These errors encapsulate issues such as whitelisting violations, invalid +//! peer key data, and database persistence failures. Each error variant +//! includes contextual information (such as source code location) to facilitate +//! debugging. use std::panic::Location; use bittorrent_primitives::info_hash::InfoHash; @@ -7,9 +15,13 @@ use torrust_tracker_located_error::LocatedError; use super::authentication::key::ParseKeyError; use super::databases; -/// Whitelist errors returned by the core tracker. +/// Errors related to torrent whitelisting. +/// +/// This error is returned when an operation involves a torrent that is not +/// present in the whitelist. #[derive(thiserror::Error, Debug, Clone)] pub enum WhitelistError { + /// Indicates that the torrent identified by `info_hash` is not whitelisted. #[error("The torrent: {info_hash}, is not whitelisted, {location}")] TorrentNotWhitelisted { info_hash: InfoHash, @@ -17,19 +29,27 @@ pub enum WhitelistError { }, } -/// Peers keys errors returned by the core tracker. +/// Errors related to peer key operations. +/// +/// This error type covers issues encountered during the handling of peer keys, +/// including validation of key durations, parsing errors, and database +/// persistence problems. #[allow(clippy::module_name_repetitions)] #[derive(thiserror::Error, Debug, Clone)] pub enum PeerKeyError { + /// Returned when the duration specified for the peer key exceeds the + /// maximum. #[error("Invalid peer key duration: {seconds_valid:?}, is not valid")] DurationOverflow { seconds_valid: u64 }, + /// Returned when the provided peer key is invalid. #[error("Invalid key: {key}")] InvalidKey { key: String, source: LocatedError<'static, ParseKeyError>, }, + /// Returned when persisting the peer key to the database fails. #[error("Can't persist key: {source}")] DatabaseError { source: LocatedError<'static, databases::error::Error>, diff --git a/packages/tracker-core/src/lib.rs b/packages/tracker-core/src/lib.rs index ecbaef9c..843817de 100644 --- a/packages/tracker-core/src/lib.rs +++ b/packages/tracker-core/src/lib.rs @@ -1,316 +1,57 @@ -//! The core `tracker` module contains the generic `BitTorrent` tracker logic which is independent of the delivery layer. +//! The core `bittorrent-tracker-core` crate 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`. +//! 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: +//! Typically this crate is intended to be used by higher components 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 | +//! 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) +//! - [Introduction](#introduction) //! - [Configuration](#configuration) -//! - [Services](#services) +//! - [Announce handler](#announce-handler) +//! - [Scrape handler](#scrape-handler) //! - [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. +//! - [Databases](#databases) +//! - [Torrent](#torrent) +//! - [Whitelist](#whitelist) //! -//! 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. +//! # Introduction //! -//! Once you have instantiated the `AnnounceHandler` you can `announce` a new [`peer::Peer`](torrust_tracker_primitives::peer::Peer) with: +//! The main purpose of this crate is to provide a generic `BitTorrent` tracker. //! -//! ```rust,no_run -//! use std::net::SocketAddr; -//! use std::net::IpAddr; -//! use std::net::Ipv4Addr; -//! use std::str::FromStr; +//! It has two main responsibilities: //! -//! use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes, PeerId}; -//! use torrust_tracker_primitives::DurationSinceUnixEpoch; -//! use torrust_tracker_primitives::peer; -//! use bittorrent_primitives::info_hash::InfoHash; +//! - To handle **announce** requests. +//! - To handle **scrape** requests. //! -//! let info_hash = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(); +//! The crate has also other features: //! -//! let peer = peer::Peer { -//! peer_id: PeerId(*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::new(0), -//! downloaded: NumberOfBytes::new(0), -//! left: NumberOfBytes::new(0), -//! event: AnnounceEvent::Completed, -//! }; -//! -//! let peer_ip = IpAddr::V4(Ipv4Addr::from_str("126.0.0.1").unwrap()); -//! ``` -//! -//! ```text -//! let announce_data = announce_handler.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 -//! use torrust_tracker_primitives::peer; -//! use torrust_tracker_configuration::AnnouncePolicy; -//! -//! pub struct AnnounceData { -//! pub peers: Vec, -//! pub swarm_stats: SwarmMetadata, -//! pub policy: AnnouncePolicy, // the tracker announce policy. -//! } -//! -//! pub struct SwarmMetadata { -//! 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 AnnounceInterval { -//! // ... -//! pub interval: u32, // Interval in seconds that the client should wait between sending regular announce requests to the tracker -//! pub interval_min: 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 -//! use bittorrent_primitives::info_hash::InfoHash; -//! use std::collections::HashMap; -//! -//! 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: +//! - **Authentication**: It handles authentication keys which are used by HTTP trackers. +//! - **Persistence**: It handles persistence of data into a database. +//! - **Torrent**: It handles the torrent data. +//! - **Whitelist**: When the tracker runs in [`listed`](https://docs.rs/torrust-tracker-configuration/latest/torrust_tracker_configuration/type.Core.html) mode +//! all operations are restricted to whitelisted torrents. //! -//! ```rust,no_run -//! use bittorrent_primitives::info_hash::InfoHash; -//! use std::str::FromStr; -//! -//! 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`] 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`] 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) -//! } -//! -//! ``` -//! -//! > **NOTICE**: that `complete` or `completed` peers are the peers that have completed downloading, but only the active ones are considered "seeders". -//! -//! `SwarmMetadata` struct follows name conventions for `scrape` responses. See [BEP 48](https://www.bittorrent.org/beps/bep_0048.html), while `SwarmMetadata` -//! is used for the rest of cases. -//! -//! Refer to [`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 -//! use std::net::SocketAddr; - -//! use aquatic_udp_protocol::PeerId; -//! use torrust_tracker_primitives::DurationSinceUnixEpoch; -//! use aquatic_udp_protocol::NumberOfBytes; -//! use aquatic_udp_protocol::AnnounceEvent; -//! -//! pub struct Peer { -//! pub peer_id: PeerId, // 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`](torrust_tracker_primitives::peer) for more information about peers. +//! Refer to [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration) +//! crate docs to get more information about the tracker settings. //! //! # Configuration //! -//! You can control the behavior of this module with the module settings: +//! You can control the behavior of this crate with the `Core` settings: //! //! ```toml //! [logging] @@ -342,47 +83,41 @@ //! //! Refer to the [`configuration` module documentation](https://docs.rs/torrust-tracker-configuration) to get more information about all options. //! -//! # Services +//! # Announce handler +//! +//! The `AnnounceHandler` is responsible for handling announce requests. //! -//! Services are domain services on top of the core tracker domain. Right now there are two types of service: +//! Please refer to the [`announce_handler`] documentation. //! -//! - For statistics: [`crate::packages::statistics::services`] -//! - For torrents: [`crate::core::torrent::services`] +//! # Scrape handler //! -//! 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. +//! The `ScrapeHandler` is responsible for handling scrape requests. //! -//! Services can include extra features like pagination, for example. +//! Please refer to the [`scrape_handler`] documentation. //! //! # 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. +//! The `Authentication` module is responsible for handling authentication keys which are used by HTTP trackers. +//! +//! Please refer to the [`authentication`] documentation. //! -//! 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`. +//! # Databases //! -//! 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). +//! The `Databases` module is responsible for handling persistence of data into a database. //! -//! To learn more about tracker authentication, refer to the following modules : +//! Please refer to the [`databases`] documentation. //! -//! - [`authentication`] module. +//! # Torrent //! -//! # Persistence +//! The `Torrent` module is responsible for handling the torrent data. //! -//! Right now the `Tracker` is responsible for storing and load data into and -//! from the database, when persistence is enabled. +//! Please refer to the [`torrent`] documentation. //! -//! There are three types of persistent object: +//! # Whitelist //! -//! - Authentication keys (only expiring keys) -//! - Torrent whitelist -//! - Torrent metrics +//! The `Whitelist` module is responsible for handling the whitelist. //! -//! Refer to [`databases`] module for more information about persistence. +//! Please refer to the [`whitelist`] documentation. pub mod announce_handler; pub mod authentication; pub mod databases; diff --git a/packages/tracker-core/src/scrape_handler.rs b/packages/tracker-core/src/scrape_handler.rs index 60d15de7..1e75580a 100644 --- a/packages/tracker-core/src/scrape_handler.rs +++ b/packages/tracker-core/src/scrape_handler.rs @@ -1,3 +1,64 @@ +//! Scrape handler. +//! +//! 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. +//! +//! ## Scrape Response Format +//! +//! The returned struct is: +//! +//! ```rust,no_run +//! use bittorrent_primitives::info_hash::InfoHash; +//! use std::collections::HashMap; +//! +//! 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) +//! } +//! ``` +//! +//! ## Example JSON Response +//! +//! 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 +//! use bittorrent_primitives::info_hash::InfoHash; +//! use std::str::FromStr; +//! +//! let info_hash: InfoHash = [255u8; 20].into(); +//! +//! assert_eq!( +//! info_hash, +//! InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap() +//! ); +//! ``` +//! +//! ## References: +//! +//! 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) use std::sync::Arc; use bittorrent_primitives::info_hash::InfoHash; @@ -7,8 +68,9 @@ use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use super::torrent::repository::in_memory::InMemoryTorrentRepository; use super::whitelist; +/// Handles scrape requests, providing torrent swarm metadata. pub struct ScrapeHandler { - /// The service to check is a torrent is whitelisted. + /// Service for authorizing access to whitelisted torrents. whitelist_authorization: Arc, /// The in-memory torrents repository. @@ -16,6 +78,7 @@ pub struct ScrapeHandler { } impl ScrapeHandler { + /// Creates a new `ScrapeHandler` instance. #[must_use] pub fn new( whitelist_authorization: &Arc, @@ -27,9 +90,14 @@ impl ScrapeHandler { } } - /// It handles a scrape request. + /// Handles a scrape request for multiple torrents. /// - /// BEP 48: [Tracker Protocol Extension: Scrape](https://www.bittorrent.org/beps/bep_0048.html). + /// - Returns metadata for each requested torrent. + /// - If a torrent isn't whitelisted or doesn't exist, returns zeroed stats. + /// + /// # BEP Reference: + /// + /// [BEP 48: Scrape Protocol](https://www.bittorrent.org/beps/bep_0048.html) pub async fn scrape(&self, info_hashes: &Vec) -> ScrapeData { let mut scrape_data = ScrapeData::empty(); diff --git a/packages/tracker-core/src/torrent/manager.rs b/packages/tracker-core/src/torrent/manager.rs index 9dac3525..51df97fb 100644 --- a/packages/tracker-core/src/torrent/manager.rs +++ b/packages/tracker-core/src/torrent/manager.rs @@ -1,3 +1,4 @@ +//! Torrents manager. use std::sync::Arc; use std::time::Duration; @@ -8,6 +9,18 @@ use super::repository::in_memory::InMemoryTorrentRepository; use super::repository::persisted::DatabasePersistentTorrentRepository; use crate::{databases, CurrentClock}; +/// The `TorrentsManager` is responsible for managing torrent entries by +/// integrating persistent storage and in-memory state. It provides methods to +/// load torrent data from the database into memory, and to periodically clean +/// up stale torrent entries by removing inactive peers or entire torrent +/// entries that no longer have active peers. +/// +/// This manager relies on two repositories: +/// +/// - An **in-memory repository** to provide fast access to the current torrent +/// state. +/// - A **persistent repository** that stores aggregate torrent metrics (e.g., +/// seeders count) across tracker restarts. pub struct TorrentsManager { /// The tracker configuration. config: Core, @@ -21,6 +34,19 @@ pub struct TorrentsManager { } impl TorrentsManager { + /// Creates a new instance of `TorrentsManager`. + /// + /// # Arguments + /// + /// * `config` - A reference to the tracker configuration. + /// * `in_memory_torrent_repository` - A shared reference to the in-memory + /// repository of torrents. + /// * `db_torrent_repository` - A shared reference to the persistent + /// repository for torrent metrics. + /// + /// # Returns + /// + /// A new `TorrentsManager` instance with cloned references of the provided dependencies. #[must_use] pub fn new( config: &Core, @@ -34,13 +60,16 @@ impl TorrentsManager { } } - /// 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. + /// Loads torrents from the persistent database into the in-memory repository. + /// + /// This function retrieves the list of persistent torrent entries (which + /// include only the aggregate metrics, not the detailed peer lists) from + /// the database, and then imports that data into the in-memory repository. /// /// # Errors /// - /// Will return a `database::Error` if unable to load the list of `persistent_torrents` from the database. + /// Returns a `databases::error::Error` if unable to load the persistent + /// torrent data. #[allow(dead_code)] pub(crate) fn load_torrents_from_database(&self) -> Result<(), databases::error::Error> { let persistent_torrents = self.db_torrent_repository.load_all()?; @@ -50,7 +79,18 @@ impl TorrentsManager { Ok(()) } - /// Remove inactive peers and (optionally) peerless torrents. + /// Cleans up torrent entries by removing inactive peers and, optionally, + /// torrents with no active peers. + /// + /// This function performs two cleanup tasks: + /// + /// 1. It removes peers from torrent entries that have not been updated + /// within a cutoff time. The cutoff time is calculated as the current + /// time minus the maximum allowed peer timeout, as specified in the + /// tracker policy. + /// 2. If the tracker is configured to remove peerless torrents + /// (`remove_peerless_torrents` is set), it removes entire torrent + /// entries that have no active peers. pub fn cleanup_torrents(&self) { let current_cutoff = CurrentClock::now_sub(&Duration::from_secs(u64::from(self.config.tracker_policy.max_peer_timeout))) .unwrap_or_default(); diff --git a/packages/tracker-core/src/torrent/mod.rs b/packages/tracker-core/src/torrent/mod.rs index 7ca9000f..8ee8fa6d 100644 --- a/packages/tracker-core/src/torrent/mod.rs +++ b/packages/tracker-core/src/torrent/mod.rs @@ -1,30 +1,168 @@ -//! Structs to store the swarm data. +//! Swarm Data Structures. //! -//! There are to main data structures: +//! This module defines the primary data structures used to store and manage +//! swarm data within the tracker. In `BitTorrent` terminology, a "swarm" is +//! the collection of peers that are sharing or downloading a given torrent. //! -//! - A torrent [`Entry`](torrust_tracker_torrent_repository::entry::Entry): it contains all the information stored by the tracker for one torrent. -//! - The [`SwarmMetadata`](torrust_tracker_primitives::swarm_metadata::SwarmMetadata): it contains aggregate information that can me derived from the torrent entries. +//! There are two main types of data stored: //! -//! A "swarm" is a network of peers that are trying to download the same torrent. +//! - **Torrent Entry** (`Entry`): Contains all the information the tracker +//! stores for a single torrent, including the list of peers currently in the +//! swarm. This data is crucial for peers to locate each other and initiate +//! downloads. //! -//! 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. +//! - **Swarm Metadata** (`SwarmMetadata`): Contains aggregate data derived from +//! all torrent entries. This metadata is split into: +//! - **Active Peers Data:** Metrics related to the peers that are currently +//! active in the swarm. +//! - **Historical Data:** Metrics collected since the tracker started, such +//! as the total number of completed downloads. //! -//! The "swarm metadata" contains aggregate data derived from the torrent entries. There two types of data: +//! ## Metrics Collected //! -//! - For **active peers**: metrics related to the current active peers in the swarm. -//! - **Historical data**: since the tracker started running. +//! The tracker collects and aggregates the following metrics: //! -//! The tracker collects metrics for: +//! - The total number of peers that have completed downloading the torrent +//! since the tracker began collecting metrics. +//! - The number of completed downloads from peers that remain active (i.e., seeders). +//! - The number of active peers that have not completed downloading the torrent (i.e., leechers). //! -//! - 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". +//! This information is used both to inform peers about available connections +//! and to provide overall swarm statistics. //! +//! This module re-exports core types from the torrent repository crate to +//! simplify integration. +//! +//! ## Internal Data Structures +//! +//! The [`torrent`](crate::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 [`crate::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) +//! } +//! ``` +//! +//! > **NOTICE**: that `complete` or `completed` peers are the peers that have +//! > completed downloading, but only the active ones are considered "seeders". +//! +//! `SwarmMetadata` struct follows name conventions for `scrape` responses. See +//! [BEP 48](https://www.bittorrent.org/beps/bep_0048.html), while `SwarmMetadata` +//! is used for the rest of cases. +//! +//! ## Peers +//! +//! A `Peer` is the struct used by the tracker to keep peers data: +//! +//! ```rust,no_run +//! use std::net::SocketAddr; +//! use aquatic_udp_protocol::PeerId; +//! use torrust_tracker_primitives::DurationSinceUnixEpoch; +//! use aquatic_udp_protocol::NumberOfBytes; +//! use aquatic_udp_protocol::AnnounceEvent; +//! +//! pub struct Peer { +//! pub peer_id: PeerId, // 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`](torrust_tracker_primitives::peer) for more information about peers. pub mod manager; pub mod repository; pub mod services; @@ -33,7 +171,11 @@ pub mod services; use torrust_tracker_torrent_repository::EntryMutexStd; use torrust_tracker_torrent_repository::TorrentsSkipMapMutexStd; -// Currently used types from the torrent repository crate. +/// Alias for the primary torrent collection type, implemented as a skip map +/// wrapped in a mutex. This type is used internally by the tracker to manage +/// and access torrent entries. pub(crate) type Torrents = TorrentsSkipMapMutexStd; + +/// Alias for a single torrent entry. #[cfg(test)] pub(crate) type TorrentEntry = EntryMutexStd; diff --git a/packages/tracker-core/src/torrent/repository/in_memory.rs b/packages/tracker-core/src/torrent/repository/in_memory.rs index 26302260..584feabc 100644 --- a/packages/tracker-core/src/torrent/repository/in_memory.rs +++ b/packages/tracker-core/src/torrent/repository/in_memory.rs @@ -1,3 +1,4 @@ +//! In-memory torrents repository. use std::cmp::max; use std::sync::Arc; @@ -13,51 +14,126 @@ use torrust_tracker_torrent_repository::EntryMutexStd; use crate::torrent::Torrents; -/// The in-memory torrents repository. +/// In-memory repository for torrent entries. /// -/// There are many implementations of the repository trait. We tried with -/// different types of data structures, but the best performance was with -/// the one we use for production. We kept the other implementations for -/// reference. +/// This repository manages the torrent entries and their associated peer lists +/// in memory. It is built on top of a high-performance data structure (the +/// production implementation) and provides methods to update, query, and remove +/// torrent entries as well as to import persisted data. +/// +/// Multiple implementations were considered, and the chosen implementation is +/// used in production. Other implementations are kept for reference. #[derive(Debug, Default)] pub struct InMemoryTorrentRepository { - /// The in-memory torrents repository implementation. + /// The underlying in-memory data structure that stores torrent entries. torrents: Arc, } impl InMemoryTorrentRepository { - /// It inserts (or updates if it's already in the list) the peer in the - /// torrent entry. + /// Inserts or updates a peer in the torrent entry corresponding to the + /// given infohash. + /// + /// If the torrent entry already exists, the peer is added to its peer list; + /// otherwise, a new torrent entry is created. + /// + /// # Arguments + /// + /// * `info_hash` - The unique identifier of the torrent. + /// * `peer` - The peer to insert or update in the torrent entry. pub fn upsert_peer(&self, info_hash: &InfoHash, peer: &peer::Peer) { self.torrents.upsert_peer(info_hash, peer); } + /// Removes a torrent entry from the repository. + /// + /// This method is only available in tests. It removes the torrent entry + /// associated with the given info hash and returns the removed entry if it + /// existed. + /// + /// # Arguments + /// + /// * `key` - The info hash of the torrent to remove. + /// + /// # Returns + /// + /// An `Option` containing the removed torrent entry if it existed. #[cfg(test)] #[must_use] pub(crate) fn remove(&self, key: &InfoHash) -> Option { self.torrents.remove(key) } + /// Removes inactive peers from all torrent entries. + /// + /// A peer is considered inactive if its last update timestamp is older than + /// the provided cutoff time. + /// + /// # Arguments + /// + /// * `current_cutoff` - The cutoff timestamp; peers not updated since this + /// time will be removed. pub(crate) fn remove_inactive_peers(&self, current_cutoff: DurationSinceUnixEpoch) { self.torrents.remove_inactive_peers(current_cutoff); } + /// Removes torrent entries that have no active peers. + /// + /// Depending on the tracker policy, torrents without any peers may be + /// removed to conserve memory. + /// + /// # Arguments + /// + /// * `policy` - The tracker policy containing the configuration for + /// removing peerless torrents. pub(crate) fn remove_peerless_torrents(&self, policy: &TrackerPolicy) { self.torrents.remove_peerless_torrents(policy); } + /// Retrieves a torrent entry by its infohash. + /// + /// # Arguments + /// + /// * `key` - The info hash of the torrent. + /// + /// # Returns + /// + /// An `Option` containing the torrent entry if found. #[must_use] pub(crate) fn get(&self, key: &InfoHash) -> Option { self.torrents.get(key) } + /// Retrieves a paginated list of torrent entries. + /// + /// This method returns a vector of tuples, each containing an infohash and + /// its associated torrent entry. The pagination parameters (offset and limit) + /// can be used to control the size of the result set. + /// + /// # Arguments + /// + /// * `pagination` - An optional reference to a `Pagination` object. + /// + /// # Returns + /// + /// A vector of `(InfoHash, EntryMutexStd)` tuples. #[must_use] pub(crate) fn get_paginated(&self, pagination: Option<&Pagination>) -> Vec<(InfoHash, EntryMutexStd)> { self.torrents.get_paginated(pagination) } - /// It returns the data for a `scrape` response or empty if the torrent is - /// not found. + /// Retrieves swarm metadata for a given torrent. + /// + /// This method returns the swarm metadata (aggregate information such as + /// peer counts) for the torrent specified by the infohash. If the torrent + /// entry is not found, a zeroed metadata struct is returned. + /// + /// # Arguments + /// + /// * `info_hash` - The info hash of the torrent. + /// + /// # Returns + /// + /// A `SwarmMetadata` struct containing the aggregated torrent data. #[must_use] pub(crate) fn get_swarm_metadata(&self, info_hash: &InfoHash) -> SwarmMetadata { match self.torrents.get(info_hash) { @@ -66,9 +142,23 @@ impl InMemoryTorrentRepository { } } - /// Get torrent peers for a given torrent and client. + /// Retrieves torrent peers for a given torrent and client, excluding the + /// requesting client. + /// + /// This method filters out the client making the request (based on its + /// network address) and returns up to a maximum number of peers, defined by + /// the greater of the provided limit or the global `TORRENT_PEERS_LIMIT`. + /// + /// # Arguments + /// + /// * `info_hash` - The info hash of the torrent. + /// * `peer` - The client peer that should be excluded from the returned list. + /// * `limit` - The maximum number of peers to return. + /// + /// # Returns /// - /// It filters out the client making the request. + /// A vector of peers (wrapped in `Arc`) representing the active peers for + /// the torrent, excluding the requesting client. #[must_use] pub(crate) fn get_peers_for(&self, info_hash: &InfoHash, peer: &peer::Peer, limit: usize) -> Vec> { match self.torrents.get(info_hash) { @@ -77,7 +167,19 @@ impl InMemoryTorrentRepository { } } - /// Get torrent peers for a given torrent. + /// Retrieves the list of peers for a given torrent. + /// + /// This method returns up to `TORRENT_PEERS_LIMIT` peers for the torrent + /// specified by the info-hash. + /// + /// # Arguments + /// + /// * `info_hash` - The info hash of the torrent. + /// + /// # Returns + /// + /// A vector of peers (wrapped in `Arc`) representing the active peers for + /// the torrent. #[must_use] pub fn get_torrent_peers(&self, info_hash: &InfoHash) -> Vec> { match self.torrents.get(info_hash) { @@ -86,12 +188,28 @@ impl InMemoryTorrentRepository { } } - /// It calculates and returns the general [`TorrentsMetrics`]. + /// Calculates and returns overall torrent metrics. + /// + /// The returned [`TorrentsMetrics`] contains aggregate data such as the + /// total number of torrents, total complete (seeders), incomplete (leechers), + /// and downloaded counts. + /// + /// # Returns + /// + /// A [`TorrentsMetrics`] struct with the aggregated metrics. #[must_use] pub fn get_torrents_metrics(&self) -> TorrentsMetrics { self.torrents.get_metrics() } + /// Imports persistent torrent data into the in-memory repository. + /// + /// This method takes a set of persisted torrent entries (e.g., from a database) + /// and imports them into the in-memory repository for immediate access. + /// + /// # Arguments + /// + /// * `persistent_torrents` - A reference to the persisted torrent data. pub fn import_persistent(&self, persistent_torrents: &PersistentTorrents) { self.torrents.import_persistent(persistent_torrents); } diff --git a/packages/tracker-core/src/torrent/repository/mod.rs b/packages/tracker-core/src/torrent/repository/mod.rs index 51723b68..ae789e5e 100644 --- a/packages/tracker-core/src/torrent/repository/mod.rs +++ b/packages/tracker-core/src/torrent/repository/mod.rs @@ -1,2 +1,3 @@ +//! Torrent repository implementations. pub mod in_memory; pub mod persisted; diff --git a/packages/tracker-core/src/torrent/repository/persisted.rs b/packages/tracker-core/src/torrent/repository/persisted.rs index 0430f03b..694a2fe7 100644 --- a/packages/tracker-core/src/torrent/repository/persisted.rs +++ b/packages/tracker-core/src/torrent/repository/persisted.rs @@ -1,3 +1,4 @@ +//! The repository that stored persistent torrents' data into the database. use std::sync::Arc; use bittorrent_primitives::info_hash::InfoHash; @@ -6,17 +7,39 @@ use torrust_tracker_primitives::PersistentTorrents; use crate::databases::error::Error; use crate::databases::Database; -/// Torrent repository implementation that persists the torrents in a database. +/// Torrent repository implementation that persists torrent metrics in a database. /// -/// Not all the torrent in-memory data is persisted. For now only some of the -/// torrent metrics are persisted. +/// This repository persists only a subset of the torrent data: the torrent +/// metrics, specifically the number of downloads (or completed counts) for each +/// torrent. It relies on a database driver (either `SQLite3` or `MySQL`) that +/// implements the [`Database`] trait to perform the actual persistence +/// operations. +/// +/// # Note +/// +/// Not all in-memory torrent data is persisted; only the aggregate metrics are +/// stored. pub struct DatabasePersistentTorrentRepository { - /// A database driver implementation: [`Sqlite3`](crate::core::databases::sqlite) - /// or [`MySQL`](crate::core::databases::mysql) + /// A shared reference to the database driver implementation. + /// + /// The driver must implement the [`Database`] trait. This allows for + /// different underlying implementations (e.g., `SQLite3` or `MySQL`) to be + /// used interchangeably. database: Arc>, } impl DatabasePersistentTorrentRepository { + /// Creates a new instance of `DatabasePersistentTorrentRepository`. + /// + /// # Arguments + /// + /// * `database` - A shared reference to a boxed database driver + /// implementing the [`Database`] trait. + /// + /// # Returns + /// + /// A new `DatabasePersistentTorrentRepository` instance with a cloned + /// reference to the provided database. #[must_use] pub fn new(database: &Arc>) -> DatabasePersistentTorrentRepository { Self { @@ -24,20 +47,31 @@ impl DatabasePersistentTorrentRepository { } } - /// It loads the persistent torrents from the database. + /// Loads all persistent torrent metrics from the database. + /// + /// This function retrieves the torrent metrics (e.g., download counts) from the persistent store + /// and returns them as a [`PersistentTorrents`] map. /// /// # Errors /// - /// Will return a database `Err` if unable to load. + /// Returns an [`Error`] if the underlying database query fails. pub(crate) fn load_all(&self) -> Result { self.database.load_persistent_torrents() } - /// It saves the persistent torrent into the database. + /// Saves the persistent torrent metric into the database. + /// + /// This function stores or updates the download count for the torrent + /// identified by the provided infohash. + /// + /// # Arguments + /// + /// * `info_hash` - The info hash of the torrent. + /// * `downloaded` - The number of times the torrent has been downloaded. /// /// # Errors /// - /// Will return a database `Err` if unable to save. + /// Returns an [`Error`] if the database operation fails. pub(crate) fn save(&self, info_hash: &InfoHash, downloaded: u32) -> Result<(), Error> { self.database.save_persistent_torrent(info_hash, downloaded) } diff --git a/packages/tracker-core/src/torrent/services.rs b/packages/tracker-core/src/torrent/services.rs index 4c470bb7..98d25ba4 100644 --- a/packages/tracker-core/src/torrent/services.rs +++ b/packages/tracker-core/src/torrent/services.rs @@ -1,9 +1,17 @@ //! Core tracker domain services. //! -//! There are two services: +//! This module defines the primary services for retrieving torrent-related data +//! from the tracker. There are two main services: //! -//! - [`get_torrent_info`]: it returns all the data about one torrent. -//! - [`get_torrents`]: it returns data about some torrent in bulk excluding the peer list. +//! - [`get_torrent_info`]: Returns all available data (including the list of +//! peers) about a single torrent. +//! - [`get_torrents_page`] and [`get_torrents`]: Return summarized data about +//! multiple torrents, excluding the peer list. +//! +//! The full torrent info is represented by the [`Info`] struct, which includes +//! swarm data (peer list) and aggregate metrics. The [`BasicInfo`] struct +//! provides similar data but without the list of peers, making it suitable for +//! bulk queries. use std::sync::Arc; use bittorrent_primitives::info_hash::InfoHash; @@ -13,37 +21,74 @@ use torrust_tracker_torrent_repository::entry::EntrySync; use crate::torrent::repository::in_memory::InMemoryTorrentRepository; -/// It contains all the information the tracker has about a torrent +/// Full torrent information, including swarm (peer) details. +/// +/// This struct contains all the information that the tracker holds about a +/// torrent, including the infohash, aggregate swarm metrics (seeders, leechers, +/// completed downloads), and the complete list of peers in the swarm. #[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 + + /// 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 + + /// 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 + + /// 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 + + /// 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 +/// Basic torrent information, excluding the list of peers. /// -/// It contains the same data as [Info] but without the list of peers in the swarm. +/// This struct contains the same aggregate metrics as [`Info`] (infohash, +/// seeders, completed, leechers) but omits the peer list. It is used when only +/// summary information is needed. #[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 + + /// 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 + + /// 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 + + /// The total number of leechers for this torrent. Peers that actively + /// downloading this torrent pub leechers: u64, } -/// It returns all the information the tracker has about one torrent in a [Info] struct. +/// Retrieves complete torrent information for a given torrent. +/// +/// This function queries the in-memory torrent repository for a torrent entry +/// matching the provided infohash. If found, it extracts the swarm metadata +/// (aggregate metrics) and the current list of peers, and returns an [`Info`] +/// struct. +/// +/// # Arguments +/// +/// * `in_memory_torrent_repository` - A shared reference to the in-memory +/// torrent repository. +/// * `info_hash` - A reference to the torrent's infohash. +/// +/// # Returns +/// +/// An [`Option`] which is: +/// - `Some(Info)` if the torrent exists in the repository. +/// - `None` if the torrent is not found. #[must_use] pub fn get_torrent_info(in_memory_torrent_repository: &Arc, info_hash: &InfoHash) -> Option { let torrent_entry_option = in_memory_torrent_repository.get(info_hash); @@ -65,7 +110,23 @@ pub fn get_torrent_info(in_memory_torrent_repository: &Arc, @@ -87,7 +148,23 @@ pub fn get_torrents_page( basic_infos } -/// It returns all the information the tracker has about multiple torrents in a [`BasicInfo`] struct, excluding the peer list. +/// Retrieves summarized torrent information for a specified list of torrents. +/// +/// This function iterates over a slice of infohashes, fetches the corresponding +/// swarm metadata from the in-memory repository (if available), and returns a +/// vector of [`BasicInfo`] structs. This function is useful for bulk queries +/// where detailed peer information is not required. +/// +/// # Arguments +/// +/// * `in_memory_torrent_repository` - A shared reference to the in-memory +/// torrent repository. +/// * `info_hashes` - A slice of infohashes for which to retrieve the torrent +/// information. +/// +/// # Returns +/// +/// A vector of [`BasicInfo`] structs for the requested torrents. #[must_use] pub fn get_torrents(in_memory_torrent_repository: &Arc, info_hashes: &[InfoHash]) -> Vec { let mut basic_infos: Vec = vec![]; diff --git a/packages/tracker-core/src/whitelist/authorization.rs b/packages/tracker-core/src/whitelist/authorization.rs index 3b7b8b4f..a8323457 100644 --- a/packages/tracker-core/src/whitelist/authorization.rs +++ b/packages/tracker-core/src/whitelist/authorization.rs @@ -1,3 +1,4 @@ +//! Whitelist authorization. use std::panic::Location; use std::sync::Arc; @@ -8,6 +9,10 @@ use tracing::instrument; use super::repository::in_memory::InMemoryWhitelist; use crate::error::WhitelistError; +/// Manages the authorization of torrents based on the whitelist. +/// +/// Used to determine whether a given torrent (`infohash`) is allowed +/// to be announced or scraped from the tracker. pub struct WhitelistAuthorization { /// Core tracker configuration. config: Core, @@ -17,7 +22,14 @@ pub struct WhitelistAuthorization { } impl WhitelistAuthorization { - /// Creates a new authorization instance. + /// Creates a new `WhitelistAuthorization` instance. + /// + /// # Arguments + /// - `config`: Tracker configuration. + /// - `in_memory_whitelist`: The in-memory whitelist instance. + /// + /// # Returns + /// A new `WhitelistAuthorization` instance. pub fn new(config: &Core, in_memory_whitelist: &Arc) -> Self { Self { config: config.clone(), @@ -25,12 +37,15 @@ impl WhitelistAuthorization { } } - /// It returns true if the torrent is authorized. + /// Checks whether a torrent is authorized. /// - /// # Errors + /// - If the tracker is **public**, all torrents are authorized. + /// - If the tracker is **private** (listed mode), only whitelisted torrents + /// are authorized. /// - /// Will return an error if the tracker is running in `listed` mode - /// and the infohash is not whitelisted. + /// # Errors + /// Returns `WhitelistError::TorrentNotWhitelisted` if the tracker is in `listed` mode + /// and the `info_hash` is not in the whitelist. #[instrument(skip(self, info_hash), err)] pub async fn authorize(&self, info_hash: &InfoHash) -> Result<(), WhitelistError> { if !self.is_listed() { @@ -47,12 +62,12 @@ impl WhitelistAuthorization { }) } - /// Returns `true` is the tracker is in listed mode. + /// Checks if the tracker is running in "listed" mode. fn is_listed(&self) -> bool { self.config.listed } - /// It checks if a torrent is whitelisted. + /// Checks if a torrent is present in the whitelist. async fn is_info_hash_whitelisted(&self, info_hash: &InfoHash) -> bool { self.in_memory_whitelist.contains(info_hash).await } diff --git a/packages/tracker-core/src/whitelist/manager.rs b/packages/tracker-core/src/whitelist/manager.rs index 5ebd6db3..452fcb6c 100644 --- a/packages/tracker-core/src/whitelist/manager.rs +++ b/packages/tracker-core/src/whitelist/manager.rs @@ -1,3 +1,7 @@ +//! Whitelist manager. +//! +//! This module provides the `WhitelistManager` struct, which is responsible for +//! managing the whitelist of torrents. use std::sync::Arc; use bittorrent_primitives::info_hash::InfoHash; @@ -5,8 +9,11 @@ use bittorrent_primitives::info_hash::InfoHash; use super::repository::in_memory::InMemoryWhitelist; use super::repository::persisted::DatabaseWhitelist; use crate::databases; - -/// It handles the list of allowed torrents. Only for listed trackers. +/// Manages the whitelist of allowed torrents. +/// +/// This structure handles both the in-memory and persistent representations of +/// the whitelist. It is primarily relevant for private trackers that restrict +/// access to specific torrents. pub struct WhitelistManager { /// The in-memory list of allowed torrents. in_memory_whitelist: Arc, @@ -16,6 +23,17 @@ pub struct WhitelistManager { } impl WhitelistManager { + /// Creates a new `WhitelistManager` instance. + /// + /// # Arguments + /// + /// - `database_whitelist`: Persistent database-backed whitelist repository. + /// - `in_memory_whitelist`: In-memory whitelist repository for fast runtime + /// access. + /// + /// # Returns + /// + /// A new `WhitelistManager` instance. #[must_use] pub fn new(database_whitelist: Arc, in_memory_whitelist: Arc) -> Self { Self { @@ -24,35 +42,39 @@ impl WhitelistManager { } } - /// It adds a torrent to the whitelist. - /// Adding torrents is not relevant to public trackers. + /// Adds a torrent to the whitelist. /// - /// # Errors + /// This operation is relevant for private trackers to control which + /// torrents are allowed. /// - /// Will return a `database::Error` if unable to add the `info_hash` into the whitelist database. + /// # Errors + /// Returns a `database::Error` if the operation fails in the database. pub async fn add_torrent_to_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { self.database_whitelist.add(info_hash)?; self.in_memory_whitelist.add(info_hash).await; Ok(()) } - /// It removes a torrent from the whitelist. - /// Removing torrents is not relevant to public trackers. + /// Removes a torrent from the whitelist. /// - /// # Errors + /// This operation is relevant for private trackers to revoke access to + /// specific torrents. /// - /// Will return a `database::Error` if unable to remove the `info_hash` from the whitelist database. + /// # Errors + /// Returns a `database::Error` if the operation fails in the database. pub async fn remove_torrent_from_whitelist(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { self.database_whitelist.remove(info_hash)?; self.in_memory_whitelist.remove(info_hash).await; Ok(()) } - /// It loads the whitelist from the database. + /// Loads the whitelist from the database into memory. /// - /// # Errors + /// This is useful when restarting the tracker to ensure the in-memory + /// whitelist is synchronized with the database. /// - /// Will return a `database::Error` if unable to load the list whitelisted `info_hash`s from the database. + /// # Errors + /// Returns a `database::Error` if the operation fails to load from the database. pub async fn load_whitelist_from_database(&self) -> Result<(), databases::error::Error> { let whitelisted_torrents_from_database = self.database_whitelist.load_from_database()?; diff --git a/packages/tracker-core/src/whitelist/mod.rs b/packages/tracker-core/src/whitelist/mod.rs index a39768e9..d9ad1831 100644 --- a/packages/tracker-core/src/whitelist/mod.rs +++ b/packages/tracker-core/src/whitelist/mod.rs @@ -1,3 +1,21 @@ +//! This module contains the logic to manage the torrent whitelist. +//! +//! In tracker configurations where the tracker operates in "listed" mode, only +//! torrents that have been explicitly added to the whitelist are allowed to +//! perform announce and scrape actions. This module provides all the +//! functionality required to manage such a whitelist. +//! +//! The module is organized into the following submodules: +//! +//! - **`authorization`**: Contains the logic to authorize torrents based on their +//! whitelist status. +//! - **`manager`**: Provides high-level management functions for the whitelist, +//! such as adding or removing torrents. +//! - **`repository`**: Implements persistence for whitelist data. +//! - **`setup`**: Provides initialization routines for setting up the whitelist +//! system. +//! - **`test_helpers`**: Contains helper functions and fixtures for testing +//! whitelist functionality. pub mod authorization; pub mod manager; pub mod repository; diff --git a/packages/tracker-core/src/whitelist/repository/in_memory.rs b/packages/tracker-core/src/whitelist/repository/in_memory.rs index 4faeda78..0cee3a94 100644 --- a/packages/tracker-core/src/whitelist/repository/in_memory.rs +++ b/packages/tracker-core/src/whitelist/repository/in_memory.rs @@ -1,29 +1,42 @@ +//! The in-memory list of allowed torrents. use bittorrent_primitives::info_hash::InfoHash; -/// The in-memory list of allowed torrents. +/// In-memory whitelist to manage allowed torrents. +/// +/// Stores `InfoHash` values for quick lookup and modification. #[derive(Debug, Default)] pub struct InMemoryWhitelist { - /// The list of allowed torrents. + /// A thread-safe set of whitelisted `InfoHash` values. whitelist: tokio::sync::RwLock>, } impl InMemoryWhitelist { - /// It adds a torrent from the whitelist in memory. + /// Adds a torrent to the in-memory whitelist. + /// + /// # Returns + /// + /// - `true` if the torrent was newly added. + /// - `false` if the torrent was already in the whitelist. pub async fn add(&self, info_hash: &InfoHash) -> bool { self.whitelist.write().await.insert(*info_hash) } - /// It removes a torrent from the whitelist in memory. + /// Removes a torrent from the in-memory whitelist. + /// + /// # Returns + /// + /// - `true` if the torrent was present and removed. + /// - `false` if the torrent was not found. pub(crate) async fn remove(&self, info_hash: &InfoHash) -> bool { self.whitelist.write().await.remove(info_hash) } - /// It checks if it contains an info-hash. + /// Checks if a torrent is in the whitelist. pub async fn contains(&self, info_hash: &InfoHash) -> bool { self.whitelist.read().await.contains(info_hash) } - /// It clears the whitelist. + /// Clears all torrents from the whitelist. pub(crate) async fn clear(&self) { let mut whitelist = self.whitelist.write().await; whitelist.clear(); diff --git a/packages/tracker-core/src/whitelist/repository/mod.rs b/packages/tracker-core/src/whitelist/repository/mod.rs index 51723b68..d900a8c2 100644 --- a/packages/tracker-core/src/whitelist/repository/mod.rs +++ b/packages/tracker-core/src/whitelist/repository/mod.rs @@ -1,2 +1,3 @@ +//! Repository implementations for the whitelist. pub mod in_memory; pub mod persisted; diff --git a/packages/tracker-core/src/whitelist/repository/persisted.rs b/packages/tracker-core/src/whitelist/repository/persisted.rs index 4773cfbe..eec6704d 100644 --- a/packages/tracker-core/src/whitelist/repository/persisted.rs +++ b/packages/tracker-core/src/whitelist/repository/persisted.rs @@ -1,3 +1,4 @@ +//! The repository that persists the whitelist. use std::sync::Arc; use bittorrent_primitives::info_hash::InfoHash; @@ -5,6 +6,9 @@ use bittorrent_primitives::info_hash::InfoHash; use crate::databases::{self, Database}; /// The persisted list of allowed torrents. +/// +/// This repository handles adding, removing, and loading torrents +/// from a persistent database like `SQLite` or `MySQL`รง. pub struct DatabaseWhitelist { /// A database driver implementation: [`Sqlite3`](crate::core::databases::sqlite) /// or [`MySQL`](crate::core::databases::mysql) @@ -12,16 +16,17 @@ pub struct DatabaseWhitelist { } impl DatabaseWhitelist { + /// Creates a new `DatabaseWhitelist`. #[must_use] pub fn new(database: Arc>) -> Self { Self { database } } - /// It adds a torrent to the whitelist if it has not been whitelisted previously + /// Adds a torrent to the whitelist if not already present. /// /// # Errors - /// - /// Will return a `database::Error` if unable to add the `info_hash` to the whitelist database. + /// Returns a `database::Error` if unable to add the `info_hash` to the + /// whitelist. pub(crate) fn add(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { let is_whitelisted = self.database.is_info_hash_whitelisted(*info_hash)?; @@ -34,11 +39,10 @@ impl DatabaseWhitelist { Ok(()) } - /// It removes a torrent from the whitelist in the database. + /// Removes a torrent from the whitelist if it exists. /// /// # Errors - /// - /// Will return a `database::Error` if unable to remove the `info_hash` from the whitelist database. + /// Returns a `database::Error` if unable to remove the `info_hash`. pub(crate) fn remove(&self, info_hash: &InfoHash) -> Result<(), databases::error::Error> { let is_whitelisted = self.database.is_info_hash_whitelisted(*info_hash)?; @@ -51,11 +55,11 @@ impl DatabaseWhitelist { Ok(()) } - /// It loads the whitelist from the database. + /// Loads the entire whitelist from the database. /// /// # Errors - /// - /// Will return a `database::Error` if unable to load the list whitelisted `info_hash`s from the database. + /// Returns a `database::Error` if unable to load whitelisted `info_hash` + /// values. pub(crate) fn load_from_database(&self) -> Result, databases::error::Error> { self.database.load_whitelist() } diff --git a/packages/tracker-core/src/whitelist/setup.rs b/packages/tracker-core/src/whitelist/setup.rs index 5b2a5de4..cb18c147 100644 --- a/packages/tracker-core/src/whitelist/setup.rs +++ b/packages/tracker-core/src/whitelist/setup.rs @@ -1,3 +1,7 @@ +//! Initializes the whitelist manager. +//! +//! This module provides functions to set up the `WhitelistManager`, which is responsible +//! for managing whitelisted torrents in both the in-memory and persistent database repositories. use std::sync::Arc; use super::manager::WhitelistManager; @@ -5,6 +9,28 @@ use super::repository::in_memory::InMemoryWhitelist; use super::repository::persisted::DatabaseWhitelist; use crate::databases::Database; +/// Initializes the `WhitelistManager` by combining in-memory and database +/// repositories. +/// +/// The `WhitelistManager` handles the operations related to whitelisted +/// torrents, such as adding, removing, and verifying torrents in the whitelist. +/// It operates with: +/// +/// 1. **In-Memory Whitelist:** Provides fast, runtime-based access to +/// whitelisted torrents. +/// 2. **Database Whitelist:** Ensures persistent storage of the whitelist data. +/// +/// # Arguments +/// +/// * `database` - An `Arc>` representing the database connection, +/// sed for persistent whitelist storage. +/// * `in_memory_whitelist` - An `Arc` representing the in-memory +/// whitelist repository for fast access. +/// +/// # Returns +/// +/// An `Arc` instance that manages both the in-memory and database +/// whitelist repositories. #[must_use] pub fn initialize_whitelist_manager( database: Arc>, diff --git a/packages/tracker-core/src/whitelist/test_helpers.rs b/packages/tracker-core/src/whitelist/test_helpers.rs index cc30c447..cf1699be 100644 --- a/packages/tracker-core/src/whitelist/test_helpers.rs +++ b/packages/tracker-core/src/whitelist/test_helpers.rs @@ -1,5 +1,8 @@ -//! Some generic test helpers functions. - +//! Generic test helper functions for the whitelist module. +//! +//! This module provides utility functions to initialize the whitelist services required for testing. +//! In particular, it sets up the `WhitelistAuthorization` and `WhitelistManager` services using a +//! configured database and an in-memory whitelist repository. #[cfg(test)] pub(crate) mod tests {