220 lines
5.5 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|