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]