From f4390155ccb9fe8fa91db9394c5ede0ff747e4f3 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 29 Jan 2024 09:47:02 +0000 Subject: [PATCH 1/5] feat: [#649] add cargo dependency: clap For console commands. --- Cargo.lock | 13 +++++++------ Cargo.toml | 1 + 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab270d0cc..6c49938de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,9 +93,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" dependencies = [ "anstyle", "anstyle-parse", @@ -574,9 +574,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.12" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfab8ba68f3668e89f6ff60f5b205cea56aa7b769451a59f34b8682f51c056d" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" dependencies = [ "clap_builder", "clap_derive", @@ -584,9 +584,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.12" +version = "4.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" dependencies = [ "anstream", "anstyle", @@ -3437,6 +3437,7 @@ dependencies = [ "axum-server", "binascii", "chrono", + "clap", "colored", "config", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 3a11786f5..4b60b8051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ uuid = { version = "1", features = ["v4"] } colored = "2.1.0" url = "2.5.0" tempfile = "3.9.0" +clap = { version = "4.4.18", features = ["derive"]} [dev-dependencies] criterion = { version = "0.5.1", features = ["async_tokio"] } From b05e2f5cfead54bcab1b5d5fb3e7e8e223c254c1 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 29 Jan 2024 09:48:04 +0000 Subject: [PATCH 2/5] refactor: [#649] use clap in HTTP tracker client An added scaffolding for scrape command. --- src/bin/http_tracker_client.rs | 62 ++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/src/bin/http_tracker_client.rs b/src/bin/http_tracker_client.rs index 1f1154fa5..29127cdf4 100644 --- a/src/bin/http_tracker_client.rs +++ b/src/bin/http_tracker_client.rs @@ -1,24 +1,60 @@ -use std::env; +//! 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 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::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 }, +} + #[tokio::main] async fn main() { - let args: Vec = env::args().collect(); - if args.len() != 3 { - eprintln!("Error: invalid number of arguments!"); - eprintln!("Usage: cargo run --bin http_tracker_client "); - eprintln!("Example: cargo run --bin http_tracker_client https://tracker.torrust-demo.com 9c38422213e30bff212b30c360d26f9a02136422"); - std::process::exit(1); + 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); + } } +} - let base_url = Url::parse(&args[1]).expect("arg 1 should be a valid HTTP tracker base URL"); - let info_hash = InfoHash::from_str(&args[2]).expect("arg 2 should be a valid infohash"); +async fn announce_command(tracker_url: String, info_hash: String) { + let base_url = Url::parse(&tracker_url).expect("Invalid HTTP tracker base URL"); + let info_hash = InfoHash::from_str(&info_hash).expect("Invalid infohash"); let response = Client::new(base_url) .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query()) @@ -31,5 +67,11 @@ async fn main() { let json = serde_json::to_string(&announce_response).expect("announce response should be a valid JSON"); - print!("{json}"); + println!("{json}"); +} + +fn scrape_command(tracker_url: &str, info_hashes: &[String]) { + println!("URL: {tracker_url}"); + println!("Infohashes: {info_hashes:#?}"); + todo!(); } From 415ca1c371cdff314d7998e9669c1deffd384a28 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 29 Jan 2024 10:28:49 +0000 Subject: [PATCH 3/5] feat: [#649] scrape req for the HTTP tracker client ```console cargo run --bin http_tracker_client scrape http://127.0.0.1:7070 9c38422213e30bff212b30c360d26f9a02136422 9c38422213e30bff212b30c360d26f9a02136423 | jq ``` ```json { "9c38422213e30bff212b30c360d26f9a02136422": { "complete": 0, "downloaded": 0, "incomplete": 0 }, "9c38422213e30bff212b30c360d26f9a02136423": { "complete": 0, "downloaded": 0, "incomplete": 0 } } ``` --- src/bin/http_tracker_client.rs | 30 +++++++++++++----- .../tracker/http/client/requests/scrape.rs | 23 +++++++++++++- .../tracker/http/client/responses/scrape.rs | 31 +++++++++++++++++-- 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/bin/http_tracker_client.rs b/src/bin/http_tracker_client.rs index 29127cdf4..5e6db722c 100644 --- a/src/bin/http_tracker_client.rs +++ b/src/bin/http_tracker_client.rs @@ -20,7 +20,8 @@ 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::Client; +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)] @@ -47,14 +48,15 @@ async fn main() { tracker_url, info_hashes, } => { - scrape_command(&tracker_url, &info_hashes); + scrape_command(&tracker_url, &info_hashes).await; } } } async fn announce_command(tracker_url: String, info_hash: String) { let base_url = Url::parse(&tracker_url).expect("Invalid HTTP tracker base URL"); - let info_hash = InfoHash::from_str(&info_hash).expect("Invalid infohash"); + 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()) @@ -63,15 +65,27 @@ async fn announce_command(tracker_url: String, info_hash: String) { 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)); + .unwrap_or_else(|_| panic!("response body should be a valid announce response, got: \"{:#?}\"", &body)); let json = serde_json::to_string(&announce_response).expect("announce response should be a valid JSON"); println!("{json}"); } -fn scrape_command(tracker_url: &str, info_hashes: &[String]) { - println!("URL: {tracker_url}"); - println!("Infohashes: {info_hashes:#?}"); - todo!(); +async fn scrape_command(tracker_url: &str, info_hashes: &[String]) { + let base_url = Url::parse(tracker_url).expect("Invalid HTTP tracker base URL"); + + let query = requests::scrape::Query::try_from(info_hashes) + .expect("All infohashes should be valid. Example infohash: `9c38422213e30bff212b30c360d26f9a02136422`"); + + 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).expect("scrape response should be a valid JSON"); + + println!("{json}"); } diff --git a/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs b/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs index e2563b8ed..2aecc1550 100644 --- a/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs +++ b/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs @@ -1,4 +1,5 @@ -use std::fmt; +use std::convert::TryFrom; +use std::fmt::{self}; use std::str::FromStr; use crate::shared::bit_torrent::info_hash::InfoHash; @@ -14,6 +15,26 @@ impl fmt::Display for Query { } } +#[derive(Debug)] +pub struct ConversionError(String); + +impl TryFrom<&[String]> for Query { + type Error = ConversionError; + + fn try_from(info_hashes: &[String]) -> Result { + let mut validated_info_hashes: Vec = Vec::new(); + + for info_hash in info_hashes { + let validated_info_hash = InfoHash::from_str(info_hash).map_err(|_| ConversionError(info_hash.clone()))?; + validated_info_hashes.push(validated_info_hash.0); + } + + Ok(Self { + info_hash: validated_info_hashes, + }) + } +} + /// HTTP Tracker Scrape Request: /// /// diff --git a/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs b/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs index ae06841e4..ee301ee7a 100644 --- a/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs +++ b/src/shared/bit_torrent/tracker/http/client/responses/scrape.rs @@ -1,12 +1,14 @@ use std::collections::HashMap; +use std::fmt::Write; use std::str; -use serde::{self, Deserialize, Serialize}; +use serde::ser::SerializeMap; +use serde::{self, Deserialize, Serialize, Serializer}; use serde_bencode::value::Value; use crate::shared::bit_torrent::tracker::http::{ByteArray20, InfoHash}; -#[derive(Debug, PartialEq, Default)] +#[derive(Debug, PartialEq, Default, Deserialize)] pub struct Response { pub files: HashMap, } @@ -60,6 +62,31 @@ struct DeserializedResponse { pub files: Value, } +// Custom serialization for Response +impl Serialize for Response { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(self.files.len()))?; + for (key, value) in &self.files { + // Convert ByteArray20 key to hex string + let hex_key = byte_array_to_hex_string(key); + map.serialize_entry(&hex_key, value)?; + } + map.end() + } +} + +// Helper function to convert ByteArray20 to hex string +fn byte_array_to_hex_string(byte_array: &ByteArray20) -> String { + let mut hex_string = String::with_capacity(byte_array.len() * 2); + for byte in byte_array { + write!(hex_string, "{byte:02x}").expect("Writing to string should never fail"); + } + hex_string +} + #[derive(Default)] pub struct ResponseBuilder { response: Response, From 271bfa853a06b53c88928667518ae56e75269f04 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 29 Jan 2024 11:04:10 +0000 Subject: [PATCH 4/5] feat: [#649] add cargo dep: anyhow To handle errors in console clients. --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + 2 files changed, 8 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 6c49938de..1af4d5b3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + [[package]] name = "aquatic_udp_protocol" version = "0.8.0" @@ -3430,6 +3436,7 @@ dependencies = [ name = "torrust-tracker" version = "3.0.0-alpha.12-develop" dependencies = [ + "anyhow", "aquatic_udp_protocol", "async-trait", "axum", diff --git a/Cargo.toml b/Cargo.toml index 4b60b8051..a512d90b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ colored = "2.1.0" url = "2.5.0" tempfile = "3.9.0" clap = { version = "4.4.18", features = ["derive"]} +anyhow = "1.0.79" [dev-dependencies] criterion = { version = "0.5.1", features = ["async_tokio"] } From 0624bf209cf995749c1027c773b15fcc6b113c83 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 29 Jan 2024 11:05:09 +0000 Subject: [PATCH 5/5] refactor: [#649] use anyhow to handle errors in the HTTP tracker client. --- src/bin/http_tracker_client.rs | 27 +++++++++++-------- .../tracker/http/client/requests/scrape.rs | 10 +++++++ 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/bin/http_tracker_client.rs b/src/bin/http_tracker_client.rs index 5e6db722c..4ca194803 100644 --- a/src/bin/http_tracker_client.rs +++ b/src/bin/http_tracker_client.rs @@ -15,6 +15,7 @@ //! ``` use std::str::FromStr; +use anyhow::Context; use clap::{Parser, Subcommand}; use reqwest::Url; use torrust_tracker::shared::bit_torrent::info_hash::InfoHash; @@ -37,24 +38,25 @@ enum Command { } #[tokio::main] -async fn 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; + announce_command(tracker_url, info_hash).await?; } Command::Scrape { tracker_url, info_hashes, } => { - scrape_command(&tracker_url, &info_hashes).await; + scrape_command(&tracker_url, &info_hashes).await?; } } + Ok(()) } -async fn announce_command(tracker_url: String, info_hash: String) { - let base_url = Url::parse(&tracker_url).expect("Invalid HTTP tracker base URL"); +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`"); @@ -67,16 +69,17 @@ async fn announce_command(tracker_url: String, info_hash: String) { 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).expect("announce response should be a valid JSON"); + 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]) { - let base_url = Url::parse(tracker_url).expect("Invalid HTTP tracker base URL"); +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) - .expect("All infohashes should be valid. Example infohash: `9c38422213e30bff212b30c360d26f9a02136422`"); + let query = requests::scrape::Query::try_from(info_hashes).context("failed to parse infohashes")?; let response = Client::new(base_url).scrape(&query).await; @@ -85,7 +88,9 @@ async fn scrape_command(tracker_url: &str, info_hashes: &[String]) { 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).expect("scrape response should be a valid JSON"); + let json = serde_json::to_string(&scrape_response).context("failed to serialize scrape response into JSON")?; println!("{json}"); + + Ok(()) } diff --git a/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs b/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs index 2aecc1550..771b3a45e 100644 --- a/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs +++ b/src/shared/bit_torrent/tracker/http/client/requests/scrape.rs @@ -1,4 +1,5 @@ use std::convert::TryFrom; +use std::error::Error; use std::fmt::{self}; use std::str::FromStr; @@ -16,8 +17,17 @@ impl fmt::Display for Query { } #[derive(Debug)] +#[allow(dead_code)] pub struct ConversionError(String); +impl fmt::Display for ConversionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Invalid infohash: {}", self.0) + } +} + +impl Error for ConversionError {} + impl TryFrom<&[String]> for Query { type Error = ConversionError;