304 lines
10 KiB
Rust
304 lines
10 KiB
Rust
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<String>,
|
|
/// Whether the tracker should prefer the compact peer format.
|
|
pub compact: bool,
|
|
/// Optional requested peer count.
|
|
pub numwant: Option<u32>,
|
|
}
|
|
|
|
/// 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<TorrentPeerModel>,
|
|
/// Optional minimum announce interval in seconds.
|
|
pub min_interval_sec: Option<u32>,
|
|
/// Optional tracker session identifier.
|
|
pub tracker_id: Option<String>,
|
|
}
|
|
|
|
/// 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<Self, TrackerParseError> {
|
|
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<u32>,
|
|
/// Number of times the torrent was downloaded.
|
|
pub downloaded: Option<u32>,
|
|
/// Number of incomplete peers.
|
|
pub incomplete: Option<u32>,
|
|
}
|
|
|
|
/// Scrape summary returned by a tracker.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct TrackerScrapeModel {
|
|
/// Aggregate completed download count when present.
|
|
pub complete: Option<u32>,
|
|
/// Aggregate download count when present.
|
|
pub downloaded: Option<u32>,
|
|
/// Aggregate incomplete peer count when present.
|
|
pub incomplete: Option<u32>,
|
|
/// Per-info-hash scrape entries.
|
|
pub files: Vec<TrackerScrapeFileModel>,
|
|
}
|
|
|
|
/// 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<TrackerScrapeModel>,
|
|
}
|
|
|
|
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<String>,
|
|
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<String, TrackerParseError> {
|
|
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<String, TrackerParseError> {
|
|
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<UdpTrackerAnnounceRequest, TrackerParseError> {
|
|
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<Self, TrackerParseError> {
|
|
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<TrackerScrapeModel, TrackerParseError> {
|
|
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<TrackerResponseModel, TransportError>;
|
|
/// Executes a tracker scrape request for the given announce URL.
|
|
fn scrape(&self, announce_url: &str) -> Result<TrackerScrapeModel, TransportError>;
|
|
}
|
|
|
|
/// 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<DhtMessageModel, TransportError>;
|
|
}
|