Skip to content

Commit

Permalink
Merge #651: HTTP Tracker Client: add scrape request
Browse files Browse the repository at this point in the history
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
  • Loading branch information
josecelano committed Jan 29, 2024
2 parents 0f573b6 + 0624bf2 commit c526cc1
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 23 deletions.
20 changes: 14 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ uuid = { version = "1", features = ["v4"] }
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"] }
Expand Down
89 changes: 75 additions & 14 deletions src/bin/http_tracker_client.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,64 @@
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 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::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)]
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<String> },
}

#[tokio::main]
async fn main() {
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
eprintln!("Error: invalid number of arguments!");
eprintln!("Usage: cargo run --bin http_tracker_client <HTTP_TRACKER_URL> <INFO_HASH>");
eprintln!("Example: cargo run --bin http_tracker_client https://tracker.torrust-demo.com 9c38422213e30bff212b30c360d26f9a02136422");
std::process::exit(1);
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(())
}

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) -> 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())
Expand All @@ -27,9 +67,30 @@ async fn main() {
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).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")?;

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

print!("{json}");
Ok(())
}
33 changes: 32 additions & 1 deletion src/shared/bit_torrent/tracker/http/client/requests/scrape.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use std::fmt;
use std::convert::TryFrom;
use std::error::Error;
use std::fmt::{self};
use std::str::FromStr;

use crate::shared::bit_torrent::info_hash::InfoHash;
Expand All @@ -14,6 +16,35 @@ 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;

fn try_from(info_hashes: &[String]) -> Result<Self, Self::Error> {
let mut validated_info_hashes: Vec<ByteArray20> = 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:
///
/// <https://www.bittorrent.org/beps/bep_0048.html>
Expand Down
31 changes: 29 additions & 2 deletions src/shared/bit_torrent/tracker/http/client/responses/scrape.rs
Original file line number Diff line number Diff line change
@@ -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<ByteArray20, File>,
}
Expand Down Expand Up @@ -60,6 +62,31 @@ struct DeserializedResponse {
pub files: Value,
}

// Custom serialization for Response
impl Serialize for Response {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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,
Expand Down

0 comments on commit c526cc1

Please sign in to comment.