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

UDP Tracker Client: add scrape request #655

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
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
296 changes: 248 additions & 48 deletions src/bin/udp_tracker_client.rs
Original file line number Diff line number Diff line change
@@ -1,58 +1,187 @@
use std::env;
use std::net::{Ipv4Addr, SocketAddr};
//! UDP Tracker client:
//!
//! Examples:
//!
//! Announce request:
//!
//! ```text
//! cargo run --bin udp_tracker_client announce 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
//! ```
//!
//! Announce response:
//!
//! ```json
//! {
//! "transaction_id": -888840697
//! "announce_interval": 120,
//! "leechers": 0,
//! "seeders": 1,
//! "peers": [
//! "123.123.123.123:51289"
//! ],
//! }
//! ```
//!
//! 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, 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;

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[command(subcommand)]
command: Command,
}

#[derive(Subcommand, Debug)]
enum Command {
Announce {
#[arg(value_parser = parse_socket_addr)]
tracker_socket_addr: SocketAddr,
#[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]
async fn main() {
async fn main() -> anyhow::Result<()> {
setup_logging(LevelFilter::Info);

let (remote_socket_addr, info_hash) = parse_arguments();
let args = Args::parse();

// 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}");

// Connect to remote socket

debug!("Connecting to remote: udp://{remote_socket_addr}");
udp_client.connect(&remote_socket_addr).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 response = match args.command {
Command::Announce {
tracker_socket_addr,
info_hash,
} => {
let (connection_id, udp_tracker_client) = connect(&tracker_socket_addr, udp_client, transaction_id).await;

send_announce_request(
connection_id,
transaction_id,
info_hash,
Port(bound_to.port()),
&udp_tracker_client,
)
.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
}
};

let response = send_announce_request(
connection_id,
transaction_id,
info_hash,
Port(bound_to.port()),
&udp_tracker_client,
)
.await;
match response {
AnnounceIpv4(announce) => {
let json = json!({
"transaction_id": announce.transaction_id.0,
"announce_interval": announce.announce_interval.0,
"leechers": announce.leechers.0,
"seeders": announce.seeders.0,
"peers": announce.peers.iter().map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)).collect::<Vec<_>>(),
});
let pretty_json = serde_json::to_string_pretty(&json).unwrap();
println!("{pretty_json}");
}
AnnounceIpv6(announce) => {
let json = json!({
"transaction_id": announce.transaction_id.0,
"announce_interval": announce.announce_interval.0,
"leechers": announce.leechers.0,
"seeders": announce.seeders.0,
"peers6": announce.peers.iter().map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)).collect::<Vec<_>>(),
});
let pretty_json = serde_json::to_string_pretty(&json).unwrap();
println!("{pretty_json}");
}
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.
}

println!("{response:#?}");
Ok(())
}

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

fn parse_arguments() -> (String, TorrustInfoHash) {
let args: Vec<String> = env::args().collect();
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:#?}");

if args.len() != 3 {
eprintln!("Error: invalid number of arguments!");
eprintln!("Usage: cargo run --bin udp_tracker_client <UDP_TRACKER_SOCKET_ADDRESS> <INFO_HASH>");
eprintln!("Example: cargo run --bin udp_tracker_client 144.126.245.19:6969 9c38422213e30bff212b30c360d26f9a02136422");
std::process::exit(1);
// 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])
}
}

let remote_socket_addr = &args[1];
let _valid_socket_addr = remote_socket_addr.parse::<SocketAddr>().unwrap_or_else(|_| {
panic!(
"Invalid argument: `{}`. Argument 1 should be a valid socket address. For example: `144.126.245.19:6969`.",
args[1]
)
});
let info_hash = TorrustInfoHash::from_str(&args[2]).unwrap_or_else(|_| {
panic!(
"Invalid argument: `{}`. Argument 2 should be a valid infohash. For example: `9c38422213e30bff212b30c360d26f9a02136422`.",
args[2]
)
});

(remote_socket_addr.to_string(), info_hash)
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:?}")))
}

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 @@ -152,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
}
Loading