Files
LibreTunes-DX/src/server/config.rs

247 lines
6.3 KiB
Rust

use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use serde::Deserialize;
/// Enable secure cookies by default only in release mode
/// (Secure cookies can't be set over HTTP. Rough assumption: development is done over HTTP)
const DEFAULT_COOKIES_SECURE: bool = cfg!(not(debug_assertions));
// Simple newtype to avoid showing secrets with `Debug` / `Display`
#[derive(Clone, Deserialize)]
pub struct SecretString(String);
impl SecretString {
pub fn expose(&self) -> &String {
&self.0
}
}
impl From<String> for SecretString {
fn from(s: String) -> Self {
Self(s)
}
}
impl std::fmt::Debug for SecretString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "*****")
}
}
impl std::fmt::Display for SecretString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "*****")
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AuthConfig {
pub open_signup: bool,
pub cookies_secure: bool,
}
/// Build a connection URI from parts
fn format_uri(
scheme: &str,
username: Option<&String>,
password: Option<&String>,
host: &str,
port: Option<u16>,
path: Option<&String>,
) -> String {
let mut url = format!("{scheme}://");
if let Some(username) = username {
url.push_str(username);
if let Some(password) = password {
url.push_str(&format!(":{password}"));
}
url.push('@');
}
url.push_str(host);
if let Some(port) = port {
url.push_str(&format!(":{port}"));
}
if let Some(path) = path {
url.push_str(&format!("/{path}"));
}
url
}
#[derive(Debug, Clone, Deserialize)]
pub struct DatabaseConfig {
#[serde(flatten)]
connection: DatabaseConnectionConfig,
}
impl DatabaseConfig {
/// Get the configured database connection URI
pub fn connection_uri(&self) -> String {
self.connection.as_uri()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum DatabaseConnectionConfig {
FromUrl {
url: SecretString,
},
FromParts {
host: String,
port: Option<u16>,
database: Option<String>,
username: Option<String>,
password: Option<SecretString>,
},
}
impl DatabaseConnectionConfig {
/// Convert this configuration into the Postgres connection URI
pub fn as_uri(&self) -> String {
match self {
Self::FromUrl { url } => url.expose().clone(),
Self::FromParts {
host,
port,
database,
username,
password,
} => format_uri(
"postgres",
username.as_ref(),
password.as_ref().map(|s| s.expose()),
host,
*port,
database.as_ref(),
),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum KeyValStoreConnectionConfig {
FromUrl {
url: SecretString,
},
FromParts {
scheme: Option<String>,
host: String,
port: Option<u16>,
database: Option<String>,
username: Option<String>,
password: Option<SecretString>,
},
}
impl KeyValStoreConnectionConfig {
/// Convert this configuration into the Redis connection URI
pub fn as_uri(&self) -> String {
match self {
Self::FromUrl { url } => url.expose().clone(),
Self::FromParts {
scheme,
host,
port,
database,
username,
password,
} => format_uri(
scheme.as_deref().unwrap_or("redis"),
username.as_ref(),
password.as_ref().map(|s| s.expose().clone()).as_ref(),
host,
*port,
database.as_ref(),
),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct KeyValStoreConfig {
#[serde(flatten)]
connection: KeyValStoreConnectionConfig,
}
impl KeyValStoreConfig {
/// Get the configured database connection URI
pub fn connection_uri(&self) -> String {
self.connection.as_uri()
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
pub public_path: PathBuf,
pub host: IpAddr,
pub port: u16,
}
impl ServerConfig {
pub fn serve_addr(&self) -> SocketAddr {
SocketAddr::new(self.host, self.port)
}
}
#[derive(Debug, Clone, Deserialize)]
/// Top-level application configuration
pub struct Config {
pub auth: AuthConfig,
pub database: DatabaseConfig,
pub key_val_store: KeyValStoreConfig,
pub server: ServerConfig,
}
/// Parse configuration from the expected files and environment variables
pub fn load_config() -> Result<Config, config::ConfigError> {
use config::{Environment, File};
let pkg_name = env!("CARGO_PKG_NAME");
config::Config::builder()
.set_default(
"server.port",
dioxus::cli_config::server_port().unwrap_or(8080),
)?
.set_default(
"server.host",
dioxus::cli_config::server_ip()
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED))
.to_string(),
)?
.set_default("auth.open_signup", false)?
.set_default("auth.cookies_secure", DEFAULT_COOKIES_SECURE)?
.set_default("server.public_path", default_public_dir())?
.add_source(File::with_name(&format!("/etc/{pkg_name}/config")).required(false))
.add_source(File::with_name(&format!("/etc/{pkg_name}")).required(false))
.add_source(File::with_name("config").required(false))
.add_source(Environment::with_prefix(pkg_name).separator("_"))
.build()?
.try_deserialize()
}
/// Provide a sane default for the public path, using the same sources as Dioxus does internally.
/// Checks the `DIOXUS_PUBLIC_PATH` environment variable, then tries relative to the path of this
/// executable
fn default_public_dir() -> Option<String> {
std::env::var("DIOXUS_PUBLIC_PATH").ok().or_else(|| {
std::env::current_exe().ok().and_then(|path| {
path.parent()
.expect("current executable path must have a parent")
.join("public")
.into_os_string()
.into_string()
.ok()
})
})
}