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");
}
}