use serde::Deserialize; use std::fmt::{Display, Formatter}; use std::net::IpAddr; use thiserror::Error; #[derive(Deserialize, Debug)] #[non_exhaustive] pub struct MagnetarNetworking { pub host: String, pub port: u16, pub bind_addr: IpAddr, pub protocol: MagnetarNetworkingProtocol, pub media_proxy: Option, pub proxy_remote_files: bool, } #[derive(Deserialize, Debug)] #[serde(rename_all = "snake_case")] pub enum MagnetarNetworkingProtocol { Http, Https, } impl AsRef for MagnetarNetworkingProtocol { fn as_ref(&self) -> &str { match *self { MagnetarNetworkingProtocol::Http => "http", MagnetarNetworkingProtocol::Https => "https", } } } impl Display for MagnetarNetworkingProtocol { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_ref()) } } fn env_host() -> String { std::env::var("MAG_C_HOST") .expect("MAG_C_HOST or \"networking.host\" in the default configuration must be set") } fn env_bind_addr() -> IpAddr { std::env::var("MAG_C_BIND_ADDR") .unwrap_or_else(|_| "::".to_owned()) .parse() .map_err(|e| format!("Failed to parse \"MAG_C_BIND_ADDR\": {e}")) .unwrap() } fn env_port() -> u16 { std::env::var("MAG_C_PORT") .unwrap_or_else(|_| "4939".to_owned()) .parse() .expect("MAG_C_PORT must be a valid port number") } fn env_protocol() -> MagnetarNetworkingProtocol { match std::env::var("MAG_C_PROTOCOL") .unwrap_or_else(|_| "https".to_owned()) .to_lowercase() .as_str() { "http" => MagnetarNetworkingProtocol::Http, "https" => MagnetarNetworkingProtocol::Https, _ => panic!("MAG_C_PROTOCOL must be a valid protocol"), } } fn env_media_proxy() -> Option { std::env::var("MAG_C_MEDIA_PROXY") .ok() .filter(String::is_empty) } fn env_proxy_remote_files() -> bool { std::env::var("MAG_C_PROXY_REMOTE_FILES") .unwrap_or_else(|_| "false".to_string()) .parse() .expect("MAG_C_PROXY_REMOTE_FILES must be a boolean") } impl Default for MagnetarNetworking { fn default() -> Self { MagnetarNetworking { host: env_host(), bind_addr: env_bind_addr(), port: env_port(), protocol: env_protocol(), media_proxy: env_media_proxy(), proxy_remote_files: env_proxy_remote_files(), } } } #[derive(Deserialize, Debug)] #[non_exhaustive] pub struct MagnetarCalckeyFrontendConfig { pub bind_addr: IpAddr, pub port: u16, } fn env_fe_bind_addr() -> IpAddr { std::env::var("MAG_C_CK_FE_BIND_ADDR") .unwrap_or_else(|_| "::".to_owned()) .parse() .map_err(|e| format!("Failed to parse \"MAG_C_CK_FE_BIND_ADDR\": {e}")) .unwrap() } fn env_fe_port() -> u16 { std::env::var("MAG_C_CK_FE_PORT") .unwrap_or_else(|_| "4938".to_owned()) .parse() .expect("MAG_C_CK_FE_PORT must be a valid port number") } impl Default for MagnetarCalckeyFrontendConfig { fn default() -> Self { MagnetarCalckeyFrontendConfig { bind_addr: env_fe_bind_addr(), port: env_fe_port(), } } } #[derive(Deserialize, Debug)] #[non_exhaustive] pub struct MagnetarBranding { pub name: String, pub version: String, pub homepage: String, pub repository: String, } fn env_brand_name() -> String { std::env::var("MAG_C_BR_NAME").unwrap_or_else(|_| "magnetar".to_owned()) } fn env_brand_version() -> String { std::env::var("MAG_C_BR_VERSION").unwrap_or_else(|_| env!("CARGO_PKG_VERSION").to_owned()) } fn env_brand_homepage() -> String { std::env::var("MAG_C_BR_HOMEPAGE") .unwrap_or_else(|_| "https://git.astolfo.cool/natty/magnetar".to_owned()) } fn env_brand_repository() -> String { std::env::var("MAG_C_BR_REPOSITORY") .unwrap_or_else(|_| "https://git.astolfo.cool/natty/magnetar".to_owned()) } impl Default for MagnetarBranding { fn default() -> Self { MagnetarBranding { name: env_brand_name(), version: env_brand_version(), homepage: env_brand_homepage(), repository: env_brand_repository(), } } } #[derive(Deserialize, Debug)] #[non_exhaustive] pub struct MagnetarData { pub database_url: String, pub redis_url: String, } fn env_database_url() -> String { std::env::var("MAG_C_DATABASE_URL") .or_else(|_| std::env::var("DATABASE_URL")) .expect("MAG_C_DATABASE_URL, DATABASE_URL or \"data.database_url\" in the default configuration must be set") } fn env_redis_url() -> String { std::env::var("MAG_C_REDIS_URL") .expect("MAG_C_REDIS_URL or \"data.redis_url\" in the default configuration must be set") } impl Default for MagnetarData { fn default() -> Self { MagnetarData { database_url: env_database_url(), redis_url: env_redis_url(), } } } #[derive(Deserialize, Debug, Default)] #[non_exhaustive] pub struct MagnetarConfig { #[serde(default)] pub networking: MagnetarNetworking, #[serde(default)] pub branding: MagnetarBranding, #[serde(default)] pub calckey_frontend: MagnetarCalckeyFrontendConfig, #[serde(default)] pub data: MagnetarData, } #[derive(Debug, Error)] pub enum MagnetarConfigError { #[error("Failed to load configuration: {0}")] IoError(#[from] std::io::Error), #[error("Failed to parse configuration: {0}")] DeserializeError(#[from] toml::de::Error), #[error("Configuration error: Not a valid hostname")] ConfigHostnameError(#[from] idna::Errors), } pub fn load_config() -> Result { let path = std::env::var("MAG_CONFIG_PATH").unwrap_or_else(|_| "config/default.toml".to_owned()); let str_cfg = std::fs::read_to_string(path)?; let mut config: MagnetarConfig = toml::from_str(&str_cfg)?; // Validate the host idna::domain_to_unicode(&config.networking.host).1?; if config .networking .media_proxy .as_deref() .is_some_and(str::is_empty) { config.networking.media_proxy = None; } Ok(config) }