Skip to content

Commit 47551ff

Browse files
committed
refactor: [#661] move UDP Tracker Client mod
1 parent b96c2c3 commit 47551ff

File tree

5 files changed

+365
-351
lines changed

5 files changed

+365
-351
lines changed

src/bin/tracker_checker.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Program to run check running trackers.
1+
//! Program to check running trackers.
22
use torrust_tracker::console::clients::checker::app;
33

44
#[tokio::main]

src/bin/udp_tracker_client.rs

+3-350
Original file line numberDiff line numberDiff line change
@@ -1,354 +1,7 @@
1-
//! UDP Tracker client:
2-
//!
3-
//! Examples:
4-
//!
5-
//! Announce request:
6-
//!
7-
//! ```text
8-
//! cargo run --bin udp_tracker_client announce 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
9-
//! ```
10-
//!
11-
//! Announce response:
12-
//!
13-
//! ```json
14-
//! {
15-
//! "transaction_id": -888840697
16-
//! "announce_interval": 120,
17-
//! "leechers": 0,
18-
//! "seeders": 1,
19-
//! "peers": [
20-
//! "123.123.123.123:51289"
21-
//! ],
22-
//! }
23-
//! ```
24-
//!
25-
//! Scrape request:
26-
//!
27-
//! ```text
28-
//! cargo run --bin udp_tracker_client scrape 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
29-
//! ```
30-
//!
31-
//! Scrape response:
32-
//!
33-
//! ```json
34-
//! {
35-
//! "transaction_id": -888840697,
36-
//! "torrent_stats": [
37-
//! {
38-
//! "completed": 0,
39-
//! "leechers": 0,
40-
//! "seeders": 0
41-
//! },
42-
//! {
43-
//! "completed": 0,
44-
//! "leechers": 0,
45-
//! "seeders": 0
46-
//! }
47-
//! ]
48-
//! }
49-
//! ```
50-
//!
51-
//! You can use an URL with instead of the socket address. For example:
52-
//!
53-
//! ```text
54-
//! cargo run --bin udp_tracker_client scrape udp://localhost:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
55-
//! cargo run --bin udp_tracker_client scrape udp://localhost:6969/scrape 9c38422213e30bff212b30c360d26f9a02136422 | jq
56-
//! ```
57-
//!
58-
//! The protocol (`udp://`) in the URL is mandatory. The path (`\scrape`) is optional. It always uses `\scrape`.
59-
use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs};
60-
use std::str::FromStr;
61-
62-
use anyhow::Context;
63-
use aquatic_udp_protocol::common::InfoHash;
64-
use aquatic_udp_protocol::Response::{AnnounceIpv4, AnnounceIpv6, Scrape};
65-
use aquatic_udp_protocol::{
66-
AnnounceEvent, AnnounceRequest, ConnectRequest, ConnectionId, NumberOfBytes, NumberOfPeers, PeerId, PeerKey, Port, Response,
67-
ScrapeRequest, TransactionId,
68-
};
69-
use clap::{Parser, Subcommand};
70-
use log::{debug, LevelFilter};
71-
use serde_json::json;
72-
use torrust_tracker::shared::bit_torrent::info_hash::InfoHash as TorrustInfoHash;
73-
use torrust_tracker::shared::bit_torrent::tracker::udp::client::{UdpClient, UdpTrackerClient};
74-
use url::Url;
75-
76-
const ASSIGNED_BY_OS: i32 = 0;
77-
const RANDOM_TRANSACTION_ID: i32 = -888_840_697;
78-
79-
#[derive(Parser, Debug)]
80-
#[command(author, version, about, long_about = None)]
81-
struct Args {
82-
#[command(subcommand)]
83-
command: Command,
84-
}
85-
86-
#[derive(Subcommand, Debug)]
87-
enum Command {
88-
Announce {
89-
#[arg(value_parser = parse_socket_addr)]
90-
tracker_socket_addr: SocketAddr,
91-
#[arg(value_parser = parse_info_hash)]
92-
info_hash: TorrustInfoHash,
93-
},
94-
Scrape {
95-
#[arg(value_parser = parse_socket_addr)]
96-
tracker_socket_addr: SocketAddr,
97-
#[arg(value_parser = parse_info_hash, num_args = 1..=74, value_delimiter = ' ')]
98-
info_hashes: Vec<TorrustInfoHash>,
99-
},
100-
}
1+
//! Program to make request to UDP trackers.
2+
use torrust_tracker::console::clients::udp::app;
1013

1024
#[tokio::main]
1035
async fn main() -> anyhow::Result<()> {
104-
setup_logging(LevelFilter::Info);
105-
106-
let args = Args::parse();
107-
108-
// Configuration
109-
let local_port = ASSIGNED_BY_OS;
110-
let local_bind_to = format!("0.0.0.0:{local_port}");
111-
let transaction_id = RANDOM_TRANSACTION_ID;
112-
113-
// Bind to local port
114-
debug!("Binding to: {local_bind_to}");
115-
let udp_client = UdpClient::bind(&local_bind_to).await;
116-
let bound_to = udp_client.socket.local_addr().unwrap();
117-
debug!("Bound to: {bound_to}");
118-
119-
let transaction_id = TransactionId(transaction_id);
120-
121-
let response = match args.command {
122-
Command::Announce {
123-
tracker_socket_addr,
124-
info_hash,
125-
} => {
126-
let (connection_id, udp_tracker_client) = connect(&tracker_socket_addr, udp_client, transaction_id).await;
127-
128-
send_announce_request(
129-
connection_id,
130-
transaction_id,
131-
info_hash,
132-
Port(bound_to.port()),
133-
&udp_tracker_client,
134-
)
135-
.await
136-
}
137-
Command::Scrape {
138-
tracker_socket_addr,
139-
info_hashes,
140-
} => {
141-
let (connection_id, udp_tracker_client) = connect(&tracker_socket_addr, udp_client, transaction_id).await;
142-
send_scrape_request(connection_id, transaction_id, info_hashes, &udp_tracker_client).await
143-
}
144-
};
145-
146-
match response {
147-
AnnounceIpv4(announce) => {
148-
let json = json!({
149-
"transaction_id": announce.transaction_id.0,
150-
"announce_interval": announce.announce_interval.0,
151-
"leechers": announce.leechers.0,
152-
"seeders": announce.seeders.0,
153-
"peers": announce.peers.iter().map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)).collect::<Vec<_>>(),
154-
});
155-
let pretty_json = serde_json::to_string_pretty(&json).unwrap();
156-
println!("{pretty_json}");
157-
}
158-
AnnounceIpv6(announce) => {
159-
let json = json!({
160-
"transaction_id": announce.transaction_id.0,
161-
"announce_interval": announce.announce_interval.0,
162-
"leechers": announce.leechers.0,
163-
"seeders": announce.seeders.0,
164-
"peers6": announce.peers.iter().map(|peer| format!("{}:{}", peer.ip_address, peer.port.0)).collect::<Vec<_>>(),
165-
});
166-
let pretty_json = serde_json::to_string_pretty(&json).unwrap();
167-
println!("{pretty_json}");
168-
}
169-
Scrape(scrape) => {
170-
let json = json!({
171-
"transaction_id": scrape.transaction_id.0,
172-
"torrent_stats": scrape.torrent_stats.iter().map(|torrent_scrape_statistics| json!({
173-
"seeders": torrent_scrape_statistics.seeders.0,
174-
"completed": torrent_scrape_statistics.completed.0,
175-
"leechers": torrent_scrape_statistics.leechers.0,
176-
})).collect::<Vec<_>>(),
177-
});
178-
let pretty_json = serde_json::to_string_pretty(&json).unwrap();
179-
println!("{pretty_json}");
180-
}
181-
_ => println!("{response:#?}"), // todo: serialize to JSON all responses.
182-
}
183-
184-
Ok(())
185-
}
186-
187-
fn setup_logging(level: LevelFilter) {
188-
if let Err(_err) = fern::Dispatch::new()
189-
.format(|out, message, record| {
190-
out.finish(format_args!(
191-
"{} [{}][{}] {}",
192-
chrono::Local::now().format("%+"),
193-
record.target(),
194-
record.level(),
195-
message
196-
));
197-
})
198-
.level(level)
199-
.chain(std::io::stdout())
200-
.apply()
201-
{
202-
panic!("Failed to initialize logging.")
203-
}
204-
205-
debug!("logging initialized.");
206-
}
207-
208-
fn parse_socket_addr(tracker_socket_addr_str: &str) -> anyhow::Result<SocketAddr> {
209-
debug!("Tracker socket address: {tracker_socket_addr_str:#?}");
210-
211-
// Check if the address is a valid URL. If so, extract the host and port.
212-
let resolved_addr = if let Ok(url) = Url::parse(tracker_socket_addr_str) {
213-
debug!("Tracker socket address URL: {url:?}");
214-
215-
let host = url
216-
.host_str()
217-
.with_context(|| format!("invalid host in URL: `{tracker_socket_addr_str}`"))?
218-
.to_owned();
219-
220-
let port = url
221-
.port()
222-
.with_context(|| format!("port not found in URL: `{tracker_socket_addr_str}`"))?
223-
.to_owned();
224-
225-
(host, port)
226-
} else {
227-
// If not a URL, assume it's a host:port pair.
228-
229-
let parts: Vec<&str> = tracker_socket_addr_str.split(':').collect();
230-
231-
if parts.len() != 2 {
232-
return Err(anyhow::anyhow!(
233-
"invalid address format: `{}`. Expected format is host:port",
234-
tracker_socket_addr_str
235-
));
236-
}
237-
238-
let host = parts[0].to_owned();
239-
240-
let port = parts[1]
241-
.parse::<u16>()
242-
.with_context(|| format!("invalid port: `{}`", parts[1]))?
243-
.to_owned();
244-
245-
(host, port)
246-
};
247-
248-
debug!("Resolved address: {resolved_addr:#?}");
249-
250-
// Perform DNS resolution.
251-
let socket_addrs: Vec<_> = resolved_addr.to_socket_addrs()?.collect();
252-
if socket_addrs.is_empty() {
253-
Err(anyhow::anyhow!("DNS resolution failed for `{}`", tracker_socket_addr_str))
254-
} else {
255-
Ok(socket_addrs[0])
256-
}
257-
}
258-
259-
fn parse_info_hash(info_hash_str: &str) -> anyhow::Result<TorrustInfoHash> {
260-
TorrustInfoHash::from_str(info_hash_str)
261-
.map_err(|e| anyhow::Error::msg(format!("failed to parse info-hash `{info_hash_str}`: {e:?}")))
262-
}
263-
264-
async fn connect(
265-
tracker_socket_addr: &SocketAddr,
266-
udp_client: UdpClient,
267-
transaction_id: TransactionId,
268-
) -> (ConnectionId, UdpTrackerClient) {
269-
debug!("Connecting to tracker: udp://{tracker_socket_addr}");
270-
271-
udp_client.connect(&tracker_socket_addr.to_string()).await;
272-
273-
let udp_tracker_client = UdpTrackerClient { udp_client };
274-
275-
let connection_id = send_connection_request(transaction_id, &udp_tracker_client).await;
276-
277-
(connection_id, udp_tracker_client)
278-
}
279-
280-
async fn send_connection_request(transaction_id: TransactionId, client: &UdpTrackerClient) -> ConnectionId {
281-
debug!("Sending connection request with transaction id: {transaction_id:#?}");
282-
283-
let connect_request = ConnectRequest { transaction_id };
284-
285-
client.send(connect_request.into()).await;
286-
287-
let response = client.receive().await;
288-
289-
debug!("connection request response:\n{response:#?}");
290-
291-
match response {
292-
Response::Connect(connect_response) => connect_response.connection_id,
293-
_ => panic!("error connecting to udp server. Unexpected response"),
294-
}
295-
}
296-
297-
async fn send_announce_request(
298-
connection_id: ConnectionId,
299-
transaction_id: TransactionId,
300-
info_hash: TorrustInfoHash,
301-
port: Port,
302-
client: &UdpTrackerClient,
303-
) -> Response {
304-
debug!("Sending announce request with transaction id: {transaction_id:#?}");
305-
306-
let announce_request = AnnounceRequest {
307-
connection_id,
308-
transaction_id,
309-
info_hash: InfoHash(info_hash.bytes()),
310-
peer_id: PeerId(*b"-qB00000000000000001"),
311-
bytes_downloaded: NumberOfBytes(0i64),
312-
bytes_uploaded: NumberOfBytes(0i64),
313-
bytes_left: NumberOfBytes(0i64),
314-
event: AnnounceEvent::Started,
315-
ip_address: Some(Ipv4Addr::new(0, 0, 0, 0)),
316-
key: PeerKey(0u32),
317-
peers_wanted: NumberOfPeers(1i32),
318-
port,
319-
};
320-
321-
client.send(announce_request.into()).await;
322-
323-
let response = client.receive().await;
324-
325-
debug!("announce request response:\n{response:#?}");
326-
327-
response
328-
}
329-
330-
async fn send_scrape_request(
331-
connection_id: ConnectionId,
332-
transaction_id: TransactionId,
333-
info_hashes: Vec<TorrustInfoHash>,
334-
client: &UdpTrackerClient,
335-
) -> Response {
336-
debug!("Sending scrape request with transaction id: {transaction_id:#?}");
337-
338-
let scrape_request = ScrapeRequest {
339-
connection_id,
340-
transaction_id,
341-
info_hashes: info_hashes
342-
.iter()
343-
.map(|torrust_info_hash| InfoHash(torrust_info_hash.bytes()))
344-
.collect(),
345-
};
346-
347-
client.send(scrape_request.into()).await;
348-
349-
let response = client.receive().await;
350-
351-
debug!("scrape request response:\n{response:#?}");
352-
353-
response
6+
app::run().await
3547
}

src/console/clients/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
//! Console clients.
22
pub mod checker;
33
pub mod http;
4+
pub mod udp;

0 commit comments

Comments
 (0)