Skip to content

Commit

Permalink
feat: [#569] numwant HTTP tracker announce param
Browse files Browse the repository at this point in the history
It allows HTTP clients to limit peers in the announce response with the
`numwant` GET param.
  • Loading branch information
josecelano committed Sep 10, 2024
1 parent 481d413 commit 084879e
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 12 deletions.
10 changes: 10 additions & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/servers/http/v1/extractors/announce_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -126,6 +126,7 @@ mod tests {
left: Some(NumberOfBytes::new(0)),
event: Some(Event::Completed),
compact: Some(Compact::NotAccepted),
numwant: Some(50),
}
);
}
Expand Down
9 changes: 7 additions & 2 deletions src/servers/http/v1/handlers/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -205,6 +209,7 @@ mod tests {
left: None,
event: None,
compact: None,
numwant: None,
}
}

Expand Down
47 changes: 44 additions & 3 deletions src/servers/http/v1/requests/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
/// };
/// ```
///
Expand All @@ -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,

Expand All @@ -80,6 +84,10 @@ pub struct Announce {

/// Whether the response should be in compact mode or not.
pub compact: Option<Compact>,

/// Number of peers that the client would receive from the tracker. The
/// value is permitted to be zero.
pub numwant: Option<u32>,
}

/// Errors that can occur when parsing the `Announce` request.
Expand Down Expand Up @@ -244,6 +252,7 @@ impl TryFrom<Query> for Announce {
left: extract_left(&query)?,
event: extract_event(&query)?,
compact: extract_compact(&query)?,
numwant: extract_numwant(&query)?,
})
}
}
Expand Down Expand Up @@ -350,6 +359,22 @@ fn extract_compact(query: &Query) -> Result<Option<Compact>, ParseAnnounceQueryE
}
}

fn extract_numwant(query: &Query) -> Result<Option<u32>, 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 {

Expand All @@ -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]
Expand All @@ -387,6 +412,7 @@ mod tests {
left: None,
event: None,
compact: None,
numwant: None,
}
);
}
Expand All @@ -402,6 +428,7 @@ mod tests {
(LEFT, "3"),
(EVENT, "started"),
(COMPACT, "0"),
(NUMWANT, "50"),
])
.to_string();

Expand All @@ -420,6 +447,7 @@ mod tests {
left: Some(NumberOfBytes::new(3)),
event: Some(Event::Started),
compact: Some(Compact::NotAccepted),
numwant: Some(50),
}
);
}
Expand All @@ -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]
Expand Down Expand Up @@ -547,6 +575,19 @@ mod tests {

assert!(Announce::try_from(raw_query.parse::<Query>().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::<Query>().unwrap()).is_err());
}
}
}
}
18 changes: 12 additions & 6 deletions src/servers/http/v1/services/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tracker>, info_hash: InfoHash, peer: &mut peer::Peer) -> AnnounceData {
pub async fn invoke(
tracker: Arc<Tracker>,
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(_) => {
Expand Down Expand Up @@ -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};

Expand All @@ -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![],
Expand Down Expand Up @@ -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<dyn statistics::EventSender>) -> Tracker {
Expand Down Expand Up @@ -185,6 +190,7 @@ mod tests {
tracker_with_an_ipv6_external_ip(stats_event_sender).into(),
sample_info_hash(),
&mut peer,
&PeersWanted::All,
)
.await;
}
Expand All @@ -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;
}
}
}
12 changes: 12 additions & 0 deletions tests/servers/http/requests/announce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct Query {
pub left: BaseTenASCII,
pub event: Option<Event>,
pub compact: Option<Compact>,
pub numwant: Option<u32>,
}

impl fmt::Display for Query {
Expand Down Expand Up @@ -98,6 +99,7 @@ impl QueryBuilder {
left: 0,
event: Some(Event::Completed),
compact: Some(Compact::NotAccepted),
numwant: None,
};
Self {
announce_query: default_announce_query,
Expand Down Expand Up @@ -149,7 +151,9 @@ impl QueryBuilder {
/// left=0
/// event=completed
/// compact=0
/// numwant=50
/// ```
#[derive(Debug)]
pub struct QueryParams {
pub info_hash: Option<String>,
pub peer_addr: Option<String>,
Expand All @@ -160,6 +164,7 @@ pub struct QueryParams {
pub left: Option<String>,
pub event: Option<String>,
pub compact: Option<String>,
pub numwant: Option<String>,
}

impl std::fmt::Display for QueryParams {
Expand Down Expand Up @@ -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()
Expand All @@ -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)),
Expand All @@ -219,6 +228,7 @@ impl QueryParams {
left: Some(announce_query.left.to_string()),
event,
compact,
numwant,
}
}

Expand All @@ -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) {
Expand All @@ -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"),
}
}
Expand Down
23 changes: 23 additions & 0 deletions tests/servers/http/v1/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(|| {
Expand Down

0 comments on commit 084879e

Please sign in to comment.