Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate services configuration before starting them #790

15 changes: 4 additions & 11 deletions packages/configuration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@
//! [health_check_api]
//! bind_address = "127.0.0.1:1313"
//!```
pub mod sections;

use std::collections::HashMap;
use std::net::IpAddr;
use std::str::FromStr;
Expand Down Expand Up @@ -261,16 +263,7 @@ pub struct Info {
}

impl Info {
/// Build Configuration Info
///
/// # Examples
///
/// ```
/// use torrust_tracker_configuration::Info;
///
/// let result = Info::new(env_var_config, env_var_path_config, default_path_config, env_var_api_admin_token);
/// assert_eq!(result, );
/// ```
/// Build Configuration Info.
///
/// # Errors
///
Expand Down Expand Up @@ -433,7 +426,7 @@ impl Default for AnnouncePolicy {

/// Core configuration for the tracker.
#[allow(clippy::struct_excessive_bools)]
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
pub struct Configuration {
/// Logging level. Possible values are: `Off`, `Error`, `Warn`, `Info`,
/// `Debug` and `Trace`. Default is `Info`.
Expand Down
60 changes: 60 additions & 0 deletions packages/configuration/src/sections/core.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! Validated configuration for the tracker core.
//!
//! This configuration is a first level of validation that can be perform
//! statically without running the service.
use serde::{Deserialize, Serialize};
use thiserror::Error;
use torrust_tracker_primitives::{DatabaseDriver, TrackerMode};

use crate::Configuration;

/// Errors that can occur when validating the plain configuration.
#[derive(Error, Debug, PartialEq)]

Check warning on line 12 in packages/configuration/src/sections/core.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/core.rs#L12

Added line #L12 was not covered by tests
pub enum ValidationError {
/// Invalid bind address.
#[error("Invalid log level, got: {log_level}")]
InvalidLogLevel { log_level: String },

Check warning on line 16 in packages/configuration/src/sections/core.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/core.rs#L15-L16

Added lines #L15 - L16 were not covered by tests
}

/// Configuration for each HTTP tracker.
#[allow(clippy::struct_excessive_bools)]
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]

Check warning on line 21 in packages/configuration/src/sections/core.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/core.rs#L21

Added line #L21 was not covered by tests
pub struct Config {
log_level: Option<String>, // todo: use enum
mode: TrackerMode,
db_driver: DatabaseDriver,
db_path: String, // todo: use Path
announce_interval: u32,
min_announce_interval: u32,
on_reverse_proxy: bool,
external_ip: Option<String>, // todo: use IpAddr
tracker_usage_statistics: bool,
persistent_torrent_completed_stat: bool,
max_peer_timeout: u32,
inactive_peer_cleanup_interval: u64,
remove_peerless_torrents: bool,

Check warning on line 35 in packages/configuration/src/sections/core.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/core.rs#L23-L35

Added lines #L23 - L35 were not covered by tests
}

impl TryFrom<Configuration> for Config {
type Error = ValidationError;

fn try_from(config: Configuration) -> Result<Self, Self::Error> {

Check warning on line 41 in packages/configuration/src/sections/core.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/core.rs#L41

Added line #L41 was not covered by tests
// todo: validation

Ok(Self {
log_level: config.log_level,
mode: config.mode,
db_driver: config.db_driver,
db_path: config.db_path,
announce_interval: config.announce_interval,
min_announce_interval: config.min_announce_interval,
on_reverse_proxy: config.on_reverse_proxy,
external_ip: config.external_ip,
tracker_usage_statistics: config.tracker_usage_statistics,
persistent_torrent_completed_stat: config.persistent_torrent_completed_stat,
max_peer_timeout: config.max_peer_timeout,
inactive_peer_cleanup_interval: config.inactive_peer_cleanup_interval,
remove_peerless_torrents: config.remove_peerless_torrents,

Check warning on line 57 in packages/configuration/src/sections/core.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/core.rs#L44-L57

Added lines #L44 - L57 were not covered by tests
})
}

Check warning on line 59 in packages/configuration/src/sections/core.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/core.rs#L59

Added line #L59 was not covered by tests
}
82 changes: 82 additions & 0 deletions packages/configuration/src/sections/health_check_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! Validated configuration for the Health Check Api service.
//!
//! [``crate::HealthCheckApi``] is a DTO containing the parsed data from the toml
//! file.
//!
//! This configuration is a first level of validation that can be perform
//! statically without running the service.
//!
//! For example, the `bind_address` must be a valid socket address.
use std::net::SocketAddr;

use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::HealthCheckApi;

/// Errors that can occur when validating the plain configuration.
#[derive(Error, Debug, PartialEq)]
pub enum ValidationError {
/// Invalid bind address.
#[error("Invalid bind address, got: {bind_address}")]

Check warning on line 21 in packages/configuration/src/sections/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/health_check_api.rs#L21

Added line #L21 was not covered by tests
InvalidBindAddress { bind_address: String },
}

/// Configuration for each HTTP tracker.
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]

Check warning on line 26 in packages/configuration/src/sections/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/health_check_api.rs#L26

Added line #L26 was not covered by tests
pub struct Config {
bind_address: String, // todo: use SocketAddr

Check warning on line 28 in packages/configuration/src/sections/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/health_check_api.rs#L28

Added line #L28 was not covered by tests
}

impl Config {
#[must_use]
pub fn bind_address(&self) -> &str {
&self.bind_address
}

Check warning on line 35 in packages/configuration/src/sections/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/health_check_api.rs#L33-L35

Added lines #L33 - L35 were not covered by tests
}

impl TryFrom<HealthCheckApi> for Config {
type Error = ValidationError;

fn try_from(config: HealthCheckApi) -> Result<Self, Self::Error> {
let socket_addr = match config.bind_address.parse::<SocketAddr>() {
Ok(socket_addr) => socket_addr,

Check warning on line 43 in packages/configuration/src/sections/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/health_check_api.rs#L43

Added line #L43 was not covered by tests
Err(_err) => {
return Err(ValidationError::InvalidBindAddress {
bind_address: config.bind_address,
})
}
};

Ok(Self {
bind_address: socket_addr.to_string(),

Check warning on line 52 in packages/configuration/src/sections/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/health_check_api.rs#L51-L52

Added lines #L51 - L52 were not covered by tests
})
}
}

impl From<Config> for HealthCheckApi {
fn from(config: Config) -> Self {
Self {
bind_address: config.bind_address,

Check warning on line 60 in packages/configuration/src/sections/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/health_check_api.rs#L58-L60

Added lines #L58 - L60 were not covered by tests
}
}

Check warning on line 62 in packages/configuration/src/sections/health_check_api.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/health_check_api.rs#L62

Added line #L62 was not covered by tests
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_should_return_an_error_when_the_bind_address_is_not_a_valid_socket_address() {
let plain_config = HealthCheckApi {
bind_address: "300.300.300.300:7070".to_string(),
};

assert_eq!(
Config::try_from(plain_config),
Err(ValidationError::InvalidBindAddress {
bind_address: "300.300.300.300:7070".to_string()
})
);
}
}
187 changes: 187 additions & 0 deletions packages/configuration/src/sections/http_tracker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//! Validated configuration for the HTTP Tracker service.
//!
//! [``crate::HttpTracker``] is a DTO containing the parsed data from the toml
//! file.
//!
//! This configuration is a first level of validation that can be perform
//! statically without running the service.
//!
//! For example, if SSL is enabled you must provide the certificate path. That
//! can be validated. However, this validation does not check if the
//! certificate is valid.
use std::net::SocketAddr;

use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::HttpTracker;

/// Errors that can occur when validating the plain configuration.
#[derive(Error, Debug, PartialEq)]
pub enum ValidationError {
/// Invalid bind address.
#[error("Invalid bind address, got: {bind_address}")]

Check warning on line 23 in packages/configuration/src/sections/http_tracker.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/http_tracker.rs#L23

Added line #L23 was not covered by tests
InvalidBindAddress { bind_address: String },
/// Missing SSL cert path.
#[error("missing SSL cert path")]
MissingSslCertPath,
/// Missing SSL key path.
#[error("missing SSL key path")]
MissingSslKeyPath,
}

/// Configuration for each HTTP tracker.
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]

Check warning on line 34 in packages/configuration/src/sections/http_tracker.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/http_tracker.rs#L34

Added line #L34 was not covered by tests
pub struct Config {
enabled: bool,
bind_address: String, // todo: use SocketAddr
ssl_enabled: bool,
ssl_cert_path: Option<String>, // todo: use Path
ssl_key_path: Option<String>, // todo: use Path

Check warning on line 40 in packages/configuration/src/sections/http_tracker.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/http_tracker.rs#L36-L40

Added lines #L36 - L40 were not covered by tests
}

impl Config {
#[must_use]
pub fn is_enabled(&self) -> bool {
self.enabled
}

Check warning on line 47 in packages/configuration/src/sections/http_tracker.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/http_tracker.rs#L45-L47

Added lines #L45 - L47 were not covered by tests
}

impl TryFrom<HttpTracker> for Config {
type Error = ValidationError;

fn try_from(config: HttpTracker) -> Result<Self, Self::Error> {
let socket_addr = match config.bind_address.parse::<SocketAddr>() {
Ok(socket_addr) => socket_addr,
Err(_err) => {
return Err(ValidationError::InvalidBindAddress {
bind_address: config.bind_address,
})
}
};

if config.ssl_enabled {
match config.ssl_cert_path.clone() {
Some(ssl_cert_path) => {
if ssl_cert_path.is_empty() {
Err(ValidationError::MissingSslCertPath)
} else {
Ok(())
}
}
None => Err(ValidationError::MissingSslCertPath),
}?;

match config.ssl_key_path.clone() {
Some(ssl_key_path) => {
if ssl_key_path.is_empty() {
Err(ValidationError::MissingSslKeyPath)
} else {
Ok(())

Check warning on line 80 in packages/configuration/src/sections/http_tracker.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/http_tracker.rs#L80

Added line #L80 was not covered by tests
}
}
None => Err(ValidationError::MissingSslKeyPath),
}?;
}

Ok(Self {
enabled: config.enabled,
bind_address: socket_addr.to_string(),
ssl_enabled: config.ssl_enabled,
ssl_cert_path: config.ssl_cert_path,
ssl_key_path: config.ssl_key_path,

Check warning on line 92 in packages/configuration/src/sections/http_tracker.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/http_tracker.rs#L87-L92

Added lines #L87 - L92 were not covered by tests
})
}
}

impl From<Config> for HttpTracker {
fn from(config: Config) -> Self {
Self {
enabled: config.enabled,
bind_address: config.bind_address,
ssl_enabled: config.ssl_enabled,
ssl_cert_path: config.ssl_cert_path,
ssl_key_path: config.ssl_key_path,

Check warning on line 104 in packages/configuration/src/sections/http_tracker.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/http_tracker.rs#L98-L104

Added lines #L98 - L104 were not covered by tests
}
}

Check warning on line 106 in packages/configuration/src/sections/http_tracker.rs

View check run for this annotation

Codecov / codecov/patch

packages/configuration/src/sections/http_tracker.rs#L106

Added line #L106 was not covered by tests
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn it_should_return_an_error_when_the_bind_address_is_not_a_valid_socket_address() {
let plain_config = HttpTracker {
enabled: true,
bind_address: "300.300.300.300:7070".to_string(),
ssl_enabled: true,
ssl_cert_path: None,
ssl_key_path: Some("./localhost.key".to_string()),
};

assert_eq!(
Config::try_from(plain_config),
Err(ValidationError::InvalidBindAddress {
bind_address: "300.300.300.300:7070".to_string()
})
);
}

mod when_ssl_is_enabled {
use crate::sections::http_tracker::{Config, ValidationError};
use crate::HttpTracker;

#[test]
fn it_should_return_an_error_when_ssl_is_enabled_but_the_cert_path_is_not_provided() {
let plain_config = HttpTracker {
enabled: true,
bind_address: "127.0.0.1:7070".to_string(),
ssl_enabled: true,
ssl_cert_path: None,
ssl_key_path: Some("./localhost.key".to_string()),
};

assert_eq!(Config::try_from(plain_config), Err(ValidationError::MissingSslCertPath));
}

#[test]
fn it_should_return_an_error_when_ssl_is_enabled_but_the_cert_path_is_empty() {
let plain_config = HttpTracker {
enabled: true,
bind_address: "127.0.0.1:7070".to_string(),
ssl_enabled: true,
ssl_cert_path: Some(String::new()),
ssl_key_path: Some("./localhost.key".to_string()),
};

assert_eq!(Config::try_from(plain_config), Err(ValidationError::MissingSslCertPath));
}

#[test]
fn it_should_return_an_error_when_ssl_is_enabled_but_the_key_path_is_not_provided() {
let plain_config = HttpTracker {
enabled: true,
bind_address: "127.0.0.1:7070".to_string(),
ssl_enabled: true,
ssl_cert_path: Some("./localhost.crt".to_string()),
ssl_key_path: None,
};

assert_eq!(Config::try_from(plain_config), Err(ValidationError::MissingSslKeyPath));
}

#[test]
fn it_should_return_an_error_when_ssl_is_enabled_but_the_key_path_is_empty() {
let plain_config = HttpTracker {
enabled: true,
bind_address: "127.0.0.1:7070".to_string(),
ssl_enabled: true,
ssl_cert_path: Some("./localhost.crt".to_string()),
ssl_key_path: Some(String::new()),
};

assert_eq!(Config::try_from(plain_config), Err(ValidationError::MissingSslKeyPath));
}
}
}
5 changes: 5 additions & 0 deletions packages/configuration/src/sections/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pub mod core;
pub mod health_check_api;
pub mod http_tracker;
pub mod tracker_api;
pub mod udp_tracker;
Loading
Loading