Skip to content

Commit 084879e

Browse files
committed
feat: [#569] numwant HTTP tracker announce param
It allows HTTP clients to limit peers in the announce response with the `numwant` GET param.
1 parent 481d413 commit 084879e

File tree

7 files changed

+110
-12
lines changed

7 files changed

+110
-12
lines changed

src/core/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,16 @@ pub enum PeersWanted {
532532
}
533533

534534
impl PeersWanted {
535+
#[must_use]
536+
pub fn only(limit: u32) -> Self {
537+
let amount: usize = match limit.try_into() {
538+
Ok(amount) => amount,
539+
Err(_) => TORRENT_PEERS_LIMIT,
540+
};
541+
542+
Self::Only { amount }
543+
}
544+
535545
fn limit(&self) -> usize {
536546
match self {
537547
PeersWanted::All => TORRENT_PEERS_LIMIT,

src/servers/http/v1/extractors/announce_request.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ mod tests {
111111

112112
#[test]
113113
fn it_should_extract_the_announce_request_from_the_url_query_params() {
114-
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";
114+
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";
115115

116116
let announce = extract_announce_from(Some(raw_query)).unwrap();
117117

@@ -126,6 +126,7 @@ mod tests {
126126
left: Some(NumberOfBytes::new(0)),
127127
event: Some(Event::Completed),
128128
compact: Some(Compact::NotAccepted),
129+
numwant: Some(50),
129130
}
130131
);
131132
}

src/servers/http/v1/handlers/announce.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use torrust_tracker_clock::clock::Time;
1616
use torrust_tracker_primitives::peer;
1717

1818
use crate::core::auth::Key;
19-
use crate::core::{AnnounceData, Tracker};
19+
use crate::core::{AnnounceData, PeersWanted, Tracker};
2020
use crate::servers::http::v1::extractors::announce_request::ExtractRequest;
2121
use crate::servers::http::v1::extractors::authentication_key::Extract as ExtractKey;
2222
use crate::servers::http::v1::extractors::client_ip_sources::Extract as ExtractClientIpSources;
@@ -110,8 +110,12 @@ async fn handle_announce(
110110
};
111111

112112
let mut peer = peer_from_request(announce_request, &peer_ip);
113+
let peers_wanted = match announce_request.numwant {
114+
Some(numwant) => PeersWanted::only(numwant),
115+
None => PeersWanted::All,
116+
};
113117

114-
let announce_data = services::announce::invoke(tracker.clone(), announce_request.info_hash, &mut peer).await;
118+
let announce_data = services::announce::invoke(tracker.clone(), announce_request.info_hash, &mut peer, &peers_wanted).await;
115119

116120
Ok(announce_data)
117121
}
@@ -205,6 +209,7 @@ mod tests {
205209
left: None,
206210
event: None,
207211
compact: None,
212+
numwant: None,
208213
}
209214
}
210215

src/servers/http/v1/requests/announce.rs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const UPLOADED: &str = "uploaded";
2424
const LEFT: &str = "left";
2525
const EVENT: &str = "event";
2626
const COMPACT: &str = "compact";
27+
const NUMWANT: &str = "numwant";
2728

2829
/// The `Announce` request. Fields use the domain types after parsing the
2930
/// query params of the request.
@@ -43,7 +44,8 @@ const COMPACT: &str = "compact";
4344
/// uploaded: Some(NumberOfBytes::new(1)),
4445
/// left: Some(NumberOfBytes::new(1)),
4546
/// event: Some(Event::Started),
46-
/// compact: Some(Compact::NotAccepted)
47+
/// compact: Some(Compact::NotAccepted),
48+
/// numwant: Some(50)
4749
/// };
4850
/// ```
4951
///
@@ -59,8 +61,10 @@ pub struct Announce {
5961
// Mandatory params
6062
/// The `InfoHash` of the torrent.
6163
pub info_hash: InfoHash,
64+
6265
/// The `PeerId` of the peer.
6366
pub peer_id: PeerId,
67+
6468
/// The port of the peer.
6569
pub port: u16,
6670

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

8185
/// Whether the response should be in compact mode or not.
8286
pub compact: Option<Compact>,
87+
88+
/// Number of peers that the client would receive from the tracker. The
89+
/// value is permitted to be zero.
90+
pub numwant: Option<u32>,
8391
}
8492

8593
/// Errors that can occur when parsing the `Announce` request.
@@ -244,6 +252,7 @@ impl TryFrom<Query> for Announce {
244252
left: extract_left(&query)?,
245253
event: extract_event(&query)?,
246254
compact: extract_compact(&query)?,
255+
numwant: extract_numwant(&query)?,
247256
})
248257
}
249258
}
@@ -350,6 +359,22 @@ fn extract_compact(query: &Query) -> Result<Option<Compact>, ParseAnnounceQueryE
350359
}
351360
}
352361

362+
fn extract_numwant(query: &Query) -> Result<Option<u32>, ParseAnnounceQueryError> {
363+
print!("numwant {query:#?}");
364+
365+
match query.get_param(NUMWANT) {
366+
Some(raw_param) => match u32::from_str(&raw_param) {
367+
Ok(numwant) => Ok(Some(numwant)),
368+
Err(_) => Err(ParseAnnounceQueryError::InvalidParam {
369+
param_name: NUMWANT.to_owned(),
370+
param_value: raw_param.clone(),
371+
location: Location::caller(),
372+
}),
373+
},
374+
None => Ok(None),
375+
}
376+
}
377+
353378
#[cfg(test)]
354379
mod tests {
355380

@@ -360,7 +385,7 @@ mod tests {
360385

361386
use crate::servers::http::v1::query::Query;
362387
use crate::servers::http::v1::requests::announce::{
363-
Announce, Compact, Event, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, PEER_ID, PORT, UPLOADED,
388+
Announce, Compact, Event, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, NUMWANT, PEER_ID, PORT, UPLOADED,
364389
};
365390

366391
#[test]
@@ -387,6 +412,7 @@ mod tests {
387412
left: None,
388413
event: None,
389414
compact: None,
415+
numwant: None,
390416
}
391417
);
392418
}
@@ -402,6 +428,7 @@ mod tests {
402428
(LEFT, "3"),
403429
(EVENT, "started"),
404430
(COMPACT, "0"),
431+
(NUMWANT, "50"),
405432
])
406433
.to_string();
407434

@@ -420,6 +447,7 @@ mod tests {
420447
left: Some(NumberOfBytes::new(3)),
421448
event: Some(Event::Started),
422449
compact: Some(Compact::NotAccepted),
450+
numwant: Some(50),
423451
}
424452
);
425453
}
@@ -428,7 +456,7 @@ mod tests {
428456

429457
use crate::servers::http::v1::query::Query;
430458
use crate::servers::http::v1::requests::announce::{
431-
Announce, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, PEER_ID, PORT, UPLOADED,
459+
Announce, COMPACT, DOWNLOADED, EVENT, INFO_HASH, LEFT, NUMWANT, PEER_ID, PORT, UPLOADED,
432460
};
433461

434462
#[test]
@@ -547,6 +575,19 @@ mod tests {
547575

548576
assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
549577
}
578+
579+
#[test]
580+
fn it_should_fail_if_the_numwant_param_is_invalid() {
581+
let raw_query = Query::from(vec![
582+
(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0"),
583+
(PEER_ID, "-qB00000000000000001"),
584+
(PORT, "17548"),
585+
(NUMWANT, "-1"),
586+
])
587+
.to_string();
588+
589+
assert!(Announce::try_from(raw_query.parse::<Query>().unwrap()).is_err());
590+
}
550591
}
551592
}
552593
}

src/servers/http/v1/services/announce.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@ use crate::core::{statistics, AnnounceData, PeersWanted, Tracker};
2626
/// > **NOTICE**: as the HTTP tracker does not requires a connection request
2727
/// > like the UDP tracker, the number of TCP connections is incremented for
2828
/// > each `announce` request.
29-
pub async fn invoke(tracker: Arc<Tracker>, info_hash: InfoHash, peer: &mut peer::Peer) -> AnnounceData {
29+
pub async fn invoke(
30+
tracker: Arc<Tracker>,
31+
info_hash: InfoHash,
32+
peer: &mut peer::Peer,
33+
peers_wanted: &PeersWanted,
34+
) -> AnnounceData {
3035
let original_peer_ip = peer.peer_addr.ip();
3136

3237
// The tracker could change the original peer ip
33-
let announce_data = tracker.announce(&info_hash, peer, &original_peer_ip, &PeersWanted::All);
38+
let announce_data = tracker.announce(&info_hash, peer, &original_peer_ip, peers_wanted);
3439

3540
match original_peer_ip {
3641
IpAddr::V4(_) => {
@@ -100,7 +105,7 @@ mod tests {
100105
use torrust_tracker_test_helpers::configuration;
101106

102107
use super::{sample_peer_using_ipv4, sample_peer_using_ipv6};
103-
use crate::core::{statistics, AnnounceData, Tracker};
108+
use crate::core::{statistics, AnnounceData, PeersWanted, Tracker};
104109
use crate::servers::http::v1::services::announce::invoke;
105110
use crate::servers::http::v1::services::announce::tests::{public_tracker, sample_info_hash, sample_peer};
106111

@@ -110,7 +115,7 @@ mod tests {
110115

111116
let mut peer = sample_peer();
112117

113-
let announce_data = invoke(tracker.clone(), sample_info_hash(), &mut peer).await;
118+
let announce_data = invoke(tracker.clone(), sample_info_hash(), &mut peer, &PeersWanted::All).await;
114119

115120
let expected_announce_data = AnnounceData {
116121
peers: vec![],
@@ -146,7 +151,7 @@ mod tests {
146151

147152
let mut peer = sample_peer_using_ipv4();
148153

149-
let _announce_data = invoke(tracker, sample_info_hash(), &mut peer).await;
154+
let _announce_data = invoke(tracker, sample_info_hash(), &mut peer, &PeersWanted::All).await;
150155
}
151156

152157
fn tracker_with_an_ipv6_external_ip(stats_event_sender: Box<dyn statistics::EventSender>) -> Tracker {
@@ -185,6 +190,7 @@ mod tests {
185190
tracker_with_an_ipv6_external_ip(stats_event_sender).into(),
186191
sample_info_hash(),
187192
&mut peer,
193+
&PeersWanted::All,
188194
)
189195
.await;
190196
}
@@ -211,7 +217,7 @@ mod tests {
211217

212218
let mut peer = sample_peer_using_ipv6();
213219

214-
let _announce_data = invoke(tracker, sample_info_hash(), &mut peer).await;
220+
let _announce_data = invoke(tracker, sample_info_hash(), &mut peer, &PeersWanted::All).await;
215221
}
216222
}
217223
}

tests/servers/http/requests/announce.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct Query {
1818
pub left: BaseTenASCII,
1919
pub event: Option<Event>,
2020
pub compact: Option<Compact>,
21+
pub numwant: Option<u32>,
2122
}
2223

2324
impl fmt::Display for Query {
@@ -98,6 +99,7 @@ impl QueryBuilder {
9899
left: 0,
99100
event: Some(Event::Completed),
100101
compact: Some(Compact::NotAccepted),
102+
numwant: None,
101103
};
102104
Self {
103105
announce_query: default_announce_query,
@@ -149,7 +151,9 @@ impl QueryBuilder {
149151
/// left=0
150152
/// event=completed
151153
/// compact=0
154+
/// numwant=50
152155
/// ```
156+
#[derive(Debug)]
153157
pub struct QueryParams {
154158
pub info_hash: Option<String>,
155159
pub peer_addr: Option<String>,
@@ -160,6 +164,7 @@ pub struct QueryParams {
160164
pub left: Option<String>,
161165
pub event: Option<String>,
162166
pub compact: Option<String>,
167+
pub numwant: Option<String>,
163168
}
164169

165170
impl std::fmt::Display for QueryParams {
@@ -193,6 +198,9 @@ impl std::fmt::Display for QueryParams {
193198
if let Some(compact) = &self.compact {
194199
params.push(("compact", compact));
195200
}
201+
if let Some(numwant) = &self.numwant {
202+
params.push(("numwant", numwant));
203+
}
196204

197205
let query = params
198206
.iter()
@@ -208,6 +216,7 @@ impl QueryParams {
208216
pub fn from(announce_query: &Query) -> Self {
209217
let event = announce_query.event.as_ref().map(std::string::ToString::to_string);
210218
let compact = announce_query.compact.as_ref().map(std::string::ToString::to_string);
219+
let numwant = announce_query.numwant.map(|numwant| numwant.to_string());
211220

212221
Self {
213222
info_hash: Some(percent_encode_byte_array(&announce_query.info_hash)),
@@ -219,6 +228,7 @@ impl QueryParams {
219228
left: Some(announce_query.left.to_string()),
220229
event,
221230
compact,
231+
numwant,
222232
}
223233
}
224234

@@ -241,6 +251,7 @@ impl QueryParams {
241251
self.left = None;
242252
self.event = None;
243253
self.compact = None;
254+
self.numwant = None;
244255
}
245256

246257
pub fn set(&mut self, param_name: &str, param_value: &str) {
@@ -254,6 +265,7 @@ impl QueryParams {
254265
"left" => self.left = Some(param_value.to_string()),
255266
"event" => self.event = Some(param_value.to_string()),
256267
"compact" => self.compact = Some(param_value.to_string()),
268+
"numwant" => self.numwant = Some(param_value.to_string()),
257269
&_ => panic!("Invalid param name for announce query"),
258270
}
259271
}

tests/servers/http/v1/contract.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,29 @@ mod for_all_config_modes {
448448
env.stop().await;
449449
}
450450

451+
#[tokio::test]
452+
async fn should_fail_when_the_numwant_param_is_invalid() {
453+
INIT.call_once(|| {
454+
tracing_stderr_init(LevelFilter::ERROR);
455+
});
456+
457+
let env = Started::new(&configuration::ephemeral().into()).await;
458+
459+
let mut params = QueryBuilder::default().query().params();
460+
461+
let invalid_values = ["-1", "1.1", "a"];
462+
463+
for invalid_value in invalid_values {
464+
params.set("numwant", invalid_value);
465+
466+
let response = Client::new(*env.bind_address()).get(&format!("announce?{params}")).await;
467+
468+
assert_bad_announce_request_error_response(response, "invalid param value").await;
469+
}
470+
471+
env.stop().await;
472+
}
473+
451474
#[tokio::test]
452475
async fn should_return_no_peers_if_the_announced_peer_is_the_first_one() {
453476
INIT.call_once(|| {

0 commit comments

Comments
 (0)