use config::{Config, ConfigError, Environment, File}; use rcgen::string::Ia5String; use serde::{Deserialize, Deserializer, Serialize}; use std::{net::IpAddr, str::FromStr}; use tracing::level_filters::LevelFilter; type ValidationError = String; trait Validate { fn validate(&self) -> Result<(), ValidationError>; } /// Master server settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Settings { pub server: ServerSettings, pub database: DatabaseSettings, pub grpc: GrpcSettings, pub auth: AuthSettings, #[serde(default)] pub log: LogSettings, } /// HTTP server settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ServerSettings { #[serde(default = "default_server_bind_address")] pub bind_address: String, #[serde(default = "default_server_port")] pub port: u16, #[serde(default)] pub certificate: CertificateSettings, #[serde(default)] pub cors: Option, } /// Database connection settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DatabaseSettings { pub url: String, pub max_connections: Option, } /// gRPC server settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GrpcSettings { #[serde(default = "default_grpc_bind_address")] pub bind_address: String, #[serde(default = "default_grpc_port")] pub port: u16, #[serde(default)] pub certificate: CertificateSettings, #[serde(default)] pub cors: Option, } /// Authentication settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthSettings { pub jwt_secret: String, #[serde(default = "default_jwt_expiration_hours")] pub jwt_expiration_hours: u64, } /// TLS certificate settings #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct CertificateSettings { #[serde(default = "default_cert_folder")] pub cert_dir: String, #[serde( default, serialize_with = "serialize_ia5string_vec", deserialize_with = "deserialize_ia5string_vec" )] pub san_dns: Vec, #[serde(default)] pub san_ip: Vec, #[serde(default)] cert_path: Option, #[serde(default)] key_path: Option, } impl CertificateSettings { pub fn cert_path(&self) -> Option { self.cert_path .as_ref() .map(|p| format!("{}/{}", self.cert_dir, p)) } pub fn key_path(&self) -> Option { self.key_path .as_ref() .map(|p| format!("{}/{}", self.cert_dir, p)) } } /// CORS settings #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct CorsSettings { #[serde(default)] pub allowed_origins: Vec, #[serde(default)] pub allowed_methods: Vec, #[serde(default)] pub allowed_headers: Vec, #[serde(default)] pub allow_credentials: bool, } /// Logging settings #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LogSettings { #[serde( deserialize_with = "deserialize_level_filter", serialize_with = "serialize_level_filter" )] pub level: LevelFilter, } impl Default for LogSettings { fn default() -> Self { Self { level: default_log_level(), } } } impl Validate for Settings { fn validate(&self) -> Result<(), ValidationError> { self.server.validate()?; self.grpc.validate()?; self.database.validate()?; self.auth.validate()?; Ok(()) } } impl Settings { /// Load settings from config files and environment pub fn load() -> Result { let run_mode = std::env::var("RUN_MODE").unwrap_or_else(|_| "development".into()); let settings = Config::builder() .add_source(File::with_name("config/default").required(false)) .add_source(File::with_name(&format!("config/{}", run_mode)).required(false)) .add_source(File::with_name("config/master/default").required(false)) .add_source(File::with_name(&format!("config/master/{}", run_mode)).required(false)) .add_source(Environment::with_prefix("NXMESH").separator("__")) .build()?; let settings: Self = settings.try_deserialize()?; settings.validate().map_err(ConfigError::Message)?; Ok(settings) } } impl Validate for ServerSettings { fn validate(&self) -> Result<(), ValidationError> { if self.bind_address.is_empty() { return Err("Server bind address cannot be empty".into()); } if self.port == 0 { return Err("Server port must be greater than 0".into()); } self.certificate.validate()?; if let Some(cors) = &self.cors { cors.validate()?; } Ok(()) } } impl Validate for GrpcSettings { fn validate(&self) -> Result<(), ValidationError> { if self.bind_address.is_empty() { return Err("gRPC bind address cannot be empty".into()); } if self.port == 0 { return Err("gRPC port must be greater than 0".into()); } self.certificate.validate()?; if let Some(cors) = &self.cors { cors.validate()?; } Ok(()) } } impl Validate for DatabaseSettings { fn validate(&self) -> Result<(), ValidationError> { if self.url.is_empty() { return Err("Database URL cannot be empty".into()); } if let Some(max_connections) = self.max_connections && max_connections == 0 { return Err("Max database connections must be greater than 0".into()); } Ok(()) } } impl Validate for AuthSettings { fn validate(&self) -> Result<(), ValidationError> { if self.jwt_secret.is_empty() { return Err("JWT secret cannot be empty".into()); } if self.jwt_expiration_hours == 0 { return Err("JWT expiration hours must be greater than 0".into()); } Ok(()) } } impl Validate for CertificateSettings { fn validate(&self) -> Result<(), ValidationError> { let base_path = std::path::Path::new(&self.cert_dir); if !base_path.exists() { // create the cert directory if it doesn't exist std::fs::create_dir_all(base_path).map_err(|e| { format!( "Failed to create certificate directory {:?}: {}", base_path, e ) })?; } let cert_path = self.cert_path.as_ref().map(|p| base_path.join(p)); let key_path = self.key_path.as_ref().map(|p| base_path.join(p)); if (cert_path.is_some() && key_path.is_none()) || (cert_path.is_none() && key_path.is_some()) { return Err("Both certificate and key paths must be provided for TLS".into()); } if let (Some(cert_path), Some(key_path)) = (&cert_path, &key_path) { if !std::path::Path::new(cert_path).exists() { return Err(format!("Certificate file not found: {:?}", cert_path)); } if !std::path::Path::new(key_path).exists() { return Err(format!("Key file not found: {:?}", key_path)); } } // validate for SAN entries - must be valid DNS names or IP addresses for dns in &self.san_dns { if dns.to_string().is_empty() { return Err("SAN DNS entries cannot be empty".into()); } } for ip in &self.san_ip { if ip.is_unspecified() { return Err("SAN IP entries cannot be unspecified".into()); } } // require at least one SAN entry for the generated certificate if self.san_dns.is_empty() && self.san_ip.is_empty() { return Err( "At least one SAN entry (DNS or IP) must be provided for the certificate".into(), ); } Ok(()) } } impl Validate for CorsSettings { fn validate(&self) -> Result<(), ValidationError> { Ok(()) } } fn default_jwt_expiration_hours() -> u64 { 24 } fn default_server_bind_address() -> String { "0.0.0.0".into() } fn default_server_port() -> u16 { 8080 } fn default_grpc_bind_address() -> String { "0.0.0.0".into() } fn default_grpc_port() -> u16 { 50051 } fn default_log_level() -> LevelFilter { LevelFilter::INFO } fn default_cert_folder() -> String { "./certs".into() } fn deserialize_level_filter<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let s = String::deserialize(deserializer)?; LevelFilter::from_str(&s).map_err(serde::de::Error::custom) } fn serialize_level_filter(level: &LevelFilter, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&level.to_string()) } fn deserialize_ia5string_vec<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let vec = Vec::::deserialize(deserializer)?; vec.into_iter() .map(|s| Ia5String::try_from(s).map_err(serde::de::Error::custom)) .collect() } fn serialize_ia5string_vec(vec: &Vec, serializer: S) -> Result where S: serde::Serializer, { let string_vec: Vec = vec.iter().map(|ia5| ia5.to_string()).collect(); string_vec.serialize(serializer) } #[cfg(test)] mod tests { use super::*; #[test] fn test_esnure_send_and_sync() { fn assert_send_sync() {} assert_send_sync::(); assert_send_sync::(); assert_send_sync::(); assert_send_sync::(); assert_send_sync::(); assert_send_sync::(); assert_send_sync::(); assert_send_sync::(); } }