diff --git a/.gitignore b/.gitignore index d574298da..8665dea4e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,5 @@ /data.db /.vscode/launch.json /storage/ - - +/config/ +/config.*/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 94f199bd6..6f4826556 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,8 @@ "[rust]": { "editor.formatOnSave": true }, - "rust-analyzer.checkOnSave.command": "clippy", - "rust-analyzer.checkOnSave.allTargets": true, - "rust-analyzer.checkOnSave.extraArgs": ["--","-W","clippy::pedantic"], + "rust-analyzer.checkOnSave": true, + "rust-analyzer.check.allTargets": true, + "rust-analyzer.check.command": "clippy", + "rust-analyzer.check.extraArgs": ["--","-W","clippy::pedantic"], } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8347362ab..8c95afdf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,6 +531,48 @@ dependencies = [ "syn", ] +[[package]] +name = "derive-getters" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c5905670fd9c320154f3a4a01c9e609733cd7b753f3c58777ab7d5ce26686b3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -2836,6 +2878,8 @@ dependencies = [ "binascii", "chrono", "config", + "derive-getters", + "derive_builder", "derive_more", "fern", "futures", diff --git a/Cargo.toml b/Cargo.toml index cf90da8f1..3d086ad2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,9 @@ uuid = { version = "1", features = ["v4"] } axum = "0.6.1" axum-server = { version = "0.4.4", features = ["tls-rustls"] } +derive_builder = "0.12" +derive-getters = "0.2" + [dev-dependencies] mockall = "0.11" diff --git a/cSpell.json b/cSpell.json index a451d18dc..47b81018a 100644 --- a/cSpell.json +++ b/cSpell.json @@ -69,6 +69,7 @@ "trackerid", "typenum", "Unamed", + "unnested", "untuple", "uroot", "Vagaa", diff --git a/src/apis/error.rs b/src/apis/error.rs new file mode 100644 index 000000000..8117133cc --- /dev/null +++ b/src/apis/error.rs @@ -0,0 +1,12 @@ +use std::panic::Location; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("failed to parse settings {message}, {location}")] + ParseConfig { + location: &'static Location<'static>, + message: String, + }, +} diff --git a/src/apis/mod.rs b/src/apis/mod.rs index a646d5543..c958ab28e 100644 --- a/src/apis/mod.rs +++ b/src/apis/mod.rs @@ -1,6 +1,8 @@ +pub mod error; pub mod handlers; pub mod middlewares; pub mod resources; pub mod responses; pub mod routes; pub mod server; +pub mod settings; diff --git a/src/apis/resources/mod.rs b/src/apis/resources/mod.rs index bf3ce273b..48060a3fb 100644 --- a/src/apis/resources/mod.rs +++ b/src/apis/resources/mod.rs @@ -1,4 +1,8 @@ +use std::collections::BTreeMap; + pub mod auth_key; pub mod peer; pub mod stats; pub mod torrent; + +pub type ApiTokens = BTreeMap; diff --git a/src/apis/settings.rs b/src/apis/settings.rs new file mode 100644 index 000000000..1ed94c34a --- /dev/null +++ b/src/apis/settings.rs @@ -0,0 +1,128 @@ +use std::collections::BTreeMap; +use std::net::SocketAddr; +use std::panic::Location; +use std::path::Path; +use std::str::FromStr; + +use derive_builder::Builder; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; + +use super::error::Error; +use crate::config::HttpApi; +use crate::errors::settings::ServiceSettingsError; +use crate::settings::{Service, ServiceProtocol}; +use crate::tracker::services::common::{Tls, TlsBuilder}; +use crate::{check_field_is_not_empty, check_field_is_not_none}; + +pub type ApiTokens = BTreeMap; + +#[derive(Builder, Getters, Default, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +#[builder(default, pattern = "immutable")] +pub struct Settings { + #[builder(setter(into), default = "\"default_api\".to_string()")] + #[getter(rename = "get_id")] + id: String, + #[builder(default = "false")] + #[getter(rename = "is_enabled")] + enabled: bool, + #[builder(setter(into), default = "\"HTTP API (default)\".to_string()")] + #[getter(rename = "get_display_name")] + display_name: String, + #[builder(default = "Some(SocketAddr::from_str(\"127.0.0.1:1212\").unwrap())")] + #[getter(rename = "get_socket")] + socket: Option, + #[builder(default = "self.api_token_default()")] + #[getter(rename = "get_access_tokens")] + access_tokens: ApiTokens, + #[getter(rename = "get_tls_settings")] + tls: Option, +} + +impl SettingsBuilder { + // Private helper method that will set the default database path if the database is Sqlite. + #[allow(clippy::unused_self)] + fn api_token_default(&self) -> ApiTokens { + let mut access_tokens = BTreeMap::new(); + access_tokens.insert("admin".to_string(), "password".to_string()); + access_tokens + } +} + +impl TryFrom<&HttpApi> for Settings { + type Error = Error; + + fn try_from(api: &HttpApi) -> Result { + let tls = if api.ssl_enabled { + let cert = Path::new(match &api.ssl_cert_path { + Some(p) => p, + None => { + return Err(Error::ParseConfig { + location: Location::caller(), + message: "ssl_cert_path is none and tls is enabled!".to_string(), + }) + } + }); + + let key = Path::new(match &api.ssl_key_path { + Some(p) => p, + None => { + return Err(Error::ParseConfig { + location: Location::caller(), + message: "ssl_key_path is none and tls is enabled!".to_string(), + }) + } + }); + + Some( + TlsBuilder::default() + .certificate_file_path(cert.into()) + .key_file_path(key.into()) + .build() + .expect("failed to build tls settings"), + ) + } else { + None + }; + + Ok(SettingsBuilder::default() + .id("imported_api") + .enabled(api.enabled) + .display_name("Imported API") + .socket(SocketAddr::from_str(api.bind_address.as_str()).ok()) + .access_tokens(api.access_tokens.clone()) + .tls(tls) + .build() + .expect("failed to import settings")) + } +} + +impl TryFrom<(&String, &Service)> for Settings { + type Error = ServiceSettingsError; + + fn try_from(value: (&String, &Service)) -> Result { + check_field_is_not_none!(value.1 => ServiceSettingsError; + enabled, service); + + if value.1.service.unwrap() != ServiceProtocol::Api { + return Err(ServiceSettingsError::WrongService { + field: "service".to_string(), + expected: ServiceProtocol::Api, + found: value.1.service.unwrap(), + data: value.1.into(), + }); + } + + check_field_is_not_empty!(value.1 => ServiceSettingsError; + display_name: String); + + Ok(Self { + id: value.0.clone(), + enabled: value.1.enabled.unwrap(), + display_name: value.1.display_name.clone().unwrap(), + socket: Some(value.1.get_socket()?), + access_tokens: value.1.get_api_tokens()?, + tls: value.1.get_tls()?, + }) + } +} diff --git a/src/config.rs b/src/config.rs index 7ed0f9fa7..07a8e4a29 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet}; use std::net::IpAddr; use std::panic::Location; use std::path::Path; @@ -46,14 +46,14 @@ pub struct HttpApi { pub ssl_cert_path: Option, #[serde_as(as = "NoneAsEmptyString")] pub ssl_key_path: Option, - pub access_tokens: HashMap, + pub access_tokens: BTreeMap, } impl HttpApi { #[must_use] pub fn contains_token(&self, token: &str) -> bool { - let tokens: HashMap = self.access_tokens.clone(); - let tokens: HashSet = tokens.into_values().collect(); + let tokens: BTreeMap = self.access_tokens.clone(); + let tokens: BTreeSet = tokens.into_values().collect(); tokens.contains(token) } } diff --git a/src/databases/driver.rs b/src/databases/driver.rs index c601f1866..efc919624 100644 --- a/src/databases/driver.rs +++ b/src/databases/driver.rs @@ -2,11 +2,13 @@ use serde::{Deserialize, Serialize}; use super::error::Error; use super::mysql::Mysql; +use super::settings::Settings; use super::sqlite::Sqlite; use super::{Builder, Database}; -#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, derive_more::Display, Clone)] +#[derive(Default, Serialize, Deserialize, Hash, PartialEq, PartialOrd, Ord, Eq, Copy, Debug, Clone)] pub enum Driver { + #[default] Sqlite3, MySQL, } @@ -17,10 +19,10 @@ impl Driver { /// # Errors /// /// This function will return an error if unable to connect to the database. - pub fn build(&self, db_path: &str) -> Result, Error> { - let database = match self { - Driver::Sqlite3 => Builder::::build(db_path), - Driver::MySQL => Builder::::build(db_path), + pub fn build(settings: &Settings) -> Result, Error> { + let database = match settings.driver { + Driver::Sqlite3 => Builder::::build(settings), + Driver::MySQL => Builder::::build(settings), }?; database.create_database_tables().expect("Could not create database tables."); @@ -28,3 +30,12 @@ impl Driver { Ok(database) } } + +impl std::fmt::Display for Driver { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Sqlite3 => write!(f, "sqllite3"), + Self::MySQL => write!(f, "my_sql"), + } + } +} diff --git a/src/databases/error.rs b/src/databases/error.rs index 4bee82f19..180d3f656 100644 --- a/src/databases/error.rs +++ b/src/databases/error.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use r2d2_mysql::mysql::UrlError; use super::driver::Driver; +use super::settings::Settings; use crate::located_error::{Located, LocatedError}; #[derive(thiserror::Error, Debug, Clone)] @@ -44,6 +45,20 @@ pub enum Error { source: LocatedError<'static, r2d2::Error>, driver: Driver, }, + + #[error("Failed to convert to driver settings, expected: {expected}, actual {actual}, settings: {settings:?}, {location}")] + WrongDriver { + location: &'static Location<'static>, + expected: Driver, + actual: Driver, + settings: Settings, + }, + + #[error("Failed to get required felid from settings: {felid}, {location}")] + MissingFelid { + location: &'static Location<'static>, + felid: String, + }, } impl From for Error { diff --git a/src/databases/mod.rs b/src/databases/mod.rs index 809decc2c..669e511fe 100644 --- a/src/databases/mod.rs +++ b/src/databases/mod.rs @@ -1,6 +1,7 @@ pub mod driver; pub mod error; pub mod mysql; +pub mod settings; pub mod sqlite; use std::marker::PhantomData; @@ -8,6 +9,7 @@ use std::marker::PhantomData; use async_trait::async_trait; use self::error::Error; +use self::settings::Settings; use crate::protocol::info_hash::InfoHash; use crate::tracker::auth; @@ -27,8 +29,8 @@ where /// # Errors /// /// Will return `r2d2::Error` if `db_path` is not able to create a database. - pub(self) fn build(db_path: &str) -> Result, Error> { - Ok(Box::new(T::new(db_path)?)) + pub(self) fn build(settings: &Settings) -> Result, Error> { + Ok(Box::new(T::new(settings)?)) } } @@ -39,7 +41,7 @@ pub trait Database: Sync + Send { /// # Errors /// /// Will return `r2d2::Error` if `db_path` is not able to create a database. - fn new(db_path: &str) -> Result + fn new(settings: &Settings) -> Result where Self: std::marker::Sized; diff --git a/src/databases/mysql.rs b/src/databases/mysql.rs index ac54ebb82..de843494f 100644 --- a/src/databases/mysql.rs +++ b/src/databases/mysql.rs @@ -9,6 +9,7 @@ use r2d2_mysql::mysql::{params, Opts, OptsBuilder}; use r2d2_mysql::MysqlConnectionManager; use super::driver::Driver; +use super::settings; use crate::databases::{Database, Error}; use crate::protocol::common::AUTH_KEY_LENGTH; use crate::protocol::info_hash::InfoHash; @@ -16,6 +17,11 @@ use crate::tracker::auth; const DRIVER: Driver = Driver::MySQL; +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Settings { + pub connection_url: String, +} + pub struct Mysql { pool: Pool, } @@ -25,8 +31,9 @@ impl Database for Mysql { /// # Errors /// /// Will return `r2d2::Error` if `db_path` is not able to create `MySQL` database. - fn new(db_path: &str) -> Result { - let opts = Opts::from_url(db_path)?; + fn new(settings: &settings::Settings) -> Result { + let mysql_settings = settings.get_mysql_settings()?; + let opts = Opts::from_url(mysql_settings.connection_url.as_str())?; let builder = OptsBuilder::from_opts(opts); let manager = MysqlConnectionManager::new(builder); let pool = r2d2::Pool::builder().build(manager).map_err(|e| (e, DRIVER))?; diff --git a/src/databases/settings.rs b/src/databases/settings.rs new file mode 100644 index 000000000..88b1bdd2c --- /dev/null +++ b/src/databases/settings.rs @@ -0,0 +1,141 @@ +use std::panic::Location; +use std::path::Path; + +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use super::driver::Driver::{self, Sqlite3}; +use super::driver::{self}; +use super::error::Error; +use super::{mysql, sqlite}; + +#[derive(Builder, Default, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +#[builder(default, pattern = "immutable")] +pub struct Settings { + #[builder(default = "driver::Driver::default()")] + pub driver: driver::Driver, + #[builder(default = "self.sql_lite_path_default()")] + sql_lite_3_db_file_path: Option>, + my_sql_connection_url: Option, +} + +impl SettingsBuilder { + // Private helper method that will set the default database path if the database is Sqlite. + #[allow(clippy::unused_self)] + fn sql_lite_path_default(&self) -> Option> { + if let Sqlite3 = driver::Driver::default() { + Some(Path::new("data.db").into()) + } else { + None + } + } +} + +impl Settings { + /// Returns the check of this [`Settings`]. + /// + /// # Errors + /// + /// This function will return an error if unable to transform into a definite database setting. + pub fn check(&self) -> Result<(), Error> { + match self.driver { + Driver::Sqlite3 => { + sqlite::Settings::try_from(self)?; + } + Driver::MySQL => { + mysql::Settings::try_from(self)?; + } + } + + Ok(()) + } + + pub fn get_sqlite_settings(&self) -> Result { + sqlite::Settings::try_from(self) + } + + pub fn get_mysql_settings(&self) -> Result { + mysql::Settings::try_from(self) + } +} + +#[derive(PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +pub struct OldConfig { + pub db_driver: driver::Driver, + pub db_path: String, +} + +impl TryFrom<&OldConfig> for Settings { + type Error = Error; + + fn try_from(value: &OldConfig) -> Result { + Ok(match value.db_driver { + Driver::Sqlite3 => SettingsBuilder::default() + .driver(Driver::Sqlite3) + .sql_lite_3_db_file_path(Some(Path::new(&value.db_path).into())) + .build() + .unwrap(), + Driver::MySQL => SettingsBuilder::default() + .driver(Driver::MySQL) + .my_sql_connection_url(Some(value.db_path.clone())) + .build() + .unwrap(), + }) + } +} + +impl TryFrom<&Settings> for sqlite::Settings { + type Error = Error; + + fn try_from(value: &Settings) -> Result { + Ok(Self { + database_file_path: match value.driver { + Driver::Sqlite3 => match &value.sql_lite_3_db_file_path { + Some(path) => path.clone(), + None => { + return Err(Error::MissingFelid { + location: Location::caller(), + felid: "sql_lite_3_db_file_path".to_string(), + }) + } + }, + driver => { + return Err(Error::WrongDriver { + location: Location::caller(), + expected: Driver::Sqlite3, + actual: driver, + settings: value.clone(), + }) + } + }, + }) + } +} + +impl TryFrom<&Settings> for mysql::Settings { + type Error = Error; + + fn try_from(value: &Settings) -> Result { + Ok(Self { + connection_url: match value.driver { + Driver::MySQL => match &value.my_sql_connection_url { + Some(url) => url.clone(), + None => { + return Err(Error::MissingFelid { + location: Location::caller(), + felid: "my_sql_connection_url".to_string(), + }) + } + }, + driver => { + return Err(Error::WrongDriver { + location: Location::caller(), + expected: Driver::MySQL, + actual: driver, + settings: value.clone(), + }) + } + }, + }) + } +} diff --git a/src/databases/sqlite.rs b/src/databases/sqlite.rs index 3425b15c8..1496f3d92 100644 --- a/src/databases/sqlite.rs +++ b/src/databases/sqlite.rs @@ -1,4 +1,5 @@ use std::panic::Location; +use std::path::Path; use std::str::FromStr; use async_trait::async_trait; @@ -6,6 +7,7 @@ use r2d2::Pool; use r2d2_sqlite::SqliteConnectionManager; use super::driver::Driver; +use super::settings; use crate::databases::{Database, Error}; use crate::protocol::clock::DurationSinceUnixEpoch; use crate::protocol::info_hash::InfoHash; @@ -13,6 +15,11 @@ use crate::tracker::auth; const DRIVER: Driver = Driver::Sqlite3; +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct Settings { + pub database_file_path: Box, +} + pub struct Sqlite { pool: Pool, } @@ -22,8 +29,9 @@ impl Database for Sqlite { /// # Errors /// /// Will return `r2d2::Error` if `db_path` is not able to create `SqLite` database. - fn new(db_path: &str) -> Result { - let cm = SqliteConnectionManager::file(db_path); + fn new(settings: &settings::Settings) -> Result { + let sqlite_settings = settings.get_sqlite_settings()?; + let cm = SqliteConnectionManager::file(sqlite_settings.database_file_path); Pool::new(cm).map_or_else(|err| Err((err, Driver::Sqlite3).into()), |pool| Ok(Sqlite { pool })) } diff --git a/src/errors/mod.rs b/src/errors/mod.rs new file mode 100644 index 000000000..d8e9105ed --- /dev/null +++ b/src/errors/mod.rs @@ -0,0 +1,24 @@ +use std::path::Path; + +use thiserror::Error; + +use crate::located_error::LocatedError; + +pub mod settings; +pub mod settings_manager; +pub mod wrappers; + +#[derive(Error, Clone, Debug)] +pub enum FilePathError { + #[error("File Path failed to Canonicalize: {input} : {source}.")] + FilePathIsUnresolvable { + input: Box, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("File Path destination is not a file: {input} : {source}.")] + FilePathIsNotAvailable { + input: Box, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, +} diff --git a/src/errors/settings.rs b/src/errors/settings.rs new file mode 100644 index 000000000..5a9faa75b --- /dev/null +++ b/src/errors/settings.rs @@ -0,0 +1,209 @@ +use thiserror::Error; + +use crate::located_error::LocatedError; +use crate::settings::{CommonSettings, GlobalSettings, ServiceNoSecrets, ServiceProtocol, TlsSettings, TrackerSettings}; + +#[derive(Error, Clone, Debug)] +pub enum SettingsError { + #[error("Bad Namespace: \".{field}\" {message}")] + NamespaceError { message: String, field: String }, + + // Todo: Expand this for Semantic Versioning 2.0.0 + #[error("Bad Version: \".{field}\" {message}")] + VersionError { message: String, field: String }, + + #[error("Tracker Settings Error: \".tracker.{field}\": {message}")] + TrackerSettingsError { + message: String, + field: String, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Global Settings Error: \".tracker.global.{field}\": {message}")] + GlobalSettingsError { + message: String, + field: String, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Common Settings Error: \".tracker.common.{field}\": {message}")] + CommonSettingsError { + message: String, + field: String, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Service Settings Error: \".tracker.service.{id}.{field}\": {message}")] + ServiceSettingsError { + message: String, + field: String, + id: String, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, +} + +#[derive(Error, Clone, Debug, Eq, Hash, PartialEq)] +pub enum TrackerSettingsError { + #[error("Required Field is missing (null)!")] + MissingRequiredField { field: String, data: TrackerSettings }, +} + +impl TrackerSettingsError { + #[must_use] + pub fn get_field(&self) -> String { + match self { + Self::MissingRequiredField { field, data: _ } => field, + } + .clone() + } +} + +#[derive(Error, Clone, Debug, Eq, Hash, PartialEq)] +pub enum GlobalSettingsError { + #[error("Required Field is missing (null)!")] + MissingRequiredField { field: String, data: GlobalSettings }, + + #[error("Bad Socket String: \"{input}\", {message}")] + ExternalIpBadSyntax { + field: String, + input: String, + message: String, + data: GlobalSettings, + }, +} + +impl GlobalSettingsError { + #[must_use] + pub fn get_field(&self) -> String { + match self { + Self::MissingRequiredField { field, data: _ } + | Self::ExternalIpBadSyntax { + field, + input: _, + message: _, + data: _, + } => field, + } + .clone() + } +} + +#[derive(Error, Clone, Debug, Eq, Hash, PartialEq)] +pub enum CommonSettingsError { + #[error("Required Field is missing (null)!")] + MissingRequiredField { field: String, data: CommonSettings }, + + #[error("Required Field is empty (0 or \"\")!")] + EmptyRequiredField { field: String, data: CommonSettings }, +} + +impl CommonSettingsError { + #[must_use] + pub fn get_field(&self) -> String { + match self { + Self::MissingRequiredField { field, data: _ } | Self::EmptyRequiredField { field, data: _ } => field, + } + .clone() + } +} + +#[derive(Error, Clone, Debug)] +pub enum ServiceSettingsError { + #[error("Required Field is missing (null)!")] + MissingRequiredField { field: String, data: ServiceNoSecrets }, + + #[error("Required Field is empty (0 or \"\")!")] + EmptyRequiredField { field: String, data: ServiceNoSecrets }, + + #[error("Api Services Requires at least one Access Token!")] + ApiRequiresAccessToken { field: String, data: ServiceNoSecrets }, + + #[error("TLS Services Requires TLS Settings!")] + TlsRequiresTlsConfig { field: String, data: ServiceNoSecrets }, + + #[error("Bad TLS Configuration: {source}.")] + TlsSettingsError { + field: String, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + data: ServiceNoSecrets, + }, + + #[error("Bad Socket String: \"{input}\".")] + BindingAddressBadSyntax { + field: String, + input: String, + message: String, + data: ServiceNoSecrets, + }, + #[error("Unexpected Service. Expected: {expected}, Found {found}.")] + WrongService { + field: String, + expected: ServiceProtocol, + found: ServiceProtocol, + data: ServiceNoSecrets, + }, +} + +impl ServiceSettingsError { + #[must_use] + pub fn get_field(&self) -> String { + match self { + Self::MissingRequiredField { field, data: _ } + | Self::EmptyRequiredField { field, data: _ } + | Self::ApiRequiresAccessToken { field, data: _ } + | Self::TlsRequiresTlsConfig { field, data: _ } + | Self::TlsSettingsError { + field, + source: _, + data: _, + } + | Self::BindingAddressBadSyntax { + field, + input: _, + message: _, + data: _, + } + | Self::WrongService { + field, + expected: _, + found: _, + data: _, + } => field, + } + .clone() + } +} + +#[derive(Error, Clone, Debug)] +pub enum TlsSettingsError { + #[error("Required Field is missing (null)!")] + MissingRequiredField { field: String, data: TlsSettings }, + + #[error("Required Field is empty (0 or \"\")!")] + EmptyRequiredField { field: String, data: TlsSettings }, + + #[error("Unable to find TLS Certificate File: {source}")] + BadCertificateFilePath { + field: String, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Unable to find TLS Key File: {source}")] + BadKeyFilePath { + field: String, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, +} + +impl TlsSettingsError { + #[must_use] + pub fn get_field(&self) -> String { + match self { + Self::BadKeyFilePath { field, source: _ } + | Self::BadCertificateFilePath { field, source: _ } + | Self::EmptyRequiredField { field, data: _ } + | Self::MissingRequiredField { field, data: _ } => field, + } + .clone() + } +} diff --git a/src/errors/settings_manager.rs b/src/errors/settings_manager.rs new file mode 100644 index 000000000..6ec382c25 --- /dev/null +++ b/src/errors/settings_manager.rs @@ -0,0 +1,95 @@ +use std::path::Path; + +use thiserror::Error; + +use crate::located_error::LocatedError; + +#[derive(Error, Clone, Debug)] +pub enum SettingsManagerError { + #[error("Unable to open file for reading : \".{source}\"")] + FailedToOpenFileForReading { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + #[error("Unable to open file for writing : \".{source}\"")] + FailedToOpenFileForWriting { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + #[error("Unable to open new file at:: {source}!")] + FailedToCreateNewFile { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Unable to resolve path at: \"{at}\"!")] + FailedToResolvePath { + at: Box, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Unable to prepare directory at: \"{at}\" : {source}!")] + FailedToPrepareDirectory { + at: Box, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Unable to resolve a directory at: \"{at}\"!")] + FailedToResolveDirectory { at: Box }, + + #[error("Unable to read file, {message}: \"{from}\" : {source}.")] + FailedToReadFromFile { + message: String, + from: Box, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + #[error("Unable to write file, {message}: \"{to}\": {source}.")] + FailedToWriteToFile { + message: String, + to: Box, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Unable to read buffer: {source}")] + FailedToReadFromBuffer { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + #[error("Unable to write buffer: {source}")] + FailedToWriteIntoBuffer { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Unable to read json, {message}: {source}")] + FailedToSerializeIntoJson { + message: String, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + #[error("Unable to write json, {message}: {source}")] + FailedToDeserializeFromJson { + message: String, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Unable to read toml: {source}")] + FailedToDeserializeFromToml { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Decoded json with unknown namespace: \"{namespace}\"")] + FailedToMatchNamespace { namespace: String }, + + #[error("Unable to process old settings from : \"{source}\"")] + FailedToProcessOldSettings { + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Unable to import old settings from: \"{from}\" : \"{source}\"")] + FailedToImportOldSettings { + from: Box, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, + + #[error("Unable to successfully move file from: {from} to: {to} \"{source}\"")] + FailedToMoveFile { + from: Box, + to: Box, + source: LocatedError<'static, dyn std::error::Error + Send + Sync>, + }, +} diff --git a/src/errors/wrappers.rs b/src/errors/wrappers.rs new file mode 100644 index 000000000..5197d2028 --- /dev/null +++ b/src/errors/wrappers.rs @@ -0,0 +1,69 @@ +use derive_more::{Deref, Display, From, Into}; +use thiserror::Error; + +#[derive(Error, Debug, Display, From, Into, Deref)] +pub struct IoError { + pub(crate) repr: std::io::Error, +} + +impl std::hash::Hash for IoError { + fn hash(&self, state: &mut H) { + self.repr.kind().hash(state); + } +} + +impl Eq for IoError {} + +impl PartialEq for IoError { + fn eq(&self, other: &Self) -> bool { + self.repr.kind() == other.repr.kind() + } +} + +impl PartialOrd for IoError { + fn partial_cmp(&self, other: &Self) -> Option { + self.repr.kind().partial_cmp(&other.repr.kind()) + } +} + +#[derive(Error, Debug, Display, From, Into, Deref)] +pub struct SerdeJsonError { + pub(crate) repr: serde_json::Error, +} + +impl std::hash::Hash for SerdeJsonError { + fn hash(&self, state: &mut H) { + self.repr.to_string().hash(state); + } +} + +impl Eq for SerdeJsonError {} + +impl PartialEq for SerdeJsonError { + fn eq(&self, other: &Self) -> bool { + self.repr.to_string() == other.repr.to_string() + } +} + +impl PartialOrd for SerdeJsonError { + fn partial_cmp(&self, other: &Self) -> Option { + self.repr.to_string().partial_cmp(&other.repr.to_string()) + } +} + +#[derive(Error, Clone, Debug, Eq, Display, From, Into, Deref)] +pub struct TomlDeError { + pub(crate) repr: toml::de::Error, +} + +impl PartialEq for TomlDeError { + fn eq(&self, other: &Self) -> bool { + self.repr.to_string() == other.repr.to_string() + } +} + +impl std::hash::Hash for TomlDeError { + fn hash(&self, state: &mut H) { + self.repr.to_string().hash(state); + } +} diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 000000000..adc2753ac --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,26 @@ +use std::fs::{File, OpenOptions}; +use std::path::Path; +use std::sync::Arc; + +use crate::errors::FilePathError; + +/// . +/// +/// # Errors +/// +/// This function will return an error if . +pub fn get_file_at(at: &Path, mode: &OpenOptions) -> Result<(File, Box), FilePathError> { + let file = mode.open(at).map_err(|err| FilePathError::FilePathIsNotAvailable { + input: at.into(), + source: (Arc::new(err) as Arc).into(), + })?; + + let at = Path::new(at) + .canonicalize() + .map_err(|err| FilePathError::FilePathIsUnresolvable { + input: at.into(), + source: (Arc::new(err) as Arc).into(), + })?; + + Ok((file, at.into_boxed_path())) +} diff --git a/src/http/mod.rs b/src/http/mod.rs index 039a2067b..44100cdb5 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -10,8 +10,17 @@ //! - //! +use std::net::SocketAddr; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; + use serde::{Deserialize, Serialize}; +use crate::errors::settings::ServiceSettingsError; +use crate::settings::{Service, ServiceProtocol}; +use crate::{check_field_is_not_empty, check_field_is_not_none}; + pub mod axum_implementation; pub mod percent_encoding; pub mod warp_implementation; @@ -21,3 +30,119 @@ pub enum Version { Warp, Axum, } + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct HttpServiceSettings { + pub id: String, + pub enabled: bool, + pub display_name: String, + pub socket: SocketAddr, +} + +impl Default for HttpServiceSettings { + fn default() -> Self { + Self { + id: "default_http".to_string(), + enabled: false, + display_name: "HTTP (default)".to_string(), + socket: SocketAddr::from_str("0.0.0.0:6969").unwrap(), + } + } +} + +impl TryFrom<(&String, &Service)> for HttpServiceSettings { + type Error = ServiceSettingsError; + + fn try_from(value: (&String, &Service)) -> Result { + check_field_is_not_none!(value.1 => ServiceSettingsError; + enabled, service); + + if value.1.service.unwrap() != ServiceProtocol::Http { + return Err(ServiceSettingsError::WrongService { + field: "service".to_string(), + expected: ServiceProtocol::Http, + found: value.1.service.unwrap(), + data: value.1.into(), + }); + } + + check_field_is_not_empty!(value.1 => ServiceSettingsError; + display_name: String); + + Ok(Self { + id: value.0.clone(), + enabled: value.1.enabled.unwrap(), + display_name: value.1.display_name.clone().unwrap(), + socket: value.1.get_socket()?, + }) + } +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct TlsServiceSettings { + pub id: String, + pub enabled: bool, + pub display_name: String, + pub socket: SocketAddr, + pub certificate_file_path: Box, + pub key_file_path: Box, +} + +impl Default for TlsServiceSettings { + fn default() -> Self { + Self { + id: "default_http".to_string(), + enabled: false, + display_name: "HTTP (default)".to_string(), + socket: SocketAddr::from_str("0.0.0.0:6969").unwrap(), + certificate_file_path: Path::new("").into(), + key_file_path: Path::new("").into(), + } + } +} + +impl TryFrom<(&String, &Service)> for TlsServiceSettings { + type Error = ServiceSettingsError; + + fn try_from(value: (&String, &Service)) -> Result { + check_field_is_not_none!(value.1 => ServiceSettingsError; + enabled, service, tls); + + if value.1.service.unwrap() != ServiceProtocol::Tls { + return Err(ServiceSettingsError::WrongService { + field: "service".to_string(), + expected: ServiceProtocol::Tls, + found: value.1.service.unwrap(), + data: value.1.into(), + }); + } + + check_field_is_not_empty!(value.1 => ServiceSettingsError; + display_name: String); + + let tls = value.1.tls.clone().unwrap(); + + Ok(Self { + id: value.0.clone(), + enabled: value.1.enabled.unwrap(), + display_name: value.1.display_name.clone().unwrap(), + socket: value.1.get_socket()?, + + certificate_file_path: tls + .get_certificate_file_path() + .map_err(|err| ServiceSettingsError::TlsSettingsError { + field: value.0.clone(), + source: (Arc::new(err) as Arc).into(), + data: value.1.into(), + })?, + + key_file_path: tls + .get_key_file_path() + .map_err(|err| ServiceSettingsError::TlsSettingsError { + field: value.0.clone(), + source: (Arc::new(err) as Arc).into(), + data: value.1.into(), + })?, + }) + } +} diff --git a/src/jobs/tracker_apis.rs b/src/jobs/tracker_apis.rs index 00e39eeba..e72a8cd73 100644 --- a/src/jobs/tracker_apis.rs +++ b/src/jobs/tracker_apis.rs @@ -5,7 +5,7 @@ use log::info; use tokio::sync::oneshot; use tokio::task::JoinHandle; -use crate::apis::server; +use crate::apis::{self, server}; use crate::config::HttpApi; use crate::tracker; @@ -15,14 +15,14 @@ pub struct ApiServerJobStarted(); /// # Panics /// /// It would panic if unable to send the `ApiServerJobStarted` notice. -pub async fn start_job(config: &HttpApi, tracker: Arc) -> JoinHandle<()> { - let bind_addr = config - .bind_address - .parse::() - .expect("Tracker API bind_address invalid."); - let ssl_enabled = config.ssl_enabled; - let ssl_cert_path = config.ssl_cert_path.clone(); - let ssl_key_path = config.ssl_key_path.clone(); +pub async fn start_job(config: &Arc, tracker: Arc) -> JoinHandle<()> { + let bind_addr = config.get_socket().expect("we need a socket..."); + let ssl_enabled = config.get_tls_settings().is_some(); + let ssl_cert_path = config + .get_tls_settings() + .as_ref() + .map(|t| t.get_certificate_file_path().clone()); + let ssl_key_path = config.get_tls_settings().as_ref().map(|t| t.get_key_file_path().clone()); let (tx, rx) = oneshot::channel::(); diff --git a/src/lib.rs b/src/lib.rs index cbda2854c..e080d63eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,16 +1,29 @@ pub mod apis; pub mod config; pub mod databases; +pub mod errors; +pub mod helpers; pub mod http; pub mod jobs; pub mod located_error; pub mod logging; pub mod protocol; +pub mod settings; pub mod setup; pub mod stats; pub mod tracker; pub mod udp; +pub mod config_const { + pub const CONFIG_FOLDER: &str = "config"; + pub const CONFIG_BACKUP_FOLDER: &str = "config.backup"; + pub const CONFIG_ERROR_FOLDER: &str = "config.error"; + pub const CONFIG_DEFAULT: &str = "default"; + pub const CONFIG_LOCAL: &str = "local"; + pub const CONFIG_OVERRIDE: &str = "override"; + pub const CONFIG_OLD: &str = "../config"; +} + #[macro_use] extern crate lazy_static; @@ -32,3 +45,7 @@ pub mod ephemeral_instance_keys { pub static ref RANDOM_SEED: Seed = Rng::gen(&mut ThreadRng::default()); } } + +pub trait Empty: Sized { + fn empty() -> Self; +} diff --git a/src/settings/manager.rs b/src/settings/manager.rs new file mode 100644 index 000000000..674d96c63 --- /dev/null +++ b/src/settings/manager.rs @@ -0,0 +1,641 @@ +use std::collections::hash_map::DefaultHasher; +use std::ffi::OsString; +use std::fs::{self, OpenOptions}; +use std::hash::{Hash, Hasher}; +use std::io::{Cursor, Read, Write}; +use std::path::Path; +use std::sync::Arc; + +use log::{info, warn}; + +use super::{ + Settings, SettingsErrored, SettingsNamespace, TrackerSettings, TrackerSettingsBuilder, SETTINGS_NAMESPACE, + SETTINGS_NAMESPACE_ERRORED, +}; +use crate::config_const::{CONFIG_BACKUP_FOLDER, CONFIG_DEFAULT, CONFIG_ERROR_FOLDER, CONFIG_FOLDER, CONFIG_LOCAL, CONFIG_OLD}; +use crate::errors::settings_manager::SettingsManagerError; +use crate::errors::wrappers::{IoError, SerdeJsonError}; +use crate::helpers::get_file_at; +use crate::located_error::Located; +use crate::settings::{Clean, Fix}; +use crate::Empty; + +#[derive(PartialEq, Eq, Debug, Clone, Hash)] +pub struct SettingsManager { + settings: Result, +} + +impl Default for SettingsManager { + fn default() -> Self { + Self { + settings: Ok(Settings::default()), + } + } +} + +impl From for SettingsManager { + fn from(okay: Settings) -> Self { + Self { settings: Ok(okay) } + } +} + +impl TryFrom for Settings { + type Error = SettingsErrored; + + fn try_from(manager: SettingsManager) -> Result { + manager.settings + } +} + +impl From for SettingsManager { + fn from(error: SettingsErrored) -> Self { + Self { settings: Err(error) } + } +} + +impl SettingsManager { + #[must_use] + pub fn empty() -> Self { + Self { + settings: Ok(Empty::empty()), + } + } + + #[must_use] + pub fn error(errored: &SettingsErrored) -> Self { + Self { + settings: Err(errored.clone()), + } + } + + /// . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn setup() -> Result { + let config = Path::new(CONFIG_FOLDER); + let backup = Path::new(CONFIG_BACKUP_FOLDER); + let error = Path::new(CONFIG_ERROR_FOLDER); + + let default = Path::new(CONFIG_FOLDER).join(CONFIG_DEFAULT).with_extension("json"); + let old = Path::new(CONFIG_FOLDER).join(CONFIG_OLD).with_extension("toml"); + let local = Path::new(CONFIG_FOLDER).join(CONFIG_LOCAL).with_extension("json"); + + Self::make_folder(config)?; + + Self::write_default(default.as_path())?; + let manager = Self::load(old.as_path(), local.as_path(), backup, error)?; + + manager.save(local.as_path(), &Some(backup.into()))?; + + Ok(manager) + } + + /// . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn load(old: &Path, local: &Path, backup_folder: &Path, error_folder: &Path) -> Result { + if let Some(res) = Self::import_old(old, backup_folder, error_folder)? { + return Ok(res); + } + + // If no old settings, lets try the local settings. + let local_settings = match Self::read(local) { + Ok(settings) => Some(settings), + Err(err) => match err { + SettingsManagerError::FailedToOpenFileForReading { .. } => { + info!("No Configuration To Load: {err}"); + None + } + err => { + return Err(err); + } + }, + }; + + if let Some(res) = local_settings { + return Ok(res); + }; + + // if nothing else, lets load the default. + Ok(SettingsManager::default()) + } + + /// . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn save(&self, to: &Path, archive_folder: &Option>) -> Result<(), SettingsManagerError> { + // lets backup the previous configuration, if we have any... + let existing = get_file_at(to, OpenOptions::new().read(true)).ok(); + + if let Some(existing) = existing { + if let Some(archive_folder) = archive_folder { + Self::archive(existing.0, &existing.1, archive_folder)?; + } + } + + let dest = get_file_at(to, OpenOptions::new().write(true).create(true).truncate(true)).map_err(|err| { + SettingsManagerError::FailedToOpenFileForWriting { + source: (Arc::new(err) as Arc).into(), + } + })?; + + self.write(dest.0) + } + + /// . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn write_default(to: &Path) -> Result<(), SettingsManagerError> { + let dest = get_file_at(to, OpenOptions::new().write(true).create(true).truncate(true)).map_err(|err| { + SettingsManagerError::FailedToOpenFileForWriting { + source: (Arc::new(err) as Arc).into(), + } + })?; + + Self::default().write(dest.0) + } + + /// . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn read(from: &Path) -> Result { + let source = + get_file_at(from, OpenOptions::new().read(true)).map_err(|err| SettingsManagerError::FailedToOpenFileForReading { + source: (Arc::new(err) as Arc).into(), + })?; + + Self::read_json(source.0) + } + + /// . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn write(&self, writer: impl Write) -> Result<(), SettingsManagerError> { + self.write_json(writer) + } + + /// . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn read_json(mut rdr: R) -> Result + where + R: Read, + { + let data: &mut Vec = &mut Vec::default(); + + rdr.read_to_end(data) + .map_err(|err| SettingsManagerError::FailedToReadFromBuffer { + source: (Arc::new(err) as Arc).into(), + })?; + + let settings = serde_json::from_reader::>, SettingsNamespace>(Cursor::new(data)).map_err(|err| { + SettingsManagerError::FailedToDeserializeFromJson { + message: "(read as \"SettingsNamespace\")".to_string(), + source: (Arc::new(err) as Arc).into(), + } + })?; + { + match settings.namespace.as_str() { + SETTINGS_NAMESPACE => serde_json::from_reader::>, Settings>(Cursor::new(data)) + .map_err(|err| SettingsManagerError::FailedToDeserializeFromJson { + message: "(read as \"Settings\")".to_string(), + source: (Arc::new(err) as Arc).into(), + }) + .map(SettingsManager::from), + + SETTINGS_NAMESPACE_ERRORED => serde_json::from_reader::>, SettingsErrored>(Cursor::new(data)) + .map_err(|err| SettingsManagerError::FailedToDeserializeFromJson { + message: "(read as \"SettingsErrored\")".to_string(), + source: (Arc::new(err) as Arc).into(), + }) + .map(SettingsManager::from), + + namespace => Err(SettingsManagerError::FailedToMatchNamespace { + namespace: namespace.to_string(), + }), + } + } + } + + /// . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn write_json(&self, writer: W) -> Result<(), SettingsManagerError> + where + W: Write, + { + match &self.settings { + Ok(okay) => { + serde_json::to_writer_pretty(writer, okay).map_err(|err| SettingsManagerError::FailedToDeserializeFromJson { + message: "(read as \"Settings\")".to_string(), + source: (Arc::new(err) as Arc).into(), + }) + } + Err(err) => { + serde_json::to_writer_pretty(writer, err).map_err(|err| SettingsManagerError::FailedToDeserializeFromJson { + message: "(read as \"SettingsErrored\")".to_string(), + source: (Arc::new(err) as Arc).into(), + }) + } + } + } + + fn backup(&self, to: &Path, folder: &Path) -> Result<(), SettingsManagerError> { + let ext = match to.extension().map(std::ffi::OsStr::to_os_string) { + Some(mut ext) => { + ext.push(".json"); + ext + } + None => OsString::from("json"), + }; + + let data: &mut Vec = &mut Vec::default(); + + self.write_json(data.by_ref()) + .map_err(|err| SettingsManagerError::FailedToWriteToFile { + message: "(backup)".to_string(), + to: to.into(), + + source: (Arc::new(err) as Arc).into(), + })?; + + Self::archive(Cursor::new(data), &to.with_extension(ext), folder)?; + Ok(()) + } + + fn archive(mut rdr: impl Read, from: &Path, to_folder: &Path) -> Result<(), SettingsManagerError> { + Self::make_folder(to_folder)?; + + let to_folder = to_folder + .canonicalize() + .map_err(|err| SettingsManagerError::FailedToResolvePath { + at: to_folder.into(), + source: (Arc::new(err) as Arc).into(), + })?; + + let mut hasher: DefaultHasher = DefaultHasher::default(); + let data: &mut Vec = &mut Vec::default(); + + // todo: lock and stream the file instead of loading the full file into memory. + let _size = rdr + .read_to_end(data) + .map_err(|err| SettingsManagerError::FailedToReadFromBuffer { + source: (Arc::new(err) as Arc).into(), + }) + .map_err(|err| SettingsManagerError::FailedToReadFromFile { + message: "(archive, read into)".to_string(), + from: from.into(), + source: (Arc::new(err) as Arc).into(), + })?; + + data.hash(&mut hasher); + + let ext = match from.extension() { + Some(ext) => { + let mut ostr = OsString::from(format!("{}.", hasher.finish())); + ostr.push(ext); + ostr + } + None => OsString::from(hasher.finish().to_string()), + }; + + let to = to_folder.join(from.file_name().unwrap()).with_extension(ext); + + // if we do not have a backup already, lets make one. + if to.canonicalize().is_err() { + let mut dest = get_file_at(&to, OpenOptions::new().write(true).create_new(true)).map_err(|err| { + SettingsManagerError::FailedToCreateNewFile { + source: (Arc::new(err) as Arc).into(), + } + })?; + + dest.0.write_all(data).map_err(|a| { + let b = SettingsManagerError::FailedToWriteIntoBuffer { + source: (Arc::new(a) as Arc).into(), + }; + + SettingsManagerError::FailedToWriteToFile { + to: dest.1, + message: "(archive, making backup)".to_string(), + source: (Arc::new(b) as Arc).into(), + } + })?; + }; + + Ok(()) + } + + /// . + /// + /// # Panics + /// + /// Panics if . + /// + /// # Errors + /// + /// This function will return an error if . + #[allow(clippy::too_many_lines)] + pub fn import_old(from: &Path, backup_folder: &Path, error_folder: &Path) -> Result, SettingsManagerError> { + let import_error_folder = error_folder.join("import"); + + let Ok(mut file) = get_file_at(from, OpenOptions::new().read(true)) else { return Ok(None) }; + + let data: &mut Vec = &mut Vec::default(); + + let _ = file.0.read_to_end(data).map_err(|a| { + let b = SettingsManagerError::FailedToReadFromBuffer { + source: (Arc::new(a) as Arc).into(), + }; + let c = SettingsManagerError::FailedToReadFromFile { + message: "(old_file)".to_string(), + from: file.1.clone(), + source: (Arc::new(b) as Arc).into(), + }; + + SettingsManagerError::FailedToProcessOldSettings { + source: (Arc::new(c) as Arc).into(), + } + })?; + + let parsed = toml::de::from_slice(data.as_slice()).map_err(|a| { + let b = SettingsManagerError::FailedToDeserializeFromToml { + source: (Arc::new(a) as Arc).into(), + }; + + let c = SettingsManagerError::FailedToReadFromFile { + message: "(old settings toml)".to_string(), + from: file.1.clone(), + source: (Arc::new(b) as Arc).into(), + }; + + SettingsManagerError::FailedToProcessOldSettings { + source: (Arc::new(c) as Arc).into(), + } + })?; + + let mut builder = TrackerSettingsBuilder::empty(); + + // Attempt One + let test_builder = builder.clone().import_old(&parsed); + { + if let Err(err) = TryInto::::try_into(test_builder.clone()) { + Self::make_folder(error_folder)?; + Self::make_folder(&import_error_folder)?; + let test = "First"; + + warn!( + "{} import attempt failed: {}\nWith Error: {}", + test, + import_error_folder.to_string_lossy(), + err + ); + + let broken = Self::error(&SettingsErrored::new(&test_builder.tracker_settings, &err)); + + let ext = match file.1.extension().map(std::ffi::OsStr::to_os_string) { + Some(mut ext) => { + ext.push(format!(".{}", test.to_lowercase())); + ext + } + None => OsString::from(test.to_lowercase()), + }; + + broken.backup(&file.1.with_extension(ext), import_error_folder.as_path())?; + } + + // Replace broken with default, and remove everything else. + + builder = test_builder.tracker_settings.empty_fix().into(); + } + + // Attempt with Defaults + let test_builder = builder.clone().import_old(&parsed); + { + if let Err(err) = TryInto::::try_into(test_builder.clone()) { + Self::make_folder(error_folder)?; + Self::make_folder(&import_error_folder)?; + let test = "Second"; + + warn!( + "{} import attempt failed: {}\nWith Error: {}", + test, + import_error_folder.to_string_lossy(), + err + ); + + let broken = Self::error(&SettingsErrored::new(&test_builder.tracker_settings, &err)); + + let ext = match file.1.extension().map(std::ffi::OsStr::to_os_string) { + Some(mut ext) => { + ext.push(format!(".{}", test.to_lowercase())); + ext + } + None => OsString::from(test.to_lowercase()), + }; + + broken.backup(&file.1.with_extension(ext), import_error_folder.as_path())?; + } + + builder = test_builder.tracker_settings.clean().into(); + } + + // Final Attempt + let settings = match TryInto::::try_into(builder.clone()) { + Ok(tracker) => Self { + settings: Ok(tracker.into()), + }, + + Err(err) => { + Self::make_folder(error_folder)?; + Self::make_folder(&import_error_folder)?; + let test = "Final"; + + warn!( + "{} import attempt failed: {}\nWith Error: {}", + test, + import_error_folder.to_string_lossy(), + err + ); + + let broken = Self::error(&SettingsErrored::new(&builder.tracker_settings, &err)); + + let ext = match file.1.extension().map(std::ffi::OsStr::to_os_string) { + Some(mut ext) => { + ext.push(format!(".{}", test.to_lowercase())); + ext + } + None => OsString::from(test.to_lowercase()), + }; + + broken.backup(&file.1.with_extension(ext), import_error_folder.as_path())?; + + return Err(SettingsManagerError::FailedToImportOldSettings { + from: file.1, + source: (Arc::new(err) as Arc).into(), + }); + } + }; + + let ext = match file.1.extension() { + Some(ext) => { + let mut ostr = OsString::from("old."); + ostr.push(ext); + ostr + } + None => OsString::from("old"), + }; + + // import was successful, lets rename the extension to ".toml.old". + let backup = backup_folder.join(file.1.file_name().unwrap()).with_extension(ext); + Self::make_folder(backup_folder)?; + + match fs::rename(&file.1, &backup) { + Ok(_) => { + info!( + "\nOld Settings Was Successfully Imported!\n And moved from: \"{}\", to: \"{}\".\n", + file.1.display(), + backup.display() + ); + Ok(Some(settings)) + } + Err(err) => Err(SettingsManagerError::FailedToMoveFile { + from: file.1, + to: backup.into_boxed_path(), + source: (Arc::new(err) as Arc).into(), + }), + } + } + + /// . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn make_folder(folder: &Path) -> Result<(), SettingsManagerError> { + if let Ok(path) = folder.canonicalize() { + if path.is_dir() { + return Ok(()); + } + return Err(SettingsManagerError::FailedToResolveDirectory { at: folder.into() }); + } + match fs::create_dir(folder) { + Ok(_) => Ok(()), + Err(err) => Err(SettingsManagerError::FailedToPrepareDirectory { + at: folder.into(), + source: (Arc::new(err) as Arc).into(), + }), + } + } +} + +#[cfg(test)] +mod tests { + use std::env; + use std::fs::OpenOptions; + use std::io::{Seek, Write}; + + use thiserror::Error; + use uuid::Uuid; + + use super::SettingsManager; + use crate::helpers::get_file_at; + use crate::settings::old_settings::OLD_DEFAULT; + use crate::settings::{Settings, SettingsErrored, TrackerSettings}; + + #[test] + fn it_should_attempt_the_default_setup() { + SettingsManager::setup().unwrap(); + } + + #[test] + fn it_should_import_the_old_default_settings() { + let tmp_dir = env::temp_dir(); + let old = tmp_dir.join(format!("old_default_{}.json", Uuid::new_v4())); + let backup = tmp_dir.join("backup"); + let error = tmp_dir.join("error"); + + let mut dest = get_file_at(old.as_path(), OpenOptions::new().write(true).create_new(true)).unwrap(); + + dest.0.write_all(OLD_DEFAULT.as_bytes()).unwrap(); + + SettingsManager::import_old(old.as_path(), backup.as_path(), error.as_path()).unwrap(); + } + + #[test] + fn it_should_write_and_read_the_default() { + let temp = env::temp_dir().as_path().join(format!("test_config_{}.json", Uuid::new_v4())); + + assert!(!temp.exists()); + + SettingsManager::write_default(&temp).unwrap(); + + assert!(temp.is_file()); + + let manager = SettingsManager::read(&temp).unwrap(); + + assert_eq!(manager, SettingsManager::default()); + } + + #[test] + fn it_should_make_config_folder() { + let temp = env::temp_dir().as_path().join(format!("test_config_{}", Uuid::new_v4())); + + assert!(!temp.exists()); + + SettingsManager::make_folder(&temp).unwrap(); + + assert!(temp.is_dir()); + } + + #[test] + fn it_should_write_and_read_errored_settings() { + #[derive(Error, Debug)] + enum TestErrors { + #[error("Test Error!")] + Error, + } + + let path = env::temp_dir().as_path().join(format!("test_errored.{}", Uuid::new_v4())); + let mut file_rw = get_file_at(&path, OpenOptions::new().write(true).read(true).create_new(true)).unwrap(); + + let errored: SettingsManager = SettingsErrored::new(&TrackerSettings::default(), &TestErrors::Error).into(); + + errored.write_json(std::io::Write::by_ref(&mut file_rw.0)).unwrap(); + file_rw.0.rewind().unwrap(); + + let error_returned = SettingsManager::read_json(file_rw.0).unwrap(); + + assert_eq!(errored, error_returned); + } + + #[test] + fn it_should_write_and_read_settings() { + let path = env::temp_dir().as_path().join(format!("test_errored.{}", Uuid::new_v4())); + let mut file_rw = get_file_at(&path, OpenOptions::new().write(true).read(true).create_new(true)).unwrap(); + + let settings: SettingsManager = Settings::default().into(); + + settings.write_json(std::io::Write::by_ref(&mut file_rw.0)).unwrap(); + file_rw.0.rewind().unwrap(); + + let settings_returned = SettingsManager::read_json(file_rw.0).unwrap(); + + assert_eq!(settings, settings_returned); + } +} diff --git a/src/settings/mod.rs b/src/settings/mod.rs new file mode 100644 index 000000000..b21f67b4c --- /dev/null +++ b/src/settings/mod.rs @@ -0,0 +1,1151 @@ +use std::collections::btree_map::Entry::Vacant; +use std::collections::hash_map::RandomState; +use std::collections::{BTreeMap, HashSet}; +use std::fs::OpenOptions; +use std::net::{IpAddr, SocketAddr}; +use std::path::Path; +use std::str::FromStr; +use std::sync::Arc; + +use derive_more::{Deref, DerefMut, Display}; +use serde::{Deserialize, Serialize}; + +use self::old_settings::DatabaseDriversOld; +use crate::apis::settings::ApiTokens; +use crate::errors::settings::{ + CommonSettingsError, GlobalSettingsError, ServiceSettingsError, SettingsError, TlsSettingsError, TrackerSettingsError, +}; +use crate::helpers::get_file_at; +use crate::http::{HttpServiceSettings, TlsServiceSettings}; +use crate::tracker::mode::Mode; +use crate::tracker::services::common::{Tls, TlsBuilder}; +use crate::udp::UdpServiceSettings; +use crate::{apis, databases, Empty}; + +pub mod manager; +pub mod old_settings; + +#[macro_export] +macro_rules! old_to_new { + ( $( $base_old:expr, $base_new:expr; $($old:ident: $new:ident),+ )? ) => { + { + $( $( + if let Some(val) = $base_old.$old{ + $base_new.$new = Some(val) + } + )+ + )? + } + }; +} + +#[macro_export] +macro_rules! check_field_is_not_none { + ( $( $ctx:expr => $error:ident; $($value:ident),+ )? ) => { + { + $( $( + if $ctx.$value.is_none() { + return Err($error::MissingRequiredField { + field: format!("{}", stringify!($value)), + data: $ctx.into(), + }) + }; + )+ + )? + } + }; +} + +#[macro_export] +macro_rules! check_field_is_not_empty { + ( $( $ctx:expr => $error:ident;$($value:ident : $value_type:ty),+ )? ) => { + { + $( $( + match &$ctx.$value { + Some(value) => { + if value == &<$value_type>::default(){ + return Err($error::EmptyRequiredField { + field: format!("{}", stringify!($value)), + data: $ctx.into()}); + } + }, + None => { + return Err($error::MissingRequiredField { + field: format!("{}", stringify!($value)), + data: $ctx.into(), + }); + }, + } + )+ + )? + } + }; +} + +trait Clean { + fn clean(self) -> Self; +} + +trait Fix { + fn fix(self) -> Self; + fn empty_fix(self) -> Self; +} + +const SETTINGS_NAMESPACE: &str = "org.torrust.tracker.config"; +const SETTINGS_NAMESPACE_ERRORED: &str = "org.torrust.tracker.config.errored"; +const SETTINGS_VERSION: &str = "1.0.0"; + +/// Only used to check what is the namespace when deserializing. +#[derive(Deserialize)] +pub struct SettingsNamespace { + pub namespace: String, +} + +/// With an extra 'error' field, used when there are deserializing problems. +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +pub struct SettingsErrored { + pub namespace: String, + pub version: String, + pub error: String, + pub tracker: TrackerSettings, +} + +impl SettingsErrored { + pub fn new(tracker: &TrackerSettings, error: &impl std::error::Error) -> Self { + Self { + namespace: SETTINGS_NAMESPACE_ERRORED.to_string(), + version: SETTINGS_VERSION.to_string(), + error: error.to_string(), + tracker: tracker.clone(), + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +pub struct Settings { + pub namespace: String, + pub version: String, + tracker: TrackerSettings, +} + +impl Default for Settings { + fn default() -> Self { + Self { + namespace: SETTINGS_NAMESPACE.to_string(), + version: SETTINGS_VERSION.to_string(), + tracker: TrackerSettings::default(), + } + } +} + +impl Empty for Settings { + fn empty() -> Self { + Self { + namespace: String::default(), + version: String::default(), + tracker: Empty::empty(), + } + } +} + +impl From for Settings { + fn from(tracker: TrackerSettings) -> Self { + Self { + namespace: SETTINGS_NAMESPACE.to_string(), + version: SETTINGS_VERSION.to_string(), + tracker, + } + } +} + +impl From for TrackerSettings { + fn from(settings: Settings) -> Self { + settings.tracker + } +} + +impl Settings { + /// Returns the check of this [`Settings`]. + /// + /// # Errors + /// + /// This function will return an error if . + pub fn check(&self) -> Result<(), Box> { + if self.namespace != *SETTINGS_NAMESPACE { + return Err(Box::new(SettingsError::NamespaceError { + message: format!("Actual: \"{}\", Expected: \"{}\"", self.namespace, SETTINGS_NAMESPACE), + field: "tracker".to_string(), + })); + } + + // Todo: Make this Check use Semantic Versioning 2.0.0 + if self.version != *SETTINGS_VERSION { + return Err(Box::new(SettingsError::VersionError { + message: format!("Actual: \"{}\", Expected: \"{}\"", self.version, SETTINGS_NAMESPACE), + field: "version".to_string(), + })); + } + + if let Err(err) = self.tracker.check() { + return Err(Box::new(SettingsError::TrackerSettingsError { + message: err.to_string(), + field: err.get_field(), + source: (Arc::new(err) as Arc).into(), + })); + } + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +pub struct TrackerSettings { + pub global: Option, + pub common: Option, + pub database: Option, + pub services: Option, +} + +impl Default for TrackerSettings { + fn default() -> Self { + Self { + global: Some(GlobalSettings::default()), + common: Some(CommonSettings::default()), + database: Some(databases::settings::SettingsBuilder::default().build().unwrap()), + services: Some(Services::default()), + } + } +} + +impl Empty for TrackerSettings { + fn empty() -> Self { + Self { + global: None, + common: None, + database: None, + services: None, + } + } +} + +impl TrackerSettings { + fn check(&self) -> Result<(), TrackerSettingsError> { + check_field_is_not_none!(self.clone() => TrackerSettingsError; + global, common, database, services + ); + Ok(()) + } +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] +pub struct TrackerSettingsBuilder { + tracker_settings: TrackerSettings, +} + +impl Empty for TrackerSettingsBuilder { + fn empty() -> Self { + Self { + tracker_settings: Empty::empty(), + } + } +} + +impl From for TrackerSettingsBuilder { + fn from(tracker_settings: TrackerSettings) -> Self { + Self { tracker_settings } + } +} + +impl From> for TrackerSettingsBuilder { + fn from(tracker_settings: Arc) -> Self { + Self { + tracker_settings: (*tracker_settings).clone(), + } + } +} + +impl Fix for TrackerSettings { + /// Replaces with Defaults. + fn fix(self) -> Self { + Self { + global: Some(self.global.filter(|p| p.check().is_ok()).unwrap_or_default()), + common: Some(self.common.filter(|p| p.check().is_ok()).unwrap_or_default()), + database: Some(self.database.filter(|p| p.check().is_ok()).unwrap_or_default()), + services: Some(self.services.filter(|p| p.check().is_ok()).unwrap_or_default()), + } + } + + /// Replaces problems, removing everything else, all services are removed. + fn empty_fix(self) -> Self { + Self { + global: self + .global + .filter(|p| p.check().is_ok()) + .map_or_else(|| Some(GlobalSettings::default()), |_f| None), + common: self + .common + .filter(|p| p.check().is_ok()) + .map_or_else(|| Some(CommonSettings::default()), |_f| None), + database: self.database.filter(|p| p.check().is_ok()).map_or_else( + || Some(databases::settings::SettingsBuilder::default().build().unwrap()), + |_f| None, + ), + services: None, + } + } +} + +impl Clean for TrackerSettings { + /// Removes Problems + fn clean(self) -> Self { + Self { + global: self.global.filter(|p| p.check().is_ok()), + common: self.common.filter(|p| p.check().is_ok()), + database: self.database.filter(|p| p.check().is_ok()), + services: self.services.map(Clean::clean), + } + } +} + +impl TryInto for TrackerSettingsBuilder { + type Error = SettingsError; + + fn try_into(self) -> Result { + if let Err(err) = self.tracker_settings.check() { + return Err(SettingsError::TrackerSettingsError { + message: err.to_string(), + field: err.get_field(), + source: (Arc::new(err) as Arc).into(), + }); + } + + let settings = TrackerSettings { + global: Some(GlobalSettingsBuilder::from(self.tracker_settings.global.unwrap()).try_into()?), + common: Some(CommonSettingsBuilder::from(self.tracker_settings.common.unwrap()).try_into()?), + database: Some(self.tracker_settings.database.unwrap()), + services: match self.tracker_settings.services { + Some(services) => Some(ServicesBuilder::from(services).try_into()?), + None => None, + }, + }; + + Ok(settings) + } +} + +impl TrackerSettingsBuilder { + #[must_use] + pub fn with_global(self, global: &GlobalSettings) -> Self { + Self { + tracker_settings: TrackerSettings { + global: Some(global.clone()), + common: self.tracker_settings.common, + database: self.tracker_settings.database, + services: self.tracker_settings.services, + }, + } + } + + #[must_use] + pub fn with_common(self, common: &CommonSettings) -> Self { + Self { + tracker_settings: TrackerSettings { + global: self.tracker_settings.global, + common: Some(common.clone()), + database: self.tracker_settings.database, + services: self.tracker_settings.services, + }, + } + } + + #[must_use] + pub fn with_database(self, database: &databases::settings::Settings) -> Self { + Self { + tracker_settings: TrackerSettings { + global: self.tracker_settings.global, + common: self.tracker_settings.common, + database: Some(database.clone()), + services: self.tracker_settings.services, + }, + } + } + + #[must_use] + pub fn with_services(self, services: &Services) -> Self { + Self { + tracker_settings: TrackerSettings { + global: self.tracker_settings.global, + common: self.tracker_settings.common, + database: self.tracker_settings.database, + services: Some(services.clone()), + }, + } + } + + /// . + /// + /// # Panics + /// + /// Panics if . + #[must_use] + pub fn import_old(mut self, old_settings: &old_settings::Settings) -> Self { + // Global + let mut builder = match self.tracker_settings.global.as_ref() { + Some(settings) => GlobalSettingsBuilder::from(settings.clone()), + None => GlobalSettingsBuilder::empty(), + }; + builder = builder.import_old(old_settings); + + self.tracker_settings.global = Some(builder.global_settings); + + // Common + let mut builder = match self.tracker_settings.common.as_ref() { + Some(settings) => CommonSettingsBuilder::from(settings.clone()), + None => CommonSettingsBuilder::empty(), + }; + builder = builder.import_old(old_settings); + + self.tracker_settings.common = Some(builder.common_settings); + + // Database + self.tracker_settings.database = old_settings.db_driver.map(|d| match d { + DatabaseDriversOld::Sqlite3 => databases::settings::SettingsBuilder::default() + .driver(databases::driver::Driver::Sqlite3) + .sql_lite_3_db_file_path(old_settings.db_path.as_ref().map(|p| Path::new(p).into())) + .build() + .unwrap(), + DatabaseDriversOld::MySQL => databases::settings::SettingsBuilder::default() + .driver(databases::driver::Driver::MySQL) + .my_sql_connection_url(old_settings.db_path.as_ref().cloned()) + .build() + .unwrap(), + }); + + // Services + let mut builder = match self.tracker_settings.services.as_ref() { + Some(settings) => ServicesBuilder::from(settings.clone()), + None => ServicesBuilder::empty(), + }; + builder = builder.import_old(old_settings); + + self.tracker_settings.services = Some(builder.services); + + self + } +} + +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +pub struct GlobalSettings { + tracker_mode: Option, + log_filter_level: Option, + external_ip: Option, + on_reverse_proxy: Option, +} + +impl Default for GlobalSettings { + fn default() -> Self { + Self { + tracker_mode: Some(Mode::Listed), + log_filter_level: Some(LogFilterLevel::Info), + external_ip: None, + on_reverse_proxy: Some(false), + } + } +} + +impl Empty for GlobalSettings { + fn empty() -> Self { + Self { + tracker_mode: None, + log_filter_level: None, + external_ip: None, + on_reverse_proxy: None, + } + } +} + +impl GlobalSettings { + fn check(&self) -> Result<(), GlobalSettingsError> { + self.is_on_reverse_proxy()?; + + Ok(()) + } + + #[must_use] + pub fn get_tracker_mode(&self) -> Mode { + self.tracker_mode.unwrap_or_default() + } + + #[must_use] + pub fn get_log_filter_level(&self) -> log::LevelFilter { + match self.log_filter_level.unwrap_or(LogFilterLevel::Info) { + LogFilterLevel::Off => log::LevelFilter::Off, + LogFilterLevel::Error => log::LevelFilter::Error, + LogFilterLevel::Warn => log::LevelFilter::Warn, + LogFilterLevel::Info => log::LevelFilter::Info, + LogFilterLevel::Debug => log::LevelFilter::Debug, + LogFilterLevel::Trace => log::LevelFilter::Trace, + } + } + + #[must_use] + pub fn get_external_ip_opt(&self) -> Option { + self.external_ip + } + + /// Returns the is on reverse proxy of this [`GlobalSettings`]. + /// + /// # Panics + /// + /// Panics if . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn is_on_reverse_proxy(&self) -> Result { + check_field_is_not_none!(self.clone() => GlobalSettingsError; on_reverse_proxy); + + Ok(self.on_reverse_proxy.unwrap()) + } +} +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash, Default)] +pub struct GlobalSettingsBuilder { + global_settings: GlobalSettings, +} + +impl Empty for GlobalSettingsBuilder { + fn empty() -> Self { + Self { + global_settings: Empty::empty(), + } + } +} + +impl From for GlobalSettingsBuilder { + fn from(global_settings: GlobalSettings) -> Self { + Self { global_settings } + } +} + +impl From> for GlobalSettingsBuilder { + fn from(global_settings: Arc) -> Self { + Self { + global_settings: (*global_settings).clone(), + } + } +} + +impl TryInto for GlobalSettingsBuilder { + type Error = SettingsError; + + fn try_into(self) -> Result { + match self.global_settings.check() { + Ok(_) => Ok(self.global_settings), + Err(err) => Err(SettingsError::GlobalSettingsError { + message: err.to_string(), + field: err.get_field(), + source: (Arc::new(err) as Arc).into(), + }), + } + } +} + +impl GlobalSettingsBuilder { + #[must_use] + pub fn with_external_ip(mut self, external_ip: &IpAddr) -> Self { + self.global_settings.external_ip = Some(*external_ip); + self + } + + #[must_use] + pub fn with_log_filter(mut self, log_filter: &LogFilterLevel) -> Self { + self.global_settings.log_filter_level = Some(*log_filter); + self + } + + #[must_use] + pub fn with_mode(mut self, mode: Mode) -> Self { + self.global_settings.tracker_mode = Some(mode); + self + } + + #[must_use] + pub fn with_reverse_proxy(mut self, reverse_proxy: bool) -> Self { + self.global_settings.on_reverse_proxy = Some(reverse_proxy); + self + } + + #[must_use] + pub fn import_old(mut self, old_settings: &old_settings::Settings) -> Self { + if let Some(val) = old_settings.mode.as_ref() { + self.global_settings.tracker_mode = Some(match val { + old_settings::TrackerModeOld::Public => Mode::Public, + old_settings::TrackerModeOld::Listed => Mode::Listed, + old_settings::TrackerModeOld::Private => Mode::Private, + old_settings::TrackerModeOld::PrivateListed => Mode::PrivateListed, + }); + } + + if let Some(val) = old_settings.log_level.as_ref() { + self.global_settings.log_filter_level = match val.to_lowercase().as_str() { + "off" => Some(LogFilterLevel::Off), + "trace" => Some(LogFilterLevel::Trace), + "debug" => Some(LogFilterLevel::Debug), + "info" => Some(LogFilterLevel::Info), + "warn" => Some(LogFilterLevel::Warn), + "error" => Some(LogFilterLevel::Error), + _ => None, + } + } + + if let Some(val) = old_settings.external_ip.as_ref() { + if let Ok(ip) = IpAddr::from_str(val) { + self.global_settings.external_ip = Some(ip); + }; + } + + if let Some(val) = old_settings.on_reverse_proxy { + self.global_settings.on_reverse_proxy = Some(val); + } + self + } +} + +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +pub struct CommonSettings { + pub announce_interval_seconds: Option, + pub announce_interval_seconds_minimum: Option, + pub peer_timeout_seconds_maximum: Option, + pub enable_tracker_usage_statistics: Option, + pub enable_persistent_statistics: Option, + pub cleanup_inactive_peers_interval_seconds: Option, + pub enable_peerless_torrent_pruning: Option, +} + +impl Default for CommonSettings { + fn default() -> Self { + Self { + announce_interval_seconds: Some(120), + announce_interval_seconds_minimum: Some(120), + peer_timeout_seconds_maximum: Some(900), + enable_tracker_usage_statistics: Some(true), + enable_persistent_statistics: Some(false), + cleanup_inactive_peers_interval_seconds: Some(600), + enable_peerless_torrent_pruning: Some(false), + } + } +} + +impl Empty for CommonSettings { + fn empty() -> Self { + Self { + announce_interval_seconds: None, + announce_interval_seconds_minimum: None, + peer_timeout_seconds_maximum: None, + enable_tracker_usage_statistics: None, + enable_persistent_statistics: None, + cleanup_inactive_peers_interval_seconds: None, + enable_peerless_torrent_pruning: None, + } + } +} + +impl CommonSettings { + fn check(&self) -> Result<(), CommonSettingsError> { + check_field_is_not_none!(self.clone() => CommonSettingsError; + enable_tracker_usage_statistics, + enable_persistent_statistics, + enable_peerless_torrent_pruning + ); + + check_field_is_not_empty!(self.clone() => CommonSettingsError; + announce_interval_seconds: u32, + announce_interval_seconds_minimum: u32, + peer_timeout_seconds_maximum: u32, + cleanup_inactive_peers_interval_seconds: u64 + ); + + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] +pub struct CommonSettingsBuilder { + common_settings: CommonSettings, +} + +impl Empty for CommonSettingsBuilder { + fn empty() -> Self { + Self { + common_settings: Empty::empty(), + } + } +} + +impl From for CommonSettingsBuilder { + fn from(common_settings: CommonSettings) -> Self { + Self { common_settings } + } +} + +impl TryInto for CommonSettingsBuilder { + type Error = SettingsError; + + fn try_into(self) -> Result { + match self.common_settings.check() { + Ok(_) => Ok(self.common_settings), + Err(err) => Err(SettingsError::CommonSettingsError { + message: err.to_string(), + field: err.get_field(), + source: (Arc::new(err) as Arc).into(), + }), + } + } +} + +impl CommonSettingsBuilder { + #[must_use] + pub fn import_old(mut self, old_settings: &old_settings::Settings) -> Self { + old_to_new!(old_settings, self.common_settings; + announce_interval: announce_interval_seconds, + max_peer_timeout: peer_timeout_seconds_maximum, + tracker_usage_statistics: enable_tracker_usage_statistics, + persistent_torrent_completed_stat: enable_persistent_statistics, + inactive_peer_cleanup_interval: cleanup_inactive_peers_interval_seconds, + remove_peerless_torrents: enable_peerless_torrent_pruning + ); + self + } +} + +/// Special Service Settings with the Private Access Secrets Removed +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +pub struct ServiceNoSecrets { + pub enabled: Option, + pub display_name: Option, + pub service: Option, + pub socket: Option, + pub tls: Option, + pub access_tokens: Option, +} + +impl From<&Service> for ServiceNoSecrets { + fn from(services: &Service) -> Self { + Self { + enabled: services.enabled, + display_name: services.display_name.clone(), + service: services.service, + socket: services.socket, + tls: services.tls.clone(), + access_tokens: { + services.api_tokens.as_ref().map(|access_tokens| { + access_tokens + .iter() + .map(|pair| (pair.0.clone(), "SECRET_REMOVED".to_string())) + .collect() + }) + }, + } + } +} + +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +pub struct Service { + pub enabled: Option, + pub display_name: Option, + pub service: Option, + pub socket: Option, + pub tls: Option, + pub api_tokens: Option, +} + +impl Empty for Service { + fn empty() -> Self { + Self { + enabled: None, + display_name: None, + service: None, + socket: None, + tls: None, + api_tokens: None, + } + } +} + +impl From for Service { + fn from(service: apis::settings::Settings) -> Self { + Self { + enabled: Some(*service.is_enabled()), + display_name: Some(service.get_display_name().clone()), + service: Some(ServiceProtocol::Api), + socket: Some(service.get_socket().expect("socket should be here!")), + tls: None, + api_tokens: Some(service.get_access_tokens().clone()), + } + } +} + +impl From for Service { + fn from(service: UdpServiceSettings) -> Self { + Self { + enabled: Some(service.enabled), + display_name: Some(service.display_name), + service: Some(ServiceProtocol::Udp), + socket: Some(service.socket), + tls: None, + api_tokens: None, + } + } +} + +impl From for Service { + fn from(service: HttpServiceSettings) -> Self { + Self { + enabled: Some(service.enabled), + display_name: Some(service.display_name), + service: Some(ServiceProtocol::Http), + socket: Some(service.socket), + tls: None, + api_tokens: None, + } + } +} + +impl From for Service { + fn from(service: TlsServiceSettings) -> Self { + Self { + enabled: Some(service.enabled), + display_name: Some(service.display_name), + service: Some(ServiceProtocol::Tls), + socket: Some(service.socket), + tls: Some(TlsSettings { + certificate_file_path: Some(service.certificate_file_path), + key_file_path: Some(service.key_file_path), + }), + api_tokens: None, + } + } +} + +impl Service { + /// . + /// + /// # Panics + /// + /// Panics if . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn check(&self, id: &String) -> Result<(), ServiceSettingsError> { + check_field_is_not_none!(self => ServiceSettingsError; + enabled, service, socket); + + check_field_is_not_empty!(self => ServiceSettingsError; + display_name: String); + + match self.service.unwrap() { + ServiceProtocol::Api => { + apis::settings::Settings::try_from((id, self))?; + } + ServiceProtocol::Udp => { + UdpServiceSettings::try_from((id, self))?; + } + ServiceProtocol::Http => { + HttpServiceSettings::try_from((id, self))?; + } + ServiceProtocol::Tls => { + TlsServiceSettings::try_from((id, self))?; + } + } + + Ok(()) + } + + /// Returns the get socket of this [`Service`]. + /// + /// # Panics + /// + /// Panics if . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn get_socket(&self) -> Result { + check_field_is_not_none!(self => ServiceSettingsError; socket); + + Ok(self.socket.unwrap()) + } + + /// Returns the get api tokens of this [`Service`]. + /// + /// # Panics + /// + /// Panics if . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn get_api_tokens(&self) -> Result { + check_field_is_not_empty!(self => ServiceSettingsError; api_tokens : ApiTokens); + + Ok(self.api_tokens.clone().unwrap()) + } + + /// Returns the get tls settings of this [`Service`]. + /// + /// # Panics + /// + /// Panics if . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn get_tls_settings(&self) -> Result { + check_field_is_not_empty!(self => ServiceSettingsError; tls : TlsSettings); + + Ok(self.tls.clone().unwrap()) + } + + pub fn get_tls(&self) -> Result, ServiceSettingsError> { + Ok(self.tls.clone().map(|t| -> Tls { + TlsBuilder::default() + .certificate_file_path( + t.certificate_file_path + .expect("if we have a tls, then we should have the certificate"), + ) + .key_file_path(t.key_file_path.expect("if we have a tls, then we should have the key")) + .build() + .expect("failed to build tls settings") + })) + } +} + +#[derive(Serialize, Deserialize, Ord, PartialOrd, PartialEq, Eq, Debug, Clone, Hash, Deref, DerefMut)] +pub struct Services(BTreeMap); + +impl Default for Services { + fn default() -> Self { + let api = apis::settings::SettingsBuilder::default() + .build() + .expect("defaults should build"); + let udp = UdpServiceSettings::default(); + let http = HttpServiceSettings::default(); + let tls = TlsServiceSettings::default(); + + let mut services = Services::empty(); + + services.insert(api.get_id().clone(), api.into()); + services.insert(udp.id.clone(), udp.into()); + services.insert(http.id.clone(), http.into()); + services.insert(tls.id.clone(), tls.into()); + + services + } +} + +impl Empty for Services { + fn empty() -> Self { + Self(BTreeMap::new()) + } +} + +/// will remove the services that failed the configuration check, returns removed services. +impl Clean for Services { + fn clean(self) -> Self { + Self( + self.iter() + .filter(|service| service.1.check(service.0).is_ok()) + .map(|pair| (pair.0.clone(), pair.1.clone())) + .collect(), + ) + } +} + +impl Services { + /// Returns the check of this [`Services`]. + /// + /// # Errors + /// + /// This function will return an error if . + pub fn check(&self) -> Result<(), ServiceSettingsError> { + for service in self.iter() { + service.1.check(service.0)?; + } + + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Hash, Default)] +pub struct ServicesBuilder { + services: Services, +} + +impl Empty for ServicesBuilder { + fn empty() -> Self { + Self { + services: Empty::empty(), + } + } +} + +impl TryInto for ServicesBuilder { + type Error = SettingsError; + + fn try_into(self) -> Result { + for service in &self.services.0 { + if let Err(err) = service.1.check(service.0) { + return Err(SettingsError::ServiceSettingsError { + id: service.0.clone(), + field: err.get_field(), + message: err.to_string(), + source: (Arc::new(err) as Arc).into(), + }); + } + } + + Ok(self.services) + } +} + +impl From for ServicesBuilder { + fn from(services: Services) -> Self { + Self { services } + } +} + +impl ServicesBuilder { + #[must_use] + pub fn import_old(mut self, old_settings: &old_settings::Settings) -> Self { + let existing_service_map = self.services.clone(); + let existing_services: HashSet<&Service, RandomState> = existing_service_map.0.values().collect::>(); + + let mut new_values: HashSet<(Service, String)> = HashSet::new(); + + if let Some(service) = old_settings.http_api.as_ref() { + new_values.insert(service.clone().into()); + }; + + if let Some(services) = old_settings.udp_trackers.as_ref() { + for service in services { + new_values.insert(service.clone().into()); + } + }; + + if let Some(services) = old_settings.http_trackers.as_ref() { + for service in services { + new_values.insert(service.clone().into()); + } + }; + + for (value, name) in new_values { + // Lets not import something we already have... + if !existing_services.contains(&value) { + for count in 0.. { + let key = format!("{name}_{count}"); + if let Vacant(e) = self.services.0.entry(key) { + e.insert(value.clone()); + break; + } + } + } + } + self + } +} + +#[derive(Serialize, Deserialize, Default, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +pub struct TlsSettings { + pub certificate_file_path: Option>, + pub key_file_path: Option>, +} + +impl Empty for TlsSettings { + fn empty() -> Self { + Self { + certificate_file_path: None, + key_file_path: None, + } + } +} + +impl TlsSettings { + /// Returns the check of this [`TlsSettings`]. + /// + /// # Errors + /// + /// This function will return an error if . + pub fn check(&self) -> Result<(), TlsSettingsError> { + self.get_certificate_file_path()?; + self.get_key_file_path()?; + + Ok(()) + } + + /// Returns the get certificate file path of this [`TlsSettings`]. + /// + /// # Panics + /// + /// Panics if . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn get_certificate_file_path(&self) -> Result, TlsSettingsError> { + check_field_is_not_none!(self.clone() => TlsSettingsError; certificate_file_path); + + get_file_at(self.certificate_file_path.as_ref().unwrap(), OpenOptions::new().read(true)) + .map(|at| at.1) + .map_err(|err| TlsSettingsError::BadCertificateFilePath { + field: "certificate_file_path".to_string(), + source: (Arc::new(err) as Arc).into(), + }) + } + + /// Returns the get key file path of this [`TlsSettings`]. + /// + /// # Panics + /// + /// Panics if . + /// + /// # Errors + /// + /// This function will return an error if . + pub fn get_key_file_path(&self) -> Result, TlsSettingsError> { + check_field_is_not_none!(self.clone() => TlsSettingsError; key_file_path); + + get_file_at(self.key_file_path.as_ref().unwrap(), OpenOptions::new().read(true)) + .map(|at| at.1) + .map_err(|err| TlsSettingsError::BadKeyFilePath { + field: "key_file_path".to_string(), + source: (Arc::new(err) as Arc).into(), + }) + } +} + +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Copy, Clone, Hash, Display)] +#[serde(rename_all = "snake_case")] +pub enum LogFilterLevel { + Off, + Error, + Warn, + Info, + Debug, + Trace, +} + +impl Default for LogFilterLevel { + fn default() -> Self { + Self::Info + } +} + +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Copy, Clone, Hash, Display)] +#[serde(rename_all = "snake_case")] +pub enum ServiceProtocol { + Udp, + Http, + Tls, + Api, +} diff --git a/src/settings/old_settings.rs b/src/settings/old_settings.rs new file mode 100644 index 000000000..a105db058 --- /dev/null +++ b/src/settings/old_settings.rs @@ -0,0 +1,227 @@ +use std::collections::BTreeMap; +use std::net::SocketAddr; +use std::path::Path; +use std::str::FromStr; + +use serde::Deserialize; +use serde_with::serde_as; + +use super::{Service, ServiceProtocol, TlsSettings}; + +#[cfg(test)] +pub const OLD_DEFAULT: &str = r#" +log_level = "info" +mode = "public" +db_driver = "Sqlite3" +db_path = "data.db" +announce_interval = 120 +min_announce_interval = 120 +max_peer_timeout = 900 +on_reverse_proxy = false +external_ip = "0.0.0.0" +tracker_usage_statistics = true +persistent_torrent_completed_stat = false +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:6969" +ssl_enabled = false +ssl_cert_path = "" +ssl_key_path = "" + +[http_api] +enabled = true +bind_address = "127.0.0.1:1212" + +[http_api.access_tokens] +admin = "MyAccessToken" +"#; + +#[derive(Deserialize, Copy, Clone, PartialEq, Eq, Debug, Hash)] +#[serde(rename_all = "snake_case")] +pub enum TrackerModeOld { + Public, + Listed, + Private, + PrivateListed, +} + +#[derive(Deserialize, PartialEq, Eq, Debug, Copy, Clone, Hash)] +pub enum DatabaseDriversOld { + Sqlite3, + MySQL, +} + +#[serde_as] +#[derive(Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct UdpTrackerConfig { + pub display_name: Option, + pub enabled: Option, + pub bind_address: Option, +} + +impl From for (Service, String) { + fn from(val: UdpTrackerConfig) -> Self { + ( + Service { + enabled: val.enabled, + display_name: Some("UDP Service (imported)".to_string()), + service: Some(ServiceProtocol::Udp), + socket: val + .bind_address + .as_ref() + .and_then(|socket| SocketAddr::from_str(socket.as_str()).ok()), + tls: None, + api_tokens: None, + }, + "udp_imported".to_string(), + ) + } +} + +#[serde_as] +#[derive(Deserialize, PartialEq, Eq, Debug, Clone, Default)] +pub struct HttpTrackerConfig { + pub display_name: Option, + pub enabled: Option, + pub bind_address: Option, + pub ssl_enabled: Option, + pub ssl_cert_path: Option, + pub ssl_key_path: Option, +} + +impl From for (Service, String) { + fn from(val: HttpTrackerConfig) -> Self { + if val.ssl_enabled.unwrap_or_default() { + ( + Service { + enabled: val.enabled, + display_name: Some("TLS Service (imported)".to_string()), + service: Some(ServiceProtocol::Tls), + socket: val + .bind_address + .as_ref() + .and_then(|socket| SocketAddr::from_str(socket.as_str()).ok()), + tls: Some(TlsSettings { + certificate_file_path: { val.ssl_cert_path.as_ref().map(|path| Path::new(path).into()) }, + key_file_path: { val.ssl_key_path.as_ref().map(|path| Path::new(path).into()) }, + }), + api_tokens: None, + }, + "tls_imported".to_string(), + ) + } else { + ( + Service { + enabled: val.enabled, + display_name: Some("HTTP Service(imported)".to_string()), + service: Some(ServiceProtocol::Http), + socket: val + .bind_address + .as_ref() + .and_then(|socket| SocketAddr::from_str(socket.as_str()).ok()), + tls: None, + api_tokens: None, + }, + "http_imported".to_string(), + ) + } + } +} + +#[derive(Deserialize, PartialEq, Eq, Debug, Clone)] +pub struct HttpApiConfig { + pub enabled: Option, + pub bind_address: Option, + pub access_tokens: Option>, +} + +impl From for (Service, String) { + fn from(val: HttpApiConfig) -> Self { + ( + Service { + enabled: val.enabled, + display_name: Some("HTTP API (imported)".to_string()), + service: Some(ServiceProtocol::Api), + socket: val + .bind_address + .as_ref() + .and_then(|socket| SocketAddr::from_str(socket.as_str()).ok()), + tls: None, + api_tokens: val.access_tokens, + }, + "api_imported".to_string(), + ) + } +} + +#[serde_as] +#[derive(Deserialize, PartialEq, Eq, Debug, Clone, Default)] +pub struct Settings { + pub log_level: Option, + pub mode: Option, + pub db_driver: Option, + pub db_path: Option, + pub announce_interval: Option, + pub min_announce_interval: Option, + pub max_peer_timeout: Option, + pub on_reverse_proxy: Option, + pub external_ip: Option, + pub tracker_usage_statistics: Option, + pub persistent_torrent_completed_stat: Option, + pub inactive_peer_cleanup_interval: Option, + pub remove_peerless_torrents: Option, + pub udp_trackers: Option>, + pub http_trackers: Option>, + pub http_api: Option, +} + +#[cfg(not)] +mod tests { + + use std::path::Path; + use std::{env, fs}; + + use uuid::Uuid; + + use crate::config_const::{CONFIG_FOLDER, CONFIG_LOCAL}; + use crate::settings::old_settings::Settings; + + #[test] + fn default_settings_should_contain_an_external_ip() { + let settings = Settings::default().unwrap(); + assert_eq!(settings.external_ip, Option::Some(String::from("0.0.0.0"))); + } + + #[test] + fn settings_should_be_automatically_saved_into_local_config() { + let local_source = Path::new(CONFIG_FOLDER).join(CONFIG_LOCAL).with_extension("toml"); + + let settings = Settings::new().unwrap(); + + let contents = fs::read_to_string(&local_source).unwrap(); + + assert_eq!(contents, toml::to_string(&settings).unwrap()); + } + + #[test] + fn configuration_should_be_saved_in_a_toml_config_file() { + let temp_config_path = env::temp_dir().as_path().join(format!("test_config_{}.toml", Uuid::new_v4())); + + let settings = Settings::default().unwrap(); + + settings + .write(temp_config_path.as_ref()) + .expect("Could not save configuration to file"); + + let contents = fs::read_to_string(&temp_config_path).unwrap(); + + assert_eq!(contents, toml::to_string(&settings).unwrap()); + } +} diff --git a/src/setup.rs b/src/setup.rs index 3461667cc..b4f4bab1e 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -6,7 +6,7 @@ use tokio::task::JoinHandle; use crate::config::Configuration; use crate::http::Version; use crate::jobs::{http_tracker, torrent_cleanup, tracker_apis, udp_tracker}; -use crate::tracker; +use crate::{apis, tracker}; /// # Panics /// @@ -53,7 +53,13 @@ pub async fn setup(config: &Configuration, tracker: Arc) -> Ve // Start HTTP API if config.http_api.enabled { - jobs.push(tracker_apis::start_job(&config.http_api, tracker.clone()).await); + jobs.push( + tracker_apis::start_job( + &Arc::new(apis::settings::Settings::try_from(&config.http_api).expect("failed to make api settings")), + tracker.clone(), + ) + .await, + ); } // Remove torrents without peers, every interval diff --git a/src/tracker/mod.rs b/src/tracker/mod.rs index acbf7d536..8eda274d2 100644 --- a/src/tracker/mod.rs +++ b/src/tracker/mod.rs @@ -19,6 +19,7 @@ use tokio::sync::{RwLock, RwLockReadGuard}; use self::error::Error; use crate::config::Configuration; use crate::databases::driver::Driver; +use crate::databases::settings::OldConfig; use crate::databases::{self, Database}; use crate::protocol::info_hash::InfoHash; @@ -50,7 +51,11 @@ impl Tracker { stats_event_sender: Option>, stats_repository: statistics::Repo, ) -> Result { - let database = Driver::build(&config.db_driver, &config.db_path)?; + let db_settings = databases::settings::Settings::try_from(&OldConfig { + db_driver: config.db_driver, + db_path: config.db_path.clone(), + })?; + let database = Driver::build(&db_settings)?; Ok(Tracker { config: config.clone(), diff --git a/src/tracker/mode.rs b/src/tracker/mode.rs index a0dba6e67..402ddefb9 100644 --- a/src/tracker/mode.rs +++ b/src/tracker/mode.rs @@ -1,21 +1,20 @@ +use derive_more::Display; use serde; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Default, Serialize, Deserialize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash, Display)] +#[serde(rename_all = "snake_case")] pub enum Mode { // Will track every new info hash and serve every peer. - #[serde(rename = "public")] Public, // Will only track whitelisted info hashes. - #[serde(rename = "listed")] + #[default] Listed, // Will only serve authenticated peers - #[serde(rename = "private")] Private, // Will only track whitelisted info hashes and serve authenticated peers - #[serde(rename = "private_listed")] PrivateListed, } diff --git a/src/tracker/services/common.rs b/src/tracker/services/common.rs index 8757e6a21..5496f3759 100644 --- a/src/tracker/services/common.rs +++ b/src/tracker/services/common.rs @@ -1,9 +1,23 @@ +use std::path::Path; use std::sync::Arc; +use derive_builder::Builder; +use derive_getters::Getters; +use serde::{Deserialize, Serialize}; + use crate::config::Configuration; use crate::tracker::statistics::Keeper; use crate::tracker::Tracker; +#[derive(Builder, Getters, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Clone, Hash)] +#[builder(pattern = "immutable")] +pub struct Tls { + #[getter(rename = "get_certificate_file_path")] + certificate_file_path: Box, + #[getter(rename = "get_key_file_path")] + key_file_path: Box, +} + /// # Panics /// /// Will panic if tracker cannot be instantiated. diff --git a/src/udp/mod.rs b/src/udp/mod.rs index 8b8c8c4f8..26b2d870b 100644 --- a/src/udp/mod.rs +++ b/src/udp/mod.rs @@ -1,3 +1,10 @@ +use std::net::SocketAddr; +use std::str::FromStr; + +use crate::errors::settings::ServiceSettingsError; +use crate::settings::{Service, ServiceProtocol}; +use crate::{check_field_is_not_empty, check_field_is_not_none}; + pub mod connection_cookie; pub mod error; pub mod handlers; @@ -10,3 +17,50 @@ pub type TransactionId = i64; pub const MAX_PACKET_SIZE: usize = 1496; pub const PROTOCOL_ID: i64 = 0x0417_2710_1980; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct UdpServiceSettings { + pub id: String, + pub enabled: bool, + pub display_name: String, + pub socket: SocketAddr, +} + +impl Default for UdpServiceSettings { + fn default() -> Self { + Self { + id: "default_udp".to_string(), + enabled: false, + display_name: "UDP (default)".to_string(), + socket: SocketAddr::from_str("0.0.0.0:6969").unwrap(), + } + } +} + +impl TryFrom<(&String, &Service)> for UdpServiceSettings { + type Error = ServiceSettingsError; + + fn try_from(value: (&String, &Service)) -> Result { + check_field_is_not_none!(value.1 => ServiceSettingsError; + enabled, service); + + if value.1.service.unwrap() != ServiceProtocol::Udp { + return Err(ServiceSettingsError::WrongService { + field: "service".to_string(), + expected: ServiceProtocol::Udp, + found: value.1.service.unwrap(), + data: value.1.into(), + }); + } + + check_field_is_not_empty!(value.1 => ServiceSettingsError; + display_name: String); + + Ok(Self { + id: value.0.clone(), + enabled: value.1.enabled.unwrap(), + display_name: value.1.display_name.clone().unwrap(), + socket: value.1.get_socket()?, + }) + } +} diff --git a/tests/api/server.rs b/tests/api/server.rs index c1cd0630a..e4eee54e9 100644 --- a/tests/api/server.rs +++ b/tests/api/server.rs @@ -6,7 +6,7 @@ use torrust_tracker::jobs::tracker_apis; use torrust_tracker::protocol::info_hash::InfoHash; use torrust_tracker::tracker::peer::Peer; use torrust_tracker::tracker::statistics::Keeper; -use torrust_tracker::{ephemeral_instance_keys, logging, static_time, tracker}; +use torrust_tracker::{apis, ephemeral_instance_keys, logging, static_time, tracker}; use super::connection_info::ConnectionInfo; @@ -21,7 +21,11 @@ pub async fn start_default_api() -> Server { pub async fn start_custom_api(configuration: Arc) -> Server { let server = start(&configuration); - tracker_apis::start_job(&configuration.http_api, server.tracker.clone()).await; + tracker_apis::start_job( + &Arc::new(apis::settings::Settings::try_from(&configuration.http_api).expect("failed to make api settings")), + server.tracker.clone(), + ) + .await; server }