diff --git a/Cargo.lock b/Cargo.lock index bd0e36c3a..0bbf0205a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,6 +366,15 @@ dependencies = [ "syn 2.0.61", ] +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -578,9 +587,6 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -dependencies = [ - "serde", -] [[package]] name = "bitvec" @@ -696,6 +702,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytemuck" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" + [[package]] name = "byteorder" version = "1.5.0" @@ -883,61 +895,12 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "config" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" -dependencies = [ - "async-trait", - "convert_case 0.6.0", - "json5", - "lazy_static", - "nom", - "pathdiff", - "ron", - "rust-ini", - "serde", - "serde_json", - "toml", - "yaml-rust", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom", - "once_cell", - "tiny-keccak", -] - [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -1156,7 +1119,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -1184,15 +1147,6 @@ dependencies = [ "crypto-common", ] -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - [[package]] name = "downcast" version = "0.11.0" @@ -1334,6 +1288,22 @@ dependencies = [ "log", ] +[[package]] +name = "figment" +version = "0.10.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953" +dependencies = [ + "atomic", + "parking_lot", + "pear", + "serde", + "tempfile", + "toml", + "uncased", + "version_check", +] + [[package]] name = "flate2" version = "1.0.30" @@ -1877,6 +1847,12 @@ dependencies = [ "serde", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "instant" version = "0.1.12" @@ -1971,17 +1947,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "kv-log-macro" version = "1.0.7" @@ -2114,12 +2079,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -2511,16 +2470,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-multimap" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" -dependencies = [ - "dlv-list", - "hashbrown 0.13.2", -] - [[package]] name = "parking" version = "2.2.0" @@ -2551,10 +2500,27 @@ dependencies = [ ] [[package]] -name = "pathdiff" -version = "0.2.1" +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.61", +] [[package]] name = "pem" @@ -2572,51 +2538,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pest" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" -dependencies = [ - "memchr", - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.61", -] - -[[package]] -name = "pest_meta" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "phf" version = "0.11.2" @@ -2853,6 +2774,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.61", + "version_check", + "yansi", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -3129,18 +3063,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags 2.5.0", - "serde", - "serde_derive", -] - [[package]] name = "rstest" version = "0.19.0" @@ -3184,16 +3106,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "rust-ini" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rust_decimal" version = "1.35.0" @@ -3814,15 +3726,6 @@ dependencies = [ "time-core", ] -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -3979,11 +3882,11 @@ dependencies = [ "axum-server", "chrono", "clap", - "config", "crossbeam-skiplist", "dashmap", "derive_more", "fern", + "figment", "futures", "hex-literal", "hyper", @@ -4035,8 +3938,8 @@ dependencies = [ name = "torrust-tracker-configuration" version = "3.0.0-alpha.12-develop" dependencies = [ - "config", "derive_more", + "figment", "serde", "serde_with", "thiserror", @@ -4217,10 +4120,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "ucd-trie" -version = "0.1.6" +name = "uncased" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] [[package]] name = "unicode-bidi" @@ -4243,12 +4149,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" - [[package]] name = "untrusted" version = "0.9.0" @@ -4624,13 +4524,10 @@ dependencies = [ ] [[package]] -name = "yaml-rust" -version = "0.4.5" +name = "yansi" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" diff --git a/Cargo.toml b/Cargo.toml index cbfdc7697..d7aa9a31c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,11 +39,11 @@ axum-extra = { version = "0", features = ["query"] } axum-server = { version = "0", features = ["tls-rustls"] } chrono = { version = "0", default-features = false, features = ["clock"] } clap = { version = "4", features = ["derive", "env"] } -config = "0" crossbeam-skiplist = "0.1" dashmap = "5.5.3" derive_more = "0" fern = "0" +figment = "0.10.18" futures = "0" hex-literal = "0" hyper = "1" @@ -79,7 +79,7 @@ uuid = { version = "1", features = ["v4"] } zerocopy = "0.7.33" [package.metadata.cargo-machete] -ignored = ["serde_bytes", "crossbeam-skiplist", "dashmap", "parking_lot"] +ignored = ["crossbeam-skiplist", "dashmap", "figment", "parking_lot", "serde_bytes"] [dev-dependencies] local-ip-address = "0" @@ -93,7 +93,7 @@ members = [ "packages/located-error", "packages/primitives", "packages/test-helpers", - "packages/torrent-repository" + "packages/torrent-repository", ] [profile.dev] @@ -107,5 +107,5 @@ lto = "fat" opt-level = 3 [profile.release-debug] -inherits = "release" debug = true +inherits = "release" diff --git a/packages/configuration/Cargo.toml b/packages/configuration/Cargo.toml index 102177816..a033dcea1 100644 --- a/packages/configuration/Cargo.toml +++ b/packages/configuration/Cargo.toml @@ -15,8 +15,8 @@ rust-version.workspace = true version.workspace = true [dependencies] -config = "0" derive_more = "0" +figment = { version = "0.10.18", features = ["env", "test", "toml"] } serde = { version = "1", features = ["derive"] } serde_with = "3" thiserror = "1" diff --git a/packages/configuration/src/lib.rs b/packages/configuration/src/lib.rs index ca873f3cd..20912990a 100644 --- a/packages/configuration/src/lib.rs +++ b/packages/configuration/src/lib.rs @@ -3,249 +3,28 @@ //! 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. -//! -//! When you run the tracker without a configuration file, a new one will be -//! created with the default values, but the tracker immediately exits. You can -//! then edit the configuration file and run the tracker again. -//! -//! Configuration can not only be loaded from a file, but also from 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. -//! -//! # 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::Configuration) -//! - [`HTTP API configuration`](crate::HttpApi) -//! - [`HTTP Tracker configuration`](crate::HttpTracker) -//! - [`UDP Tracker configuration`](crate::UdpTracker) -//! -//! ## 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 port but the -//! tracker will not print the port it is listening to when it starts. It just -//! says `Starting Torrust HTTP tracker server on: http://0.0.0.0:0`. It shows -//! the port used in the configuration file, and not the port the -//! tracker is actually listening to. This is a planned feature, see issue -//! [186](https://github.com/torrust/torrust-tracker/issues/186) for more -//! information. -//! -//! ## 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 directory structure like this: -//! -//! ```text -//! storage/ -//! ├── database -//! │ └── data.db -//! └── tls -//! ├── localhost.crt -//! └── localhost.key -//! ``` -//! -//! where you can store 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 -//! announce_interval = 120 -//! db_driver = "Sqlite3" -//! db_path = "./storage/tracker/lib/database/sqlite3.db" -//! external_ip = "0.0.0.0" -//! inactive_peer_cleanup_interval = 600 -//! log_level = "info" -//! max_peer_timeout = 900 -//! min_announce_interval = 120 -//! mode = "public" -//! on_reverse_proxy = false -//! persistent_torrent_completed_stat = false -//! remove_peerless_torrents = true -//! tracker_usage_statistics = true -//! -//! [[udp_trackers]] -//! bind_address = "0.0.0.0:6969" -//! enabled = false -//! -//! [[http_trackers]] -//! bind_address = "0.0.0.0:7070" -//! enabled = false -//! ssl_cert_path = "" -//! ssl_enabled = false -//! ssl_key_path = "" -//! -//! [http_api] -//! bind_address = "127.0.0.1:1212" -//! enabled = true -//! ssl_cert_path = "" -//! ssl_enabled = false -//! ssl_key_path = "" -//! -//! [http_api.access_tokens] -//! admin = "MyAccessToken" -//! -//! [health_check_api] -//! bind_address = "127.0.0.1:1313" -//!``` +//! The current version for configuration is [`v1`](crate::v1). +pub mod v1; + use std::collections::HashMap; -use std::net::IpAddr; -use std::str::FromStr; use std::sync::Arc; use std::{env, fs}; -use config::{Config, ConfigError, File, FileFormat}; use derive_more::Constructor; -use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, NoneAsEmptyString}; use thiserror::Error; -use torrust_tracker_located_error::{DynError, Located, LocatedError}; -use torrust_tracker_primitives::{DatabaseDriver, TrackerMode}; +use torrust_tracker_located_error::{DynError, LocatedError}; /// The maximum number of returned peers for a torrent. pub const TORRENT_PEERS_LIMIT: usize = 74; +pub type Configuration = v1::Configuration; +pub type UdpTracker = v1::udp_tracker::UdpTracker; +pub type HttpTracker = v1::http_tracker::HttpTracker; +pub type HttpApi = v1::tracker_api::HttpApi; +pub type HealthCheckApi = v1::health_check_api::HealthCheckApi; + +pub type AccessTokens = HashMap; + #[derive(Copy, Clone, Debug, PartialEq, Constructor)] pub struct TrackerPolicy { pub remove_peerless_torrents: bool, @@ -263,15 +42,6 @@ pub struct Info { impl Info { /// Build Configuration Info /// - /// # Examples - /// - /// ``` - /// use torrust_tracker_configuration::Info; - /// - /// let result = Info::new(env_var_config, env_var_path_config, default_path_config, env_var_api_admin_token); - /// assert_eq!(result, ); - /// ``` - /// /// # Errors /// /// Will return `Err` if unable to obtain a configuration. @@ -314,84 +84,6 @@ impl Info { } } -/// Configuration for each UDP tracker. -#[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, -} - -/// 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, -} - -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, -} - -impl HttpApi { - fn override_admin_token(&mut self, api_admin_token: &str) { - self.access_tokens.insert("admin".to_string(), api_admin_token.to_string()); - } -} - -/// 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, -} - /// Announce policy #[derive(PartialEq, Eq, Debug, Clone, Copy, Constructor)] pub struct AnnouncePolicy { @@ -431,81 +123,6 @@ impl Default for 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, -} - /// Errors that can occur when loading the configuration. #[derive(Error, Debug)] pub enum Error { @@ -524,291 +141,19 @@ pub enum Error { /// Unable to load the configuration from the configuration file. #[error("Failed processing the configuration: {source}")] - ConfigError { source: LocatedError<'static, ConfigError> }, + ConfigError { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, #[error("The error for errors that can never happen.")] Infallible, } -impl From for Error { +impl From for Error { #[track_caller] - fn from(err: ConfigError) -> Self { + fn from(err: figment::Error) -> Self { Self::ConfigError { - source: Located(err).into(), + source: (Arc::new(err) as DynError).into(), } } } - -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 - } -} - -impl Configuration { - fn override_api_admin_token(&mut self, api_admin_token: &str) { - self.http_api.override_admin_token(api_admin_token); - } - - /// Returns the tracker public IP address id defined in the configuration, - /// and `None` otherwise. - #[must_use] - pub fn get_ext_ip(&self) -> Option { - match &self.external_ip { - None => None, - Some(external_ip) => match IpAddr::from_str(external_ip) { - Ok(external_ip) => Some(external_ip), - Err(_) => None, - }, - } - } - - /// Loads the configuration from the configuration file. - /// - /// # Errors - /// - /// Will return `Err` if `path` does not exist or has a bad configuration. - pub fn load_from_file(path: &str) -> Result { - let config_builder = Config::builder(); - - #[allow(unused_assignments)] - let mut config = Config::default(); - - config = config_builder.add_source(File::with_name(path)).build()?; - - let torrust_config: Configuration = config.try_deserialize()?; - - Ok(torrust_config) - } - - /// Saves the default configuration at the given path. - /// - /// # Errors - /// - /// Will return `Err` if `path` is not a valid path or the configuration - /// file cannot be created. - pub fn create_default_configuration_file(path: &str) -> Result { - let config = Configuration::default(); - config.save_to_file(path)?; - Ok(config) - } - - /// Loads the configuration from the `Info` struct. The whole - /// configuration in toml format is included in the `info.tracker_toml` string. - /// - /// Optionally will override the admin api token. - /// - /// # Errors - /// - /// Will return `Err` if the environment variable does not exist or has a bad configuration. - pub fn load(info: &Info) -> Result { - let config_builder = Config::builder() - .add_source(File::from_str(&info.tracker_toml, FileFormat::Toml)) - .build()?; - let mut config: Configuration = config_builder.try_deserialize()?; - - if let Some(ref token) = info.api_admin_token { - config.override_api_admin_token(token); - }; - - Ok(config) - } - - /// Saves the configuration to the configuration file. - /// - /// # Errors - /// - /// Will return `Err` if `filename` does not exist or the user does not have - /// permission to read it. Will also return `Err` if the configuration is - /// not valid or cannot be encoded to TOML. - /// - /// # Panics - /// - /// Will panic if the configuration cannot be written into the file. - pub fn save_to_file(&self, path: &str) -> Result<(), Error> { - fs::write(path, self.to_toml()).expect("Could not write to file!"); - Ok(()) - } - - /// Encodes the configuration to TOML. - fn to_toml(&self) -> String { - toml::to_string(self).expect("Could not encode TOML value") - } -} - -#[cfg(test)] -mod tests { - use crate::Configuration; - - #[cfg(test)] - fn default_config_toml() -> String { - let config = 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" - "# - .lines() - .map(str::trim_start) - .collect::>() - .join("\n"); - config - } - - #[test] - fn configuration_should_have_default_values() { - let configuration = Configuration::default(); - - let toml = toml::to_string(&configuration).expect("Could not encode TOML value"); - - assert_eq!(toml, default_config_toml()); - } - - #[test] - fn configuration_should_contain_the_external_ip() { - let configuration = Configuration::default(); - - assert_eq!(configuration.external_ip, Some(String::from("0.0.0.0"))); - } - - #[test] - fn configuration_should_be_saved_in_a_toml_config_file() { - use std::{env, fs}; - - use uuid::Uuid; - - // Build temp config file path - let temp_directory = env::temp_dir(); - let temp_file = temp_directory.join(format!("test_config_{}.toml", Uuid::new_v4())); - - // Convert to argument type for Configuration::save_to_file - let config_file_path = temp_file; - let path = config_file_path.to_string_lossy().to_string(); - - let default_configuration = Configuration::default(); - - default_configuration - .save_to_file(&path) - .expect("Could not save configuration to file"); - - let contents = fs::read_to_string(&path).expect("Something went wrong reading the file"); - - assert_eq!(contents, default_config_toml()); - } - - #[cfg(test)] - fn create_temp_config_file_with_default_config() -> String { - use std::env; - use std::fs::File; - use std::io::Write; - - use uuid::Uuid; - - // Build temp config file path - let temp_directory = env::temp_dir(); - let temp_file = temp_directory.join(format!("test_config_{}.toml", Uuid::new_v4())); - - // Convert to argument type for Configuration::load_from_file - let config_file_path = temp_file.clone(); - let path = config_file_path.to_string_lossy().to_string(); - - // Write file contents - let mut file = File::create(temp_file).unwrap(); - writeln!(&mut file, "{}", default_config_toml()).unwrap(); - - path - } - - #[test] - fn configuration_should_be_loaded_from_a_toml_config_file() { - let config_file_path = create_temp_config_file_with_default_config(); - - let configuration = Configuration::load_from_file(&config_file_path).expect("Could not load configuration from file"); - - assert_eq!(configuration, Configuration::default()); - } - - #[test] - fn http_api_configuration_should_check_if_it_contains_a_token() { - let configuration = Configuration::default(); - - assert!(configuration.http_api.access_tokens.values().any(|t| t == "MyAccessToken")); - assert!(!configuration.http_api.access_tokens.values().any(|t| t == "NonExistingToken")); - } -} 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 000000000..1c2cd073a --- /dev/null +++ b/packages/configuration/src/v1/health_check_api.rs @@ -0,0 +1,21 @@ +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, +} + +impl Default for HealthCheckApi { + fn default() -> Self { + Self { + bind_address: String::from("127.0.0.1:1313"), + } + } +} diff --git a/packages/configuration/src/v1/http_tracker.rs b/packages/configuration/src/v1/http_tracker.rs new file mode 100644 index 000000000..c2d5928e2 --- /dev/null +++ b/packages/configuration/src/v1/http_tracker.rs @@ -0,0 +1,35 @@ +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, +} + +impl Default for HttpTracker { + fn default() -> Self { + Self { + enabled: false, + bind_address: String::from("0.0.0.0:7070"), + ssl_enabled: false, + ssl_cert_path: None, + ssl_key_path: None, + } + } +} diff --git a/packages/configuration/src/v1/mod.rs b/packages/configuration/src/v1/mod.rs new file mode 100644 index 000000000..25aa587b3 --- /dev/null +++ b/packages/configuration/src/v1/mod.rs @@ -0,0 +1,599 @@ +//! Version `1` for [Torrust Tracker](https://docs.rs/torrust-tracker) +//! configuration data structures. +//! +//! 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 std::fs; +use std::net::IpAddr; +use std::str::FromStr; + +use figment::providers::{Env, Format, Serialized, Toml}; +use figment::Figment; +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, Error, Info}; + +/// 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::default(), + health_check_api: HealthCheckApi::default(), + }; + configuration.udp_trackers.push(UdpTracker::default()); + configuration.http_trackers.push(HttpTracker::default()); + configuration + } +} + +impl Configuration { + fn override_api_admin_token(&mut self, api_admin_token: &str) { + self.http_api.override_admin_token(api_admin_token); + } + + /// Returns the tracker public IP address id defined in the configuration, + /// and `None` otherwise. + #[must_use] + pub fn get_ext_ip(&self) -> Option { + match &self.external_ip { + None => None, + Some(external_ip) => match IpAddr::from_str(external_ip) { + Ok(external_ip) => Some(external_ip), + Err(_) => None, + }, + } + } + + /// Saves the default configuration at the given path. + /// + /// # Errors + /// + /// Will return `Err` if `path` is not a valid path or the configuration + /// file cannot be created. + pub fn create_default_configuration_file(path: &str) -> Result { + let config = Configuration::default(); + config.save_to_file(path)?; + Ok(config) + } + + /// Loads the configuration from the `Info` struct. The whole + /// configuration in toml format is included in the `info.tracker_toml` string. + /// + /// Optionally will override the admin api token. + /// + /// # Errors + /// + /// Will return `Err` if the environment variable does not exist or has a bad configuration. + pub fn load(info: &Info) -> Result { + let figment = Figment::from(Serialized::defaults(Configuration::default())) + .merge(Toml::string(&info.tracker_toml)) + .merge(Env::prefixed("TORRUST_TRACKER__").split("__")); + + let mut config: Configuration = figment.extract()?; + + if let Some(ref token) = info.api_admin_token { + config.override_api_admin_token(token); + }; + + Ok(config) + } + + /// Saves the configuration to the configuration file. + /// + /// # Errors + /// + /// Will return `Err` if `filename` does not exist or the user does not have + /// permission to read it. Will also return `Err` if the configuration is + /// not valid or cannot be encoded to TOML. + /// + /// # Panics + /// + /// Will panic if the configuration cannot be written into the file. + pub fn save_to_file(&self, path: &str) -> Result<(), Error> { + fs::write(path, self.to_toml()).expect("Could not write to file!"); + Ok(()) + } + + /// Encodes the configuration to TOML. + fn to_toml(&self) -> String { + // code-review: do we need to use Figment also to serialize into toml? + toml::to_string(self).expect("Could not encode TOML value") + } +} + +#[cfg(test)] +mod tests { + + use crate::v1::Configuration; + use crate::Info; + + #[cfg(test)] + fn default_config_toml() -> String { + let config = 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" + "# + .lines() + .map(str::trim_start) + .collect::>() + .join("\n"); + config + } + + #[test] + fn configuration_should_have_default_values() { + let configuration = Configuration::default(); + + let toml = toml::to_string(&configuration).expect("Could not encode TOML value"); + + assert_eq!(toml, default_config_toml()); + } + + #[test] + fn configuration_should_contain_the_external_ip() { + let configuration = Configuration::default(); + + assert_eq!(configuration.external_ip, Some(String::from("0.0.0.0"))); + } + + #[test] + fn configuration_should_be_saved_in_a_toml_config_file() { + use std::{env, fs}; + + use uuid::Uuid; + + // Build temp config file path + let temp_directory = env::temp_dir(); + let temp_file = temp_directory.join(format!("test_config_{}.toml", Uuid::new_v4())); + + // Convert to argument type for Configuration::save_to_file + let config_file_path = temp_file; + let path = config_file_path.to_string_lossy().to_string(); + + let default_configuration = Configuration::default(); + + default_configuration + .save_to_file(&path) + .expect("Could not save configuration to file"); + + let contents = fs::read_to_string(&path).expect("Something went wrong reading the file"); + + assert_eq!(contents, default_config_toml()); + } + + #[test] + fn configuration_should_use_the_default_values_when_an_empty_configuration_is_provided_by_the_user() { + figment::Jail::expect_with(|_jail| { + let empty_configuration = String::new(); + + let info = Info { + tracker_toml: empty_configuration, + api_admin_token: None, + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); + + assert_eq!(configuration, Configuration::default()); + + Ok(()) + }); + } + + #[test] + fn configuration_should_be_loaded_from_a_toml_config_file() { + figment::Jail::expect_with(|_jail| { + let info = Info { + tracker_toml: default_config_toml(), + api_admin_token: None, + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); + + assert_eq!(configuration, Configuration::default()); + + Ok(()) + }); + } + + #[test] + fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_env_var() { + figment::Jail::expect_with(|jail| { + jail.set_env("TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN", "NewToken"); + + let info = Info { + tracker_toml: default_config_toml(), + api_admin_token: None, + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); + + assert_eq!( + configuration.http_api.access_tokens.get("admin"), + Some("NewToken".to_owned()).as_ref() + ); + + Ok(()) + }); + } + + #[test] + fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_the_deprecated_env_var_name() { + figment::Jail::expect_with(|_jail| { + let info = Info { + tracker_toml: default_config_toml(), + api_admin_token: Some("NewToken".to_owned()), + }; + + let configuration = Configuration::load(&info).expect("Could not load configuration from file"); + + assert_eq!( + configuration.http_api.access_tokens.get("admin"), + Some("NewToken".to_owned()).as_ref() + ); + + Ok(()) + }); + } +} diff --git a/packages/configuration/src/v1/tracker_api.rs b/packages/configuration/src/v1/tracker_api.rs new file mode 100644 index 000000000..8749478c8 --- /dev/null +++ b/packages/configuration/src/v1/tracker_api.rs @@ -0,0 +1,67 @@ +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, +} + +impl Default for HttpApi { + fn default() -> Self { + Self { + 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(), + } + } +} + +impl HttpApi { + pub fn override_admin_token(&mut self, api_admin_token: &str) { + self.access_tokens.insert("admin".to_string(), api_admin_token.to_string()); + } +} + +#[cfg(test)] +mod tests { + use crate::v1::tracker_api::HttpApi; + + #[test] + fn http_api_configuration_should_check_if_it_contains_a_token() { + let configuration = HttpApi::default(); + + assert!(configuration.access_tokens.values().any(|t| t == "MyAccessToken")); + assert!(!configuration.access_tokens.values().any(|t| t == "NonExistingToken")); + } +} diff --git a/packages/configuration/src/v1/udp_tracker.rs b/packages/configuration/src/v1/udp_tracker.rs new file mode 100644 index 000000000..254272bdd --- /dev/null +++ b/packages/configuration/src/v1/udp_tracker.rs @@ -0,0 +1,20 @@ +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, +} +impl Default for UdpTracker { + fn default() -> Self { + Self { + enabled: false, + bind_address: String::from("0.0.0.0:6969"), + } + } +} diff --git a/share/default/config/tracker.container.mysql.toml b/share/default/config/tracker.container.mysql.toml index e7714c229..f2db06228 100644 --- a/share/default/config/tracker.container.mysql.toml +++ b/share/default/config/tracker.container.mysql.toml @@ -30,11 +30,11 @@ ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" -# Please override the admin token setting the -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -# environmental variable! - [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.container.sqlite3.toml b/share/default/config/tracker.container.sqlite3.toml index 4ec055c56..4a3ba03b6 100644 --- a/share/default/config/tracker.container.sqlite3.toml +++ b/share/default/config/tracker.container.sqlite3.toml @@ -30,11 +30,11 @@ ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" -# Please override the admin token setting the -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -# environmental variable! - [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.development.sqlite3.toml b/share/default/config/tracker.development.sqlite3.toml index 9304a2d51..62e5b478e 100644 --- a/share/default/config/tracker.development.sqlite3.toml +++ b/share/default/config/tracker.development.sqlite3.toml @@ -31,6 +31,10 @@ ssl_enabled = false ssl_key_path = "" [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.e2e.container.sqlite3.toml b/share/default/config/tracker.e2e.container.sqlite3.toml index 86ffb3ffd..3738704b5 100644 --- a/share/default/config/tracker.e2e.container.sqlite3.toml +++ b/share/default/config/tracker.e2e.container.sqlite3.toml @@ -30,11 +30,11 @@ ssl_cert_path = "/var/lib/torrust/tracker/tls/localhost.crt" ssl_enabled = false ssl_key_path = "/var/lib/torrust/tracker/tls/localhost.key" -# Please override the admin token setting the -# `TORRUST_TRACKER_API_ADMIN_TOKEN` -# environmental variable! - [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api] diff --git a/share/default/config/tracker.udp.benchmarking.toml b/share/default/config/tracker.udp.benchmarking.toml index 70298e9dc..1e951d8fc 100644 --- a/share/default/config/tracker.udp.benchmarking.toml +++ b/share/default/config/tracker.udp.benchmarking.toml @@ -31,6 +31,10 @@ ssl_enabled = false ssl_key_path = "" [http_api.access_tokens] +# Please override the admin token setting the environmental variable: +# `TORRUST_TRACKER__HTTP_API__ACCESS_TOKENS__ADMIN` +# The old variable name is deprecated: +# `TORRUST_TRACKER_API_ADMIN_TOKEN` admin = "MyAccessToken" [health_check_api]