diff --git a/src/apis/responses.rs b/src/apis/responses.rs index 3b0946396..c0a6cbcf8 100644 --- a/src/apis/responses.rs +++ b/src/apis/responses.rs @@ -141,7 +141,7 @@ pub fn failed_to_reload_keys_response(e: E) -> Response { unhandled_rejection_response(format!("failed to reload keys: {e}")) } -/// This error response is to keep backward compatibility with the old Warp API. +/// This error response is to keep backward compatibility with the old API. /// It should be a plain text or json. #[must_use] pub fn unhandled_rejection_response(reason: String) -> Response { diff --git a/src/http/mod.rs b/src/http/mod.rs index b4841c0af..b8aa6b19f 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -12,13 +12,11 @@ use serde::{Deserialize, Serialize}; -pub mod axum_implementation; pub mod percent_encoding; pub mod server; -pub mod warp_implementation; +pub mod v1; #[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)] pub enum Version { - Warp, - Axum, + V1, } diff --git a/src/http/axum_implementation/extractors/announce_request.rs b/src/http/v1/extractors/announce_request.rs similarity index 90% rename from src/http/axum_implementation/extractors/announce_request.rs rename to src/http/v1/extractors/announce_request.rs index 1680cd15c..c0b0451b3 100644 --- a/src/http/axum_implementation/extractors/announce_request.rs +++ b/src/http/v1/extractors/announce_request.rs @@ -5,9 +5,9 @@ use axum::extract::FromRequestParts; use axum::http::request::Parts; use axum::response::{IntoResponse, Response}; -use crate::http::axum_implementation::query::Query; -use crate::http::axum_implementation::requests::announce::{Announce, ParseAnnounceQueryError}; -use crate::http::axum_implementation::responses; +use crate::http::v1::query::Query; +use crate::http::v1::requests::announce::{Announce, ParseAnnounceQueryError}; +use crate::http::v1::responses; pub struct ExtractRequest(pub Announce); @@ -53,8 +53,8 @@ mod tests { use std::str::FromStr; use super::extract_announce_from; - use crate::http::axum_implementation::requests::announce::{Announce, Compact, Event}; - use crate::http::axum_implementation::responses::error::Error; + use crate::http::v1::requests::announce::{Announce, Compact, Event}; + use crate::http::v1::responses::error::Error; use crate::protocol::info_hash::InfoHash; use crate::tracker::peer; diff --git a/src/http/axum_implementation/extractors/authentication_key.rs b/src/http/v1/extractors/authentication_key.rs similarity index 94% rename from src/http/axum_implementation/extractors/authentication_key.rs rename to src/http/v1/extractors/authentication_key.rs index 8ffc4ff12..3b2680a5f 100644 --- a/src/http/axum_implementation/extractors/authentication_key.rs +++ b/src/http/v1/extractors/authentication_key.rs @@ -8,8 +8,8 @@ use axum::http::request::Parts; use axum::response::{IntoResponse, Response}; use serde::Deserialize; -use crate::http::axum_implementation::handlers::common::auth; -use crate::http::axum_implementation::responses; +use crate::http::v1::handlers::common::auth; +use crate::http::v1::responses; use crate::tracker::auth::Key; pub struct Extract(pub Key); @@ -85,7 +85,7 @@ fn custom_error(rejection: &PathRejection) -> responses::error::Error { mod tests { use super::parse_key; - use crate::http::axum_implementation::responses::error::Error; + use crate::http::v1::responses::error::Error; fn assert_error_response(error: &Error, error_message: &str) { assert!( diff --git a/src/http/axum_implementation/extractors/client_ip_sources.rs b/src/http/v1/extractors/client_ip_sources.rs similarity index 93% rename from src/http/axum_implementation/extractors/client_ip_sources.rs rename to src/http/v1/extractors/client_ip_sources.rs index b41478c22..c8b3659f3 100644 --- a/src/http/axum_implementation/extractors/client_ip_sources.rs +++ b/src/http/v1/extractors/client_ip_sources.rs @@ -8,7 +8,7 @@ use axum::http::request::Parts; use axum::response::Response; use axum_client_ip::RightmostXForwardedFor; -use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources; +use crate::http::v1::services::peer_ip_resolver::ClientIpSources; pub struct Extract(pub ClientIpSources); diff --git a/src/http/axum_implementation/extractors/mod.rs b/src/http/v1/extractors/mod.rs similarity index 100% rename from src/http/axum_implementation/extractors/mod.rs rename to src/http/v1/extractors/mod.rs diff --git a/src/http/axum_implementation/extractors/scrape_request.rs b/src/http/v1/extractors/scrape_request.rs similarity index 92% rename from src/http/axum_implementation/extractors/scrape_request.rs rename to src/http/v1/extractors/scrape_request.rs index 998728f59..d63470897 100644 --- a/src/http/axum_implementation/extractors/scrape_request.rs +++ b/src/http/v1/extractors/scrape_request.rs @@ -5,9 +5,9 @@ use axum::extract::FromRequestParts; use axum::http::request::Parts; use axum::response::{IntoResponse, Response}; -use crate::http::axum_implementation::query::Query; -use crate::http::axum_implementation::requests::scrape::{ParseScrapeQueryError, Scrape}; -use crate::http::axum_implementation::responses; +use crate::http::v1::query::Query; +use crate::http::v1::requests::scrape::{ParseScrapeQueryError, Scrape}; +use crate::http::v1::responses; pub struct ExtractRequest(pub Scrape); @@ -53,8 +53,8 @@ mod tests { use std::str::FromStr; use super::extract_scrape_from; - use crate::http::axum_implementation::requests::scrape::Scrape; - use crate::http::axum_implementation::responses::error::Error; + use crate::http::v1::requests::scrape::Scrape; + use crate::http::v1::responses::error::Error; use crate::protocol::info_hash::InfoHash; struct TestInfoHash { diff --git a/src/http/axum_implementation/handlers/announce.rs b/src/http/v1/handlers/announce.rs similarity index 85% rename from src/http/axum_implementation/handlers/announce.rs rename to src/http/v1/handlers/announce.rs index ebb8c8586..1f10c3fa4 100644 --- a/src/http/axum_implementation/handlers/announce.rs +++ b/src/http/v1/handlers/announce.rs @@ -7,14 +7,14 @@ use axum::extract::State; use axum::response::{IntoResponse, Response}; use log::debug; -use crate::http::axum_implementation::extractors::announce_request::ExtractRequest; -use crate::http::axum_implementation::extractors::authentication_key::Extract as ExtractKey; -use crate::http::axum_implementation::extractors::client_ip_sources::Extract as ExtractClientIpSources; -use crate::http::axum_implementation::handlers::common::auth; -use crate::http::axum_implementation::requests::announce::{Announce, Compact, Event}; -use crate::http::axum_implementation::responses::{self, announce}; -use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources; -use crate::http::axum_implementation::services::{self, peer_ip_resolver}; +use crate::http::v1::extractors::announce_request::ExtractRequest; +use crate::http::v1::extractors::authentication_key::Extract as ExtractKey; +use crate::http::v1::extractors::client_ip_sources::Extract as ExtractClientIpSources; +use crate::http::v1::handlers::common::auth; +use crate::http::v1::requests::announce::{Announce, Compact, Event}; +use crate::http::v1::responses::{self, announce}; +use crate::http::v1::services::peer_ip_resolver::ClientIpSources; +use crate::http::v1::services::{self, peer_ip_resolver}; use crate::protocol::clock::{Current, Time}; use crate::tracker::auth::Key; use crate::tracker::peer::Peer; @@ -141,9 +141,9 @@ mod tests { use torrust_tracker_test_helpers::configuration; - use crate::http::axum_implementation::requests::announce::Announce; - use crate::http::axum_implementation::responses; - use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources; + use crate::http::v1::requests::announce::Announce; + use crate::http::v1::responses; + use crate::http::v1::services::peer_ip_resolver::ClientIpSources; use crate::protocol::info_hash::InfoHash; use crate::tracker::services::common::tracker_factory; use crate::tracker::{peer, Tracker}; @@ -197,8 +197,8 @@ mod tests { use std::sync::Arc; use super::{private_tracker, sample_announce_request, sample_client_ip_sources}; - use crate::http::axum_implementation::handlers::announce::handle_announce; - use crate::http::axum_implementation::handlers::announce::tests::assert_error_response; + use crate::http::v1::handlers::announce::handle_announce; + use crate::http::v1::handlers::announce::tests::assert_error_response; use crate::tracker::auth; #[tokio::test] @@ -238,8 +238,8 @@ mod tests { use std::sync::Arc; use super::{sample_announce_request, sample_client_ip_sources, whitelisted_tracker}; - use crate::http::axum_implementation::handlers::announce::handle_announce; - use crate::http::axum_implementation::handlers::announce::tests::assert_error_response; + use crate::http::v1::handlers::announce::handle_announce; + use crate::http::v1::handlers::announce::tests::assert_error_response; #[tokio::test] async fn it_should_fail_when_the_announced_torrent_is_not_whitelisted() { @@ -266,9 +266,9 @@ mod tests { use std::sync::Arc; use super::{sample_announce_request, tracker_on_reverse_proxy}; - use crate::http::axum_implementation::handlers::announce::handle_announce; - use crate::http::axum_implementation::handlers::announce::tests::assert_error_response; - use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources; + use crate::http::v1::handlers::announce::handle_announce; + use crate::http::v1::handlers::announce::tests::assert_error_response; + use crate::http::v1::services::peer_ip_resolver::ClientIpSources; #[tokio::test] async fn it_should_fail_when_the_right_most_x_forwarded_for_header_ip_is_not_available() { @@ -295,9 +295,9 @@ mod tests { use std::sync::Arc; use super::{sample_announce_request, tracker_not_on_reverse_proxy}; - use crate::http::axum_implementation::handlers::announce::handle_announce; - use crate::http::axum_implementation::handlers::announce::tests::assert_error_response; - use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources; + use crate::http::v1::handlers::announce::handle_announce; + use crate::http::v1::handlers::announce::tests::assert_error_response; + use crate::http::v1::services::peer_ip_resolver::ClientIpSources; #[tokio::test] async fn it_should_fail_when_the_client_ip_from_the_connection_info_is_not_available() { diff --git a/src/http/axum_implementation/handlers/common/auth.rs b/src/http/v1/handlers/common/auth.rs similarity index 95% rename from src/http/axum_implementation/handlers/common/auth.rs rename to src/http/v1/handlers/common/auth.rs index 30971725a..938fc3f01 100644 --- a/src/http/axum_implementation/handlers/common/auth.rs +++ b/src/http/v1/handlers/common/auth.rs @@ -2,7 +2,7 @@ use std::panic::Location; use thiserror::Error; -use crate::http::axum_implementation::responses; +use crate::http::v1::responses; use crate::tracker::auth; #[derive(Debug, Error)] diff --git a/src/http/axum_implementation/handlers/common/mod.rs b/src/http/v1/handlers/common/mod.rs similarity index 100% rename from src/http/axum_implementation/handlers/common/mod.rs rename to src/http/v1/handlers/common/mod.rs diff --git a/src/http/axum_implementation/handlers/common/peer_ip.rs b/src/http/v1/handlers/common/peer_ip.rs similarity index 75% rename from src/http/axum_implementation/handlers/common/peer_ip.rs rename to src/http/v1/handlers/common/peer_ip.rs index df10e5eb1..e182c716b 100644 --- a/src/http/axum_implementation/handlers/common/peer_ip.rs +++ b/src/http/v1/handlers/common/peer_ip.rs @@ -1,5 +1,5 @@ -use crate::http::axum_implementation::responses; -use crate::http::axum_implementation::services::peer_ip_resolver::PeerIpResolutionError; +use crate::http::v1::responses; +use crate::http::v1::services::peer_ip_resolver::PeerIpResolutionError; impl From for responses::error::Error { fn from(err: PeerIpResolutionError) -> Self { @@ -13,8 +13,8 @@ impl From for responses::error::Error { mod tests { use std::panic::Location; - use crate::http::axum_implementation::responses; - use crate::http::axum_implementation::services::peer_ip_resolver::PeerIpResolutionError; + use crate::http::v1::responses; + use crate::http::v1::services::peer_ip_resolver::PeerIpResolutionError; fn assert_error_response(error: &responses::error::Error, error_message: &str) { assert!( diff --git a/src/http/axum_implementation/handlers/mod.rs b/src/http/v1/handlers/mod.rs similarity index 100% rename from src/http/axum_implementation/handlers/mod.rs rename to src/http/v1/handlers/mod.rs diff --git a/src/http/axum_implementation/handlers/scrape.rs b/src/http/v1/handlers/scrape.rs similarity index 85% rename from src/http/axum_implementation/handlers/scrape.rs rename to src/http/v1/handlers/scrape.rs index fd316882d..50f92cd36 100644 --- a/src/http/axum_implementation/handlers/scrape.rs +++ b/src/http/v1/handlers/scrape.rs @@ -4,12 +4,12 @@ use axum::extract::State; use axum::response::{IntoResponse, Response}; use log::debug; -use crate::http::axum_implementation::extractors::authentication_key::Extract as ExtractKey; -use crate::http::axum_implementation::extractors::client_ip_sources::Extract as ExtractClientIpSources; -use crate::http::axum_implementation::extractors::scrape_request::ExtractRequest; -use crate::http::axum_implementation::requests::scrape::Scrape; -use crate::http::axum_implementation::services::peer_ip_resolver::{self, ClientIpSources}; -use crate::http::axum_implementation::{responses, services}; +use crate::http::v1::extractors::authentication_key::Extract as ExtractKey; +use crate::http::v1::extractors::client_ip_sources::Extract as ExtractClientIpSources; +use crate::http::v1::extractors::scrape_request::ExtractRequest; +use crate::http::v1::requests::scrape::Scrape; +use crate::http::v1::services::peer_ip_resolver::{self, ClientIpSources}; +use crate::http::v1::{responses, services}; use crate::tracker::auth::Key; use crate::tracker::{ScrapeData, Tracker}; @@ -99,9 +99,9 @@ mod tests { use torrust_tracker_test_helpers::configuration; - use crate::http::axum_implementation::requests::scrape::Scrape; - use crate::http::axum_implementation::responses; - use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources; + use crate::http::v1::requests::scrape::Scrape; + use crate::http::v1::responses; + use crate::http::v1::services::peer_ip_resolver::ClientIpSources; use crate::protocol::info_hash::InfoHash; use crate::tracker::services::common::tracker_factory; use crate::tracker::Tracker; @@ -147,7 +147,7 @@ mod tests { use std::sync::Arc; use super::{private_tracker, sample_client_ip_sources, sample_scrape_request}; - use crate::http::axum_implementation::handlers::scrape::handle_scrape; + use crate::http::v1::handlers::scrape::handle_scrape; use crate::tracker::{auth, ScrapeData}; #[tokio::test] @@ -189,7 +189,7 @@ mod tests { use std::sync::Arc; use super::{sample_client_ip_sources, sample_scrape_request, whitelisted_tracker}; - use crate::http::axum_implementation::handlers::scrape::handle_scrape; + use crate::http::v1::handlers::scrape::handle_scrape; use crate::tracker::ScrapeData; #[tokio::test] @@ -212,9 +212,9 @@ mod tests { use std::sync::Arc; use super::{sample_scrape_request, tracker_on_reverse_proxy}; - use crate::http::axum_implementation::handlers::scrape::handle_scrape; - use crate::http::axum_implementation::handlers::scrape::tests::assert_error_response; - use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources; + use crate::http::v1::handlers::scrape::handle_scrape; + use crate::http::v1::handlers::scrape::tests::assert_error_response; + use crate::http::v1::services::peer_ip_resolver::ClientIpSources; #[tokio::test] async fn it_should_fail_when_the_right_most_x_forwarded_for_header_ip_is_not_available() { @@ -240,9 +240,9 @@ mod tests { use std::sync::Arc; use super::{sample_scrape_request, tracker_not_on_reverse_proxy}; - use crate::http::axum_implementation::handlers::scrape::handle_scrape; - use crate::http::axum_implementation::handlers::scrape::tests::assert_error_response; - use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources; + use crate::http::v1::handlers::scrape::handle_scrape; + use crate::http::v1::handlers::scrape::tests::assert_error_response; + use crate::http::v1::services::peer_ip_resolver::ClientIpSources; #[tokio::test] async fn it_should_fail_when_the_client_ip_from_the_connection_info_is_not_available() { diff --git a/src/http/axum_implementation/launcher.rs b/src/http/v1/launcher.rs similarity index 100% rename from src/http/axum_implementation/launcher.rs rename to src/http/v1/launcher.rs diff --git a/src/http/axum_implementation/mod.rs b/src/http/v1/mod.rs similarity index 100% rename from src/http/axum_implementation/mod.rs rename to src/http/v1/mod.rs diff --git a/src/http/axum_implementation/query.rs b/src/http/v1/query.rs similarity index 97% rename from src/http/axum_implementation/query.rs rename to src/http/v1/query.rs index 8b01e9db7..45484ea38 100644 --- a/src/http/axum_implementation/query.rs +++ b/src/http/v1/query.rs @@ -174,7 +174,7 @@ impl std::fmt::Display for FieldValuePairSet { mod tests { mod url_query { - use crate::http::axum_implementation::query::Query; + use crate::http::v1::query::Query; #[test] fn should_parse_the_query_params_from_an_url_query_string() { @@ -227,7 +227,7 @@ mod tests { } mod should_allow_more_than_one_value_for_the_same_param { - use crate::http::axum_implementation::query::Query; + use crate::http::v1::query::Query; #[test] fn instantiated_from_a_vector() { @@ -249,7 +249,7 @@ mod tests { } mod should_be_displayed { - use crate::http::axum_implementation::query::Query; + use crate::http::v1::query::Query; #[test] fn with_one_param() { @@ -270,7 +270,7 @@ mod tests { } mod param_name_value_pair { - use crate::http::axum_implementation::query::NameValuePair; + use crate::http::v1::query::NameValuePair; #[test] fn should_parse_a_single_query_param() { diff --git a/src/http/axum_implementation/requests/announce.rs b/src/http/v1/requests/announce.rs similarity index 97% rename from src/http/axum_implementation/requests/announce.rs rename to src/http/v1/requests/announce.rs index 6e357ea6d..eeab97d5f 100644 --- a/src/http/axum_implementation/requests/announce.rs +++ b/src/http/v1/requests/announce.rs @@ -5,9 +5,9 @@ use std::str::FromStr; use thiserror::Error; use torrust_tracker_located_error::{Located, LocatedError}; -use crate::http::axum_implementation::query::{ParseQueryError, Query}; -use crate::http::axum_implementation::responses; use crate::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id}; +use crate::http::v1::query::{ParseQueryError, Query}; +use crate::http::v1::responses; use crate::protocol::info_hash::{ConversionError, InfoHash}; use crate::tracker::peer::{self, IdConversionError}; @@ -280,8 +280,8 @@ mod tests { mod announce_request { - use crate::http::axum_implementation::query::Query; - use crate::http::axum_implementation::requests::announce::{ + use crate::http::v1::query::Query; + use crate::http::v1::requests::announce::{ Announce, Compact, Event, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, PEER_ID, PORT, UPLOADED, }; use crate::protocol::info_hash::InfoHash; @@ -350,8 +350,8 @@ mod tests { mod when_it_is_instantiated_from_the_url_query_params { - use crate::http::axum_implementation::query::Query; - use crate::http::axum_implementation::requests::announce::{ + use crate::http::v1::query::Query; + use crate::http::v1::requests::announce::{ Announce, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, PEER_ID, PORT, UPLOADED, }; diff --git a/src/http/axum_implementation/requests/mod.rs b/src/http/v1/requests/mod.rs similarity index 100% rename from src/http/axum_implementation/requests/mod.rs rename to src/http/v1/requests/mod.rs diff --git a/src/http/axum_implementation/requests/scrape.rs b/src/http/v1/requests/scrape.rs similarity index 90% rename from src/http/axum_implementation/requests/scrape.rs rename to src/http/v1/requests/scrape.rs index 505be566e..6257f0733 100644 --- a/src/http/axum_implementation/requests/scrape.rs +++ b/src/http/v1/requests/scrape.rs @@ -3,9 +3,9 @@ use std::panic::Location; use thiserror::Error; use torrust_tracker_located_error::{Located, LocatedError}; -use crate::http::axum_implementation::query::Query; -use crate::http::axum_implementation::responses; use crate::http::percent_encoding::percent_decode_info_hash; +use crate::http::v1::query::Query; +use crate::http::v1::responses; use crate::protocol::info_hash::{ConversionError, InfoHash}; pub type NumberOfBytes = i64; @@ -85,8 +85,8 @@ mod tests { mod scrape_request { - use crate::http::axum_implementation::query::Query; - use crate::http::axum_implementation::requests::scrape::{Scrape, INFO_HASH}; + use crate::http::v1::query::Query; + use crate::http::v1::requests::scrape::{Scrape, INFO_HASH}; use crate::protocol::info_hash::InfoHash; #[test] @@ -107,8 +107,8 @@ mod tests { mod when_it_is_instantiated_from_the_url_query_params { - use crate::http::axum_implementation::query::Query; - use crate::http::axum_implementation::requests::scrape::{Scrape, INFO_HASH}; + use crate::http::v1::query::Query; + use crate::http::v1::requests::scrape::{Scrape, INFO_HASH}; #[test] fn it_should_fail_if_the_query_does_not_include_the_info_hash_param() { diff --git a/src/http/axum_implementation/responses/announce.rs b/src/http/v1/responses/announce.rs similarity index 98% rename from src/http/axum_implementation/responses/announce.rs rename to src/http/v1/responses/announce.rs index 81651767b..8b178ff7e 100644 --- a/src/http/axum_implementation/responses/announce.rs +++ b/src/http/v1/responses/announce.rs @@ -8,7 +8,7 @@ use bip_bencode::{ben_bytes, ben_int, ben_list, ben_map, BMutAccess, BencodeMut} use serde::{self, Deserialize, Serialize}; use thiserror::Error; -use crate::http::axum_implementation::responses; +use crate::http::v1::responses; use crate::tracker::{self, AnnounceData}; /// Normal (non compact) "announce" response @@ -250,7 +250,7 @@ mod tests { use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use super::{NonCompact, Peer}; - use crate::http::axum_implementation::responses::announce::{Compact, CompactPeer}; + use crate::http::v1::responses::announce::{Compact, CompactPeer}; // Some ascii values used in tests: // diff --git a/src/http/axum_implementation/responses/error.rs b/src/http/v1/responses/error.rs similarity index 100% rename from src/http/axum_implementation/responses/error.rs rename to src/http/v1/responses/error.rs diff --git a/src/http/axum_implementation/responses/mod.rs b/src/http/v1/responses/mod.rs similarity index 100% rename from src/http/axum_implementation/responses/mod.rs rename to src/http/v1/responses/mod.rs diff --git a/src/http/axum_implementation/responses/scrape.rs b/src/http/v1/responses/scrape.rs similarity index 97% rename from src/http/axum_implementation/responses/scrape.rs rename to src/http/v1/responses/scrape.rs index 3fc34a0e5..5cbe6502e 100644 --- a/src/http/axum_implementation/responses/scrape.rs +++ b/src/http/v1/responses/scrape.rs @@ -55,7 +55,7 @@ impl IntoResponse for Bencoded { mod tests { mod scrape_response { - use crate::http::axum_implementation::responses::scrape::Bencoded; + use crate::http::v1::responses::scrape::Bencoded; use crate::protocol::info_hash::InfoHash; use crate::tracker::torrent::SwarmMetadata; use crate::tracker::ScrapeData; diff --git a/src/http/axum_implementation/routes.rs b/src/http/v1/routes.rs similarity index 100% rename from src/http/axum_implementation/routes.rs rename to src/http/v1/routes.rs diff --git a/src/http/axum_implementation/services/announce.rs b/src/http/v1/services/announce.rs similarity index 97% rename from src/http/axum_implementation/services/announce.rs rename to src/http/v1/services/announce.rs index 73d6ed468..a8b9f0d06 100644 --- a/src/http/axum_implementation/services/announce.rs +++ b/src/http/v1/services/announce.rs @@ -77,8 +77,8 @@ mod tests { use torrust_tracker_test_helpers::configuration; use super::{sample_peer_using_ipv4, sample_peer_using_ipv6}; - use crate::http::axum_implementation::services::announce::invoke; - use crate::http::axum_implementation::services::announce::tests::{public_tracker, sample_info_hash, sample_peer}; + use crate::http::v1::services::announce::invoke; + use crate::http::v1::services::announce::tests::{public_tracker, sample_info_hash, sample_peer}; use crate::tracker::peer::Peer; use crate::tracker::torrent::SwarmStats; use crate::tracker::{statistics, AnnounceData, Tracker}; diff --git a/src/http/axum_implementation/services/mod.rs b/src/http/v1/services/mod.rs similarity index 100% rename from src/http/axum_implementation/services/mod.rs rename to src/http/v1/services/mod.rs diff --git a/src/http/axum_implementation/services/peer_ip_resolver.rs b/src/http/v1/services/peer_ip_resolver.rs similarity index 95% rename from src/http/axum_implementation/services/peer_ip_resolver.rs rename to src/http/v1/services/peer_ip_resolver.rs index fae1e4ec0..c7bc183b4 100644 --- a/src/http/axum_implementation/services/peer_ip_resolver.rs +++ b/src/http/v1/services/peer_ip_resolver.rs @@ -73,7 +73,7 @@ mod tests { use std::str::FromStr; use super::invoke; - use crate::http::axum_implementation::services::peer_ip_resolver::{ClientIpSources, PeerIpResolutionError}; + use crate::http::v1::services::peer_ip_resolver::{ClientIpSources, PeerIpResolutionError}; #[test] fn it_should_get_the_peer_ip_from_the_connection_info() { @@ -112,7 +112,7 @@ mod tests { use std::net::IpAddr; use std::str::FromStr; - use crate::http::axum_implementation::services::peer_ip_resolver::{invoke, ClientIpSources, PeerIpResolutionError}; + use crate::http::v1::services::peer_ip_resolver::{invoke, ClientIpSources, PeerIpResolutionError}; #[test] fn it_should_get_the_peer_ip_from_the_right_most_ip_in_the_x_forwarded_for_header() { diff --git a/src/http/axum_implementation/services/scrape.rs b/src/http/v1/services/scrape.rs similarity index 94% rename from src/http/axum_implementation/services/scrape.rs rename to src/http/v1/services/scrape.rs index b48bab642..b6f319375 100644 --- a/src/http/axum_implementation/services/scrape.rs +++ b/src/http/v1/services/scrape.rs @@ -77,10 +77,8 @@ mod tests { use mockall::predicate::eq; use torrust_tracker_test_helpers::configuration; - use crate::http::axum_implementation::services::scrape::invoke; - use crate::http::axum_implementation::services::scrape::tests::{ - public_tracker, sample_info_hash, sample_info_hashes, sample_peer, - }; + use crate::http::v1::services::scrape::invoke; + use crate::http::v1::services::scrape::tests::{public_tracker, sample_info_hash, sample_info_hashes, sample_peer}; use crate::tracker::torrent::SwarmMetadata; use crate::tracker::{statistics, ScrapeData, Tracker}; @@ -169,10 +167,8 @@ mod tests { use mockall::predicate::eq; use torrust_tracker_test_helpers::configuration; - use crate::http::axum_implementation::services::scrape::fake; - use crate::http::axum_implementation::services::scrape::tests::{ - public_tracker, sample_info_hash, sample_info_hashes, sample_peer, - }; + use crate::http::v1::services::scrape::fake; + use crate::http::v1::services::scrape::tests::{public_tracker, sample_info_hash, sample_info_hashes, sample_peer}; use crate::tracker::{statistics, ScrapeData, Tracker}; #[tokio::test] diff --git a/src/http/warp_implementation/error.rs b/src/http/warp_implementation/error.rs deleted file mode 100644 index 55b22c27a..000000000 --- a/src/http/warp_implementation/error.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::panic::Location; - -use thiserror::Error; -use torrust_tracker_located_error::LocatedError; -use warp::reject::Reject; - -#[derive(Error, Debug)] -pub enum Error { - #[error("tracker server error: {source}")] - TrackerError { - source: LocatedError<'static, dyn std::error::Error + Send + Sync>, - }, - - #[error("internal server error: {message}, {location}")] - InternalServer { - location: &'static Location<'static>, - message: String, - }, - - #[error("no valid infohashes found, {location}")] - EmptyInfoHash { location: &'static Location<'static> }, - - #[error("peer_id is either missing or invalid, {location}")] - InvalidPeerId { location: &'static Location<'static> }, - - #[error("could not find remote address: {message}, {location}")] - AddressNotFound { - location: &'static Location<'static>, - message: String, - }, - - #[error("too many infohashes: {message}, {location}")] - TwoManyInfoHashes { - location: &'static Location<'static>, - message: String, - }, -} - -impl Reject for Error {} diff --git a/src/http/warp_implementation/filter_helpers.rs b/src/http/warp_implementation/filter_helpers.rs deleted file mode 100644 index 583d38352..000000000 --- a/src/http/warp_implementation/filter_helpers.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::net::{AddrParseError, IpAddr}; -use std::panic::Location; -use std::str::FromStr; - -use thiserror::Error; -use torrust_tracker_located_error::{Located, LocatedError}; - -#[derive(Error, Debug)] -pub enum XForwardedForParseError { - #[error("Empty X-Forwarded-For header value, {location}")] - EmptyValue { location: &'static Location<'static> }, - - #[error("Invalid IP in X-Forwarded-For header: {source}")] - InvalidIp { source: LocatedError<'static, AddrParseError> }, -} - -impl From for XForwardedForParseError { - #[track_caller] - fn from(err: AddrParseError) -> Self { - Self::InvalidIp { - source: Located(err).into(), - } - } -} - -/// It extracts the last IP address from the `X-Forwarded-For` http header value. -/// -/// # Errors -/// -/// Will return and error if the last IP in the `X-Forwarded-For` header is not a valid IP -pub fn maybe_rightmost_forwarded_ip(x_forwarded_for_value: &str) -> Result { - let mut x_forwarded_for_raw = x_forwarded_for_value.to_string(); - - // Remove whitespace chars - x_forwarded_for_raw.retain(|c| !c.is_whitespace()); - - // Get all forwarded IP's in a vec - let x_forwarded_ips: Vec<&str> = x_forwarded_for_raw.split(',').collect(); - - match x_forwarded_ips.last() { - Some(last_ip) => match IpAddr::from_str(last_ip) { - Ok(ip) => Ok(ip), - Err(err) => Err(err.into()), - }, - None => Err(XForwardedForParseError::EmptyValue { - location: Location::caller(), - }), - } -} - -#[cfg(test)] -mod tests { - - use std::net::IpAddr; - use std::str::FromStr; - - use super::maybe_rightmost_forwarded_ip; - - #[test] - fn the_last_forwarded_ip_can_be_parsed_from_the_the_corresponding_http_header() { - assert!(maybe_rightmost_forwarded_ip("").is_err()); - - assert!(maybe_rightmost_forwarded_ip("INVALID IP").is_err()); - - assert_eq!( - maybe_rightmost_forwarded_ip("2001:db8:85a3:8d3:1319:8a2e:370:7348").unwrap(), - IpAddr::from_str("2001:db8:85a3:8d3:1319:8a2e:370:7348").unwrap() - ); - - assert_eq!( - maybe_rightmost_forwarded_ip("203.0.113.195").unwrap(), - IpAddr::from_str("203.0.113.195").unwrap() - ); - - assert_eq!( - maybe_rightmost_forwarded_ip("203.0.113.195, 2001:db8:85a3:8d3:1319:8a2e:370:7348").unwrap(), - IpAddr::from_str("2001:db8:85a3:8d3:1319:8a2e:370:7348").unwrap() - ); - - assert_eq!( - maybe_rightmost_forwarded_ip("203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178").unwrap(), - IpAddr::from_str("150.172.238.178").unwrap() - ); - } -} diff --git a/src/http/warp_implementation/filters.rs b/src/http/warp_implementation/filters.rs deleted file mode 100644 index 06168d82a..000000000 --- a/src/http/warp_implementation/filters.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::convert::Infallible; -use std::net::{IpAddr, SocketAddr}; -use std::panic::Location; -use std::str::FromStr; -use std::sync::Arc; - -use warp::{reject, Filter, Rejection}; - -use super::error::Error; -use super::filter_helpers::maybe_rightmost_forwarded_ip; -use super::{request, WebResult}; -use crate::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id}; -use crate::protocol::common::MAX_SCRAPE_TORRENTS; -use crate::protocol::info_hash::InfoHash; -use crate::tracker::auth::Key; -use crate::tracker::{self, peer}; - -/// Pass Arc along -#[must_use] -pub fn with_tracker( - tracker: Arc, -) -> impl Filter,), Error = Infallible> + Clone { - warp::any().map(move || tracker.clone()) -} - -/// Check for infoHash -#[must_use] -pub fn with_info_hash() -> impl Filter,), Error = Rejection> + Clone { - warp::filters::query::raw().and_then(|q| async move { info_hashes(&q) }) -} - -/// Check for `PeerId` -#[must_use] -pub fn with_peer_id() -> impl Filter + Clone { - warp::filters::query::raw().and_then(|q| async move { peer_id(&q) }) -} - -/// Pass Arc along -#[must_use] -pub fn with_auth_key() -> impl Filter,), Error = Infallible> + Clone { - warp::path::param::() - .map(|key: String| { - let key = Key::from_str(&key); - match key { - Ok(id) => Some(id), - Err(_) => None, - } - }) - .or_else(|_| async { Ok::<(Option,), Infallible>((None,)) }) -} - -/// Check for `PeerAddress` -#[must_use] -pub fn with_peer_addr(on_reverse_proxy: bool) -> impl Filter + Clone { - warp::addr::remote() - .and(warp::header::optional::("X-Forwarded-For")) - .map(move |remote_addr: Option, x_forwarded_for: Option| { - (on_reverse_proxy, remote_addr, x_forwarded_for) - }) - .and_then(|q| async move { peer_addr(q) }) -} - -/// Check for `request::Announce` -#[must_use] -pub fn with_announce_request(on_reverse_proxy: bool) -> impl Filter + Clone { - warp::filters::query::query::() - .and(with_info_hash()) - .and(with_peer_id()) - .and(with_peer_addr(on_reverse_proxy)) - .and_then(|q, r, s, t| async move { announce_request(q, &r, s, t) }) -} - -/// Check for `ScrapeRequest` -#[must_use] -pub fn with_scrape_request(on_reverse_proxy: bool) -> impl Filter + Clone { - warp::any() - .and(with_info_hash()) - .and(with_peer_addr(on_reverse_proxy)) - .and_then(|q, r| async move { scrape_request(q, r) }) -} - -/// Parse `InfoHash` from raw query string -#[allow(clippy::ptr_arg)] -fn info_hashes(raw_query: &String) -> WebResult> { - let split_raw_query: Vec<&str> = raw_query.split('&').collect(); - let mut info_hashes: Vec = Vec::new(); - - for v in split_raw_query { - if v.contains("info_hash") { - // get raw percent encoded infohash - let raw_info_hash = v.split('=').collect::>()[1]; - - let info_hash = percent_decode_info_hash(raw_info_hash); - - if let Ok(ih) = info_hash { - info_hashes.push(ih); - } - } - } - - if info_hashes.len() > MAX_SCRAPE_TORRENTS as usize { - Err(reject::custom(Error::TwoManyInfoHashes { - location: Location::caller(), - message: format! {"found: {}, but limit is: {}",info_hashes.len(), MAX_SCRAPE_TORRENTS}, - })) - } else if info_hashes.is_empty() { - Err(reject::custom(Error::EmptyInfoHash { - location: Location::caller(), - })) - } else { - Ok(info_hashes) - } -} - -/// Parse `PeerId` from raw query string -#[allow(clippy::ptr_arg)] -fn peer_id(raw_query: &String) -> WebResult { - // put all query params in a vec - let split_raw_query: Vec<&str> = raw_query.split('&').collect(); - - let mut peer_id: Option = None; - - for v in split_raw_query { - // look for the peer_id param - if v.contains("peer_id") { - // get raw percent encoded peer id - let raw_peer_id = v.split('=').collect::>()[1]; - - if let Ok(id) = percent_decode_peer_id(raw_peer_id) { - peer_id = Some(id); - } else { - return Err(reject::custom(Error::InvalidPeerId { - location: Location::caller(), - })); - } - - break; - } - } - - match peer_id { - Some(id) => Ok(id), - None => Err(reject::custom(Error::InvalidPeerId { - location: Location::caller(), - })), - } -} - -/// Get peer IP from HTTP client IP or X-Forwarded-For HTTP header -fn peer_addr( - (on_reverse_proxy, remote_client_ip, maybe_x_forwarded_for): (bool, Option, Option), -) -> WebResult { - if on_reverse_proxy { - if maybe_x_forwarded_for.is_none() { - return Err(reject::custom(Error::AddressNotFound { - location: Location::caller(), - message: "must have a x-forwarded-for when using a reverse proxy".to_string(), - })); - } - - let x_forwarded_for = maybe_x_forwarded_for.unwrap(); - - maybe_rightmost_forwarded_ip(&x_forwarded_for).map_err(|e| { - reject::custom(Error::AddressNotFound { - location: Location::caller(), - message: format!("on remote proxy and unable to parse the last x-forwarded-ip: `{e}`, from `{x_forwarded_for}`"), - }) - }) - } else if remote_client_ip.is_none() { - return Err(reject::custom(Error::AddressNotFound { - location: Location::caller(), - message: "neither on have remote address or on a reverse proxy".to_string(), - })); - } else { - return Ok(remote_client_ip.unwrap().ip()); - } -} - -/// Parse `AnnounceRequest` from raw `AnnounceRequestQuery`, `InfoHash` and Option -#[allow(clippy::unnecessary_wraps)] -#[allow(clippy::ptr_arg)] -fn announce_request( - announce_request_query: request::AnnounceQuery, - info_hashes: &Vec, - peer_id: peer::Id, - peer_addr: IpAddr, -) -> WebResult { - Ok(request::Announce { - info_hash: info_hashes[0], - peer_addr, - downloaded: announce_request_query.downloaded.unwrap_or(0), - uploaded: announce_request_query.uploaded.unwrap_or(0), - peer_id, - port: announce_request_query.port, - left: announce_request_query.left.unwrap_or(0), - event: announce_request_query.event, - compact: announce_request_query.compact, - }) -} - -/// Parse `ScrapeRequest` from `InfoHash` -#[allow(clippy::unnecessary_wraps)] -fn scrape_request(info_hashes: Vec, peer_addr: IpAddr) -> WebResult { - Ok(request::Scrape { info_hashes, peer_addr }) -} diff --git a/src/http/warp_implementation/handlers.rs b/src/http/warp_implementation/handlers.rs deleted file mode 100644 index f9aedeb8f..000000000 --- a/src/http/warp_implementation/handlers.rs +++ /dev/null @@ -1,207 +0,0 @@ -use std::collections::HashMap; -use std::convert::Infallible; -use std::net::IpAddr; -use std::panic::Location; -use std::sync::Arc; - -use log::debug; -use warp::http::Response; -use warp::{reject, Rejection, Reply}; - -use super::error::Error; -use super::{request, response, WebResult}; -use crate::http::warp_implementation::peer_builder; -use crate::protocol::info_hash::InfoHash; -use crate::tracker::auth::Key; -use crate::tracker::{self, auth, peer, statistics, torrent}; - -/// Authenticate `InfoHash` using optional `auth::Key` -/// -/// # Errors -/// -/// Will return `ServerError` that wraps the `tracker::error::Error` if unable to `authenticate_request`. -pub async fn authenticate( - info_hash: &InfoHash, - auth_key: &Option, - tracker: Arc, -) -> Result<(), Error> { - tracker - .authenticate_request(info_hash, auth_key) - .await - .map_err(|e| Error::TrackerError { - source: (Arc::new(e) as Arc).into(), - }) -} - -/// # Errors -/// -/// Will return `warp::Rejection` that wraps the `ServerError` if unable to `send_announce_response`. -pub async fn handle_announce( - announce_request: request::Announce, - auth_key: Option, - tracker: Arc, -) -> WebResult { - debug!("http announce request: {:#?}", announce_request); - - let info_hash = announce_request.info_hash; - let remote_client_ip = announce_request.peer_addr; - - authenticate(&info_hash, &auth_key, tracker.clone()).await?; - - let mut peer = peer_builder::from_request(&announce_request, &remote_client_ip); - - // todo: we should be use the http::axum_implementation::services::announce::announce service, - // but this Warp implementation is going to be removed. - - let response = tracker.announce(&info_hash, &mut peer, &remote_client_ip).await; - - match remote_client_ip { - IpAddr::V4(_) => { - tracker.send_stats_event(statistics::Event::Tcp4Announce).await; - } - IpAddr::V6(_) => { - tracker.send_stats_event(statistics::Event::Tcp6Announce).await; - } - } - - send_announce_response( - &announce_request, - &response.swarm_stats, - &response.peers, - tracker.config.announce_interval, - tracker.config.min_announce_interval, - ) -} - -/// # Errors -/// -/// Will return `warp::Rejection` that wraps the `ServerError` if unable to `send_scrape_response`. -pub async fn handle_scrape( - scrape_request: request::Scrape, - auth_key: Option, - tracker: Arc, -) -> WebResult { - let mut files: HashMap = HashMap::new(); - let db = tracker.get_torrents().await; - - for info_hash in &scrape_request.info_hashes { - let scrape_entry = match db.get(info_hash) { - Some(torrent_info) => { - if authenticate(info_hash, &auth_key, tracker.clone()).await.is_ok() { - let (seeders, completed, leechers) = torrent_info.get_stats(); - response::ScrapeEntry { - complete: seeders, - downloaded: completed, - incomplete: leechers, - } - } else { - response::ScrapeEntry { - complete: 0, - downloaded: 0, - incomplete: 0, - } - } - } - None => response::ScrapeEntry { - complete: 0, - downloaded: 0, - incomplete: 0, - }, - }; - - files.insert(*info_hash, scrape_entry); - } - - // send stats event - match scrape_request.peer_addr { - IpAddr::V4(_) => { - tracker.send_stats_event(statistics::Event::Tcp4Scrape).await; - } - IpAddr::V6(_) => { - tracker.send_stats_event(statistics::Event::Tcp6Scrape).await; - } - } - - send_scrape_response(files) -} - -/// Send announce response -#[allow(clippy::ptr_arg)] -fn send_announce_response( - announce_request: &request::Announce, - torrent_stats: &torrent::SwarmStats, - peers: &Vec, - interval: u32, - interval_min: u32, -) -> WebResult { - let http_peers: Vec = peers - .iter() - .map(|peer| response::Peer { - peer_id: peer.peer_id.to_string(), - ip: peer.peer_addr.ip(), - port: peer.peer_addr.port(), - }) - .collect(); - - let res = response::Announce { - interval, - interval_min, - complete: torrent_stats.seeders, - incomplete: torrent_stats.leechers, - peers: http_peers, - }; - - // check for compact response request - if let Some(1) = announce_request.compact { - match res.write_compact() { - Ok(body) => Ok(Response::new(body)), - Err(e) => Err(reject::custom(Error::InternalServer { - message: e.to_string(), - location: Location::caller(), - })), - } - } else { - Ok(Response::new(res.write().into())) - } -} - -/// Send scrape response -fn send_scrape_response(files: HashMap) -> WebResult { - let res = response::Scrape { files }; - - match res.write() { - Ok(body) => Ok(Response::new(body)), - Err(e) => Err(reject::custom(Error::InternalServer { - message: e.to_string(), - location: Location::caller(), - })), - } -} - -/// Handle all server errors and send error reply -/// -/// # Errors -/// -/// Will not return a error, `Infallible`, but instead convert the `ServerError` into a `Response`. -pub fn send_error(r: &Rejection) -> std::result::Result { - let warp_reject_error = r.find::(); - - let body = if let Some(error) = warp_reject_error { - debug!("{:?}", error); - response::Error { - failure_reason: error.to_string(), - } - .write() - } else { - response::Error { - failure_reason: Error::InternalServer { - message: "Undefined".to_string(), - location: Location::caller(), - } - .to_string(), - } - .write() - }; - - Ok(Response::new(body)) -} diff --git a/src/http/warp_implementation/launcher.rs b/src/http/warp_implementation/launcher.rs deleted file mode 100644 index 46ec2bf3c..000000000 --- a/src/http/warp_implementation/launcher.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::future::Future; -use std::net::SocketAddr; -use std::str::FromStr; -use std::sync::Arc; - -use futures::future::BoxFuture; - -use super::routes; -use crate::http::server::HttpServerLauncher; -use crate::tracker; -use crate::tracker::Tracker; - -#[derive(Debug)] -pub enum Error { - Error(String), -} - -pub struct Launcher; - -impl Launcher { - pub fn start_with_graceful_shutdown( - addr: SocketAddr, - tracker: Arc, - shutdown_signal: F, - ) -> (SocketAddr, BoxFuture<'static, ()>) - where - F: Future + Send + 'static, - { - let (bind_addr, server) = warp::serve(routes::routes(tracker)).bind_with_graceful_shutdown(addr, shutdown_signal); - - (bind_addr, Box::pin(server)) - } - - pub fn start_tls_with_graceful_shutdown( - addr: SocketAddr, - (ssl_cert_path, ssl_key_path): (&str, &str), - tracker: Arc, - shutdown_signal: F, - ) -> (SocketAddr, BoxFuture<'static, ()>) - where - F: Future + Send + 'static, - { - let (bind_addr, server) = warp::serve(routes::routes(tracker)) - .tls() - .cert_path(ssl_cert_path) - .key_path(ssl_key_path) - .bind_with_graceful_shutdown(addr, shutdown_signal); - - (bind_addr, Box::pin(server)) - } -} - -impl HttpServerLauncher for Launcher { - fn new() -> Self { - Self {} - } - - fn start_with_graceful_shutdown( - &self, - cfg: torrust_tracker_configuration::HttpTracker, - tracker: Arc, - shutdown_signal: F, - ) -> (SocketAddr, BoxFuture<'static, ()>) - where - F: Future + Send + 'static, - { - let addr = SocketAddr::from_str(&cfg.bind_address).expect("bind_address is not a valid SocketAddr."); - - if let (true, Some(ssl_cert_path), Some(ssl_key_path)) = (cfg.ssl_enabled, &cfg.ssl_cert_path, &cfg.ssl_key_path) { - Self::start_tls_with_graceful_shutdown(addr, (ssl_cert_path, ssl_key_path), tracker, shutdown_signal) - } else { - Self::start_with_graceful_shutdown(addr, tracker, shutdown_signal) - } - } -} - -/// Server that listens on HTTP, needs a `tracker::TorrentTracker` -#[derive(Clone)] -pub struct Http { - tracker: Arc, -} - -impl Http { - #[must_use] - pub fn new(tracker: Arc) -> Http { - Http { tracker } - } - - /// Start the `HttpServer` - pub fn start(&self, socket_addr: SocketAddr) -> impl warp::Future { - let (_addr, server) = - warp::serve(routes::routes(self.tracker.clone())).bind_with_graceful_shutdown(socket_addr, async move { - tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); - }); - - server - } - - /// Start the `HttpServer` in TLS mode - pub fn start_tls( - &self, - socket_addr: SocketAddr, - ssl_cert_path: String, - ssl_key_path: String, - ) -> impl warp::Future { - let (_addr, server) = warp::serve(routes::routes(self.tracker.clone())) - .tls() - .cert_path(ssl_cert_path) - .key_path(ssl_key_path) - .bind_with_graceful_shutdown(socket_addr, async move { - tokio::signal::ctrl_c().await.expect("Failed to listen to shutdown signal."); - }); - - server - } -} diff --git a/src/http/warp_implementation/mod.rs b/src/http/warp_implementation/mod.rs deleted file mode 100644 index c0e046f4f..000000000 --- a/src/http/warp_implementation/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub mod error; -pub mod filter_helpers; -pub mod filters; -pub mod handlers; -pub mod launcher; -pub mod peer_builder; -pub mod request; -pub mod response; -pub mod routes; - -use warp::Rejection; - -pub type Bytes = u64; -pub type WebResult = std::result::Result; diff --git a/src/http/warp_implementation/peer_builder.rs b/src/http/warp_implementation/peer_builder.rs deleted file mode 100644 index 70cf7b508..000000000 --- a/src/http/warp_implementation/peer_builder.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::net::{IpAddr, SocketAddr}; - -use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; - -use super::request::Announce; -use crate::protocol::clock::{Current, Time}; -use crate::tracker::peer::Peer; - -#[must_use] -pub fn from_request(announce_request: &Announce, peer_ip: &IpAddr) -> Peer { - let event: AnnounceEvent = if let Some(event) = &announce_request.event { - match event.as_ref() { - "started" => AnnounceEvent::Started, - "stopped" => AnnounceEvent::Stopped, - "completed" => AnnounceEvent::Completed, - _ => AnnounceEvent::None, - } - } else { - AnnounceEvent::None - }; - - #[allow(clippy::cast_possible_truncation)] - Peer { - peer_id: announce_request.peer_id, - peer_addr: SocketAddr::new(*peer_ip, announce_request.port), - updated: Current::now(), - uploaded: NumberOfBytes(i128::from(announce_request.uploaded) as i64), - downloaded: NumberOfBytes(i128::from(announce_request.downloaded) as i64), - left: NumberOfBytes(i128::from(announce_request.left) as i64), - event, - } -} diff --git a/src/http/warp_implementation/request.rs b/src/http/warp_implementation/request.rs deleted file mode 100644 index f666b48c5..000000000 --- a/src/http/warp_implementation/request.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::net::IpAddr; - -use serde::Deserialize; - -use crate::http::warp_implementation::Bytes; -use crate::protocol::info_hash::InfoHash; -use crate::tracker::peer; - -#[derive(Deserialize)] -pub struct AnnounceQuery { - pub downloaded: Option, - pub uploaded: Option, - pub key: Option, - pub port: u16, - pub left: Option, - pub event: Option, - pub compact: Option, -} - -#[derive(Debug)] -pub struct Announce { - pub info_hash: InfoHash, - pub peer_addr: IpAddr, - pub downloaded: Bytes, - pub uploaded: Bytes, - pub peer_id: peer::Id, - pub port: u16, - pub left: Bytes, - pub event: Option, - pub compact: Option, -} - -pub struct Scrape { - pub info_hashes: Vec, - pub peer_addr: IpAddr, -} diff --git a/src/http/warp_implementation/response.rs b/src/http/warp_implementation/response.rs deleted file mode 100644 index 1e9f7fa09..000000000 --- a/src/http/warp_implementation/response.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::collections::HashMap; -use std::io::Write; -use std::net::IpAddr; - -use serde::{self, Deserialize, Serialize}; - -use crate::protocol::info_hash::InfoHash; - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct Peer { - pub peer_id: String, - pub ip: IpAddr, - pub port: u16, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct Announce { - pub interval: u32, - #[serde(rename = "min interval")] - pub interval_min: u32, - //pub tracker_id: String, - pub complete: u32, - pub incomplete: u32, - pub peers: Vec, -} - -impl Announce { - /// # Panics - /// - /// It would panic if the `Announce` struct would contain an inappropriate type. - #[must_use] - pub fn write(&self) -> String { - serde_bencode::to_string(&self).unwrap() - } - - /// # Errors - /// - /// Will return `Err` if internally interrupted. - pub fn write_compact(&self) -> Result, Box> { - let mut peers_v4: Vec = Vec::new(); - let mut peers_v6: Vec = Vec::new(); - - for peer in &self.peers { - match peer.ip { - IpAddr::V4(ip) => { - peers_v4.write_all(&u32::from(ip).to_be_bytes())?; - peers_v4.write_all(&peer.port.to_be_bytes())?; - } - IpAddr::V6(ip) => { - peers_v6.write_all(&u128::from(ip).to_be_bytes())?; - peers_v6.write_all(&peer.port.to_be_bytes())?; - } - } - } - - let mut bytes: Vec = Vec::new(); - bytes.write_all(b"d8:intervali")?; - bytes.write_all(self.interval.to_string().as_bytes())?; - bytes.write_all(b"e12:min intervali")?; - bytes.write_all(self.interval_min.to_string().as_bytes())?; - bytes.write_all(b"e8:completei")?; - bytes.write_all(self.complete.to_string().as_bytes())?; - bytes.write_all(b"e10:incompletei")?; - bytes.write_all(self.incomplete.to_string().as_bytes())?; - bytes.write_all(b"e5:peers")?; - bytes.write_all(peers_v4.len().to_string().as_bytes())?; - bytes.write_all(b":")?; - bytes.write_all(peers_v4.as_slice())?; - bytes.write_all(b"e6:peers6")?; - bytes.write_all(peers_v6.len().to_string().as_bytes())?; - bytes.write_all(b":")?; - bytes.write_all(peers_v6.as_slice())?; - bytes.write_all(b"e")?; - - Ok(bytes) - } -} - -#[derive(Serialize)] -pub struct ScrapeEntry { - pub complete: u32, - pub downloaded: u32, - pub incomplete: u32, -} - -#[derive(Serialize)] -pub struct Scrape { - pub files: HashMap, -} - -impl Scrape { - /// # Errors - /// - /// Will return `Err` if internally interrupted. - pub fn write(&self) -> Result, Box> { - let mut bytes: Vec = Vec::new(); - - bytes.write_all(b"d5:filesd")?; - - for (info_hash, scrape_response_entry) in &self.files { - bytes.write_all(b"20:")?; - bytes.write_all(&info_hash.0)?; - bytes.write_all(b"d8:completei")?; - bytes.write_all(scrape_response_entry.complete.to_string().as_bytes())?; - bytes.write_all(b"e10:downloadedi")?; - bytes.write_all(scrape_response_entry.downloaded.to_string().as_bytes())?; - bytes.write_all(b"e10:incompletei")?; - bytes.write_all(scrape_response_entry.incomplete.to_string().as_bytes())?; - bytes.write_all(b"ee")?; - } - - bytes.write_all(b"ee")?; - - Ok(bytes) - } -} - -#[derive(Serialize)] -pub struct Error { - #[serde(rename = "failure reason")] - pub failure_reason: String, -} - -impl Error { - /// # Panics - /// - /// It would panic if the `Error` struct would contain an inappropriate type. - #[must_use] - pub fn write(&self) -> String { - serde_bencode::to_string(&self).unwrap() - } -} diff --git a/src/http/warp_implementation/routes.rs b/src/http/warp_implementation/routes.rs deleted file mode 100644 index c46c502e4..000000000 --- a/src/http/warp_implementation/routes.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::convert::Infallible; -use std::sync::Arc; - -use warp::{Filter, Rejection}; - -use super::filters::{with_announce_request, with_auth_key, with_scrape_request, with_tracker}; -use super::handlers::{handle_announce, handle_scrape, send_error}; -use crate::tracker; - -/// All routes -#[must_use] -pub fn routes(tracker: Arc) -> impl Filter + Clone { - announce(tracker.clone()) - .or(scrape(tracker)) - .recover(|q| async move { send_error(&q) }) -} - -/// GET /announce or /announce/ -fn announce(tracker: Arc) -> impl Filter + Clone { - warp::path::path("announce") - .and(warp::filters::method::get()) - .and(with_announce_request(tracker.config.on_reverse_proxy)) - .and(with_auth_key()) - .and(with_tracker(tracker)) - .and_then(handle_announce) -} - -/// GET /scrape/ -fn scrape(tracker: Arc) -> impl Filter + Clone { - warp::path::path("scrape") - .and(warp::filters::method::get()) - .and(with_scrape_request(tracker.config.on_reverse_proxy)) - .and(with_auth_key()) - .and(with_tracker(tracker)) - .and_then(handle_scrape) -} diff --git a/src/jobs/http_tracker.rs b/src/jobs/http_tracker.rs index 40caa8e88..e0091958b 100644 --- a/src/jobs/http_tracker.rs +++ b/src/jobs/http_tracker.rs @@ -1,14 +1,12 @@ -use std::net::SocketAddr; use std::sync::Arc; use axum_server::tls_rustls::RustlsConfig; -use log::{info, warn}; +use log::info; use tokio::sync::oneshot; use tokio::task::JoinHandle; use torrust_tracker_configuration::HttpTracker; -use crate::http::axum_implementation::launcher; -use crate::http::warp_implementation::launcher::Http; +use crate::http::v1::launcher; use crate::http::Version; use crate::tracker; @@ -17,72 +15,14 @@ pub struct ServerJobStarted(); pub async fn start_job(config: &HttpTracker, tracker: Arc, version: Version) -> JoinHandle<()> { match version { - Version::Warp => start_warp(config, tracker.clone()).await, - Version::Axum => start_axum(config, tracker.clone()).await, + Version::V1 => start_v1(config, tracker.clone()).await, } } /// # Panics /// /// It would panic if the `config::HttpTracker` struct would contain inappropriate values. -async fn start_warp(config: &HttpTracker, tracker: Arc) -> JoinHandle<()> { - let bind_addr = config - .bind_address - .parse::() - .expect("HTTP tracker server bind_address invalid."); - let ssl_enabled = config.ssl_enabled; - let ssl_cert_path = config.ssl_cert_path.clone(); - let ssl_key_path = config.ssl_key_path.clone(); - - let (tx, rx) = oneshot::channel::(); - - // Run the HTTP tracker server - let join_handle = tokio::spawn(async move { - let http_tracker = Http::new(tracker); - - if !ssl_enabled { - info!("Starting HTTP tracker server on: http://{}", bind_addr); - - let handle = http_tracker.start(bind_addr); - - tx.send(ServerJobStarted()) - .expect("HTTP tracker server should not be dropped"); - - handle.await; - - info!("HTTP tracker server on http://{} stopped", bind_addr); - } else if ssl_enabled && ssl_cert_path.is_some() && ssl_key_path.is_some() { - info!("Starting HTTPS server on: https://{}", bind_addr); - - let handle = http_tracker.start_tls(bind_addr, ssl_cert_path.unwrap(), ssl_key_path.unwrap()); - - tx.send(ServerJobStarted()) - .expect("HTTP tracker server should not be dropped"); - - handle.await; - - info!("HTTP tracker server on https://{} stopped", bind_addr); - } else { - warn!( - "Could not start HTTPS tracker server on: {}, missing SSL Cert or Key!", - bind_addr - ); - } - }); - - // Wait until the HTTPS tracker server job is running - match rx.await { - Ok(_msg) => info!("HTTP tracker server started"), - Err(e) => panic!("HTTP tracker server was dropped: {e}"), - } - - join_handle -} - -/// # Panics -/// -/// It would panic if the `config::HttpTracker` struct would contain inappropriate values. -async fn start_axum(config: &HttpTracker, tracker: Arc) -> JoinHandle<()> { +async fn start_v1(config: &HttpTracker, tracker: Arc) -> JoinHandle<()> { let bind_addr = config .bind_address .parse::() diff --git a/src/setup.rs b/src/setup.rs index ee32f5a81..86de0723c 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -51,7 +51,7 @@ pub async fn setup(config: &Configuration, tracker: Arc) -> Ve if !http_tracker_config.enabled { continue; } - jobs.push(http_tracker::start_job(http_tracker_config, tracker.clone(), Version::Axum).await); + jobs.push(http_tracker::start_job(http_tracker_config, tracker.clone(), Version::V1).await); } // Start HTTP API diff --git a/tests/http/asserts.rs b/tests/http/asserts.rs index 0d5441f89..932b48be4 100644 --- a/tests/http/asserts.rs +++ b/tests/http/asserts.rs @@ -108,48 +108,12 @@ pub async fn assert_missing_query_params_for_scrape_request_error_response(respo // Other errors -pub async fn assert_internal_server_error_response(response: Response) { - assert_eq!(response.status(), 200); - - assert_bencoded_error(&response.text().await.unwrap(), "internal server", Location::caller()); -} - -pub async fn assert_invalid_info_hash_error_response(response: Response) { - assert_eq!(response.status(), 200); - - assert_bencoded_error( - &response.text().await.unwrap(), - "no valid infohashes found", - Location::caller(), - ); -} - -pub async fn assert_invalid_peer_id_error_response(response: Response) { - assert_eq!(response.status(), 200); - - assert_bencoded_error( - &response.text().await.unwrap(), - "peer_id is either missing or invalid", - Location::caller(), - ); -} - pub async fn assert_torrent_not_in_whitelist_error_response(response: Response) { assert_eq!(response.status(), 200); assert_bencoded_error(&response.text().await.unwrap(), "is not whitelisted", Location::caller()); } -pub async fn assert_could_not_find_remote_address_on_xff_header_error_response(response: Response) { - assert_eq!(response.status(), 200); - - assert_bencoded_error( - &response.text().await.unwrap(), - "could not find remote address: must have a x-forwarded-for when using a reverse proxy", - Location::caller(), - ); -} - pub async fn assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response: Response) { assert_eq!(response.status(), 200); @@ -160,16 +124,6 @@ pub async fn assert_could_not_find_remote_address_on_x_forwarded_for_header_erro ); } -pub async fn assert_invalid_remote_address_on_xff_header_error_response(response: Response) { - assert_eq!(response.status(), 200); - - assert_bencoded_error( - &response.text().await.unwrap(), - "could not find remote address: on remote proxy and unable to parse the last x-forwarded-ip", - Location::caller(), - ); -} - pub async fn assert_cannot_parse_query_param_error_response(response: Response, failure: &str) { assert_cannot_parse_query_params_error_response(response, &format!(": {failure}")).await; } diff --git a/tests/http/asserts_warp.rs b/tests/http/asserts_warp.rs deleted file mode 100644 index d1a936efa..000000000 --- a/tests/http/asserts_warp.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::panic::Location; - -/// todo: this mod should be removed when we remove the Warp implementation for the HTTP tracker. -use reqwest::Response; - -use super::responses::announce_warp::WarpAnnounce; -use crate::http::asserts::assert_bencoded_error; - -pub async fn assert_warp_announce_response(response: Response, expected_announce_response: &WarpAnnounce) { - assert_eq!(response.status(), 200); - - let body = response.text().await.unwrap(); - - let announce_response: WarpAnnounce = serde_bencode::from_str(&body) - .unwrap_or_else(|_| panic!("response body should be a valid announce response, got \"{:#?}\"", &body)); - - assert_eq!(announce_response, *expected_announce_response); -} - -pub async fn assert_warp_peer_not_authenticated_error_response(response: Response) { - assert_eq!(response.status(), 200); - - assert_bencoded_error( - &response.text().await.unwrap(), - "The peer is not authenticated", - Location::caller(), - ); -} - -pub async fn assert_warp_invalid_authentication_key_error_response(response: Response) { - assert_eq!(response.status(), 200); - - assert_bencoded_error(&response.text().await.unwrap(), "is not valid", Location::caller()); -} diff --git a/tests/http/mod.rs b/tests/http/mod.rs index 771145f46..b0d896c99 100644 --- a/tests/http/mod.rs +++ b/tests/http/mod.rs @@ -1,5 +1,4 @@ pub mod asserts; -pub mod asserts_warp; pub mod client; pub mod requests; pub mod responses; diff --git a/tests/http/responses/announce_warp.rs b/tests/http/responses/announce_warp.rs deleted file mode 100644 index 0fcf05eb8..000000000 --- a/tests/http/responses/announce_warp.rs +++ /dev/null @@ -1,30 +0,0 @@ -/// todo: this mod should be removed when we remove the Warp implementation for the HTTP tracker. -use serde::{self, Deserialize, Serialize}; -use torrust_tracker::tracker::peer::Peer; - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct WarpAnnounce { - pub complete: u32, - pub incomplete: u32, - pub interval: u32, - #[serde(rename = "min interval")] - pub min_interval: u32, - pub peers: Vec, // Peers using IPV4 -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct WarpDictionaryPeer { - pub ip: String, - pub peer_id: String, - pub port: u16, -} - -impl From for WarpDictionaryPeer { - fn from(peer: Peer) -> Self { - Self { - peer_id: peer.peer_id.to_string(), - ip: peer.peer_addr.ip().to_string(), - port: peer.peer_addr.port(), - } - } -} diff --git a/tests/http/responses/mod.rs b/tests/http/responses/mod.rs index aecb53fed..bdc689056 100644 --- a/tests/http/responses/mod.rs +++ b/tests/http/responses/mod.rs @@ -1,4 +1,3 @@ pub mod announce; -pub mod announce_warp; pub mod error; pub mod scrape; diff --git a/tests/http_tracker.rs b/tests/http_tracker.rs index aea8fac37..730da93d5 100644 --- a/tests/http_tracker.rs +++ b/tests/http_tracker.rs @@ -1,2780 +1,1443 @@ /// Integration tests for HTTP tracker server /// -/// Warp version: /// ```text -/// cargo test `warp_test_env` -- --nocapture +/// cargo test `http_tracker_server` -- --nocapture /// ``` -/// -/// Axum version (WIP): -/// ```text -/// cargo test `warp_test_env` -- --nocapture -/// ``` -mod common; -mod http; - -pub type Axum = torrust_tracker::http::axum_implementation::launcher::Launcher; -pub type Warp = torrust_tracker::http::warp_implementation::launcher::Launcher; - -mod test_env_test_environment { - use torrust_tracker_test_helpers::configuration; - - use crate::http::test_environment::running_test_environment; - use crate::{Axum, Warp}; - - #[tokio::test] - async fn should_be_able_to_start_and_stop_a_test_environment_using_axum() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_be_able_to_start_and_stop_a_test_environment_using_warp() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - test_env.stop().await; - } -} - -mod warp_test_env { - - mod for_all_config_modes { - - mod running_on_reverse_proxy { - use torrust_tracker_test_helpers::configuration; - - use crate::http::asserts::{ - assert_could_not_find_remote_address_on_xff_header_error_response, - assert_invalid_remote_address_on_xff_header_error_response, - }; - use crate::http::client::Client; - use crate::http::requests::announce::QueryBuilder; - use crate::http::test_environment::running_test_environment; - use crate::Warp; - - #[tokio::test] - async fn should_fail_when_the_http_request_does_not_include_the_xff_http_request_header() { - // If the tracker is running behind a reverse proxy, the peer IP is the - // last IP in the `X-Forwarded-For` HTTP header, which is the IP of the proxy client. - - let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; - - let params = QueryBuilder::default().query().params(); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_could_not_find_remote_address_on_xff_header_error_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_xff_http_request_header_contains_an_invalid_ip() { - let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; - - let params = QueryBuilder::default().query().params(); - - let response = Client::new(*test_env.bind_address()) - .get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP") - .await; - - assert_invalid_remote_address_on_xff_header_error_response(response).await; - - test_env.stop().await; - } - } - - mod receiving_an_announce_request { - - // Announce request documentation: - // - // BEP 03. The BitTorrent Protocol Specification - // https://www.bittorrent.org/beps/bep_0003.html - // - // BEP 23. Tracker Returns Compact Peer Lists - // https://www.bittorrent.org/beps/bep_0023.html - // - // Vuze (bittorrent client) docs: - // https://wiki.vuze.com/w/Announce - - use std::net::{IpAddr, Ipv6Addr}; - use std::str::FromStr; - - use local_ip_address::local_ip; - use reqwest::Response; - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::{invalid_info_hashes, PeerBuilder}; - use crate::http::asserts::{ - assert_announce_response, assert_compact_announce_response, assert_empty_announce_response, - assert_internal_server_error_response, assert_invalid_info_hash_error_response, - assert_invalid_peer_id_error_response, assert_is_announce_response, - }; - use crate::http::asserts_warp::assert_warp_announce_response; - use crate::http::client::Client; - use crate::http::requests::announce::{Compact, QueryBuilder}; - use crate::http::responses; - use crate::http::responses::announce::{Announce, CompactPeer, CompactPeerList}; - use crate::http::responses::announce_warp::{WarpAnnounce, WarpDictionaryPeer}; - use crate::http::test_environment::running_test_environment; - use crate::Warp; - - #[tokio::test] - async fn should_respond_if_only_the_mandatory_fields_are_provided() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - params.remove_optional_params(); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_is_announce_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_url_query_component_is_empty() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let response = Client::new(*test_env.bind_address()).get("announce").await; - - assert_internal_server_error_response(response).await; - } - - #[tokio::test] - async fn should_fail_when_a_mandatory_field_is_missing() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - // Without `info_hash` param - - let mut params = QueryBuilder::default().query().params(); - - params.info_hash = None; - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_invalid_info_hash_error_response(response).await; - - // Without `peer_id` param - - let mut params = QueryBuilder::default().query().params(); - - params.peer_id = None; - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_invalid_peer_id_error_response(response).await; - - // Without `port` param - - let mut params = QueryBuilder::default().query().params(); - - params.port = None; - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_internal_server_error_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_info_hash_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - for invalid_value in &invalid_info_hashes() { - params.set("info_hash", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_invalid_info_hash_error_response(response).await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_fail_when_the_peer_address_param_is_invalid() { - // AnnounceQuery does not even contain the `peer_addr` - // The peer IP is obtained in two ways: - // 1. If tracker is NOT running `on_reverse_proxy` from the remote client IP if there. - // 2. If tracker is running `on_reverse_proxy` from `X-Forwarded-For` request header is tracker is running `on_reverse_proxy`. - - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - params.peer_addr = Some("INVALID-IP-ADDRESS".to_string()); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_is_announce_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_downloaded_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = ["-1", "1.1", "a"]; - - for invalid_value in invalid_values { - params.set("downloaded", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_internal_server_error_response(response).await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_uploaded_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = ["-1", "1.1", "a"]; - - for invalid_value in invalid_values { - params.set("uploaded", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_internal_server_error_response(response).await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_peer_id_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = [ - "0", - "-1", - "1.1", - "a", - "-qB0000000000000000", // 19 bytes - "-qB000000000000000000", // 21 bytes - ]; - - for invalid_value in invalid_values { - params.set("peer_id", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_invalid_peer_id_error_response(response).await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_port_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = ["-1", "1.1", "a"]; - - for invalid_value in invalid_values { - params.set("port", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_internal_server_error_response(response).await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_left_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = ["-1", "1.1", "a"]; - - for invalid_value in invalid_values { - params.set("left", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_internal_server_error_response(response).await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_fail_when_the_event_param_is_invalid() { - // All invalid values are ignored as if the `event` param were empty - - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = [ - "0", - "-1", - "1.1", - "a", - "Started", // It should be lowercase - "Stopped", // It should be lowercase - "Completed", // It should be lowercase - ]; - - for invalid_value in invalid_values { - params.set("event", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_is_announce_response(response).await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_fail_when_the_compact_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - let invalid_values = ["-1", "1.1", "a"]; - - for invalid_value in invalid_values { - params.set("compact", invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_internal_server_error_response(response).await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap()) - .query(), - ) - .await; - - assert_announce_response( - response, - &Announce { - complete: 1, // the peer for this test - incomplete: 0, - interval: test_env.tracker.config.announce_interval, - min_interval: test_env.tracker.config.min_announce_interval, - peers: vec![], - }, - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_list_of_previously_announced_peers() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - // Peer 1 - let previously_announced_peer = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .build(); - - // Add the Peer 1 - test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; - - // Announce the new Peer 2. This new peer is non included on the response peer list - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .query(), - ) - .await; - - // It should only contain the previously announced peer - assert_warp_announce_response( - response, - &WarpAnnounce { - complete: 2, - incomplete: 0, - interval: test_env.tracker.config.announce_interval, - min_interval: test_env.tracker.config.min_announce_interval, - peers: vec![WarpDictionaryPeer::from(previously_announced_peer)], - }, - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_peer_id_even_if_the_ip_is_different() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let peer = PeerBuilder::default().build(); - - // Add a peer - test_env.add_torrent_peer(&info_hash, &peer).await; - - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer.peer_id) - .query(); - - assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr); - - let response = Client::new(*test_env.bind_address()).announce(&announce_query).await; - - assert_empty_announce_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_compact_response() { - // Tracker Returns Compact Peer Lists - // https://www.bittorrent.org/beps/bep_0023.html - - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - // Peer 1 - let previously_announced_peer = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .build(); - - // Add the Peer 1 - test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; - - // Announce the new Peer 2 accepting compact responses - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .with_compact(Compact::Accepted) - .query(), - ) - .await; - - let expected_response = responses::announce::Compact { - complete: 2, - incomplete: 0, - interval: 120, - min_interval: 120, - peers: CompactPeerList::new([CompactPeer::new(&previously_announced_peer.peer_addr)].to_vec()), - }; - - assert_compact_announce_response(response, &expected_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_return_the_compact_response_by_default() { - // code-review: the HTTP tracker does not return the compact response by default if the "compact" - // param is not provided in the announce URL. The BEP 23 suggest to do so. - - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - // Peer 1 - let previously_announced_peer = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .build(); - - // Add the Peer 1 - test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; - - // Announce the new Peer 2 without passing the "compact" param - // By default it should respond with the compact peer list - // https://www.bittorrent.org/beps/bep_0023.html - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .without_compact() - .query(), - ) - .await; - - assert!(!is_a_compact_announce_response(response).await); - - test_env.stop().await; - } - - async fn is_a_compact_announce_response(response: Response) -> bool { - let bytes = response.bytes().await.unwrap(); - let compact_announce = serde_bencode::from_bytes::(&bytes); - compact_announce.is_ok() - } - - #[tokio::test] - async fn should_increase_the_number_of_tcp4_connections_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().query()) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp4_connections_handled, 1); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_of_tcp6_connections_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - - Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) - .announce(&QueryBuilder::default().query()) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_connections_handled, 1); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_increase_the_number_of_tcp6_connections_handled_if_the_client_is_not_using_an_ipv6_ip() { - // The tracker ignores the peer address in the request param. It uses the client remote ip address. - - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) - .query(), - ) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_connections_handled, 0); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_of_tcp4_announce_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().query()) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp4_announces_handled, 1); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_of_tcp6_announce_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - - Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) - .announce(&QueryBuilder::default().query()) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_announces_handled, 1); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_not_increase_the_number_of_tcp6_announce_requests_handled_if_the_client_is_not_using_an_ipv6_ip() { - // The tracker ignores the peer address in the request param. It uses the client remote ip address. - - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) - .query(), - ) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_announces_handled, 0); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_assign_to_the_peer_ip_the_remote_client_ip_instead_of_the_peer_address_in_the_request_param() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let client_ip = local_ip().unwrap(); - - let client = Client::bind(*test_env.bind_address(), client_ip); - - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); - - client.announce(&announce_query).await; - - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; - - assert_eq!(peer_addr.ip(), client_ip); - assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); - - test_env.stop().await; - } - - #[tokio::test] - async fn when_the_client_ip_is_a_loopback_ipv4_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration( - ) { - /* We assume that both the client and tracker share the same public IP. - - client <-> tracker <-> Internet - 127.0.0.1 external_ip = "2.137.87.41" - */ - - let test_env = running_test_environment::(configuration::ephemeral_with_external_ip( - IpAddr::from_str("2.137.87.41").unwrap(), - )) - .await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); - let client_ip = loopback_ip; - - let client = Client::bind(*test_env.bind_address(), client_ip); - - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); - - client.announce(&announce_query).await; - - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; - - assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap()); - assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); - - test_env.stop().await; - } - - #[tokio::test] - async fn when_the_client_ip_is_a_loopback_ipv6_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration( - ) { - /* We assume that both the client and tracker share the same public IP. - - client <-> tracker <-> Internet - ::1 external_ip = "2345:0425:2CA1:0000:0000:0567:5673:23b5" - */ - - let test_env = running_test_environment::(configuration::ephemeral_with_external_ip( - IpAddr::from_str("2345:0425:2CA1:0000:0000:0567:5673:23b5").unwrap(), - )) - .await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); - let client_ip = loopback_ip; - - let client = Client::bind(*test_env.bind_address(), client_ip); - - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); - - client.announce(&announce_query).await; - - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; - - assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap()); - assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); - - test_env.stop().await; - } - - #[tokio::test] - async fn when_the_tracker_is_behind_a_reverse_proxy_it_should_assign_to_the_peer_ip_the_ip_in_the_x_forwarded_for_http_header( - ) { - /* - client <-> http proxy <-> tracker <-> Internet - ip: header: config: peer addr: - 145.254.214.256 X-Forwarded-For = 145.254.214.256 on_reverse_proxy = true 145.254.214.256 - */ - - let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - let client = Client::new(*test_env.bind_address()); - - let announce_query = QueryBuilder::default().with_info_hash(&info_hash).query(); - - client - .announce_with_header( - &announce_query, - "X-Forwarded-For", - "203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178", - ) - .await; - - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; - - assert_eq!(peer_addr.ip(), IpAddr::from_str("150.172.238.178").unwrap()); - - test_env.stop().await; - } - } - - mod receiving_an_scrape_request { - - // Scrape documentation: - // - // BEP 48. Tracker Protocol Extension: Scrape - // https://www.bittorrent.org/beps/bep_0048.html - // - // Vuze (bittorrent client) docs: - // https://wiki.vuze.com/w/Scrape - - use std::net::IpAddr; - use std::str::FromStr; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::{invalid_info_hashes, PeerBuilder}; - use crate::http::asserts::{assert_internal_server_error_response, assert_scrape_response}; - use crate::http::client::Client; - use crate::http::requests; - use crate::http::requests::scrape::QueryBuilder; - use crate::http::responses::scrape::{self, File, ResponseBuilder}; - use crate::http::test_environment::running_test_environment; - use crate::Warp; - - #[tokio::test] - async fn should_fail_when_the_request_is_empty() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - let response = Client::new(*test_env.bind_address()).get("scrape").await; - - assert_internal_server_error_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_when_the_info_hash_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let mut params = QueryBuilder::default().query().params(); - - for invalid_value in &invalid_info_hashes() { - params.set_one_info_hash_param(invalid_value); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - // code-review: it's not returning the invalid info hash error - assert_internal_server_error_response(response).await; - } - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_file_with_the_incomplete_peer_when_there_is_one_peer_with_bytes_pending_to_download() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_file_with_the_complete_peer_when_there_is_one_peer_with_no_bytes_pending_to_download() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_no_bytes_pending_to_download() - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 1, - downloaded: 0, - incomplete: 0, - }, - ) - .build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_a_file_with_zeroed_values_when_there_are_no_peers() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - assert_scrape_response(response, &scrape::Response::with_one_file(info_hash.bytes(), File::zeroed())).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_accept_multiple_infohashes() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash1 = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let info_hash2 = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(); - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .add_info_hash(&info_hash1) - .add_info_hash(&info_hash2) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default() - .add_file(info_hash1.bytes(), File::zeroed()) - .add_file(info_hash2.bytes(), File::zeroed()) - .build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_ot_tcp4_scrape_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp4_scrapes_handled, 1); - - drop(stats); - - test_env.stop().await; - } - - #[tokio::test] - async fn should_increase_the_number_ot_tcp6_scrape_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let stats = test_env.tracker.get_stats().await; - - assert_eq!(stats.tcp6_scrapes_handled, 1); - - drop(stats); - - test_env.stop().await; - } - } - } - - mod configured_as_whitelisted { - - mod and_receiving_an_announce_request { - use std::str::FromStr; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker_test_helpers::configuration; - - use crate::http::asserts::{assert_is_announce_response, assert_torrent_not_in_whitelist_error_response}; - use crate::http::client::Client; - use crate::http::requests::announce::QueryBuilder; - use crate::http::test_environment::running_test_environment; - use crate::Warp; - - #[tokio::test] - async fn should_fail_if_the_torrent_is_not_in_the_whitelist() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - let response = Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) - .await; - - assert_torrent_not_in_whitelist_error_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - - async fn should_allow_announcing_a_whitelisted_torrent() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .tracker - .add_torrent_to_whitelist(&info_hash) - .await - .expect("should add the torrent to the whitelist"); - - let response = Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) - .await; - - assert_is_announce_response(response).await; - - test_env.stop().await; - } - } - - mod receiving_an_scrape_request { - use std::str::FromStr; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::PeerBuilder; - use crate::http::asserts::assert_scrape_response; - use crate::http::client::Client; - use crate::http::requests; - use crate::http::responses::scrape::{File, ResponseBuilder}; - use crate::http::test_environment::running_test_environment; - use crate::Warp; - - #[tokio::test] - async fn should_return_the_zeroed_file_when_the_requested_file_is_not_whitelisted() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_file_stats_when_the_requested_file_is_whitelisted() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - test_env - .tracker - .add_torrent_to_whitelist(&info_hash) - .await - .expect("should add the torrent to the whitelist"); - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - } - } - - mod configured_as_private { - - mod and_receiving_an_announce_request { - use std::str::FromStr; - use std::time::Duration; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::auth::Key; - use torrust_tracker_test_helpers::configuration; - - use crate::http::asserts::assert_is_announce_response; - use crate::http::asserts_warp::{ - assert_warp_invalid_authentication_key_error_response, assert_warp_peer_not_authenticated_error_response, - }; - use crate::http::client::Client; - use crate::http::requests::announce::QueryBuilder; - use crate::http::test_environment::running_test_environment; - use crate::Warp; - - #[tokio::test] - async fn should_respond_to_authenticated_peers() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - - let response = Client::authenticated(*test_env.bind_address(), key.id()) - .announce(&QueryBuilder::default().query()) - .await; - - assert_is_announce_response(response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_fail_if_the_peer_has_not_provided_the_authentication_key() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - let response = Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) - .await; - - assert_warp_peer_not_authenticated_error_response(response).await; - } - - #[tokio::test] - async fn should_fail_if_the_peer_authentication_key_is_not_valid() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - // The tracker does not have this key - let unregistered_key = Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap(); - - let response = Client::authenticated(*test_env.bind_address(), unregistered_key) - .announce(&QueryBuilder::default().query()) - .await; - - assert_warp_invalid_authentication_key_error_response(response).await; - - test_env.stop().await; - } - } - - mod receiving_an_scrape_request { - - use std::str::FromStr; - use std::time::Duration; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::auth::Key; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::PeerBuilder; - use crate::http::asserts::assert_scrape_response; - use crate::http::client::Client; - use crate::http::requests; - use crate::http::responses::scrape::{File, ResponseBuilder}; - use crate::http::test_environment::running_test_environment; - use crate::Warp; - - #[tokio::test] - async fn should_return_the_zeroed_file_when_the_client_is_not_authenticated() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_real_file_stats_when_the_client_is_authenticated() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - - let response = Client::authenticated(*test_env.bind_address(), key.id()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); - - assert_scrape_response(response, &expected_scrape_response).await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_the_zeroed_file_when_the_authentication_key_provided_by_the_client_is_invalid() { - // There is not authentication error - - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; +mod common; +mod http; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); +pub type V1 = torrust_tracker::http::v1::launcher::Launcher; - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; +mod http_tracker { - let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap(); + mod v1 { - let response = Client::authenticated(*test_env.bind_address(), false_key) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; + use torrust_tracker_test_helpers::configuration; - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); + use crate::http::test_environment::running_test_environment; + use crate::V1; - assert_scrape_response(response, &expected_scrape_response).await; + #[tokio::test] + async fn test_environment_should_be_started_and_stopped() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - test_env.stop().await; - } + test_env.stop().await; } - } - mod configured_as_private_and_whitelisted { + mod for_all_config_modes { - mod and_receiving_an_announce_request {} + mod and_running_on_reverse_proxy { + use torrust_tracker_test_helpers::configuration; - mod receiving_an_scrape_request {} - } -} + use crate::http::asserts::assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response; + use crate::http::client::Client; + use crate::http::requests::announce::QueryBuilder; + use crate::http::test_environment::running_test_environment; + use crate::V1; -mod axum_test_env { + #[tokio::test] + async fn should_fail_when_the_http_request_does_not_include_the_xff_http_request_header() { + // If the tracker is running behind a reverse proxy, the peer IP is the + // right most IP in the `X-Forwarded-For` HTTP header, which is the IP of the proxy's client. - // WIP: migration HTTP from Warp to Axum + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; - mod for_all_config_modes { + let params = QueryBuilder::default().query().params(); - mod and_running_on_reverse_proxy { - use torrust_tracker_test_helpers::configuration; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - use crate::http::asserts::assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response; - use crate::http::client::Client; - use crate::http::requests::announce::QueryBuilder; - use crate::http::test_environment::running_test_environment; - use crate::Axum; + assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; - #[tokio::test] - async fn should_fail_when_the_http_request_does_not_include_the_xff_http_request_header() { - // If the tracker is running behind a reverse proxy, the peer IP is the - // right most IP in the `X-Forwarded-For` HTTP header, which is the IP of the proxy's client. + test_env.stop().await; + } - let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; + #[tokio::test] + async fn should_fail_when_the_xff_http_request_header_contains_an_invalid_ip() { + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; - let params = QueryBuilder::default().query().params(); + let params = QueryBuilder::default().query().params(); - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + let response = Client::new(*test_env.bind_address()) + .get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP") + .await; - assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; + assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; - test_env.stop().await; + test_env.stop().await; + } } - #[tokio::test] - async fn should_fail_when_the_xff_http_request_header_contains_an_invalid_ip() { - let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; - - let params = QueryBuilder::default().query().params(); - - let response = Client::new(*test_env.bind_address()) - .get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP") - .await; - - assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; - - test_env.stop().await; - } - } + mod receiving_an_announce_request { - mod receiving_an_announce_request { - - // Announce request documentation: - // - // BEP 03. The BitTorrent Protocol Specification - // https://www.bittorrent.org/beps/bep_0003.html - // - // BEP 23. Tracker Returns Compact Peer Lists - // https://www.bittorrent.org/beps/bep_0023.html - // - // Vuze (bittorrent client) docs: - // https://wiki.vuze.com/w/Announce - - use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - use std::str::FromStr; - - use local_ip_address::local_ip; - use reqwest::Response; - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::{invalid_info_hashes, PeerBuilder}; - use crate::http::asserts::{ - assert_announce_response, assert_bad_announce_request_error_response, - assert_cannot_parse_query_param_error_response, assert_cannot_parse_query_params_error_response, - assert_compact_announce_response, assert_empty_announce_response, assert_is_announce_response, - assert_missing_query_params_for_announce_request_error_response, - }; - use crate::http::client::Client; - use crate::http::requests::announce::{Compact, QueryBuilder}; - use crate::http::responses; - use crate::http::responses::announce::{Announce, CompactPeer, CompactPeerList, DictionaryPeer}; - use crate::http::test_environment::running_test_environment; - use crate::Axum; - - #[tokio::test] - async fn should_respond_if_only_the_mandatory_fields_are_provided() { - let test_env = running_test_environment::(configuration::ephemeral()).await; - - let mut params = QueryBuilder::default().query().params(); - - params.remove_optional_params(); - - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - - assert_is_announce_response(response).await; - - test_env.stop().await; - } + // Announce request documentation: + // + // BEP 03. The BitTorrent Protocol Specification + // https://www.bittorrent.org/beps/bep_0003.html + // + // BEP 23. Tracker Returns Compact Peer Lists + // https://www.bittorrent.org/beps/bep_0023.html + // + // Vuze (bittorrent client) docs: + // https://wiki.vuze.com/w/Announce + + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + use std::str::FromStr; + + use local_ip_address::local_ip; + use reqwest::Response; + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::peer; + use torrust_tracker_test_helpers::configuration; + + use crate::common::fixtures::{invalid_info_hashes, PeerBuilder}; + use crate::http::asserts::{ + assert_announce_response, assert_bad_announce_request_error_response, + assert_cannot_parse_query_param_error_response, assert_cannot_parse_query_params_error_response, + assert_compact_announce_response, assert_empty_announce_response, assert_is_announce_response, + assert_missing_query_params_for_announce_request_error_response, + }; + use crate::http::client::Client; + use crate::http::requests::announce::{Compact, QueryBuilder}; + use crate::http::responses; + use crate::http::responses::announce::{Announce, CompactPeer, CompactPeerList, DictionaryPeer}; + use crate::http::test_environment::running_test_environment; + use crate::V1; - #[tokio::test] - async fn should_fail_when_the_url_query_component_is_empty() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + #[tokio::test] + async fn should_respond_if_only_the_mandatory_fields_are_provided() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - let response = Client::new(*test_env.bind_address()).get("announce").await; + let mut params = QueryBuilder::default().query().params(); - assert_missing_query_params_for_announce_request_error_response(response).await; + params.remove_optional_params(); - test_env.stop().await; - } + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - #[tokio::test] - async fn should_fail_when_url_query_parameters_are_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + assert_is_announce_response(response).await; - let invalid_query_param = "a=b=c"; + test_env.stop().await; + } - let response = Client::new(*test_env.bind_address()) - .get(&format!("announce?{invalid_query_param}")) - .await; + #[tokio::test] + async fn should_fail_when_the_url_query_component_is_empty() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - assert_cannot_parse_query_param_error_response(response, "invalid param a=b=c").await; + let response = Client::new(*test_env.bind_address()).get("announce").await; - test_env.stop().await; - } + assert_missing_query_params_for_announce_request_error_response(response).await; - #[tokio::test] - async fn should_fail_when_a_mandatory_field_is_missing() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + test_env.stop().await; + } - // Without `info_hash` param + #[tokio::test] + async fn should_fail_when_url_query_parameters_are_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - let mut params = QueryBuilder::default().query().params(); + let invalid_query_param = "a=b=c"; - params.info_hash = None; + let response = Client::new(*test_env.bind_address()) + .get(&format!("announce?{invalid_query_param}")) + .await; - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + assert_cannot_parse_query_param_error_response(response, "invalid param a=b=c").await; - assert_bad_announce_request_error_response(response, "missing param info_hash").await; + test_env.stop().await; + } - // Without `peer_id` param + #[tokio::test] + async fn should_fail_when_a_mandatory_field_is_missing() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - let mut params = QueryBuilder::default().query().params(); + // Without `info_hash` param - params.peer_id = None; + let mut params = QueryBuilder::default().query().params(); - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + params.info_hash = None; - assert_bad_announce_request_error_response(response, "missing param peer_id").await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - // Without `port` param + assert_bad_announce_request_error_response(response, "missing param info_hash").await; - let mut params = QueryBuilder::default().query().params(); + // Without `peer_id` param - params.port = None; + let mut params = QueryBuilder::default().query().params(); - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + params.peer_id = None; - assert_bad_announce_request_error_response(response, "missing param port").await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - test_env.stop().await; - } + assert_bad_announce_request_error_response(response, "missing param peer_id").await; - #[tokio::test] - async fn should_fail_when_the_info_hash_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + // Without `port` param - let mut params = QueryBuilder::default().query().params(); + let mut params = QueryBuilder::default().query().params(); - for invalid_value in &invalid_info_hashes() { - params.set("info_hash", invalid_value); + params.port = None; let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - assert_cannot_parse_query_params_error_response(response, "").await; - } - - test_env.stop().await; - } + assert_bad_announce_request_error_response(response, "missing param port").await; - #[tokio::test] - async fn should_not_fail_when_the_peer_address_param_is_invalid() { - // AnnounceQuery does not even contain the `peer_addr` - // The peer IP is obtained in two ways: - // 1. If tracker is NOT running `on_reverse_proxy` from the remote client IP. - // 2. If tracker is running `on_reverse_proxy` from `X-Forwarded-For` request HTTP header. + test_env.stop().await; + } - let test_env = running_test_environment::(configuration::ephemeral()).await; + #[tokio::test] + async fn should_fail_when_the_info_hash_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - let mut params = QueryBuilder::default().query().params(); + let mut params = QueryBuilder::default().query().params(); - params.peer_addr = Some("INVALID-IP-ADDRESS".to_string()); + for invalid_value in &invalid_info_hashes() { + params.set("info_hash", invalid_value); - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - assert_is_announce_response(response).await; + assert_cannot_parse_query_params_error_response(response, "").await; + } - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_fail_when_the_downloaded_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + #[tokio::test] + async fn should_not_fail_when_the_peer_address_param_is_invalid() { + // AnnounceQuery does not even contain the `peer_addr` + // The peer IP is obtained in two ways: + // 1. If tracker is NOT running `on_reverse_proxy` from the remote client IP. + // 2. If tracker is running `on_reverse_proxy` from `X-Forwarded-For` request HTTP header. - let mut params = QueryBuilder::default().query().params(); + let test_env = running_test_environment::(configuration::ephemeral()).await; - let invalid_values = ["-1", "1.1", "a"]; + let mut params = QueryBuilder::default().query().params(); - for invalid_value in invalid_values { - params.set("downloaded", invalid_value); + params.peer_addr = Some("INVALID-IP-ADDRESS".to_string()); let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - assert_bad_announce_request_error_response(response, "invalid param value").await; + assert_is_announce_response(response).await; + + test_env.stop().await; } - test_env.stop().await; - } + #[tokio::test] + async fn should_fail_when_the_downloaded_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - #[tokio::test] - async fn should_fail_when_the_uploaded_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + let mut params = QueryBuilder::default().query().params(); - let mut params = QueryBuilder::default().query().params(); + let invalid_values = ["-1", "1.1", "a"]; - let invalid_values = ["-1", "1.1", "a"]; + for invalid_value in invalid_values { + params.set("downloaded", invalid_value); - for invalid_value in invalid_values { - params.set("uploaded", invalid_value); + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + assert_bad_announce_request_error_response(response, "invalid param value").await; + } - assert_bad_announce_request_error_response(response, "invalid param value").await; + test_env.stop().await; } - test_env.stop().await; - } + #[tokio::test] + async fn should_fail_when_the_uploaded_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - #[tokio::test] - async fn should_fail_when_the_peer_id_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + let mut params = QueryBuilder::default().query().params(); - let mut params = QueryBuilder::default().query().params(); + let invalid_values = ["-1", "1.1", "a"]; - let invalid_values = [ - "0", - "-1", - "1.1", - "a", - "-qB0000000000000000", // 19 bytes - "-qB000000000000000000", // 21 bytes - ]; + for invalid_value in invalid_values { + params.set("uploaded", invalid_value); - for invalid_value in invalid_values { - params.set("peer_id", invalid_value); + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + assert_bad_announce_request_error_response(response, "invalid param value").await; + } - assert_bad_announce_request_error_response(response, "invalid param value").await; + test_env.stop().await; } - test_env.stop().await; - } + #[tokio::test] + async fn should_fail_when_the_peer_id_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - #[tokio::test] - async fn should_fail_when_the_port_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + let mut params = QueryBuilder::default().query().params(); - let mut params = QueryBuilder::default().query().params(); + let invalid_values = [ + "0", + "-1", + "1.1", + "a", + "-qB0000000000000000", // 19 bytes + "-qB000000000000000000", // 21 bytes + ]; - let invalid_values = ["-1", "1.1", "a"]; + for invalid_value in invalid_values { + params.set("peer_id", invalid_value); - for invalid_value in invalid_values { - params.set("port", invalid_value); + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + assert_bad_announce_request_error_response(response, "invalid param value").await; + } - assert_bad_announce_request_error_response(response, "invalid param value").await; + test_env.stop().await; } - test_env.stop().await; - } + #[tokio::test] + async fn should_fail_when_the_port_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - #[tokio::test] - async fn should_fail_when_the_left_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + let mut params = QueryBuilder::default().query().params(); - let mut params = QueryBuilder::default().query().params(); + let invalid_values = ["-1", "1.1", "a"]; - let invalid_values = ["-1", "1.1", "a"]; + for invalid_value in invalid_values { + params.set("port", invalid_value); - for invalid_value in invalid_values { - params.set("left", invalid_value); + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + assert_bad_announce_request_error_response(response, "invalid param value").await; + } - assert_bad_announce_request_error_response(response, "invalid param value").await; + test_env.stop().await; } - test_env.stop().await; - } + #[tokio::test] + async fn should_fail_when_the_left_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - #[tokio::test] - async fn should_fail_when_the_event_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + let mut params = QueryBuilder::default().query().params(); - let mut params = QueryBuilder::default().query().params(); + let invalid_values = ["-1", "1.1", "a"]; - let invalid_values = [ - "0", - "-1", - "1.1", - "a", - "Started", // It should be lowercase to be valid: `started` - "Stopped", // It should be lowercase to be valid: `stopped` - "Completed", // It should be lowercase to be valid: `completed` - ]; + for invalid_value in invalid_values { + params.set("left", invalid_value); - for invalid_value in invalid_values { - params.set("event", invalid_value); + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + assert_bad_announce_request_error_response(response, "invalid param value").await; + } - assert_bad_announce_request_error_response(response, "invalid param value").await; + test_env.stop().await; } - test_env.stop().await; - } + #[tokio::test] + async fn should_fail_when_the_event_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - #[tokio::test] - async fn should_fail_when_the_compact_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral()).await; + let mut params = QueryBuilder::default().query().params(); - let mut params = QueryBuilder::default().query().params(); + let invalid_values = [ + "0", + "-1", + "1.1", + "a", + "Started", // It should be lowercase to be valid: `started` + "Stopped", // It should be lowercase to be valid: `stopped` + "Completed", // It should be lowercase to be valid: `completed` + ]; - let invalid_values = ["-1", "1.1", "a"]; + for invalid_value in invalid_values { + params.set("event", invalid_value); - for invalid_value in invalid_values { - params.set("compact", invalid_value); + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + assert_bad_announce_request_error_response(response, "invalid param value").await; + } - assert_bad_announce_request_error_response(response, "invalid param value").await; + test_env.stop().await; } - test_env.stop().await; - } - - #[tokio::test] - async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + #[tokio::test] + async fn should_fail_when_the_compact_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral()).await; - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap()) - .query(), - ) - .await; + let mut params = QueryBuilder::default().query().params(); - assert_announce_response( - response, - &Announce { - complete: 1, // the peer for this test - incomplete: 0, - interval: test_env.tracker.config.announce_interval, - min_interval: test_env.tracker.config.min_announce_interval, - peers: vec![], - }, - ) - .await; - - test_env.stop().await; - } + let invalid_values = ["-1", "1.1", "a"]; - #[tokio::test] - async fn should_return_the_list_of_previously_announced_peers() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + for invalid_value in invalid_values { + params.set("compact", invalid_value); - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - // Peer 1 - let previously_announced_peer = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .build(); + assert_bad_announce_request_error_response(response, "invalid param value").await; + } - // Add the Peer 1 - test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; + test_env.stop().await; + } - // Announce the new Peer 2. This new peer is non included on the response peer list - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .query(), + #[tokio::test] + async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let response = Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_info_hash(&InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap()) + .query(), + ) + .await; + + assert_announce_response( + response, + &Announce { + complete: 1, // the peer for this test + incomplete: 0, + interval: test_env.tracker.config.announce_interval, + min_interval: test_env.tracker.config.min_announce_interval, + peers: vec![], + }, ) .await; - // It should only contain the previously announced peer - assert_announce_response( - response, - &Announce { - complete: 2, - incomplete: 0, - interval: test_env.tracker.config.announce_interval, - min_interval: test_env.tracker.config.min_announce_interval, - peers: vec![DictionaryPeer::from(previously_announced_peer)], - }, - ) - .await; - - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_return_the_list_of_previously_announced_peers_including_peers_using_ipv4_and_ipv6() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - // Announce a peer using IPV4 - let peer_using_ipv4 = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), 8080)) - .build(); - test_env.add_torrent_peer(&info_hash, &peer_using_ipv4).await; - - // Announce a peer using IPV6 - let peer_using_ipv6 = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .with_peer_addr(&SocketAddr::new( - IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)), - 8080, - )) - .build(); - test_env.add_torrent_peer(&info_hash, &peer_using_ipv6).await; - - // Announce the new Peer. - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000003")) - .query(), + #[tokio::test] + async fn should_return_the_list_of_previously_announced_peers() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + // Peer 1 + let previously_announced_peer = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .build(); + + // Add the Peer 1 + test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; + + // Announce the new Peer 2. This new peer is non included on the response peer list + let response = Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .query(), + ) + .await; + + // It should only contain the previously announced peer + assert_announce_response( + response, + &Announce { + complete: 2, + incomplete: 0, + interval: test_env.tracker.config.announce_interval, + min_interval: test_env.tracker.config.min_announce_interval, + peers: vec![DictionaryPeer::from(previously_announced_peer)], + }, ) .await; - // The newly announced peer is not included on the response peer list, - // but all the previously announced peers should be included regardless the IP version they are using. - assert_announce_response( - response, - &Announce { - complete: 3, - incomplete: 0, - interval: test_env.tracker.config.announce_interval, - min_interval: test_env.tracker.config.min_announce_interval, - peers: vec![DictionaryPeer::from(peer_using_ipv4), DictionaryPeer::from(peer_using_ipv6)], - }, - ) - .await; - - test_env.stop().await; - } - - #[tokio::test] - async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_peer_id_even_if_the_ip_is_different() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let peer = PeerBuilder::default().build(); - - // Add a peer - test_env.add_torrent_peer(&info_hash, &peer).await; - - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer.peer_id) - .query(); + test_env.stop().await; + } - assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr); + #[tokio::test] + async fn should_return_the_list_of_previously_announced_peers_including_peers_using_ipv4_and_ipv6() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + // Announce a peer using IPV4 + let peer_using_ipv4 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), 8080)) + .build(); + test_env.add_torrent_peer(&info_hash, &peer_using_ipv4).await; + + // Announce a peer using IPV6 + let peer_using_ipv6 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .with_peer_addr(&SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)), + 8080, + )) + .build(); + test_env.add_torrent_peer(&info_hash, &peer_using_ipv6).await; + + // Announce the new Peer. + let response = Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer::Id(*b"-qB00000000000000003")) + .query(), + ) + .await; + + // The newly announced peer is not included on the response peer list, + // but all the previously announced peers should be included regardless the IP version they are using. + assert_announce_response( + response, + &Announce { + complete: 3, + incomplete: 0, + interval: test_env.tracker.config.announce_interval, + min_interval: test_env.tracker.config.min_announce_interval, + peers: vec![DictionaryPeer::from(peer_using_ipv4), DictionaryPeer::from(peer_using_ipv6)], + }, + ) + .await; - let response = Client::new(*test_env.bind_address()).announce(&announce_query).await; + test_env.stop().await; + } - assert_empty_announce_response(response).await; + #[tokio::test] + async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_peer_id_even_if_the_ip_is_different() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - test_env.stop().await; - } + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let peer = PeerBuilder::default().build(); - #[tokio::test] - async fn should_return_the_compact_response() { - // Tracker Returns Compact Peer Lists - // https://www.bittorrent.org/beps/bep_0023.html + // Add a peer + test_env.add_torrent_peer(&info_hash, &peer).await; - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + let announce_query = QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer.peer_id) + .query(); - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + assert_ne!(peer.peer_addr.ip(), announce_query.peer_addr); - // Peer 1 - let previously_announced_peer = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .build(); + let response = Client::new(*test_env.bind_address()).announce(&announce_query).await; - // Add the Peer 1 - test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; + assert_empty_announce_response(response).await; - // Announce the new Peer 2 accepting compact responses - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .with_compact(Compact::Accepted) - .query(), - ) - .await; + test_env.stop().await; + } - let expected_response = responses::announce::Compact { - complete: 2, - incomplete: 0, - interval: 120, - min_interval: 120, - peers: CompactPeerList::new([CompactPeer::new(&previously_announced_peer.peer_addr)].to_vec()), - }; + #[tokio::test] + async fn should_return_the_compact_response() { + // Tracker Returns Compact Peer Lists + // https://www.bittorrent.org/beps/bep_0023.html - assert_compact_announce_response(response, &expected_response).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - test_env.stop().await; - } + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - #[tokio::test] - async fn should_not_return_the_compact_response_by_default() { - // code-review: the HTTP tracker does not return the compact response by default if the "compact" - // param is not provided in the announce URL. The BEP 23 suggest to do so. + // Peer 1 + let previously_announced_peer = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .build(); - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + // Add the Peer 1 + test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + // Announce the new Peer 2 accepting compact responses + let response = Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .with_compact(Compact::Accepted) + .query(), + ) + .await; - // Peer 1 - let previously_announced_peer = PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .build(); + let expected_response = responses::announce::Compact { + complete: 2, + incomplete: 0, + interval: 120, + min_interval: 120, + peers: CompactPeerList::new([CompactPeer::new(&previously_announced_peer.peer_addr)].to_vec()), + }; - // Add the Peer 1 - test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; + assert_compact_announce_response(response, &expected_response).await; - // Announce the new Peer 2 without passing the "compact" param - // By default it should respond with the compact peer list - // https://www.bittorrent.org/beps/bep_0023.html - let response = Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_id(&peer::Id(*b"-qB00000000000000002")) - .without_compact() - .query(), - ) - .await; + test_env.stop().await; + } - assert!(!is_a_compact_announce_response(response).await); + #[tokio::test] + async fn should_not_return_the_compact_response_by_default() { + // code-review: the HTTP tracker does not return the compact response by default if the "compact" + // param is not provided in the announce URL. The BEP 23 suggest to do so. - test_env.stop().await; - } + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - async fn is_a_compact_announce_response(response: Response) -> bool { - let bytes = response.bytes().await.unwrap(); - let compact_announce = serde_bencode::from_bytes::(&bytes); - compact_announce.is_ok() - } + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - #[tokio::test] - async fn should_increase_the_number_of_tcp4_connections_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + // Peer 1 + let previously_announced_peer = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .build(); - Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().query()) - .await; + // Add the Peer 1 + test_env.add_torrent_peer(&info_hash, &previously_announced_peer).await; - let stats = test_env.tracker.get_stats().await; + // Announce the new Peer 2 without passing the "compact" param + // By default it should respond with the compact peer list + // https://www.bittorrent.org/beps/bep_0023.html + let response = Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .without_compact() + .query(), + ) + .await; - assert_eq!(stats.tcp4_connections_handled, 1); + assert!(!is_a_compact_announce_response(response).await); - drop(stats); + test_env.stop().await; + } - test_env.stop().await; - } + async fn is_a_compact_announce_response(response: Response) -> bool { + let bytes = response.bytes().await.unwrap(); + let compact_announce = serde_bencode::from_bytes::(&bytes); + compact_announce.is_ok() + } - #[tokio::test] - async fn should_increase_the_number_of_tcp6_connections_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; + #[tokio::test] + async fn should_increase_the_number_of_tcp4_connections_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) - .announce(&QueryBuilder::default().query()) - .await; + Client::new(*test_env.bind_address()) + .announce(&QueryBuilder::default().query()) + .await; - let stats = test_env.tracker.get_stats().await; + let stats = test_env.tracker.get_stats().await; - assert_eq!(stats.tcp6_connections_handled, 1); + assert_eq!(stats.tcp4_connections_handled, 1); - drop(stats); + drop(stats); - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_not_increase_the_number_of_tcp6_connections_handled_if_the_client_is_not_using_an_ipv6_ip() { - // The tracker ignores the peer address in the request param. It uses the client remote ip address. + #[tokio::test] + async fn should_increase_the_number_of_tcp6_connections_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) + .announce(&QueryBuilder::default().query()) + .await; - Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) - .query(), - ) - .await; + let stats = test_env.tracker.get_stats().await; - let stats = test_env.tracker.get_stats().await; + assert_eq!(stats.tcp6_connections_handled, 1); - assert_eq!(stats.tcp6_connections_handled, 0); + drop(stats); - drop(stats); + test_env.stop().await; + } - test_env.stop().await; - } + #[tokio::test] + async fn should_not_increase_the_number_of_tcp6_connections_handled_if_the_client_is_not_using_an_ipv6_ip() { + // The tracker ignores the peer address in the request param. It uses the client remote ip address. - #[tokio::test] - async fn should_increase_the_number_of_tcp4_announce_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().query()) - .await; + Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) + .query(), + ) + .await; - let stats = test_env.tracker.get_stats().await; + let stats = test_env.tracker.get_stats().await; - assert_eq!(stats.tcp4_announces_handled, 1); + assert_eq!(stats.tcp6_connections_handled, 0); - drop(stats); + drop(stats); - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_increase_the_number_of_tcp6_announce_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; + #[tokio::test] + async fn should_increase_the_number_of_tcp4_announce_requests_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) - .announce(&QueryBuilder::default().query()) - .await; + Client::new(*test_env.bind_address()) + .announce(&QueryBuilder::default().query()) + .await; - let stats = test_env.tracker.get_stats().await; + let stats = test_env.tracker.get_stats().await; - assert_eq!(stats.tcp6_announces_handled, 1); + assert_eq!(stats.tcp4_announces_handled, 1); - drop(stats); + drop(stats); - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_not_increase_the_number_of_tcp6_announce_requests_handled_if_the_client_is_not_using_an_ipv6_ip() { - // The tracker ignores the peer address in the request param. It uses the client remote ip address. + #[tokio::test] + async fn should_increase_the_number_of_tcp6_announce_requests_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) + .announce(&QueryBuilder::default().query()) + .await; - Client::new(*test_env.bind_address()) - .announce( - &QueryBuilder::default() - .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) - .query(), - ) - .await; + let stats = test_env.tracker.get_stats().await; - let stats = test_env.tracker.get_stats().await; + assert_eq!(stats.tcp6_announces_handled, 1); - assert_eq!(stats.tcp6_announces_handled, 0); + drop(stats); - drop(stats); + test_env.stop().await; + } - test_env.stop().await; - } + #[tokio::test] + async fn should_not_increase_the_number_of_tcp6_announce_requests_handled_if_the_client_is_not_using_an_ipv6_ip() + { + // The tracker ignores the peer address in the request param. It uses the client remote ip address. - #[tokio::test] - async fn should_assign_to_the_peer_ip_the_remote_client_ip_instead_of_the_peer_address_in_the_request_param() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let client_ip = local_ip().unwrap(); + Client::new(*test_env.bind_address()) + .announce( + &QueryBuilder::default() + .with_peer_addr(&IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))) + .query(), + ) + .await; - let client = Client::bind(*test_env.bind_address(), client_ip); + let stats = test_env.tracker.get_stats().await; - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); + assert_eq!(stats.tcp6_announces_handled, 0); - client.announce(&announce_query).await; + drop(stats); - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; + test_env.stop().await; + } - assert_eq!(peer_addr.ip(), client_ip); - assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); + #[tokio::test] + async fn should_assign_to_the_peer_ip_the_remote_client_ip_instead_of_the_peer_address_in_the_request_param() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - test_env.stop().await; - } + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let client_ip = local_ip().unwrap(); - #[tokio::test] - async fn when_the_client_ip_is_a_loopback_ipv4_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration( - ) { - /* We assume that both the client and tracker share the same public IP. + let client = Client::bind(*test_env.bind_address(), client_ip); - client <-> tracker <-> Internet - 127.0.0.1 external_ip = "2.137.87.41" - */ + let announce_query = QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) + .query(); - let test_env = running_test_environment::(configuration::ephemeral_with_external_ip( - IpAddr::from_str("2.137.87.41").unwrap(), - )) - .await; + client.announce(&announce_query).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); - let client_ip = loopback_ip; + let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; + let peer_addr = peers[0].peer_addr; - let client = Client::bind(*test_env.bind_address(), client_ip); + assert_eq!(peer_addr.ip(), client_ip); + assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); + test_env.stop().await; + } - client.announce(&announce_query).await; + #[tokio::test] + async fn when_the_client_ip_is_a_loopback_ipv4_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration( + ) { + /* We assume that both the client and tracker share the same public IP. - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; + client <-> tracker <-> Internet + 127.0.0.1 external_ip = "2.137.87.41" + */ - assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap()); - assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); + let test_env = running_test_environment::(configuration::ephemeral_with_external_ip( + IpAddr::from_str("2.137.87.41").unwrap(), + )) + .await; - test_env.stop().await; - } + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); + let client_ip = loopback_ip; - #[tokio::test] - async fn when_the_client_ip_is_a_loopback_ipv6_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration( - ) { - /* We assume that both the client and tracker share the same public IP. + let client = Client::bind(*test_env.bind_address(), client_ip); - client <-> tracker <-> Internet - ::1 external_ip = "2345:0425:2CA1:0000:0000:0567:5673:23b5" - */ + let announce_query = QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) + .query(); - let test_env = running_test_environment::(configuration::ephemeral_with_external_ip( - IpAddr::from_str("2345:0425:2CA1:0000:0000:0567:5673:23b5").unwrap(), - )) - .await; + client.announce(&announce_query).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); - let client_ip = loopback_ip; + let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; + let peer_addr = peers[0].peer_addr; - let client = Client::bind(*test_env.bind_address(), client_ip); + assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap()); + assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); - let announce_query = QueryBuilder::default() - .with_info_hash(&info_hash) - .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) - .query(); + test_env.stop().await; + } - client.announce(&announce_query).await; + #[tokio::test] + async fn when_the_client_ip_is_a_loopback_ipv6_it_should_assign_to_the_peer_ip_the_external_ip_in_the_tracker_configuration( + ) { + /* We assume that both the client and tracker share the same public IP. - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; + client <-> tracker <-> Internet + ::1 external_ip = "2345:0425:2CA1:0000:0000:0567:5673:23b5" + */ - assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap()); - assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); + let test_env = running_test_environment::(configuration::ephemeral_with_external_ip( + IpAddr::from_str("2345:0425:2CA1:0000:0000:0567:5673:23b5").unwrap(), + )) + .await; - test_env.stop().await; - } + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let loopback_ip = IpAddr::from_str("127.0.0.1").unwrap(); + let client_ip = loopback_ip; - #[tokio::test] - async fn when_the_tracker_is_behind_a_reverse_proxy_it_should_assign_to_the_peer_ip_the_ip_in_the_x_forwarded_for_http_header( - ) { - /* - client <-> http proxy <-> tracker <-> Internet - ip: header: config: peer addr: - 145.254.214.256 X-Forwarded-For = 145.254.214.256 on_reverse_proxy = true 145.254.214.256 - */ + let client = Client::bind(*test_env.bind_address(), client_ip); - let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; + let announce_query = QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_addr(&IpAddr::from_str("2.2.2.2").unwrap()) + .query(); - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + client.announce(&announce_query).await; - let client = Client::new(*test_env.bind_address()); + let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; + let peer_addr = peers[0].peer_addr; - let announce_query = QueryBuilder::default().with_info_hash(&info_hash).query(); + assert_eq!(peer_addr.ip(), test_env.tracker.config.get_ext_ip().unwrap()); + assert_ne!(peer_addr.ip(), IpAddr::from_str("2.2.2.2").unwrap()); - client - .announce_with_header( - &announce_query, - "X-Forwarded-For", - "203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178", - ) - .await; + test_env.stop().await; + } - let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; - let peer_addr = peers[0].peer_addr; + #[tokio::test] + async fn when_the_tracker_is_behind_a_reverse_proxy_it_should_assign_to_the_peer_ip_the_ip_in_the_x_forwarded_for_http_header( + ) { + /* + client <-> http proxy <-> tracker <-> Internet + ip: header: config: peer addr: + 145.254.214.256 X-Forwarded-For = 145.254.214.256 on_reverse_proxy = true 145.254.214.256 + */ - assert_eq!(peer_addr.ip(), IpAddr::from_str("150.172.238.178").unwrap()); + let test_env = running_test_environment::(configuration::ephemeral_with_reverse_proxy()).await; - test_env.stop().await; - } - } + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - mod receiving_an_scrape_request { - - // Scrape documentation: - // - // BEP 48. Tracker Protocol Extension: Scrape - // https://www.bittorrent.org/beps/bep_0048.html - // - // Vuze (bittorrent client) docs: - // https://wiki.vuze.com/w/Scrape - - use std::net::IpAddr; - use std::str::FromStr; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::{invalid_info_hashes, PeerBuilder}; - use crate::http::asserts::{ - assert_cannot_parse_query_params_error_response, assert_missing_query_params_for_scrape_request_error_response, - assert_scrape_response, - }; - use crate::http::client::Client; - use crate::http::requests; - use crate::http::requests::scrape::QueryBuilder; - use crate::http::responses::scrape::{self, File, ResponseBuilder}; - use crate::http::test_environment::running_test_environment; - use crate::Axum; - - //#[tokio::test] - #[allow(dead_code)] - async fn should_fail_when_the_request_is_empty() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - let response = Client::new(*test_env.bind_address()).get("scrape").await; - - assert_missing_query_params_for_scrape_request_error_response(response).await; - - test_env.stop().await; - } + let client = Client::new(*test_env.bind_address()); - #[tokio::test] - async fn should_fail_when_the_info_hash_param_is_invalid() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + let announce_query = QueryBuilder::default().with_info_hash(&info_hash).query(); - let mut params = QueryBuilder::default().query().params(); + client + .announce_with_header( + &announce_query, + "X-Forwarded-For", + "203.0.113.195,2001:db8:85a3:8d3:1319:8a2e:370:7348,150.172.238.178", + ) + .await; - for invalid_value in &invalid_info_hashes() { - params.set_one_info_hash_param(invalid_value); + let peers = test_env.tracker.get_all_torrent_peers(&info_hash).await; + let peer_addr = peers[0].peer_addr; - let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; + assert_eq!(peer_addr.ip(), IpAddr::from_str("150.172.238.178").unwrap()); - assert_cannot_parse_query_params_error_response(response, "").await; + test_env.stop().await; } - - test_env.stop().await; } - #[tokio::test] - async fn should_return_the_file_with_the_incomplete_peer_when_there_is_one_peer_with_bytes_pending_to_download() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + mod receiving_an_scrape_request { - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + // Scrape documentation: + // + // BEP 48. Tracker Protocol Extension: Scrape + // https://www.bittorrent.org/beps/bep_0048.html + // + // Vuze (bittorrent client) docs: + // https://wiki.vuze.com/w/Scrape - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; + use std::net::IpAddr; + use std::str::FromStr; - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::peer; + use torrust_tracker_test_helpers::configuration; - assert_scrape_response(response, &expected_scrape_response).await; + use crate::common::fixtures::{invalid_info_hashes, PeerBuilder}; + use crate::http::asserts::{ + assert_cannot_parse_query_params_error_response, + assert_missing_query_params_for_scrape_request_error_response, assert_scrape_response, + }; + use crate::http::client::Client; + use crate::http::requests; + use crate::http::requests::scrape::QueryBuilder; + use crate::http::responses::scrape::{self, File, ResponseBuilder}; + use crate::http::test_environment::running_test_environment; + use crate::V1; + + //#[tokio::test] + #[allow(dead_code)] + async fn should_fail_when_the_request_is_empty() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + let response = Client::new(*test_env.bind_address()).get("scrape").await; + + assert_missing_query_params_for_scrape_request_error_response(response).await; + + test_env.stop().await; + } - test_env.stop().await; - } + #[tokio::test] + async fn should_fail_when_the_info_hash_param_is_invalid() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - #[tokio::test] - async fn should_return_the_file_with_the_complete_peer_when_there_is_one_peer_with_no_bytes_pending_to_download() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + let mut params = QueryBuilder::default().query().params(); - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + for invalid_value in &invalid_info_hashes() { + params.set_one_info_hash_param(invalid_value); - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_no_bytes_pending_to_download() - .build(), - ) - .await; + let response = Client::new(*test_env.bind_address()).get(&format!("announce?{params}")).await; - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; + assert_cannot_parse_query_params_error_response(response, "").await; + } - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 1, - downloaded: 0, - incomplete: 0, - }, - ) - .build(); + test_env.stop().await; + } - assert_scrape_response(response, &expected_scrape_response).await; + #[tokio::test] + async fn should_return_the_file_with_the_incomplete_peer_when_there_is_one_peer_with_bytes_pending_to_download() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; + + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default() + .add_file( + info_hash.bytes(), + File { + complete: 0, + downloaded: 0, + incomplete: 1, + }, + ) + .build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } - test_env.stop().await; - } + #[tokio::test] + async fn should_return_the_file_with_the_complete_peer_when_there_is_one_peer_with_no_bytes_pending_to_download() + { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_no_bytes_pending_to_download() + .build(), + ) + .await; + + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default() + .add_file( + info_hash.bytes(), + File { + complete: 1, + downloaded: 0, + incomplete: 0, + }, + ) + .build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } - #[tokio::test] - async fn should_return_a_file_with_zeroed_values_when_there_are_no_peers() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + #[tokio::test] + async fn should_return_a_file_with_zeroed_values_when_there_are_no_peers() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; - assert_scrape_response(response, &scrape::Response::with_one_file(info_hash.bytes(), File::zeroed())).await; + assert_scrape_response(response, &scrape::Response::with_one_file(info_hash.bytes(), File::zeroed())).await; - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_accept_multiple_infohashes() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + #[tokio::test] + async fn should_accept_multiple_infohashes() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - let info_hash1 = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let info_hash2 = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(); + let info_hash1 = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash2 = InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(); - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .add_info_hash(&info_hash1) - .add_info_hash(&info_hash2) - .query(), - ) - .await; + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .add_info_hash(&info_hash1) + .add_info_hash(&info_hash2) + .query(), + ) + .await; - let expected_scrape_response = ResponseBuilder::default() - .add_file(info_hash1.bytes(), File::zeroed()) - .add_file(info_hash2.bytes(), File::zeroed()) - .build(); + let expected_scrape_response = ResponseBuilder::default() + .add_file(info_hash1.bytes(), File::zeroed()) + .add_file(info_hash2.bytes(), File::zeroed()) + .build(); - assert_scrape_response(response, &expected_scrape_response).await; + assert_scrape_response(response, &expected_scrape_response).await; - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_increase_the_number_ot_tcp4_scrape_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; + #[tokio::test] + async fn should_increase_the_number_ot_tcp4_scrape_requests_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_mode_public()).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; + Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; - let stats = test_env.tracker.get_stats().await; + let stats = test_env.tracker.get_stats().await; - assert_eq!(stats.tcp4_scrapes_handled, 1); + assert_eq!(stats.tcp4_scrapes_handled, 1); - drop(stats); + drop(stats); - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_increase_the_number_ot_tcp6_scrape_requests_handled_in_statistics() { - let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; + #[tokio::test] + async fn should_increase_the_number_ot_tcp6_scrape_requests_handled_in_statistics() { + let test_env = running_test_environment::(configuration::ephemeral_ipv6()).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; + Client::bind(*test_env.bind_address(), IpAddr::from_str("::1").unwrap()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; - let stats = test_env.tracker.get_stats().await; + let stats = test_env.tracker.get_stats().await; - assert_eq!(stats.tcp6_scrapes_handled, 1); + assert_eq!(stats.tcp6_scrapes_handled, 1); - drop(stats); + drop(stats); - test_env.stop().await; + test_env.stop().await; + } } } - } - mod configured_as_whitelisted { + mod configured_as_whitelisted { - mod and_receiving_an_announce_request { - use std::str::FromStr; + mod and_receiving_an_announce_request { + use std::str::FromStr; - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker_test_helpers::configuration; + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker_test_helpers::configuration; - use crate::http::asserts::{assert_is_announce_response, assert_torrent_not_in_whitelist_error_response}; - use crate::http::client::Client; - use crate::http::requests::announce::QueryBuilder; - use crate::http::test_environment::running_test_environment; - use crate::Axum; + use crate::http::asserts::{assert_is_announce_response, assert_torrent_not_in_whitelist_error_response}; + use crate::http::client::Client; + use crate::http::requests::announce::QueryBuilder; + use crate::http::test_environment::running_test_environment; + use crate::V1; - #[tokio::test] - async fn should_fail_if_the_torrent_is_not_in_the_whitelist() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; + #[tokio::test] + async fn should_fail_if_the_torrent_is_not_in_the_whitelist() { + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) - .await; + let response = Client::new(*test_env.bind_address()) + .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) + .await; - assert_torrent_not_in_whitelist_error_response(response).await; + assert_torrent_not_in_whitelist_error_response(response).await; - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_allow_announcing_a_whitelisted_torrent() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; + #[tokio::test] + async fn should_allow_announcing_a_whitelisted_torrent() { + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - test_env - .tracker - .add_torrent_to_whitelist(&info_hash) - .await - .expect("should add the torrent to the whitelist"); + test_env + .tracker + .add_torrent_to_whitelist(&info_hash) + .await + .expect("should add the torrent to the whitelist"); - let response = Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) - .await; + let response = Client::new(*test_env.bind_address()) + .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) + .await; - assert_is_announce_response(response).await; + assert_is_announce_response(response).await; - test_env.stop().await; + test_env.stop().await; + } } - } - mod receiving_an_scrape_request { - use std::str::FromStr; - - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; - - use crate::common::fixtures::PeerBuilder; - use crate::http::asserts::assert_scrape_response; - use crate::http::client::Client; - use crate::http::requests; - use crate::http::responses::scrape::{File, ResponseBuilder}; - use crate::http::test_environment::running_test_environment; - use crate::Axum; - - #[tokio::test] - async fn should_return_the_zeroed_file_when_the_requested_file_is_not_whitelisted() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; + mod receiving_an_scrape_request { + use std::str::FromStr; - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::peer; + use torrust_tracker_test_helpers::configuration; - assert_scrape_response(response, &expected_scrape_response).await; + use crate::common::fixtures::PeerBuilder; + use crate::http::asserts::assert_scrape_response; + use crate::http::client::Client; + use crate::http::requests; + use crate::http::responses::scrape::{File, ResponseBuilder}; + use crate::http::test_environment::running_test_environment; + use crate::V1; - test_env.stop().await; - } + #[tokio::test] + async fn should_return_the_zeroed_file_when_the_requested_file_is_not_whitelisted() { + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; - #[tokio::test] - async fn should_return_the_file_stats_when_the_requested_file_is_whitelisted() { - let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; - test_env - .tracker - .add_torrent_to_whitelist(&info_hash) - .await - .expect("should add the torrent to the whitelist"); - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; + let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); + assert_scrape_response(response, &expected_scrape_response).await; - assert_scrape_response(response, &expected_scrape_response).await; + test_env.stop().await; + } - test_env.stop().await; + #[tokio::test] + async fn should_return_the_file_stats_when_the_requested_file_is_whitelisted() { + let test_env = running_test_environment::(configuration::ephemeral_mode_whitelisted()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; + + test_env + .tracker + .add_torrent_to_whitelist(&info_hash) + .await + .expect("should add the torrent to the whitelist"); + + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default() + .add_file( + info_hash.bytes(), + File { + complete: 0, + downloaded: 0, + incomplete: 1, + }, + ) + .build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } } } - } - mod configured_as_private { + mod configured_as_private { - mod and_receiving_an_announce_request { - use std::str::FromStr; - use std::time::Duration; + mod and_receiving_an_announce_request { + use std::str::FromStr; + use std::time::Duration; - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::auth::Key; - use torrust_tracker_test_helpers::configuration; + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::auth::Key; + use torrust_tracker_test_helpers::configuration; - use crate::http::asserts::{assert_authentication_error_response, assert_is_announce_response}; - use crate::http::client::Client; - use crate::http::requests::announce::QueryBuilder; - use crate::http::test_environment::running_test_environment; - use crate::Axum; + use crate::http::asserts::{assert_authentication_error_response, assert_is_announce_response}; + use crate::http::client::Client; + use crate::http::requests::announce::QueryBuilder; + use crate::http::test_environment::running_test_environment; + use crate::V1; - #[tokio::test] - async fn should_respond_to_authenticated_peers() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + #[tokio::test] + async fn should_respond_to_authenticated_peers() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); + let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); - let response = Client::authenticated(*test_env.bind_address(), key.id()) - .announce(&QueryBuilder::default().query()) - .await; + let response = Client::authenticated(*test_env.bind_address(), key.id()) + .announce(&QueryBuilder::default().query()) + .await; - assert_is_announce_response(response).await; + assert_is_announce_response(response).await; - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_fail_if_the_peer_has_not_provided_the_authentication_key() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + #[tokio::test] + async fn should_fail_if_the_peer_has_not_provided_the_authentication_key() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - let response = Client::new(*test_env.bind_address()) - .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) - .await; + let response = Client::new(*test_env.bind_address()) + .announce(&QueryBuilder::default().with_info_hash(&info_hash).query()) + .await; - assert_authentication_error_response(response).await; + assert_authentication_error_response(response).await; - test_env.stop().await; - } + test_env.stop().await; + } - #[tokio::test] - async fn should_fail_if_the_key_query_param_cannot_be_parsed() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + #[tokio::test] + async fn should_fail_if_the_key_query_param_cannot_be_parsed() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - let invalid_key = "INVALID_KEY"; + let invalid_key = "INVALID_KEY"; - let response = Client::new(*test_env.bind_address()) + let response = Client::new(*test_env.bind_address()) .get(&format!( "announce/{invalid_key}?info_hash=%81%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port=17548&left=0&event=completed&compact=0" )) .await; - assert_authentication_error_response(response).await; - } + assert_authentication_error_response(response).await; + } - #[tokio::test] - async fn should_fail_if_the_peer_cannot_be_authenticated_with_the_provided_key() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + #[tokio::test] + async fn should_fail_if_the_peer_cannot_be_authenticated_with_the_provided_key() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - // The tracker does not have this key - let unregistered_key = Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap(); + // The tracker does not have this key + let unregistered_key = Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap(); - let response = Client::authenticated(*test_env.bind_address(), unregistered_key) - .announce(&QueryBuilder::default().query()) - .await; + let response = Client::authenticated(*test_env.bind_address(), unregistered_key) + .announce(&QueryBuilder::default().query()) + .await; - assert_authentication_error_response(response).await; + assert_authentication_error_response(response).await; - test_env.stop().await; + test_env.stop().await; + } } - } - - mod receiving_an_scrape_request { - use std::str::FromStr; - use std::time::Duration; + mod receiving_an_scrape_request { - use torrust_tracker::protocol::info_hash::InfoHash; - use torrust_tracker::tracker::auth::Key; - use torrust_tracker::tracker::peer; - use torrust_tracker_test_helpers::configuration; + use std::str::FromStr; + use std::time::Duration; - use crate::common::fixtures::PeerBuilder; - use crate::http::asserts::{assert_authentication_error_response, assert_scrape_response}; - use crate::http::client::Client; - use crate::http::requests; - use crate::http::responses::scrape::{File, ResponseBuilder}; - use crate::http::test_environment::running_test_environment; - use crate::Axum; + use torrust_tracker::protocol::info_hash::InfoHash; + use torrust_tracker::tracker::auth::Key; + use torrust_tracker::tracker::peer; + use torrust_tracker_test_helpers::configuration; - #[tokio::test] - async fn should_fail_if_the_key_query_param_cannot_be_parsed() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + use crate::common::fixtures::PeerBuilder; + use crate::http::asserts::{assert_authentication_error_response, assert_scrape_response}; + use crate::http::client::Client; + use crate::http::requests; + use crate::http::responses::scrape::{File, ResponseBuilder}; + use crate::http::test_environment::running_test_environment; + use crate::V1; - let invalid_key = "INVALID_KEY"; + #[tokio::test] + async fn should_fail_if_the_key_query_param_cannot_be_parsed() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - let response = Client::new(*test_env.bind_address()) - .get(&format!( - "scrape/{invalid_key}?info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0" - )) - .await; - - assert_authentication_error_response(response).await; - } - - #[tokio::test] - async fn should_return_the_zeroed_file_when_the_client_is_not_authenticated() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; - - let response = Client::new(*test_env.bind_address()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; - - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); + let invalid_key = "INVALID_KEY"; - assert_scrape_response(response, &expected_scrape_response).await; + let response = Client::new(*test_env.bind_address()) + .get(&format!( + "scrape/{invalid_key}?info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0" + )) + .await; - test_env.stop().await; - } + assert_authentication_error_response(response).await; + } - #[tokio::test] - async fn should_return_the_real_file_stats_when_the_client_is_authenticated() { - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + #[tokio::test] + async fn should_return_the_zeroed_file_when_the_client_is_not_authenticated() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; - let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); + let response = Client::new(*test_env.bind_address()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; - let response = Client::authenticated(*test_env.bind_address(), key.id()) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; + let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); - let expected_scrape_response = ResponseBuilder::default() - .add_file( - info_hash.bytes(), - File { - complete: 0, - downloaded: 0, - incomplete: 1, - }, - ) - .build(); + assert_scrape_response(response, &expected_scrape_response).await; - assert_scrape_response(response, &expected_scrape_response).await; + test_env.stop().await; + } - test_env.stop().await; - } + #[tokio::test] + async fn should_return_the_real_file_stats_when_the_client_is_authenticated() { + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; + + let key = test_env.tracker.generate_auth_key(Duration::from_secs(60)).await.unwrap(); + + let response = Client::authenticated(*test_env.bind_address(), key.id()) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; + + let expected_scrape_response = ResponseBuilder::default() + .add_file( + info_hash.bytes(), + File { + complete: 0, + downloaded: 0, + incomplete: 1, + }, + ) + .build(); + + assert_scrape_response(response, &expected_scrape_response).await; + + test_env.stop().await; + } - #[tokio::test] - async fn should_return_the_zeroed_file_when_the_authentication_key_provided_by_the_client_is_invalid() { - // There is not authentication error - // code-review: should this really be this way? + #[tokio::test] + async fn should_return_the_zeroed_file_when_the_authentication_key_provided_by_the_client_is_invalid() { + // There is not authentication error + // code-review: should this really be this way? - let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; + let test_env = running_test_environment::(configuration::ephemeral_mode_private()).await; - let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); - test_env - .add_torrent_peer( - &info_hash, - &PeerBuilder::default() - .with_peer_id(&peer::Id(*b"-qB00000000000000001")) - .with_bytes_pending_to_download(1) - .build(), - ) - .await; + test_env + .add_torrent_peer( + &info_hash, + &PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_bytes_pending_to_download(1) + .build(), + ) + .await; - let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap(); + let false_key: Key = "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ".parse().unwrap(); - let response = Client::authenticated(*test_env.bind_address(), false_key) - .scrape( - &requests::scrape::QueryBuilder::default() - .with_one_info_hash(&info_hash) - .query(), - ) - .await; + let response = Client::authenticated(*test_env.bind_address(), false_key) + .scrape( + &requests::scrape::QueryBuilder::default() + .with_one_info_hash(&info_hash) + .query(), + ) + .await; - let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); + let expected_scrape_response = ResponseBuilder::default().add_file(info_hash.bytes(), File::zeroed()).build(); - assert_scrape_response(response, &expected_scrape_response).await; + assert_scrape_response(response, &expected_scrape_response).await; - test_env.stop().await; + test_env.stop().await; + } } } - } - mod configured_as_private_and_whitelisted { + mod configured_as_private_and_whitelisted { - mod and_receiving_an_announce_request {} + mod and_receiving_an_announce_request {} - mod receiving_an_scrape_request {} + mod receiving_an_scrape_request {} + } } } diff --git a/tests/udp_tracker.rs b/tests/udp_tracker.rs index 0f9283a8b..3fe78c03d 100644 --- a/tests/udp_tracker.rs +++ b/tests/udp_tracker.rs @@ -1,6 +1,8 @@ /// Integration tests for UDP tracker server /// +/// ```text /// cargo test `udp_tracker_server` -- --nocapture +/// ``` extern crate rand; mod common;