Skip to content

Commit

Permalink
feat: [#654] UDP tracker client: scrape
Browse files Browse the repository at this point in the history
```text
cargo run --bin udp_tracker_client scrape 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
cargo run --bin udp_tracker_client scrape udp://localhost:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
```

Scrape response:

```json
{
  "transaction_id": -888840697,
  "torrent_stats": [
    {
      "completed": 0,
      "leechers": 0,
      "seeders": 0
    },
    {
      "completed": 0,
      "leechers": 0,
      "seeders": 0
   }
 ]
}
```
  • Loading branch information
josecelano committed Jan 29, 2024
1 parent 1b34d93 commit f4e9bda
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 22 deletions.
1 change: 1 addition & 0 deletions cSpell.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"words": [
"Addrs",
"adduser",
"alekitto",
"appuser",
Expand Down
189 changes: 167 additions & 22 deletions src/bin/udp_tracker_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! Announce request:
//!
//! ```text
//! cargo run --bin udp_tracker_client 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
//! cargo run --bin udp_tracker_client announce 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
//! ```
//!
//! Announce response:
Expand All @@ -20,22 +20,58 @@
//! "123.123.123.123:51289"
//! ],
//! }
/// ````
use std::net::{Ipv4Addr, SocketAddr};
//! ```
//!
//! Scrape request:
//!
//! ```text
//! cargo run --bin udp_tracker_client scrape 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
//! ```
//!
//! Scrape response:
//!
//! ```json
//! {
//! "transaction_id": -888840697,
//! "torrent_stats": [
//! {
//! "completed": 0,
//! "leechers": 0,
//! "seeders": 0
//! },
//! {
//! "completed": 0,
//! "leechers": 0,
//! "seeders": 0
//! }
//! ]
//! }
//! ```
//!
//! You can use an URL with instead of the socket address. For example:
//!
//! ```text
//! cargo run --bin udp_tracker_client scrape udp://localhost:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
//! cargo run --bin udp_tracker_client scrape udp://localhost:6969/scrape 9c38422213e30bff212b30c360d26f9a02136422 | jq
//! ```
//!
//! The protocol (`udp://`) in the URL is mandatory. The path (`\scrape`) is optional. It always uses `\scrape`.
use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs};
use std::str::FromStr;

use anyhow::Context;
use aquatic_udp_protocol::common::InfoHash;
use aquatic_udp_protocol::Response::{AnnounceIpv4, AnnounceIpv6};
use aquatic_udp_protocol::Response::{AnnounceIpv4, AnnounceIpv6, Scrape};
use aquatic_udp_protocol::{
AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Response,
TransactionId,
ScrapeRequest, TransactionId,
};
use clap::{Parser, Subcommand};
use log::{debug, LevelFilter};
use serde_json::json;
use torrust_tracker::shared::bit_torrent::info_hash::InfoHash as TorrustInfoHash;
use torrust_tracker::shared::bit_torrent::tracker::udp::client::{UdpClient, UdpTrackerClient};
use url::Url;

const ASSIGNED_BY_OS: i32 = 0;
const RANDOM_TRANSACTION_ID: i32 = -888_840_697;
Expand All @@ -55,6 +91,12 @@ enum Command {
#[arg(value_parser = parse_info_hash)]
info_hash: TorrustInfoHash,
},
Scrape {
#[arg(value_parser = parse_socket_addr)]
tracker_socket_addr: SocketAddr,
#[arg(value_parser = parse_info_hash, num_args = 1..=74, value_delimiter = ' ')]
info_hashes: Vec<TorrustInfoHash>,
},
}

#[tokio::main]
Expand All @@ -65,29 +107,23 @@ async fn main() -> anyhow::Result<()> {

// Configuration
let local_port = ASSIGNED_BY_OS;
let local_bind_to = format!("0.0.0.0:{local_port}");
let transaction_id = RANDOM_TRANSACTION_ID;
let bind_to = format!("0.0.0.0:{local_port}");

// Bind to local port
debug!("Binding to: {bind_to}");
let udp_client = UdpClient::bind(&bind_to).await;
debug!("Binding to: {local_bind_to}");
let udp_client = UdpClient::bind(&local_bind_to).await;
let bound_to = udp_client.socket.local_addr().unwrap();
debug!("Bound to: {bound_to}");

let transaction_id = TransactionId(transaction_id);

let response = match args.command {
Command::Announce {
tracker_socket_addr,
info_hash,
} => {
debug!("Connecting to remote: udp://{tracker_socket_addr}");

udp_client.connect(&tracker_socket_addr.to_string()).await;

let udp_tracker_client = UdpTrackerClient { udp_client };

let transaction_id = TransactionId(transaction_id);

let connection_id = send_connection_request(transaction_id, &udp_tracker_client).await;
let (connection_id, udp_tracker_client) = connect(&tracker_socket_addr, udp_client, transaction_id).await;

send_announce_request(
connection_id,
Expand All @@ -98,6 +134,13 @@ async fn main() -> anyhow::Result<()> {
)
.await
}
Command::Scrape {
tracker_socket_addr,
info_hashes,
} => {
let (connection_id, udp_tracker_client) = connect(&tracker_socket_addr, udp_client, transaction_id).await;
send_scrape_request(connection_id, transaction_id, info_hashes, &udp_tracker_client).await
}
};

match response {
Expand All @@ -123,7 +166,19 @@ async fn main() -> anyhow::Result<()> {
let pretty_json = serde_json::to_string_pretty(&json).unwrap();
println!("{pretty_json}");
}
_ => println!("{response:#?}"),
Scrape(scrape) => {
let json = json!({
"transaction_id": scrape.transaction_id.0,
"torrent_stats": scrape.torrent_stats.iter().map(|torrent_scrape_statistics| json!({
"seeders": torrent_scrape_statistics.seeders.0,
"completed": torrent_scrape_statistics.completed.0,
"leechers": torrent_scrape_statistics.leechers.0,
})).collect::<Vec<_>>(),
});
let pretty_json = serde_json::to_string_pretty(&json).unwrap();
println!("{pretty_json}");
}
_ => println!("{response:#?}"), // todo: serialize to JSON all responses.
}

Ok(())
Expand All @@ -150,12 +205,76 @@ fn setup_logging(level: LevelFilter) {
debug!("logging initialized.");
}

fn parse_socket_addr(s: &str) -> anyhow::Result<SocketAddr> {
s.parse().with_context(|| format!("failed to parse socket address: `{s}`"))
fn parse_socket_addr(tracker_socket_addr_str: &str) -> anyhow::Result<SocketAddr> {
debug!("Tracker socket address: {tracker_socket_addr_str:#?}");

// Check if the address is a valid URL. If so, extract the host and port.
let resolved_addr = if let Ok(url) = Url::parse(tracker_socket_addr_str) {
debug!("Tracker socket address URL: {url:?}");

let host = url
.host_str()
.with_context(|| format!("invalid host in URL: `{tracker_socket_addr_str}`"))?
.to_owned();

let port = url
.port()
.with_context(|| format!("port not found in URL: `{tracker_socket_addr_str}`"))?
.to_owned();

(host, port)
} else {
// If not a URL, assume it's a host:port pair.

let parts: Vec<&str> = tracker_socket_addr_str.split(':').collect();

if parts.len() != 2 {
return Err(anyhow::anyhow!(
"invalid address format: `{}`. Expected format is host:port",
tracker_socket_addr_str
));
}

let host = parts[0].to_owned();

let port = parts[1]
.parse::<u16>()
.with_context(|| format!("invalid port: `{}`", parts[1]))?
.to_owned();

(host, port)
};

debug!("Resolved address: {resolved_addr:#?}");

// Perform DNS resolution.
let socket_addrs: Vec<_> = resolved_addr.to_socket_addrs()?.collect();
if socket_addrs.is_empty() {
Err(anyhow::anyhow!("DNS resolution failed for `{}`", tracker_socket_addr_str))
} else {
Ok(socket_addrs[0])
}
}

fn parse_info_hash(info_hash_str: &str) -> anyhow::Result<TorrustInfoHash> {
TorrustInfoHash::from_str(info_hash_str)
.map_err(|e| anyhow::Error::msg(format!("failed to parse info-hash `{info_hash_str}`: {e:?}")))
}

fn parse_info_hash(s: &str) -> anyhow::Result<TorrustInfoHash> {
TorrustInfoHash::from_str(s).map_err(|e| anyhow::Error::msg(format!("failed to parse info-hash `{s}`: {e:?}")))
async fn connect(
tracker_socket_addr: &SocketAddr,
udp_client: UdpClient,
transaction_id: TransactionId,
) -> (ConnectionId, UdpTrackerClient) {
debug!("Connecting to tracker: udp://{tracker_socket_addr}");

udp_client.connect(&tracker_socket_addr.to_string()).await;

let udp_tracker_client = UdpTrackerClient { udp_client };

let connection_id = send_connection_request(transaction_id, &udp_tracker_client).await;

(connection_id, udp_tracker_client)
}

async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrackerClient) -> ConnectionId {
Expand Down Expand Up @@ -207,3 +326,29 @@ async fn send_announce_request(

response
}

async fn send_scrape_request(
connection_id: ConnectionId,
transaction_id: TransactionId,
info_hashes: Vec<TorrustInfoHash>,
client: &UdpTrackerClient,
) -> Response {
debug!("Sending scrape request with transaction id: {transaction_id:#?}");

let scrape_request = ScrapeRequest {
connection_id,
transaction_id,
info_hashes: info_hashes
.iter()
.map(|torrust_info_hash| InfoHash(torrust_info_hash.bytes()))
.collect(),
};

client.send(scrape_request.into()).await;

let response = client.receive().await;

debug!("scrape request response:\n{response:#?}");

response
}

0 comments on commit f4e9bda

Please sign in to comment.