use super::{DhtMessageModel, TorrentPeerModel, TransportError}; use crate::tracker::{ TrackerParseError, UdpTrackerAnnounceEvent, UdpTrackerAnnounceRequest, UdpTrackerTransactionId, }; /// Announce request fields sent to an HTTP or UDP tracker. #[derive(Clone, Debug, Eq, PartialEq)] pub struct TrackerRequestModel { /// Base announce URL. pub announce_url: String, /// Hex-encoded torrent info hash. pub info_hash: String, /// Hex-encoded local peer identifier. pub peer_id: String, /// Listening port exposed to peers. pub port: u16, /// Uploaded byte counter sent to the tracker. pub uploaded: u64, /// Downloaded byte counter sent to the tracker. pub downloaded: u64, /// Remaining byte counter sent to the tracker. pub left: u64, /// Optional tracker lifecycle event. pub event: Option, /// Whether the tracker should prefer the compact peer format. pub compact: bool, /// Optional requested peer count. pub numwant: Option, } /// Peer list returned by a tracker announce response. #[derive(Clone, Debug, Eq, PartialEq)] pub struct TrackerPeerListModel { /// Recommended announce interval in seconds. pub interval_sec: u32, /// Parsed peer entries. pub peers: Vec, /// Optional minimum announce interval in seconds. pub min_interval_sec: Option, /// Optional tracker session identifier. pub tracker_id: Option, } /// DHT node coordinate returned by tracker or DHT metadata. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DhtNodeModel { /// Hex-encoded node identifier when available. pub node_id: String, /// Node IP address or hostname. pub address: String, /// Node UDP port. pub port: u16, } impl DhtNodeModel { /// Parses a DHT bootstrap node from a `host:port` or `[ipv6]:port` spec. /// /// # Errors /// /// Returns an error when the node spec is malformed or the port is invalid. pub fn from_spec(raw: &str) -> Result { let (address, port) = super::parsing::parse_endpoint_spec(raw, "dht node")?; Ok(Self { node_id: String::new(), address, port, }) } /// Formats the node as a stable `host:port` or `[ipv6]:port` spec. #[must_use] pub fn to_spec(&self) -> String { super::parsing::format_endpoint_spec(&self.address, self.port) } } /// Scrape statistics for a single info hash. #[derive(Clone, Debug, Eq, PartialEq)] pub struct TrackerScrapeFileModel { /// Hex-encoded info hash the entry describes. pub info_hash: String, /// Number of completed downloads. pub complete: Option, /// Number of times the torrent was downloaded. pub downloaded: Option, /// Number of incomplete peers. pub incomplete: Option, } /// Scrape summary returned by a tracker. #[derive(Clone, Debug, Eq, PartialEq)] pub struct TrackerScrapeModel { /// Aggregate completed download count when present. pub complete: Option, /// Aggregate download count when present. pub downloaded: Option, /// Aggregate incomplete peer count when present. pub incomplete: Option, /// Per-info-hash scrape entries. pub files: Vec, } /// Parsed tracker response containing announce peers and optional scrape data. #[derive(Clone, Debug, Eq, PartialEq)] pub struct TrackerResponseModel { /// Peer-list payload from the announce response. pub peers: TrackerPeerListModel, /// Optional scrape metadata synthesized from the response. pub scrape: Option, } impl TrackerRequestModel { /// Builds a tracker request from raw `BitTorrent` info-hash and peer-id bytes. #[must_use] pub fn from_bt_bytes( announce_url: impl Into, info_hash: [u8; 20], peer_id: [u8; 20], port: u16, uploaded: u64, downloaded: u64, left: u64, ) -> Self { Self { announce_url: announce_url.into(), info_hash: super::parsing::hex_encode(&info_hash), peer_id: super::parsing::hex_encode(&peer_id), port, uploaded, downloaded, left, event: None, compact: true, numwant: Some(50), } } /// Returns sorted query pairs suitable for announce and scrape URLs. #[must_use] pub fn query_pairs(&self) -> Vec<(String, String)> { let mut pairs = vec![ ("info_hash".to_owned(), self.info_hash.clone()), ("peer_id".to_owned(), self.peer_id.clone()), ("port".to_owned(), self.port.to_string()), ("uploaded".to_owned(), self.uploaded.to_string()), ("downloaded".to_owned(), self.downloaded.to_string()), ("left".to_owned(), self.left.to_string()), ("compact".to_owned(), u8::from(self.compact).to_string()), ]; if let Some(event) = &self.event { pairs.push(("event".to_owned(), event.clone())); } if let Some(numwant) = self.numwant { pairs.push(("numwant".to_owned(), numwant.to_string())); } pairs } /// Builds the full announce URL with query parameters. /// /// # Errors /// /// Returns an error when the announce URL is invalid. pub fn announce_url(&self) -> Result { super::parsing::build_url(&self.announce_url, &self.query_pairs()) } /// Builds the matching scrape URL with query parameters. /// /// # Errors /// /// Returns an error when the derived scrape URL is invalid. pub fn scrape_url(&self) -> Result { let base = super::parsing::tracker_scrape_url(&self.announce_url); super::parsing::build_url(&base, &self.query_pairs()) } /// Decodes the hex-encoded info hash into its raw 20-byte form. /// /// # Errors /// /// Returns an error when the info hash is not a valid 20-byte hex string. pub fn info_hash_bytes(&self) -> Result<[u8; 20], TrackerParseError> { super::parsing::decode_hex_20(&self.info_hash) } /// Decodes the hex-encoded peer id into its raw 20-byte form. /// /// # Errors /// /// Returns an error when the peer id is not a valid 20-byte hex string. pub fn peer_id_bytes(&self) -> Result<[u8; 20], TrackerParseError> { super::parsing::decode_hex_20(&self.peer_id) } /// Converts the higher-level request into a UDP tracker announce request. /// /// # Errors /// /// Returns an error when the info hash, peer id, event, or numwant cannot be represented /// in a UDP announce packet. pub fn to_udp_announce_request( &self, connection_id: u64, transaction_id: UdpTrackerTransactionId, ) -> Result { let event = match self.event.as_deref() { None | Some("") => UdpTrackerAnnounceEvent::None, Some("completed") => UdpTrackerAnnounceEvent::Completed, Some("started") => UdpTrackerAnnounceEvent::Started, Some("stopped") => UdpTrackerAnnounceEvent::Stopped, Some(other) => { return Err(TrackerParseError::InvalidUdpPacket(format!( "unsupported udp tracker event: {other}" ))); } }; let numwant = match self.numwant { Some(numwant) => i32::try_from(numwant).map_err(|_| { TrackerParseError::InvalidUdpPacket(format!( "udp tracker numwant exceeds i32 range: {numwant}" )) })?, None => -1, }; Ok(UdpTrackerAnnounceRequest { connection_id, transaction_id, info_hash: self.info_hash_bytes()?, peer_id: self.peer_id_bytes()?, downloaded: self.downloaded, left: self.left, uploaded: self.uploaded, event, ip_address: 0, key: 0, numwant, port: self.port, }) } } impl TrackerResponseModel { /// Parses an HTTP tracker announce payload. /// /// # Errors /// /// Returns an error when the payload is malformed bencode or lacks required fields. pub fn from_announce_bytes(input: &[u8]) -> Result { let root = super::parsing::parse_bencode(input)?; let interval_sec = super::parsing::i64_to_u32( super::parsing::dict_get_int(&root, "interval").unwrap_or(1800), ); let min_interval_sec = super::parsing::dict_get_int(&root, "min interval").map(super::parsing::i64_to_u32); let tracker_id = super::parsing::dict_get_bytes(&root, "tracker id") .map(super::parsing::bytes_to_string); let peers = super::parsing::parse_peer_list(&root)?; let scrape = super::parsing::parse_scrape_section(&root); Ok(Self { peers: TrackerPeerListModel { interval_sec, peers, min_interval_sec, tracker_id, }, scrape, }) } /// Parses an HTTP tracker scrape payload. /// /// # Errors /// /// Returns an error when the payload is malformed or lacks scrape metadata. pub fn from_scrape_bytes(input: &[u8]) -> Result { let root = super::parsing::parse_bencode(input)?; super::parsing::parse_scrape_section_from_root(&root) .ok_or(TrackerParseError::MissingField("files")) } } /// Tracker transport contract for announce and scrape requests. pub trait TrackerTransport { /// Executes a tracker announce request. fn announce( &self, request: &TrackerRequestModel, ) -> Result; /// Executes a tracker scrape request for the given announce URL. fn scrape(&self, announce_url: &str) -> Result; } /// DHT transport contract for request/response messaging. pub trait DhtTransport { /// Sends a DHT message to the target node and returns the response. fn send_message( &self, node: &DhtNodeModel, message: &DhtMessageModel, ) -> Result; }