1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Axum [`extractor`](axum::extract) to extract the authentication [`Key`](crate::tracker::auth::Key)
//! from the URL path.
//!
//! It's only used when the tracker is running in private mode.
//!
//! Given the following URL route with a path param: `/announce/:key`,
//! it extracts the `key` param from the URL path.
//!
//! It's a wrapper for Axum `Path` extractor in order to return custom
//! authentication errors.
//!
//! It returns a bencoded [`Error`](crate::servers::http::v1::responses::error)
//! response (`500`) if the `key` parameter are missing or invalid.
//!
//! **Sample authentication error responses**
//!
//! When the key param is **missing**:
//!
//! ```text
//! d14:failure reason131:Authentication error: Missing authentication key param for private tracker. Error in src/servers/http/v1/handlers/announce.rs:79:31e
//! ```
//!
//! When the key param has an **invalid format**:
//!
//! ```text
//! d14:failure reason134:Authentication error: Invalid format for authentication key param. Error in src/servers/http/v1/extractors/authentication_key.rs:73:23e
//! ```
//!
//! When the key is **not found** in the database:
//!
//! ```text
//! d14:failure reason101:Authentication error: Failed to read key: YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ, src/tracker/mod.rs:848:27e
//! ```
//!
//! When the key is found in the database but it's **expired**:
//!
//! ```text
//! d14:failure reason64:Authentication error: Key has expired, src/tracker/auth.rs:88:23e
//! ```
//!
//! > **NOTICE**: the returned HTTP status code is always `200` for authentication errors.
//! Neither [The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html)
//! nor [The Private Torrents](https://www.bittorrent.org/beps/bep_0027.html)
//! specifications specify any HTTP status code for authentication errors.
use std::panic::Location;

use axum::async_trait;
use axum::extract::rejection::PathRejection;
use axum::extract::{FromRequestParts, Path};
use axum::http::request::Parts;
use axum::response::{IntoResponse, Response};
use serde::Deserialize;

use crate::servers::http::v1::handlers::common::auth;
use crate::servers::http::v1::responses;
use crate::tracker::auth::Key;

/// Extractor for the [`Key`](crate::tracker::auth::Key) struct.
pub struct Extract(pub Key);

#[derive(Deserialize)]
pub struct KeyParam(String);

impl KeyParam {
    #[must_use]
    pub fn value(&self) -> String {
        self.0.clone()
    }
}

#[async_trait]
impl<S> FromRequestParts<S> for Extract
where
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
        // Extract `key` from URL path with Axum `Path` extractor
        let maybe_path_with_key = Path::<KeyParam>::from_request_parts(parts, state).await;

        match extract_key(maybe_path_with_key) {
            Ok(key) => Ok(Extract(key)),
            Err(error) => Err(error.into_response()),
        }
    }
}

fn extract_key(path_extractor_result: Result<Path<KeyParam>, PathRejection>) -> Result<Key, responses::error::Error> {
    match path_extractor_result {
        Ok(key_param) => match parse_key(&key_param.0.value()) {
            Ok(key) => Ok(key),
            Err(error) => Err(error),
        },
        Err(path_rejection) => Err(custom_error(&path_rejection)),
    }
}

fn parse_key(key: &str) -> Result<Key, responses::error::Error> {
    let key = key.parse::<Key>();

    match key {
        Ok(key) => Ok(key),
        Err(_parse_key_error) => Err(responses::error::Error::from(auth::Error::InvalidKeyFormat {
            location: Location::caller(),
        })),
    }
}

fn custom_error(rejection: &PathRejection) -> responses::error::Error {
    match rejection {
        axum::extract::rejection::PathRejection::FailedToDeserializePathParams(_) => {
            responses::error::Error::from(auth::Error::InvalidKeyFormat {
                location: Location::caller(),
            })
        }
        axum::extract::rejection::PathRejection::MissingPathParams(_) => {
            responses::error::Error::from(auth::Error::MissingAuthKey {
                location: Location::caller(),
            })
        }
        _ => responses::error::Error::from(auth::Error::CannotExtractKeyParam {
            location: Location::caller(),
        }),
    }
}

#[cfg(test)]
mod tests {

    use super::parse_key;
    use crate::servers::http::v1::responses::error::Error;

    fn assert_error_response(error: &Error, error_message: &str) {
        assert!(
            error.failure_reason.contains(error_message),
            "Error response does not contain message: '{error_message}'. Error: {error:?}"
        );
    }

    #[test]
    fn it_should_return_an_authentication_error_if_the_key_cannot_be_parsed() {
        let invalid_key = "invalid_key";

        let response = parse_key(invalid_key).unwrap_err();

        assert_error_response(&response, "Authentication error: Invalid format for authentication key param");
    }
}