diff --git a/src/core/mod.rs b/src/core/mod.rs index 1d2d856b..f12eb9a3 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -532,6 +532,16 @@ pub enum PeersWanted { } impl PeersWanted { + #[must_use] + pub fn only(limit: u32) -> Self { + let amount: usize = match limit.try_into() { + Ok(amount) => amount, + Err(_) => TORRENT_PEERS_LIMIT, + }; + + Self::Only { amount } + } + fn limit(&self) -> usize { match self { PeersWanted::All => TORRENT_PEERS_LIMIT, diff --git a/src/servers/http/v1/extractors/announce_request.rs b/src/servers/http/v1/extractors/announce_request.rs index b1d82059..324e91bf 100644 --- a/src/servers/http/v1/extractors/announce_request.rs +++ b/src/servers/http/v1/extractors/announce_request.rs @@ -111,7 +111,7 @@ mod tests { #[test] fn it_should_extract_the_announce_request_from_the_url_query_params() { - let raw_query = "info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port=17548&left=0&event=completed&compact=0"; + let raw_query = "info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port=17548&left=0&event=completed&compact=0&numwant=50"; let announce = extract_announce_from(Some(raw_query)).unwrap(); @@ -126,6 +126,7 @@ mod tests { left: Some(NumberOfBytes::new(0)), event: Some(Event::Completed), compact: Some(Compact::NotAccepted), + numwant: Some(50), } ); } diff --git a/src/servers/http/v1/handlers/announce.rs b/src/servers/http/v1/handlers/announce.rs index ee70b784..1c7796fc 100644 --- a/src/servers/http/v1/handlers/announce.rs +++ b/src/servers/http/v1/handlers/announce.rs @@ -16,7 +16,7 @@ use torrust_tracker_clock::clock::Time; use torrust_tracker_primitives::peer; use crate::core::auth::Key; -use crate::core::{AnnounceData, Tracker}; +use crate::core::{AnnounceData, PeersWanted, Tracker}; use crate::servers::http::v1::extractors::announce_request::ExtractRequest; use crate::servers::http::v1::extractors::authentication_key::Extract as ExtractKey; use crate::servers::http::v1::extractors::client_ip_sources::Extract as ExtractClientIpSources; @@ -110,8 +110,12 @@ async fn handle_announce( }; let mut peer = peer_from_request(announce_request, &peer_ip); + let peers_wanted = match announce_request.numwant { + Some(numwant) => PeersWanted::only(numwant), + None => PeersWanted::All, + }; - let announce_data = services::announce::invoke(tracker.clone(), announce_request.info_hash, &mut peer).await; + let announce_data = services::announce::invoke(tracker.clone(), announce_request.info_hash, &mut peer, &peers_wanted).await; Ok(announce_data) } @@ -205,6 +209,7 @@ mod tests { left: None, event: None, compact: None, + numwant: None, } } diff --git a/src/servers/http/v1/requests/announce.rs b/src/servers/http/v1/requests/announce.rs index 3253a07c..b432d347 100644 --- a/src/servers/http/v1/requests/announce.rs +++ b/src/servers/http/v1/requests/announce.rs @@ -24,6 +24,7 @@ const UPLOADED: &str = "uploaded"; const LEFT: &str = "left"; const EVENT: &str = "event"; const COMPACT: &str = "compact"; +const NUMWANT: &str = "numwant"; /// The `Announce` request. Fields use the domain types after parsing the /// query params of the request. @@ -43,7 +44,8 @@ const COMPACT: &str = "compact"; /// uploaded: Some(NumberOfBytes::new(1)), /// left: Some(NumberOfBytes::new(1)), /// event: Some(Event::Started), -/// compact: Some(Compact::NotAccepted) +/// compact: Some(Compact::NotAccepted), +/// numwant: Some(50) /// }; /// ``` /// @@ -59,8 +61,10 @@ pub struct Announce { // Mandatory params /// The `InfoHash` of the torrent. pub info_hash: InfoHash, + /// The `PeerId` of the peer. pub peer_id: PeerId, + /// The port of the peer. pub port: u16, @@ -80,6 +84,10 @@ pub struct Announce { /// Whether the response should be in compact mode or not. pub compact: Option, + + /// Number of peers that the client would receive from the tracker. The + /// value is permitted to be zero. + pub numwant: Option, } /// Errors that can occur when parsing the `Announce` request. @@ -244,6 +252,7 @@ impl TryFrom for Announce { left: extract_left(&query)?, event: extract_event(&query)?, compact: extract_compact(&query)?, + numwant: extract_numwant(&query)?, }) } } @@ -350,6 +359,22 @@ fn extract_compact(query: &Query) -> Result, ParseAnnounceQueryE } } +fn extract_numwant(query: &Query) -> Result, ParseAnnounceQueryError> { + print!("numwant {query:#?}"); + + match query.get_param(NUMWANT) { + Some(raw_param) => match u32::from_str(&raw_param) { + Ok(numwant) => Ok(Some(numwant)), + Err(_) => Err(ParseAnnounceQueryError::InvalidParam { + param_name: NUMWANT.to_owned(), + param_value: raw_param.clone(), + location: Location::caller(), + }), + }, + None => Ok(None), + } +} + #[cfg(test)] mod tests { @@ -360,7 +385,7 @@ mod tests { use crate::servers::http::v1::query::Query; use crate::servers::http::v1::requests::announce::{ - Announce, Compact, Event, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, PEER_ID, PORT, UPLOADED, + Announce, Compact, Event, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, NUMWANT, PEER_ID, PORT, UPLOADED, }; #[test] @@ -387,6 +412,7 @@ mod tests { left: None, event: None, compact: None, + numwant: None, } ); } @@ -402,6 +428,7 @@ mod tests { (LEFT, "3"), (EVENT, "started"), (COMPACT, "0"), + (NUMWANT, "50"), ]) .to_string(); @@ -420,6 +447,7 @@ mod tests { left: Some(NumberOfBytes::new(3)), event: Some(Event::Started), compact: Some(Compact::NotAccepted), + numwant: Some(50), } ); } @@ -428,7 +456,7 @@ mod tests { use crate::servers::http::v1::query::Query; use crate::servers::http::v1::requests::announce::{ - Announce, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, PEER_ID, PORT, UPLOADED, + Announce, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, NUMWANT, PEER_ID, PORT, UPLOADED, }; #[test] @@ -547,6 +575,19 @@ mod tests { assert!(Announce::try_from(raw_query.parse::().unwrap()).is_err()); } + + #[test] + fn it_should_fail_if_the_numwant_param_is_invalid() { + let raw_query = Query::from(vec![ + (INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"), + (PEER_ID, "-qB00000000000000001"), + (PORT, "17548"), + (NUMWANT, "-1"), + ]) + .to_string(); + + assert!(Announce::try_from(raw_query.parse::().unwrap()).is_err()); + } } } } diff --git a/src/servers/http/v1/services/announce.rs b/src/servers/http/v1/services/announce.rs index a58df4e1..9c5dfdad 100644 --- a/src/servers/http/v1/services/announce.rs +++ b/src/servers/http/v1/services/announce.rs @@ -26,11 +26,16 @@ use crate::core::{statistics, AnnounceData, PeersWanted, Tracker}; /// > **NOTICE**: as the HTTP tracker does not requires a connection request /// > like the UDP tracker, the number of TCP connections is incremented for /// > each `announce` request. -pub async fn invoke(tracker: Arc, info_hash: InfoHash, peer: &mut peer::Peer) -> AnnounceData { +pub async fn invoke( + tracker: Arc, + info_hash: InfoHash, + peer: &mut peer::Peer, + peers_wanted: &PeersWanted, +) -> AnnounceData { let original_peer_ip = peer.peer_addr.ip(); // The tracker could change the original peer ip - let announce_data = tracker.announce(&info_hash, peer, &original_peer_ip, &PeersWanted::All); + let announce_data = tracker.announce(&info_hash, peer, &original_peer_ip, peers_wanted); match original_peer_ip { IpAddr::V4(_) => { @@ -100,7 +105,7 @@ mod tests { use torrust_tracker_test_helpers::configuration; use super::{sample_peer_using_ipv4, sample_peer_using_ipv6}; - use crate::core::{statistics, AnnounceData, Tracker}; + use crate::core::{statistics, AnnounceData, PeersWanted, Tracker}; use crate::servers::http::v1::services::announce::invoke; use crate::servers::http::v1::services::announce::tests::{public_tracker, sample_info_hash, sample_peer}; @@ -110,7 +115,7 @@ mod tests { let mut peer = sample_peer(); - let announce_data = invoke(tracker.clone(), sample_info_hash(), &mut peer).await; + let announce_data = invoke(tracker.clone(), sample_info_hash(), &mut peer, &PeersWanted::All).await; let expected_announce_data = AnnounceData { peers: vec![], @@ -146,7 +151,7 @@ mod tests { let mut peer = sample_peer_using_ipv4(); - let _announce_data = invoke(tracker, sample_info_hash(), &mut peer).await; + let _announce_data = invoke(tracker, sample_info_hash(), &mut peer, &PeersWanted::All).await; } fn tracker_with_an_ipv6_external_ip(stats_event_sender: Box) -> Tracker { @@ -185,6 +190,7 @@ mod tests { tracker_with_an_ipv6_external_ip(stats_event_sender).into(), sample_info_hash(), &mut peer, + &PeersWanted::All, ) .await; } @@ -211,7 +217,7 @@ mod tests { let mut peer = sample_peer_using_ipv6(); - let _announce_data = invoke(tracker, sample_info_hash(), &mut peer).await; + let _announce_data = invoke(tracker, sample_info_hash(), &mut peer, &PeersWanted::All).await; } } } diff --git a/tests/servers/http/requests/announce.rs b/tests/servers/http/requests/announce.rs index bcbb3685..fa20553d 100644 --- a/tests/servers/http/requests/announce.rs +++ b/tests/servers/http/requests/announce.rs @@ -18,6 +18,7 @@ pub struct Query { pub left: BaseTenASCII, pub event: Option, pub compact: Option, + pub numwant: Option, } impl fmt::Display for Query { @@ -98,6 +99,7 @@ impl QueryBuilder { left: 0, event: Some(Event::Completed), compact: Some(Compact::NotAccepted), + numwant: None, }; Self { announce_query: default_announce_query, @@ -149,7 +151,9 @@ impl QueryBuilder { /// left=0 /// event=completed /// compact=0 +/// numwant=50 /// ``` +#[derive(Debug)] pub struct QueryParams { pub info_hash: Option, pub peer_addr: Option, @@ -160,6 +164,7 @@ pub struct QueryParams { pub left: Option, pub event: Option, pub compact: Option, + pub numwant: Option, } impl std::fmt::Display for QueryParams { @@ -193,6 +198,9 @@ impl std::fmt::Display for QueryParams { if let Some(compact) = &self.compact { params.push(("compact", compact)); } + if let Some(numwant) = &self.numwant { + params.push(("numwant", numwant)); + } let query = params .iter() @@ -208,6 +216,7 @@ impl QueryParams { pub fn from(announce_query: &Query) -> Self { let event = announce_query.event.as_ref().map(std::string::ToString::to_string); let compact = announce_query.compact.as_ref().map(std::string::ToString::to_string); + let numwant = announce_query.numwant.map(|numwant| numwant.to_string()); Self { info_hash: Some(percent_encode_byte_array(&announce_query.info_hash)), @@ -219,6 +228,7 @@ impl QueryParams { left: Some(announce_query.left.to_string()), event, compact, + numwant, } } @@ -241,6 +251,7 @@ impl QueryParams { self.left = None; self.event = None; self.compact = None; + self.numwant = None; } pub fn set(&mut self, param_name: &str, param_value: &str) { @@ -254,6 +265,7 @@ impl QueryParams { "left" => self.left = Some(param_value.to_string()), "event" => self.event = Some(param_value.to_string()), "compact" => self.compact = Some(param_value.to_string()), + "numwant" => self.numwant = Some(param_value.to_string()), &_ => panic!("Invalid param name for announce query"), } } diff --git a/tests/servers/http/v1/contract.rs b/tests/servers/http/v1/contract.rs index 41e92c9d..405a35dc 100644 --- a/tests/servers/http/v1/contract.rs +++ b/tests/servers/http/v1/contract.rs @@ -448,6 +448,29 @@ mod for_all_config_modes { env.stop().await; } + #[tokio::test] + async fn should_fail_when_the_numwant_param_is_invalid() { + INIT.call_once(|| { + tracing_stderr_init(LevelFilter::ERROR); + }); + + let env = Started::new(&configuration::ephemeral().into()).await; + + let mut params = QueryBuilder::default().query().params(); + + let invalid_values = ["-1", "1.1", "a"]; + + for invalid_value in invalid_values { + params.set("numwant", invalid_value); + + let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await; + + assert_bad_announce_request_error_response(response, "invalid param value").await; + } + + env.stop().await; + } + #[tokio::test] async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() { INIT.call_once(|| {