Skip to content

Commit c71d88c

Browse files
committed
Merge #1035: Allow UDP clients to limit peers in response
084879e feat: [#569] numwant HTTP tracker announce param (Jose Celano) 481d413 feat: [#569] allow UDP clients to limit peers in response (Jose Celano) Pull request description: The UDP tracker announce response always include all peers available up to a maxium of 74 peers, ignoring the `num_want` param in the request described in: https://www.bittorrent.org/beps/bep_0015.html This change applies that limit only when is lower than then TORRENT_PEERS_LIMIT (74). It also adds the `numwant` GET param to the announce request for the HTTP tracker for the same purpose. - [x] UDP tracker (`num_want`, positional param in the UDP packet). - [x] HTTP tracker (`numwant` GET param). ACKs for top commit: josecelano: ACK 084879e Tree-SHA512: 6e3a7a672d393852d8655c4e2732910dbf2a5beecb3f92a39b251e127e05d3a5ae068962ec1577189ed397b2529201558c971e8c050c2763d8781efd8441f540
2 parents 1e437f7 + 084879e commit c71d88c

File tree

10 files changed

+276
-41
lines changed

10 files changed

+276
-41
lines changed

src/core/mod.rs

Lines changed: 167 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@ pub mod torrent;
448448

449449
pub mod peer_tests;
450450

451+
use std::cmp::max;
451452
use std::collections::HashMap;
452453
use std::net::IpAddr;
453454
use std::panic::Location;
@@ -520,6 +521,48 @@ pub struct AnnounceData {
520521
pub policy: AnnouncePolicy,
521522
}
522523

524+
/// How many peers the peer announcing wants in the announce response.
525+
#[derive(Clone, Debug, PartialEq, Default)]
526+
pub enum PeersWanted {
527+
/// The peer wants as many peers as possible in the announce response.
528+
#[default]
529+
All,
530+
/// The peer only wants a certain amount of peers in the announce response.
531+
Only { amount: usize },
532+
}
533+
534+
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+
545+
fn limit(&self) -> usize {
546+
match self {
547+
PeersWanted::All => TORRENT_PEERS_LIMIT,
548+
PeersWanted::Only { amount } => *amount,
549+
}
550+
}
551+
}
552+
553+
impl From<i32> for PeersWanted {
554+
fn from(value: i32) -> Self {
555+
if value > 0 {
556+
match value.try_into() {
557+
Ok(peers_wanted) => Self::Only { amount: peers_wanted },
558+
Err(_) => Self::All,
559+
}
560+
} else {
561+
Self::All
562+
}
563+
}
564+
}
565+
523566
/// Structure that holds the data returned by the `scrape` request.
524567
#[derive(Debug, PartialEq, Default)]
525568
pub struct ScrapeData {
@@ -639,7 +682,13 @@ impl Tracker {
639682
/// # Context: Tracker
640683
///
641684
/// BEP 03: [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html).
642-
pub fn announce(&self, info_hash: &InfoHash, peer: &mut peer::Peer, remote_client_ip: &IpAddr) -> AnnounceData {
685+
pub fn announce(
686+
&self,
687+
info_hash: &InfoHash,
688+
peer: &mut peer::Peer,
689+
remote_client_ip: &IpAddr,
690+
peers_wanted: &PeersWanted,
691+
) -> AnnounceData {
643692
// code-review: maybe instead of mutating the peer we could just return
644693
// a tuple with the new peer and the announce data: (Peer, AnnounceData).
645694
// It could even be a different struct: `StoredPeer` or `PublicPeer`.
@@ -661,7 +710,7 @@ impl Tracker {
661710

662711
let stats = self.upsert_peer_and_get_stats(info_hash, peer);
663712

664-
let peers = self.get_peers_for(info_hash, peer);
713+
let peers = self.get_peers_for(info_hash, peer, peers_wanted.limit());
665714

666715
AnnounceData {
667716
peers,
@@ -713,16 +762,21 @@ impl Tracker {
713762
Ok(())
714763
}
715764

716-
fn get_peers_for(&self, info_hash: &InfoHash, peer: &peer::Peer) -> Vec<Arc<peer::Peer>> {
765+
/// # Context: Tracker
766+
///
767+
/// Get torrent peers for a given torrent and client.
768+
///
769+
/// It filters out the client making the request.
770+
fn get_peers_for(&self, info_hash: &InfoHash, peer: &peer::Peer, limit: usize) -> Vec<Arc<peer::Peer>> {
717771
match self.torrents.get(info_hash) {
718772
None => vec![],
719-
Some(entry) => entry.get_peers_for_client(&peer.peer_addr, Some(TORRENT_PEERS_LIMIT)),
773+
Some(entry) => entry.get_peers_for_client(&peer.peer_addr, Some(max(limit, TORRENT_PEERS_LIMIT))),
720774
}
721775
}
722776

723777
/// # Context: Tracker
724778
///
725-
/// Get all torrent peers for a given torrent
779+
/// Get torrent peers for a given torrent.
726780
pub fn get_torrent_peers(&self, info_hash: &InfoHash) -> Vec<Arc<peer::Peer>> {
727781
match self.torrents.get(info_hash) {
728782
None => vec![],
@@ -1199,6 +1253,7 @@ mod tests {
11991253
use std::sync::Arc;
12001254

12011255
use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes, PeerId};
1256+
use torrust_tracker_configuration::TORRENT_PEERS_LIMIT;
12021257
use torrust_tracker_primitives::info_hash::InfoHash;
12031258
use torrust_tracker_primitives::DurationSinceUnixEpoch;
12041259
use torrust_tracker_test_helpers::configuration;
@@ -1328,7 +1383,7 @@ mod tests {
13281383
}
13291384

13301385
#[tokio::test]
1331-
async fn it_should_return_all_the_peers_for_a_given_torrent() {
1386+
async fn it_should_return_the_peers_for_a_given_torrent() {
13321387
let tracker = public_tracker();
13331388

13341389
let info_hash = sample_info_hash();
@@ -1341,20 +1396,93 @@ mod tests {
13411396
assert_eq!(peers, vec![Arc::new(peer)]);
13421397
}
13431398

1399+
/// It generates a peer id from a number where the number is the last
1400+
/// part of the peer ID. For example, for `12` it returns
1401+
/// `-qB00000000000000012`.
1402+
fn numeric_peer_id(two_digits_value: i32) -> PeerId {
1403+
// Format idx as a string with leading zeros, ensuring it has exactly 2 digits
1404+
let idx_str = format!("{two_digits_value:02}");
1405+
1406+
// Create the base part of the peer ID.
1407+
let base = b"-qB00000000000000000";
1408+
1409+
// Concatenate the base with idx bytes, ensuring the total length is 20 bytes.
1410+
let mut peer_id_bytes = [0u8; 20];
1411+
peer_id_bytes[..base.len()].copy_from_slice(base);
1412+
peer_id_bytes[base.len() - idx_str.len()..].copy_from_slice(idx_str.as_bytes());
1413+
1414+
PeerId(peer_id_bytes)
1415+
}
1416+
1417+
#[tokio::test]
1418+
async fn it_should_return_74_peers_at_the_most_for_a_given_torrent() {
1419+
let tracker = public_tracker();
1420+
1421+
let info_hash = sample_info_hash();
1422+
1423+
for idx in 1..=75 {
1424+
let peer = Peer {
1425+
peer_id: numeric_peer_id(idx),
1426+
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, idx.try_into().unwrap())), 8080),
1427+
updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0),
1428+
uploaded: NumberOfBytes::new(0),
1429+
downloaded: NumberOfBytes::new(0),
1430+
left: NumberOfBytes::new(0), // No bytes left to download
1431+
event: AnnounceEvent::Completed,
1432+
};
1433+
1434+
tracker.upsert_peer_and_get_stats(&info_hash, &peer);
1435+
}
1436+
1437+
let peers = tracker.get_torrent_peers(&info_hash);
1438+
1439+
assert_eq!(peers.len(), 74);
1440+
}
1441+
13441442
#[tokio::test]
1345-
async fn it_should_return_all_the_peers_for_a_given_torrent_excluding_a_given_peer() {
1443+
async fn it_should_return_the_peers_for_a_given_torrent_excluding_a_given_peer() {
13461444
let tracker = public_tracker();
13471445

13481446
let info_hash = sample_info_hash();
13491447
let peer = sample_peer();
13501448

13511449
tracker.upsert_peer_and_get_stats(&info_hash, &peer);
13521450

1353-
let peers = tracker.get_peers_for(&info_hash, &peer);
1451+
let peers = tracker.get_peers_for(&info_hash, &peer, TORRENT_PEERS_LIMIT);
13541452

13551453
assert_eq!(peers, vec![]);
13561454
}
13571455

1456+
#[tokio::test]
1457+
async fn it_should_return_74_peers_at_the_most_for_a_given_torrent_when_it_filters_out_a_given_peer() {
1458+
let tracker = public_tracker();
1459+
1460+
let info_hash = sample_info_hash();
1461+
1462+
let excluded_peer = sample_peer();
1463+
1464+
tracker.upsert_peer_and_get_stats(&info_hash, &excluded_peer);
1465+
1466+
// Add 74 peers
1467+
for idx in 2..=75 {
1468+
let peer = Peer {
1469+
peer_id: numeric_peer_id(idx),
1470+
peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(126, 0, 0, idx.try_into().unwrap())), 8080),
1471+
updated: DurationSinceUnixEpoch::new(1_669_397_478_934, 0),
1472+
uploaded: NumberOfBytes::new(0),
1473+
downloaded: NumberOfBytes::new(0),
1474+
left: NumberOfBytes::new(0), // No bytes left to download
1475+
event: AnnounceEvent::Completed,
1476+
};
1477+
1478+
tracker.upsert_peer_and_get_stats(&info_hash, &peer);
1479+
}
1480+
1481+
let peers = tracker.get_peers_for(&info_hash, &excluded_peer, TORRENT_PEERS_LIMIT);
1482+
1483+
assert_eq!(peers.len(), 74);
1484+
}
1485+
13581486
#[tokio::test]
13591487
async fn it_should_return_the_torrent_metrics() {
13601488
let tracker = public_tracker();
@@ -1409,6 +1537,7 @@ mod tests {
14091537
use crate::core::tests::the_tracker::{
14101538
peer_ip, public_tracker, sample_info_hash, sample_peer, sample_peer_1, sample_peer_2,
14111539
};
1540+
use crate::core::PeersWanted;
14121541

14131542
mod should_assign_the_ip_to_the_peer {
14141543

@@ -1514,7 +1643,7 @@ mod tests {
15141643

15151644
let mut peer = sample_peer();
15161645

1517-
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip());
1646+
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip(), &PeersWanted::All);
15181647

15191648
assert_eq!(announce_data.peers, vec![]);
15201649
}
@@ -1524,10 +1653,15 @@ mod tests {
15241653
let tracker = public_tracker();
15251654

15261655
let mut previously_announced_peer = sample_peer_1();
1527-
tracker.announce(&sample_info_hash(), &mut previously_announced_peer, &peer_ip());
1656+
tracker.announce(
1657+
&sample_info_hash(),
1658+
&mut previously_announced_peer,
1659+
&peer_ip(),
1660+
&PeersWanted::All,
1661+
);
15281662

15291663
let mut peer = sample_peer_2();
1530-
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip());
1664+
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip(), &PeersWanted::All);
15311665

15321666
assert_eq!(announce_data.peers, vec![Arc::new(previously_announced_peer)]);
15331667
}
@@ -1537,14 +1671,15 @@ mod tests {
15371671
use crate::core::tests::the_tracker::{
15381672
completed_peer, leecher, peer_ip, public_tracker, sample_info_hash, seeder, started_peer,
15391673
};
1674+
use crate::core::PeersWanted;
15401675

15411676
#[tokio::test]
15421677
async fn when_the_peer_is_a_seeder() {
15431678
let tracker = public_tracker();
15441679

15451680
let mut peer = seeder();
15461681

1547-
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip());
1682+
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip(), &PeersWanted::All);
15481683

15491684
assert_eq!(announce_data.stats.complete, 1);
15501685
}
@@ -1555,7 +1690,7 @@ mod tests {
15551690

15561691
let mut peer = leecher();
15571692

1558-
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip());
1693+
let announce_data = tracker.announce(&sample_info_hash(), &mut peer, &peer_ip(), &PeersWanted::All);
15591694

15601695
assert_eq!(announce_data.stats.incomplete, 1);
15611696
}
@@ -1566,10 +1701,11 @@ mod tests {
15661701

15671702
// We have to announce with "started" event because peer does not count if peer was not previously known
15681703
let mut started_peer = started_peer();
1569-
tracker.announce(&sample_info_hash(), &mut started_peer, &peer_ip());
1704+
tracker.announce(&sample_info_hash(), &mut started_peer, &peer_ip(), &PeersWanted::All);
15701705

15711706
let mut completed_peer = completed_peer();
1572-
let announce_data = tracker.announce(&sample_info_hash(), &mut completed_peer, &peer_ip());
1707+
let announce_data =
1708+
tracker.announce(&sample_info_hash(), &mut completed_peer, &peer_ip(), &PeersWanted::All);
15731709

15741710
assert_eq!(announce_data.stats.downloaded, 1);
15751711
}
@@ -1583,7 +1719,7 @@ mod tests {
15831719
use torrust_tracker_primitives::info_hash::InfoHash;
15841720

15851721
use crate::core::tests::the_tracker::{complete_peer, incomplete_peer, public_tracker};
1586-
use crate::core::{ScrapeData, SwarmMetadata};
1722+
use crate::core::{PeersWanted, ScrapeData, SwarmMetadata};
15871723

15881724
#[tokio::test]
15891725
async fn it_should_return_a_zeroed_swarm_metadata_for_the_requested_file_if_the_tracker_does_not_have_that_torrent(
@@ -1609,11 +1745,21 @@ mod tests {
16091745

16101746
// Announce a "complete" peer for the torrent
16111747
let mut complete_peer = complete_peer();
1612-
tracker.announce(&info_hash, &mut complete_peer, &IpAddr::V4(Ipv4Addr::new(126, 0, 0, 10)));
1748+
tracker.announce(
1749+
&info_hash,
1750+
&mut complete_peer,
1751+
&IpAddr::V4(Ipv4Addr::new(126, 0, 0, 10)),
1752+
&PeersWanted::All,
1753+
);
16131754

16141755
// Announce an "incomplete" peer for the torrent
16151756
let mut incomplete_peer = incomplete_peer();
1616-
tracker.announce(&info_hash, &mut incomplete_peer, &IpAddr::V4(Ipv4Addr::new(126, 0, 0, 11)));
1757+
tracker.announce(
1758+
&info_hash,
1759+
&mut incomplete_peer,
1760+
&IpAddr::V4(Ipv4Addr::new(126, 0, 0, 11)),
1761+
&PeersWanted::All,
1762+
);
16171763

16181764
// Scrape
16191765
let scrape_data = tracker.scrape(&vec![info_hash]).await;
@@ -1740,7 +1886,7 @@ mod tests {
17401886
use crate::core::tests::the_tracker::{
17411887
complete_peer, incomplete_peer, peer_ip, sample_info_hash, whitelisted_tracker,
17421888
};
1743-
use crate::core::ScrapeData;
1889+
use crate::core::{PeersWanted, ScrapeData};
17441890

17451891
#[test]
17461892
fn it_should_be_able_to_build_a_zeroed_scrape_data_for_a_list_of_info_hashes() {
@@ -1761,11 +1907,11 @@ mod tests {
17611907
let info_hash = "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap();
17621908

17631909
let mut peer = incomplete_peer();
1764-
tracker.announce(&info_hash, &mut peer, &peer_ip());
1910+
tracker.announce(&info_hash, &mut peer, &peer_ip(), &PeersWanted::All);
17651911

17661912
// Announce twice to force non zeroed swarm metadata
17671913
let mut peer = complete_peer();
1768-
tracker.announce(&info_hash, &mut peer, &peer_ip());
1914+
tracker.announce(&info_hash, &mut peer, &peer_ip(), &PeersWanted::All);
17691915

17701916
let scrape_data = tracker.scrape(&vec![info_hash]).await;
17711917

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

0 commit comments

Comments
 (0)