deepslate 0.1.0

A high-performance Minecraft server proxy written in Rust.
Documentation
//! Configuration loaded from environment variables or a builder API.

use std::net::{Ipv4Addr, SocketAddr};

use thiserror::Error;

/// Errors returned when building proxy configuration.
#[derive(Debug, Error)]
pub enum ConfigError {
    /// A socket address could not be parsed.
    #[error(transparent)]
    AddrParse(#[from] std::net::AddrParseError),
    /// A numeric value could not be parsed.
    #[error(transparent)]
    ParseInt(#[from] std::num::ParseIntError),
    /// A required environment variable was missing.
    #[error("required environment variable {0} is not set")]
    MissingEnv(&'static str),
    /// An environment variable contained an invalid boolean value.
    #[error("invalid boolean for {key}: {value}")]
    InvalidBool {
        /// The environment variable name.
        key: &'static str,
        /// The invalid value.
        value: String,
    },
    /// The configured forwarding secret is empty.
    #[error("forwarding secret must not be empty")]
    MissingForwardingSecret,
    /// The configured compression level is invalid.
    #[error("invalid compression level {0}: must be 0-12")]
    InvalidCompressionLevel(i32),
}

/// Proxy configuration.
#[derive(Debug, Clone)]
pub struct Config {
    /// Address the proxy listens on for Minecraft clients.
    pub listen_addr: SocketAddr,
    /// Address the gRPC control plane listens on.
    pub grpc_addr: SocketAddr,
    /// Whether online-mode (Mojang authentication) is enabled.
    pub online_mode: bool,
    /// HMAC secret for Velocity modern forwarding.
    pub forwarding_secret: Vec<u8>,
    /// Compression threshold in bytes (-1 to disable).
    pub compression_threshold: i32,
    /// Zlib compression level (0-12, where 1 is fastest and 12 is best).
    pub compression_level: i32,
    /// MOTD shown in the server list.
    pub motd: String,
    /// Maximum player count shown in the server list.
    pub max_players: i32,
    /// Read timeout in milliseconds.
    pub read_timeout_ms: u64,
    /// Ordered list of server IDs to try for initial connections.
    pub try_servers: Vec<String>,
    /// Log level filter string.
    pub log_level: String,
    /// Whether to output logs as JSON.
    pub log_json: bool,
}

impl Config {
    /// Load configuration from environment variables.
    ///
    /// # Errors
    ///
    /// Returns an error if a required variable is missing or a value is invalid.
    pub fn from_env() -> Result<Self, ConfigError> {
        let listen_addr = env_or("DEEPSLATE_ADDR", "0.0.0.0:25565").parse()?;
        let grpc_addr = env_or("DEEPSLATE_GRPC_ADDR", "0.0.0.0:25577").parse()?;
        let online_mode = env_bool("DEEPSLATE_ONLINE_MODE", true)?;
        let forwarding_secret = env_required("DEEPSLATE_FORWARDING_SECRET")?.into_bytes();
        let compression_threshold = env_or("DEEPSLATE_COMPRESSION_THRESHOLD", "256").parse()?;
        let compression_level: i32 = env_or("DEEPSLATE_COMPRESSION_LEVEL", "1").parse()?;
        let motd = env_or("DEEPSLATE_MOTD", "A Deepslate Proxy");
        let max_players = env_or("DEEPSLATE_MAX_PLAYERS", "500").parse()?;
        let read_timeout_ms = env_or("DEEPSLATE_READ_TIMEOUT", "30000").parse()?;
        let try_servers = env_or("DEEPSLATE_TRY_SERVERS", "")
            .split(',')
            .map(str::trim)
            .filter(|s| !s.is_empty())
            .map(String::from)
            .collect();
        let log_level = env_or("DEEPSLATE_LOG_LEVEL", "info");
        let log_json = env_bool("DEEPSLATE_LOG_JSON", false)?;

        Self {
            listen_addr,
            grpc_addr,
            online_mode,
            forwarding_secret,
            compression_threshold,
            compression_level,
            motd,
            max_players,
            read_timeout_ms,
            try_servers,
            log_level,
            log_json,
        }
        .validate()
    }

    /// Validate the configuration and return it unchanged on success.
    ///
    /// # Errors
    ///
    /// Returns an error if any field contains an unsupported value.
    pub fn validate(self) -> Result<Self, ConfigError> {
        if self.forwarding_secret.is_empty() {
            return Err(ConfigError::MissingForwardingSecret);
        }

        if !(0..=12).contains(&self.compression_level) {
            return Err(ConfigError::InvalidCompressionLevel(self.compression_level));
        }

        Ok(self)
    }
}

impl Default for Config {
    fn default() -> Self {
        Self {
            listen_addr: SocketAddr::from((Ipv4Addr::UNSPECIFIED, 25_565)),
            grpc_addr: SocketAddr::from((Ipv4Addr::UNSPECIFIED, 25_577)),
            online_mode: true,
            forwarding_secret: Vec::new(),
            compression_threshold: 256,
            compression_level: 1,
            motd: "A Deepslate Proxy".to_string(),
            max_players: 500,
            read_timeout_ms: 30_000,
            try_servers: vec![],
            log_level: "info".to_string(),
            log_json: false,
        }
    }
}

/// Read an environment variable or return a default.
fn env_or(key: &str, default: &str) -> String {
    std::env::var(key).unwrap_or_else(|_| default.to_string())
}

/// Read a required environment variable.
fn env_required(key: &'static str) -> Result<String, ConfigError> {
    std::env::var(key).map_err(|_| ConfigError::MissingEnv(key))
}

/// Parse a boolean environment variable (accepts true/false/1/0, case-insensitive).
fn env_bool(key: &'static str, default: bool) -> Result<bool, ConfigError> {
    std::env::var(key).map_or_else(
        |_| Ok(default),
        |val| match val.to_lowercase().as_str() {
            "true" | "1" => Ok(true),
            "false" | "0" => Ok(false),
            _ => Err(ConfigError::InvalidBool { key, value: val }),
        },
    )
}