pub mod core;
pub mod database;
pub mod health_check_api;
pub mod http_tracker;
pub mod logging;
pub mod network;
pub mod tracker_api;
pub mod udp_tracker;
use std::fs;
use std::net::IpAddr;
use figment::providers::{Env, Format, Serialized, Toml};
use figment::Figment;
use logging::Logging;
use serde::{Deserialize, Serialize};
use self::core::Core;
use self::health_check_api::HealthCheckApi;
use self::http_tracker::HttpTracker;
use self::tracker_api::HttpApi;
use self::udp_tracker::UdpTracker;
use crate::validator::{SemanticValidationError, Validator};
use crate::{Error, Info, Metadata, Version};
const VERSION_2_0_0: &str = "2.0.0";
const CONFIG_OVERRIDE_PREFIX: &str = "TORRUST_TRACKER_CONFIG_OVERRIDE_";
const CONFIG_OVERRIDE_SEPARATOR: &str = "__";
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Default, Clone)]
pub struct Configuration {
pub metadata: Metadata,
pub logging: Logging,
pub core: Core,
pub udp_trackers: Option<Vec<UdpTracker>>,
pub http_trackers: Option<Vec<HttpTracker>>,
pub http_api: Option<HttpApi>,
pub health_check_api: HealthCheckApi,
}
impl Configuration {
#[must_use]
pub fn get_ext_ip(&self) -> Option<IpAddr> {
self.core.net.external_ip.as_ref().map(|external_ip| *external_ip)
}
pub fn create_default_configuration_file(path: &str) -> Result<Configuration, Error> {
let config = Configuration::default();
config.save_to_file(path)?;
Ok(config)
}
pub fn load(info: &Info) -> Result<Configuration, Error> {
let figment = if let Some(config_toml) = &info.config_toml {
Figment::from(Toml::string(config_toml)).merge(Env::prefixed(CONFIG_OVERRIDE_PREFIX).split(CONFIG_OVERRIDE_SEPARATOR))
} else {
Figment::from(Toml::file(&info.config_toml_path))
.merge(Env::prefixed(CONFIG_OVERRIDE_PREFIX).split(CONFIG_OVERRIDE_SEPARATOR))
};
Self::check_mandatory_options(&figment)?;
let figment = figment.join(Serialized::defaults(Configuration::default()));
let config: Configuration = figment.extract()?;
if config.metadata.schema_version != Version::new(VERSION_2_0_0) {
return Err(Error::UnsupportedVersion {
version: config.metadata.schema_version,
});
}
Ok(config)
}
fn check_mandatory_options(figment: &Figment) -> Result<(), Error> {
let mandatory_options = ["metadata.schema_version", "logging.threshold", "core.private", "core.listed"];
for mandatory_option in mandatory_options {
figment
.find_value(mandatory_option)
.map_err(|_err| Error::MissingMandatoryOption {
path: mandatory_option.to_owned(),
})?;
}
Ok(())
}
pub fn save_to_file(&self, path: &str) -> Result<(), Error> {
fs::write(path, self.to_toml()).expect("Could not write to file!");
Ok(())
}
#[must_use]
fn to_toml(&self) -> String {
toml::to_string(self).expect("Could not encode TOML value")
}
#[must_use]
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).expect("Could not encode JSON value")
}
#[must_use]
pub fn mask_secrets(mut self) -> Self {
self.core.database.mask_secrets();
if let Some(ref mut api) = self.http_api {
api.mask_secrets();
}
self
}
}
impl Validator for Configuration {
fn validate(&self) -> Result<(), SemanticValidationError> {
self.core.validate()
}
}
#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr};
use crate::v2_0_0::Configuration;
use crate::Info;
#[cfg(test)]
fn default_config_toml() -> String {
let config = r#"[metadata]
app = "torrust-tracker"
purpose = "configuration"
schema_version = "2.0.0"
[logging]
threshold = "info"
[core]
inactive_peer_cleanup_interval = 600
listed = false
private = false
tracker_usage_statistics = true
[core.announce_policy]
interval = 120
interval_min = 120
[core.database]
driver = "sqlite3"
path = "./storage/tracker/lib/database/sqlite3.db"
[core.net]
external_ip = "0.0.0.0"
on_reverse_proxy = false
[core.tracker_policy]
max_peer_timeout = 900
persistent_torrent_completed_stat = false
remove_peerless_torrents = true
[health_check_api]
bind_address = "127.0.0.1:1313"
"#
.lines()
.map(str::trim_start)
.collect::<Vec<&str>>()
.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.core.net.external_ip,
Some(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))
);
}
#[test]
fn configuration_should_be_saved_in_a_toml_config_file() {
use std::{env, fs};
use uuid::Uuid;
let temp_directory = env::temp_dir();
let temp_file = temp_directory.join(format!("test_config_{}.toml", Uuid::new_v4()));
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_only_the_mandatory_options_are_provided_by_the_user_via_toml_file() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"tracker.toml",
r#"
[metadata]
schema_version = "2.0.0"
[logging]
threshold = "info"
[core]
listed = false
private = false
"#,
)?;
let info = Info {
config_toml: None,
config_toml_path: "tracker.toml".to_string(),
};
let configuration = Configuration::load(&info).expect("Could not load configuration from file");
assert_eq!(configuration, Configuration::default());
Ok(())
});
}
#[test]
fn configuration_should_use_the_default_values_when_only_the_mandatory_options_are_provided_by_the_user_via_toml_content() {
figment::Jail::expect_with(|_jail| {
let config_toml = r#"
[metadata]
schema_version = "2.0.0"
[logging]
threshold = "info"
[core]
listed = false
private = false
"#
.to_string();
let info = Info {
config_toml: Some(config_toml),
config_toml_path: String::new(),
};
let configuration = Configuration::load(&info).expect("Could not load configuration from file");
assert_eq!(configuration, Configuration::default());
Ok(())
});
}
#[test]
fn default_configuration_could_be_overwritten_from_a_single_env_var_with_toml_contents() {
figment::Jail::expect_with(|_jail| {
let config_toml = r#"
[metadata]
schema_version = "2.0.0"
[logging]
threshold = "info"
[core]
listed = false
private = false
[core.database]
path = "OVERWRITTEN DEFAULT DB PATH"
"#
.to_string();
let info = Info {
config_toml: Some(config_toml),
config_toml_path: String::new(),
};
let configuration = Configuration::load(&info).expect("Could not load configuration from file");
assert_eq!(configuration.core.database.path, "OVERWRITTEN DEFAULT DB PATH".to_string());
Ok(())
});
}
#[test]
fn default_configuration_could_be_overwritten_from_a_toml_config_file() {
figment::Jail::expect_with(|jail| {
jail.create_file(
"tracker.toml",
r#"
[metadata]
schema_version = "2.0.0"
[logging]
threshold = "info"
[core]
listed = false
private = false
[core.database]
path = "OVERWRITTEN DEFAULT DB PATH"
"#,
)?;
let info = Info {
config_toml: None,
config_toml_path: "tracker.toml".to_string(),
};
let configuration = Configuration::load(&info).expect("Could not load configuration from file");
assert_eq!(configuration.core.database.path, "OVERWRITTEN DEFAULT DB PATH".to_string());
Ok(())
});
}
#[test]
fn configuration_should_allow_to_overwrite_the_default_tracker_api_token_for_admin_with_an_env_var() {
figment::Jail::expect_with(|jail| {
jail.set_env("TORRUST_TRACKER_CONFIG_OVERRIDE_HTTP_API__ACCESS_TOKENS__ADMIN", "NewToken");
let info = Info {
config_toml: Some(default_config_toml()),
config_toml_path: String::new(),
};
let configuration = Configuration::load(&info).expect("Could not load configuration from file");
assert_eq!(
configuration.http_api.unwrap().access_tokens.get("admin"),
Some("NewToken".to_owned()).as_ref()
);
Ok(())
});
}
}