From d8a9f7b358ce0b8cbf806d188a8c89abb4f54ffd Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 30 Jan 2024 10:19:22 +0000 Subject: [PATCH 1/4] refactor: [#661] move E2E tests runner mod --- src/bin/e2e_tests_runner.rs | 6 +----- src/{ => console/ci}/e2e/docker.rs | 0 src/{ => console/ci}/e2e/logs_parser.rs | 0 src/{ => console/ci}/e2e/mod.rs | 1 + src/{ => console/ci}/e2e/runner.rs | 11 ++++++++--- src/{ => console/ci}/e2e/tracker_checker.rs | 0 src/{ => console/ci}/e2e/tracker_container.rs | 2 +- src/console/ci/mod.rs | 2 ++ src/console/clients/mod.rs | 1 + src/console/mod.rs | 3 +++ src/lib.rs | 2 +- 11 files changed, 18 insertions(+), 10 deletions(-) rename src/{ => console/ci}/e2e/docker.rs (100%) rename src/{ => console/ci}/e2e/logs_parser.rs (100%) rename src/{ => console/ci}/e2e/mod.rs (82%) rename src/{ => console/ci}/e2e/runner.rs (93%) rename src/{ => console/ci}/e2e/tracker_checker.rs (100%) rename src/{ => console/ci}/e2e/tracker_container.rs (98%) create mode 100644 src/console/ci/mod.rs create mode 100644 src/console/clients/mod.rs create mode 100644 src/console/mod.rs diff --git a/src/bin/e2e_tests_runner.rs b/src/bin/e2e_tests_runner.rs index 35368b612..b21459d2e 100644 --- a/src/bin/e2e_tests_runner.rs +++ b/src/bin/e2e_tests_runner.rs @@ -1,9 +1,5 @@ //! Program to run E2E tests. -//! -//! ```text -//! cargo run --bin e2e_tests_runner share/default/config/tracker.e2e.container.sqlite3.toml -//! ``` -use torrust_tracker::e2e; +use torrust_tracker::console::ci::e2e; fn main() { e2e::runner::run(); diff --git a/src/e2e/docker.rs b/src/console/ci/e2e/docker.rs similarity index 100% rename from src/e2e/docker.rs rename to src/console/ci/e2e/docker.rs diff --git a/src/e2e/logs_parser.rs b/src/console/ci/e2e/logs_parser.rs similarity index 100% rename from src/e2e/logs_parser.rs rename to src/console/ci/e2e/logs_parser.rs diff --git a/src/e2e/mod.rs b/src/console/ci/e2e/mod.rs similarity index 82% rename from src/e2e/mod.rs rename to src/console/ci/e2e/mod.rs index e4384e160..58a876cbe 100644 --- a/src/e2e/mod.rs +++ b/src/console/ci/e2e/mod.rs @@ -1,3 +1,4 @@ +//! E2E tests scripts. pub mod docker; pub mod logs_parser; pub mod runner; diff --git a/src/e2e/runner.rs b/src/console/ci/e2e/runner.rs similarity index 93% rename from src/e2e/runner.rs rename to src/console/ci/e2e/runner.rs index a4bcb3aa3..1a4746800 100644 --- a/src/e2e/runner.rs +++ b/src/console/ci/e2e/runner.rs @@ -1,9 +1,14 @@ +//! Program to run E2E tests. +//! +//! ```text +//! cargo run --bin e2e_tests_runner share/default/config/tracker.e2e.container.sqlite3.toml +//! ``` use log::{debug, info, LevelFilter}; use super::tracker_container::TrackerContainer; -use crate::e2e::docker::RunOptions; -use crate::e2e::logs_parser::RunningServices; -use crate::e2e::tracker_checker::{self}; +use crate::console::ci::e2e::docker::RunOptions; +use crate::console::ci::e2e::logs_parser::RunningServices; +use crate::console::ci::e2e::tracker_checker::{self}; /* code-review: - We use always the same docker image name. Should we use a random image name (tag)? diff --git a/src/e2e/tracker_checker.rs b/src/console/ci/e2e/tracker_checker.rs similarity index 100% rename from src/e2e/tracker_checker.rs rename to src/console/ci/e2e/tracker_checker.rs diff --git a/src/e2e/tracker_container.rs b/src/console/ci/e2e/tracker_container.rs similarity index 98% rename from src/e2e/tracker_container.rs rename to src/console/ci/e2e/tracker_container.rs index 3e70942b5..5a4d11d02 100644 --- a/src/e2e/tracker_container.rs +++ b/src/console/ci/e2e/tracker_container.rs @@ -6,7 +6,7 @@ use rand::Rng; use super::docker::{RunOptions, RunningContainer}; use super::logs_parser::RunningServices; -use crate::e2e::docker::Docker; +use crate::console::ci::e2e::docker::Docker; #[derive(Debug)] pub struct TrackerContainer { diff --git a/src/console/ci/mod.rs b/src/console/ci/mod.rs new file mode 100644 index 000000000..6eac3e120 --- /dev/null +++ b/src/console/ci/mod.rs @@ -0,0 +1,2 @@ +//! Continuos integration scripts. +pub mod e2e; diff --git a/src/console/clients/mod.rs b/src/console/clients/mod.rs new file mode 100644 index 000000000..a3fd318b2 --- /dev/null +++ b/src/console/clients/mod.rs @@ -0,0 +1 @@ +//! Console clients. diff --git a/src/console/mod.rs b/src/console/mod.rs new file mode 100644 index 000000000..54ed8e415 --- /dev/null +++ b/src/console/mod.rs @@ -0,0 +1,3 @@ +//! Console apps. +pub mod ci; +pub mod clients; diff --git a/src/lib.rs b/src/lib.rs index f239039bd..398795d37 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -472,8 +472,8 @@ pub mod app; pub mod bootstrap; pub mod checker; +pub mod console; pub mod core; -pub mod e2e; pub mod servers; pub mod shared; From 0960ff269529fadff6dd9152445c7939c1cbd9f0 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 30 Jan 2024 12:16:14 +0000 Subject: [PATCH 2/4] refactor: [#661] move Tracker Checker mod --- src/bin/tracker_checker.rs | 17 ++--------------- src/{ => console/clients}/checker/app.rs | 16 +++++++++++++++- src/{ => console/clients}/checker/config.rs | 2 +- src/{ => console/clients}/checker/console.rs | 0 src/{ => console/clients}/checker/logger.rs | 4 ++-- src/{ => console/clients}/checker/mod.rs | 0 src/{ => console/clients}/checker/printer.rs | 0 src/{ => console/clients}/checker/service.rs | 2 +- src/console/clients/mod.rs | 1 + src/lib.rs | 1 - 10 files changed, 22 insertions(+), 21 deletions(-) rename src/{ => console/clients}/checker/app.rs (73%) rename src/{ => console/clients}/checker/config.rs (98%) rename src/{ => console/clients}/checker/console.rs (100%) rename src/{ => console/clients}/checker/logger.rs (91%) rename src/{ => console/clients}/checker/mod.rs (100%) rename src/{ => console/clients}/checker/printer.rs (100%) rename src/{ => console/clients}/checker/service.rs (98%) diff --git a/src/bin/tracker_checker.rs b/src/bin/tracker_checker.rs index 926a0026c..1bda0f54f 100644 --- a/src/bin/tracker_checker.rs +++ b/src/bin/tracker_checker.rs @@ -1,18 +1,5 @@ -//! Program to run checks against running trackers. -//! -//! Run providing a config file path: -//! -//! ```text -//! cargo run --bin tracker_checker -- --config-path "./share/default/config/tracker_checker.json" -//! TORRUST_CHECKER_CONFIG_PATH="./share/default/config/tracker_checker.json" cargo run --bin tracker_checker -//! ``` -//! -//! Run providing the configuration: -//! -//! ```text -//! TORRUST_CHECKER_CONFIG=$(cat "./share/default/config/tracker_checker.json") cargo run --bin tracker_checker -//! ``` -use torrust_tracker::checker::app; +//! Program to run check running trackers. +use torrust_tracker::console::clients::checker::app; #[tokio::main] async fn main() { diff --git a/src/checker/app.rs b/src/console/clients/checker/app.rs similarity index 73% rename from src/checker/app.rs rename to src/console/clients/checker/app.rs index 1e91ce846..bca4b64dc 100644 --- a/src/checker/app.rs +++ b/src/console/clients/checker/app.rs @@ -1,3 +1,17 @@ +//! Program to run checks against running trackers. +//! +//! Run providing a config file path: +//! +//! ```text +//! cargo run --bin tracker_checker -- --config-path "./share/default/config/tracker_checker.json" +//! TORRUST_CHECKER_CONFIG_PATH="./share/default/config/tracker_checker.json" cargo run --bin tracker_checker +//! ``` +//! +//! Run providing the configuration: +//! +//! ```text +//! TORRUST_CHECKER_CONFIG=$(cat "./share/default/config/tracker_checker.json") cargo run --bin tracker_checker +//! ``` use std::path::PathBuf; use std::sync::Arc; @@ -7,7 +21,7 @@ use clap::Parser; use super::config::Configuration; use super::console::Console; use super::service::{CheckResult, Service}; -use crate::checker::config::parse_from_json; +use crate::console::clients::checker::config::parse_from_json; #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] diff --git a/src/checker/config.rs b/src/console/clients/checker/config.rs similarity index 98% rename from src/checker/config.rs rename to src/console/clients/checker/config.rs index 5cfee0760..0a2c09b03 100644 --- a/src/checker/config.rs +++ b/src/console/clients/checker/config.rs @@ -117,7 +117,7 @@ mod tests { } mod building_configuration_from_plan_configuration { - use crate::checker::config::{Configuration, PlainConfiguration}; + use crate::console::clients::checker::config::{Configuration, PlainConfiguration}; #[test] fn it_should_fail_when_a_tracker_udp_address_is_invalid() { diff --git a/src/checker/console.rs b/src/console/clients/checker/console.rs similarity index 100% rename from src/checker/console.rs rename to src/console/clients/checker/console.rs diff --git a/src/checker/logger.rs b/src/console/clients/checker/logger.rs similarity index 91% rename from src/checker/logger.rs rename to src/console/clients/checker/logger.rs index 3d1074e7b..50e97189f 100644 --- a/src/checker/logger.rs +++ b/src/console/clients/checker/logger.rs @@ -49,8 +49,8 @@ impl Printer for Logger { #[cfg(test)] mod tests { - use crate::checker::logger::Logger; - use crate::checker::printer::{Printer, CLEAR_SCREEN}; + use crate::console::clients::checker::logger::Logger; + use crate::console::clients::checker::printer::{Printer, CLEAR_SCREEN}; #[test] fn should_capture_the_clear_screen_command() { diff --git a/src/checker/mod.rs b/src/console/clients/checker/mod.rs similarity index 100% rename from src/checker/mod.rs rename to src/console/clients/checker/mod.rs diff --git a/src/checker/printer.rs b/src/console/clients/checker/printer.rs similarity index 100% rename from src/checker/printer.rs rename to src/console/clients/checker/printer.rs diff --git a/src/checker/service.rs b/src/console/clients/checker/service.rs similarity index 98% rename from src/checker/service.rs rename to src/console/clients/checker/service.rs index fd93ed8c0..5f464fbd1 100644 --- a/src/checker/service.rs +++ b/src/console/clients/checker/service.rs @@ -7,7 +7,7 @@ use reqwest::{Client, Url}; use super::config::Configuration; use super::console::Console; -use crate::checker::printer::Printer; +use crate::console::clients::checker::printer::Printer; pub struct Service { pub(crate) config: Arc, diff --git a/src/console/clients/mod.rs b/src/console/clients/mod.rs index a3fd318b2..55ece612b 100644 --- a/src/console/clients/mod.rs +++ b/src/console/clients/mod.rs @@ -1 +1,2 @@ //! Console clients. +pub mod checker; diff --git a/src/lib.rs b/src/lib.rs index 398795d37..b4ad298ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -471,7 +471,6 @@ //! examples on the integration and unit tests. pub mod app; pub mod bootstrap; -pub mod checker; pub mod console; pub mod core; pub mod servers; From b96c2c37544c2db6d7ef3c9f1fb2070dacb52eb1 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 30 Jan 2024 12:25:04 +0000 Subject: [PATCH 3/4] refactor: [#661] move HTTP Tracker Client mod --- src/bin/http_tracker_client.rs | 95 +----------------------------- src/console/clients/http/app.rs | 100 ++++++++++++++++++++++++++++++++ src/console/clients/http/mod.rs | 1 + src/console/clients/mod.rs | 1 + 4 files changed, 105 insertions(+), 92 deletions(-) create mode 100644 src/console/clients/http/app.rs create mode 100644 src/console/clients/http/mod.rs diff --git a/src/bin/http_tracker_client.rs b/src/bin/http_tracker_client.rs index 4ca194803..0de040549 100644 --- a/src/bin/http_tracker_client.rs +++ b/src/bin/http_tracker_client.rs @@ -1,96 +1,7 @@ -//! HTTP Tracker client: -//! -//! Examples: -//! -//! `Announce` request: -//! -//! ```text -//! cargo run --bin http_tracker_client announce http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 | jq -//! ``` -//! -//! `Scrape` request: -//! -//! ```text -//! cargo run --bin http_tracker_client scrape http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 | jq -//! ``` -use std::str::FromStr; - -use anyhow::Context; -use clap::{Parser, Subcommand}; -use reqwest::Url; -use torrust_tracker::shared::bit_torrent::info_hash::InfoHash; -use torrust_tracker::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder; -use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::announce::Announce; -use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::scrape; -use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, Client}; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - #[command(subcommand)] - command: Command, -} - -#[derive(Subcommand, Debug)] -enum Command { - Announce { tracker_url: String, info_hash: String }, - Scrape { tracker_url: String, info_hashes: Vec }, -} +//! Program to make request to HTTP trackers. +use torrust_tracker::console::clients::http::app; #[tokio::main] async fn main() -> anyhow::Result<()> { - let args = Args::parse(); - - match args.command { - Command::Announce { tracker_url, info_hash } => { - announce_command(tracker_url, info_hash).await?; - } - Command::Scrape { - tracker_url, - info_hashes, - } => { - scrape_command(&tracker_url, &info_hashes).await?; - } - } - Ok(()) -} - -async fn announce_command(tracker_url: String, info_hash: String) -> anyhow::Result<()> { - let base_url = Url::parse(&tracker_url).context("failed to parse HTTP tracker base URL")?; - let info_hash = - InfoHash::from_str(&info_hash).expect("Invalid infohash. Example infohash: `9c38422213e30bff212b30c360d26f9a02136422`"); - - let response = Client::new(base_url) - .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query()) - .await; - - let body = response.bytes().await.unwrap(); - - let announce_response: Announce = serde_bencode::from_bytes(&body) - .unwrap_or_else(|_| panic!("response body should be a valid announce response, got: \"{:#?}\"", &body)); - - let json = serde_json::to_string(&announce_response).context("failed to serialize scrape response into JSON")?; - - println!("{json}"); - - Ok(()) -} - -async fn scrape_command(tracker_url: &str, info_hashes: &[String]) -> anyhow::Result<()> { - let base_url = Url::parse(tracker_url).context("failed to parse HTTP tracker base URL")?; - - let query = requests::scrape::Query::try_from(info_hashes).context("failed to parse infohashes")?; - - let response = Client::new(base_url).scrape(&query).await; - - let body = response.bytes().await.unwrap(); - - let scrape_response = scrape::Response::try_from_bencoded(&body) - .unwrap_or_else(|_| panic!("response body should be a valid scrape response, got: \"{:#?}\"", &body)); - - let json = serde_json::to_string(&scrape_response).context("failed to serialize scrape response into JSON")?; - - println!("{json}"); - - Ok(()) + app::run().await } diff --git a/src/console/clients/http/app.rs b/src/console/clients/http/app.rs new file mode 100644 index 000000000..80db07231 --- /dev/null +++ b/src/console/clients/http/app.rs @@ -0,0 +1,100 @@ +//! HTTP Tracker client: +//! +//! Examples: +//! +//! `Announce` request: +//! +//! ```text +//! cargo run --bin http_tracker_client announce http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! ``` +//! +//! `Scrape` request: +//! +//! ```text +//! cargo run --bin http_tracker_client scrape http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! ``` +use std::str::FromStr; + +use anyhow::Context; +use clap::{Parser, Subcommand}; +use reqwest::Url; + +use crate::shared::bit_torrent::info_hash::InfoHash; +use crate::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder; +use crate::shared::bit_torrent::tracker::http::client::responses::announce::Announce; +use crate::shared::bit_torrent::tracker::http::client::responses::scrape; +use crate::shared::bit_torrent::tracker::http::client::{requests, Client}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + Announce { tracker_url: String, info_hash: String }, + Scrape { tracker_url: String, info_hashes: Vec }, +} + +/// # Errors +/// +/// Will return an error if the command fails. +pub async fn run() -> anyhow::Result<()> { + let args = Args::parse(); + + match args.command { + Command::Announce { tracker_url, info_hash } => { + announce_command(tracker_url, info_hash).await?; + } + Command::Scrape { + tracker_url, + info_hashes, + } => { + scrape_command(&tracker_url, &info_hashes).await?; + } + } + + Ok(()) +} + +async fn announce_command(tracker_url: String, info_hash: String) -> anyhow::Result<()> { + let base_url = Url::parse(&tracker_url).context("failed to parse HTTP tracker base URL")?; + let info_hash = + InfoHash::from_str(&info_hash).expect("Invalid infohash. Example infohash: `9c38422213e30bff212b30c360d26f9a02136422`"); + + let response = Client::new(base_url) + .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query()) + .await; + + let body = response.bytes().await.unwrap(); + + let announce_response: Announce = serde_bencode::from_bytes(&body) + .unwrap_or_else(|_| panic!("response body should be a valid announce response, got: \"{:#?}\"", &body)); + + let json = serde_json::to_string(&announce_response).context("failed to serialize scrape response into JSON")?; + + println!("{json}"); + + Ok(()) +} + +async fn scrape_command(tracker_url: &str, info_hashes: &[String]) -> anyhow::Result<()> { + let base_url = Url::parse(tracker_url).context("failed to parse HTTP tracker base URL")?; + + let query = requests::scrape::Query::try_from(info_hashes).context("failed to parse infohashes")?; + + let response = Client::new(base_url).scrape(&query).await; + + let body = response.bytes().await.unwrap(); + + let scrape_response = scrape::Response::try_from_bencoded(&body) + .unwrap_or_else(|_| panic!("response body should be a valid scrape response, got: \"{:#?}\"", &body)); + + let json = serde_json::to_string(&scrape_response).context("failed to serialize scrape response into JSON")?; + + println!("{json}"); + + Ok(()) +} diff --git a/src/console/clients/http/mod.rs b/src/console/clients/http/mod.rs new file mode 100644 index 000000000..309be6287 --- /dev/null +++ b/src/console/clients/http/mod.rs @@ -0,0 +1 @@ +pub mod app; diff --git a/src/console/clients/mod.rs b/src/console/clients/mod.rs index 55ece612b..278b736e4 100644 --- a/src/console/clients/mod.rs +++ b/src/console/clients/mod.rs @@ -1,2 +1,3 @@ //! Console clients. pub mod checker; +pub mod http; From 47551ff5c029b6110c06d3d408528472edfc376f Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 30 Jan 2024 12:36:04 +0000 Subject: [PATCH 4/4] refactor: [#661] move UDP Tracker Client mod --- src/bin/tracker_checker.rs | 2 +- src/bin/udp_tracker_client.rs | 353 +------------------------------- src/console/clients/mod.rs | 1 + src/console/clients/udp/app.rs | 359 +++++++++++++++++++++++++++++++++ src/console/clients/udp/mod.rs | 1 + 5 files changed, 365 insertions(+), 351 deletions(-) create mode 100644 src/console/clients/udp/app.rs create mode 100644 src/console/clients/udp/mod.rs diff --git a/src/bin/tracker_checker.rs b/src/bin/tracker_checker.rs index 1bda0f54f..87aeedeac 100644 --- a/src/bin/tracker_checker.rs +++ b/src/bin/tracker_checker.rs @@ -1,4 +1,4 @@ -//! Program to run check running trackers. +//! Program to check running trackers. use torrust_tracker::console::clients::checker::app; #[tokio::main] diff --git a/src/bin/udp_tracker_client.rs b/src/bin/udp_tracker_client.rs index 2c8e63cd0..909b296ca 100644 --- a/src/bin/udp_tracker_client.rs +++ b/src/bin/udp_tracker_client.rs @@ -1,354 +1,7 @@ -//! UDP Tracker client: -//! -//! Examples: -//! -//! Announce request: -//! -//! ```text -//! cargo run --bin udp_tracker_client announce 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq -//! ``` -//! -//! Announce response: -//! -//! ```json -//! { -//! "transaction_id": -888840697 -//! "announce_interval": 120, -//! "leechers": 0, -//! "seeders": 1, -//! "peers": [ -//! "123.123.123.123:51289" -//! ], -//! } -//! ``` -//! -//! Scrape request: -//! -//! ```text -//! cargo run --bin udp_tracker_client scrape 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq -//! ``` -//! -//! Scrape response: -//! -//! ```json -//! { -//! "transaction_id": -888840697, -//! "torrent_stats": [ -//! { -//! "completed": 0, -//! "leechers": 0, -//! "seeders": 0 -//! }, -//! { -//! "completed": 0, -//! "leechers": 0, -//! "seeders": 0 -//! } -//! ] -//! } -//! ``` -//! -//! You can use an URL with instead of the socket address. For example: -//! -//! ```text -//! cargo run --bin udp_tracker_client scrape udp://localhost:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq -//! cargo run --bin udp_tracker_client scrape udp://localhost:6969/scrape 9c38422213e30bff212b30c360d26f9a02136422 | jq -//! ``` -//! -//! The protocol (`udp://`) in the URL is mandatory. The path (`\scrape`) is optional. It always uses `\scrape`. -use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs}; -use std::str::FromStr; - -use anyhow::Context; -use aquatic_udp_protocol::common::InfoHash; -use aquatic_udp_protocol::Response::{AnnounceIpv4, AnnounceIpv6, Scrape}; -use aquatic_udp_protocol::{ - AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Response, - ScrapeRequest, TransactionId, -}; -use clap::{Parser, Subcommand}; -use log::{debug, LevelFilter}; -use serde_json::json; -use torrust_tracker::shared::bit_torrent::info_hash::InfoHash as TorrustInfoHash; -use torrust_tracker::shared::bit_torrent::tracker::udp::client::{UdpClient, UdpTrackerClient}; -use url::Url; - -const ASSIGNED_BY_OS: i32 = 0; -const RANDOM_TRANSACTION_ID: i32 = -888_840_697; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - #[command(subcommand)] - command: Command, -} - -#[derive(Subcommand, Debug)] -enum Command { - Announce { - #[arg(value_parser = parse_socket_addr)] - tracker_socket_addr: SocketAddr, - #[arg(value_parser = parse_info_hash)] - info_hash: TorrustInfoHash, - }, - Scrape { - #[arg(value_parser = parse_socket_addr)] - tracker_socket_addr: SocketAddr, - #[arg(value_parser = parse_info_hash, num_args = 1..=74, value_delimiter = ' ')] - info_hashes: Vec, - }, -} +//! Program to make request to UDP trackers. +use torrust_tracker::console::clients::udp::app; #[tokio::main] async fn main() -> anyhow::Result<()> { - setup_logging(LevelFilter::Info); - - let args = Args::parse(); - - // Configuration - let local_port = ASSIGNED_BY_OS; - let local_bind_to = format!("0.0.0.0:{local_port}"); - let transaction_id = RANDOM_TRANSACTION_ID; - - // Bind to local port - debug!("Binding to: {local_bind_to}"); - let udp_client = UdpClient::bind(&local_bind_to).await; - let bound_to = udp_client.socket.local_addr().unwrap(); - debug!("Bound to: {bound_to}"); - - let transaction_id = TransactionId(transaction_id); - - let response = match args.command { - Command::Announce { - tracker_socket_addr, - info_hash, - } => { - let (connection_id, udp_tracker_client) = connect(&tracker_socket_addr, udp_client, transaction_id).await; - - send_announce_request( - connection_id, - transaction_id, - info_hash, - Port(bound_to.port()), - &udp_tracker_client, - ) - .await - } - Command::Scrape { - tracker_socket_addr, - info_hashes, - } => { - let (connection_id, udp_tracker_client) = connect(&tracker_socket_addr, udp_client, transaction_id).await; - send_scrape_request(connection_id, transaction_id, info_hashes, &udp_tracker_client).await - } - }; - - match response { - AnnounceIpv4(announce) => { - let json = json!({ - "transaction_id": announce.transaction_id.0, - "announce_interval": announce.announce_interval.0, - "leechers": announce.leechers.0, - "seeders": announce.seeders.0, - "peers": announce.peers.iter().map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)).collect::>(), - }); - let pretty_json = serde_json::to_string_pretty(&json).unwrap(); - println!("{pretty_json}"); - } - AnnounceIpv6(announce) => { - let json = json!({ - "transaction_id": announce.transaction_id.0, - "announce_interval": announce.announce_interval.0, - "leechers": announce.leechers.0, - "seeders": announce.seeders.0, - "peers6": announce.peers.iter().map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)).collect::>(), - }); - let pretty_json = serde_json::to_string_pretty(&json).unwrap(); - println!("{pretty_json}"); - } - Scrape(scrape) => { - let json = json!({ - "transaction_id": scrape.transaction_id.0, - "torrent_stats": scrape.torrent_stats.iter().map(|torrent_scrape_statistics| json!({ - "seeders": torrent_scrape_statistics.seeders.0, - "completed": torrent_scrape_statistics.completed.0, - "leechers": torrent_scrape_statistics.leechers.0, - })).collect::>(), - }); - let pretty_json = serde_json::to_string_pretty(&json).unwrap(); - println!("{pretty_json}"); - } - _ => println!("{response:#?}"), // todo: serialize to JSON all responses. - } - - Ok(()) -} - -fn setup_logging(level: LevelFilter) { - if let Err(_err) = fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{} [{}][{}] {}", - chrono::Local::now().format("%+"), - record.target(), - record.level(), - message - )); - }) - .level(level) - .chain(std::io::stdout()) - .apply() - { - panic!("Failed to initialize logging.") - } - - debug!("logging initialized."); -} - -fn parse_socket_addr(tracker_socket_addr_str: &str) -> anyhow::Result { - debug!("Tracker socket address: {tracker_socket_addr_str:#?}"); - - // Check if the address is a valid URL. If so, extract the host and port. - let resolved_addr = if let Ok(url) = Url::parse(tracker_socket_addr_str) { - debug!("Tracker socket address URL: {url:?}"); - - let host = url - .host_str() - .with_context(|| format!("invalid host in URL: `{tracker_socket_addr_str}`"))? - .to_owned(); - - let port = url - .port() - .with_context(|| format!("port not found in URL: `{tracker_socket_addr_str}`"))? - .to_owned(); - - (host, port) - } else { - // If not a URL, assume it's a host:port pair. - - let parts: Vec<&str> = tracker_socket_addr_str.split(':').collect(); - - if parts.len() != 2 { - return Err(anyhow::anyhow!( - "invalid address format: `{}`. Expected format is host:port", - tracker_socket_addr_str - )); - } - - let host = parts[0].to_owned(); - - let port = parts[1] - .parse::() - .with_context(|| format!("invalid port: `{}`", parts[1]))? - .to_owned(); - - (host, port) - }; - - debug!("Resolved address: {resolved_addr:#?}"); - - // Perform DNS resolution. - let socket_addrs: Vec<_> = resolved_addr.to_socket_addrs()?.collect(); - if socket_addrs.is_empty() { - Err(anyhow::anyhow!("DNS resolution failed for `{}`", tracker_socket_addr_str)) - } else { - Ok(socket_addrs[0]) - } -} - -fn parse_info_hash(info_hash_str: &str) -> anyhow::Result { - TorrustInfoHash::from_str(info_hash_str) - .map_err(|e| anyhow::Error::msg(format!("failed to parse info-hash `{info_hash_str}`: {e:?}"))) -} - -async fn connect( - tracker_socket_addr: &SocketAddr, - udp_client: UdpClient, - transaction_id: TransactionId, -) -> (ConnectionId, UdpTrackerClient) { - debug!("Connecting to tracker: udp://{tracker_socket_addr}"); - - udp_client.connect(&tracker_socket_addr.to_string()).await; - - let udp_tracker_client = UdpTrackerClient { udp_client }; - - let connection_id = send_connection_request(transaction_id, &udp_tracker_client).await; - - (connection_id, udp_tracker_client) -} - -async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrackerClient) -> ConnectionId { - debug!("Sending connection request with transaction id: {transaction_id:#?}"); - - let connect_request = ConnectRequest { transaction_id }; - - client.send(connect_request.into()).await; - - let response = client.receive().await; - - debug!("connection request response:\n{response:#?}"); - - match response { - Response::Connect(connect_response) => connect_response.connection_id, - _ => panic!("error connecting to udp server. Unexpected response"), - } -} - -async fn send_announce_request( - connection_id: ConnectionId, - transaction_id: TransactionId, - info_hash: TorrustInfoHash, - port: Port, - client: &UdpTrackerClient, -) -> Response { - debug!("Sending announce request with transaction id: {transaction_id:#?}"); - - let announce_request = AnnounceRequest { - connection_id, - transaction_id, - info_hash: InfoHash(info_hash.bytes()), - peer_id: PeerId(*b"-qB00000000000000001"), - bytes_downloaded: NumberOfBytes(0i64), - bytes_uploaded: NumberOfBytes(0i64), - bytes_left: NumberOfBytes(0i64), - event: AnnounceEvent::Started, - ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)), - key: PeerKey(0u32), - peers_wanted: NumberOfPeers(1i32), - port, - }; - - client.send(announce_request.into()).await; - - let response = client.receive().await; - - debug!("announce request response:\n{response:#?}"); - - response -} - -async fn send_scrape_request( - connection_id: ConnectionId, - transaction_id: TransactionId, - info_hashes: Vec, - client: &UdpTrackerClient, -) -> Response { - debug!("Sending scrape request with transaction id: {transaction_id:#?}"); - - let scrape_request = ScrapeRequest { - connection_id, - transaction_id, - info_hashes: info_hashes - .iter() - .map(|torrust_info_hash| InfoHash(torrust_info_hash.bytes())) - .collect(), - }; - - client.send(scrape_request.into()).await; - - let response = client.receive().await; - - debug!("scrape request response:\n{response:#?}"); - - response + app::run().await } diff --git a/src/console/clients/mod.rs b/src/console/clients/mod.rs index 278b736e4..8492f8ba5 100644 --- a/src/console/clients/mod.rs +++ b/src/console/clients/mod.rs @@ -1,3 +1,4 @@ //! Console clients. pub mod checker; pub mod http; +pub mod udp; diff --git a/src/console/clients/udp/app.rs b/src/console/clients/udp/app.rs new file mode 100644 index 000000000..e9c8b5274 --- /dev/null +++ b/src/console/clients/udp/app.rs @@ -0,0 +1,359 @@ +//! UDP Tracker client: +//! +//! Examples: +//! +//! Announce request: +//! +//! ```text +//! cargo run --bin udp_tracker_client announce 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! ``` +//! +//! Announce response: +//! +//! ```json +//! { +//! "transaction_id": -888840697 +//! "announce_interval": 120, +//! "leechers": 0, +//! "seeders": 1, +//! "peers": [ +//! "123.123.123.123:51289" +//! ], +//! } +//! ``` +//! +//! Scrape request: +//! +//! ```text +//! cargo run --bin udp_tracker_client scrape 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! ``` +//! +//! Scrape response: +//! +//! ```json +//! { +//! "transaction_id": -888840697, +//! "torrent_stats": [ +//! { +//! "completed": 0, +//! "leechers": 0, +//! "seeders": 0 +//! }, +//! { +//! "completed": 0, +//! "leechers": 0, +//! "seeders": 0 +//! } +//! ] +//! } +//! ``` +//! +//! You can use an URL with instead of the socket address. For example: +//! +//! ```text +//! cargo run --bin udp_tracker_client scrape udp://localhost:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! cargo run --bin udp_tracker_client scrape udp://localhost:6969/scrape 9c38422213e30bff212b30c360d26f9a02136422 | jq +//! ``` +//! +//! The protocol (`udp://`) in the URL is mandatory. The path (`\scrape`) is optional. It always uses `\scrape`. +use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs}; +use std::str::FromStr; + +use anyhow::Context; +use aquatic_udp_protocol::common::InfoHash; +use aquatic_udp_protocol::Response::{AnnounceIpv4, AnnounceIpv6, Scrape}; +use aquatic_udp_protocol::{ + AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Response, + ScrapeRequest, TransactionId, +}; +use clap::{Parser, Subcommand}; +use log::{debug, LevelFilter}; +use serde_json::json; +use url::Url; + +use crate::shared::bit_torrent::info_hash::InfoHash as TorrustInfoHash; +use crate::shared::bit_torrent::tracker::udp::client::{UdpClient, UdpTrackerClient}; + +const ASSIGNED_BY_OS: i32 = 0; +const RANDOM_TRANSACTION_ID: i32 = -888_840_697; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand, Debug)] +enum Command { + Announce { + #[arg(value_parser = parse_socket_addr)] + tracker_socket_addr: SocketAddr, + #[arg(value_parser = parse_info_hash)] + info_hash: TorrustInfoHash, + }, + Scrape { + #[arg(value_parser = parse_socket_addr)] + tracker_socket_addr: SocketAddr, + #[arg(value_parser = parse_info_hash, num_args = 1..=74, value_delimiter = ' ')] + info_hashes: Vec, + }, +} + +/// # Errors +/// +/// Will return an error if the command fails. +/// +/// +pub async fn run() -> anyhow::Result<()> { + setup_logging(LevelFilter::Info); + + let args = Args::parse(); + + // Configuration + let local_port = ASSIGNED_BY_OS; + let local_bind_to = format!("0.0.0.0:{local_port}"); + let transaction_id = RANDOM_TRANSACTION_ID; + + // Bind to local port + debug!("Binding to: {local_bind_to}"); + let udp_client = UdpClient::bind(&local_bind_to).await; + let bound_to = udp_client.socket.local_addr().context("binding local address")?; + debug!("Bound to: {bound_to}"); + + let transaction_id = TransactionId(transaction_id); + + let response = match args.command { + Command::Announce { + tracker_socket_addr, + info_hash, + } => { + let (connection_id, udp_tracker_client) = connect(&tracker_socket_addr, udp_client, transaction_id).await; + + send_announce_request( + connection_id, + transaction_id, + info_hash, + Port(bound_to.port()), + &udp_tracker_client, + ) + .await + } + Command::Scrape { + tracker_socket_addr, + info_hashes, + } => { + let (connection_id, udp_tracker_client) = connect(&tracker_socket_addr, udp_client, transaction_id).await; + send_scrape_request(connection_id, transaction_id, info_hashes, &udp_tracker_client).await + } + }; + + match response { + AnnounceIpv4(announce) => { + let json = json!({ + "transaction_id": announce.transaction_id.0, + "announce_interval": announce.announce_interval.0, + "leechers": announce.leechers.0, + "seeders": announce.seeders.0, + "peers": announce.peers.iter().map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)).collect::>(), + }); + let pretty_json = serde_json::to_string_pretty(&json).context("announce IPv4 response JSON serialization")?; + println!("{pretty_json}"); + } + AnnounceIpv6(announce) => { + let json = json!({ + "transaction_id": announce.transaction_id.0, + "announce_interval": announce.announce_interval.0, + "leechers": announce.leechers.0, + "seeders": announce.seeders.0, + "peers6": announce.peers.iter().map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)).collect::>(), + }); + let pretty_json = serde_json::to_string_pretty(&json).context("announce IPv6 response JSON serialization")?; + println!("{pretty_json}"); + } + Scrape(scrape) => { + let json = json!({ + "transaction_id": scrape.transaction_id.0, + "torrent_stats": scrape.torrent_stats.iter().map(|torrent_scrape_statistics| json!({ + "seeders": torrent_scrape_statistics.seeders.0, + "completed": torrent_scrape_statistics.completed.0, + "leechers": torrent_scrape_statistics.leechers.0, + })).collect::>(), + }); + let pretty_json = serde_json::to_string_pretty(&json).context("scrape response JSON serialization")?; + println!("{pretty_json}"); + } + _ => println!("{response:#?}"), // todo: serialize to JSON all responses. + }; + + Ok(()) +} + +fn setup_logging(level: LevelFilter) { + if let Err(_err) = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} [{}][{}] {}", + chrono::Local::now().format("%+"), + record.target(), + record.level(), + message + )); + }) + .level(level) + .chain(std::io::stdout()) + .apply() + { + panic!("Failed to initialize logging.") + } + + debug!("logging initialized."); +} + +fn parse_socket_addr(tracker_socket_addr_str: &str) -> anyhow::Result { + debug!("Tracker socket address: {tracker_socket_addr_str:#?}"); + + // Check if the address is a valid URL. If so, extract the host and port. + let resolved_addr = if let Ok(url) = Url::parse(tracker_socket_addr_str) { + debug!("Tracker socket address URL: {url:?}"); + + let host = url + .host_str() + .with_context(|| format!("invalid host in URL: `{tracker_socket_addr_str}`"))? + .to_owned(); + + let port = url + .port() + .with_context(|| format!("port not found in URL: `{tracker_socket_addr_str}`"))? + .to_owned(); + + (host, port) + } else { + // If not a URL, assume it's a host:port pair. + + let parts: Vec<&str> = tracker_socket_addr_str.split(':').collect(); + + if parts.len() != 2 { + return Err(anyhow::anyhow!( + "invalid address format: `{}`. Expected format is host:port", + tracker_socket_addr_str + )); + } + + let host = parts[0].to_owned(); + + let port = parts[1] + .parse::() + .with_context(|| format!("invalid port: `{}`", parts[1]))? + .to_owned(); + + (host, port) + }; + + debug!("Resolved address: {resolved_addr:#?}"); + + // Perform DNS resolution. + let socket_addrs: Vec<_> = resolved_addr.to_socket_addrs()?.collect(); + if socket_addrs.is_empty() { + Err(anyhow::anyhow!("DNS resolution failed for `{}`", tracker_socket_addr_str)) + } else { + Ok(socket_addrs[0]) + } +} + +fn parse_info_hash(info_hash_str: &str) -> anyhow::Result { + TorrustInfoHash::from_str(info_hash_str) + .map_err(|e| anyhow::Error::msg(format!("failed to parse info-hash `{info_hash_str}`: {e:?}"))) +} + +async fn connect( + tracker_socket_addr: &SocketAddr, + udp_client: UdpClient, + transaction_id: TransactionId, +) -> (ConnectionId, UdpTrackerClient) { + debug!("Connecting to tracker: udp://{tracker_socket_addr}"); + + udp_client.connect(&tracker_socket_addr.to_string()).await; + + let udp_tracker_client = UdpTrackerClient { udp_client }; + + let connection_id = send_connection_request(transaction_id, &udp_tracker_client).await; + + (connection_id, udp_tracker_client) +} + +async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrackerClient) -> ConnectionId { + debug!("Sending connection request with transaction id: {transaction_id:#?}"); + + let connect_request = ConnectRequest { transaction_id }; + + client.send(connect_request.into()).await; + + let response = client.receive().await; + + debug!("connection request response:\n{response:#?}"); + + match response { + Response::Connect(connect_response) => connect_response.connection_id, + _ => panic!("error connecting to udp server. Unexpected response"), + } +} + +async fn send_announce_request( + connection_id: ConnectionId, + transaction_id: TransactionId, + info_hash: TorrustInfoHash, + port: Port, + client: &UdpTrackerClient, +) -> Response { + debug!("Sending announce request with transaction id: {transaction_id:#?}"); + + let announce_request = AnnounceRequest { + connection_id, + transaction_id, + info_hash: InfoHash(info_hash.bytes()), + peer_id: PeerId(*b"-qB00000000000000001"), + bytes_downloaded: NumberOfBytes(0i64), + bytes_uploaded: NumberOfBytes(0i64), + bytes_left: NumberOfBytes(0i64), + event: AnnounceEvent::Started, + ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)), + key: PeerKey(0u32), + peers_wanted: NumberOfPeers(1i32), + port, + }; + + client.send(announce_request.into()).await; + + let response = client.receive().await; + + debug!("announce request response:\n{response:#?}"); + + response +} + +async fn send_scrape_request( + connection_id: ConnectionId, + transaction_id: TransactionId, + info_hashes: Vec, + client: &UdpTrackerClient, +) -> Response { + debug!("Sending scrape request with transaction id: {transaction_id:#?}"); + + let scrape_request = ScrapeRequest { + connection_id, + transaction_id, + info_hashes: info_hashes + .iter() + .map(|torrust_info_hash| InfoHash(torrust_info_hash.bytes())) + .collect(), + }; + + client.send(scrape_request.into()).await; + + let response = client.receive().await; + + debug!("scrape request response:\n{response:#?}"); + + response +} diff --git a/src/console/clients/udp/mod.rs b/src/console/clients/udp/mod.rs new file mode 100644 index 000000000..309be6287 --- /dev/null +++ b/src/console/clients/udp/mod.rs @@ -0,0 +1 @@ +pub mod app;