Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ pub struct Key(String);
/// ```
///
/// If the string does not contains a valid key, the parser function will return this error.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Display)]
pub struct ParseKeyError;

impl FromStr for Key {
Expand Down
38 changes: 33 additions & 5 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,16 +453,17 @@ use std::panic::Location;
use std::sync::Arc;
use std::time::Duration;

use auth::ExpiringKey;
use databases::driver::Driver;
use derive_more::Constructor;
use tokio::sync::mpsc::error::SendError;
use torrust_tracker_clock::clock::Time;
use torrust_tracker_configuration::v2::database;
use torrust_tracker_configuration::{AnnouncePolicy, Core, TORRENT_PEERS_LIMIT};
use torrust_tracker_primitives::info_hash::InfoHash;
use torrust_tracker_primitives::peer;
use torrust_tracker_primitives::swarm_metadata::SwarmMetadata;
use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics;
use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch};
use torrust_tracker_torrent_repository::entry::EntrySync;
use torrust_tracker_torrent_repository::repository::Repository;
use tracing::debug;
Expand Down Expand Up @@ -804,6 +805,37 @@ impl Tracker {
/// Will return a `database::Error` if unable to add the `auth_key` to the database.
pub async fn generate_auth_key(&self, lifetime: Duration) -> Result<auth::ExpiringKey, databases::error::Error> {
let auth_key = auth::generate(lifetime);

self.database.add_key_to_keys(&auth_key)?;
self.keys.write().await.insert(auth_key.key.clone(), auth_key.clone());
Ok(auth_key)
}

/// It adds a pre-generated authentication key.
///
/// Authentication keys are used by HTTP trackers.
///
/// # Context: Authentication
///
/// # Errors
///
/// Will return a `database::Error` if unable to add the `auth_key` to the
/// database. For example, if the key already exist.
///
/// # Arguments
///
/// * `lifetime` - The duration in seconds for the new key. The key will be
/// no longer valid after `lifetime` seconds.
pub async fn add_auth_key(
&self,
key: Key,
valid_until: DurationSinceUnixEpoch,
) -> Result<auth::ExpiringKey, databases::error::Error> {
let auth_key = ExpiringKey { key, valid_until };

// code-review: should we return a friendly error instead of the DB
// constrain error when the key already exist? For now, it's returning
// the specif error for each DB driver when a UNIQUE constrain fails.
self.database.add_key_to_keys(&auth_key)?;
self.keys.write().await.insert(auth_key.key.clone(), auth_key.clone());
Ok(auth_key)
Expand All @@ -816,10 +848,6 @@ impl Tracker {
/// # Errors
///
/// Will return a `database::Error` if unable to remove the `key` to the database.
///
/// # Panics
///
/// Will panic if key cannot be converted into a valid `Key`.
pub async fn remove_auth_key(&self, key: &Key) -> Result<(), databases::error::Error> {
self.database.remove_key_from_keys(key)?;
self.keys.write().await.remove(key);
Expand Down
8 changes: 8 additions & 0 deletions src/servers/apis/v1/context/auth_key/forms.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct AddKeyForm {
#[serde(rename = "key")]
pub opt_key: Option<String>,
pub seconds_valid: u64,
}
55 changes: 53 additions & 2 deletions src/servers/apis/v1/context/auth_key/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,66 @@ use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

use axum::extract::{Path, State};
use axum::extract::{self, Path, State};
use axum::response::Response;
use serde::Deserialize;
use torrust_tracker_clock::clock::Time;

use super::forms::AddKeyForm;
use super::responses::{
auth_key_response, failed_to_delete_key_response, failed_to_generate_key_response, failed_to_reload_keys_response,
auth_key_response, failed_to_add_key_response, failed_to_delete_key_response, failed_to_generate_key_response,
failed_to_reload_keys_response, invalid_auth_key_duration_response, invalid_auth_key_response,
};
use crate::core::auth::Key;
use crate::core::Tracker;
use crate::servers::apis::v1::context::auth_key::resources::AuthKey;
use crate::servers::apis::v1::responses::{invalid_auth_key_param_response, ok_response};
use crate::CurrentClock;

/// It handles the request to add a new authentication key.
///
/// It returns these types of responses:
///
/// - `200` with a json [`AuthKey`]
/// resource. If the key was generated successfully.
/// - `400` with an error if the key couldn't been added because of an invalid
/// request.
/// - `500` with serialized error in debug format. If the key couldn't be
/// generated.
///
/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key#generate-a-new-authentication-key)
/// for more information about this endpoint.
pub async fn add_auth_key_handler(
State(tracker): State<Arc<Tracker>>,
extract::Json(add_key_form): extract::Json<AddKeyForm>,
) -> Response {
match add_key_form.opt_key {
Some(pre_existing_key) => {
let Some(valid_until) = CurrentClock::now_add(&Duration::from_secs(add_key_form.seconds_valid)) else {
return invalid_auth_key_duration_response(add_key_form.seconds_valid);
};

let key = pre_existing_key.parse::<Key>();

match key {
Ok(key) => match tracker.add_auth_key(key, valid_until).await {
Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)),
Err(e) => failed_to_add_key_response(e),
},
Err(e) => invalid_auth_key_response(&pre_existing_key, &e),
}
}
None => {
match tracker
.generate_auth_key(Duration::from_secs(add_key_form.seconds_valid))
.await
{
Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)),
Err(e) => failed_to_generate_key_response(e),
}
}
}
}

/// It handles the request to generate a new authentication key.
///
Expand All @@ -26,6 +75,8 @@ use crate::servers::apis::v1::responses::{invalid_auth_key_param_response, ok_re
///
/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key#generate-a-new-authentication-key)
/// for more information about this endpoint.
///
/// This endpoint has been deprecated. Use [`add_auth_key_handler`].
pub async fn generate_auth_key_handler(State(tracker): State<Arc<Tracker>>, Path(seconds_valid_or_key): Path<u64>) -> Response {
let seconds_valid = seconds_valid_or_key;
match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await {
Expand Down
22 changes: 16 additions & 6 deletions src/servers/apis/v1/context/auth_key/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
//! Authentication keys are used to authenticate HTTP tracker `announce` and
//! `scrape` requests.
//!
//! When the tracker is running in `private` or `private_listed` mode, the
//! authentication keys are required to announce and scrape torrents.
//! When the tracker is running in `private` mode, the authentication keys are
//! required to announce and scrape torrents.
//!
//! A sample `announce` request **without** authentication key:
//!
Expand All @@ -22,22 +22,31 @@
//!
//! # Generate a new authentication key
//!
//! `POST /key/:seconds_valid`
//! `POST /keys`
//!
//! It generates a new authentication key.
//! It generates a new authentication key or upload a pre-generated key.
//!
//! > **NOTICE**: keys expire after a certain amount of time.
//!
//! **Path parameters**
//! **POST parameters**
//!
//! Name | Type | Description | Required | Example
//! ---|---|---|---|---
//! `key` | 32-char string (0-9, a-z, A-Z) | The optional pre-generated key. | No | `Xc1L4PbQJSFGlrgSRZl8wxSFAuMa21z7`
//! `seconds_valid` | positive integer | The number of seconds the key will be valid. | Yes | `3600`
//!
//! > **NOTICE**: the `key` field is optional. If is not provided the tracker
//! > will generated a random one.
//!
//! **Example request**
//!
//! ```bash
//! curl -X POST "http://127.0.0.1:1212/api/v1/key/120?token=MyAccessToken"
//! curl -X POST http://localhost:1212/api/v1/keys?token=MyAccessToken \
//! -H "Content-Type: application/json" \
//! -d '{
//! "key": "xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6",
//! "seconds_valid": 7200
//! }'
//! ```
//!
//! **Example response** `200`
Expand Down Expand Up @@ -119,6 +128,7 @@
//! "status": "ok"
//! }
//! ```
pub mod forms;
pub mod handlers;
pub mod resources;
pub mod responses;
Expand Down
21 changes: 20 additions & 1 deletion src/servers/apis/v1/context/auth_key/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use std::error::Error;
use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response};

use crate::core::auth::ParseKeyError;
use crate::servers::apis::v1::context::auth_key::resources::AuthKey;
use crate::servers::apis::v1::responses::unhandled_rejection_response;
use crate::servers::apis::v1::responses::{bad_request_response, unhandled_rejection_response};

/// `200` response that contains the `AuthKey` resource as json.
///
Expand All @@ -22,12 +23,20 @@ pub fn auth_key_response(auth_key: &AuthKey) -> Response {
.into_response()
}

// Error responses

/// `500` error response when a new authentication key cannot be generated.
#[must_use]
pub fn failed_to_generate_key_response<E: Error>(e: E) -> Response {
unhandled_rejection_response(format!("failed to generate key: {e}"))
}

/// `500` error response when the provide key cannot be added.
#[must_use]
pub fn failed_to_add_key_response<E: Error>(e: E) -> Response {
unhandled_rejection_response(format!("failed to add key: {e}"))
}

/// `500` error response when an authentication key cannot be deleted.
#[must_use]
pub fn failed_to_delete_key_response<E: Error>(e: E) -> Response {
Expand All @@ -40,3 +49,13 @@ pub fn failed_to_delete_key_response<E: Error>(e: E) -> Response {
pub fn failed_to_reload_keys_response<E: Error>(e: E) -> Response {
unhandled_rejection_response(format!("failed to reload keys: {e}"))
}

#[must_use]
pub fn invalid_auth_key_response(auth_key: &str, error: &ParseKeyError) -> Response {
bad_request_response(&format!("Invalid URL: invalid auth key: string \"{auth_key}\", {error}"))
}

#[must_use]
pub fn invalid_auth_key_duration_response(duration: u64) -> Response {
bad_request_response(&format!("Invalid URL: invalid auth key duration: \"{duration}\""))
}
16 changes: 12 additions & 4 deletions src/servers/apis/v1/context/auth_key/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::sync::Arc;
use axum::routing::{get, post};
use axum::Router;

use super::handlers::{delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler};
use super::handlers::{add_auth_key_handler, delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler};
use crate::core::Tracker;

/// It adds the routes to the router for the [`auth_key`](crate::servers::apis::v1::context::auth_key) API context.
Expand All @@ -21,14 +21,22 @@ pub fn add(prefix: &str, router: Router, tracker: Arc<Tracker>) -> Router {
.route(
// code-review: Axum does not allow two routes with the same path but different path variable name.
// In the new major API version, `seconds_valid` should be a POST form field so that we will have two paths:
// POST /key
// DELETE /key/:key
//
// POST /keys
// DELETE /keys/:key
//
// The POST /key/:seconds_valid has been deprecated and it will removed in the future.
// Use POST /keys
&format!("{prefix}/key/:seconds_valid_or_key"),
post(generate_auth_key_handler)
.with_state(tracker.clone())
.delete(delete_auth_key_handler)
.with_state(tracker.clone()),
)
// Keys command
.route(&format!("{prefix}/keys/reload"), get(reload_keys_handler).with_state(tracker))
.route(
&format!("{prefix}/keys/reload"),
get(reload_keys_handler).with_state(tracker.clone()),
)
.route(&format!("{prefix}/keys"), post(add_auth_key_handler).with_state(tracker))
}
3 changes: 2 additions & 1 deletion src/servers/apis/v1/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ pub fn invalid_auth_key_param_response(invalid_key: &str) -> Response {
bad_request_response(&format!("Invalid auth key id param \"{invalid_key}\""))
}

fn bad_request_response(body: &str) -> Response {
#[must_use]
pub fn bad_request_response(body: &str) -> Response {
(
StatusCode::BAD_REQUEST,
[(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
Expand Down
35 changes: 34 additions & 1 deletion tests/servers/api/v1/asserts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ pub async fn assert_bad_request(response: Response, body: &str) {
assert_eq!(response.text().await.unwrap(), body);
}

pub async fn assert_unprocessable_content(response: Response, text: &str) {
assert_eq!(response.status(), 422);
assert_eq!(response.headers().get("content-type").unwrap(), "text/plain; charset=utf-8");
assert!(response.text().await.unwrap().contains(text));
}

pub async fn assert_not_found(response: Response) {
assert_eq!(response.status(), 404);
// todo: missing header in the response
Expand All @@ -82,10 +88,37 @@ pub async fn assert_invalid_infohash_param(response: Response, invalid_infohash:
.await;
}

pub async fn assert_invalid_auth_key_param(response: Response, invalid_auth_key: &str) {
pub async fn assert_invalid_auth_key_get_param(response: Response, invalid_auth_key: &str) {
assert_bad_request(response, &format!("Invalid auth key id param \"{}\"", &invalid_auth_key)).await;
}

pub async fn assert_invalid_auth_key_post_param(response: Response, invalid_auth_key: &str) {
assert_bad_request(
response,
&format!(
"Invalid URL: invalid auth key: string \"{}\", ParseKeyError",
&invalid_auth_key
),
)
.await;
}

pub async fn _assert_unprocessable_auth_key_param(response: Response, _invalid_value: &str) {
assert_unprocessable_content(
response,
"Failed to deserialize the JSON body into the target type: seconds_valid: invalid type",
)
.await;
}

pub async fn assert_unprocessable_auth_key_duration_param(response: Response, _invalid_value: &str) {
assert_unprocessable_content(
response,
"Failed to deserialize the JSON body into the target type: seconds_valid: invalid type",
)
.await;
}

pub async fn assert_invalid_key_duration_param(response: Response, invalid_key_duration: &str) {
assert_bad_request(
response,
Expand Down
Loading