openws-server 0.1.4

WebSocket server with room-based messaging, private messages, HTTP broadcast, and configurable limits
use serde::Deserialize;

#[derive(Debug, Deserialize, Clone)]
pub struct ServerSection {
    #[serde(default = "default_host")]
    pub host: String,
    #[serde(default = "default_port")]
    pub port: u16,
}

impl Default for ServerSection {
    fn default() -> Self {
        Self {
            host: default_host(),
            port: default_port(),
        }
    }
}

#[derive(Debug, Deserialize, Clone)]
pub struct LimitsSection {
    #[serde(default = "default_max_clients")]
    pub max_clients: usize,
    #[serde(default = "default_max_rooms")]
    pub max_rooms: usize,
    #[serde(default = "default_max_message_size")]
    pub max_message_size: usize,
    #[serde(default = "default_max_username_len")]
    pub max_username_len: usize,
}

impl Default for LimitsSection {
    fn default() -> Self {
        Self {
            max_clients: default_max_clients(),
            max_rooms: default_max_rooms(),
            max_message_size: default_max_message_size(),
            max_username_len: default_max_username_len(),
        }
    }
}

#[derive(Debug, Deserialize, Clone)]
pub struct HeartbeatSection {
    #[serde(default = "default_ping_interval")]
    pub ping_interval_secs: u64,
    #[serde(default = "default_pong_timeout")]
    pub pong_timeout_secs: u64,
}

impl Default for HeartbeatSection {
    fn default() -> Self {
        Self {
            ping_interval_secs: default_ping_interval(),
            pong_timeout_secs: default_pong_timeout(),
        }
    }
}

#[derive(Debug, Default, Deserialize, Clone)]
pub struct BroadcastSection {
    #[serde(default)]
    pub api_key: String,
}

#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
    #[serde(default)]
    pub server: ServerSection,
    #[serde(default)]
    pub limits: LimitsSection,
    #[serde(default)]
    pub heartbeat: HeartbeatSection,
    #[serde(default)]
    pub broadcast: BroadcastSection,
}

fn default_host() -> String {
    "0.0.0.0".into()
}
fn default_port() -> u16 {
    3000
}
fn default_max_clients() -> usize {
    1000
}
fn default_max_rooms() -> usize {
    100
}
fn default_max_message_size() -> usize {
    8192
}
fn default_max_username_len() -> usize {
    32
}
fn default_ping_interval() -> u64 {
    30
}
fn default_pong_timeout() -> u64 {
    10
}

pub fn load_config() -> Result<AppConfig, config::ConfigError> {
    let c = config::Config::builder()
        .add_source(config::File::with_name("openws").required(false))
        .add_source(
            config::Environment::with_prefix("OPENWS")
                .separator("_")
                .try_parsing(true),
        )
        .build()?;

    let mut cfg: AppConfig = c.try_deserialize()?;

    if let Ok(v) = std::env::var("OPENWS_PORT") {
        if let Ok(p) = v.parse() {
            cfg.server.port = p;
        }
    }
    if let Ok(v) = std::env::var("OPENWS_MAX_CLIENTS") {
        if let Ok(n) = v.parse() {
            cfg.limits.max_clients = n;
        }
    }
    if let Ok(v) = std::env::var("OPENWS_PING_INTERVAL") {
        if let Ok(s) = v.parse() {
            cfg.heartbeat.ping_interval_secs = s;
        }
    }
    if let Ok(v) = std::env::var("OPENWS_API_KEY") {
        cfg.broadcast.api_key = v;
    }

    Ok(cfg)
}

pub fn mask_api_key(key: &str) -> String {
    if key.is_empty() {
        return "(none)".into();
    }
    if key.len() <= 4 {
        return "****".into();
    }
    format!("{}****", &key[..4])
}