torrust_tracker/servers/http/v1/extractors/
authentication_key.rs

1//! Axum [`extractor`](axum::extract) to extract the authentication [`Key`]
2//! from the URL path.
3//!
4//! It's only used when the tracker is running in private mode.
5//!
6//! Given the following URL route with a path param: `/announce/:key`,
7//! it extracts the `key` param from the URL path.
8//!
9//! It's a wrapper for Axum `Path` extractor in order to return custom
10//! authentication errors.
11//!
12//! It returns a bencoded [`Error`](crate::servers::http::v1::responses::error)
13//! response (`500`) if the `key` parameter are missing or invalid.
14//!
15//! **Sample authentication error responses**
16//!
17//! When the key param is **missing**:
18//!
19//! ```text
20//! d14:failure reason131:Authentication error: Missing authentication key param for private tracker. Error in src/servers/http/v1/handlers/announce.rs:79:31e
21//! ```
22//!
23//! When the key param has an **invalid format**:
24//!
25//! ```text
26//! d14:failure reason134:Authentication error: Invalid format for authentication key param. Error in src/servers/http/v1/extractors/authentication_key.rs:73:23e
27//! ```
28//!
29//! When the key is **not found** in the database:
30//!
31//! ```text
32//! d14:failure reason101:Authentication error: Failed to read key: YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ, src/tracker/mod.rs:848:27e
33//! ```
34//!
35//! When the key is found in the database but it's **expired**:
36//!
37//! ```text
38//! d14:failure reason64:Authentication error: Key has expired, src/tracker/auth.rs:88:23e
39//! ```
40//!
41//! > **NOTICE**: the returned HTTP status code is always `200` for authentication errors.
42//! > Neither [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html)
43//! > nor [The Private Torrents](https://www.bittorrent.org/beps/bep_0027.html)
44//! > specifications specify any HTTP status code for authentication errors.
45use std::panic::Location;
46
47use axum::extract::rejection::PathRejection;
48use axum::extract::{FromRequestParts, Path};
49use axum::http::request::Parts;
50use axum::response::{IntoResponse, Response};
51use futures::future::BoxFuture;
52use futures::FutureExt;
53use serde::Deserialize;
54
55use crate::core::auth::Key;
56use crate::servers::http::v1::handlers::common::auth;
57use crate::servers::http::v1::responses;
58
59/// Extractor for the [`Key`] struct.
60pub struct Extract(pub Key);
61
62#[derive(Deserialize)]
63pub struct KeyParam(String);
64
65impl KeyParam {
66    #[must_use]
67    pub fn value(&self) -> String {
68        self.0.clone()
69    }
70}
71
72impl<S> FromRequestParts<S> for Extract
73where
74    S: Send + Sync,
75{
76    type Rejection = Response;
77
78    #[must_use]
79    fn from_request_parts<'life0, 'life1, 'async_trait>(
80        parts: &'life0 mut Parts,
81        state: &'life1 S,
82    ) -> BoxFuture<'async_trait, Result<Self, Self::Rejection>>
83    where
84        'life0: 'async_trait,
85        'life1: 'async_trait,
86        Self: 'async_trait,
87    {
88        async {
89            // Extract `key` from URL path with Axum `Path` extractor
90            let maybe_path_with_key = Path::<KeyParam>::from_request_parts(parts, state).await;
91
92            match extract_key(maybe_path_with_key) {
93                Ok(key) => Ok(Extract(key)),
94                Err(error) => Err(error.into_response()),
95            }
96        }
97        .boxed()
98    }
99}
100
101fn extract_key(path_extractor_result: Result<Path<KeyParam>, PathRejection>) -> Result<Key, responses::error::Error> {
102    match path_extractor_result {
103        Ok(key_param) => match parse_key(&key_param.0.value()) {
104            Ok(key) => Ok(key),
105            Err(error) => Err(error),
106        },
107        Err(path_rejection) => Err(custom_error(&path_rejection)),
108    }
109}
110
111fn parse_key(key: &str) -> Result<Key, responses::error::Error> {
112    let key = key.parse::<Key>();
113
114    match key {
115        Ok(key) => Ok(key),
116        Err(_parse_key_error) => Err(responses::error::Error::from(auth::Error::InvalidKeyFormat {
117            location: Location::caller(),
118        })),
119    }
120}
121
122fn custom_error(rejection: &PathRejection) -> responses::error::Error {
123    match rejection {
124        axum::extract::rejection::PathRejection::FailedToDeserializePathParams(_) => {
125            responses::error::Error::from(auth::Error::InvalidKeyFormat {
126                location: Location::caller(),
127            })
128        }
129        axum::extract::rejection::PathRejection::MissingPathParams(_) => {
130            responses::error::Error::from(auth::Error::MissingAuthKey {
131                location: Location::caller(),
132            })
133        }
134        _ => responses::error::Error::from(auth::Error::CannotExtractKeyParam {
135            location: Location::caller(),
136        }),
137    }
138}
139
140#[cfg(test)]
141mod tests {
142
143    use super::parse_key;
144    use crate::servers::http::v1::responses::error::Error;
145
146    fn assert_error_response(error: &Error, error_message: &str) {
147        assert!(
148            error.failure_reason.contains(error_message),
149            "Error response does not contain message: '{error_message}'. Error: {error:?}"
150        );
151    }
152
153    #[test]
154    fn it_should_return_an_authentication_error_if_the_key_cannot_be_parsed() {
155        let invalid_key = "invalid_key";
156
157        let response = parse_key(invalid_key).unwrap_err();
158
159        assert_error_response(&response, "Authentication error: Invalid format for authentication key param");
160    }
161}