rustybit_lib/
tracker.rs

1use std::net::{Ipv4Addr, SocketAddrV4};
2
3use anyhow::Context;
4use serde::{Deserialize, Serialize};
5use serde_with::{serde_as, Bytes};
6
7pub const PORT: u16 = 6881;
8
9#[derive(Debug, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum EventType {
12    Started,
13    Stopped,
14    Completed,
15}
16
17#[derive(Debug, Serialize, Deserialize)]
18pub struct TrackerRequest<'a> {
19    /// SHA1 hash of the info field in the MetaInfo struct
20    #[serde(skip_serializing)]
21    info_hash: [u8; 20],
22    /// Unique client ID. Randomly generated
23    peer_id: &'a str,
24    /// The port number the client is listening on
25    port: u16,
26    /// The total amount uploaded
27    uploaded: u64,
28    /// The total amount downloaded
29    downloaded: u64,
30    /// The number of bytes the client STILL has to download
31    left: u64,
32    /// States that the client accepts a compact response. Is always set to 1
33    compact: u8,
34    /// If omitted - this message is a message sent on a regular basis without any specific event
35    event: Option<EventType>,
36}
37
38impl<'a> TrackerRequest<'a> {
39    pub fn new(
40        peer_id: &'a str,
41        info_hash: [u8; 20],
42        download_state: &[Option<u64>; 3],
43        event: Option<EventType>,
44    ) -> Self {
45        TrackerRequest {
46            info_hash,
47            peer_id,
48            port: PORT,
49            uploaded: download_state[0].unwrap_or_default(),
50            downloaded: download_state[1].unwrap_or_default(),
51            left: download_state[2].unwrap_or_default(),
52            compact: 1,
53            event,
54        }
55    }
56
57    pub fn into_query_params(self) -> anyhow::Result<String> {
58        let info_hash = form_urlencoded::byte_serialize(&self.info_hash).collect::<String>();
59
60        // HACK: Serialize everything but `info_hash`. I didn't find a way to properly serialize it,
61        // as serde can't serialize arrays and I also can't use URL encoding on the array to make it
62        // a String before serializing with Serde, as that results in double URL encoding
63        let mut serialized = serde_urlencoded::to_string(self).context("failed to serialize the tracker request")?;
64
65        // Add `peer_id`
66        serialized.push_str("&info_hash=");
67        serialized.push_str(&info_hash);
68
69        Ok(serialized)
70    }
71}
72
73#[serde_as]
74#[derive(Debug, Serialize, Deserialize)]
75pub struct TrackerResponse {
76    /// A human-readable error
77    #[serde(rename = "failure reason")]
78    failure_reason: Option<String>,
79    /// Number of seconds to wait between regular requests
80    interval: Option<u64>,
81    /// List of peers
82    #[serde_as(as = "Option<Bytes>")]
83    peers: Option<Vec<u8>>,
84}
85
86impl TrackerResponse {
87    /// Uses compact format as described in [BEP-23](https://www.bittorrent.org/beps/bep_0023.https)
88    pub fn get_peers(&self) -> Option<anyhow::Result<Vec<SocketAddrV4>>> {
89        self.peers.as_ref().map(|peers| {
90            // BitTorrent represents each peer as a 6-byte value. The first 4 bytes are the IP address, and the last 2 are the port
91            let mut peer_ips: Vec<SocketAddrV4> = Vec::with_capacity(peers.len() / 6);
92            for chunk in peers.chunks(6) {
93                let ip = try_into!(&chunk[..4], [u8; 4])?;
94                let port = ((chunk[4] as u16) << 8) | chunk[5] as u16;
95
96                let ip_addr = Ipv4Addr::from(ip);
97
98                peer_ips.push(SocketAddrV4::new(ip_addr, port));
99            }
100            Ok(peer_ips)
101        })
102    }
103}