diff --git a/Cargo.lock b/Cargo.lock index 6f9d9231b..05b439353 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -183,6 +192,21 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.6.2", + "object", + "rustc-demangle", +] + [[package]] name = "base-x" version = "0.2.11" @@ -236,6 +260,15 @@ dependencies = [ "which", ] +[[package]] +name = "bip_bencode" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6048cc5d9680544a5098a290d2845df7dae292c97687b9896b70365bad0ea416" +dependencies = [ + "error-chain", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -637,6 +670,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "error-chain" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" +dependencies = [ + "backtrace", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -675,7 +717,7 @@ checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ "crc32fast", "libz-sys", - "miniz_oxide", + "miniz_oxide 0.5.4", ] [[package]] @@ -913,6 +955,12 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + [[package]] name = "glob" version = "0.3.0" @@ -1381,6 +1429,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.4" @@ -1643,6 +1700,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.15.0" @@ -2120,6 +2186,12 @@ dependencies = [ "serde", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2862,6 +2934,7 @@ dependencies = [ "axum-client-ip", "axum-server", "binascii", + "bip_bencode", "chrono", "config", "derive_more", diff --git a/Cargo.toml b/Cargo.toml index 75ffa7935..917bc9e31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ uuid = { version = "1", features = ["v4"] } axum = "0.6.1" axum-server = { version = "0.4.4", features = ["tls-rustls"] } axum-client-ip = "0.4.0" +bip_bencode = "0.4.4" [dev-dependencies] diff --git a/src/http/axum_implementation/extractors/peer_ip.rs b/src/http/axum_implementation/extractors/peer_ip.rs index 7d615d0dc..9f7e92a9b 100644 --- a/src/http/axum_implementation/extractors/peer_ip.rs +++ b/src/http/axum_implementation/extractors/peer_ip.rs @@ -9,7 +9,9 @@ use crate::http::axum_implementation::responses; #[derive(Error, Debug)] pub enum ResolutionError { - #[error("missing the right most X-Forwarded-For IP (mandatory on reverse proxy tracker configuration) in {location}")] + #[error( + "missing or invalid the right most X-Forwarded-For IP (mandatory on reverse proxy tracker configuration) in {location}" + )] MissingRightMostXForwardedForIp { location: &'static Location<'static> }, #[error("cannot get the client IP from the connection info in {location}")] MissingClientIp { location: &'static Location<'static> }, diff --git a/src/http/axum_implementation/handlers/announce.rs b/src/http/axum_implementation/handlers/announce.rs index 0960510ba..81f57e810 100644 --- a/src/http/axum_implementation/handlers/announce.rs +++ b/src/http/axum_implementation/handlers/announce.rs @@ -9,8 +9,9 @@ use log::debug; use crate::http::axum_implementation::extractors::announce_request::ExtractRequest; use crate::http::axum_implementation::extractors::peer_ip::assign_ip_address_to_peer; use crate::http::axum_implementation::extractors::remote_client_ip::RemoteClientIp; -use crate::http::axum_implementation::requests::announce::{Announce, Event}; -use crate::http::axum_implementation::{responses, services}; +use crate::http::axum_implementation::requests::announce::{Announce, Compact, Event}; +use crate::http::axum_implementation::responses::announce; +use crate::http::axum_implementation::services; use crate::protocol::clock::{Current, Time}; use crate::tracker::peer::Peer; use crate::tracker::Tracker; @@ -30,9 +31,16 @@ pub async fn handle( let mut peer = peer_from_request(&announce_request, &peer_ip); - let response = 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).await; - responses::announce::Announce::from(response).into_response() + match announce_request.compact { + Some(compact) => match compact { + Compact::Accepted => announce::Compact::from(announce_data).into_response(), + Compact::NotAccepted => announce::NonCompact::from(announce_data).into_response(), + }, + // Default response format non compact + None => announce::NonCompact::from(announce_data).into_response(), + } } /// It ignores the peer address in the announce request params. diff --git a/src/http/axum_implementation/responses/announce.rs b/src/http/axum_implementation/responses/announce.rs index 63ec74ac2..16bb51e4c 100644 --- a/src/http/axum_implementation/responses/announce.rs +++ b/src/http/axum_implementation/responses/announce.rs @@ -1,13 +1,23 @@ +use std::io::Write; use std::net::IpAddr; +use std::panic::Location; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; +use bip_bencode::{ben_bytes, ben_int, ben_map}; use serde::{self, Deserialize, Serialize}; +use thiserror::Error; -use crate::tracker::{self, AnnounceResponse}; +use crate::http::axum_implementation::responses; +use crate::tracker::{self, AnnounceData}; +/// Normal (non compact) "announce" response +/// +/// BEP 03: The ``BitTorrent`` Protocol Specification +/// +/// #[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct Announce { +pub struct NonCompact { pub interval: u32, #[serde(rename = "min interval")] pub interval_min: u32, @@ -33,7 +43,7 @@ impl From for Peer { } } -impl Announce { +impl NonCompact { /// # Panics /// /// It would panic if the `Announce` struct contained an inappropriate type. @@ -43,14 +53,14 @@ impl Announce { } } -impl IntoResponse for Announce { +impl IntoResponse for NonCompact { fn into_response(self) -> Response { (StatusCode::OK, self.write()).into_response() } } -impl From for Announce { - fn from(domain_announce_response: AnnounceResponse) -> Self { +impl From for NonCompact { + fn from(domain_announce_response: AnnounceData) -> Self { let peers: Vec = domain_announce_response.peers.iter().map(|peer| Peer::from(*peer)).collect(); Self { @@ -63,29 +73,222 @@ impl From for Announce { } } +/// Compact "announce" response +/// +/// BEP 23: Tracker Returns Compact Peer Lists +/// +/// +/// BEP 07: IPv6 Tracker Extension +/// +/// +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct Compact { + pub interval: u32, + #[serde(rename = "min interval")] + pub interval_min: u32, + pub complete: u32, + pub incomplete: u32, + pub peers: Vec, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct CompactPeer { + pub ip: IpAddr, + pub port: u16, +} + +impl CompactPeer { + /// # Errors + /// + /// Will return `Err` if internally interrupted. + pub fn write(&self) -> Result, Box> { + let mut bytes: Vec = Vec::new(); + match self.ip { + IpAddr::V4(ip) => { + bytes.write_all(&u32::from(ip).to_be_bytes())?; + } + IpAddr::V6(ip) => { + bytes.write_all(&u128::from(ip).to_be_bytes())?; + } + } + bytes.write_all(&self.port.to_be_bytes())?; + Ok(bytes) + } +} + +impl From for CompactPeer { + fn from(peer: tracker::peer::Peer) -> Self { + CompactPeer { + ip: peer.peer_addr.ip(), + port: peer.peer_addr.port(), + } + } +} + +impl Compact { + /// # Errors + /// + /// Will return `Err` if internally interrupted. + pub fn write(&self) -> Result, Box> { + let mut peers_v4: Vec = Vec::new(); + for compact_peer in &self.peers { + match compact_peer.ip { + IpAddr::V4(_ip) => { + let peer_bytes = compact_peer.write()?; + peers_v4.write_all(&peer_bytes)?; + } + IpAddr::V6(_) => {} + } + } + + let mut peers_v6: Vec = Vec::new(); + for compact_peer in &self.peers { + match compact_peer.ip { + IpAddr::V6(_ip) => { + let peer_bytes = compact_peer.write()?; + peers_v6.write_all(&peer_bytes)?; + } + IpAddr::V4(_) => {} + } + } + + let bytes = (ben_map! { + "complete" => ben_int!(i64::from(self.complete)), + "incomplete" => ben_int!(i64::from(self.incomplete)), + "interval" => ben_int!(i64::from(self.interval)), + "min interval" => ben_int!(i64::from(self.interval_min)), + "peers" => ben_bytes!(peers_v4), + "peers6" => ben_bytes!(peers_v6) + }) + .encode(); + + Ok(bytes) + } +} + +#[derive(Error, Debug)] +pub enum CompactSerializationError { + #[error("cannot write bytes: {inner_error} in {location}")] + CannotWriteBytes { + location: &'static Location<'static>, + inner_error: String, + }, +} + +impl From for responses::error::Error { + fn from(err: CompactSerializationError) -> Self { + responses::error::Error { + failure_reason: format!("{err}"), + } + } +} + +impl IntoResponse for Compact { + fn into_response(self) -> Response { + match self.write() { + Ok(bytes) => (StatusCode::OK, bytes).into_response(), + Err(err) => responses::error::Error::from(CompactSerializationError::CannotWriteBytes { + location: Location::caller(), + inner_error: format!("{err}"), + }) + .into_response(), + } + } +} + +impl From for Compact { + fn from(domain_announce_response: AnnounceData) -> Self { + let peers: Vec = domain_announce_response + .peers + .iter() + .map(|peer| CompactPeer::from(*peer)) + .collect(); + + Self { + interval: domain_announce_response.interval, + interval_min: domain_announce_response.interval_min, + complete: domain_announce_response.swam_stats.seeders, + incomplete: domain_announce_response.swam_stats.leechers, + peers, + } + } +} + #[cfg(test)] mod tests { - use std::net::IpAddr; - use std::str::FromStr; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - use super::{Announce, Peer}; + use super::{NonCompact, Peer}; + use crate::http::axum_implementation::responses::announce::{Compact, CompactPeer}; + + // Some ascii values used in tests: + // + // +-----------------+ + // | Dec | Hex | Chr | + // +-----------------+ + // | 105 | 69 | i | + // | 112 | 70 | p | + // +-----------------+ + // + // IP addresses and port numbers used in tests are chosen so that their bencoded representation + // is also a valid string which makes asserts more readable. + + #[test] + fn non_compact_announce_response_can_be_bencoded() { + let response = NonCompact { + interval: 111, + interval_min: 222, + complete: 333, + incomplete: 444, + peers: vec![ + // IPV4 + Peer { + peer_id: "-qB00000000000000001".to_string(), + ip: IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), // 105.105.105.105 + port: 0x7070, // 28784 + }, + // IPV6 + Peer { + peer_id: "-qB00000000000000002".to_string(), + ip: IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)), + port: 0x7070, // 28784 + }, + ], + }; + + // cspell:disable-next-line + assert_eq!(response.write(), "d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peersld2:ip15:105.105.105.1057:peer_id20:-qB000000000000000014:porti28784eed2:ip39:6969:6969:6969:6969:6969:6969:6969:69697:peer_id20:-qB000000000000000024:porti28784eeee"); + } #[test] - fn announce_response_can_be_bencoded() { - let response = Announce { - interval: 1, - interval_min: 2, - complete: 3, - incomplete: 4, - peers: vec![Peer { - peer_id: "-qB00000000000000001".to_string(), - ip: IpAddr::from_str("127.0.0.1").unwrap(), - port: 8080, - }], + fn compact_announce_response_can_be_bencoded() { + let response = Compact { + interval: 111, + interval_min: 222, + complete: 333, + incomplete: 444, + peers: vec![ + // IPV4 + CompactPeer { + ip: IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), // 105.105.105.105 + port: 0x7070, // 28784 + }, + // IPV6 + CompactPeer { + ip: IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)), + port: 0x7070, // 28784 + }, + ], }; + let bytes = response.write().unwrap(); + // cspell:disable-next-line - assert_eq!(response.write(), "d8:completei3e10:incompletei4e8:intervali1e12:min intervali2e5:peersld2:ip9:127.0.0.17:peer_id20:-qB000000000000000014:porti8080eeee"); + assert_eq!( + bytes, + // cspell:disable-next-line + b"d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peers6:iiiipp6:peers618:iiiiiiiiiiiiiiiippe" + ); } } diff --git a/src/http/axum_implementation/services/announce.rs b/src/http/axum_implementation/services/announce.rs index 9481354ba..6378c3008 100644 --- a/src/http/axum_implementation/services/announce.rs +++ b/src/http/axum_implementation/services/announce.rs @@ -3,9 +3,9 @@ use std::sync::Arc; use crate::protocol::info_hash::InfoHash; use crate::tracker::peer::Peer; -use crate::tracker::{statistics, AnnounceResponse, Tracker}; +use crate::tracker::{statistics, AnnounceData, Tracker}; -pub async fn invoke(tracker: Arc, info_hash: InfoHash, peer: &mut Peer) -> AnnounceResponse { +pub async fn invoke(tracker: Arc, info_hash: InfoHash, peer: &mut Peer) -> AnnounceData { let original_peer_ip = peer.peer_addr.ip(); // The tracker could change the original peer ip diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs index cb3bd0e96..e01fe6a19 100644 --- a/src/tracker/mod.rs +++ b/src/tracker/mod.rs @@ -8,7 +8,7 @@ pub mod torrent; use std::collections::btree_map::Entry; use std::collections::BTreeMap; -use std::net::{IpAddr, SocketAddr}; +use std::net::IpAddr; use std::panic::Location; use std::sync::Arc; use std::time::Duration; @@ -43,7 +43,7 @@ pub struct TorrentsMetrics { pub torrents: u64, } -pub struct AnnounceResponse { +pub struct AnnounceData { pub peers: Vec, pub swam_stats: SwamStats, pub interval: u32, @@ -86,15 +86,14 @@ impl Tracker { } /// It handles an announce request - pub async fn announce(&self, info_hash: &InfoHash, peer: &mut Peer, remote_client_ip: &IpAddr) -> AnnounceResponse { + pub async fn announce(&self, info_hash: &InfoHash, peer: &mut Peer, remote_client_ip: &IpAddr) -> AnnounceData { peer.change_ip(&assign_ip_address_to_peer(remote_client_ip, self.config.get_ext_ip())); let swam_stats = self.update_torrent_with_peer_and_get_stats(info_hash, peer).await; - // todo: remove peer by using its `Id` instead of its socket address: `get_peers_excluding_peer(peer_id: peer::Id)` - let peers = self.get_peers_excluding_peers_with_address(info_hash, &peer.peer_addr).await; + let peers = self.get_peers_for_peer(info_hash, peer).await; - AnnounceResponse { + AnnounceData { peers, swam_stats, interval: self.config.announce_interval, @@ -298,16 +297,12 @@ impl Tracker { Ok(()) } - async fn get_peers_excluding_peers_with_address( - &self, - info_hash: &InfoHash, - excluded_address: &SocketAddr, - ) -> Vec { + async fn get_peers_for_peer(&self, info_hash: &InfoHash, peer: &Peer) -> Vec { let read_lock = self.torrents.read().await; match read_lock.get(info_hash) { None => vec![], - Some(entry) => entry.get_peers(Some(excluded_address)).into_iter().copied().collect(), + Some(entry) => entry.get_peers_for_peer(peer).into_iter().copied().collect(), } } @@ -317,11 +312,14 @@ impl Tracker { match read_lock.get(info_hash) { None => vec![], - Some(entry) => entry.get_peers(None).into_iter().copied().collect(), + Some(entry) => entry.get_all_peers().into_iter().copied().collect(), } } pub async fn update_torrent_with_peer_and_get_stats(&self, info_hash: &InfoHash, peer: &peer::Peer) -> torrent::SwamStats { + // code-review: consider splitting the function in two (command and query segregation). + // `update_torrent_with_peer` and `get_stats` + let mut torrents = self.torrents.write().await; let torrent_entry = match torrents.entry(*info_hash) { diff --git a/src/tracker/peer.rs b/src/tracker/peer.rs index 735754529..18ce1b75f 100644 --- a/src/tracker/peer.rs +++ b/src/tracker/peer.rs @@ -10,6 +10,12 @@ use crate::protocol::clock::DurationSinceUnixEpoch; use crate::protocol::common::{AnnounceEventDef, NumberOfBytesDef}; use crate::protocol::utils::ser_unix_time_value; +#[derive(PartialEq, Eq, Debug)] +pub enum IPVersion { + IPv4, + IPv6, +} + #[derive(PartialEq, Eq, Debug, Clone, Serialize, Copy)] pub struct Peer { pub peer_id: Id, @@ -37,6 +43,15 @@ impl Peer { pub fn change_ip(&mut self, new_ip: &IpAddr) { self.peer_addr = SocketAddr::new(*new_ip, self.peer_addr.port()); } + + /// The IP version used by the peer: IPV4 or IPV6 + #[must_use] + pub fn ip_version(&self) -> IPVersion { + if self.peer_addr.is_ipv4() { + return IPVersion::IPv4; + } + IPVersion::IPv6 + } } #[derive(PartialEq, Eq, Hash, Clone, Debug, PartialOrd, Ord, Copy)] diff --git a/src/tracker/services/torrent.rs b/src/tracker/services/torrent.rs index ba66d15f4..e2353876e 100644 --- a/src/tracker/services/torrent.rs +++ b/src/tracker/services/torrent.rs @@ -80,7 +80,7 @@ pub async fn get_torrent_info(tracker: Arc, info_hash: &InfoHash) -> Op let (seeders, completed, leechers) = torrent_entry.get_stats(); - let peers = torrent_entry.get_peers(None); + let peers = torrent_entry.get_all_peers(); let peers = Some(peers.iter().map(|peer| (**peer)).collect()); diff --git a/src/tracker/torrent.rs b/src/tracker/torrent.rs index b7b79f0f5..c2db6b027 100644 --- a/src/tracker/torrent.rs +++ b/src/tracker/torrent.rs @@ -1,10 +1,9 @@ -use std::net::{IpAddr, SocketAddr}; use std::time::Duration; use aquatic_udp_protocol::AnnounceEvent; use serde::{Deserialize, Serialize}; -use super::peer; +use super::peer::{self, Peer}; use crate::protocol::clock::{Current, TimeNow}; use crate::protocol::common::MAX_SCRAPE_TORRENTS; @@ -48,26 +47,24 @@ impl Entry { did_torrent_stats_change } + /// Get all peers, limiting the result to the maximum number of scrape torrents. #[must_use] - pub fn get_peers(&self, optional_excluded_address: Option<&SocketAddr>) -> Vec<&peer::Peer> { + pub fn get_all_peers(&self) -> Vec<&peer::Peer> { + self.peers.values().take(MAX_SCRAPE_TORRENTS as usize).collect() + } + + /// Returns the list of peers for a given client. The list filters out: + /// - The client peer that is making the request to the tracker + /// - Other peers that are not using the same IP version as the client peer. + #[must_use] + pub fn get_peers_for_peer(&self, client: &Peer) -> Vec<&peer::Peer> { self.peers .values() - .filter(|peer| match optional_excluded_address { - // Don't filter on ip_version - None => true, - // Filter out different ip_version from remote_addr - Some(excluded_address) => { - // Skip ip address of client - if peer.peer_addr.ip() == excluded_address.ip() { - return false; - } - - match peer.peer_addr.ip() { - IpAddr::V4(_) => excluded_address.is_ipv4(), - IpAddr::V6(_) => excluded_address.is_ipv6(), - } - } - }) + // Take peers which are not the client peer + .filter(|peer| peer.peer_addr != client.peer_addr) + // Take only peers with the same IP version as the client peer + .filter(|peer| peer.ip_version() == client.ip_version()) + // Limit the number of peers on the result .take(MAX_SCRAPE_TORRENTS as usize) .collect() } @@ -101,264 +98,317 @@ pub struct SwamStats { #[cfg(test)] mod tests { - use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - use std::ops::Sub; - use std::time::Duration; - use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; + mod torrent_entry { - use crate::protocol::clock::{Current, DurationSinceUnixEpoch, Stopped, StoppedTime, Time, Working}; - use crate::tracker::peer; - use crate::tracker::torrent::Entry; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + use std::ops::Sub; + use std::time::Duration; - struct TorrentPeerBuilder { - peer: peer::Peer, - } + use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes}; - impl TorrentPeerBuilder { - pub fn default() -> TorrentPeerBuilder { - let default_peer = peer::Peer { - peer_id: peer::Id([0u8; 20]), - peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), - updated: Current::now(), - uploaded: NumberOfBytes(0), - downloaded: NumberOfBytes(0), - left: NumberOfBytes(0), - event: AnnounceEvent::Started, - }; - TorrentPeerBuilder { peer: default_peer } - } + use crate::protocol::clock::{Current, DurationSinceUnixEpoch, Stopped, StoppedTime, Time, Working}; + use crate::tracker::peer; + use crate::tracker::torrent::Entry; - pub fn with_event_completed(mut self) -> Self { - self.peer.event = AnnounceEvent::Completed; - self + struct TorrentPeerBuilder { + peer: peer::Peer, } - pub fn with_peer_address(mut self, peer_addr: SocketAddr) -> Self { - self.peer.peer_addr = peer_addr; - self - } + impl TorrentPeerBuilder { + pub fn default() -> TorrentPeerBuilder { + let default_peer = peer::Peer { + peer_id: peer::Id([0u8; 20]), + peer_addr: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080), + updated: Current::now(), + uploaded: NumberOfBytes(0), + downloaded: NumberOfBytes(0), + left: NumberOfBytes(0), + event: AnnounceEvent::Started, + }; + TorrentPeerBuilder { peer: default_peer } + } + + pub fn with_event_completed(mut self) -> Self { + self.peer.event = AnnounceEvent::Completed; + self + } + + pub fn with_peer_address(mut self, peer_addr: SocketAddr) -> Self { + self.peer.peer_addr = peer_addr; + self + } + + pub fn with_peer_id(mut self, peer_id: peer::Id) -> Self { + self.peer.peer_id = peer_id; + self + } + + pub fn with_number_of_bytes_left(mut self, left: i64) -> Self { + self.peer.left = NumberOfBytes(left); + self + } - pub fn with_peer_id(mut self, peer_id: peer::Id) -> Self { - self.peer.peer_id = peer_id; - self + pub fn updated_at(mut self, updated: DurationSinceUnixEpoch) -> Self { + self.peer.updated = updated; + self + } + + pub fn into(self) -> peer::Peer { + self.peer + } } - pub fn with_number_of_bytes_left(mut self, left: i64) -> Self { - self.peer.left = NumberOfBytes(left); - self + /// A torrent seeder is a peer with 0 bytes left to download which + /// has not announced it has stopped + fn a_torrent_seeder() -> peer::Peer { + TorrentPeerBuilder::default() + .with_number_of_bytes_left(0) + .with_event_completed() + .into() } - pub fn updated_at(mut self, updated: DurationSinceUnixEpoch) -> Self { - self.peer.updated = updated; - self + /// A torrent leecher is a peer that is not a seeder. + /// Leecher: left > 0 OR event = Stopped + fn a_torrent_leecher() -> peer::Peer { + TorrentPeerBuilder::default() + .with_number_of_bytes_left(1) + .with_event_completed() + .into() } - pub fn into(self) -> peer::Peer { - self.peer + #[test] + fn the_default_torrent_entry_should_contain_an_empty_list_of_peers() { + let torrent_entry = Entry::new(); + + assert_eq!(torrent_entry.get_all_peers().len(), 0); } - } - /// A torrent seeder is a peer with 0 bytes left to download which - /// has not announced it has stopped - fn a_torrent_seeder() -> peer::Peer { - TorrentPeerBuilder::default() - .with_number_of_bytes_left(0) - .with_event_completed() - .into() - } + #[test] + fn a_new_peer_can_be_added_to_a_torrent_entry() { + let mut torrent_entry = Entry::new(); + let torrent_peer = TorrentPeerBuilder::default().into(); - /// A torrent leecher is a peer that is not a seeder. - /// Leecher: left > 0 OR event = Stopped - fn a_torrent_leecher() -> peer::Peer { - TorrentPeerBuilder::default() - .with_number_of_bytes_left(1) - .with_event_completed() - .into() - } + torrent_entry.update_peer(&torrent_peer); // Add the peer - #[test] - fn the_default_torrent_entry_should_contain_an_empty_list_of_peers() { - let torrent_entry = Entry::new(); + assert_eq!(*torrent_entry.get_all_peers()[0], torrent_peer); + assert_eq!(torrent_entry.get_all_peers().len(), 1); + } - assert_eq!(torrent_entry.get_peers(None).len(), 0); - } + #[test] + fn a_torrent_entry_should_contain_the_list_of_peers_that_were_added_to_the_torrent() { + let mut torrent_entry = Entry::new(); + let torrent_peer = TorrentPeerBuilder::default().into(); - #[test] - fn a_new_peer_can_be_added_to_a_torrent_entry() { - let mut torrent_entry = Entry::new(); - let torrent_peer = TorrentPeerBuilder::default().into(); + torrent_entry.update_peer(&torrent_peer); // Add the peer - torrent_entry.update_peer(&torrent_peer); // Add the peer + assert_eq!(torrent_entry.get_all_peers(), vec![&torrent_peer]); + } - assert_eq!(*torrent_entry.get_peers(None)[0], torrent_peer); - assert_eq!(torrent_entry.get_peers(None).len(), 1); - } + #[test] + fn a_peer_can_be_updated_in_a_torrent_entry() { + let mut torrent_entry = Entry::new(); + let mut torrent_peer = TorrentPeerBuilder::default().into(); + torrent_entry.update_peer(&torrent_peer); // Add the peer - #[test] - fn a_torrent_entry_should_contain_the_list_of_peers_that_were_added_to_the_torrent() { - let mut torrent_entry = Entry::new(); - let torrent_peer = TorrentPeerBuilder::default().into(); + torrent_peer.event = AnnounceEvent::Completed; // Update the peer + torrent_entry.update_peer(&torrent_peer); // Update the peer in the torrent entry - torrent_entry.update_peer(&torrent_peer); // Add the peer + assert_eq!(torrent_entry.get_all_peers()[0].event, AnnounceEvent::Completed); + } - assert_eq!(torrent_entry.get_peers(None), vec![&torrent_peer]); - } + #[test] + fn a_peer_should_be_removed_from_a_torrent_entry_when_the_peer_announces_it_has_stopped() { + let mut torrent_entry = Entry::new(); + let mut torrent_peer = TorrentPeerBuilder::default().into(); + torrent_entry.update_peer(&torrent_peer); // Add the peer - #[test] - fn a_peer_can_be_updated_in_a_torrent_entry() { - let mut torrent_entry = Entry::new(); - let mut torrent_peer = TorrentPeerBuilder::default().into(); - torrent_entry.update_peer(&torrent_peer); // Add the peer + torrent_peer.event = AnnounceEvent::Stopped; // Update the peer + torrent_entry.update_peer(&torrent_peer); // Update the peer in the torrent entry - torrent_peer.event = AnnounceEvent::Completed; // Update the peer - torrent_entry.update_peer(&torrent_peer); // Update the peer in the torrent entry + assert_eq!(torrent_entry.get_all_peers().len(), 0); + } - assert_eq!(torrent_entry.get_peers(None)[0].event, AnnounceEvent::Completed); - } + #[test] + fn torrent_stats_change_when_a_previously_known_peer_announces_it_has_completed_the_torrent() { + let mut torrent_entry = Entry::new(); + let mut torrent_peer = TorrentPeerBuilder::default().into(); - #[test] - fn a_peer_should_be_removed_from_a_torrent_entry_when_the_peer_announces_it_has_stopped() { - let mut torrent_entry = Entry::new(); - let mut torrent_peer = TorrentPeerBuilder::default().into(); - torrent_entry.update_peer(&torrent_peer); // Add the peer + torrent_entry.update_peer(&torrent_peer); // Add the peer - torrent_peer.event = AnnounceEvent::Stopped; // Update the peer - torrent_entry.update_peer(&torrent_peer); // Update the peer in the torrent entry + torrent_peer.event = AnnounceEvent::Completed; // Update the peer + let stats_have_changed = torrent_entry.update_peer(&torrent_peer); // Update the peer in the torrent entry - assert_eq!(torrent_entry.get_peers(None).len(), 0); - } + assert!(stats_have_changed); + } - #[test] - fn torrent_stats_change_when_a_previously_known_peer_announces_it_has_completed_the_torrent() { - let mut torrent_entry = Entry::new(); - let mut torrent_peer = TorrentPeerBuilder::default().into(); + #[test] + fn torrent_stats_should_not_change_when_a_peer_announces_it_has_completed_the_torrent_if_it_is_the_first_announce_from_the_peer( + ) { + let mut torrent_entry = Entry::new(); + let torrent_peer_announcing_complete_event = TorrentPeerBuilder::default().with_event_completed().into(); - torrent_entry.update_peer(&torrent_peer); // Add the peer + // Add a peer that did not exist before in the entry + let torrent_stats_have_not_changed = !torrent_entry.update_peer(&torrent_peer_announcing_complete_event); - torrent_peer.event = AnnounceEvent::Completed; // Update the peer - let stats_have_changed = torrent_entry.update_peer(&torrent_peer); // Update the peer in the torrent entry + assert!(torrent_stats_have_not_changed); + } - assert!(stats_have_changed); - } + #[test] + fn a_torrent_entry_should_return_the_list_of_peers_for_a_given_peer_filtering_out_the_client_that_is_making_the_request() + { + let mut torrent_entry = Entry::new(); + let peer_socket_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let torrent_peer = TorrentPeerBuilder::default().with_peer_address(peer_socket_address).into(); + torrent_entry.update_peer(&torrent_peer); // Add peer - #[test] - fn torrent_stats_should_not_change_when_a_peer_announces_it_has_completed_the_torrent_if_it_is_the_first_announce_from_the_peer( - ) { - let mut torrent_entry = Entry::new(); - let torrent_peer_announcing_complete_event = TorrentPeerBuilder::default().with_event_completed().into(); + // Get peers excluding the one we have just added + let peers = torrent_entry.get_peers_for_peer(&torrent_peer); - // Add a peer that did not exist before in the entry - let torrent_stats_have_not_changed = !torrent_entry.update_peer(&torrent_peer_announcing_complete_event); + assert_eq!(peers.len(), 0); + } - assert!(torrent_stats_have_not_changed); - } + #[test] + fn a_torrent_entry_should_return_the_list_of_peers_for_a_given_peer_filtering_out_peers_that_do_not_use_the_same_ip_version( + ) { + let mut torrent_entry = Entry::new(); - #[test] - fn a_torrent_entry_could_filter_out_peers_with_a_given_socket_address() { - let mut torrent_entry = Entry::new(); - let peer_socket_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); - let torrent_peer = TorrentPeerBuilder::default().with_peer_address(peer_socket_address).into(); - torrent_entry.update_peer(&torrent_peer); // Add peer + // Add peer 1 using IPV4 + let peer1_socket_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + let torrent_peer_1 = TorrentPeerBuilder::default().with_peer_address(peer1_socket_address).into(); + torrent_entry.update_peer(&torrent_peer_1); - // Get peers excluding the one we have just added - let peers = torrent_entry.get_peers(Some(&peer_socket_address)); + // Add peer 2 using IPV6 + let peer2_socket_address = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff)), 8080); + let torrent_peer_2 = TorrentPeerBuilder::default().with_peer_address(peer2_socket_address).into(); + torrent_entry.update_peer(&torrent_peer_2); - assert_eq!(peers.len(), 0); - } + // Get peers for peer 1 + let peers = torrent_entry.get_peers_for_peer(&torrent_peer_1); - fn peer_id_from_i32(number: i32) -> peer::Id { - let peer_id = number.to_le_bytes(); - peer::Id([ - 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, peer_id[0], peer_id[1], peer_id[2], - peer_id[3], - ]) - } + // Peer using IPV6 should not be included + assert_eq!(peers.len(), 0); + } + + #[test] + fn two_peers_with_the_same_ip_but_different_port_should_be_considered_different_peers() { + let mut torrent_entry = Entry::new(); - #[test] - fn the_tracker_should_limit_the_list_of_peers_to_74_when_clients_scrape_torrents() { - let mut torrent_entry = Entry::new(); + let peer_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + + // Add peer 1 + let torrent_peer_1 = TorrentPeerBuilder::default() + .with_peer_address(SocketAddr::new(peer_ip, 8080)) + .into(); + torrent_entry.update_peer(&torrent_peer_1); - // We add one more peer than the scrape limit - for peer_number in 1..=74 + 1 { - let torrent_peer = TorrentPeerBuilder::default() - .with_peer_id(peer_id_from_i32(peer_number)) + // Add peer 2 + let torrent_peer_2 = TorrentPeerBuilder::default() + .with_peer_address(SocketAddr::new(peer_ip, 8081)) .into(); - torrent_entry.update_peer(&torrent_peer); + torrent_entry.update_peer(&torrent_peer_2); + + // Get peers for peer 1 + let peers = torrent_entry.get_peers_for_peer(&torrent_peer_1); + + // The peer 2 using the same IP but different port should be included + assert_eq!(peers[0].peer_addr.ip(), Ipv4Addr::new(127, 0, 0, 1)); + assert_eq!(peers[0].peer_addr.port(), 8081); } - let peers = torrent_entry.get_peers(None); + fn peer_id_from_i32(number: i32) -> peer::Id { + let peer_id = number.to_le_bytes(); + peer::Id([ + 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, peer_id[0], peer_id[1], + peer_id[2], peer_id[3], + ]) + } - assert_eq!(peers.len(), 74); - } + #[test] + fn the_tracker_should_limit_the_list_of_peers_to_74_when_clients_scrape_torrents() { + let mut torrent_entry = Entry::new(); - #[test] - fn torrent_stats_should_have_the_number_of_seeders_for_a_torrent() { - let mut torrent_entry = Entry::new(); - let torrent_seeder = a_torrent_seeder(); + // We add one more peer than the scrape limit + for peer_number in 1..=74 + 1 { + let torrent_peer = TorrentPeerBuilder::default() + .with_peer_id(peer_id_from_i32(peer_number)) + .into(); + torrent_entry.update_peer(&torrent_peer); + } - torrent_entry.update_peer(&torrent_seeder); // Add seeder + let peers = torrent_entry.get_all_peers(); - assert_eq!(torrent_entry.get_stats().0, 1); - } + assert_eq!(peers.len(), 74); + } - #[test] - fn torrent_stats_should_have_the_number_of_leechers_for_a_torrent() { - let mut torrent_entry = Entry::new(); - let torrent_leecher = a_torrent_leecher(); + #[test] + fn torrent_stats_should_have_the_number_of_seeders_for_a_torrent() { + let mut torrent_entry = Entry::new(); + let torrent_seeder = a_torrent_seeder(); - torrent_entry.update_peer(&torrent_leecher); // Add leecher + torrent_entry.update_peer(&torrent_seeder); // Add seeder - assert_eq!(torrent_entry.get_stats().2, 1); - } + assert_eq!(torrent_entry.get_stats().0, 1); + } - #[test] - fn torrent_stats_should_have_the_number_of_peers_that_having_announced_at_least_two_events_the_latest_one_is_the_completed_event( - ) { - let mut torrent_entry = Entry::new(); - let mut torrent_peer = TorrentPeerBuilder::default().into(); - torrent_entry.update_peer(&torrent_peer); // Add the peer + #[test] + fn torrent_stats_should_have_the_number_of_leechers_for_a_torrent() { + let mut torrent_entry = Entry::new(); + let torrent_leecher = a_torrent_leecher(); - // Announce "Completed" torrent download event. - torrent_peer.event = AnnounceEvent::Completed; - torrent_entry.update_peer(&torrent_peer); // Update the peer + torrent_entry.update_peer(&torrent_leecher); // Add leecher - let number_of_previously_known_peers_with_completed_torrent = torrent_entry.get_stats().1; + assert_eq!(torrent_entry.get_stats().2, 1); + } - assert_eq!(number_of_previously_known_peers_with_completed_torrent, 1); - } + #[test] + fn torrent_stats_should_have_the_number_of_peers_that_having_announced_at_least_two_events_the_latest_one_is_the_completed_event( + ) { + let mut torrent_entry = Entry::new(); + let mut torrent_peer = TorrentPeerBuilder::default().into(); + torrent_entry.update_peer(&torrent_peer); // Add the peer - #[test] - fn torrent_stats_should_not_include_a_peer_in_the_completed_counter_if_the_peer_has_announced_only_one_event() { - let mut torrent_entry = Entry::new(); - let torrent_peer_announcing_complete_event = TorrentPeerBuilder::default().with_event_completed().into(); + // Announce "Completed" torrent download event. + torrent_peer.event = AnnounceEvent::Completed; + torrent_entry.update_peer(&torrent_peer); // Update the peer - // Announce "Completed" torrent download event. - // It's the first event announced from this peer. - torrent_entry.update_peer(&torrent_peer_announcing_complete_event); // Add the peer + let number_of_previously_known_peers_with_completed_torrent = torrent_entry.get_stats().1; - let number_of_peers_with_completed_torrent = torrent_entry.get_stats().1; + assert_eq!(number_of_previously_known_peers_with_completed_torrent, 1); + } - assert_eq!(number_of_peers_with_completed_torrent, 0); - } + #[test] + fn torrent_stats_should_not_include_a_peer_in_the_completed_counter_if_the_peer_has_announced_only_one_event() { + let mut torrent_entry = Entry::new(); + let torrent_peer_announcing_complete_event = TorrentPeerBuilder::default().with_event_completed().into(); + + // Announce "Completed" torrent download event. + // It's the first event announced from this peer. + torrent_entry.update_peer(&torrent_peer_announcing_complete_event); // Add the peer - #[test] - fn a_torrent_entry_should_remove_a_peer_not_updated_after_a_timeout_in_seconds() { - let mut torrent_entry = Entry::new(); + let number_of_peers_with_completed_torrent = torrent_entry.get_stats().1; - let timeout = 120u32; + assert_eq!(number_of_peers_with_completed_torrent, 0); + } - let now = Working::now(); - Stopped::local_set(&now); + #[test] + fn a_torrent_entry_should_remove_a_peer_not_updated_after_a_timeout_in_seconds() { + let mut torrent_entry = Entry::new(); - let timeout_seconds_before_now = now.sub(Duration::from_secs(u64::from(timeout))); - let inactive_peer = TorrentPeerBuilder::default() - .updated_at(timeout_seconds_before_now.sub(Duration::from_secs(1))) - .into(); - torrent_entry.update_peer(&inactive_peer); // Add the peer + let timeout = 120u32; - torrent_entry.remove_inactive_peers(timeout); + let now = Working::now(); + Stopped::local_set(&now); - assert_eq!(torrent_entry.peers.len(), 0); + let timeout_seconds_before_now = now.sub(Duration::from_secs(u64::from(timeout))); + let inactive_peer = TorrentPeerBuilder::default() + .updated_at(timeout_seconds_before_now.sub(Duration::from_secs(1))) + .into(); + torrent_entry.update_peer(&inactive_peer); // Add the peer + + torrent_entry.remove_inactive_peers(timeout); + + assert_eq!(torrent_entry.peers.len(), 0); + } } } diff --git a/tests/http/asserts.rs b/tests/http/asserts.rs index e146f252d..ffb857951 100644 --- a/tests/http/asserts.rs +++ b/tests/http/asserts.rs @@ -138,6 +138,16 @@ pub async fn assert_could_not_find_remote_address_on_xff_header_error_response(r ); } +pub async fn assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response: Response) { + assert_eq!(response.status(), 200); + + assert_bencoded_error( + &response.text().await.unwrap(), + "missing or invalid the right most X-Forwarded-For IP (mandatory on reverse proxy tracker configuration)", + Location::caller(), + ); +} + pub async fn assert_invalid_remote_address_on_xff_header_error_response(response: Response) { assert_eq!(response.status(), 200); diff --git a/tests/http_tracker.rs b/tests/http_tracker.rs index ded30a0b4..413d28bcf 100644 --- a/tests/http_tracker.rs +++ b/tests/http_tracker.rs @@ -1405,19 +1405,15 @@ mod axum_http_tracker_server { mod and_running_on_reverse_proxy { use torrust_tracker::http::Version; - 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::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::server::start_http_tracker_on_reverse_proxy; - //#[tokio::test] - #[allow(dead_code)] + #[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. + // right most IP in the `X-Forwarded-For` HTTP header, which is the IP of the proxy's client. let http_tracker_server = start_http_tracker_on_reverse_proxy(Version::Axum).await; @@ -1427,11 +1423,10 @@ mod axum_http_tracker_server { .get(&format!("announce?{params}")) .await; - assert_could_not_find_remote_address_on_xff_header_error_response(response).await; + assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; } - //#[tokio::test] - #[allow(dead_code)] + #[tokio::test] async fn should_fail_when_the_xff_http_request_header_contains_an_invalid_ip() { let http_tracker_server = start_http_tracker_on_reverse_proxy(Version::Axum).await; @@ -1441,7 +1436,7 @@ mod axum_http_tracker_server { .get_with_header(&format!("announce?{params}"), "X-Forwarded-For", "INVALID IP") .await; - assert_invalid_remote_address_on_xff_header_error_response(response).await; + assert_could_not_find_remote_address_on_x_forwarded_for_header_error_response(response).await; } } @@ -1833,8 +1828,7 @@ mod axum_http_tracker_server { assert_empty_announce_response(response).await; } - //#[tokio::test] - #[allow(dead_code)] + #[tokio::test] async fn should_return_the_compact_response() { // Tracker Returns Compact Peer Lists // https://www.bittorrent.org/beps/bep_0023.html