Files
oxspeak_server/src/config.rs
T
2026-09-24 13:55:59 +02:00

220 lines
5.5 KiB
Rust

use config::{Config, ConfigError, File, FileFormat};
use serde::Deserialize;
use std::error::Error;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::path::Path;
use std::{fmt, fs};
#[derive(Debug)]
pub enum AppConfigError {
Io(std::io::Error),
Config(ConfigError),
}
impl fmt::Display for AppConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(err) => write!(f, "failed to access config file: {err}"),
Self::Config(err) => write!(f, "failed to load config: {err}"),
}
}
}
impl Error for AppConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Io(err) => Some(err),
Self::Config(err) => Some(err),
}
}
}
impl From<std::io::Error> for AppConfigError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
impl From<ConfigError> for AppConfigError {
fn from(err: ConfigError) -> Self {
Self::Config(err)
}
}
pub const DEFAULT_CONFIG_TOML: &str = r#"[network]
# IP address to bind to
host = "0.0.0.0"
# hostv6 = "::"
# TCP and UDP port can be the same
# HTTP port
tcp_port = 8080
# WebRTC ICE/Media UDP multiplexing port
udp_port = 8080
# Public IPv4 address advertised by ICE when behind NAT (forward udp_port to this server).
# external_ip = "203.0.113.1"
# STUN servers used when external_ip is absent; set [] to disable discovery.
# stun_servers = ["stun:stun.l.google.com:19302"]
# Optional native HTTPS (omit this section to keep plain HTTP behind a proxy).
# [network.tls]
# cert_path = "certs/server.pem"
# key_path = "certs/server-key.pem"
# names = ["localhost", "127.0.0.1"]
[database]
# DSN for database
# SQLite
url = "sqlite://oxspeak.db"
# PostgreSQL
# url = "postgresql://user:passwd@localhost:5432/db_name"
# MySQL
# url = "mysql://user:passwd@localhost:3306/db_name"
[media]
root = "media"
[jwt]
secret = "changeme"
# Duration in seconds
duration = 86400 # 1 day
refresh_duration = 1296000 # 15 days
"#;
#[derive(Debug, Clone, Deserialize)]
pub struct AppConfig {
pub network: NetworkConfig,
pub database: DatabaseConfig,
pub jwt: JwtConfig,
#[serde(default)]
pub media: MediaConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct MediaConfig {
#[serde(default = "default_media_root")]
pub root: String,
}
impl Default for MediaConfig {
fn default() -> Self {
Self {
root: default_media_root(),
}
}
}
fn default_media_root() -> String {
"media".to_string()
}
#[derive(Debug, Clone, Deserialize)]
pub struct NetworkConfig {
pub host: Ipv4Addr,
pub hostv6: Option<Ipv6Addr>,
pub tcp_port: u16,
pub udp_port: u16,
#[serde(default)]
pub external_ip: Option<Ipv4Addr>,
#[serde(default = "default_stun_servers")]
pub stun_servers: Vec<String>,
#[serde(default)]
pub tls: Option<TlsConfig>,
}
fn default_stun_servers() -> Vec<String> {
vec!["stun:stun.l.google.com:19302".to_string()]
}
#[derive(Debug, Clone, Deserialize)]
pub struct TlsConfig {
pub cert_path: std::path::PathBuf,
pub key_path: std::path::PathBuf,
pub names: Vec<String>,
}
#[derive(Clone, Deserialize)]
pub struct DatabaseConfig {
pub url: String,
}
impl fmt::Debug for DatabaseConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DatabaseConfig")
.field("url", &"<hidden>")
.finish()
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct JwtConfig {
pub secret: String,
pub duration: u64,
pub refresh_duration: u64,
}
impl AppConfig {
pub fn file_exists() -> bool {
Path::new("config.toml").exists()
}
pub fn load() -> Result<Self, AppConfigError> {
let settings = Config::builder()
.add_source(File::new("config.toml", FileFormat::Toml))
.build()?;
Ok(settings.try_deserialize()?)
}
pub fn gen_config() -> Result<(), AppConfigError> {
if !Self::file_exists() {
fs::write("config.toml", DEFAULT_CONFIG_TOML)?;
}
Ok(())
}
pub fn load_or_generate() -> Result<Self, AppConfigError> {
if !Self::file_exists() {
Self::gen_config()?;
tracing::info!(config_path = "config.toml", "Generated");
}
Self::load()
}
}
#[cfg(test)]
mod tests {
use super::{AppConfig, DEFAULT_CONFIG_TOML};
use std::net::Ipv4Addr;
#[test]
fn external_ip_is_optional_and_accepts_public_ipv4() {
let default: AppConfig = toml::from_str(DEFAULT_CONFIG_TOML).unwrap();
assert_eq!(default.network.external_ip, None);
assert_eq!(default.network.stun_servers, vec!["stun:stun.l.google.com:19302"]);
let configured = DEFAULT_CONFIG_TOML.replace(
"# external_ip = \"203.0.113.1\"",
"external_ip = \"203.0.113.1\"",
);
let config: AppConfig = toml::from_str(&configured).unwrap();
assert_eq!(config.network.external_ip, Some(Ipv4Addr::new(203, 0, 113, 1)));
}
#[test]
fn stun_servers_can_be_disabled_or_replaced() {
for (value, expected) in [
("[]", vec![]),
("[\"stun:example.org:3478\"]", vec!["stun:example.org:3478"]),
] {
let text = DEFAULT_CONFIG_TOML.replace(
"# stun_servers = [\"stun:stun.l.google.com:19302\"]",
&format!("stun_servers = {value}"),
);
let config: AppConfig = toml::from_str(&text).unwrap();
assert_eq!(config.network.stun_servers, expected);
}
}
}