Skip to content

Commit c526cc1

Browse files
committed
Merge #651: HTTP Tracker Client: add scrape request
0624bf2 refactor: [#649] use anyhow to handle errors (Jose Celano) 271bfa8 feat: [#649] add cargo dep: anyhow (Jose Celano) 415ca1c feat: [#649] scrape req for the HTTP tracker client (Jose Celano) b05e2f5 refactor: [#649] use clap in HTTP tracker client (Jose Celano) f439015 feat: [#649] add cargo dependency: clap (Jose Celano) Pull request description: Usage: ```console cargo run --bin http_tracker_client scrape https://tracker.torrust-demo.com 9c38422213e30bff212b30c360d26f9a02136422 | jq ``` Response: ```json { "9c38422213e30bff212b30c360d26f9a02136422": { "complete": 0, "downloaded": 0, "incomplete": 1 } } ``` ACKs for top commit: josecelano: ACK 0624bf2 Tree-SHA512: a801824a95e9bae480df452792861ba6e1cac34ade1b53d3ba921865a352bf879a84a5b50ec482d0db0c6f20d4af8a41cc3da7acaa2a403b188287ebc7e97913
2 parents 0f573b6 + 0624bf2 commit c526cc1

File tree

5 files changed

+152
-23
lines changed

5 files changed

+152
-23
lines changed

Cargo.lock

+14-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ uuid = { version = "1", features = ["v4"] }
7272
colored = "2.1.0"
7373
url = "2.5.0"
7474
tempfile = "3.9.0"
75+
clap = { version = "4.4.18", features = ["derive"]}
76+
anyhow = "1.0.79"
7577

7678
[dev-dependencies]
7779
criterion = { version = "0.5.1", features = ["async_tokio"] }

src/bin/http_tracker_client.rs

+75-14
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,64 @@
1-
use std::env;
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+
//! ```
216
use std::str::FromStr;
317

18+
use anyhow::Context;
19+
use clap::{Parser, Subcommand};
420
use reqwest::Url;
521
use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
622
use torrust_tracker::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder;
723
use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::announce::Announce;
8-
use torrust_tracker::shared::bit_torrent::tracker::http::client::Client;
24+
use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::scrape;
25+
use torrust_tracker::shared::bit_torrent::tracker::http::client::{requests, Client};
26+
27+
#[derive(Parser, Debug)]
28+
#[command(author, version, about, long_about = None)]
29+
struct Args {
30+
#[command(subcommand)]
31+
command: Command,
32+
}
33+
34+
#[derive(Subcommand, Debug)]
35+
enum Command {
36+
Announce { tracker_url: String, info_hash: String },
37+
Scrape { tracker_url: String, info_hashes: Vec<String> },
38+
}
939

1040
#[tokio::main]
11-
async fn main() {
12-
let args: Vec<String> = env::args().collect();
13-
if args.len() != 3 {
14-
eprintln!("Error: invalid number of arguments!");
15-
eprintln!("Usage: cargo run --bin http_tracker_client <HTTP_TRACKER_URL> <INFO_HASH>");
16-
eprintln!("Example: cargo run --bin http_tracker_client https://tracker.torrust-demo.com 9c38422213e30bff212b30c360d26f9a02136422");
17-
std::process::exit(1);
41+
async fn main() -> anyhow::Result<()> {
42+
let args = Args::parse();
43+
44+
match args.command {
45+
Command::Announce { tracker_url, info_hash } => {
46+
announce_command(tracker_url, info_hash).await?;
47+
}
48+
Command::Scrape {
49+
tracker_url,
50+
info_hashes,
51+
} => {
52+
scrape_command(&tracker_url, &info_hashes).await?;
53+
}
1854
}
55+
Ok(())
56+
}
1957

20-
let base_url = Url::parse(&args[1]).expect("arg 1 should be a valid HTTP tracker base URL");
21-
let info_hash = InfoHash::from_str(&args[2]).expect("arg 2 should be a valid infohash");
58+
async fn announce_command(tracker_url: String, info_hash: String) -> anyhow::Result<()> {
59+
let base_url = Url::parse(&tracker_url).context("failed to parse HTTP tracker base URL")?;
60+
let info_hash =
61+
InfoHash::from_str(&info_hash).expect("Invalid infohash. Example infohash: `9c38422213e30bff212b30c360d26f9a02136422`");
2262

2363
let response = Client::new(base_url)
2464
.announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query())
@@ -27,9 +67,30 @@ async fn main() {
2767
let body = response.bytes().await.unwrap();
2868

2969
let announce_response: Announce = serde_bencode::from_bytes(&body)
30-
.unwrap_or_else(|_| panic!("response body should be a valid announce response, got \"{:#?}\"", &body));
70+
.unwrap_or_else(|_| panic!("response body should be a valid announce response, got: \"{:#?}\"", &body));
71+
72+
let json = serde_json::to_string(&announce_response).context("failed to serialize scrape response into JSON")?;
73+
74+
println!("{json}");
75+
76+
Ok(())
77+
}
78+
79+
async fn scrape_command(tracker_url: &str, info_hashes: &[String]) -> anyhow::Result<()> {
80+
let base_url = Url::parse(tracker_url).context("failed to parse HTTP tracker base URL")?;
81+
82+
let query = requests::scrape::Query::try_from(info_hashes).context("failed to parse infohashes")?;
83+
84+
let response = Client::new(base_url).scrape(&query).await;
85+
86+
let body = response.bytes().await.unwrap();
87+
88+
let scrape_response = scrape::Response::try_from_bencoded(&body)
89+
.unwrap_or_else(|_| panic!("response body should be a valid scrape response, got: \"{:#?}\"", &body));
90+
91+
let json = serde_json::to_string(&scrape_response).context("failed to serialize scrape response into JSON")?;
3192

32-
let json = serde_json::to_string(&announce_response).expect("announce response should be a valid JSON");
93+
println!("{json}");
3394

34-
print!("{json}");
95+
Ok(())
3596
}

src/shared/bit_torrent/tracker/http/client/requests/scrape.rs

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use std::fmt;
1+
use std::convert::TryFrom;
2+
use std::error::Error;
3+
use std::fmt::{self};
24
use std::str::FromStr;
35

46
use crate::shared::bit_torrent::info_hash::InfoHash;
@@ -14,6 +16,35 @@ impl fmt::Display for Query {
1416
}
1517
}
1618

19+
#[derive(Debug)]
20+
#[allow(dead_code)]
21+
pub struct ConversionError(String);
22+
23+
impl fmt::Display for ConversionError {
24+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25+
write!(f, "Invalid infohash: {}", self.0)
26+
}
27+
}
28+
29+
impl Error for ConversionError {}
30+
31+
impl TryFrom<&[String]> for Query {
32+
type Error = ConversionError;
33+
34+
fn try_from(info_hashes: &[String]) -> Result<Self, Self::Error> {
35+
let mut validated_info_hashes: Vec<ByteArray20> = Vec::new();
36+
37+
for info_hash in info_hashes {
38+
let validated_info_hash = InfoHash::from_str(info_hash).map_err(|_| ConversionError(info_hash.clone()))?;
39+
validated_info_hashes.push(validated_info_hash.0);
40+
}
41+
42+
Ok(Self {
43+
info_hash: validated_info_hashes,
44+
})
45+
}
46+
}
47+
1748
/// HTTP Tracker Scrape Request:
1849
///
1950
/// <https://www.bittorrent.org/beps/bep_0048.html>

src/shared/bit_torrent/tracker/http/client/responses/scrape.rs

+29-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use std::collections::HashMap;
2+
use std::fmt::Write;
23
use std::str;
34

4-
use serde::{self, Deserialize, Serialize};
5+
use serde::ser::SerializeMap;
6+
use serde::{self, Deserialize, Serialize, Serializer};
57
use serde_bencode::value::Value;
68

79
use crate::shared::bit_torrent::tracker::http::{ByteArray20, InfoHash};
810

9-
#[derive(Debug, PartialEq, Default)]
11+
#[derive(Debug, PartialEq, Default, Deserialize)]
1012
pub struct Response {
1113
pub files: HashMap<ByteArray20, File>,
1214
}
@@ -60,6 +62,31 @@ struct DeserializedResponse {
6062
pub files: Value,
6163
}
6264

65+
// Custom serialization for Response
66+
impl Serialize for Response {
67+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
68+
where
69+
S: Serializer,
70+
{
71+
let mut map = serializer.serialize_map(Some(self.files.len()))?;
72+
for (key, value) in &self.files {
73+
// Convert ByteArray20 key to hex string
74+
let hex_key = byte_array_to_hex_string(key);
75+
map.serialize_entry(&hex_key, value)?;
76+
}
77+
map.end()
78+
}
79+
}
80+
81+
// Helper function to convert ByteArray20 to hex string
82+
fn byte_array_to_hex_string(byte_array: &ByteArray20) -> String {
83+
let mut hex_string = String::with_capacity(byte_array.len() * 2);
84+
for byte in byte_array {
85+
write!(hex_string, "{byte:02x}").expect("Writing to string should never fail");
86+
}
87+
hex_string
88+
}
89+
6390
#[derive(Default)]
6491
pub struct ResponseBuilder {
6592
response: Response,

0 commit comments

Comments
 (0)