Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP Tracker Client: add scrape request #651

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
}
}

#[derive(Debug)]

Check warning on line 19 in src/shared/bit_torrent/tracker/http/client/requests/scrape.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L19 was not covered by tests
#[allow(dead_code)]
pub struct ConversionError(String);

Check warning on line 21 in src/shared/bit_torrent/tracker/http/client/requests/scrape.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L21 was not covered by tests

impl fmt::Display for ConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Invalid infohash: {}", self.0)
}

Check warning on line 26 in src/shared/bit_torrent/tracker/http/client/requests/scrape.rs

View check run for this annotation

Codecov / codecov/patch

src/shared/bit_torrent/tracker/http/client/requests/scrape.rs#L24-L26

Added lines #L24 - L26 were not covered by tests
}

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();

Check warning on line 35 in src/shared/bit_torrent/tracker/http/client/requests/scrape.rs

View check run for this annotation

Codecov / codecov/patch

src/shared/bit_torrent/tracker/http/client/requests/scrape.rs#L34-L35

Added lines #L34 - L35 were not covered by tests

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);

Check warning on line 39 in src/shared/bit_torrent/tracker/http/client/requests/scrape.rs

View check run for this annotation

Codecov / codecov/patch

src/shared/bit_torrent/tracker/http/client/requests/scrape.rs#L37-L39

Added lines #L37 - L39 were not covered by tests
}

Ok(Self {
info_hash: validated_info_hashes,
})
}

Check warning on line 45 in src/shared/bit_torrent/tracker/http/client/requests/scrape.rs

View check run for this annotation

Codecov / codecov/patch

src/shared/bit_torrent/tracker/http/client/requests/scrape.rs#L42-L45

Added lines #L42 - L45 were not covered by tests
}

/// 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)]

Check warning on line 11 in src/shared/bit_torrent/tracker/http/client/responses/scrape.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L11 was not covered by tests
pub struct Response {
pub files: HashMap<ByteArray20, File>,
}
Expand Down Expand Up @@ -60,6 +62,31 @@
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
}

Check warning on line 88 in src/shared/bit_torrent/tracker/http/client/responses/scrape.rs

View check run for this annotation

Codecov / codecov/patch

src/shared/bit_torrent/tracker/http/client/responses/scrape.rs#L82-L88

Added lines #L82 - L88 were not covered by tests

#[derive(Default)]
pub struct ResponseBuilder {
response: Response,
Expand Down
Loading