torrust_tracker/console/clients/http/
app.rs

1//! HTTP Tracker client:
2//!
3//! Examples:
4//!
5//! `Announce` request:
6//!
7//! ```text
8//! cargo run --bin http_tracker_client announce http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 | jq
9//! ```
10//!
11//! `Scrape` request:
12//!
13//! ```text
14//! cargo run --bin http_tracker_client scrape http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 | jq
15//! ```
16use std::str::FromStr;
17use std::time::Duration;
18
19use anyhow::Context;
20use clap::{Parser, Subcommand};
21use reqwest::Url;
22use torrust_tracker_configuration::DEFAULT_TIMEOUT;
23use torrust_tracker_primitives::info_hash::InfoHash;
24
25use crate::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder;
26use crate::shared::bit_torrent::tracker::http::client::responses::announce::Announce;
27use crate::shared::bit_torrent::tracker::http::client::responses::scrape;
28use crate::shared::bit_torrent::tracker::http::client::{requests, Client};
29
30#[derive(Parser, Debug)]
31#[command(author, version, about, long_about = None)]
32struct Args {
33    #[command(subcommand)]
34    command: Command,
35}
36
37#[derive(Subcommand, Debug)]
38enum Command {
39    Announce { tracker_url: String, info_hash: String },
40    Scrape { tracker_url: String, info_hashes: Vec<String> },
41}
42
43/// # Errors
44///
45/// Will return an error if the command fails.
46pub async fn run() -> anyhow::Result<()> {
47    let args = Args::parse();
48
49    match args.command {
50        Command::Announce { tracker_url, info_hash } => {
51            announce_command(tracker_url, info_hash, DEFAULT_TIMEOUT).await?;
52        }
53        Command::Scrape {
54            tracker_url,
55            info_hashes,
56        } => {
57            scrape_command(&tracker_url, &info_hashes, DEFAULT_TIMEOUT).await?;
58        }
59    }
60
61    Ok(())
62}
63
64async fn announce_command(tracker_url: String, info_hash: String, timeout: Duration) -> anyhow::Result<()> {
65    let base_url = Url::parse(&tracker_url).context("failed to parse HTTP tracker base URL")?;
66    let info_hash =
67        InfoHash::from_str(&info_hash).expect("Invalid infohash. Example infohash: `9c38422213e30bff212b30c360d26f9a02136422`");
68
69    let response = Client::new(base_url, timeout)?
70        .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query())
71        .await?;
72
73    let body = response.bytes().await?;
74
75    let announce_response: Announce = serde_bencode::from_bytes(&body)
76        .unwrap_or_else(|_| panic!("response body should be a valid announce response, got: \"{:#?}\"", &body));
77
78    let json = serde_json::to_string(&announce_response).context("failed to serialize scrape response into JSON")?;
79
80    println!("{json}");
81
82    Ok(())
83}
84
85async fn scrape_command(tracker_url: &str, info_hashes: &[String], timeout: Duration) -> anyhow::Result<()> {
86    let base_url = Url::parse(tracker_url).context("failed to parse HTTP tracker base URL")?;
87
88    let query = requests::scrape::Query::try_from(info_hashes).context("failed to parse infohashes")?;
89
90    let response = Client::new(base_url, timeout)?.scrape(&query).await?;
91
92    let body = response.bytes().await?;
93
94    let scrape_response = scrape::Response::try_from_bencoded(&body)
95        .unwrap_or_else(|_| panic!("response body should be a valid scrape response, got: \"{:#?}\"", &body));
96
97    let json = serde_json::to_string(&scrape_response).context("failed to serialize scrape response into JSON")?;
98
99    println!("{json}");
100
101    Ok(())
102}