@@ -16,13 +16,9 @@ use crate::http::axum_implementation::responses::{self, announce};
1616use crate :: http:: axum_implementation:: services:: peer_ip_resolver:: ClientIpSources ;
1717use crate :: http:: axum_implementation:: services:: { self , peer_ip_resolver} ;
1818use crate :: protocol:: clock:: { Current , Time } ;
19+ use crate :: tracker:: auth:: Key ;
1920use crate :: tracker:: peer:: Peer ;
20- use crate :: tracker:: Tracker ;
21-
22- /* code-review: authentication, authorization and peer IP resolution could be moved
23- from the handler (Axum) layer into the app layer `services::announce::invoke`.
24- That would make the handler even simpler and the code more reusable and decoupled from Axum.
25- */
21+ use crate :: tracker:: { AnnounceData , Tracker } ;
2622
2723#[ allow( clippy:: unused_async) ]
2824pub async fn handle_without_key (
@@ -32,14 +28,7 @@ pub async fn handle_without_key(
3228) -> Response {
3329 debug ! ( "http announce request: {:#?}" , announce_request) ;
3430
35- if tracker. requires_authentication ( ) {
36- return responses:: error:: Error :: from ( auth:: Error :: MissingAuthKey {
37- location : Location :: caller ( ) ,
38- } )
39- . into_response ( ) ;
40- }
41-
42- handle ( & tracker, & announce_request, & client_ip_sources) . await
31+ handle ( & tracker, & announce_request, & client_ip_sources, None ) . await
4332}
4433
4534#[ allow( clippy:: unused_async) ]
@@ -51,29 +40,67 @@ pub async fn handle_with_key(
5140) -> Response {
5241 debug ! ( "http announce request: {:#?}" , announce_request) ;
5342
54- match tracker. authenticate ( & key) . await {
55- Ok ( _) => ( ) ,
56- Err ( error) => return responses:: error:: Error :: from ( error) . into_response ( ) ,
57- }
43+ handle ( & tracker, & announce_request, & client_ip_sources, Some ( key) ) . await
44+ }
5845
59- handle ( & tracker, & announce_request, & client_ip_sources) . await
46+ async fn handle (
47+ tracker : & Arc < Tracker > ,
48+ announce_request : & Announce ,
49+ client_ip_sources : & ClientIpSources ,
50+ maybe_key : Option < Key > ,
51+ ) -> Response {
52+ let announce_data = match handle_announce ( tracker, announce_request, client_ip_sources, maybe_key) . await {
53+ Ok ( announce_data) => announce_data,
54+ Err ( error) => return error. into_response ( ) ,
55+ } ;
56+ build_response ( announce_request, announce_data)
6057}
6158
62- async fn handle ( tracker : & Arc < Tracker > , announce_request : & Announce , client_ip_sources : & ClientIpSources ) -> Response {
59+ /* code-review: authentication, authorization and peer IP resolution could be moved
60+ from the handler (Axum) layer into the app layer `services::announce::invoke`.
61+ That would make the handler even simpler and the code more reusable and decoupled from Axum.
62+ */
63+
64+ async fn handle_announce (
65+ tracker : & Arc < Tracker > ,
66+ announce_request : & Announce ,
67+ client_ip_sources : & ClientIpSources ,
68+ maybe_key : Option < Key > ,
69+ ) -> Result < AnnounceData , responses:: error:: Error > {
70+ // Authentication
71+ if tracker. requires_authentication ( ) {
72+ match maybe_key {
73+ Some ( key) => match tracker. authenticate ( & key) . await {
74+ Ok ( _) => ( ) ,
75+ Err ( error) => return Err ( responses:: error:: Error :: from ( error) ) ,
76+ } ,
77+ None => {
78+ return Err ( responses:: error:: Error :: from ( auth:: Error :: MissingAuthKey {
79+ location : Location :: caller ( ) ,
80+ } ) )
81+ }
82+ }
83+ }
84+
85+ // Authorization
6386 match tracker. authorize ( & announce_request. info_hash ) . await {
6487 Ok ( _) => ( ) ,
65- Err ( error) => return responses:: error:: Error :: from ( error) . into_response ( ) ,
88+ Err ( error) => return Err ( responses:: error:: Error :: from ( error) ) ,
6689 }
6790
6891 let peer_ip = match peer_ip_resolver:: invoke ( tracker. config . on_reverse_proxy , client_ip_sources) {
6992 Ok ( peer_ip) => peer_ip,
70- Err ( error) => return responses:: error:: Error :: from ( error) . into_response ( ) ,
93+ Err ( error) => return Err ( responses:: error:: Error :: from ( error) ) ,
7194 } ;
7295
7396 let mut peer = peer_from_request ( announce_request, & peer_ip) ;
7497
7598 let announce_data = services:: announce:: invoke ( tracker. clone ( ) , announce_request. info_hash , & mut peer) . await ;
7699
100+ Ok ( announce_data)
101+ }
102+
103+ fn build_response ( announce_request : & Announce , announce_data : AnnounceData ) -> Response {
77104 match & announce_request. compact {
78105 Some ( compact) => match compact {
79106 Compact :: Accepted => announce:: Compact :: from ( announce_data) . into_response ( ) ,
@@ -108,3 +135,211 @@ fn map_to_aquatic_event(event: &Option<Event>) -> AnnounceEvent {
108135 None => aquatic_udp_protocol:: AnnounceEvent :: None ,
109136 }
110137}
138+
139+ #[ cfg( test) ]
140+ mod tests {
141+ use std:: sync:: Arc ;
142+
143+ use crate :: config:: { ephemeral_configuration, Configuration } ;
144+ use crate :: http:: axum_implementation:: requests:: announce:: Announce ;
145+ use crate :: http:: axum_implementation:: responses;
146+ use crate :: http:: axum_implementation:: services:: peer_ip_resolver:: ClientIpSources ;
147+ use crate :: protocol:: info_hash:: InfoHash ;
148+ use crate :: tracker:: mode:: Mode ;
149+ use crate :: tracker:: statistics:: Keeper ;
150+ use crate :: tracker:: { peer, Tracker } ;
151+
152+ fn private_tracker ( ) -> Tracker {
153+ let mut configuration = ephemeral_configuration ( ) ;
154+ configuration. mode = Mode :: Private ;
155+ tracker_factory ( configuration)
156+ }
157+
158+ fn listed_tracker ( ) -> Tracker {
159+ let mut configuration = ephemeral_configuration ( ) ;
160+ configuration. mode = Mode :: Listed ;
161+ tracker_factory ( configuration)
162+ }
163+
164+ fn tracker_on_reverse_proxy ( ) -> Tracker {
165+ let mut configuration = ephemeral_configuration ( ) ;
166+ configuration. on_reverse_proxy = true ;
167+ tracker_factory ( configuration)
168+ }
169+
170+ fn tracker_not_on_reverse_proxy ( ) -> Tracker {
171+ let mut configuration = ephemeral_configuration ( ) ;
172+ configuration. on_reverse_proxy = false ;
173+ tracker_factory ( configuration)
174+ }
175+
176+ fn tracker_factory ( configuration : Configuration ) -> Tracker {
177+ // code-review: the tracker initialization is duplicated in many places. Consider make this function public.
178+
179+ // Initialize stats tracker
180+ let ( stats_event_sender, stats_repository) = Keeper :: new_active_instance ( ) ;
181+
182+ // Initialize Torrust tracker
183+ match Tracker :: new ( & Arc :: new ( configuration) , Some ( stats_event_sender) , stats_repository) {
184+ Ok ( tracker) => tracker,
185+ Err ( error) => {
186+ panic ! ( "{}" , error)
187+ }
188+ }
189+ }
190+
191+ fn sample_announce_request ( ) -> Announce {
192+ Announce {
193+ info_hash : "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0" . parse :: < InfoHash > ( ) . unwrap ( ) ,
194+ peer_id : "-qB00000000000000001" . parse :: < peer:: Id > ( ) . unwrap ( ) ,
195+ port : 17548 ,
196+ downloaded : None ,
197+ uploaded : None ,
198+ left : None ,
199+ event : None ,
200+ compact : None ,
201+ }
202+ }
203+
204+ fn sample_client_ip_sources ( ) -> ClientIpSources {
205+ ClientIpSources {
206+ right_most_x_forwarded_for : None ,
207+ connection_info_ip : None ,
208+ }
209+ }
210+
211+ fn assert_error_response ( error : & responses:: error:: Error , error_message : & str ) {
212+ assert ! (
213+ error. failure_reason. contains( error_message) ,
214+ "Error response does not contain message: '{error_message}'. Error: {error:?}"
215+ ) ;
216+ }
217+
218+ mod with_tracker_in_private_mode {
219+
220+ use std:: str:: FromStr ;
221+ use std:: sync:: Arc ;
222+
223+ use super :: { private_tracker, sample_announce_request, sample_client_ip_sources} ;
224+ use crate :: http:: axum_implementation:: handlers:: announce:: handle_announce;
225+ use crate :: http:: axum_implementation:: handlers:: announce:: tests:: assert_error_response;
226+ use crate :: tracker:: auth;
227+
228+ #[ tokio:: test]
229+ async fn it_should_fail_when_the_authentication_key_is_missing ( ) {
230+ let tracker = Arc :: new ( private_tracker ( ) ) ;
231+
232+ let maybe_key = None ;
233+
234+ let response = handle_announce ( & tracker, & sample_announce_request ( ) , & sample_client_ip_sources ( ) , maybe_key)
235+ . await
236+ . unwrap_err ( ) ;
237+
238+ assert_error_response (
239+ & response,
240+ "Authentication error: Missing authentication key param for private tracker" ,
241+ ) ;
242+ }
243+
244+ #[ tokio:: test]
245+ async fn it_should_fail_when_the_authentication_key_is_invalid ( ) {
246+ let tracker = Arc :: new ( private_tracker ( ) ) ;
247+
248+ let unregistered_key = auth:: Key :: from_str ( "YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ" ) . unwrap ( ) ;
249+
250+ let maybe_key = Some ( unregistered_key) ;
251+
252+ let response = handle_announce ( & tracker, & sample_announce_request ( ) , & sample_client_ip_sources ( ) , maybe_key)
253+ . await
254+ . unwrap_err ( ) ;
255+
256+ assert_error_response ( & response, "Authentication error: Failed to read key" ) ;
257+ }
258+ }
259+
260+ mod with_tracker_in_listed_mode {
261+
262+ use std:: sync:: Arc ;
263+
264+ use super :: { listed_tracker, sample_announce_request, sample_client_ip_sources} ;
265+ use crate :: http:: axum_implementation:: handlers:: announce:: handle_announce;
266+ use crate :: http:: axum_implementation:: handlers:: announce:: tests:: assert_error_response;
267+
268+ #[ tokio:: test]
269+ async fn it_should_fail_when_the_announced_torrent_is_not_whitelisted ( ) {
270+ let tracker = Arc :: new ( listed_tracker ( ) ) ;
271+
272+ let announce_request = sample_announce_request ( ) ;
273+
274+ let response = handle_announce ( & tracker, & announce_request, & sample_client_ip_sources ( ) , None )
275+ . await
276+ . unwrap_err ( ) ;
277+
278+ assert_error_response (
279+ & response,
280+ & format ! (
281+ "Tracker error: The torrent: {}, is not whitelisted" ,
282+ announce_request. info_hash
283+ ) ,
284+ ) ;
285+ }
286+ }
287+
288+ mod with_tracker_on_reverse_proxy {
289+
290+ use std:: sync:: Arc ;
291+
292+ use super :: { sample_announce_request, tracker_on_reverse_proxy} ;
293+ use crate :: http:: axum_implementation:: handlers:: announce:: handle_announce;
294+ use crate :: http:: axum_implementation:: handlers:: announce:: tests:: assert_error_response;
295+ use crate :: http:: axum_implementation:: services:: peer_ip_resolver:: ClientIpSources ;
296+
297+ #[ tokio:: test]
298+ async fn it_should_fail_when_the_right_most_x_forwarded_for_header_ip_is_not_available ( ) {
299+ let tracker = Arc :: new ( tracker_on_reverse_proxy ( ) ) ;
300+
301+ let client_ip_sources = ClientIpSources {
302+ right_most_x_forwarded_for : None ,
303+ connection_info_ip : None ,
304+ } ;
305+
306+ let response = handle_announce ( & tracker, & sample_announce_request ( ) , & client_ip_sources, None )
307+ . await
308+ . unwrap_err ( ) ;
309+
310+ assert_error_response (
311+ & response,
312+ "Error resolving peer IP: missing or invalid the right most X-Forwarded-For IP" ,
313+ ) ;
314+ }
315+ }
316+
317+ mod with_tracker_not_on_reverse_proxy {
318+
319+ use std:: sync:: Arc ;
320+
321+ use super :: { sample_announce_request, tracker_not_on_reverse_proxy} ;
322+ use crate :: http:: axum_implementation:: handlers:: announce:: handle_announce;
323+ use crate :: http:: axum_implementation:: handlers:: announce:: tests:: assert_error_response;
324+ use crate :: http:: axum_implementation:: services:: peer_ip_resolver:: ClientIpSources ;
325+
326+ #[ tokio:: test]
327+ async fn it_should_fail_when_the_client_ip_from_the_connection_info_is_not_available ( ) {
328+ let tracker = Arc :: new ( tracker_not_on_reverse_proxy ( ) ) ;
329+
330+ let client_ip_sources = ClientIpSources {
331+ right_most_x_forwarded_for : None ,
332+ connection_info_ip : None ,
333+ } ;
334+
335+ let response = handle_announce ( & tracker, & sample_announce_request ( ) , & client_ip_sources, None )
336+ . await
337+ . unwrap_err ( ) ;
338+
339+ assert_error_response (
340+ & response,
341+ "Error resolving peer IP: cannot get the client IP from the connection info" ,
342+ ) ;
343+ }
344+ }
345+ }
0 commit comments