diff --git a/Cargo.lock b/Cargo.lock index 07c08ab04..5e84c108a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -554,7 +554,7 @@ dependencies = [ "aquatic_udp_protocol", "bittorrent-primitives", "bittorrent-tracker-core", - "derive_more", + "derive_more 1.0.0", "multimap", "percent-encoding", "serde", @@ -587,7 +587,7 @@ version = "3.0.0-develop" dependencies = [ "aquatic_udp_protocol", "bittorrent-primitives", - "derive_more", + "derive_more 2.0.1", "hyper", "percent-encoding", "reqwest", @@ -611,7 +611,7 @@ dependencies = [ "aquatic_udp_protocol", "bittorrent-primitives", "chrono", - "derive_more", + "derive_more 2.0.1", "local-ip-address", "mockall", "r2d2", @@ -970,9 +970,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.29" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" +checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" dependencies = [ "clap_builder", "clap_derive", @@ -980,9 +980,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.29" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" +checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" dependencies = [ "anstream", "anstyle", @@ -1264,7 +1264,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", ] [[package]] @@ -1272,6 +1281,17 @@ name = "derive_more-impl" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", @@ -3175,7 +3195,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.0", + "rand_core 0.9.1", "zerocopy 0.8.18", ] @@ -3196,7 +3216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.0", + "rand_core 0.9.1", ] [[package]] @@ -3210,9 +3230,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3" dependencies = [ "getrandom 0.3.1", "zerocopy 0.8.18", @@ -4010,9 +4030,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.16.0" +version = "3.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" dependencies = [ "cfg-if", "fastrand", @@ -4319,7 +4339,7 @@ dependencies = [ "clap", "crossbeam-skiplist", "dashmap", - "derive_more", + "derive_more 2.0.1", "figment", "futures", "futures-util", @@ -4414,7 +4434,7 @@ name = "torrust-tracker-configuration" version = "3.0.0-develop" dependencies = [ "camino", - "derive_more", + "derive_more 2.0.1", "figment", "serde", "serde_json", @@ -4449,7 +4469,7 @@ dependencies = [ "aquatic_udp_protocol", "binascii", "bittorrent-primitives", - "derive_more", + "derive_more 2.0.1", "serde", "tdyne-peer-id", "tdyne-peer-id-registry", @@ -4640,9 +4660,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "uncased" @@ -4703,9 +4723,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6" dependencies = [ "getrandom 0.3.1", "rand 0.9.0", diff --git a/Cargo.toml b/Cargo.toml index 7337b49af..5007acfda 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ cipher = "0" clap = { version = "4", features = ["derive", "env"] } crossbeam-skiplist = "0" dashmap = "6" -derive_more = { version = "1", features = ["as_ref", "constructor", "from"] } +derive_more = { version = "2", features = ["as_ref", "constructor", "from"] } figment = "0" futures = "0" futures-util = "0" diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index 05789b882..da04f29cd 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -16,7 +16,7 @@ version.workspace = true [dependencies] camino = { version = "1", features = ["serde", "serde1"] } -derive_more = { version = "1", features = ["constructor", "display"] } +derive_more = { version = "2", features = ["constructor", "display"] } figment = { version = "0", features = ["env", "test", "toml"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["preserve_order"] } diff --git a/packages/http-protocol/src/v1/responses/error.rs b/packages/http-protocol/src/v1/responses/error.rs index 2bd8cd95c..30749f73a 100644 --- a/packages/http-protocol/src/v1/responses/error.rs +++ b/packages/http-protocol/src/v1/responses/error.rs @@ -79,6 +79,14 @@ impl From for Error { } } +impl From for Error { + fn from(err: bittorrent_tracker_core::authentication::Error) -> Self { + Error { + failure_reason: format!("Tracker authentication error: {err}"), + } + } +} + #[cfg(test)] mod tests { diff --git a/packages/primitives/Cargo.toml b/packages/primitives/Cargo.toml index b83886385..1396d8bc8 100644 --- a/packages/primitives/Cargo.toml +++ b/packages/primitives/Cargo.toml @@ -18,7 +18,7 @@ version.workspace = true aquatic_udp_protocol = "0" binascii = "0" bittorrent-primitives = "0.1.0" -derive_more = { version = "1", features = ["constructor"] } +derive_more = { version = "2", features = ["constructor"] } serde = { version = "1", features = ["derive"] } tdyne-peer-id = "1" tdyne-peer-id-registry = "0" diff --git a/packages/tracker-client/Cargo.toml b/packages/tracker-client/Cargo.toml index 67a4c767a..ef5cccaa2 100644 --- a/packages/tracker-client/Cargo.toml +++ b/packages/tracker-client/Cargo.toml @@ -17,7 +17,7 @@ version.workspace = true [dependencies] aquatic_udp_protocol = "0" bittorrent-primitives = "0.1.0" -derive_more = { version = "1", features = ["as_ref", "constructor", "from"] } +derive_more = { version = "2", features = ["as_ref", "constructor", "from"] } hyper = "1" percent-encoding = "2" reqwest = { version = "0", features = ["json"] } diff --git a/packages/tracker-core/Cargo.toml b/packages/tracker-core/Cargo.toml index 46807a534..4fa0c0132 100644 --- a/packages/tracker-core/Cargo.toml +++ b/packages/tracker-core/Cargo.toml @@ -17,7 +17,7 @@ version.workspace = true aquatic_udp_protocol = "0" bittorrent-primitives = "0.1.0" chrono = { version = "0", default-features = false, features = ["clock"] } -derive_more = { version = "1", features = ["as_ref", "constructor", "from"] } +derive_more = { version = "2", features = ["as_ref", "constructor", "from"] } mockall = "0" r2d2 = "0" r2d2_mysql = "25" diff --git a/packages/tracker-core/src/authentication/key/mod.rs b/packages/tracker-core/src/authentication/key/mod.rs index 648143928..efc734356 100644 --- a/packages/tracker-core/src/authentication/key/mod.rs +++ b/packages/tracker-core/src/authentication/key/mod.rs @@ -185,6 +185,10 @@ pub enum Error { /// Indicates that the key has expired. #[error("Key has expired, {location}")] KeyExpired { location: &'static Location<'static> }, + + /// Indicates that the required key for authentication was not provided. + #[error("Missing authentication key, {location}")] + MissingAuthKey { location: &'static Location<'static> }, } impl From for Error { diff --git a/src/packages/http_tracker_core/services/announce.rs b/src/packages/http_tracker_core/services/announce.rs index 2bc421f1d..6c9cbec17 100644 --- a/src/packages/http_tracker_core/services/announce.rs +++ b/src/packages/http_tracker_core/services/announce.rs @@ -8,6 +8,7 @@ //! It also sends an [`http_tracker_core::statistics::event::Event`] //! because events are specific for the HTTP tracker. use std::net::IpAddr; +use std::panic::Location; use std::sync::Arc; use bittorrent_http_protocol::v1::requests::announce::{peer_from_request, Announce}; @@ -15,6 +16,7 @@ use bittorrent_http_protocol::v1::responses; use bittorrent_http_protocol::v1::services::peer_ip_resolver::{self, ClientIpSources}; use bittorrent_tracker_core::announce_handler::{AnnounceHandler, PeersWanted}; use bittorrent_tracker_core::authentication::service::AuthenticationService; +use bittorrent_tracker_core::authentication::{self, Key}; use bittorrent_tracker_core::whitelist; use torrust_tracker_configuration::Core; use torrust_tracker_primitives::core::AnnounceData; @@ -42,12 +44,28 @@ use crate::packages::http_tracker_core; pub async fn handle_announce( core_config: &Arc, announce_handler: &Arc, - _authentication_service: &Arc, + authentication_service: &Arc, whitelist_authorization: &Arc, opt_http_stats_event_sender: &Arc>>, announce_request: &Announce, client_ip_sources: &ClientIpSources, + maybe_key: Option, ) -> Result { + // Authentication + if core_config.private { + match maybe_key { + Some(key) => match authentication_service.authenticate(&key).await { + Ok(()) => (), + Err(error) => return Err(error.into()), + }, + None => { + return Err(responses::error::Error::from(authentication::key::Error::MissingAuthKey { + location: Location::caller(), + })) + } + } + } + // Authorization match whitelist_authorization.authorize(&announce_request.info_hash).await { Ok(()) => (), @@ -257,6 +275,7 @@ mod tests { &core_http_tracker_services.http_stats_event_sender, &announce_request, &client_ip_sources, + None, ) .await .unwrap(); @@ -300,6 +319,7 @@ mod tests { &core_http_tracker_services.http_stats_event_sender, &announce_request, &client_ip_sources, + None, ) .await .unwrap(); @@ -351,6 +371,7 @@ mod tests { &core_http_tracker_services.http_stats_event_sender, &announce_request, &client_ip_sources, + None, ) .await .unwrap(); @@ -383,6 +404,7 @@ mod tests { &core_http_tracker_services.http_stats_event_sender, &announce_request, &client_ip_sources, + None, ) .await .unwrap(); diff --git a/src/packages/http_tracker_core/services/scrape.rs b/src/packages/http_tracker_core/services/scrape.rs index 667ce8d0d..7e3ea47fd 100644 --- a/src/packages/http_tracker_core/services/scrape.rs +++ b/src/packages/http_tracker_core/services/scrape.rs @@ -14,6 +14,8 @@ use bittorrent_http_protocol::v1::requests::scrape::Scrape; use bittorrent_http_protocol::v1::responses; use bittorrent_http_protocol::v1::services::peer_ip_resolver::{self, ClientIpSources}; use bittorrent_primitives::info_hash::InfoHash; +use bittorrent_tracker_core::authentication::service::AuthenticationService; +use bittorrent_tracker_core::authentication::Key; use bittorrent_tracker_core::scrape_handler::ScrapeHandler; use torrust_tracker_configuration::Core; use torrust_tracker_primitives::core::ScrapeData; @@ -40,12 +42,26 @@ use crate::packages::http_tracker_core; pub async fn handle_scrape( core_config: &Arc, scrape_handler: &Arc, + authentication_service: &Arc, opt_http_stats_event_sender: &Arc>>, scrape_request: &Scrape, client_ip_sources: &ClientIpSources, - return_fake_scrape_data: bool, + maybe_key: Option, ) -> Result { - // Authorization for scrape requests is handled at the `bittorrent-_racker_core` + // Authentication + let return_fake_scrape_data = if core_config.private { + match maybe_key { + Some(key) => match authentication_service.authenticate(&key).await { + Ok(()) => false, + Err(_error) => true, + }, + None => true, + } + } else { + false + }; + + // Authorization for scrape requests is handled at the `bittorrent_tracker_core` // level for each torrent. let peer_ip = match peer_ip_resolver::invoke(core_config.net.on_reverse_proxy, client_ip_sources) { @@ -111,6 +127,8 @@ mod tests { use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes, PeerId}; use bittorrent_primitives::info_hash::InfoHash; use bittorrent_tracker_core::announce_handler::AnnounceHandler; + use bittorrent_tracker_core::authentication::key::repository::in_memory::InMemoryKeyRepository; + use bittorrent_tracker_core::authentication::service::AuthenticationService; use bittorrent_tracker_core::databases::setup::initialize_database; use bittorrent_tracker_core::scrape_handler::ScrapeHandler; use bittorrent_tracker_core::torrent::repository::in_memory::InMemoryTorrentRepository; @@ -127,27 +145,39 @@ mod tests { use crate::packages::http_tracker_core; use crate::servers::http::test_helpers::tests::sample_info_hash; - fn initialize_announce_and_scrape_handlers_for_public_tracker() -> (Arc, Arc) { - initialize_announce_and_scrape_handlers_with_configuration(&configuration::ephemeral_public()) + struct Container { + announce_handler: Arc, + scrape_handler: Arc, + authentication_service: Arc, } - fn initialize_announce_and_scrape_handlers_with_configuration( - config: &Configuration, - ) -> (Arc, Arc) { + fn initialize_services_for_public_tracker() -> Container { + initialize_services_with_configuration(&configuration::ephemeral_public()) + } + + fn initialize_services_with_configuration(config: &Configuration) -> Container { let database = initialize_database(&config.core); let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); let whitelist_authorization = Arc::new(WhitelistAuthorization::new(&config.core, &in_memory_whitelist.clone())); let in_memory_torrent_repository = Arc::new(InMemoryTorrentRepository::default()); let db_torrent_repository = Arc::new(DatabasePersistentTorrentRepository::new(&database)); + let in_memory_key_repository = Arc::new(InMemoryKeyRepository::default()); + let authentication_service = Arc::new(AuthenticationService::new(&config.core, &in_memory_key_repository)); + let announce_handler = Arc::new(AnnounceHandler::new( &config.core, &whitelist_authorization, &in_memory_torrent_repository, &db_torrent_repository, )); + let scrape_handler = Arc::new(ScrapeHandler::new(&whitelist_authorization, &in_memory_torrent_repository)); - (announce_handler, scrape_handler) + Container { + announce_handler, + scrape_handler, + authentication_service, + } } fn sample_info_hashes() -> Vec { @@ -166,14 +196,6 @@ mod tests { } } - fn initialize_scrape_handler_with_config(config: &Configuration) -> Arc { - let in_memory_whitelist = Arc::new(InMemoryWhitelist::default()); - let whitelist_authorization = Arc::new(WhitelistAuthorization::new(&config.core, &in_memory_whitelist.clone())); - let in_memory_torrent_repository = Arc::new(InMemoryTorrentRepository::default()); - - Arc::new(ScrapeHandler::new(&whitelist_authorization, &in_memory_torrent_repository)) - } - mock! { HttpStatsEventSender {} impl http_tracker_core::statistics::event::sender::Sender for HttpStatsEventSender { @@ -197,8 +219,7 @@ mod tests { use crate::packages::http_tracker_core::services::scrape::handle_scrape; use crate::packages::http_tracker_core::services::scrape::tests::{ - initialize_announce_and_scrape_handlers_with_configuration, initialize_scrape_handler_with_config, - sample_info_hashes, sample_peer, MockHttpStatsEventSender, + initialize_services_with_configuration, sample_info_hashes, sample_peer, MockHttpStatsEventSender, }; use crate::packages::{self, http_tracker_core}; use crate::servers::http::test_helpers::tests::sample_info_hash; @@ -212,7 +233,7 @@ mod tests { packages::http_tracker_core::statistics::setup::factory(false); let http_stats_event_sender = Arc::new(http_stats_event_sender); - let (announce_handler, scrape_handler) = initialize_announce_and_scrape_handlers_with_configuration(&configuration); + let container = initialize_services_with_configuration(&configuration); let info_hash = sample_info_hash(); let info_hashes = vec![info_hash]; @@ -220,7 +241,8 @@ mod tests { // Announce a new peer to force scrape data to contain non zeroed data let mut peer = sample_peer(); let original_peer_ip = peer.ip(); - announce_handler + container + .announce_handler .announce(&info_hash, &mut peer, &original_peer_ip, &PeersWanted::AsManyAsPossible) .await .unwrap(); @@ -236,11 +258,12 @@ mod tests { let scrape_data = handle_scrape( &core_config, - &scrape_handler, + &container.scrape_handler, + &container.authentication_service, &http_stats_event_sender, &scrape_request, &client_ip_sources, - false, + None, ) .await .unwrap(); @@ -271,7 +294,7 @@ mod tests { let http_stats_event_sender: Arc>> = Arc::new(Some(Box::new(http_stats_event_sender_mock))); - let scrape_handler = initialize_scrape_handler_with_config(&config); + let container = initialize_services_with_configuration(&config); let peer_ip = IpAddr::V4(Ipv4Addr::new(126, 0, 0, 1)); @@ -286,11 +309,12 @@ mod tests { handle_scrape( &Arc::new(config.core), - &scrape_handler, + &container.scrape_handler, + &container.authentication_service, &http_stats_event_sender, &scrape_request, &client_ip_sources, - false, + None, ) .await .unwrap(); @@ -309,7 +333,7 @@ mod tests { let http_stats_event_sender: Arc>> = Arc::new(Some(Box::new(http_stats_event_sender_mock))); - let scrape_handler = initialize_scrape_handler_with_config(&config); + let container = initialize_services_with_configuration(&config); let peer_ip = IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)); @@ -324,11 +348,12 @@ mod tests { handle_scrape( &Arc::new(config.core), - &scrape_handler, + &container.scrape_handler, + &container.authentication_service, &http_stats_event_sender, &scrape_request, &client_ip_sources, - false, + None, ) .await .unwrap(); @@ -347,7 +372,7 @@ mod tests { use crate::packages::http_tracker_core::services::scrape::fake; use crate::packages::http_tracker_core::services::scrape::tests::{ - initialize_announce_and_scrape_handlers_for_public_tracker, sample_info_hashes, sample_peer, MockHttpStatsEventSender, + initialize_services_for_public_tracker, sample_info_hashes, sample_peer, MockHttpStatsEventSender, }; use crate::packages::{self, http_tracker_core}; use crate::servers::http::test_helpers::tests::sample_info_hash; @@ -358,7 +383,7 @@ mod tests { packages::http_tracker_core::statistics::setup::factory(false); let http_stats_event_sender = Arc::new(http_stats_event_sender); - let (announce_handler, _scrape_handler) = initialize_announce_and_scrape_handlers_for_public_tracker(); + let container = initialize_services_for_public_tracker(); let info_hash = sample_info_hash(); let info_hashes = vec![info_hash]; @@ -366,7 +391,8 @@ mod tests { // Announce a new peer to force scrape data to contain not zeroed data let mut peer = sample_peer(); let original_peer_ip = peer.ip(); - announce_handler + container + .announce_handler .announce(&info_hash, &mut peer, &original_peer_ip, &PeersWanted::AsManyAsPossible) .await .unwrap(); diff --git a/src/servers/udp/connection_cookie.rs b/src/packages/udp_tracker_core/connection_cookie.rs similarity index 89% rename from src/servers/udp/connection_cookie.rs rename to src/packages/udp_tracker_core/connection_cookie.rs index 439be9da7..b9070c63a 100644 --- a/src/servers/udp/connection_cookie.rs +++ b/src/packages/udp_tracker_core/connection_cookie.rs @@ -79,12 +79,25 @@ use aquatic_udp_protocol::ConnectionId as Cookie; use cookie_builder::{assemble, decode, disassemble, encode}; +use thiserror::Error; use tracing::instrument; use zerocopy::AsBytes; -use super::error::Error; use crate::shared::crypto::keys::CipherArrayBlowfish; +/// Error returned when there was an error with the connection cookie. +#[derive(Error, Debug, Clone)] +pub enum ConnectionCookieError { + #[error("cookie value is not normal: {not_normal_value}")] + ValueNotNormal { not_normal_value: f64 }, + + #[error("cookie value is expired: {expired_value}, expected > {min_value}")] + ValueExpired { expired_value: f64, min_value: f64 }, + + #[error("cookie value is from future: {future_value}, expected < {max_value}")] + ValueFromFuture { future_value: f64, max_value: f64 }, +} + /// Generates a new connection cookie. /// /// # Errors @@ -96,9 +109,9 @@ use crate::shared::crypto::keys::CipherArrayBlowfish; /// It would panic if the cookie is not exactly 8 bytes is size. /// #[instrument(err)] -pub fn make(fingerprint: u64, issue_at: f64) -> Result { +pub fn make(fingerprint: u64, issue_at: f64) -> Result { if !issue_at.is_normal() { - return Err(Error::CookieValueNotNormal { + return Err(ConnectionCookieError::ValueNotNormal { not_normal_value: issue_at, }); } @@ -110,6 +123,8 @@ pub fn make(fingerprint: u64, issue_at: f64) -> Result { Ok(zerocopy::FromBytes::read_from(cookie.as_slice()).expect("it should be the same size")) } +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::net::SocketAddr; use std::ops::Range; /// Checks if the supplied `connection_cookie` is valid. @@ -122,7 +137,7 @@ use std::ops::Range; /// /// It would panic if the range start is not smaller than it's end. #[instrument] -pub fn check(cookie: &Cookie, fingerprint: u64, valid_range: Range) -> Result { +pub fn check(cookie: &Cookie, fingerprint: u64, valid_range: Range) -> Result { assert!(valid_range.start <= valid_range.end, "range start is larger than range end"); let cookie_bytes = CipherArrayBlowfish::from_slice(cookie.0.as_bytes()); @@ -131,20 +146,20 @@ pub fn check(cookie: &Cookie, fingerprint: u64, valid_range: Range) -> Resu let issue_time = disassemble(fingerprint, cookie_bytes); if !issue_time.is_normal() { - return Err(Error::CookieValueNotNormal { + return Err(ConnectionCookieError::ValueNotNormal { not_normal_value: issue_time, }); } if issue_time < valid_range.start { - return Err(Error::CookieValueExpired { + return Err(ConnectionCookieError::ValueExpired { expired_value: issue_time, min_value: valid_range.start, }); } if issue_time > valid_range.end { - return Err(Error::CookieValueFromFuture { + return Err(ConnectionCookieError::ValueFromFuture { future_value: issue_time, max_value: valid_range.end, }); @@ -153,6 +168,13 @@ pub fn check(cookie: &Cookie, fingerprint: u64, valid_range: Range) -> Resu Ok(issue_time) } +#[must_use] +pub(crate) fn gen_remote_fingerprint(remote_addr: &SocketAddr) -> u64 { + let mut state = DefaultHasher::new(); + remote_addr.hash(&mut state); + state.finish() +} + mod cookie_builder { use cipher::{BlockDecrypt, BlockEncrypt}; use tracing::instrument; @@ -287,7 +309,7 @@ mod tests { let result = check(&cookie, fingerprint, min..max).unwrap_err(); match result { - Error::CookieValueExpired { .. } => {} // Expected error + ConnectionCookieError::ValueExpired { .. } => {} // Expected error _ => panic!("Expected ConnectionIdExpired error"), } } @@ -305,7 +327,7 @@ mod tests { let result = check(&cookie, fingerprint, min..max).unwrap_err(); match result { - Error::CookieValueFromFuture { .. } => {} // Expected error + ConnectionCookieError::ValueFromFuture { .. } => {} // Expected error _ => panic!("Expected ConnectionIdFromFuture error"), } } diff --git a/src/packages/udp_tracker_core/mod.rs b/src/packages/udp_tracker_core/mod.rs index 4f3e54857..1c93f811a 100644 --- a/src/packages/udp_tracker_core/mod.rs +++ b/src/packages/udp_tracker_core/mod.rs @@ -1,2 +1,3 @@ +pub mod connection_cookie; pub mod services; pub mod statistics; diff --git a/src/packages/udp_tracker_core/services/announce.rs b/src/packages/udp_tracker_core/services/announce.rs index 5851cdc92..a825d06ad 100644 --- a/src/packages/udp_tracker_core/services/announce.rs +++ b/src/packages/udp_tracker_core/services/announce.rs @@ -8,19 +8,57 @@ //! It also sends an [`udp_tracker_core::statistics::event::Event`] //! because events are specific for the HTTP tracker. use std::net::{IpAddr, SocketAddr}; +use std::ops::Range; use std::sync::Arc; use aquatic_udp_protocol::AnnounceRequest; use bittorrent_primitives::info_hash::InfoHash; use bittorrent_tracker_core::announce_handler::{AnnounceHandler, PeersWanted}; -use bittorrent_tracker_core::error::AnnounceError; +use bittorrent_tracker_core::error::{AnnounceError, WhitelistError}; use bittorrent_tracker_core::whitelist; use bittorrent_udp_protocol::peer_builder; use torrust_tracker_primitives::core::AnnounceData; use torrust_tracker_primitives::peer; +use crate::packages::udp_tracker_core::connection_cookie::{check, gen_remote_fingerprint, ConnectionCookieError}; use crate::packages::udp_tracker_core::{self}; +/// Errors related to announce requests. +#[derive(thiserror::Error, Debug, Clone)] +pub enum UdpAnnounceError { + /// Error returned when there was an error with the connection cookie. + #[error("Connection cookie error: {source}")] + ConnectionCookieError { source: ConnectionCookieError }, + + /// Error returned when there was an error with the tracker core announce handler. + #[error("Tracker core announce error: {source}")] + TrackerCoreAnnounceError { source: AnnounceError }, + + /// Error returned when there was an error with the tracker core whitelist. + #[error("Tracker core whitelist error: {source}")] + TrackerCoreWhitelistError { source: WhitelistError }, +} + +impl From for UdpAnnounceError { + fn from(connection_cookie_error: ConnectionCookieError) -> Self { + Self::ConnectionCookieError { + source: connection_cookie_error, + } + } +} + +impl From for UdpAnnounceError { + fn from(announce_error: AnnounceError) -> Self { + Self::TrackerCoreAnnounceError { source: announce_error } + } +} + +impl From for UdpAnnounceError { + fn from(whitelist_error: WhitelistError) -> Self { + Self::TrackerCoreWhitelistError { source: whitelist_error } + } +} + /// It handles the `Announce` request. /// /// # Errors @@ -36,7 +74,17 @@ pub async fn handle_announce( announce_handler: &Arc, whitelist_authorization: &Arc, opt_udp_stats_event_sender: &Arc>>, -) -> Result { + cookie_valid_range: Range, +) -> Result { + // todo: return a UDP response like the HTTP tracker instead of raw AnnounceData. + + // Authentication + check( + &request.connection_id, + gen_remote_fingerprint(&remote_addr), + cookie_valid_range, + )?; + let info_hash = request.info_hash.into(); let remote_client_ip = remote_addr.ip(); diff --git a/src/packages/udp_tracker_core/services/connect.rs b/src/packages/udp_tracker_core/services/connect.rs new file mode 100644 index 000000000..4cc8b0a3b --- /dev/null +++ b/src/packages/udp_tracker_core/services/connect.rs @@ -0,0 +1,129 @@ +//! The `connect` service. +//! +//! The service is responsible for handling the `connect` requests. +use std::net::SocketAddr; +use std::sync::Arc; + +use aquatic_udp_protocol::ConnectionId; + +use crate::packages::udp_tracker_core; +use crate::packages::udp_tracker_core::connection_cookie::{gen_remote_fingerprint, make}; + +/// # Panics +/// +/// IT will panic if there was an error making the connection cookie. +pub async fn handle_connect( + remote_addr: SocketAddr, + opt_udp_stats_event_sender: &Arc>>, + cookie_issue_time: f64, +) -> ConnectionId { + // todo: return a UDP response like the HTTP tracker instead of raw ConnectionId. + + let connection_id = make(gen_remote_fingerprint(&remote_addr), cookie_issue_time).expect("it should be a normal value"); + + if let Some(udp_stats_event_sender) = opt_udp_stats_event_sender.as_deref() { + match remote_addr { + SocketAddr::V4(_) => { + udp_stats_event_sender + .send_event(udp_tracker_core::statistics::event::Event::Udp4Connect) + .await; + } + SocketAddr::V6(_) => { + udp_stats_event_sender + .send_event(udp_tracker_core::statistics::event::Event::Udp6Connect) + .await; + } + } + } + + connection_id +} + +#[cfg(test)] +mod tests { + + mod connect_request { + + use std::future; + use std::sync::Arc; + + use mockall::predicate::eq; + + use crate::packages::udp_tracker_core::connection_cookie::make; + use crate::packages::udp_tracker_core::services::connect::handle_connect; + use crate::packages::udp_tracker_core::services::tests::{ + sample_ipv4_remote_addr, sample_ipv4_remote_addr_fingerprint, sample_ipv4_socket_address, sample_ipv6_remote_addr, + sample_ipv6_remote_addr_fingerprint, sample_issue_time, MockUdpStatsEventSender, + }; + use crate::packages::{self, udp_tracker_core}; + + #[tokio::test] + async fn a_connect_response_should_contain_the_same_transaction_id_as_the_connect_request() { + let (udp_stats_event_sender, _udp_stats_repository) = packages::udp_tracker_core::statistics::setup::factory(false); + let udp_stats_event_sender = Arc::new(udp_stats_event_sender); + + let response = handle_connect(sample_ipv4_remote_addr(), &udp_stats_event_sender, sample_issue_time()).await; + + assert_eq!( + response, + make(sample_ipv4_remote_addr_fingerprint(), sample_issue_time()).unwrap() + ); + } + + #[tokio::test] + async fn a_connect_response_should_contain_a_new_connection_id() { + let (udp_stats_event_sender, _udp_stats_repository) = packages::udp_tracker_core::statistics::setup::factory(false); + let udp_stats_event_sender = Arc::new(udp_stats_event_sender); + + let response = handle_connect(sample_ipv4_remote_addr(), &udp_stats_event_sender, sample_issue_time()).await; + + assert_eq!( + response, + make(sample_ipv4_remote_addr_fingerprint(), sample_issue_time()).unwrap(), + ); + } + + #[tokio::test] + async fn a_connect_response_should_contain_a_new_connection_id_ipv6() { + let (udp_stats_event_sender, _udp_stats_repository) = packages::udp_tracker_core::statistics::setup::factory(false); + let udp_stats_event_sender = Arc::new(udp_stats_event_sender); + + let response = handle_connect(sample_ipv6_remote_addr(), &udp_stats_event_sender, sample_issue_time()).await; + + assert_eq!( + response, + make(sample_ipv6_remote_addr_fingerprint(), sample_issue_time()).unwrap(), + ); + } + + #[tokio::test] + async fn it_should_send_the_upd4_connect_event_when_a_client_tries_to_connect_using_a_ip4_socket_address() { + let mut udp_stats_event_sender_mock = MockUdpStatsEventSender::new(); + udp_stats_event_sender_mock + .expect_send_event() + .with(eq(udp_tracker_core::statistics::event::Event::Udp4Connect)) + .times(1) + .returning(|_| Box::pin(future::ready(Some(Ok(()))))); + let udp_stats_event_sender: Arc>> = + Arc::new(Some(Box::new(udp_stats_event_sender_mock))); + + let client_socket_address = sample_ipv4_socket_address(); + + handle_connect(client_socket_address, &udp_stats_event_sender, sample_issue_time()).await; + } + + #[tokio::test] + async fn it_should_send_the_upd6_connect_event_when_a_client_tries_to_connect_using_a_ip6_socket_address() { + let mut udp_stats_event_sender_mock = MockUdpStatsEventSender::new(); + udp_stats_event_sender_mock + .expect_send_event() + .with(eq(udp_tracker_core::statistics::event::Event::Udp6Connect)) + .times(1) + .returning(|_| Box::pin(future::ready(Some(Ok(()))))); + let udp_stats_event_sender: Arc>> = + Arc::new(Some(Box::new(udp_stats_event_sender_mock))); + + handle_connect(sample_ipv6_remote_addr(), &udp_stats_event_sender, sample_issue_time()).await; + } + } +} diff --git a/src/packages/udp_tracker_core/services/mod.rs b/src/packages/udp_tracker_core/services/mod.rs index 776d2dfbf..5b222c4d9 100644 --- a/src/packages/udp_tracker_core/services/mod.rs +++ b/src/packages/udp_tracker_core/services/mod.rs @@ -1,2 +1,51 @@ pub mod announce; +pub mod connect; pub mod scrape; + +#[cfg(test)] +pub(crate) mod tests { + + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + + use futures::future::BoxFuture; + use mockall::mock; + use tokio::sync::mpsc::error::SendError; + + use crate::packages::udp_tracker_core; + use crate::packages::udp_tracker_core::connection_cookie::gen_remote_fingerprint; + + pub(crate) fn sample_ipv4_remote_addr() -> SocketAddr { + sample_ipv4_socket_address() + } + + pub(crate) fn sample_ipv4_remote_addr_fingerprint() -> u64 { + gen_remote_fingerprint(&sample_ipv4_socket_address()) + } + + pub(crate) fn sample_ipv6_remote_addr() -> SocketAddr { + sample_ipv6_socket_address() + } + + pub(crate) fn sample_ipv6_remote_addr_fingerprint() -> u64 { + gen_remote_fingerprint(&sample_ipv6_socket_address()) + } + + pub(crate) fn sample_ipv4_socket_address() -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080) + } + + fn sample_ipv6_socket_address() -> SocketAddr { + SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8080) + } + + pub(crate) fn sample_issue_time() -> f64 { + 1_000_000_000_f64 + } + + mock! { + pub(crate) UdpStatsEventSender {} + impl udp_tracker_core::statistics::event::sender::Sender for UdpStatsEventSender { + fn send_event(&self, event: udp_tracker_core::statistics::event::Event) -> BoxFuture<'static,Option > > > ; + } + } +} diff --git a/src/packages/udp_tracker_core/services/scrape.rs b/src/packages/udp_tracker_core/services/scrape.rs index 07ae452f8..5beb54e9f 100644 --- a/src/packages/udp_tracker_core/services/scrape.rs +++ b/src/packages/udp_tracker_core/services/scrape.rs @@ -8,15 +8,53 @@ //! It also sends an [`udp_tracker_core::statistics::event::Event`] //! because events are specific for the UDP tracker. use std::net::SocketAddr; +use std::ops::Range; use std::sync::Arc; use aquatic_udp_protocol::ScrapeRequest; use bittorrent_primitives::info_hash::InfoHash; -use bittorrent_tracker_core::error::ScrapeError; +use bittorrent_tracker_core::error::{ScrapeError, WhitelistError}; use bittorrent_tracker_core::scrape_handler::ScrapeHandler; use torrust_tracker_primitives::core::ScrapeData; use crate::packages::udp_tracker_core; +use crate::packages::udp_tracker_core::connection_cookie::{check, gen_remote_fingerprint, ConnectionCookieError}; + +/// Errors related to scrape requests. +#[derive(thiserror::Error, Debug, Clone)] +pub enum UdpScrapeError { + /// Error returned when there was an error with the connection cookie. + #[error("Connection cookie error: {source}")] + ConnectionCookieError { source: ConnectionCookieError }, + + /// Error returned when there was an error with the tracker core scrape handler. + #[error("Tracker core scrape error: {source}")] + TrackerCoreScrapeError { source: ScrapeError }, + + /// Error returned when there was an error with the tracker core whitelist. + #[error("Tracker core whitelist error: {source}")] + TrackerCoreWhitelistError { source: WhitelistError }, +} + +impl From for UdpScrapeError { + fn from(connection_cookie_error: ConnectionCookieError) -> Self { + Self::ConnectionCookieError { + source: connection_cookie_error, + } + } +} + +impl From for UdpScrapeError { + fn from(scrape_error: ScrapeError) -> Self { + Self::TrackerCoreScrapeError { source: scrape_error } + } +} + +impl From for UdpScrapeError { + fn from(whitelist_error: WhitelistError) -> Self { + Self::TrackerCoreWhitelistError { source: whitelist_error } + } +} /// It handles the `Scrape` request. /// @@ -28,7 +66,16 @@ pub async fn handle_scrape( request: &ScrapeRequest, scrape_handler: &Arc, opt_udp_stats_event_sender: &Arc>>, -) -> Result { + cookie_valid_range: Range, +) -> Result { + // todo: return a UDP response like the HTTP tracker instead of raw ScrapeData. + + check( + &request.connection_id, + gen_remote_fingerprint(&remote_addr), + cookie_valid_range, + )?; + // Convert from aquatic infohashes let info_hashes: Vec = request.info_hashes.iter().map(|&x| x.into()).collect(); diff --git a/src/servers/http/v1/extractors/authentication_key.rs b/src/servers/http/v1/extractors/authentication_key.rs index 0e46b75dd..c99c7000a 100644 --- a/src/servers/http/v1/extractors/authentication_key.rs +++ b/src/servers/http/v1/extractors/authentication_key.rs @@ -117,11 +117,6 @@ fn custom_error(rejection: &PathRejection) -> responses::error::Error { location: Location::caller(), }) } - axum::extract::rejection::PathRejection::MissingPathParams(_) => { - responses::error::Error::from(auth::Error::MissingAuthKey { - location: Location::caller(), - }) - } _ => responses::error::Error::from(auth::Error::CannotExtractKeyParam { location: Location::caller(), }), @@ -148,6 +143,9 @@ mod tests { let response = parse_key(invalid_key).unwrap_err(); - assert_error_response(&response, "Authentication error: Invalid format for authentication key param"); + assert_error_response( + &response, + "Tracker authentication error: Invalid format for authentication key param", + ); } } diff --git a/src/servers/http/v1/handlers/announce.rs b/src/servers/http/v1/handlers/announce.rs index 5f25c317b..76f4e5134 100644 --- a/src/servers/http/v1/handlers/announce.rs +++ b/src/servers/http/v1/handlers/announce.rs @@ -5,7 +5,6 @@ //! //! The handlers perform the authentication and authorization of the request, //! and resolve the client IP address. -use std::panic::Location; use std::sync::Arc; use aquatic_udp_protocol::AnnounceEvent; @@ -22,12 +21,10 @@ use hyper::StatusCode; use torrust_tracker_configuration::Core; use torrust_tracker_primitives::core::AnnounceData; -use super::common::auth::map_auth_error_to_error_response; use crate::packages::http_tracker_core; use crate::servers::http::v1::extractors::announce_request::ExtractRequest; use crate::servers::http::v1::extractors::authentication_key::Extract as ExtractKey; use crate::servers::http::v1::extractors::client_ip_sources::Extract as ExtractClientIpSources; -use crate::servers::http::v1::handlers::common::auth; /// It handles the `announce` request when the HTTP tracker does not require /// authentication (no PATH `key` parameter required). @@ -134,23 +131,6 @@ async fn handle_announce( client_ip_sources: &ClientIpSources, maybe_key: Option, ) -> Result { - // todo: move authentication inside `http_tracker_core::services::announce::handle_announce` - - // Authentication - if core_config.private { - match maybe_key { - Some(key) => match authentication_service.authenticate(&key).await { - Ok(()) => (), - Err(error) => return Err(map_auth_error_to_error_response(&error)), - }, - None => { - return Err(responses::error::Error::from(auth::Error::MissingAuthKey { - location: Location::caller(), - })) - } - } - } - http_tracker_core::services::announce::handle_announce( &core_config.clone(), &announce_handler.clone(), @@ -159,6 +139,7 @@ async fn handle_announce( &opt_http_stats_event_sender.clone(), announce_request, client_ip_sources, + maybe_key, ) .await } @@ -339,10 +320,7 @@ mod tests { .await .unwrap_err(); - assert_error_response( - &response, - "Authentication error: Missing authentication key param for private tracker", - ); + assert_error_response(&response, "Tracker authentication error: Missing authentication key"); } #[tokio::test] @@ -366,7 +344,10 @@ mod tests { .await .unwrap_err(); - assert_error_response(&response, "Authentication error: Failed to read key"); + assert_error_response( + &response, + "Tracker authentication error: Failed to read key: YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ", + ); } } diff --git a/src/servers/http/v1/handlers/common/auth.rs b/src/servers/http/v1/handlers/common/auth.rs index c8625d03a..f45064ae3 100644 --- a/src/servers/http/v1/handlers/common/auth.rs +++ b/src/servers/http/v1/handlers/common/auth.rs @@ -14,10 +14,9 @@ use thiserror::Error; /// from the URL path. #[derive(Debug, Error)] pub enum Error { - #[error("Missing authentication key param for private tracker. Error in {location}")] - MissingAuthKey { location: &'static Location<'static> }, #[error("Invalid format for authentication key param. Error in {location}")] InvalidKeyFormat { location: &'static Location<'static> }, + #[error("Cannot extract authentication key param from URL path. Error in {location}")] CannotExtractKeyParam { location: &'static Location<'static> }, } @@ -25,7 +24,7 @@ pub enum Error { impl From for responses::error::Error { fn from(err: Error) -> Self { responses::error::Error { - failure_reason: format!("Authentication error: {err}"), + failure_reason: format!("Tracker authentication error: {err}"), } } } @@ -36,6 +35,6 @@ pub fn map_auth_error_to_error_response(err: &authentication::Error) -> response // impl From for responses::error::Error // Consider moving the trait implementation to the http-protocol package. responses::error::Error { - failure_reason: format!("Authentication error: {err}"), + failure_reason: format!("Tracker authentication error: {err}"), } } diff --git a/src/servers/http/v1/handlers/scrape.rs b/src/servers/http/v1/handlers/scrape.rs index 2d41c2f78..946190e8f 100644 --- a/src/servers/http/v1/handlers/scrape.rs +++ b/src/servers/http/v1/handlers/scrape.rs @@ -107,6 +107,7 @@ async fn handle( Ok(scrape_data) => scrape_data, Err(error) => return (StatusCode::OK, error.write()).into_response(), }; + build_response(scrape_data) } @@ -120,28 +121,14 @@ async fn handle_scrape( client_ip_sources: &ClientIpSources, maybe_key: Option, ) -> Result { - // todo: move authentication inside `http_tracker_core::services::scrape::handle_scrape` - - // Authentication - let return_fake_scrape_data = if core_config.private { - match maybe_key { - Some(key) => match authentication_service.authenticate(&key).await { - Ok(()) => false, - Err(_error) => true, - }, - None => true, - } - } else { - false - }; - http_tracker_core::services::scrape::handle_scrape( core_config, scrape_handler, + authentication_service, opt_http_stats_event_sender, scrape_request, client_ip_sources, - return_fake_scrape_data, + maybe_key, ) .await } diff --git a/src/servers/udp/error.rs b/src/servers/udp/error.rs index cda562aed..9105ba0cb 100644 --- a/src/servers/udp/error.rs +++ b/src/servers/udp/error.rs @@ -6,6 +6,9 @@ use derive_more::derive::Display; use thiserror::Error; use torrust_tracker_located_error::LocatedError; +use crate::packages::udp_tracker_core::services::announce::UdpAnnounceError; +use crate::packages::udp_tracker_core::services::scrape::UdpScrapeError; + #[derive(Display, Debug)] #[display(":?")] pub struct ConnectionCookie(pub ConnectionId); @@ -13,23 +16,17 @@ pub struct ConnectionCookie(pub ConnectionId); /// Error returned by the UDP server. #[derive(Error, Debug)] pub enum Error { - #[error("cookie value is not normal: {not_normal_value}")] - CookieValueNotNormal { not_normal_value: f64 }, - - #[error("cookie value is expired: {expired_value}, expected > {min_value}")] - CookieValueExpired { expired_value: f64, min_value: f64 }, - - #[error("cookie value is from future: {future_value}, expected < {max_value}")] - CookieValueFromFuture { future_value: f64, max_value: f64 }, - + /// Error returned when the request is invalid. #[error("error when phrasing request: {request_parse_error:?}")] RequestParseError { request_parse_error: RequestParseError }, - /// Error returned when the domain tracker returns an error. - #[error("tracker server error: {source}")] - TrackerError { - source: LocatedError<'static, dyn std::error::Error + Send + Sync>, - }, + /// Error returned when the domain tracker returns an announce error. + #[error("tracker announce error: {source}")] + UdpAnnounceError { source: UdpAnnounceError }, + + /// Error returned when the domain tracker returns an scrape error. + #[error("tracker scrape error: {source}")] + UdpScrapeError { source: UdpScrapeError }, /// Error returned from a third-party library (`aquatic_udp_protocol`). #[error("internal server error: {message}, {location}")] @@ -54,3 +51,19 @@ impl From for Error { Self::RequestParseError { request_parse_error } } } + +impl From for Error { + fn from(udp_announce_error: UdpAnnounceError) -> Self { + Self::UdpAnnounceError { + source: udp_announce_error, + } + } +} + +impl From for Error { + fn from(udp_scrape_error: UdpScrapeError) -> Self { + Self::UdpScrapeError { + source: udp_scrape_error, + } + } +} diff --git a/src/servers/udp/handlers/announce.rs b/src/servers/udp/handlers/announce.rs index a273a2ecb..1003b4041 100644 --- a/src/servers/udp/handlers/announce.rs +++ b/src/servers/udp/handlers/announce.rs @@ -11,13 +11,12 @@ use bittorrent_primitives::info_hash::InfoHash; use bittorrent_tracker_core::announce_handler::AnnounceHandler; use bittorrent_tracker_core::whitelist; use torrust_tracker_configuration::Core; +use torrust_tracker_primitives::core::AnnounceData; use tracing::{instrument, Level}; use zerocopy::network_endian::I32; use crate::packages::udp_tracker_core::{self}; -use crate::servers::udp::connection_cookie::check; use crate::servers::udp::error::Error; -use crate::servers::udp::handlers::gen_remote_fingerprint; /// It handles the `Announce` request. Refer to [`Announce`](crate::servers::udp#announce) /// request for more information. @@ -43,40 +42,36 @@ pub async fn handle_announce( tracing::trace!("handle announce"); - // todo: move authentication to `udp_tracker_core::services::announce::handle_announce` - - check( - &request.connection_id, - gen_remote_fingerprint(&remote_addr), - cookie_valid_range, - ) - .map_err(|e| (e, request.transaction_id))?; - - let response = udp_tracker_core::services::announce::handle_announce( + let announce_data = udp_tracker_core::services::announce::handle_announce( remote_addr, request, announce_handler, whitelist_authorization, opt_udp_stats_event_sender, + cookie_valid_range, ) .await - .map_err(|e| Error::TrackerError { - source: (Arc::new(e) as Arc).into(), - }) - .map_err(|e| (e, request.transaction_id))?; + .map_err(|e| (e.into(), request.transaction_id))?; - // todo: extract `build_response` function. + Ok(build_response(remote_addr, request, core_config, &announce_data)) +} +fn build_response( + remote_addr: SocketAddr, + request: &AnnounceRequest, + core_config: &Arc, + announce_data: &AnnounceData, +) -> Response { #[allow(clippy::cast_possible_truncation)] if remote_addr.is_ipv4() { let announce_response = AnnounceResponse { fixed: AnnounceResponseFixedData { transaction_id: request.transaction_id, announce_interval: AnnounceInterval(I32::new(i64::from(core_config.announce_policy.interval) as i32)), - leechers: NumberOfPeers(I32::new(i64::from(response.stats.incomplete) as i32)), - seeders: NumberOfPeers(I32::new(i64::from(response.stats.complete) as i32)), + leechers: NumberOfPeers(I32::new(i64::from(announce_data.stats.incomplete) as i32)), + seeders: NumberOfPeers(I32::new(i64::from(announce_data.stats.complete) as i32)), }, - peers: response + peers: announce_data .peers .iter() .filter_map(|peer| { @@ -92,16 +87,16 @@ pub async fn handle_announce( .collect(), }; - Ok(Response::from(announce_response)) + Response::from(announce_response) } else { let announce_response = AnnounceResponse { fixed: AnnounceResponseFixedData { transaction_id: request.transaction_id, announce_interval: AnnounceInterval(I32::new(i64::from(core_config.announce_policy.interval) as i32)), - leechers: NumberOfPeers(I32::new(i64::from(response.stats.incomplete) as i32)), - seeders: NumberOfPeers(I32::new(i64::from(response.stats.complete) as i32)), + leechers: NumberOfPeers(I32::new(i64::from(announce_data.stats.incomplete) as i32)), + seeders: NumberOfPeers(I32::new(i64::from(announce_data.stats.complete) as i32)), }, - peers: response + peers: announce_data .peers .iter() .filter_map(|peer| { @@ -117,7 +112,7 @@ pub async fn handle_announce( .collect(), }; - Ok(Response::from(announce_response)) + Response::from(announce_response) } } @@ -134,7 +129,7 @@ mod tests { PeerId as AquaticPeerId, PeerKey, Port, TransactionId, }; - use crate::servers::udp::connection_cookie::make; + use crate::packages::udp_tracker_core::connection_cookie::make; use crate::servers::udp::handlers::tests::{sample_ipv4_remote_addr_fingerprint, sample_issue_time}; struct AnnounceRequestBuilder { @@ -213,15 +208,15 @@ mod tests { use mockall::predicate::eq; use torrust_tracker_configuration::Core; + use crate::packages::udp_tracker_core::connection_cookie::{gen_remote_fingerprint, make}; use crate::packages::{self, udp_tracker_core}; - use crate::servers::udp::connection_cookie::make; use crate::servers::udp::handlers::announce::tests::announce_request::AnnounceRequestBuilder; + use crate::servers::udp::handlers::handle_announce; use crate::servers::udp::handlers::tests::{ initialize_core_tracker_services_for_default_tracker_configuration, initialize_core_tracker_services_for_public_tracker, sample_cookie_valid_range, sample_ipv4_socket_address, sample_issue_time, MockUdpStatsEventSender, TorrentPeerBuilder, }; - use crate::servers::udp::handlers::{gen_remote_fingerprint, handle_announce}; #[tokio::test] async fn an_announced_peer_should_be_added_to_the_tracker() { @@ -447,13 +442,13 @@ mod tests { use aquatic_udp_protocol::{InfoHash as AquaticInfoHash, PeerId as AquaticPeerId}; - use crate::servers::udp::connection_cookie::make; + use crate::packages::udp_tracker_core::connection_cookie::{gen_remote_fingerprint, make}; use crate::servers::udp::handlers::announce::tests::announce_request::AnnounceRequestBuilder; + use crate::servers::udp::handlers::handle_announce; use crate::servers::udp::handlers::tests::{ initialize_core_tracker_services_for_public_tracker, sample_cookie_valid_range, sample_issue_time, TorrentPeerBuilder, }; - use crate::servers::udp::handlers::{gen_remote_fingerprint, handle_announce}; #[tokio::test] async fn the_peer_ip_should_be_changed_to_the_external_ip_in_the_tracker_configuration_if_defined() { @@ -520,15 +515,15 @@ mod tests { use mockall::predicate::eq; use torrust_tracker_configuration::Core; + use crate::packages::udp_tracker_core::connection_cookie::{gen_remote_fingerprint, make}; use crate::packages::{self, udp_tracker_core}; - use crate::servers::udp::connection_cookie::make; use crate::servers::udp::handlers::announce::tests::announce_request::AnnounceRequestBuilder; + use crate::servers::udp::handlers::handle_announce; use crate::servers::udp::handlers::tests::{ initialize_core_tracker_services_for_default_tracker_configuration, initialize_core_tracker_services_for_public_tracker, sample_cookie_valid_range, sample_ipv6_remote_addr, sample_issue_time, MockUdpStatsEventSender, TorrentPeerBuilder, }; - use crate::servers::udp::handlers::{gen_remote_fingerprint, handle_announce}; #[tokio::test] async fn an_announced_peer_should_be_added_to_the_tracker() { @@ -776,12 +771,12 @@ mod tests { use mockall::predicate::eq; use crate::packages::udp_tracker_core; - use crate::servers::udp::connection_cookie::make; + use crate::packages::udp_tracker_core::connection_cookie::{gen_remote_fingerprint, make}; use crate::servers::udp::handlers::announce::tests::announce_request::AnnounceRequestBuilder; + use crate::servers::udp::handlers::handle_announce; use crate::servers::udp::handlers::tests::{ sample_cookie_valid_range, sample_issue_time, MockUdpStatsEventSender, TrackerConfigurationBuilder, }; - use crate::servers::udp::handlers::{gen_remote_fingerprint, handle_announce}; #[tokio::test] async fn the_peer_ip_should_be_changed_to_the_external_ip_in_the_tracker_configuration() { diff --git a/src/servers/udp/handlers/connect.rs b/src/servers/udp/handlers/connect.rs index 431c3bb4d..aae6a25f5 100644 --- a/src/servers/udp/handlers/connect.rs +++ b/src/servers/udp/handlers/connect.rs @@ -2,19 +2,13 @@ use std::net::SocketAddr; use std::sync::Arc; -use aquatic_udp_protocol::{ConnectRequest, ConnectResponse, Response}; +use aquatic_udp_protocol::{ConnectRequest, ConnectResponse, ConnectionId, Response}; use tracing::{instrument, Level}; use crate::packages::udp_tracker_core; -use crate::servers::udp::connection_cookie::make; -use crate::servers::udp::handlers::gen_remote_fingerprint; /// It handles the `Connect` request. Refer to [`Connect`](crate::servers::udp#connect) /// request for more information. -/// -/// # Errors -/// -/// This function does not ever return an error. #[instrument(fields(transaction_id), skip(opt_udp_stats_event_sender), ret(level = Level::TRACE))] pub async fn handle_connect( remote_addr: SocketAddr, @@ -23,31 +17,20 @@ pub async fn handle_connect( cookie_issue_time: f64, ) -> Response { tracing::Span::current().record("transaction_id", request.transaction_id.0.to_string()); - tracing::trace!("handle connect"); - let connection_id = make(gen_remote_fingerprint(&remote_addr), cookie_issue_time).expect("it should be a normal value"); + let connection_id = + udp_tracker_core::services::connect::handle_connect(remote_addr, opt_udp_stats_event_sender, cookie_issue_time).await; + + build_response(*request, connection_id) +} +fn build_response(request: ConnectRequest, connection_id: ConnectionId) -> Response { let response = ConnectResponse { transaction_id: request.transaction_id, connection_id, }; - if let Some(udp_stats_event_sender) = opt_udp_stats_event_sender.as_deref() { - match remote_addr { - SocketAddr::V4(_) => { - udp_stats_event_sender - .send_event(udp_tracker_core::statistics::event::Event::Udp4Connect) - .await; - } - SocketAddr::V6(_) => { - udp_stats_event_sender - .send_event(udp_tracker_core::statistics::event::Event::Udp6Connect) - .await; - } - } - } - Response::from(response) } @@ -62,8 +45,8 @@ mod tests { use aquatic_udp_protocol::{ConnectRequest, ConnectResponse, Response, TransactionId}; use mockall::predicate::eq; + use crate::packages::udp_tracker_core::connection_cookie::make; use crate::packages::{self, udp_tracker_core}; - use crate::servers::udp::connection_cookie::make; use crate::servers::udp::handlers::handle_connect; use crate::servers::udp::handlers::tests::{ sample_ipv4_remote_addr, sample_ipv4_remote_addr_fingerprint, sample_ipv4_socket_address, sample_ipv6_remote_addr, diff --git a/src/servers/udp/handlers/error.rs b/src/servers/udp/handlers/error.rs index 36095eeed..6cf273e78 100644 --- a/src/servers/udp/handlers/error.rs +++ b/src/servers/udp/handlers/error.rs @@ -9,9 +9,8 @@ use uuid::Uuid; use zerocopy::network_endian::I32; use crate::packages::udp_tracker_core; -use crate::servers::udp::connection_cookie::check; +use crate::packages::udp_tracker_core::connection_cookie::{check, gen_remote_fingerprint}; use crate::servers::udp::error::Error; -use crate::servers::udp::handlers::gen_remote_fingerprint; use crate::servers::udp::UDP_TRACKER_LOG_TARGET; #[allow(clippy::too_many_arguments)] diff --git a/src/servers/udp/handlers/mod.rs b/src/servers/udp/handlers/mod.rs index f9f8edae7..e58497d4b 100644 --- a/src/servers/udp/handlers/mod.rs +++ b/src/servers/udp/handlers/mod.rs @@ -4,7 +4,6 @@ pub mod connect; pub mod error; pub mod scrape; -use std::hash::{DefaultHasher, Hash, Hasher as _}; use std::net::SocketAddr; use std::ops::Range; use std::sync::Arc; @@ -21,6 +20,7 @@ use uuid::Uuid; use super::RawRequest; use crate::container::UdpTrackerContainer; +use crate::packages::udp_tracker_core::services::announce::UdpAnnounceError; use crate::servers::udp::error::Error; use crate::shared::bit_torrent::common::MAX_SCRAPE_TORRENTS; use crate::CurrentClock; @@ -77,16 +77,14 @@ pub(crate) async fn handle_packet( .await { Ok(response) => return response, - Err((e, transaction_id)) => { - match &e { - Error::CookieValueNotNormal { .. } - | Error::CookieValueExpired { .. } - | Error::CookieValueFromFuture { .. } => { - // code-review: should we include `RequestParseError` and `BadRequest`? - let mut ban_service = udp_tracker_container.ban_service.write().await; - ban_service.increase_counter(&udp_request.from.ip()); - } - _ => {} + Err((error, transaction_id)) => { + if let Error::UdpAnnounceError { + source: UdpAnnounceError::ConnectionCookieError { .. }, + } = error + { + // code-review: should we include `RequestParseError` and `BadRequest`? + let mut ban_service = udp_tracker_container.ban_service.write().await; + ban_service.increase_counter(&udp_request.from.ip()); } handle_error( @@ -95,7 +93,7 @@ pub(crate) async fn handle_packet( request_id, &udp_tracker_container.udp_stats_event_sender, cookie_time_values.valid_range.clone(), - &e, + &error, Some(transaction_id), ) .await @@ -168,13 +166,6 @@ pub async fn handle_request( } } -#[must_use] -pub(crate) fn gen_remote_fingerprint(remote_addr: &SocketAddr) -> u64 { - let mut state = DefaultHasher::new(); - remote_addr.hash(&mut state); - state.finish() -} - #[cfg(test)] pub(crate) mod tests { @@ -199,8 +190,8 @@ pub(crate) mod tests { use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; use torrust_tracker_test_helpers::configuration; - use super::gen_remote_fingerprint; use crate::packages::udp_tracker_core; + use crate::packages::udp_tracker_core::connection_cookie::gen_remote_fingerprint; use crate::{packages, CurrentClock}; pub(crate) struct CoreTrackerServices { diff --git a/src/servers/udp/handlers/scrape.rs b/src/servers/udp/handlers/scrape.rs index 2b03e0dc7..b36eb92a0 100644 --- a/src/servers/udp/handlers/scrape.rs +++ b/src/servers/udp/handlers/scrape.rs @@ -7,13 +7,12 @@ use aquatic_udp_protocol::{ NumberOfDownloads, NumberOfPeers, Response, ScrapeRequest, ScrapeResponse, TorrentScrapeStatistics, TransactionId, }; use bittorrent_tracker_core::scrape_handler::ScrapeHandler; +use torrust_tracker_primitives::core::ScrapeData; use tracing::{instrument, Level}; use zerocopy::network_endian::I32; use crate::packages::udp_tracker_core; -use crate::servers::udp::connection_cookie::check; use crate::servers::udp::error::Error; -use crate::servers::udp::handlers::gen_remote_fingerprint; /// It handles the `Scrape` request. Refer to [`Scrape`](crate::servers::udp#scrape) /// request for more information. @@ -35,25 +34,20 @@ pub async fn handle_scrape( tracing::trace!("handle scrape"); - // todo: move authentication to `udp_tracker_core::services::scrape::handle_scrape` - - check( - &request.connection_id, - gen_remote_fingerprint(&remote_addr), + let scrape_data = udp_tracker_core::services::scrape::handle_scrape( + remote_addr, + request, + scrape_handler, + opt_udp_stats_event_sender, cookie_valid_range, ) - .map_err(|e| (e, request.transaction_id))?; - - let scrape_data = - udp_tracker_core::services::scrape::handle_scrape(remote_addr, request, scrape_handler, opt_udp_stats_event_sender) - .await - .map_err(|e| Error::TrackerError { - source: (Arc::new(e) as Arc).into(), - }) - .map_err(|e| (e, request.transaction_id))?; + .await + .map_err(|e| (e.into(), request.transaction_id))?; - // todo: extract `build_response` function. + Ok(build_response(request, &scrape_data)) +} +fn build_response(request: &ScrapeRequest, scrape_data: &ScrapeData) -> Response { let mut torrent_stats: Vec = Vec::new(); for file in &scrape_data.files { @@ -76,7 +70,7 @@ pub async fn handle_scrape( torrent_stats, }; - Ok(Response::from(response)) + Response::from(response) } #[cfg(test)] @@ -94,12 +88,12 @@ mod tests { use bittorrent_tracker_core::torrent::repository::in_memory::InMemoryTorrentRepository; use crate::packages; - use crate::servers::udp::connection_cookie::make; + use crate::packages::udp_tracker_core::connection_cookie::{gen_remote_fingerprint, make}; + use crate::servers::udp::handlers::handle_scrape; use crate::servers::udp::handlers::tests::{ initialize_core_tracker_services_for_public_tracker, sample_cookie_valid_range, sample_ipv4_remote_addr, sample_issue_time, TorrentPeerBuilder, }; - use crate::servers::udp::handlers::{gen_remote_fingerprint, handle_scrape}; fn zeroed_torrent_statistics() -> TorrentScrapeStatistics { TorrentScrapeStatistics { diff --git a/src/servers/udp/mod.rs b/src/servers/udp/mod.rs index e8410e5f0..614db5bf6 100644 --- a/src/servers/udp/mod.rs +++ b/src/servers/udp/mod.rs @@ -105,7 +105,7 @@ //! connection ID = hash(client IP + current time slot + secret seed) //! ``` //! -//! The BEP-15 recommends a two-minute time slot. Refer to [`connection_cookie`] +//! The BEP-15 recommends a two-minute time slot. Refer to [`connection_cookie`](crate::packages::udp_tracker_core::connection_cookie) //! for more information about the connection ID generation with this method. //! //! #### Connect Request @@ -637,7 +637,6 @@ use std::net::SocketAddr; -pub mod connection_cookie; pub mod error; pub mod handlers; pub mod server; diff --git a/tests/servers/http/asserts.rs b/tests/servers/http/asserts.rs index 8d40d7e74..a68a1896e 100644 --- a/tests/servers/http/asserts.rs +++ b/tests/servers/http/asserts.rs @@ -141,5 +141,9 @@ pub async fn assert_cannot_parse_query_params_error_response(response: Response, pub async fn assert_authentication_error_response(response: Response) { assert_eq!(response.status(), 200); - assert_bencoded_error(&response.text().await.unwrap(), "Authentication error", Location::caller()); + assert_bencoded_error( + &response.text().await.unwrap(), + "Tracker authentication error", + Location::caller(), + ); }