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..a91266490 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_list, ben_map, BMutAccess, BencodeMut}; 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, @@ -18,39 +28,63 @@ pub struct Announce { #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct Peer { - pub peer_id: String, + pub peer_id: [u8; 20], pub ip: IpAddr, pub port: u16, } +impl Peer { + #[must_use] + pub fn ben_map(&self) -> BencodeMut { + ben_map! { + "peer id" => ben_bytes!(self.peer_id.clone().to_vec()), + "ip" => ben_bytes!(self.ip.to_string()), + "port" => ben_int!(i64::from(self.port)) + } + } +} + impl From for Peer { fn from(peer: tracker::peer::Peer) -> Self { Peer { - peer_id: peer.peer_id.to_string(), + peer_id: peer.peer_id.to_bytes(), ip: peer.peer_addr.ip(), port: peer.peer_addr.port(), } } } -impl Announce { +impl NonCompact { /// # Panics /// - /// It would panic if the `Announce` struct contained an inappropriate type. + /// Will return an error if it can't access the bencode as a mutable `BListAccess`. #[must_use] - pub fn write(&self) -> String { - serde_bencode::to_string(&self).unwrap() + pub fn body(&self) -> Vec { + let mut peers_list = ben_list!(); + let peers_list_mut = peers_list.list_mut().unwrap(); + for peer in &self.peers { + peers_list_mut.push(peer.ben_map()); + } + + (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" => peers_list.clone() + }) + .encode() } } -impl IntoResponse for Announce { +impl IntoResponse for NonCompact { fn into_response(self) -> Response { - (StatusCode::OK, self.write()).into_response() + (StatusCode::OK, self.body()).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 +97,237 @@ 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 bytes(&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 body(&self) -> Result, Box> { + 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!(self.peers_v4_bytes()?), + "peers6" => ben_bytes!(self.peers_v6_bytes()?) + }) + .encode(); + + Ok(bytes) + } + + fn peers_v4_bytes(&self) -> Result, Box> { + let mut bytes: Vec = Vec::new(); + for compact_peer in &self.peers { + match compact_peer.ip { + IpAddr::V4(_ip) => { + let peer_bytes = compact_peer.bytes()?; + bytes.write_all(&peer_bytes)?; + } + IpAddr::V6(_) => {} + } + } + Ok(bytes) + } + + fn peers_v6_bytes(&self) -> Result, Box> { + let mut bytes: Vec = Vec::new(); + for compact_peer in &self.peers { + match compact_peer.ip { + IpAddr::V6(_ip) => { + let peer_bytes = compact_peer.bytes()?; + bytes.write_all(&peer_bytes)?; + } + IpAddr::V4(_) => {} + } + } + 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.body() { + 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::{NonCompact, Peer}; + use crate::http::axum_implementation::responses::announce::{Compact, CompactPeer}; - use super::{Announce, Peer}; + // 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 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 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: *b"-qB00000000000000001", + ip: IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), // 105.105.105.105 + port: 0x7070, // 28784 + }, + // IPV6 + Peer { + peer_id: *b"-qB00000000000000002", + ip: IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)), + port: 0x7070, // 28784 + }, + ], }; + let bytes = response.body(); + // cspell:disable-next-line - assert_eq!(response.write(), "d8:completei3e10:incompletei4e8:intervali1e12:min intervali2e5:peersld2:ip9:127.0.0.17:peer_id20:-qB000000000000000014:porti8080eeee"); + let expected_bytes = b"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"; + + assert_eq!( + String::from_utf8(bytes).unwrap(), + String::from_utf8(expected_bytes.to_vec()).unwrap() + ); + } + + #[test] + 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.body().unwrap(); + + let expected_bytes = + // cspell:disable-next-line + b"d8:completei333e10:incompletei444e8:intervali111e12:min intervali222e5:peers6:iiiipp6:peers618:iiiiiiiiiiiiiiiippe"; + + assert_eq!( + String::from_utf8(bytes).unwrap(), + String::from_utf8(expected_bytes.to_vec()).unwrap() + ); } } 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..c6d87f036 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)] @@ -69,6 +84,11 @@ impl Id { ret.0.clone_from_slice(bytes); ret } + + #[must_use] + pub fn to_bytes(&self) -> [u8; 20] { + self.0 + } } impl From<[u8; 20]> for Id { @@ -354,6 +374,11 @@ mod test { ]); assert_eq!(id.to_string(), "009f9296009f9296009f9296009f9296009f9296"); } + + #[test] + fn should_return_the_inner_bytes() { + assert_eq!(peer::Id(*b"-qB00000000000000000").to_bytes(), *b"-qB00000000000000000"); + } } mod torrent_peer { 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..3161cd36b 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,21 @@ 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. + /// It filters out the input 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) + // Limit the number of peers on the result .take(MAX_SCRAPE_TORRENTS as usize) .collect() } @@ -101,264 +95,295 @@ 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, 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_peer_id(mut self, peer_id: peer::Id) -> Self { - self.peer.peer_id = peer_id; - self + 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 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 two_peers_with_the_same_ip_but_different_port_should_be_considered_different_peers() { + 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 + let peer_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); - // Get peers excluding the one we have just added - let peers = torrent_entry.get_peers(Some(&peer_socket_address)); + // 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); - assert_eq!(peers.len(), 0); - } + // Add peer 2 + let torrent_peer_2 = TorrentPeerBuilder::default() + .with_peer_address(SocketAddr::new(peer_ip, 8081)) + .into(); + torrent_entry.update_peer(&torrent_peer_2); - 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], - ]) - } + // Get peers for peer 1 + let peers = torrent_entry.get_peers_for_peer(&torrent_peer_1); - #[test] - fn the_tracker_should_limit_the_list_of_peers_to_74_when_clients_scrape_torrents() { - let mut torrent_entry = Entry::new(); + // 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); + } - // 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); + 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], + ]) } - let peers = torrent_entry.get_peers(None); + #[test] + fn the_tracker_should_limit_the_list_of_peers_to_74_when_clients_scrape_torrents() { + let mut torrent_entry = Entry::new(); - assert_eq!(peers.len(), 74); - } + // 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); + } - #[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(); + let peers = torrent_entry.get_all_peers(); - torrent_entry.update_peer(&torrent_seeder); // Add seeder + assert_eq!(peers.len(), 74); + } - assert_eq!(torrent_entry.get_stats().0, 1); - } + #[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(); - #[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(); + torrent_entry.update_peer(&torrent_seeder); // Add seeder - torrent_entry.update_peer(&torrent_leecher); // Add leecher + assert_eq!(torrent_entry.get_stats().0, 1); + } - assert_eq!(torrent_entry.get_stats().2, 1); - } + #[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_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 + torrent_entry.update_peer(&torrent_leecher); // Add leecher - // Announce "Completed" torrent download event. - torrent_peer.event = AnnounceEvent::Completed; - torrent_entry.update_peer(&torrent_peer); // Update the peer + assert_eq!(torrent_entry.get_stats().2, 1); + } - let number_of_previously_known_peers_with_completed_torrent = torrent_entry.get_stats().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 - assert_eq!(number_of_previously_known_peers_with_completed_torrent, 1); - } + // Announce "Completed" torrent download event. + torrent_peer.event = AnnounceEvent::Completed; + torrent_entry.update_peer(&torrent_peer); // Update 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(); + let number_of_previously_known_peers_with_completed_torrent = torrent_entry.get_stats().1; - // 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 + assert_eq!(number_of_previously_known_peers_with_completed_torrent, 1); + } - let number_of_peers_with_completed_torrent = torrent_entry.get_stats().1; + #[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(); - assert_eq!(number_of_peers_with_completed_torrent, 0); - } + // 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/api/server.rs b/tests/api/server.rs index c1cd0630a..0e23a4320 100644 --- a/tests/api/server.rs +++ b/tests/api/server.rs @@ -72,7 +72,7 @@ impl Server { } /// Add a torrent to the tracker - pub async fn add_torrent(&self, info_hash: &InfoHash, peer: &Peer) { + pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &Peer) { self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; } } diff --git a/tests/common/fixtures.rs b/tests/common/fixtures.rs index 1ead0db0c..d4b3e9812 100644 --- a/tests/common/fixtures.rs +++ b/tests/common/fixtures.rs @@ -22,6 +22,12 @@ impl PeerBuilder { self } + #[allow(dead_code)] + pub fn with_peer_addr(mut self, peer_addr: &SocketAddr) -> Self { + self.peer.peer_addr = *peer_addr; + self + } + #[allow(dead_code)] pub fn with_bytes_pending_to_download(mut self, left: i64) -> Self { self.peer.left = NumberOfBytes(left); diff --git a/tests/http/asserts.rs b/tests/http/asserts.rs index e146f252d..a10edc9e6 100644 --- a/tests/http/asserts.rs +++ b/tests/http/asserts.rs @@ -30,17 +30,15 @@ pub async fn assert_empty_announce_response(response: Response) { pub async fn assert_announce_response(response: Response, expected_announce_response: &Announce) { assert_eq!(response.status(), 200); - let body = response.text().await.unwrap(); - let announce_response: Announce = serde_bencode::from_str(&body) - .unwrap_or_else(|_| panic!("response body should be a valid announce response, got \"{}\"", &body)); + + let body = response.bytes().await.unwrap(); + + let announce_response: Announce = serde_bencode::from_bytes(&body) + .unwrap_or_else(|_| panic!("response body should be a valid announce response, got \"{:#?}\"", &body)); + assert_eq!(announce_response, *expected_announce_response); } -/// Sample bencoded announce response as byte array: -/// -/// ```text -/// b"d8:intervali120e12:min intervali120e8:completei2e10:incompletei0e5:peers6:~\0\0\x01\x1f\x90e6:peers60:e" -/// ``` pub async fn assert_compact_announce_response(response: Response, expected_response: &Compact) { assert_eq!(response.status(), 200); @@ -138,6 +136,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/asserts_warp.rs b/tests/http/asserts_warp.rs new file mode 100644 index 000000000..6bda82f6c --- /dev/null +++ b/tests/http/asserts_warp.rs @@ -0,0 +1,15 @@ +/// todo: this mod should be removed when we remove the Warp implementation for the HTTP tracker. +use reqwest::Response; + +use super::responses::announce_warp::WarpAnnounce; + +pub async fn assert_warp_announce_response(response: Response, expected_announce_response: &WarpAnnounce) { + assert_eq!(response.status(), 200); + + let body = response.text().await.unwrap(); + + let announce_response: WarpAnnounce = serde_bencode::from_str(&body) + .unwrap_or_else(|_| panic!("response body should be a valid announce response, got \"{:#?}\"", &body)); + + assert_eq!(announce_response, *expected_announce_response); +} diff --git a/tests/http/mod.rs b/tests/http/mod.rs index 8c1e3c995..40616025b 100644 --- a/tests/http/mod.rs +++ b/tests/http/mod.rs @@ -1,4 +1,5 @@ pub mod asserts; +pub mod asserts_warp; pub mod client; pub mod connection_info; pub mod requests; diff --git a/tests/http/responses/announce.rs b/tests/http/responses/announce.rs index e976ba9ae..8a07ebd5e 100644 --- a/tests/http/responses/announce.rs +++ b/tests/http/responses/announce.rs @@ -10,20 +10,22 @@ pub struct Announce { pub interval: u32, #[serde(rename = "min interval")] pub min_interval: u32, - pub peers: Vec, // Peers with IPV4 + pub peers: Vec, // Peers using IPV4 and IPV6 } #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct DictionaryPeer { pub ip: String, - pub peer_id: String, + #[serde(rename = "peer id")] + #[serde(with = "serde_bytes")] + pub peer_id: Vec, pub port: u16, } impl From for DictionaryPeer { fn from(peer: Peer) -> Self { DictionaryPeer { - peer_id: peer.peer_id.to_string(), + peer_id: peer.peer_id.to_bytes().to_vec(), ip: peer.peer_addr.ip().to_string(), port: peer.peer_addr.port(), } diff --git a/tests/http/responses/announce_warp.rs b/tests/http/responses/announce_warp.rs new file mode 100644 index 000000000..0fcf05eb8 --- /dev/null +++ b/tests/http/responses/announce_warp.rs @@ -0,0 +1,30 @@ +/// todo: this mod should be removed when we remove the Warp implementation for the HTTP tracker. +use serde::{self, Deserialize, Serialize}; +use torrust_tracker::tracker::peer::Peer; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct WarpAnnounce { + pub complete: u32, + pub incomplete: u32, + pub interval: u32, + #[serde(rename = "min interval")] + pub min_interval: u32, + pub peers: Vec, // Peers using IPV4 +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct WarpDictionaryPeer { + pub ip: String, + pub peer_id: String, + pub port: u16, +} + +impl From for WarpDictionaryPeer { + fn from(peer: Peer) -> Self { + Self { + peer_id: peer.peer_id.to_string(), + ip: peer.peer_addr.ip().to_string(), + port: peer.peer_addr.port(), + } + } +} diff --git a/tests/http/responses/mod.rs b/tests/http/responses/mod.rs index bdc689056..aecb53fed 100644 --- a/tests/http/responses/mod.rs +++ b/tests/http/responses/mod.rs @@ -1,3 +1,4 @@ pub mod announce; +pub mod announce_warp; pub mod error; pub mod scrape; diff --git a/tests/http/server.rs b/tests/http/server.rs index e5266eee5..1c8d1cb77 100644 --- a/tests/http/server.rs +++ b/tests/http/server.rs @@ -131,7 +131,7 @@ impl Server { self.connection_info.clone() } - pub async fn add_torrent(&self, info_hash: &InfoHash, peer: &Peer) { + pub async fn add_torrent_peer(&self, info_hash: &InfoHash, peer: &Peer) { self.tracker.update_torrent_with_peer_and_get_stats(info_hash, peer).await; } } diff --git a/tests/http_tracker.rs b/tests/http_tracker.rs index ded30a0b4..a09802724 100644 --- a/tests/http_tracker.rs +++ b/tests/http_tracker.rs @@ -85,10 +85,12 @@ mod warp_http_tracker_server { assert_internal_server_error_response, assert_invalid_info_hash_error_response, assert_invalid_peer_id_error_response, assert_is_announce_response, }; + use crate::http::asserts_warp::assert_warp_announce_response; use crate::http::client::Client; use crate::http::requests::announce::{Compact, QueryBuilder}; use crate::http::responses; - use crate::http::responses::announce::{Announce, CompactPeer, CompactPeerList, DictionaryPeer}; + use crate::http::responses::announce::{Announce, CompactPeer, CompactPeerList}; + use crate::http::responses::announce_warp::{WarpAnnounce, WarpDictionaryPeer}; use crate::http::server::{ start_default_http_tracker, start_http_tracker_on_reverse_proxy, start_http_tracker_with_external_ip, start_ipv6_http_tracker, start_public_http_tracker, @@ -383,7 +385,9 @@ mod warp_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server.add_torrent(&info_hash, &previously_announced_peer).await; + http_tracker_server + .add_torrent_peer(&info_hash, &previously_announced_peer) + .await; // Announce the new Peer 2. This new peer is non included on the response peer list let response = Client::new(http_tracker_server.get_connection_info()) @@ -395,15 +399,15 @@ mod warp_http_tracker_server { ) .await; - // It should only contain teh previously announced peer - assert_announce_response( + // It should only contain the previously announced peer + assert_warp_announce_response( response, - &Announce { + &WarpAnnounce { complete: 2, incomplete: 0, interval: http_tracker_server.tracker.config.announce_interval, min_interval: http_tracker_server.tracker.config.min_announce_interval, - peers: vec![DictionaryPeer::from(previously_announced_peer)], + peers: vec![WarpDictionaryPeer::from(previously_announced_peer)], }, ) .await; @@ -417,7 +421,7 @@ mod warp_http_tracker_server { let peer = PeerBuilder::default().build(); // Add a peer - http_tracker_server.add_torrent(&info_hash, &peer).await; + http_tracker_server.add_torrent_peer(&info_hash, &peer).await; let announce_query = QueryBuilder::default() .with_info_hash(&info_hash) @@ -448,7 +452,9 @@ mod warp_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server.add_torrent(&info_hash, &previously_announced_peer).await; + http_tracker_server + .add_torrent_peer(&info_hash, &previously_announced_peer) + .await; // Announce the new Peer 2 accepting compact responses let response = Client::new(http_tracker_server.get_connection_info()) @@ -487,7 +493,9 @@ mod warp_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server.add_torrent(&info_hash, &previously_announced_peer).await; + http_tracker_server + .add_torrent_peer(&info_hash, &previously_announced_peer) + .await; // Announce the new Peer 2 without passing the "compact" param // By default it should respond with the compact peer list @@ -781,7 +789,7 @@ mod warp_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -819,7 +827,7 @@ mod warp_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -999,7 +1007,7 @@ mod warp_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -1028,7 +1036,7 @@ mod warp_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -1154,7 +1162,7 @@ mod warp_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -1183,7 +1191,7 @@ mod warp_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -1225,7 +1233,7 @@ mod warp_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -1405,19 +1413,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 +1431,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 +1444,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; } } @@ -1458,7 +1461,7 @@ mod axum_http_tracker_server { // Vuze (bittorrent client) docs: // https://wiki.vuze.com/w/Announce - use std::net::{IpAddr, Ipv6Addr}; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::str::FromStr; use local_ip_address::local_ip; @@ -1783,7 +1786,9 @@ mod axum_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server.add_torrent(&info_hash, &previously_announced_peer).await; + http_tracker_server + .add_torrent_peer(&info_hash, &previously_announced_peer) + .await; // Announce the new Peer 2. This new peer is non included on the response peer list let response = Client::new(http_tracker_server.get_connection_info()) @@ -1809,6 +1814,54 @@ mod axum_http_tracker_server { .await; } + #[tokio::test] + async fn should_return_the_list_of_previously_announced_peers_including_peers_using_ipv4_and_ipv6() { + let http_tracker_server = start_public_http_tracker(Version::Axum).await; + + let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); + + // Announce a peer using IPV4 + let peer_using_ipv4 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000001")) + .with_peer_addr(&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0x69, 0x69, 0x69, 0x69)), 8080)) + .build(); + http_tracker_server.add_torrent_peer(&info_hash, &peer_using_ipv4).await; + + // Announce a peer using IPV6 + let peer_using_ipv6 = PeerBuilder::default() + .with_peer_id(&peer::Id(*b"-qB00000000000000002")) + .with_peer_addr(&SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969, 0x6969)), + 8080, + )) + .build(); + http_tracker_server.add_torrent_peer(&info_hash, &peer_using_ipv6).await; + + // Announce the new Peer. + let response = Client::new(http_tracker_server.get_connection_info()) + .announce( + &QueryBuilder::default() + .with_info_hash(&info_hash) + .with_peer_id(&peer::Id(*b"-qB00000000000000003")) + .query(), + ) + .await; + + // The newly announced peer is not included on the response peer list, + // but all the previously announced peers should be included regardless the IP version they are using. + assert_announce_response( + response, + &Announce { + complete: 3, + incomplete: 0, + interval: http_tracker_server.tracker.config.announce_interval, + min_interval: http_tracker_server.tracker.config.min_announce_interval, + peers: vec![DictionaryPeer::from(peer_using_ipv4), DictionaryPeer::from(peer_using_ipv6)], + }, + ) + .await; + } + #[tokio::test] async fn should_consider_two_peers_to_be_the_same_when_they_have_the_same_peer_id_even_if_the_ip_is_different() { let http_tracker_server = start_public_http_tracker(Version::Axum).await; @@ -1817,7 +1870,7 @@ mod axum_http_tracker_server { let peer = PeerBuilder::default().build(); // Add a peer - http_tracker_server.add_torrent(&info_hash, &peer).await; + http_tracker_server.add_torrent_peer(&info_hash, &peer).await; let announce_query = QueryBuilder::default() .with_info_hash(&info_hash) @@ -1833,8 +1886,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 @@ -1849,7 +1901,9 @@ mod axum_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server.add_torrent(&info_hash, &previously_announced_peer).await; + http_tracker_server + .add_torrent_peer(&info_hash, &previously_announced_peer) + .await; // Announce the new Peer 2 accepting compact responses let response = Client::new(http_tracker_server.get_connection_info()) @@ -1888,7 +1942,9 @@ mod axum_http_tracker_server { .build(); // Add the Peer 1 - http_tracker_server.add_torrent(&info_hash, &previously_announced_peer).await; + http_tracker_server + .add_torrent_peer(&info_hash, &previously_announced_peer) + .await; // Announce the new Peer 2 without passing the "compact" param // By default it should respond with the compact peer list @@ -2185,7 +2241,7 @@ mod axum_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -2224,7 +2280,7 @@ mod axum_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -2410,7 +2466,7 @@ mod axum_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -2440,7 +2496,7 @@ mod axum_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -2570,7 +2626,7 @@ mod axum_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -2600,7 +2656,7 @@ mod axum_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) @@ -2643,7 +2699,7 @@ mod axum_http_tracker_server { let info_hash = InfoHash::from_str("9c38422213e30bff212b30c360d26f9a02136422").unwrap(); http_tracker - .add_torrent( + .add_torrent_peer( &info_hash, &PeerBuilder::default() .with_peer_id(&peer::Id(*b"-qB00000000000000001")) diff --git a/tests/tracker_api.rs b/tests/tracker_api.rs index b79e8a8af..193c6487c 100644 --- a/tests/tracker_api.rs +++ b/tests/tracker_api.rs @@ -115,7 +115,7 @@ mod tracker_apis { let api_server = start_default_api().await; api_server - .add_torrent( + .add_torrent_peer( &InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(), &PeerBuilder::default().into(), ) @@ -189,7 +189,7 @@ mod tracker_apis { let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - api_server.add_torrent(&info_hash, &PeerBuilder::default().into()).await; + api_server.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await; let response = Client::new(api_server.get_connection_info()) .get_torrents(Query::empty()) @@ -216,8 +216,12 @@ mod tracker_apis { let info_hash_1 = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); let info_hash_2 = InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap(); - api_server.add_torrent(&info_hash_1, &PeerBuilder::default().into()).await; - api_server.add_torrent(&info_hash_2, &PeerBuilder::default().into()).await; + api_server + .add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()) + .await; + api_server + .add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()) + .await; let response = Client::new(api_server.get_connection_info()) .get_torrents(Query::params([QueryParam::new("limit", "1")].to_vec())) @@ -244,8 +248,12 @@ mod tracker_apis { let info_hash_1 = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); let info_hash_2 = InfoHash::from_str("0b3aea4adc213ce32295be85d3883a63bca25446").unwrap(); - api_server.add_torrent(&info_hash_1, &PeerBuilder::default().into()).await; - api_server.add_torrent(&info_hash_2, &PeerBuilder::default().into()).await; + api_server + .add_torrent_peer(&info_hash_1, &PeerBuilder::default().into()) + .await; + api_server + .add_torrent_peer(&info_hash_2, &PeerBuilder::default().into()) + .await; let response = Client::new(api_server.get_connection_info()) .get_torrents(Query::params([QueryParam::new("offset", "1")].to_vec())) @@ -319,7 +327,7 @@ mod tracker_apis { let peer = PeerBuilder::default().into(); - api_server.add_torrent(&info_hash, &peer).await; + api_server.add_torrent_peer(&info_hash, &peer).await; let response = Client::new(api_server.get_connection_info()) .get_torrent(&info_hash.to_string()) @@ -378,7 +386,7 @@ mod tracker_apis { let info_hash = InfoHash::from_str("9e0217d0fa71c87332cd8bf9dbeabcb2c2cf3c4d").unwrap(); - api_server.add_torrent(&info_hash, &PeerBuilder::default().into()).await; + api_server.add_torrent_peer(&info_hash, &PeerBuilder::default().into()).await; let response = Client::new(connection_with_invalid_token(&api_server.get_bind_address())) .get_torrent(&info_hash.to_string())