diff --git a/src/server.rs b/src/server.rs index 7648ea6..f93bd52 100644 --- a/src/server.rs +++ b/src/server.rs @@ -210,11 +210,13 @@ impl UDPTracker { let client_addr = SocketAddr::new(remote_addr.ip(), packet.port); let info_hash = packet.info_hash.into(); + let peer_id: &tracker::PeerId = tracker::PeerId::from_array(&packet.peer_id); + match self .tracker .update_torrent_and_get_stats( &info_hash, - &packet.peer_id, + peer_id, &client_addr, packet.uploaded, packet.downloaded, diff --git a/src/tracker.rs b/src/tracker.rs index 20ac9f2..896b23a 100644 --- a/src/tracker.rs +++ b/src/tracker.rs @@ -25,16 +25,21 @@ pub enum TrackerMode { PrivateMode, } -#[derive(Clone)] -struct TorrentPeer { +#[derive(Clone, Serialize)] +pub struct TorrentPeer { ip: std::net::SocketAddr, uploaded: u64, downloaded: u64, left: u64, event: Events, + #[serde(serialize_with = "ser_instant")] updated: std::time::Instant, } +fn ser_instant(inst: &std::time::Instant, ser: S) -> Result { + ser.serialize_u64(inst.elapsed().as_millis() as u64) +} + #[derive(Ord, PartialEq, Eq, Clone)] pub struct InfoHash { info_hash: [u8; 20], @@ -128,7 +133,112 @@ impl<'de> serde::de::Deserialize<'de> for InfoHash { } } -pub type PeerId = [u8; 20]; +#[repr(transparent)] +#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq)] +pub struct PeerId([u8; 20]); +impl PeerId { + pub fn from_array(v: &[u8; 20]) -> &PeerId { + unsafe { + // This is safe since PeerId's repr is transparent and content's are identical. PeerId == [0u8; 20] + core::mem::transmute(v) + } + } + + pub fn get_client_name(&self) -> Option<&'static str> { + if self.0[0] == b'M' { + return Some("BitTorrent"); + } + if self.0[0] == b'-' { + let name = match &self.0[1..3] { + b"AG" => "Ares", + b"A~" => "Ares", + b"AR" => "Arctic", + b"AV" => "Avicora", + b"AX" => "BitPump", + b"AZ" => "Azureus", + b"BB" => "BitBuddy", + b"BC" => "BitComet", + b"BF" => "Bitflu", + b"BG" => "BTG (uses Rasterbar libtorrent)", + b"BR" => "BitRocket", + b"BS" => "BTSlave", + b"BX" => "~Bittorrent X", + b"CD" => "Enhanced CTorrent", + b"CT" => "CTorrent", + b"DE" => "DelugeTorrent", + b"DP" => "Propagate Data Client", + b"EB" => "EBit", + b"ES" => "electric sheep", + b"FT" => "FoxTorrent", + b"FW" => "FrostWire", + b"FX" => "Freebox BitTorrent", + b"GS" => "GSTorrent", + b"HL" => "Halite", + b"HN" => "Hydranode", + b"KG" => "KGet", + b"KT" => "KTorrent", + b"LH" => "LH-ABC", + b"LP" => "Lphant", + b"LT" => "libtorrent", + b"lt" => "libTorrent", + b"LW" => "LimeWire", + b"MO" => "MonoTorrent", + b"MP" => "MooPolice", + b"MR" => "Miro", + b"MT" => "MoonlightTorrent", + b"NX" => "Net Transport", + b"PD" => "Pando", + b"qB" => "qBittorrent", + b"QD" => "QQDownload", + b"QT" => "Qt 4 Torrent example", + b"RT" => "Retriever", + b"S~" => "Shareaza alpha/beta", + b"SB" => "~Swiftbit", + b"SS" => "SwarmScope", + b"ST" => "SymTorrent", + b"st" => "sharktorrent", + b"SZ" => "Shareaza", + b"TN" => "TorrentDotNET", + b"TR" => "Transmission", + b"TS" => "Torrentstorm", + b"TT" => "TuoTu", + b"UL" => "uLeecher!", + b"UT" => "µTorrent", + b"UW" => "µTorrent Web", + b"VG" => "Vagaa", + b"WD" => "WebTorrent Desktop", + b"WT" => "BitLet", + b"WW" => "WebTorrent", + b"WY" => "FireTorrent", + b"XL" => "Xunlei", + b"XT" => "XanTorrent", + b"XX" => "Xtorrent", + b"ZT" => "ZipTorrent", + _ => return None, + }; + Some(name) + } else { + None + } + } +} +impl Serialize for PeerId { + fn serialize(&self, serializer: S) -> Result + where S: serde::Serializer { + let mut tmp = [0u8; 40]; + binascii::bin2hex(&self.0, &mut tmp).unwrap(); + let id = std::str::from_utf8(&tmp).ok(); + + #[derive(Serialize)] + struct PeerIdInfo<'a> { + id: Option<&'a str>, + client: Option<&'a str>, + } + + let obj = PeerIdInfo { id, client: self.get_client_name() }; + obj.serialize(serializer) + } +} #[derive(Serialize, Deserialize, Clone)] pub struct TorrentEntry { @@ -208,6 +318,10 @@ impl TorrentEntry { list } + pub fn get_peers_iter(&self) -> impl Iterator { + self.peers.iter() + } + pub fn get_stats(&self) -> (u32, u32, u32) { let leechers = (self.peers.len() as u32) - self.seeders; (self.seeders, self.completed, leechers) diff --git a/src/webserver.rs b/src/webserver.rs index 187782b..208eea0 100644 --- a/src/webserver.rs +++ b/src/webserver.rs @@ -31,6 +31,11 @@ struct TorrentEntry<'a> { info_hash: &'a InfoHash, #[serde(flatten)] data: &'a crate::tracker::TorrentEntry, + seeders: u32, + leechers: u32, + + #[serde(skip_serializing_if="Option::is_none")] + peers: Option>, } #[derive(Serialize, Deserialize)] @@ -101,7 +106,10 @@ pub fn build_server( let db = tracker.get_database().await; let results: Vec<_> = db .iter() - .map(|(k, v)| TorrentEntry { info_hash: k, data: v }) + .map(|(k, v)| { + let (seeders, _, leechers) = v.get_stats(); + TorrentEntry { info_hash: k, data: v, seeders, leechers, peers: None } + }) .skip(offset as usize) .take(limit as usize) .collect(); @@ -113,7 +121,7 @@ pub fn build_server( let t2 = tracker.clone(); // view_torrent_info -> GET /t/:infohash HTTP/* let view_torrent_info = filters::method::get() - .and(filters::path::param()) + .and(filters::path::param()).and(filters::path::end()) .map(move |info_hash: InfoHash| { let tracker = t2.clone(); (info_hash, tracker) @@ -125,10 +133,22 @@ pub fn build_server( Some(v) => v, None => return Err(warp::reject::reject()), }; + let (seeders, _, leechers) = info.get_stats(); + + let peers: Vec<_> = info + .get_peers_iter() + .take(1000) + .map(|(peer_id, peer_info)| { + (peer_id.clone(), peer_info.clone()) + }) + .collect(); Ok(reply::json(&TorrentEntry { info_hash: &info_hash, data: info, + seeders, + leechers, + peers: Some(peers), })) } }); @@ -136,7 +156,7 @@ pub fn build_server( // DELETE /t/:info_hash let t3 = tracker.clone(); let delete_torrent = filters::method::delete() - .and(filters::path::param()) + .and(filters::path::param()).and(filters::path::end()) .map(move |info_hash: InfoHash| { let tracker = t3.clone(); (info_hash, tracker) @@ -160,7 +180,7 @@ pub fn build_server( // add_torrent/alter: POST /t/:info_hash // (optional) BODY: json: {"is_flagged": boolean} let change_torrent = filters::method::post() - .and(filters::path::param()) + .and(filters::path::param()).and(filters::path::end()) .and(filters::body::content_length_limit(4096)) .and(filters::body::json()) .map(move |info_hash: InfoHash, body: Option| {