diff --git a/src/bin/http_tracker_client.rs b/src/bin/http_tracker_client.rs index 29127cdf..5e6db722 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 e2563b8e..2aecc155 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 ae06841e..ee301ee7 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,