From e7d344c5f8af51cfff7af4abf71db3a08f039096 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 22 Apr 2024 14:00:20 +0100 Subject: [PATCH] refactor: create new configuration v1 mod with figment - Clone config strcuctures into a new mod `v1`. - Introduce versioning for configuration API. - Split config sections into submodules. TODO: - Still using root mod types in production. - Not using figment to build config in production. --- .../configuration/src/v1/health_check_api.rs | 13 + packages/configuration/src/v1/http_tracker.rs | 23 + packages/configuration/src/v1/mod.rs | 433 ++++++++++++++++++ packages/configuration/src/v1/tracker_api.rs | 32 ++ packages/configuration/src/v1/udp_tracker.rs | 12 + 5 files changed, 513 insertions(+) create mode 100644 packages/configuration/src/v1/health_check_api.rs create mode 100644 packages/configuration/src/v1/http_tracker.rs create mode 100644 packages/configuration/src/v1/mod.rs create mode 100644 packages/configuration/src/v1/tracker_api.rs create mode 100644 packages/configuration/src/v1/udp_tracker.rs diff --git a/packages/configuration/src/v1/health_check_api.rs b/packages/configuration/src/v1/health_check_api.rs new file mode 100644 index 00000000..f7b15249 --- /dev/null +++ b/packages/configuration/src/v1/health_check_api.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +/// Configuration for the Health Check API. +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct HealthCheckApi { + /// The address the API will bind to. + /// The format is `ip:port`, for example `127.0.0.1:1313`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, +} diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs new file mode 100644 index 00000000..4c88feb9 --- /dev/null +++ b/packages/configuration/src/v1/http_tracker.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, NoneAsEmptyString}; + +/// Configuration for each HTTP tracker. +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct HttpTracker { + /// Weather the HTTP tracker is enabled or not. + pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, + /// Weather the HTTP tracker will use SSL or not. + pub ssl_enabled: bool, + /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_cert_path: Option, + /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_key_path: Option, +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs new file mode 100644 index 00000000..815d74e4 --- /dev/null +++ b/packages/configuration/src/v1/mod.rs @@ -0,0 +1,433 @@ +//! Configuration data structures for [Torrust Tracker](https://docs.rs/torrust-tracker). +//! +//! This module contains the configuration data structures for the +//! Torrust Tracker, which is a `BitTorrent` tracker server. +//! +//! The configuration is loaded from a [TOML](https://toml.io/en/) file +//! `tracker.toml` in the project root folder or from an environment variable +//! with the same content as the file. +//! +//! Configuration can not only be loaded from a file, but also from an +//! environment variable `TORRUST_TRACKER_CONFIG`. This is useful when running +//! the tracker in a Docker container or environments where you do not have a +//! persistent storage or you cannot inject a configuration file. Refer to +//! [`Torrust Tracker documentation`](https://docs.rs/torrust-tracker) for more +//! information about how to pass configuration to the tracker. +//! +//! When you run the tracker without providing the configuration via a file or +//! env var, the default configuration is used. +//! +//! # Table of contents +//! +//! - [Sections](#sections) +//! - [Port binding](#port-binding) +//! - [TSL support](#tsl-support) +//! - [Generating self-signed certificates](#generating-self-signed-certificates) +//! - [Default configuration](#default-configuration) +//! +//! ## Sections +//! +//! Each section in the toml structure is mapped to a data structure. For +//! example, the `[http_api]` section (configuration for the tracker HTTP API) +//! is mapped to the [`HttpApi`] structure. +//! +//! > **NOTICE**: some sections are arrays of structures. For example, the +//! > `[[udp_trackers]]` section is an array of [`UdpTracker`] since +//! > you can have multiple running UDP trackers bound to different ports. +//! +//! Please refer to the documentation of each structure for more information +//! about each section. +//! +//! - [`Core configuration`](crate::v1::Configuration) +//! - [`HTTP API configuration`](crate::v1::tracker_api::HttpApi) +//! - [`HTTP Tracker configuration`](crate::v1::http_tracker::HttpTracker) +//! - [`UDP Tracker configuration`](crate::v1::udp_tracker::UdpTracker) +//! - [`Health Check API configuration`](crate::v1::health_check_api::HealthCheckApi) +//! +//! ## Port binding +//! +//! For the API, HTTP and UDP trackers you can bind to a random port by using +//! port `0`. For example, if you want to bind to a random port on all +//! interfaces, use `0.0.0.0:0`. The OS will choose a random free port. +//! +//! ## TSL support +//! +//! For the API and HTTP tracker you can enable TSL by setting `ssl_enabled` to +//! `true` and setting the paths to the certificate and key files. +//! +//! Typically, you will have a `storage` directory like the following: +//! +//! ```text +//! storage/ +//! ├── config.toml +//! └── tracker +//! ├── etc +//! │ └── tracker.toml +//! ├── lib +//! │ ├── database +//! │ │ ├── sqlite3.db +//! │ │ └── sqlite.db +//! │ └── tls +//! │ ├── localhost.crt +//! │ └── localhost.key +//! └── log +//! ``` +//! +//! where the application stores all the persistent data. +//! +//! Alternatively, you could setup a reverse proxy like Nginx or Apache to +//! handle the SSL/TLS part and forward the requests to the tracker. If you do +//! that, you should set [`on_reverse_proxy`](crate::Configuration::on_reverse_proxy) +//! to `true` in the configuration file. It's out of scope for this +//! documentation to explain in detail how to setup a reverse proxy, but the +//! configuration file should be something like this: +//! +//! For [NGINX](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/): +//! +//! ```text +//! # HTTPS only (with SSL - force redirect to HTTPS) +//! +//! server { +//! listen 80; +//! server_name tracker.torrust.com; +//! +//! return 301 https://$host$request_uri; +//! } +//! +//! server { +//! listen 443; +//! server_name tracker.torrust.com; +//! +//! ssl_certificate CERT_PATH +//! ssl_certificate_key CERT_KEY_PATH; +//! +//! location / { +//! proxy_set_header X-Forwarded-For $remote_addr; +//! proxy_pass http://127.0.0.1:6969; +//! } +//! } +//! ``` +//! +//! For [Apache](https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html): +//! +//! ```text +//! # HTTPS only (with SSL - force redirect to HTTPS) +//! +//! +//! ServerAdmin webmaster@tracker.torrust.com +//! ServerName tracker.torrust.com +//! +//! +//! RewriteEngine on +//! RewriteCond %{HTTPS} off +//! RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] +//! +//! +//! +//! +//! +//! ServerAdmin webmaster@tracker.torrust.com +//! ServerName tracker.torrust.com +//! +//! +//! Order allow,deny +//! Allow from all +//! +//! +//! ProxyPreserveHost On +//! ProxyRequests Off +//! AllowEncodedSlashes NoDecode +//! +//! ProxyPass / http://localhost:3000/ +//! ProxyPassReverse / http://localhost:3000/ +//! ProxyPassReverse / http://tracker.torrust.com/ +//! +//! RequestHeader set X-Forwarded-Proto "https" +//! RequestHeader set X-Forwarded-Port "443" +//! +//! ErrorLog ${APACHE_LOG_DIR}/tracker.torrust.com-error.log +//! CustomLog ${APACHE_LOG_DIR}/tracker.torrust.com-access.log combined +//! +//! SSLCertificateFile CERT_PATH +//! SSLCertificateKeyFile CERT_KEY_PATH +//! +//! +//! ``` +//! +//! ## Generating self-signed certificates +//! +//! For testing purposes, you can use self-signed certificates. +//! +//! Refer to [Let's Encrypt - Certificates for localhost](https://letsencrypt.org/docs/certificates-for-localhost/) +//! for more information. +//! +//! Running the following command will generate a certificate (`localhost.crt`) +//! and key (`localhost.key`) file in your current directory: +//! +//! ```s +//! openssl req -x509 -out localhost.crt -keyout localhost.key \ +//! -newkey rsa:2048 -nodes -sha256 \ +//! -subj '/CN=localhost' -extensions EXT -config <( \ +//! printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") +//! ``` +//! +//! You can then use the generated files in the configuration file: +//! +//! ```s +//! [[http_trackers]] +//! enabled = true +//! ... +//! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" +//! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" +//! +//! [http_api] +//! enabled = true +//! ... +//! ssl_cert_path = "./storage/tracker/lib/tls/localhost.crt" +//! ssl_key_path = "./storage/tracker/lib/tls/localhost.key" +//! ``` +//! +//! ## Default configuration +//! +//! The default configuration is: +//! +//! ```toml +//! log_level = "info" +//! mode = "public" +//! db_driver = "Sqlite3" +//! db_path = "./storage/tracker/lib/database/sqlite3.db" +//! announce_interval = 120 +//! min_announce_interval = 120 +//! on_reverse_proxy = false +//! external_ip = "0.0.0.0" +//! tracker_usage_statistics = true +//! persistent_torrent_completed_stat = false +//! max_peer_timeout = 900 +//! inactive_peer_cleanup_interval = 600 +//! remove_peerless_torrents = true +//! +//! [[udp_trackers]] +//! enabled = false +//! bind_address = "0.0.0.0:6969" +//! +//! [[http_trackers]] +//! enabled = false +//! bind_address = "0.0.0.0:7070" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! +//! [http_api] +//! enabled = true +//! bind_address = "127.0.0.1:1212" +//! ssl_enabled = false +//! ssl_cert_path = "" +//! ssl_key_path = "" +//! +//! [http_api.access_tokens] +//! admin = "MyAccessToken" +//! [health_check_api] +//! bind_address = "127.0.0.1:1313" +//!``` +pub mod health_check_api; +pub mod http_tracker; +pub mod tracker_api; +pub mod udp_tracker; + +use serde::{Deserialize, Serialize}; +use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; + +use self::health_check_api::HealthCheckApi; +use self::http_tracker::HttpTracker; +use self::tracker_api::HttpApi; +use self::udp_tracker::UdpTracker; +use crate::AnnouncePolicy; + +/// Core configuration for the tracker. +#[allow(clippy::struct_excessive_bools)] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] +pub struct Configuration { + /// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`, + /// `Debug` and `Trace`. Default is `Info`. + pub log_level: Option, + /// Tracker mode. See [`TrackerMode`] for more information. + pub mode: TrackerMode, + + // Database configuration + /// Database driver. Possible values are: `Sqlite3`, and `MySQL`. + pub db_driver: DatabaseDriver, + /// Database connection string. The format depends on the database driver. + /// For `Sqlite3`, the format is `path/to/database.db`, for example: + /// `./storage/tracker/lib/database/sqlite3.db`. + /// For `Mysql`, the format is `mysql://db_user:db_user_password:port/db_name`, for + /// example: `root:password@localhost:3306/torrust`. + pub db_path: String, + + /// See [`AnnouncePolicy::interval`] + pub announce_interval: u32, + + /// See [`AnnouncePolicy::interval_min`] + pub min_announce_interval: u32, + /// Weather the tracker is behind a reverse proxy or not. + /// If the tracker is behind a reverse proxy, the `X-Forwarded-For` header + /// sent from the proxy will be used to get the client's IP address. + pub on_reverse_proxy: bool, + /// The external IP address of the tracker. If the client is using a + /// loopback IP address, this IP address will be used instead. If the peer + /// is using a loopback IP address, the tracker assumes that the peer is + /// in the same network as the tracker and will use the tracker's IP + /// address instead. + pub external_ip: Option, + /// Weather the tracker should collect statistics about tracker usage. + /// If enabled, the tracker will collect statistics like the number of + /// connections handled, the number of announce requests handled, etc. + /// Refer to the [`Tracker`](https://docs.rs/torrust-tracker) for more + /// information about the collected metrics. + pub tracker_usage_statistics: bool, + /// If enabled the tracker will persist the number of completed downloads. + /// That's how many times a torrent has been downloaded completely. + pub persistent_torrent_completed_stat: bool, + + // Cleanup job configuration + /// Maximum time in seconds that a peer can be inactive before being + /// considered an inactive peer. If a peer is inactive for more than this + /// time, it will be removed from the torrent peer list. + pub max_peer_timeout: u32, + /// Interval in seconds that the cleanup job will run to remove inactive + /// peers from the torrent peer list. + pub inactive_peer_cleanup_interval: u64, + /// If enabled, the tracker will remove torrents that have no peers. + /// The clean up torrent job runs every `inactive_peer_cleanup_interval` + /// seconds and it removes inactive peers. Eventually, the peer list of a + /// torrent could be empty and the torrent will be removed if this option is + /// enabled. + pub remove_peerless_torrents: bool, + + // Server jobs configuration + /// The list of UDP trackers the tracker is running. Each UDP tracker + /// represents a UDP server that the tracker is running and it has its own + /// configuration. + pub udp_trackers: Vec, + /// The list of HTTP trackers the tracker is running. Each HTTP tracker + /// represents a HTTP server that the tracker is running and it has its own + /// configuration. + pub http_trackers: Vec, + /// The HTTP API configuration. + pub http_api: HttpApi, + /// The Health Check API configuration. + pub health_check_api: HealthCheckApi, +} + +impl Default for Configuration { + fn default() -> Self { + let announce_policy = AnnouncePolicy::default(); + + let mut configuration = Configuration { + log_level: Option::from(String::from("info")), + mode: TrackerMode::Public, + db_driver: DatabaseDriver::Sqlite3, + db_path: String::from("./storage/tracker/lib/database/sqlite3.db"), + announce_interval: announce_policy.interval, + min_announce_interval: announce_policy.interval_min, + max_peer_timeout: 900, + on_reverse_proxy: false, + external_ip: Some(String::from("0.0.0.0")), + tracker_usage_statistics: true, + persistent_torrent_completed_stat: false, + inactive_peer_cleanup_interval: 600, + remove_peerless_torrents: true, + udp_trackers: Vec::new(), + http_trackers: Vec::new(), + http_api: HttpApi { + enabled: true, + bind_address: String::from("127.0.0.1:1212"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + access_tokens: [(String::from("admin"), String::from("MyAccessToken"))] + .iter() + .cloned() + .collect(), + }, + health_check_api: HealthCheckApi { + bind_address: String::from("127.0.0.1:1313"), + }, + }; + configuration.udp_trackers.push(UdpTracker { + enabled: false, + bind_address: String::from("0.0.0.0:6969"), + }); + configuration.http_trackers.push(HttpTracker { + enabled: false, + bind_address: String::from("0.0.0.0:7070"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + }); + configuration + } +} + +#[cfg(test)] +mod tests { + use figment::providers::{Format, Toml}; + use figment::Figment; + + use crate::v1::Configuration; + + #[test] + fn configuration_should_be_loaded_from_a_toml_config_file() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "Config.toml", + r#" + log_level = "info" + mode = "public" + db_driver = "Sqlite3" + db_path = "./storage/tracker/lib/database/sqlite3.db" + announce_interval = 120 + min_announce_interval = 120 + on_reverse_proxy = false + external_ip = "0.0.0.0" + tracker_usage_statistics = true + persistent_torrent_completed_stat = false + max_peer_timeout = 900 + inactive_peer_cleanup_interval = 600 + remove_peerless_torrents = true + + [[udp_trackers]] + enabled = false + bind_address = "0.0.0.0:6969" + + [[http_trackers]] + enabled = false + bind_address = "0.0.0.0:7070" + ssl_enabled = false + ssl_cert_path = "" + ssl_key_path = "" + + [http_api] + enabled = true + bind_address = "127.0.0.1:1212" + ssl_enabled = false + ssl_cert_path = "" + ssl_key_path = "" + + [http_api.access_tokens] + admin = "MyAccessToken" + + [health_check_api] + bind_address = "127.0.0.1:1313" + "#, + )?; + + let figment = Figment::new().merge(Toml::file("Config.toml")); + + let config: Configuration = figment.extract()?; + + assert_eq!(config, Configuration::default()); + + Ok(()) + }); + } +} diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs new file mode 100644 index 00000000..6cda9b43 --- /dev/null +++ b/packages/configuration/src/v1/tracker_api.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, NoneAsEmptyString}; + +pub type AccessTokens = HashMap; + +/// Configuration for the HTTP API. +#[serde_as] +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct HttpApi { + /// Weather the HTTP API is enabled or not. + pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, + /// Weather the HTTP API will use SSL or not. + pub ssl_enabled: bool, + /// Path to the SSL certificate file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_cert_path: Option, + /// Path to the SSL key file. Only used if `ssl_enabled` is `true`. + #[serde_as(as = "NoneAsEmptyString")] + pub ssl_key_path: Option, + /// Access tokens for the HTTP API. The key is a label identifying the + /// token and the value is the token itself. The token is used to + /// authenticate the user. All tokens are valid for all endpoints and have + /// the all permissions. + pub access_tokens: AccessTokens, +} diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs new file mode 100644 index 00000000..b304054c --- /dev/null +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -0,0 +1,12 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct UdpTracker { + /// Weather the UDP tracker is enabled or not. + pub enabled: bool, + /// The address the tracker will bind to. + /// The format is `ip:port`, for example `0.0.0.0:6969`. If you want to + /// listen to all interfaces, use `0.0.0.0`. If you want the operating + /// system to choose a random port, use port `0`. + pub bind_address: String, +}