|
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