diff --git a/cSpell.json b/cSpell.json index b0ad4caf7..e7c0166f8 100644 --- a/cSpell.json +++ b/cSpell.json @@ -16,6 +16,7 @@ "byteorder", "canonicalize", "canonicalized", + "certbot", "chrono", "clippy", "completei", diff --git a/src/lib.rs b/src/lib.rs index a460a28b8..3b9777b36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -315,7 +315,7 @@ //! Using `curl` you can create a 2-minute valid auth key: //! //! ```text -//! $ curl -X POST http://127.0.0.1:1212/api/v1/key/120?token=MyAccessToken +//! $ curl -X POST "http://127.0.0.1:1212/api/v1/key/120?token=MyAccessToken" //! ``` //! //! Response: @@ -329,7 +329,7 @@ //! ``` //! //! You can also use the Torrust Tracker together with the [Torrust Index](https://github.com/torrust/torrust-index). If that's the case, -//! the Index will create the keys by using the API. +//! the Index will create the keys by using the tracker [API](crate::servers::apis). //! //! ## UDP tracker usage //! diff --git a/src/servers/apis/mod.rs b/src/servers/apis/mod.rs index 1bc257916..203f1d146 100644 --- a/src/servers/apis/mod.rs +++ b/src/servers/apis/mod.rs @@ -1,8 +1,174 @@ +//! The tracker REST API with all its versions. +//! +//! > **NOTICE**: This API should not be exposed directly to the internet, it is +//! intended for internal use only. +//! +//! Endpoints for the latest API: [v1](crate::servers::apis::v1). +//! +//! All endpoints require an authorization token which must be set in the +//! configuration before running the tracker. The default configuration uses +//! `?token=MyAccessToken`. Refer to [Authentication](#authentication) for more +//! information. +//! +//! # Table of contents +//! +//! - [Configuration](#configuration) +//! - [Authentication](#authentication) +//! - [Versioning](#versioning) +//! - [Endpoints](#endpoints) +//! - [Documentation](#documentation) +//! +//! # Configuration +//! +//! The configuration file has a [`[http_api]`](torrust_tracker_configuration::HttpApi) +//! section that can be used to enable the API. +//! +//! ```toml +//! [http_api] +//! enabled = true +//! bind_address = "0.0.0.0:1212" +//! ssl_enabled = false +//! ssl_cert_path = "./storage/ssl_certificates/localhost.crt" +//! ssl_key_path = "./storage/ssl_certificates/localhost.key" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! ``` +//! +//! Refer to [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration>) +//! for more information about the API configuration. +//! +//! When you run the tracker with enabled API, you will see the following message: +//! +//! ```text +//! Loading configuration from config file ./config.toml +//! 023-03-28T12:19:24.963054069+01:00 [torrust_tracker::bootstrap::logging][INFO] logging initialized. +//! ... +//! 023-03-28T12:19:24.964138723+01:00 [torrust_tracker::bootstrap::jobs::tracker_apis][INFO] Starting Torrust APIs server on: http://0.0.0.0:1212 +//! ``` +//! +//! The API server will be available on the address specified in the configuration. +//! +//! You can test the API by loading the following URL on a browser: +//! +//! +//! +//! Or using `curl`: +//! +//! ```bash +//! $ curl -s "http://0.0.0.0:1212/api/v1/stats?token=MyAccessToken" +//! ``` +//! +//! The response will be a JSON object. For example, the [tracker statistics +//! endpoint](crate::servers::apis::v1::context::stats#get-tracker-statistics): +//! +//! ```json +//! { +//! "torrents": 0, +//! "seeders": 0, +//! "completed": 0, +//! "leechers": 0, +//! "tcp4_connections_handled": 0, +//! "tcp4_announces_handled": 0, +//! "tcp4_scrapes_handled": 0, +//! "tcp6_connections_handled": 0, +//! "tcp6_announces_handled": 0, +//! "tcp6_scrapes_handled": 0, +//! "udp4_connections_handled": 0, +//! "udp4_announces_handled": 0, +//! "udp4_scrapes_handled": 0, +//! "udp6_connections_handled": 0, +//! "udp6_announces_handled": 0, +//! "udp6_scrapes_handled": 0 +//! } +//! ``` +//! +//! # Authentication +//! +//! The API supports authentication using a GET parameter token. +//! +//! +//! +//! You can set as many tokens as you want in the configuration file: +//! +//! ```toml +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! ``` +//! +//! The token label is used to identify the token. All tokens have full access +//! to the API. +//! +//! Refer to [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration>) +//! for more information about the API configuration and to the +//! [`auth`](crate::servers::apis::v1::middlewares::auth) middleware for more +//! information about the authentication process. +//! +//! # Setup SSL (optional) +//! +//! The API server supports SSL. You can enable it by setting the +//! [`ssl_enabled`](torrust_tracker_configuration::HttpApi::ssl_enabled) option +//! to `true` in the configuration file +//! ([`http_api`](torrust_tracker_configuration::HttpApi) section). +//! +//! ```toml +//! [http_api] +//! enabled = true +//! bind_address = "0.0.0.0:1212" +//! ssl_enabled = true +//! ssl_cert_path = "./storage/ssl_certificates/localhost.crt" +//! ssl_key_path = "./storage/ssl_certificates/localhost.key" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! ``` +//! +//! > **NOTICE**: If you are using a reverse proxy like NGINX, you can skip this +//! step and use NGINX for the SSL instead. See +//! [other alternatives to Nginx/certbot](https://github.com/torrust/torrust-tracker/discussions/131) +//! +//! > **NOTICE**: You can generate a self-signed certificate for localhost using +//! OpenSSL. See [Let's Encrypt](https://letsencrypt.org/docs/certificates-for-localhost/). +//! That's particularly useful for testing purposes. Once you have the certificate +//! you need to set the [`ssl_cert_path`](torrust_tracker_configuration::HttpApi::ssl_cert_path) +//! and [`ssl_key_path`](torrust_tracker_configuration::HttpApi::ssl_key_path) +//! options in the configuration file with the paths to the certificate +//! (`localhost.crt`) and key (`localhost.key`) files. +//! +//! # Versioning +//! +//! The API is versioned and each version has its own module. +//! The API server runs all the API versions on the same server using +//! the same port. Currently there is only one API version: [v1](crate::servers::apis::v1) +//! but a version [`v2`](https://github.com/torrust/torrust-tracker/issues/144) +//! is planned. +//! +//! # Endpoints +//! +//! Refer to the [v1](crate::servers::apis::v1) module for the list of available +//! API endpoints. +//! +//! # Documentation +//! +//! If you want to contribute to this documentation you can [open a new pull request](https://github.com/torrust/torrust-tracker/pulls). +//! +//! > **NOTICE**: we are using [curl](https://curl.se/) in the API examples. +//! And you have to use quotes around the URL in order to avoid unexpected +//! errors. For example: `curl "http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken"`. pub mod routes; pub mod server; pub mod v1; use serde::Deserialize; +/// The info hash URL path parameter. +/// +/// Some API endpoints require an info hash as a path parameter. +/// +/// For example: `http://localhost:1212/api/v1/torrent/{info_hash}`. +/// +/// The info hash represents teh value collected from the URL path parameter. +/// It does not include validation as this is done by the API endpoint handler, +/// in order to provide a more specific error message. #[derive(Deserialize)] pub struct InfoHashParam(pub String); diff --git a/src/servers/apis/routes.rs b/src/servers/apis/routes.rs index 2545d6b88..a4c4642c7 100644 --- a/src/servers/apis/routes.rs +++ b/src/servers/apis/routes.rs @@ -1,11 +1,18 @@ +//! API routes. +//! +//! It loads all the API routes for all API versions and adds the authentication +//! middleware to them. +//! +//! All the API routes have the `/api` prefix and the version number as the +//! first path segment. For example: `/api/v1/torrents`. use std::sync::Arc; use axum::{middleware, Router}; use super::v1; -use super::v1::middlewares::auth::auth; use crate::tracker::Tracker; +/// Add all API routes to the router. #[allow(clippy::needless_pass_by_value)] pub fn router(tracker: Arc) -> Router { let router = Router::new(); @@ -14,5 +21,8 @@ pub fn router(tracker: Arc) -> Router { let router = v1::routes::add(prefix, router, tracker.clone()); - router.layer(middleware::from_fn_with_state(tracker.config.clone(), auth)) + router.layer(middleware::from_fn_with_state( + tracker.config.clone(), + v1::middlewares::auth::auth, + )) } diff --git a/src/servers/apis/server.rs b/src/servers/apis/server.rs index e4714cd9a..76396cc51 100644 --- a/src/servers/apis/server.rs +++ b/src/servers/apis/server.rs @@ -1,3 +1,28 @@ +//! Logic to run the HTTP API server. +//! +//! It contains two main structs: `ApiServer` and `Launcher`, +//! and two main functions: `start` and `start_tls`. +//! +//! The `ApiServer` struct is responsible for: +//! - Starting and stopping the server. +//! - Storing the configuration. +//! +//! `ApiServer` relies on a launcher to start the actual server. +/// +/// 1. `ApiServer::start` -> spawns new asynchronous task. +/// 2. `Launcher::start` -> starts the server on the spawned task. +/// +/// The `Launcher` struct is responsible for: +/// +/// - Knowing how to start the server with graceful shutdown. +/// +/// For the time being the `ApiServer` and `Launcher` are only used in tests +/// where we need to start and stop the server multiple times. In production +/// code and the main application uses the `start` and `start_tls` functions +/// to start the servers directly since we do not need to control the server +/// when it's running. In the future we might need to control the server, +/// for example, to restart it to apply new configuration changes, to remotely +/// shutdown the server, etc. use std::net::SocketAddr; use std::str::FromStr; use std::sync::Arc; @@ -12,24 +37,35 @@ use super::routes::router; use crate::servers::signals::shutdown_signal; use crate::tracker::Tracker; +/// Errors that can occur when starting or stopping the API server. #[derive(Debug)] pub enum Error { Error(String), } +/// An alias for the `ApiServer` struct with the `Stopped` state. #[allow(clippy::module_name_repetitions)] pub type StoppedApiServer = ApiServer; + +/// An alias for the `ApiServer` struct with the `Running` state. #[allow(clippy::module_name_repetitions)] pub type RunningApiServer = ApiServer; +/// A struct responsible for starting and stopping an API server with a +/// specific configuration and keeping track of the started server. +/// +/// It's a state machine that can be in one of two +/// states: `Stopped` or `Running`. #[allow(clippy::module_name_repetitions)] pub struct ApiServer { pub cfg: torrust_tracker_configuration::HttpApi, pub state: S, } +/// The `Stopped` state of the `ApiServer` struct. pub struct Stopped; +/// The `Running` state of the `ApiServer` struct. pub struct Running { pub bind_addr: SocketAddr, task_killer: tokio::sync::oneshot::Sender, @@ -42,6 +78,8 @@ impl ApiServer { Self { cfg, state: Stopped {} } } + /// Starts the API server with the given configuration. + /// /// # Errors /// /// It would return an error if no `SocketAddr` is returned after launching the server. @@ -75,6 +113,8 @@ impl ApiServer { } impl ApiServer { + /// Stops the API server. + /// /// # Errors /// /// It would return an error if the channel for the task killer signal was closed. @@ -93,9 +133,15 @@ impl ApiServer { } } +/// A struct responsible for starting the API server. struct Launcher; impl Launcher { + /// Starts the API server with graceful shutdown. + /// + /// If TLS is enabled in the configuration, it will start the server with + /// TLS. See [torrust-tracker-configuration](https://docs.rs/torrust-tracker-configuration>) + /// for more information about configuration. pub fn start( cfg: &torrust_tracker_configuration::HttpApi, tracker: Arc, @@ -126,6 +172,7 @@ impl Launcher { } } + /// Starts the API server with graceful shutdown. pub fn start_with_graceful_shutdown( tcp_listener: std::net::TcpListener, tracker: Arc, @@ -146,6 +193,7 @@ impl Launcher { }) } + /// Starts the API server with graceful shutdown and TLS. pub fn start_tls_with_graceful_shutdown( tcp_listener: std::net::TcpListener, (ssl_cert_path, ssl_key_path): (String, String), @@ -180,6 +228,7 @@ impl Launcher { } } +/// Starts the API server with graceful shutdown on the current thread. pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl Future> { let app = router(tracker); @@ -191,6 +240,7 @@ pub fn start(socket_addr: SocketAddr, tracker: Arc) -> impl Future>, Path(seconds_valid_or_key): Path) -> Response { let seconds_valid = seconds_valid_or_key; match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await { @@ -22,9 +34,35 @@ pub async fn generate_auth_key_handler(State(tracker): State>, Path } } +/// A container for the `key` parameter extracted from the URL PATH. +/// +/// It does not perform any validation, it just stores the value. +/// +/// In the current API version, the `key` parameter can be either a valid key +/// like `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6` or the number of seconds the +/// key will be valid, for example two minutes `120`. +/// +/// For example, the `key` is used in the following requests: +/// +/// - `POST /api/v1/key/120`. It will generate a new key valid for two minutes. +/// - `DELETE /api/v1/key/xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. It will delete the +/// key `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6`. +/// +/// > **NOTICE**: this may change in the future, in the [API v2](https://github.com/torrust/torrust-tracker/issues/144). #[derive(Deserialize)] pub struct KeyParam(String); +/// It handles the request to delete an authentication key. +/// +/// It returns two types of responses: +/// +/// - `200` with an json [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) +/// response. If the key was deleted successfully. +/// - `500` with serialized error in debug format. If the key couldn't be +/// deleted. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key#delete-an-authentication-key) +/// for more information about this endpoint. pub async fn delete_auth_key_handler( State(tracker): State>, Path(seconds_valid_or_key): Path, @@ -38,6 +76,18 @@ pub async fn delete_auth_key_handler( } } +/// It handles the request to reload the authentication keys from the database +/// into memory. +/// +/// It returns two types of responses: +/// +/// - `200` with an json [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) +/// response. If the keys were successfully reloaded. +/// - `500` with serialized error in debug format. If the they couldn't be +/// reloaded. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key#reload-authentication-keys) +/// for more information about this endpoint. pub async fn reload_keys_handler(State(tracker): State>) -> Response { match tracker.load_keys_from_database().await { Ok(_) => ok_response(), diff --git a/src/servers/apis/v1/context/auth_key/mod.rs b/src/servers/apis/v1/context/auth_key/mod.rs index 746a2f064..11bc8a43f 100644 --- a/src/servers/apis/v1/context/auth_key/mod.rs +++ b/src/servers/apis/v1/context/auth_key/mod.rs @@ -1,3 +1,124 @@ +//! Authentication keys API context. +//! +//! 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. +//! +//! A sample `announce` request **without** authentication key: +//! +//! +//! +//! A sample `announce` request **with** authentication key: +//! +//! +//! +//! # Endpoints +//! +//! - [Generate a new authentication key](#generate-a-new-authentication-key) +//! - [Delete an authentication key](#delete-an-authentication-key) +//! - [Reload authentication keys](#reload-authentication-keys) +//! +//! # Generate a new authentication key +//! +//! `POST /key/:seconds_valid` +//! +//! It generates a new authentication key. +//! +//! > **NOTICE**: keys expire after a certain amount of time. +//! +//! **Path parameters** +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `seconds_valid` | positive integer | The number of seconds the key will be valid. | Yes | `3600` +//! +//! **Example request** +//! +//! ```bash +//! curl -X POST "http://127.0.0.1:1212/api/v1/key/120?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "key": "xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6", +//! "valid_until": 1680009900, +//! "expiry_time": "2023-03-28 13:25:00.058085050 UTC" +//! } +//! ``` +//! +//! > **NOTICE**: `valid_until` and `expiry_time` represent the same time. +//! `valid_until` is the number of seconds since the Unix epoch +//! ([timestamp](https://en.wikipedia.org/wiki/Timestamp)), while `expiry_time` +//! is the human-readable time ([ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html)). +//! +//! **Resource** +//! +//! Refer to the API [`AuthKey`](crate::servers::apis::v1::context::auth_key::resources::AuthKey) +//! resource for more information about the response attributes. +//! +//! # Delete an authentication key +//! +//! `DELETE /key/:key` +//! +//! It deletes a previously generated authentication key. +//! +//! **Path parameters** +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `key` | 40-char string | The `key` to remove. | Yes | `xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6` +//! +//! **Example request** +//! +//! ```bash +//! curl -X DELETE "http://127.0.0.1:1212/api/v1/key/xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "status": "ok" +//! } +//! ``` +//! +//! It you try to delete a non-existent key, the response will be an error with +//! a `500` status code. +//! +//! **Example error response** `500` +//! +//! ```text +//! Unhandled rejection: Err { reason: "failed to delete key: Failed to remove record from Sqlite3 database, error-code: 0, src/tracker/databases/sqlite.rs:267:27" } +//! ``` +//! +//! > **NOTICE**: a `500` status code will be returned and the body is not a +//! valid JSON. It's a text body containing the serialized-to-display error +//! message. +//! +//! # Reload authentication keys +//! +//! `GET /keys/reload` +//! +//! The tracker persists the authentication keys in a database. This endpoint +//! reloads the keys from the database. +//! +//! **Example request** +//! +//! ```bash +//! curl "http://127.0.0.1:1212/api/v1/keys/reload?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "status": "ok" +//! } +//! ``` pub mod handlers; pub mod resources; pub mod responses; diff --git a/src/servers/apis/v1/context/auth_key/resources.rs b/src/servers/apis/v1/context/auth_key/resources.rs index 400b34eb7..3eeafbda0 100644 --- a/src/servers/apis/v1/context/auth_key/resources.rs +++ b/src/servers/apis/v1/context/auth_key/resources.rs @@ -1,3 +1,4 @@ +//! API resources for the [`auth_key`](crate::servers::apis::v1::context::auth_key) API context. use std::convert::From; use serde::{Deserialize, Serialize}; @@ -5,10 +6,15 @@ use serde::{Deserialize, Serialize}; use crate::shared::clock::convert_from_iso_8601_to_timestamp; use crate::tracker::auth::{self, Key}; +/// A resource that represents an authentication key. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct AuthKey { + /// The authentication key. pub key: String, + /// The timestamp when the key will expire. + #[deprecated(since = "3.0.0", note = "please use `expiry_time` instead")] pub valid_until: u64, // todo: remove when the torrust-index-backend starts using the `expiry_time` attribute. + /// The ISO 8601 timestamp when the key will expire. pub expiry_time: String, } diff --git a/src/servers/apis/v1/context/auth_key/responses.rs b/src/servers/apis/v1/context/auth_key/responses.rs index 4e3b0c711..51be162c5 100644 --- a/src/servers/apis/v1/context/auth_key/responses.rs +++ b/src/servers/apis/v1/context/auth_key/responses.rs @@ -1,3 +1,4 @@ +//! API responses for the [`auth_key`](crate::servers::apis::v1::context::auth_key) API context. use std::error::Error; use axum::http::{header, StatusCode}; @@ -6,6 +7,8 @@ use axum::response::{IntoResponse, Response}; use crate::servers::apis::v1::context::auth_key::resources::AuthKey; use crate::servers::apis::v1::responses::unhandled_rejection_response; +/// `200` response that contains the `AuthKey` resource as json. +/// /// # Panics /// /// Will panic if it can't convert the `AuthKey` resource to json @@ -19,16 +22,20 @@ pub fn auth_key_response(auth_key: &AuthKey) -> Response { .into_response() } +/// `500` error response when a new authentication key cannot be generated. #[must_use] pub fn failed_to_generate_key_response(e: E) -> Response { unhandled_rejection_response(format!("failed to generate key: {e}")) } +/// `500` error response when an authentication key cannot be deleted. #[must_use] pub fn failed_to_delete_key_response(e: E) -> Response { unhandled_rejection_response(format!("failed to delete key: {e}")) } +/// `500` error response when the authentication keys cannot be reloaded from +/// the database into memory. #[must_use] pub fn failed_to_reload_keys_response(e: E) -> Response { unhandled_rejection_response(format!("failed to reload keys: {e}")) diff --git a/src/servers/apis/v1/context/auth_key/routes.rs b/src/servers/apis/v1/context/auth_key/routes.rs index 9b155c2a5..76c634e21 100644 --- a/src/servers/apis/v1/context/auth_key/routes.rs +++ b/src/servers/apis/v1/context/auth_key/routes.rs @@ -1,3 +1,11 @@ +//! API routes for the [`auth_key`](crate::servers::apis::v1::context::auth_key) +//! API context. +//! +//! - `POST /key/:seconds_valid` +//! - `DELETE /key/:key` +//! - `GET /keys/reload` +//! +//! Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key). use std::sync::Arc; use axum::routing::{get, post}; @@ -6,6 +14,7 @@ use axum::Router; use super::handlers::{delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler}; use crate::tracker::Tracker; +/// It adds the routes to the router for the [`auth_key`](crate::servers::apis::v1::context::auth_key) API context. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { // Keys router diff --git a/src/servers/apis/v1/context/mod.rs b/src/servers/apis/v1/context/mod.rs index 6d3fb7566..5e268a429 100644 --- a/src/servers/apis/v1/context/mod.rs +++ b/src/servers/apis/v1/context/mod.rs @@ -1,3 +1,7 @@ +//! API is organized in resource groups called contexts. +//! +//! Each context is a module that contains the API endpoints related to a +//! specific resource group. pub mod auth_key; pub mod stats; pub mod torrent; diff --git a/src/servers/apis/v1/context/stats/handlers.rs b/src/servers/apis/v1/context/stats/handlers.rs index e93e65996..dfb983f77 100644 --- a/src/servers/apis/v1/context/stats/handlers.rs +++ b/src/servers/apis/v1/context/stats/handlers.rs @@ -1,3 +1,5 @@ +//! API handlers for the [`stats`](crate::servers::apis::v1::context::stats) +//! API context. use std::sync::Arc; use axum::extract::State; @@ -8,6 +10,12 @@ use super::responses::stats_response; use crate::tracker::services::statistics::get_metrics; use crate::tracker::Tracker; +/// It handles the request to get the tracker statistics. +/// +/// It returns a `200` response with a json [`Stats`](crate::servers::apis::v1::context::stats::resources::Stats) +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::stats#get-tracker-statistics) +/// for more information about this endpoint. pub async fn get_stats_handler(State(tracker): State>) -> Json { stats_response(get_metrics(tracker.clone()).await) } diff --git a/src/servers/apis/v1/context/stats/mod.rs b/src/servers/apis/v1/context/stats/mod.rs index 746a2f064..80f37f73f 100644 --- a/src/servers/apis/v1/context/stats/mod.rs +++ b/src/servers/apis/v1/context/stats/mod.rs @@ -1,3 +1,51 @@ +//! Tracker statistics API context. +//! +//! The tracker collects statistics about the number of torrents, seeders, +//! leechers, completed downloads, and the number of requests handled. +//! +//! # Endpoints +//! +//! - [Get tracker statistics](#get-tracker-statistics) +//! +//! # Get tracker statistics +//! +//! `GET /stats` +//! +//! Returns the tracker statistics. +//! +//! **Example request** +//! +//! ```bash +//! curl "http://127.0.0.1:1212/api/v1/stats?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "torrents": 0, +//! "seeders": 0, +//! "completed": 0, +//! "leechers": 0, +//! "tcp4_connections_handled": 0, +//! "tcp4_announces_handled": 0, +//! "tcp4_scrapes_handled": 0, +//! "tcp6_connections_handled": 0, +//! "tcp6_announces_handled": 0, +//! "tcp6_scrapes_handled": 0, +//! "udp4_connections_handled": 0, +//! "udp4_announces_handled": 0, +//! "udp4_scrapes_handled": 0, +//! "udp6_connections_handled": 0, +//! "udp6_announces_handled": 0, +//! "udp6_scrapes_handled": 0 +//! } +//! ``` +//! +//! **Resource** +//! +//! Refer to the API [`Stats`](crate::servers::apis::v1::context::stats::resources::Stats) +//! resource for more information about the response attributes. pub mod handlers; pub mod resources; pub mod responses; diff --git a/src/servers/apis/v1/context/stats/resources.rs b/src/servers/apis/v1/context/stats/resources.rs index 44ac814dc..355a1e448 100644 --- a/src/servers/apis/v1/context/stats/resources.rs +++ b/src/servers/apis/v1/context/stats/resources.rs @@ -1,24 +1,48 @@ +//! API resources for the [`stats`](crate::servers::apis::v1::context::stats) +//! API context. use serde::{Deserialize, Serialize}; use crate::tracker::services::statistics::TrackerMetrics; +/// It contains all the statistics generated by the tracker. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Stats { + // Torrent metrics + /// Total number of torrents. pub torrents: u64, + /// Total number of seeders for all torrents. pub seeders: u64, + /// Total number of peers that have ever completed downloading for all torrents. pub completed: u64, + /// Total number of leechers for all torrents. pub leechers: u64, + + // Protocol metrics + /// Total number of TCP (HTTP tracker) connections from IPv4 peers. + /// Since the HTTP tracker spec does not require a handshake, this metric + /// increases for every HTTP request. pub tcp4_connections_handled: u64, + /// Total number of TCP (HTTP tracker) `announce` requests from IPv4 peers. pub tcp4_announces_handled: u64, + /// Total number of TCP (HTTP tracker) `scrape` requests from IPv4 peers. pub tcp4_scrapes_handled: u64, + /// Total number of TCP (HTTP tracker) connections from IPv6 peers. pub tcp6_connections_handled: u64, + /// Total number of TCP (HTTP tracker) `announce` requests from IPv6 peers. pub tcp6_announces_handled: u64, + /// Total number of TCP (HTTP tracker) `scrape` requests from IPv6 peers. pub tcp6_scrapes_handled: u64, + /// Total number of UDP (UDP tracker) connections from IPv4 peers. pub udp4_connections_handled: u64, + /// Total number of UDP (UDP tracker) `announce` requests from IPv4 peers. pub udp4_announces_handled: u64, + /// Total number of UDP (UDP tracker) `scrape` requests from IPv4 peers. pub udp4_scrapes_handled: u64, + /// Total number of UDP (UDP tracker) `connection` requests from IPv6 peers. pub udp6_connections_handled: u64, + /// Total number of UDP (UDP tracker) `announce` requests from IPv6 peers. pub udp6_announces_handled: u64, + /// Total number of UDP (UDP tracker) `scrape` requests from IPv6 peers. pub udp6_scrapes_handled: u64, } diff --git a/src/servers/apis/v1/context/stats/responses.rs b/src/servers/apis/v1/context/stats/responses.rs index ea9a2480a..a4dad77e4 100644 --- a/src/servers/apis/v1/context/stats/responses.rs +++ b/src/servers/apis/v1/context/stats/responses.rs @@ -1,8 +1,11 @@ +//! API responses for the [`stats`](crate::servers::apis::v1::context::stats) +//! API context. use axum::response::Json; use super::resources::Stats; use crate::tracker::services::statistics::TrackerMetrics; +/// `200` response that contains the [`Stats`](crate::servers::apis::v1::context::stats::resources::Stats) resource as json. pub fn stats_response(tracker_metrics: TrackerMetrics) -> Json { Json(Stats::from(tracker_metrics)) } diff --git a/src/servers/apis/v1/context/stats/routes.rs b/src/servers/apis/v1/context/stats/routes.rs index 07f88aa70..9198562dd 100644 --- a/src/servers/apis/v1/context/stats/routes.rs +++ b/src/servers/apis/v1/context/stats/routes.rs @@ -1,3 +1,8 @@ +//! API routes for the [`stats`](crate::servers::apis::v1::context::stats) API context. +//! +//! - `GET /stats` +//! +//! Refer to the [API endpoint documentation](crate::servers::apis::v1::context::stats). use std::sync::Arc; use axum::routing::get; @@ -6,6 +11,7 @@ use axum::Router; use super::handlers::get_stats_handler; use crate::tracker::Tracker; +/// It adds the routes to the router for the [`stats`](crate::servers::apis::v1::context::stats) API context. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { router.route(&format!("{prefix}/stats"), get(get_stats_handler).with_state(tracker)) } diff --git a/src/servers/apis/v1/context/torrent/handlers.rs b/src/servers/apis/v1/context/torrent/handlers.rs index 4032f2e9a..002d4356e 100644 --- a/src/servers/apis/v1/context/torrent/handlers.rs +++ b/src/servers/apis/v1/context/torrent/handlers.rs @@ -1,9 +1,12 @@ +//! API handlers for the [`torrent`](crate::servers::apis::v1::context::torrent) +//! API context. use std::fmt; use std::str::FromStr; use std::sync::Arc; use axum::extract::{Path, Query, State}; use axum::response::{IntoResponse, Json, Response}; +use log::debug; use serde::{de, Deserialize, Deserializer}; use super::resources::torrent::ListItem; @@ -14,6 +17,15 @@ use crate::shared::bit_torrent::info_hash::InfoHash; use crate::tracker::services::torrent::{get_torrent_info, get_torrents, Pagination}; use crate::tracker::Tracker; +/// It handles the request to get the torrent data. +/// +/// It returns: +/// +/// - `200` response with a json [`Torrent`](crate::servers::apis::v1::context::torrent::resources::torrent::Torrent). +/// - `500` with serialized error in debug format if the torrent is not known. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::torrent#get-a-torrent) +/// for more information about this endpoint. pub async fn get_torrent_handler(State(tracker): State>, Path(info_hash): Path) -> Response { match InfoHash::from_str(&info_hash.0) { Err(_) => invalid_info_hash_param_response(&info_hash.0), @@ -24,17 +36,32 @@ pub async fn get_torrent_handler(State(tracker): State>, Path(info_ } } -#[derive(Deserialize)] +/// A container for the optional URL query pagination parameters: +/// `offset` and `limit`. +#[derive(Deserialize, Debug)] pub struct PaginationParams { + /// The offset of the first page to return. Starts at 0. #[serde(default, deserialize_with = "empty_string_as_none")] pub offset: Option, + /// The maximum number of items to return per page + #[serde(default, deserialize_with = "empty_string_as_none")] pub limit: Option, } +/// It handles the request to get a list of torrents. +/// +/// It returns a `200` response with a json array with +/// [`ListItem`](crate::servers::apis::v1::context::torrent::resources::torrent::ListItem) +/// resources. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::torrent#list-torrents) +/// for more information about this endpoint. pub async fn get_torrents_handler( State(tracker): State>, pagination: Query, ) -> Json> { + debug!("pagination: {:?}", pagination); + torrent_list_response( &get_torrents( tracker.clone(), diff --git a/src/servers/apis/v1/context/torrent/mod.rs b/src/servers/apis/v1/context/torrent/mod.rs index 746a2f064..1658e1748 100644 --- a/src/servers/apis/v1/context/torrent/mod.rs +++ b/src/servers/apis/v1/context/torrent/mod.rs @@ -1,3 +1,112 @@ +//! Torrents API context. +//! +//! This API context is responsible for handling all the requests related to +//! the torrents data stored by the tracker. +//! +//! # Endpoints +//! +//! - [Get a torrent](#get-a-torrent) +//! - [List torrents](#list-torrents) +//! +//! # Get a torrent +//! +//! `GET /torrent/:info_hash` +//! +//! Returns all the information about a torrent. +//! +//! **Path parameters** +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `info_hash` | 40-char string | The Info Hash v1 | Yes | `5452869be36f9f3350ccee6b4544e7e76caaadab` +//! +//! **Example request** +//! +//! ```bash +//! curl "http://127.0.0.1:1212/api/v1/torrent/5452869be36f9f3350ccee6b4544e7e76caaadab?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "info_hash": "5452869be36f9f3350ccee6b4544e7e76caaadab", +//! "seeders": 1, +//! "completed": 0, +//! "leechers": 0, +//! "peers": [ +//! { +//! "peer_id": { +//! "id": "0x2d7142343431302d2a64465a3844484944704579", +//! "client": "qBittorrent" +//! }, +//! "peer_addr": "192.168.1.88:17548", +//! "updated": 1680082693001, +//! "updated_milliseconds_ago": 1680082693001, +//! "uploaded": 0, +//! "downloaded": 0, +//! "left": 0, +//! "event": "None" +//! } +//! ] +//! } +//! ``` +//! +//! **Not Found response** `200` +//! +//! This response is returned when the tracker does not have the torrent. +//! +//! ```json +//! "torrent not known" +//! ``` +//! +//! **Resource** +//! +//! Refer to the API [`Torrent`](crate::servers::apis::v1::context::torrent::resources::torrent::Torrent) +//! resource for more information about the response attributes. +//! +//! # List torrents +//! +//! `GET /torrents` +//! +//! Returns basic information (no peer list) for all torrents. +//! +//! **Query parameters** +//! +//! The endpoint supports pagination. +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `offset` | positive integer | The page number, starting at 0 | No | `1` +//! `limit` | positive integer | Page size. The number of results per page | No | `10` +//! +//! **Example request** +//! +//! ```bash +//! curl "http://127.0.0.1:1212/api/v1/torrents?token=MyAccessToken&offset=1&limit=1" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! [ +//! { +//! "info_hash": "5452869be36f9f3350ccee6b4544e7e76caaadab", +//! "seeders": 1, +//! "completed": 0, +//! "leechers": 0, +//! "peers": null +//! } +//! ] +//! ``` +//! +//! **Resource** +//! +//! Refer to the API [`ListItem`](crate::servers::apis::v1::context::torrent::resources::torrent::ListItem) +//! resource for more information about the attributes for a single item in the +//! response. +//! +//! > **NOTICE**: this endpoint does not include the `peers` list. pub mod handlers; pub mod resources; pub mod responses; diff --git a/src/servers/apis/v1/context/torrent/resources/mod.rs b/src/servers/apis/v1/context/torrent/resources/mod.rs index 46d62aac5..a6dbff726 100644 --- a/src/servers/apis/v1/context/torrent/resources/mod.rs +++ b/src/servers/apis/v1/context/torrent/resources/mod.rs @@ -1,2 +1,4 @@ +//! API resources for the [`torrent`](crate::servers::apis::v1::context::torrent) +//! API context. pub mod peer; pub mod torrent; diff --git a/src/servers/apis/v1/context/torrent/resources/peer.rs b/src/servers/apis/v1/context/torrent/resources/peer.rs index 5284d26f6..539637b35 100644 --- a/src/servers/apis/v1/context/torrent/resources/peer.rs +++ b/src/servers/apis/v1/context/torrent/resources/peer.rs @@ -1,23 +1,37 @@ +//! `Peer` and Peer `Id` API resources. use serde::{Deserialize, Serialize}; use crate::tracker; +/// `Peer` API resource. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Peer { + /// The peer's ID. See [`Id`](crate::servers::apis::v1::context::torrent::resources::peer::Id). pub peer_id: Id, + /// The peer's socket address. For example: `192.168.1.88:17548`. pub peer_addr: String, + /// The peer's last update time in milliseconds. #[deprecated(since = "2.0.0", note = "please use `updated_milliseconds_ago` instead")] pub updated: u128, + /// The peer's last update time in milliseconds. pub updated_milliseconds_ago: u128, + /// The peer's uploaded bytes. pub uploaded: i64, + /// The peer's downloaded bytes. pub downloaded: i64, + /// The peer's left bytes (pending to download). pub left: i64, + /// The peer's event: `started`, `stopped`, `completed`. + /// See [`AnnounceEventDef`](crate::shared::bit_torrent::common::AnnounceEventDef). pub event: String, } +/// Peer `Id` API resource. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Id { + /// The peer's ID in hex format. For example: `0x2d7142343431302d2a64465a3844484944704579`. pub id: Option, + /// The peer's client name. For example: `qBittorrent`. pub client: Option, } diff --git a/src/servers/apis/v1/context/torrent/resources/torrent.rs b/src/servers/apis/v1/context/torrent/resources/torrent.rs index e328f80c4..c9dbd1c02 100644 --- a/src/servers/apis/v1/context/torrent/resources/torrent.rs +++ b/src/servers/apis/v1/context/torrent/resources/torrent.rs @@ -1,26 +1,52 @@ +//! `Torrent` and `ListItem` API resources. +//! +//! - `Torrent` is the full torrent resource. +//! - `ListItem` is a list item resource on a torrent list. `ListItem` does +//! include a `peers` field but it is always `None` in the struct and `null` in +//! the JSON response. use serde::{Deserialize, Serialize}; use super::peer; use crate::tracker::services::torrent::{BasicInfo, Info}; +/// `Torrent` API resource. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct Torrent { + /// The torrent's info hash v1. pub info_hash: String, + /// The torrent's seeders counter. Active peers with a full copy of the + /// torrent. pub seeders: u64, + /// The torrent's completed counter. Peers that have ever completed the + /// download. pub completed: u64, + /// The torrent's leechers counter. Active peers that are downloading the + /// torrent. pub leechers: u64, + /// The torrent's peers. See [`Peer`](crate::servers::apis::v1::context::torrent::resources::peer::Peer). #[serde(skip_serializing_if = "Option::is_none")] pub peers: Option>, } +/// `ListItem` API resource. A list item on a torrent list. +/// `ListItem` does include a `peers` field but it is always `None` in the +/// struct and `null` in the JSON response. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct ListItem { + /// The torrent's info hash v1. pub info_hash: String, + /// The torrent's seeders counter. Active peers with a full copy of the + /// torrent. pub seeders: u64, + /// The torrent's completed counter. Peers that have ever completed the + /// download. pub completed: u64, + /// The torrent's leechers counter. Active peers that are downloading the + /// torrent. pub leechers: u64, - // todo: this is always None. Remove field from endpoint? - pub peers: Option>, + /// The torrent's peers. It's always `None` in the struct and `null` in the + /// JSON response. + pub peers: Option>, // todo: this is always None. Remove field from endpoint? } impl ListItem { @@ -33,6 +59,8 @@ impl ListItem { } } +/// Maps an array of the domain type [`BasicInfo`](crate::tracker::services::torrent::BasicInfo) +/// to the API resource type [`ListItem`](crate::servers::apis::v1::context::torrent::resources::torrent::ListItem). #[must_use] pub fn to_resource(basic_info_vec: &[BasicInfo]) -> Vec { basic_info_vec diff --git a/src/servers/apis/v1/context/torrent/responses.rs b/src/servers/apis/v1/context/torrent/responses.rs index 48e3c6e7f..d3be092eb 100644 --- a/src/servers/apis/v1/context/torrent/responses.rs +++ b/src/servers/apis/v1/context/torrent/responses.rs @@ -1,17 +1,26 @@ +//! API responses for the [`torrent`](crate::servers::apis::v1::context::torrent) +//! API context. use axum::response::{IntoResponse, Json, Response}; use serde_json::json; use super::resources::torrent::{ListItem, Torrent}; use crate::tracker::services::torrent::{BasicInfo, Info}; +/// `200` response that contains an array of +/// [`ListItem`](crate::servers::apis::v1::context::torrent::resources::torrent::ListItem) +/// resources as json. pub fn torrent_list_response(basic_infos: &[BasicInfo]) -> Json> { Json(ListItem::new_vec(basic_infos)) } +/// `200` response that contains a +/// [`Torrent`](crate::servers::apis::v1::context::torrent::resources::torrent::Torrent) +/// resources as json. pub fn torrent_info_response(info: Info) -> Json { Json(Torrent::from(info)) } +/// `500` error response in plain text returned when a torrent is not found. #[must_use] pub fn torrent_not_known_response() -> Response { Json(json!("torrent not known")).into_response() diff --git a/src/servers/apis/v1/context/torrent/routes.rs b/src/servers/apis/v1/context/torrent/routes.rs index 00faa9665..18295f2a2 100644 --- a/src/servers/apis/v1/context/torrent/routes.rs +++ b/src/servers/apis/v1/context/torrent/routes.rs @@ -1,3 +1,9 @@ +//! API routes for the [`torrent`](crate::servers::apis::v1::context::torrent) API context. +//! +//! - `GET /torrent/:info_hash` +//! - `GET /torrents` +//! +//! Refer to the [API endpoint documentation](crate::servers::apis::v1::context::torrent). use std::sync::Arc; use axum::routing::get; @@ -6,6 +12,7 @@ use axum::Router; use super::handlers::{get_torrent_handler, get_torrents_handler}; use crate::tracker::Tracker; +/// It adds the routes to the router for the [`torrent`](crate::servers::apis::v1::context::torrent) API context. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { // Torrents router diff --git a/src/servers/apis/v1/context/whitelist/handlers.rs b/src/servers/apis/v1/context/whitelist/handlers.rs index 25e285c0b..8e8c20b50 100644 --- a/src/servers/apis/v1/context/whitelist/handlers.rs +++ b/src/servers/apis/v1/context/whitelist/handlers.rs @@ -1,3 +1,5 @@ +//! API handlers for the [`whitelist`](crate::servers::apis::v1::context::whitelist) +//! API context. use std::str::FromStr; use std::sync::Arc; @@ -12,6 +14,15 @@ use crate::servers::apis::InfoHashParam; use crate::shared::bit_torrent::info_hash::InfoHash; use crate::tracker::Tracker; +/// It handles the request to add a torrent to the whitelist. +/// +/// It returns: +/// +/// - `200` response with a [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) in json. +/// - `500` with serialized error in debug format if the torrent couldn't be whitelisted. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::whitelist#add-a-torrent-to-the-whitelist) +/// for more information about this endpoint. pub async fn add_torrent_to_whitelist_handler( State(tracker): State>, Path(info_hash): Path, @@ -25,6 +36,16 @@ pub async fn add_torrent_to_whitelist_handler( } } +/// It handles the request to remove a torrent to the whitelist. +/// +/// It returns: +/// +/// - `200` response with a [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) in json. +/// - `500` with serialized error in debug format if the torrent couldn't be +/// removed from the whitelisted. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::whitelist#remove-a-torrent-from-the-whitelist) +/// for more information about this endpoint. pub async fn remove_torrent_from_whitelist_handler( State(tracker): State>, Path(info_hash): Path, @@ -38,6 +59,16 @@ pub async fn remove_torrent_from_whitelist_handler( } } +/// It handles the request to reload the torrent whitelist from the database. +/// +/// It returns: +/// +/// - `200` response with a [`ActionStatus::Ok`](crate::servers::apis::v1::responses::ActionStatus::Ok) in json. +/// - `500` with serialized error in debug format if the torrent whitelist +/// couldn't be reloaded from the database. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::whitelist#reload-the-whitelist) +/// for more information about this endpoint. pub async fn reload_whitelist_handler(State(tracker): State>) -> Response { match tracker.load_whitelist_from_database().await { Ok(_) => ok_response(), diff --git a/src/servers/apis/v1/context/whitelist/mod.rs b/src/servers/apis/v1/context/whitelist/mod.rs index f6f000f34..2bb35ef65 100644 --- a/src/servers/apis/v1/context/whitelist/mod.rs +++ b/src/servers/apis/v1/context/whitelist/mod.rs @@ -1,3 +1,98 @@ +//! Whitelist API context. +//! +//! This API context is responsible for handling all the requests related to +//! the torrent whitelist. +//! +//! A torrent whitelist is a list of Info Hashes that are allowed to be tracked +//! by the tracker. This is useful when you want to limit the torrents that are +//! tracked by the tracker. +//! +//! Common tracker requests like `announce` and `scrape` are limited to the +//! torrents in the whitelist. The whitelist can be updated using the API. +//! +//! > **NOTICE**: the whitelist is only used when the tracker is configured to +//! in `listed` or `private_listed` modes. Refer to the +//! [configuration crate documentation](https://docs.rs/torrust-tracker-configuration) +//! to know how to enable the those modes. +//! +//! > **NOTICE**: if the tracker is not running in `listed` or `private_listed` +//! modes the requests to the whitelist API will be ignored. +//! +//! # Endpoints +//! +//! - [Add a torrent to the whitelist](#add-a-torrent-to-the-whitelist) +//! - [Remove a torrent from the whitelist](#remove-a-torrent-from-the-whitelist) +//! - [Reload the whitelist](#reload-the-whitelist) +//! +//! # Add a torrent to the whitelist +//! +//! `POST /whitelist/:info_hash` +//! +//! It adds a torrent infohash to the whitelist. +//! +//! **Path parameters** +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `info_hash` | 40-char string | The Info Hash v1 | Yes | `5452869be36f9f3350ccee6b4544e7e76caaadab` +//! +//! **Example request** +//! +//! ```bash +//! curl -X POST "http://127.0.0.1:1212/api/v1/whitelist/5452869be36f9f3350ccee6b4544e7e76caaadab?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "status": "ok" +//! } +//! ``` +//! +//! # Remove a torrent from the whitelist +//! +//! `DELETE /whitelist/:info_hash` +//! +//! It removes a torrent infohash to the whitelist. +//! +//! **Path parameters** +//! +//! Name | Type | Description | Required | Example +//! ---|---|---|---|--- +//! `info_hash` | 40-char string | The Info Hash v1 | Yes | `5452869be36f9f3350ccee6b4544e7e76caaadab` +//! +//! **Example request** +//! +//! ```bash +//! curl -X DELETE "http://127.0.0.1:1212/api/v1/whitelist/5452869be36f9f3350ccee6b4544e7e76caaadab?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "status": "ok" +//! } +//! ``` +//! +//! # Reload the whitelist +//! +//! It reloads the whitelist from the database. +//! +//! **Example request** +//! +//! ```bash +//! curl "http://127.0.0.1:1212/api/v1/whitelist/reload?token=MyAccessToken" +//! ``` +//! +//! **Example response** `200` +//! +//! ```json +//! { +//! "status": "ok" +//! } +//! ``` pub mod handlers; pub mod responses; pub mod routes; diff --git a/src/servers/apis/v1/context/whitelist/responses.rs b/src/servers/apis/v1/context/whitelist/responses.rs index 06d4a9448..ce901c2f0 100644 --- a/src/servers/apis/v1/context/whitelist/responses.rs +++ b/src/servers/apis/v1/context/whitelist/responses.rs @@ -1,19 +1,24 @@ +//! API responses for the [`whitelist`](crate::servers::apis::v1::context::whitelist) +//! API context. use std::error::Error; use axum::response::Response; use crate::servers::apis::v1::responses::unhandled_rejection_response; +/// `500` error response when a torrent cannot be removed from the whitelist. #[must_use] pub fn failed_to_remove_torrent_from_whitelist_response(e: E) -> Response { unhandled_rejection_response(format!("failed to remove torrent from whitelist: {e}")) } +/// `500` error response when a torrent cannot be added to the whitelist. #[must_use] pub fn failed_to_whitelist_torrent_response(e: E) -> Response { unhandled_rejection_response(format!("failed to whitelist torrent: {e}")) } +/// `500` error response when the whitelist cannot be reloaded from the database. #[must_use] pub fn failed_to_reload_whitelist_response(e: E) -> Response { unhandled_rejection_response(format!("failed to reload whitelist: {e}")) diff --git a/src/servers/apis/v1/context/whitelist/routes.rs b/src/servers/apis/v1/context/whitelist/routes.rs index 06011b462..65d511341 100644 --- a/src/servers/apis/v1/context/whitelist/routes.rs +++ b/src/servers/apis/v1/context/whitelist/routes.rs @@ -1,3 +1,10 @@ +//! API routes for the [`whitelist`](crate::servers::apis::v1::context::whitelist) API context. +//! +//! - `POST /whitelist/:info_hash` +//! - `DELETE /whitelist/:info_hash` +//! - `GET /whitelist/reload` +//! +//! Refer to the [API endpoint documentation](crate::servers::apis::v1::context::torrent). use std::sync::Arc; use axum::routing::{delete, get, post}; @@ -6,6 +13,7 @@ use axum::Router; use super::handlers::{add_torrent_to_whitelist_handler, reload_whitelist_handler, remove_torrent_from_whitelist_handler}; use crate::tracker::Tracker; +/// It adds the routes to the router for the [`whitelist`](crate::servers::apis::v1::context::whitelist) API context. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { let prefix = format!("{prefix}/whitelist"); diff --git a/src/servers/apis/v1/middlewares/auth.rs b/src/servers/apis/v1/middlewares/auth.rs index f0c63250b..608a1b7d2 100644 --- a/src/servers/apis/v1/middlewares/auth.rs +++ b/src/servers/apis/v1/middlewares/auth.rs @@ -1,3 +1,26 @@ +//! Authentication middleware for the API. +//! +//! It uses a "token" GET param to authenticate the user. URLs must be of the +//! form: +//! +//! `http://:/api/v1/?token=`. +//! +//! > **NOTICE**: the token can be at any position in the URL, not just at the +//! > beginning or at the end. +//! +//! The token must be one of the `access_tokens` in the tracker +//! [HTTP API configuration](torrust_tracker_configuration::HttpApi). +//! +//! The configuration file `config.toml` contains a list of tokens: +//! +//! ```toml +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! ``` +//! +//! All the tokes have the same permissions, so it is not possible to have +//! different permissions for different tokens. The label is only used to +//! identify the token. use std::sync::Arc; use axum::extract::{Query, State}; @@ -9,13 +32,14 @@ use torrust_tracker_configuration::{Configuration, HttpApi}; use crate::servers::apis::v1::responses::unhandled_rejection_response; +/// Container for the `token` extracted from the query params. #[derive(Deserialize, Debug)] pub struct QueryParams { pub token: Option, } /// Middleware for authentication using a "token" GET param. -/// The token must be one of the tokens in the tracker HTTP API configuration. +/// The token must be one of the tokens in the tracker [HTTP API configuration](torrust_tracker_configuration::HttpApi). pub async fn auth( State(config): State>, Query(params): Query, @@ -35,7 +59,9 @@ where } enum AuthError { + /// Missing token for authentication. Unauthorized, + /// Token was provided but it is not valid. TokenNotValid, } @@ -52,11 +78,13 @@ fn authenticate(token: &str, http_api_config: &HttpApi) -> bool { http_api_config.contains_token(token) } +/// `500` error response returned when the token is missing. #[must_use] pub fn unauthorized_response() -> Response { unhandled_rejection_response("unauthorized".to_string()) } +/// `500` error response when the provided token is not valid. #[must_use] pub fn token_not_valid_response() -> Response { unhandled_rejection_response("token not valid".to_string()) diff --git a/src/servers/apis/v1/middlewares/mod.rs b/src/servers/apis/v1/middlewares/mod.rs index 0e4a05d59..141e3038a 100644 --- a/src/servers/apis/v1/middlewares/mod.rs +++ b/src/servers/apis/v1/middlewares/mod.rs @@ -1 +1,2 @@ +//! API middlewares. See [Axum middlewares](axum::middleware). pub mod auth; diff --git a/src/servers/apis/v1/mod.rs b/src/servers/apis/v1/mod.rs index e87984b8e..213ee9335 100644 --- a/src/servers/apis/v1/mod.rs +++ b/src/servers/apis/v1/mod.rs @@ -1,3 +1,21 @@ +//! The API version `v1`. +//! +//! The API is organized in the following contexts: +//! +//! Context | Description | Version +//! ---|---|--- +//! `Stats` | Tracker statistics | [`v1`](crate::servers::apis::v1::context::stats) +//! `Torrents` | Torrents | [`v1`](crate::servers::apis::v1::context::torrent) +//! `Whitelist` | Torrents whitelist | [`v1`](crate::servers::apis::v1::context::whitelist) +//! `Authentication keys` | Authentication keys | [`v1`](crate::servers::apis::v1::context::auth_key) +//! +//! > **NOTICE**: +//! - The authentication keys are only used by the HTTP tracker. +//! - The whitelist is only used when the tracker is running in `listed` or +//! `private_listed` mode. +//! +//! Refer to the [authentication middleware](crate::servers::apis::v1::middlewares::auth) +//! for more information about the authentication process. pub mod context; pub mod middlewares; pub mod responses; diff --git a/src/servers/apis/v1/responses.rs b/src/servers/apis/v1/responses.rs index 4a9c39bf9..ecaf90098 100644 --- a/src/servers/apis/v1/responses.rs +++ b/src/servers/apis/v1/responses.rs @@ -1,3 +1,4 @@ +//! Common responses for the API v1 shared by all the contexts. use axum::http::{header, StatusCode}; use axum::response::{IntoResponse, Response}; use serde::Serialize; @@ -22,6 +23,8 @@ use serde::Serialize; We can put the second level of validation in the application and domain services. */ +/// Response status used when requests have only two possible results +/// `Ok` or `Error` and no data is returned. #[derive(Serialize, Debug)] #[serde(tag = "status", rename_all = "snake_case")] pub enum ActionStatus<'a> { diff --git a/src/servers/apis/v1/routes.rs b/src/servers/apis/v1/routes.rs index d45319c4b..7b792f8a8 100644 --- a/src/servers/apis/v1/routes.rs +++ b/src/servers/apis/v1/routes.rs @@ -1,3 +1,4 @@ +//! Route initialization for the v1 API. use std::sync::Arc; use axum::Router; @@ -5,6 +6,12 @@ use axum::Router; use super::context::{auth_key, stats, torrent, whitelist}; use crate::tracker::Tracker; +/// Add the routes for the v1 API. +/// +/// > **NOTICE**: the old API endpoints without `v1` prefix are kept for +/// backward compatibility. For example, the `GET /api/stats` endpoint is +/// still available, but it is deprecated and will be removed in the future. +/// You should use the `GET /api/v1/stats` endpoint instead. pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { // Without `v1` prefix. // We keep the old API endpoints without `v1` prefix for backward compatibility. diff --git a/src/servers/http/mod.rs b/src/servers/http/mod.rs index b8aa6b19f..4212f86c4 100644 --- a/src/servers/http/mod.rs +++ b/src/servers/http/mod.rs @@ -1,4 +1,4 @@ -//! Tracker HTTP/HTTPS Protocol: +//! Tracker HTTP/HTTPS Protocol. //! //! Original specification in BEP 3 (section "Trackers"): //! diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs index faabbe095..dd2e94660 100644 --- a/src/tracker/mod.rs +++ b/src/tracker/mod.rs @@ -79,7 +79,8 @@ //! //! let peer_ip = IpAddr::V4(Ipv4Addr::from_str("126.0.0.1").unwrap()); //! ``` -//! ```rust,ignore +//! +//! ```text //! let announce_data = tracker.announce(&info_hash, &mut peer, &peer_ip).await; //! ``` //! @@ -412,7 +413,7 @@ //! //! For example, the HTTP tracker would send an event like the following when it handles an `announce` request received from a peer using IP version 4. //! -//! ```rust,ignore +//! ```text //! tracker.send_stats_event(statistics::Event::Tcp4Announce).await //! ``` //! diff --git a/src/tracker/services/statistics/mod.rs b/src/tracker/services/statistics/mod.rs index ac3ba510e..3761e38de 100644 --- a/src/tracker/services/statistics/mod.rs +++ b/src/tracker/services/statistics/mod.rs @@ -12,7 +12,7 @@ //! - An statistics [`EventSender`](crate::tracker::statistics::EventSender) //! - An statistics [`Repo`](crate::tracker::statistics::Repo) //! -//! ```rust,ignore +//! ```text //! let (stats_event_sender, stats_repository) = factory(tracker_usage_statistics); //! ``` //! @@ -23,7 +23,7 @@ //! //! For example, if you send the event [`Event::Udp4Connect`](crate::tracker::statistics::Event::Udp4Connect): //! -//! ```rust,ignore +//! ```text //! let result = event_sender.send_event(Event::Udp4Connect).await; //! ``` //! diff --git a/src/tracker/statistics.rs b/src/tracker/statistics.rs index 03f4fc081..85cc4f255 100644 --- a/src/tracker/statistics.rs +++ b/src/tracker/statistics.rs @@ -62,17 +62,31 @@ pub enum Event { /// and also for each IP version used by the peers: IPv4 and IPv6. #[derive(Debug, PartialEq, Default)] pub struct Metrics { + /// Total number of TCP (HTTP tracker) connections from IPv4 peers. + /// Since the HTTP tracker spec does not require a handshake, this metric + /// increases for every HTTP request. pub tcp4_connections_handled: u64, + /// Total number of TCP (HTTP tracker) `announce` requests from IPv4 peers. pub tcp4_announces_handled: u64, + /// Total number of TCP (HTTP tracker) `scrape` requests from IPv4 peers. pub tcp4_scrapes_handled: u64, + /// Total number of TCP (HTTP tracker) connections from IPv6 peers. pub tcp6_connections_handled: u64, + /// Total number of TCP (HTTP tracker) `announce` requests from IPv6 peers. pub tcp6_announces_handled: u64, + /// Total number of TCP (HTTP tracker) `scrape` requests from IPv6 peers. pub tcp6_scrapes_handled: u64, + /// Total number of UDP (UDP tracker) connections from IPv4 peers. pub udp4_connections_handled: u64, + /// Total number of UDP (UDP tracker) `announce` requests from IPv4 peers. pub udp4_announces_handled: u64, + /// Total number of UDP (UDP tracker) `scrape` requests from IPv4 peers. pub udp4_scrapes_handled: u64, + /// Total number of UDP (UDP tracker) `connection` requests from IPv6 peers. pub udp6_connections_handled: u64, + /// Total number of UDP (UDP tracker) `announce` requests from IPv6 peers. pub udp6_announces_handled: u64, + /// Total number of UDP (UDP tracker) `scrape` requests from IPv6 peers. pub udp6_scrapes_handled: u64, }