chore: initial sanitized public snapshot
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
use super::{AtomicU32, Ordering, TorrentPeerModel};
|
||||
use crate::tracker::{
|
||||
TrackerParseError, TrackerPeerListModel, TrackerResponseModel, TrackerScrapeFileModel,
|
||||
TrackerScrapeModel,
|
||||
};
|
||||
|
||||
/// UDP tracker protocol identifier from BEP 15.
|
||||
pub const UDP_TRACKER_PROTOCOL_ID: u64 = 0x0417_2710_1980;
|
||||
|
||||
/// Process-local counter used to allocate monotonic UDP tracker transaction ids.
|
||||
static UDP_TRACKER_TRANSACTION_COUNTER: AtomicU32 = AtomicU32::new(0x6d69_0000);
|
||||
|
||||
/// Monotonic transaction identifier used for UDP tracker requests.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct UdpTrackerTransactionId(u32);
|
||||
|
||||
impl UdpTrackerTransactionId {
|
||||
/// Creates a transaction identifier from a raw integer value.
|
||||
#[must_use]
|
||||
pub const fn new(value: u32) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Allocates the next process-local UDP tracker transaction identifier.
|
||||
#[must_use]
|
||||
pub fn next() -> Self {
|
||||
Self(UDP_TRACKER_TRANSACTION_COUNTER.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// Returns the raw integer value sent on the wire.
|
||||
#[must_use]
|
||||
pub const fn get(self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP tracker actions defined by BEP 15.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum UdpTrackerAction {
|
||||
/// Connection-id bootstrap request/response.
|
||||
Connect,
|
||||
/// Announce request/response.
|
||||
Announce,
|
||||
/// Scrape request/response.
|
||||
Scrape,
|
||||
/// Error response.
|
||||
Error,
|
||||
}
|
||||
|
||||
impl UdpTrackerAction {
|
||||
/// Returns the wire value associated with the action.
|
||||
#[must_use]
|
||||
pub const fn wire_value(self) -> u32 {
|
||||
match self {
|
||||
Self::Connect => 0,
|
||||
Self::Announce => 1,
|
||||
Self::Scrape => 2,
|
||||
Self::Error => 3,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes a raw BEP 15 action code into the typed tracker action.
|
||||
fn decode(value: u32) -> Result<Self, TrackerParseError> {
|
||||
match value {
|
||||
0 => Ok(Self::Connect),
|
||||
1 => Ok(Self::Announce),
|
||||
2 => Ok(Self::Scrape),
|
||||
3 => Ok(Self::Error),
|
||||
_ => Err(TrackerParseError::InvalidUdpAction(value)),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns a short diagnostic label for the tracker action.
|
||||
const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Connect => "connect",
|
||||
Self::Announce => "announce",
|
||||
Self::Scrape => "scrape",
|
||||
Self::Error => "error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Announce lifecycle values defined by the UDP tracker protocol.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum UdpTrackerAnnounceEvent {
|
||||
/// No explicit lifecycle event.
|
||||
None,
|
||||
/// Download completed.
|
||||
Completed,
|
||||
/// Download started.
|
||||
Started,
|
||||
/// Download stopped.
|
||||
Stopped,
|
||||
}
|
||||
|
||||
impl UdpTrackerAnnounceEvent {
|
||||
/// Returns the wire value associated with the announce event.
|
||||
#[must_use]
|
||||
pub const fn wire_value(self) -> u32 {
|
||||
match self {
|
||||
Self::None => 0,
|
||||
Self::Completed => 1,
|
||||
Self::Started => 2,
|
||||
Self::Stopped => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Header shared by all UDP tracker responses.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct UdpTrackerResponseHeader {
|
||||
/// Action encoded by the response.
|
||||
pub action: UdpTrackerAction,
|
||||
/// Transaction identifier associated with the request/response pair.
|
||||
pub transaction_id: UdpTrackerTransactionId,
|
||||
}
|
||||
|
||||
impl UdpTrackerResponseHeader {
|
||||
/// Decodes a UDP tracker response header from raw bytes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the payload is truncated or the action code is invalid.
|
||||
pub fn decode(input: &[u8]) -> Result<Self, TrackerParseError> {
|
||||
ensure_udp_payload_len(input, 8, "tracker response header")?;
|
||||
Ok(Self {
|
||||
action: UdpTrackerAction::decode(read_u32_be(input, 0, "tracker action id")?)?,
|
||||
transaction_id: UdpTrackerTransactionId::new(read_u32_be(
|
||||
input,
|
||||
4,
|
||||
"tracker transaction id",
|
||||
)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Verifies that the header action matches the expected value.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the action differs from `expected`.
|
||||
pub fn expect_action(self, expected: UdpTrackerAction) -> Result<Self, TrackerParseError> {
|
||||
if self.action == expected {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(TrackerParseError::InvalidUdpPacket(format!(
|
||||
"expected tracker action {} but got {}",
|
||||
expected.label(),
|
||||
self.action.label()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies that the header transaction identifier matches the expected value.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the transaction identifier differs from `expected`.
|
||||
pub fn expect_transaction_id(
|
||||
self,
|
||||
expected: UdpTrackerTransactionId,
|
||||
) -> Result<Self, TrackerParseError> {
|
||||
if self.transaction_id == expected {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(TrackerParseError::TransactionIdMismatch {
|
||||
expected: expected.get(),
|
||||
actual: self.transaction_id.get(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP tracker connect request payload.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct UdpTrackerConnectRequest {
|
||||
/// Transaction identifier to match in the response.
|
||||
pub transaction_id: UdpTrackerTransactionId,
|
||||
}
|
||||
|
||||
impl UdpTrackerConnectRequest {
|
||||
/// Encodes the request into BEP 15 wire bytes.
|
||||
#[must_use]
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(16);
|
||||
out.extend_from_slice(&UDP_TRACKER_PROTOCOL_ID.to_be_bytes());
|
||||
out.extend_from_slice(&UdpTrackerAction::Connect.wire_value().to_be_bytes());
|
||||
out.extend_from_slice(&self.transaction_id.get().to_be_bytes());
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP tracker connect response payload.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct UdpTrackerConnectResponse {
|
||||
/// Transaction identifier echoed by the tracker.
|
||||
pub transaction_id: UdpTrackerTransactionId,
|
||||
/// Connection identifier used by later requests.
|
||||
pub connection_id: u64,
|
||||
}
|
||||
|
||||
impl UdpTrackerConnectResponse {
|
||||
/// Decodes a connect response from BEP 15 wire bytes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the payload is truncated or malformed.
|
||||
pub fn decode(input: &[u8]) -> Result<Self, TrackerParseError> {
|
||||
ensure_udp_payload_len(input, 16, "connect response")?;
|
||||
let header =
|
||||
UdpTrackerResponseHeader::decode(input)?.expect_action(UdpTrackerAction::Connect)?;
|
||||
Ok(Self {
|
||||
transaction_id: header.transaction_id,
|
||||
connection_id: read_u64_be(input, 8, "tracker connection id")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP tracker announce request payload.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct UdpTrackerAnnounceRequest {
|
||||
/// Connection identifier previously returned by the tracker.
|
||||
pub connection_id: u64,
|
||||
/// Transaction identifier to match in the response.
|
||||
pub transaction_id: UdpTrackerTransactionId,
|
||||
/// Raw 20-byte torrent info hash.
|
||||
pub info_hash: [u8; 20],
|
||||
/// Raw 20-byte local peer identifier.
|
||||
pub peer_id: [u8; 20],
|
||||
/// Uploaded byte count.
|
||||
pub downloaded: u64,
|
||||
/// Remaining byte count.
|
||||
pub left: u64,
|
||||
/// Uploaded byte count.
|
||||
pub uploaded: u64,
|
||||
/// Announce lifecycle event.
|
||||
pub event: UdpTrackerAnnounceEvent,
|
||||
/// Optional explicit IPv4 address encoded as a `u32`.
|
||||
pub ip_address: u32,
|
||||
/// Opaque tracker key.
|
||||
pub key: u32,
|
||||
/// Desired peer count or `-1` for tracker default.
|
||||
pub numwant: i32,
|
||||
/// Listening port exposed to peers.
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl UdpTrackerAnnounceRequest {
|
||||
/// Encodes the announce request into BEP 15 wire bytes.
|
||||
#[must_use]
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(98);
|
||||
out.extend_from_slice(&self.connection_id.to_be_bytes());
|
||||
out.extend_from_slice(&UdpTrackerAction::Announce.wire_value().to_be_bytes());
|
||||
out.extend_from_slice(&self.transaction_id.get().to_be_bytes());
|
||||
out.extend_from_slice(&self.info_hash);
|
||||
out.extend_from_slice(&self.peer_id);
|
||||
out.extend_from_slice(&self.downloaded.to_be_bytes());
|
||||
out.extend_from_slice(&self.left.to_be_bytes());
|
||||
out.extend_from_slice(&self.uploaded.to_be_bytes());
|
||||
out.extend_from_slice(&self.event.wire_value().to_be_bytes());
|
||||
out.extend_from_slice(&self.ip_address.to_be_bytes());
|
||||
out.extend_from_slice(&self.key.to_be_bytes());
|
||||
out.extend_from_slice(&self.numwant.to_be_bytes());
|
||||
out.extend_from_slice(&self.port.to_be_bytes());
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP tracker announce response payload.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct UdpTrackerAnnounceResponse {
|
||||
/// Transaction identifier echoed by the tracker.
|
||||
pub transaction_id: UdpTrackerTransactionId,
|
||||
/// Recommended announce interval in seconds.
|
||||
pub interval_sec: u32,
|
||||
/// Number of incomplete peers.
|
||||
pub leechers: u32,
|
||||
/// Number of complete peers.
|
||||
pub seeders: u32,
|
||||
/// Parsed compact IPv4 peer list.
|
||||
pub peers: Vec<TorrentPeerModel>,
|
||||
}
|
||||
|
||||
impl UdpTrackerAnnounceResponse {
|
||||
/// Decodes an announce response from BEP 15 wire bytes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the payload is truncated or malformed.
|
||||
pub fn decode(input: &[u8]) -> Result<Self, TrackerParseError> {
|
||||
ensure_udp_payload_len(input, 20, "announce response")?;
|
||||
let header =
|
||||
UdpTrackerResponseHeader::decode(input)?.expect_action(UdpTrackerAction::Announce)?;
|
||||
Ok(Self {
|
||||
transaction_id: header.transaction_id,
|
||||
interval_sec: read_u32_be(input, 8, "announce interval")?,
|
||||
leechers: read_u32_be(input, 12, "announce leechers")?,
|
||||
seeders: read_u32_be(input, 16, "announce seeders")?,
|
||||
peers: super::parsing::parse_compact_peers_ipv4(&input[20..])?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts the announce response into the higher-level tracker response model.
|
||||
#[must_use]
|
||||
pub fn into_tracker_response(self) -> TrackerResponseModel {
|
||||
TrackerResponseModel {
|
||||
peers: TrackerPeerListModel {
|
||||
interval_sec: self.interval_sec,
|
||||
peers: self.peers,
|
||||
min_interval_sec: None,
|
||||
tracker_id: None,
|
||||
},
|
||||
scrape: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP tracker scrape request payload.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct UdpTrackerScrapeRequest {
|
||||
/// Connection identifier previously returned by the tracker.
|
||||
pub connection_id: u64,
|
||||
/// Transaction identifier to match in the response.
|
||||
pub transaction_id: UdpTrackerTransactionId,
|
||||
/// Raw 20-byte info hashes to scrape.
|
||||
pub info_hashes: Vec<[u8; 20]>,
|
||||
}
|
||||
|
||||
impl UdpTrackerScrapeRequest {
|
||||
/// Encodes the scrape request into BEP 15 wire bytes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when no info hashes were supplied.
|
||||
pub fn encode(&self) -> Result<Vec<u8>, TrackerParseError> {
|
||||
if self.info_hashes.is_empty() {
|
||||
return Err(TrackerParseError::InvalidUdpPacket(
|
||||
"udp tracker scrape request requires at least one info hash".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(16 + self.info_hashes.len() * 20);
|
||||
out.extend_from_slice(&self.connection_id.to_be_bytes());
|
||||
out.extend_from_slice(&UdpTrackerAction::Scrape.wire_value().to_be_bytes());
|
||||
out.extend_from_slice(&self.transaction_id.get().to_be_bytes());
|
||||
for info_hash in &self.info_hashes {
|
||||
out.extend_from_slice(info_hash);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-torrent counters returned by a UDP tracker scrape response.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct UdpTrackerScrapeStats {
|
||||
/// Number of completed downloads.
|
||||
pub complete: u32,
|
||||
/// Number of completed client downloads.
|
||||
pub downloaded: u32,
|
||||
/// Number of incomplete peers.
|
||||
pub incomplete: u32,
|
||||
}
|
||||
|
||||
/// UDP tracker scrape response payload.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct UdpTrackerScrapeResponse {
|
||||
/// Transaction identifier echoed by the tracker.
|
||||
pub transaction_id: UdpTrackerTransactionId,
|
||||
/// Per-info-hash scrape statistics.
|
||||
pub files: Vec<UdpTrackerScrapeStats>,
|
||||
}
|
||||
|
||||
impl UdpTrackerScrapeResponse {
|
||||
/// Decodes a scrape response from BEP 15 wire bytes.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the payload is truncated or malformed.
|
||||
pub fn decode(input: &[u8]) -> Result<Self, TrackerParseError> {
|
||||
ensure_udp_payload_len(input, 8, "scrape response header")?;
|
||||
let header =
|
||||
UdpTrackerResponseHeader::decode(input)?.expect_action(UdpTrackerAction::Scrape)?;
|
||||
let payload = &input[8..];
|
||||
if !payload.len().is_multiple_of(12) {
|
||||
return Err(TrackerParseError::InvalidUdpPacket(
|
||||
"scrape response payload length must be divisible by 12".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut files = Vec::with_capacity(payload.len() / 12);
|
||||
for offset in (0..payload.len()).step_by(12) {
|
||||
files.push(UdpTrackerScrapeStats {
|
||||
complete: read_u32_be(payload, offset, "scrape complete count")?,
|
||||
downloaded: read_u32_be(payload, offset + 4, "scrape downloaded count")?,
|
||||
incomplete: read_u32_be(payload, offset + 8, "scrape incomplete count")?,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
transaction_id: header.transaction_id,
|
||||
files,
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts the response into the higher-level scrape model.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error when the info-hash list does not match the response entry count.
|
||||
pub fn to_scrape_model(
|
||||
&self,
|
||||
info_hashes: &[[u8; 20]],
|
||||
) -> Result<TrackerScrapeModel, TrackerParseError> {
|
||||
if info_hashes.len() != self.files.len() {
|
||||
return Err(TrackerParseError::InvalidUdpPacket(format!(
|
||||
"scrape response entry count {} does not match info-hash count {}",
|
||||
self.files.len(),
|
||||
info_hashes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let files = info_hashes
|
||||
.iter()
|
||||
.zip(self.files.iter())
|
||||
.map(|(info_hash, stats)| TrackerScrapeFileModel {
|
||||
info_hash: super::parsing::hex_encode(info_hash),
|
||||
complete: Some(stats.complete),
|
||||
downloaded: Some(stats.downloaded),
|
||||
incomplete: Some(stats.incomplete),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (complete, downloaded, incomplete) = if self.files.len() == 1 {
|
||||
let stats = self.files[0];
|
||||
(
|
||||
Some(stats.complete),
|
||||
Some(stats.downloaded),
|
||||
Some(stats.incomplete),
|
||||
)
|
||||
} else {
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
Ok(TrackerScrapeModel {
|
||||
complete,
|
||||
downloaded,
|
||||
incomplete,
|
||||
files,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Verifies that a UDP tracker payload is at least `min_len` bytes long.
|
||||
fn ensure_udp_payload_len(
|
||||
input: &[u8],
|
||||
min_len: usize,
|
||||
label: &str,
|
||||
) -> Result<(), TrackerParseError> {
|
||||
if input.len() < min_len {
|
||||
return Err(TrackerParseError::InvalidUdpPacket(format!(
|
||||
"{label} truncated: expected at least {min_len} bytes but got {}",
|
||||
input.len()
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads one big-endian `u32` from a UDP tracker payload.
|
||||
fn read_u32_be(input: &[u8], offset: usize, field: &str) -> Result<u32, TrackerParseError> {
|
||||
let end = offset.saturating_add(4);
|
||||
let bytes = input
|
||||
.get(offset..end)
|
||||
.ok_or_else(|| TrackerParseError::InvalidUdpPacket(format!("{field} truncated")))?;
|
||||
let mut out = [0_u8; 4];
|
||||
out.copy_from_slice(bytes);
|
||||
Ok(u32::from_be_bytes(out))
|
||||
}
|
||||
|
||||
/// Reads one big-endian `u64` from a UDP tracker payload.
|
||||
fn read_u64_be(input: &[u8], offset: usize, field: &str) -> Result<u64, TrackerParseError> {
|
||||
let end = offset.saturating_add(8);
|
||||
let bytes = input
|
||||
.get(offset..end)
|
||||
.ok_or_else(|| TrackerParseError::InvalidUdpPacket(format!("{field} truncated")))?;
|
||||
let mut out = [0_u8; 8];
|
||||
out.copy_from_slice(bytes);
|
||||
Ok(u64::from_be_bytes(out))
|
||||
}
|
||||
Reference in New Issue
Block a user