btpeer 0.8.0

Simple CLI tool and library to get peers from TCP/HTTP and UDP BitTorrent trackers
Documentation
pub mod response;
pub use response::{Announce, Scrape};

use crate::Peer;
use anyhow::{Result, bail};
use cyphernet::addr::{HostName, PartialAddr};
use rand::RngExt;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};

pub const MAGIC_BYTES: u64 = 0x41727101980u64;

pub trait Server {
    fn announce(
        &self,
        id20: &[u8; 20],
        tracker: &SocketAddr,
        peers_buffer: Option<&mut Vec<Peer>>,
    ) -> Result<Announce>;

    fn scrape(&self, id20: &[[u8; 20]], tracker: &SocketAddr) -> Result<Scrape>;
}

impl Server for UdpSocket {
    fn announce(
        &self,
        id20: &[u8; 20],
        tracker: &SocketAddr,
        peers_buffer: Option<&mut Vec<Peer>>,
    ) -> Result<Announce> {
        // Setup environment...
        let mut rng = rand::rng();
        let transaction_id: u32 = rng.random();

        let mut c = Vec::with_capacity(16);
        c.extend_from_slice(&MAGIC_BYTES.to_be_bytes()); // magic const
        c.extend_from_slice(&0u32.to_be_bytes());
        c.extend_from_slice(&transaction_id.to_be_bytes());

        // Initial connection...
        self.send_to(&c, tracker)?;

        let mut buf = [0u8; 2048];
        let (amt, _) = self.recv_from(&mut buf)?;

        if amt < 16 {
            bail!("Tracker response too short");
        }

        let action = u32::from_be_bytes(buf[0..4].try_into()?);
        let res_transaction_id = u32::from_be_bytes(buf[4..8].try_into()?);

        if res_transaction_id != transaction_id {
            bail!("Transaction ID missmatch");
        }

        if action == 3 {
            bail!("Tracker error: `{}`", String::from_utf8_lossy(&buf[8..amt]));
        }

        let connection_id = u64::from_be_bytes(buf[8..16].try_into()?);

        // Sending announce request...
        let mut a = Vec::with_capacity(98);
        let announce_trans_id: u32 = rng.random();
        let mut peer_id = [0u8; 20];
        rng.fill(&mut peer_id);

        a.extend_from_slice(&connection_id.to_be_bytes());
        a.extend_from_slice(&1u32.to_be_bytes()); // action = 1 (announce)
        a.extend_from_slice(&announce_trans_id.to_be_bytes());
        a.extend_from_slice(id20);
        a.extend_from_slice(&peer_id);
        a.extend_from_slice(&0u64.to_be_bytes()); // downloaded
        a.extend_from_slice(&1024u64.to_be_bytes()); // left
        a.extend_from_slice(&0u64.to_be_bytes()); // uploaded
        a.extend_from_slice(&0u32.to_be_bytes()); // event = 0 (none)
        a.extend_from_slice(&0u32.to_be_bytes()); // IP address = 0 (default)
        a.extend_from_slice(&0u32.to_be_bytes()); // key
        a.extend_from_slice(&(-1i32).to_be_bytes()); // num want = -1
        a.extend_from_slice(&self.local_addr()?.port().to_be_bytes());

        self.send_to(&a, tracker)?;

        let (amt, _) = self.recv_from(&mut buf)?;

        let res_action = u32::from_be_bytes(buf[0..4].try_into()?);
        let res_announce_trans_id = u32::from_be_bytes(buf[4..8].try_into()?);

        if res_announce_trans_id != announce_trans_id {
            bail!("Announce Transaction ID does not match.");
        }

        if res_action == 3 {
            bail!(
                "Tracker announce declined: {}",
                String::from_utf8_lossy(&buf[8..amt])
            );
        }

        if amt < 20 {
            bail!("Unexpected prefix len");
        }

        // Extract peers info...
        let interval = u32::from_be_bytes(buf[8..12].try_into()?);
        let leechers = u32::from_be_bytes(buf[12..16].try_into()?);
        let seeders = u32::from_be_bytes(buf[16..20].try_into()?);

        let peers_data = &buf[20..amt];

        if let Some(peers) = peers_buffer {
            if tracker.is_ipv6() {
                for chunk in peers_data.chunks_exact(18) {
                    let ip_bytes: [u8; 16] = chunk[0..16].try_into()?;
                    peers.push(PartialAddr {
                        host: HostName::Ip(IpAddr::V6(Ipv6Addr::from(ip_bytes))),
                        port: Some(u16::from_be_bytes(chunk[16..18].try_into()?)),
                    })
                }
            } else {
                for chunk in peers_data.chunks_exact(6) {
                    let ip_bytes: [u8; 4] = chunk[0..4].try_into()?;
                    peers.push(PartialAddr {
                        host: HostName::Ip(IpAddr::V4(Ipv4Addr::from(ip_bytes))),
                        port: Some(u16::from_be_bytes(chunk[4..6].try_into()?)),
                    })
                }
            }
        }

        // Done.
        Ok(Announce {
            interval,
            leechers,
            seeders,
        })
    }

    fn scrape(&self, id20: &[[u8; 20]], tracker: &SocketAddr) -> Result<Scrape> {
        let mut response = Scrape::default();

        self.send_to(
            &{
                let mut request = Vec::new();
                request.extend_from_slice(&MAGIC_BYTES.to_be_bytes());
                request.extend_from_slice(&0u32.to_be_bytes());
                request.extend_from_slice(&rand::rng().random::<u32>().to_be_bytes());
                request
            },
            tracker,
        )?;

        let mut b = [0u8; 16];
        if self.recv(&mut b)? < 16 {
            bail!("unexpected buffer len")
        }

        self.send_to(
            &{
                let mut q = Vec::new();
                q.extend_from_slice(&u64::from_be_bytes(b[8..16].try_into()?).to_be_bytes());
                q.extend_from_slice(&2u32.to_be_bytes());
                q.extend_from_slice(&rand::rng().random::<u32>().to_be_bytes());
                // * up to about 74 torrents can be scraped at once
                //   https://www.bittorrent.org/beps/bep_0015.html
                if id20.len() > 74 {
                    bail!(
                        "up to about 74 torrents can be scraped at once (given {})",
                        id20.len()
                    )
                }
                for i in id20 {
                    q.extend_from_slice(i);
                }
                q
            },
            tracker,
        )?;

        let mut b = [0u8; 1024];
        let l = self.recv(&mut b)?;
        if l < 20 {
            bail!("unexpected response len")
        }

        response.seeders += u32::from_be_bytes(b[8..12].try_into()?);
        response.leechers += u32::from_be_bytes(b[12..16].try_into()?);
        response.peers += u32::from_be_bytes(b[16..20].try_into()?);

        Ok(response)
    }
}