btpeer 0.10.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::Buffer;
use anyhow::{Result, bail};
use rand::RngExt;
use std::net::{SocketAddr, UdpSocket};

pub const MAGIC_BYTES: u64 = 0x41727101980u64;

pub trait Server {
    fn announce(&self, id20: &[u8; 20], tracker: &SocketAddr) -> Result<Announce>;
    fn scrape(&self, id20: &[[u8; 20]], tracker: &SocketAddr) -> Result<Scrape>;
}

impl Server for UdpSocket {
    fn announce(&self, id20: &[u8; 20], tracker: &SocketAddr) -> 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");
        }

        Ok(Announce {
            interval: u32::from_be_bytes(buf[8..12].try_into()?),
            leechers: u32::from_be_bytes(buf[12..16].try_into()?),
            seeders: u32::from_be_bytes(buf[16..20].try_into()?),
            peers: {
                let peers = &buf[20..amt];
                if tracker.is_ipv6() {
                    Buffer::from_peers6(peers)?
                } else {
                    Buffer::from_peers(peers)?
                }
            },
        })
    }

    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)
    }
}