Skip to content

Commit 470e608

Browse files
committed
feat: a simple HTTP tracker client command
You can execute it with: ``` cargo run --bin http_tracker_client https://tracker.torrust-demo.com 9c38422213e30bff212b30c360d26f9a02136422" ``` and the output should be something like: ```json{ "complete": 1, "incomplete": 1, "interval": 300, "min interval": 300, "peers": [ { "ip": "90.XX.XX.167", "peer id": [ 45, 66, 76, 50, 52, 54, 51, 54, 51, 45, 51, 70, 41, 46, 114, 46, 68, 100, 74, 69 ], "port": 59568 } ] } ```
1 parent 44e8076 commit 470e608

File tree

14 files changed

+935
-7
lines changed

14 files changed

+935
-7
lines changed

Cargo.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ rand = "0"
5454
reqwest = "0"
5555
serde = { version = "1", features = ["derive"] }
5656
serde_bencode = "0"
57+
serde_bytes = "0"
5758
serde_json = "1"
5859
serde_with = "3"
60+
serde_repr = "0"
5961
tdyne-peer-id = "1"
6062
tdyne-peer-id-registry = "0"
6163
thiserror = "1"
@@ -73,8 +75,6 @@ local-ip-address = "0"
7375
mockall = "0"
7476
once_cell = "1.18.0"
7577
reqwest = { version = "0", features = ["json"] }
76-
serde_bytes = "0"
77-
serde_repr = "0"
7878
serde_urlencoded = "0"
7979
torrust-tracker-test-helpers = { version = "3.0.0-alpha.12-develop", path = "packages/test-helpers" }
8080

share/default/config/tracker.development.sqlite3.toml

+5-5
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ remove_peerless_torrents = true
1313
tracker_usage_statistics = true
1414

1515
[[udp_trackers]]
16-
bind_address = "0.0.0.0:6969"
17-
enabled = false
16+
bind_address = "0.0.0.0:0"
17+
enabled = true
1818

1919
[[http_trackers]]
20-
bind_address = "0.0.0.0:7070"
21-
enabled = false
20+
bind_address = "0.0.0.0:0"
21+
enabled = true
2222
ssl_cert_path = ""
2323
ssl_enabled = false
2424
ssl_key_path = ""
2525

2626
[http_api]
27-
bind_address = "127.0.0.1:1212"
27+
bind_address = "127.0.0.1:0"
2828
enabled = true
2929
ssl_cert_path = ""
3030
ssl_enabled = false

src/bin/http_tracker_client.rs

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use std::env;
2+
use std::str::FromStr;
3+
4+
use reqwest::Url;
5+
use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
6+
use torrust_tracker::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder;
7+
use torrust_tracker::shared::bit_torrent::tracker::http::client::responses::announce::Announce;
8+
use torrust_tracker::shared::bit_torrent::tracker::http::client::Client;
9+
10+
#[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);
18+
}
19+
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");
22+
23+
let response = Client::new(base_url)
24+
.announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query())
25+
.await;
26+
27+
let body = response.bytes().await.unwrap();
28+
29+
let announce_response: Announce = serde_bencode::from_bytes(&body)
30+
.unwrap_or_else(|_| panic!("response body should be a valid announce response, got \"{:#?}\"", &body));
31+
32+
let json = serde_json::to_string(&announce_response).expect("announce response should be a valid JSON");
33+
34+
print!("{json}");
35+
}

src/shared/bit_torrent/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,5 @@
6969
//!Bencode & bdecode in your browser | <https://github.com/Chocobo1/bencode_online>
7070
pub mod common;
7171
pub mod info_hash;
72+
pub mod tracker;
7273
pub mod udp;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
pub mod requests;
2+
pub mod responses;
3+
4+
use std::net::IpAddr;
5+
6+
use requests::announce::{self, Query};
7+
use requests::scrape;
8+
use reqwest::{Client as ReqwestClient, Response, Url};
9+
10+
use crate::core::auth::Key;
11+
12+
/// HTTP Tracker Client
13+
pub struct Client {
14+
base_url: Url,
15+
reqwest: ReqwestClient,
16+
key: Option<Key>,
17+
}
18+
19+
/// URL components in this context:
20+
///
21+
/// ```text
22+
/// http://127.0.0.1:62304/announce/YZ....rJ?info_hash=%9C8B%22%13%E3%0B%FF%21%2B0%C3%60%D2o%9A%02%13d%22
23+
/// \_____________________/\_______________/ \__________________________________________________________/
24+
/// | | |
25+
/// base url path query
26+
/// ```
27+
impl Client {
28+
/// # Panics
29+
///
30+
/// This method fails if the client builder fails.
31+
#[must_use]
32+
pub fn new(base_url: Url) -> Self {
33+
Self {
34+
base_url,
35+
reqwest: reqwest::Client::builder().build().unwrap(),
36+
key: None,
37+
}
38+
}
39+
40+
/// Creates the new client binding it to an specific local address.
41+
///
42+
/// # Panics
43+
///
44+
/// This method fails if the client builder fails.
45+
#[must_use]
46+
pub fn bind(base_url: Url, local_address: IpAddr) -> Self {
47+
Self {
48+
base_url,
49+
reqwest: reqwest::Client::builder().local_address(local_address).build().unwrap(),
50+
key: None,
51+
}
52+
}
53+
54+
/// # Panics
55+
///
56+
/// This method fails if the client builder fails.
57+
#[must_use]
58+
pub fn authenticated(base_url: Url, key: Key) -> Self {
59+
Self {
60+
base_url,
61+
reqwest: reqwest::Client::builder().build().unwrap(),
62+
key: Some(key),
63+
}
64+
}
65+
66+
pub async fn announce(&self, query: &announce::Query) -> Response {
67+
self.get(&self.build_announce_path_and_query(query)).await
68+
}
69+
70+
pub async fn scrape(&self, query: &scrape::Query) -> Response {
71+
self.get(&self.build_scrape_path_and_query(query)).await
72+
}
73+
74+
pub async fn announce_with_header(&self, query: &Query, key: &str, value: &str) -> Response {
75+
self.get_with_header(&self.build_announce_path_and_query(query), key, value)
76+
.await
77+
}
78+
79+
pub async fn health_check(&self) -> Response {
80+
self.get(&self.build_path("health_check")).await
81+
}
82+
83+
/// # Panics
84+
///
85+
/// This method fails if there was an error while sending request.
86+
pub async fn get(&self, path: &str) -> Response {
87+
self.reqwest.get(self.build_url(path)).send().await.unwrap()
88+
}
89+
90+
/// # Panics
91+
///
92+
/// This method fails if there was an error while sending request.
93+
pub async fn get_with_header(&self, path: &str, key: &str, value: &str) -> Response {
94+
self.reqwest
95+
.get(self.build_url(path))
96+
.header(key, value)
97+
.send()
98+
.await
99+
.unwrap()
100+
}
101+
102+
fn build_announce_path_and_query(&self, query: &announce::Query) -> String {
103+
format!("{}?{query}", self.build_path("announce"))
104+
}
105+
106+
fn build_scrape_path_and_query(&self, query: &scrape::Query) -> String {
107+
format!("{}?{query}", self.build_path("scrape"))
108+
}
109+
110+
fn build_path(&self, path: &str) -> String {
111+
match &self.key {
112+
Some(key) => format!("{path}/{key}"),
113+
None => path.to_string(),
114+
}
115+
}
116+
117+
fn build_url(&self, path: &str) -> String {
118+
let base_url = self.base_url();
119+
format!("{base_url}{path}")
120+
}
121+
122+
fn base_url(&self) -> String {
123+
self.base_url.to_string()
124+
}
125+
}

0 commit comments

Comments
 (0)