1
- use std:: env;
2
- use std:: net:: { Ipv4Addr , SocketAddr } ;
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 } ;
3
60
use std:: str:: FromStr ;
4
61
62
+ use anyhow:: Context ;
5
63
use aquatic_udp_protocol:: common:: InfoHash ;
64
+ use aquatic_udp_protocol:: Response :: { AnnounceIpv4 , AnnounceIpv6 , Scrape } ;
6
65
use aquatic_udp_protocol:: {
7
66
AnnounceEvent , AnnounceRequest , ConnectRequest , ConnectionId , NumberOfBytes , NumberOfPeers , PeerId , PeerKey , Port , Response ,
8
- TransactionId ,
67
+ ScrapeRequest , TransactionId ,
9
68
} ;
69
+ use clap:: { Parser , Subcommand } ;
10
70
use log:: { debug, LevelFilter } ;
71
+ use serde_json:: json;
11
72
use torrust_tracker:: shared:: bit_torrent:: info_hash:: InfoHash as TorrustInfoHash ;
12
73
use torrust_tracker:: shared:: bit_torrent:: tracker:: udp:: client:: { UdpClient , UdpTrackerClient } ;
74
+ use url:: Url ;
13
75
14
76
const ASSIGNED_BY_OS : i32 = 0 ;
15
77
const RANDOM_TRANSACTION_ID : i32 = -888_840_697 ;
16
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
+ }
101
+
17
102
#[ tokio:: main]
18
- async fn main ( ) {
103
+ async fn main ( ) -> anyhow :: Result < ( ) > {
19
104
setup_logging ( LevelFilter :: Info ) ;
20
105
21
- let ( remote_socket_addr , info_hash ) = parse_arguments ( ) ;
106
+ let args = Args :: parse ( ) ;
22
107
23
108
// Configuration
24
109
let local_port = ASSIGNED_BY_OS ;
110
+ let local_bind_to = format ! ( "0.0.0.0:{local_port}" ) ;
25
111
let transaction_id = RANDOM_TRANSACTION_ID ;
26
- let bind_to = format ! ( "0.0.0.0:{local_port}" ) ;
27
112
28
113
// Bind to local port
29
-
30
- debug ! ( "Binding to: {bind_to}" ) ;
31
- let udp_client = UdpClient :: bind ( & bind_to) . await ;
114
+ debug ! ( "Binding to: {local_bind_to}" ) ;
115
+ let udp_client = UdpClient :: bind ( & local_bind_to) . await ;
32
116
let bound_to = udp_client. socket . local_addr ( ) . unwrap ( ) ;
33
117
debug ! ( "Bound to: {bound_to}" ) ;
34
118
35
- // Connect to remote socket
36
-
37
- debug ! ( "Connecting to remote: udp://{remote_socket_addr}" ) ;
38
- udp_client. connect ( & remote_socket_addr) . await ;
39
-
40
- let udp_tracker_client = UdpTrackerClient { udp_client } ;
41
-
42
119
let transaction_id = TransactionId ( transaction_id) ;
43
120
44
- let connection_id = send_connection_request ( transaction_id, & udp_tracker_client) . await ;
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
+ } ;
45
145
46
- let response = send_announce_request (
47
- connection_id,
48
- transaction_id,
49
- info_hash,
50
- Port ( bound_to. port ( ) ) ,
51
- & udp_tracker_client,
52
- )
53
- . await ;
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
+ }
54
183
55
- println ! ( "{response:#?}" ) ;
184
+ Ok ( ( ) )
56
185
}
57
186
58
187
fn setup_logging ( level : LevelFilter ) {
@@ -76,31 +205,76 @@ fn setup_logging(level: LevelFilter) {
76
205
debug ! ( "logging initialized." ) ;
77
206
}
78
207
79
- fn parse_arguments ( ) -> ( String , TorrustInfoHash ) {
80
- let args: Vec < String > = env:: args ( ) . collect ( ) ;
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:#?}" ) ;
81
249
82
- if args. len ( ) != 3 {
83
- eprintln ! ( "Error: invalid number of arguments!" ) ;
84
- eprintln ! ( "Usage: cargo run --bin udp_tracker_client <UDP_TRACKER_SOCKET_ADDRESS> <INFO_HASH>" ) ;
85
- eprintln ! ( "Example: cargo run --bin udp_tracker_client 144.126.245.19:6969 9c38422213e30bff212b30c360d26f9a02136422" ) ;
86
- std:: process:: exit ( 1 ) ;
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 ] )
87
256
}
257
+ }
88
258
89
- let remote_socket_addr = & args[ 1 ] ;
90
- let _valid_socket_addr = remote_socket_addr. parse :: < SocketAddr > ( ) . unwrap_or_else ( |_| {
91
- panic ! (
92
- "Invalid argument: `{}`. Argument 1 should be a valid socket address. For example: `144.126.245.19:6969`." ,
93
- args[ 1 ]
94
- )
95
- } ) ;
96
- let info_hash = TorrustInfoHash :: from_str ( & args[ 2 ] ) . unwrap_or_else ( |_| {
97
- panic ! (
98
- "Invalid argument: `{}`. Argument 2 should be a valid infohash. For example: `9c38422213e30bff212b30c360d26f9a02136422`." ,
99
- args[ 2 ]
100
- )
101
- } ) ;
102
-
103
- ( remote_socket_addr. to_string ( ) , info_hash)
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)
104
278
}
105
279
106
280
async fn send_connection_request ( transaction_id : TransactionId , client : & UdpTrackerClient ) -> ConnectionId {
@@ -152,3 +326,29 @@ async fn send_announce_request(
152
326
153
327
response
154
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
354
+ }
0 commit comments