chore: initial sanitized public snapshot
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
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>;
|
||||
}
|
||||
Reference in New Issue
Block a user