5
5
//! Announce request:
6
6
//!
7
7
//! ```text
8
- //! cargo run --bin udp_tracker_client 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
8
+ //! cargo run --bin udp_tracker_client announce 127.0.0.1:6969 9c38422213e30bff212b30c360d26f9a02136422 | jq
9
9
//! ```
10
10
//!
11
11
//! Announce response:
20
20
//! "123.123.123.123:51289"
21
21
//! ],
22
22
//! }
23
- /// ````
24
- use std:: net:: { Ipv4Addr , SocketAddr } ;
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 } ;
25
60
use std:: str:: FromStr ;
26
61
27
62
use anyhow:: Context ;
28
63
use aquatic_udp_protocol:: common:: InfoHash ;
29
- use aquatic_udp_protocol:: Response :: { AnnounceIpv4 , AnnounceIpv6 } ;
64
+ use aquatic_udp_protocol:: Response :: { AnnounceIpv4 , AnnounceIpv6 , Scrape } ;
30
65
use aquatic_udp_protocol:: {
31
66
AnnounceEvent , AnnounceRequest , ConnectRequest , ConnectionId , NumberOfBytes , NumberOfPeers , PeerId , PeerKey , Port , Response ,
32
- TransactionId ,
67
+ ScrapeRequest , TransactionId ,
33
68
} ;
34
69
use clap:: { Parser , Subcommand } ;
35
70
use log:: { debug, LevelFilter } ;
36
71
use serde_json:: json;
37
72
use torrust_tracker:: shared:: bit_torrent:: info_hash:: InfoHash as TorrustInfoHash ;
38
73
use torrust_tracker:: shared:: bit_torrent:: tracker:: udp:: client:: { UdpClient , UdpTrackerClient } ;
74
+ use url:: Url ;
39
75
40
76
const ASSIGNED_BY_OS : i32 = 0 ;
41
77
const RANDOM_TRANSACTION_ID : i32 = -888_840_697 ;
@@ -55,6 +91,12 @@ enum Command {
55
91
#[ arg( value_parser = parse_info_hash) ]
56
92
info_hash : TorrustInfoHash ,
57
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
+ } ,
58
100
}
59
101
60
102
#[ tokio:: main]
@@ -65,29 +107,23 @@ async fn main() -> anyhow::Result<()> {
65
107
66
108
// Configuration
67
109
let local_port = ASSIGNED_BY_OS ;
110
+ let local_bind_to = format ! ( "0.0.0.0:{local_port}" ) ;
68
111
let transaction_id = RANDOM_TRANSACTION_ID ;
69
- let bind_to = format ! ( "0.0.0.0:{local_port}" ) ;
70
112
71
113
// Bind to local port
72
- debug ! ( "Binding to: {bind_to }" ) ;
73
- 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 ;
74
116
let bound_to = udp_client. socket . local_addr ( ) . unwrap ( ) ;
75
117
debug ! ( "Bound to: {bound_to}" ) ;
76
118
119
+ let transaction_id = TransactionId ( transaction_id) ;
120
+
77
121
let response = match args. command {
78
122
Command :: Announce {
79
123
tracker_socket_addr,
80
124
info_hash,
81
125
} => {
82
- debug ! ( "Connecting to remote: udp://{tracker_socket_addr}" ) ;
83
-
84
- udp_client. connect ( & tracker_socket_addr. to_string ( ) ) . await ;
85
-
86
- let udp_tracker_client = UdpTrackerClient { udp_client } ;
87
-
88
- let transaction_id = TransactionId ( transaction_id) ;
89
-
90
- let connection_id = send_connection_request ( transaction_id, & udp_tracker_client) . await ;
126
+ let ( connection_id, udp_tracker_client) = connect ( & tracker_socket_addr, udp_client, transaction_id) . await ;
91
127
92
128
send_announce_request (
93
129
connection_id,
@@ -98,6 +134,13 @@ async fn main() -> anyhow::Result<()> {
98
134
)
99
135
. await
100
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
+ }
101
144
} ;
102
145
103
146
match response {
@@ -123,7 +166,19 @@ async fn main() -> anyhow::Result<()> {
123
166
let pretty_json = serde_json:: to_string_pretty ( & json) . unwrap ( ) ;
124
167
println ! ( "{pretty_json}" ) ;
125
168
}
126
- _ => println ! ( "{response:#?}" ) ,
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.
127
182
}
128
183
129
184
Ok ( ( ) )
@@ -150,12 +205,76 @@ fn setup_logging(level: LevelFilter) {
150
205
debug ! ( "logging initialized." ) ;
151
206
}
152
207
153
- fn parse_socket_addr ( s : & str ) -> anyhow:: Result < SocketAddr > {
154
- s. parse ( ) . with_context ( || format ! ( "failed to parse socket address: `{s}`" ) )
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:?}" ) ) )
155
262
}
156
263
157
- fn parse_info_hash ( s : & str ) -> anyhow:: Result < TorrustInfoHash > {
158
- TorrustInfoHash :: from_str ( s) . map_err ( |e| anyhow:: Error :: msg ( format ! ( "failed to parse info-hash `{s}`: {e:?}" ) ) )
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)
159
278
}
160
279
161
280
async fn send_connection_request ( transaction_id : TransactionId , client : & UdpTrackerClient ) -> ConnectionId {
@@ -207,3 +326,29 @@ async fn send_announce_request(
207
326
208
327
response
209
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