btpeer 0.3.0

Simple CLI tool to get peers from TCP/HTTP and UDP BitTorrent trackers
mod response;

use bendy::decoding::FromBencode;
use rand::RngExt;
use std::{
    error::Error,
    net::{Ipv4Addr, Ipv6Addr},
    time::Duration,
};
use url::Url;

pub fn handle(
    info_hash: &[u8; 20],
    mut url: Url,
    port: u16,
    timeout: u64,
) -> Result<(), Box<dyn Error>> {
    fn url_encode_bytes(bytes: &[u8]) -> String {
        bytes.iter().map(|b| format!("%{:02x}", b)).collect()
    }

    let mut rng = rand::rng();
    let mut peer_id = [0u8; 20];
    rng.fill(&mut peer_id);

    url.set_query(Some(&format!(
        "info_hash={}&peer_id={}&port={port}&uploaded=0&downloaded=0&left=1000&compact=1",
        url_encode_bytes(info_hash),
        url_encode_bytes(&peer_id)
    )));

    let target = url.as_str();

    let client = reqwest::blocking::Client::builder()
        .timeout(Duration::from_secs(timeout))
        .build()?;

    println!("Sending HTTP announce to `{target}`...");

    let response = client.get(target).send()?;

    if !response.status().is_success() {
        return Err(format!("Tracker returned status: {}", response.status()).into());
    }

    let response = client.get(target).send()?;

    if !response.status().is_success() {
        return Err(format!("Tracker returned status: {}", response.status()).into());
    }

    let body = response.bytes()?;

    let result = response::Response::from_bencode(&body)?;

    println!("Update interval: {}s", result.interval);
    println!("L: {}, S: {}", result.incomplete, result.complete);

    if !result.peers.is_empty() {
        if url.host_str().is_some_and(|h| h.ends_with(".i2p")) {
            for chunk in result.peers.chunks_exact(32) {
                println!(
                    "{}.b32.i2p",
                    data_encoding::BASE32_NOPAD.encode(chunk).to_lowercase()
                );
            }
        } else {
            for chunk in result.peers.chunks_exact(6) {
                let ip_bytes: [u8; 4] = chunk[0..4].try_into()?;
                let ip = Ipv4Addr::from(ip_bytes);
                let port = u16::from_be_bytes(chunk[4..6].try_into()?);
                println!("{ip}:{port}");
            }
        }
    }

    if !result.peers6.is_empty() {
        for chunk in result.peers6.chunks_exact(18) {
            let ip_bytes: [u8; 16] = chunk[0..16].try_into()?;
            let ip = Ipv6Addr::from(ip_bytes);
            let port = u16::from_be_bytes(chunk[16..18].try_into()?);
            println!("[{ip}]:{port}");
        }
    }

    if result.peers.is_empty() && result.peers6.is_empty() {
        println!("No peers.");
    }

    Ok(())
}