|
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; |
101 | 3 |
|
102 | 4 | #[tokio::main] |
103 | 5 | 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 |
354 | 7 | } |
0 commit comments