rust-cache-server 0.1.0

High-performance Redis-like cache server in Rust - open source alternative without Redis licensing concerns
Documentation
use std::fs;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};

use anyhow::Context;
use serde::Deserialize;

#[derive(Debug, Deserialize, Clone)]
pub struct Config {
    #[serde(default = "default_host")]
    pub host: String,
    /// Alias for host. If set, overrides host.
    pub listen: Option<String>,
    #[serde(default = "default_port")]
    pub port: u16,
    /// Maximum memory for values (in megabytes). If None, auto-detect from system.
    pub max_memory_mb: Option<u64>,
    /// Socket read buffer size (in kilobytes).
    pub read_buffer_kb: Option<usize>,
    /// Username for authentication (optional)
    pub username: Option<String>,
    /// Password for authentication (optional)
    pub password: Option<String>,
    /// Maximum failed login attempts before lockout (default: 3)
    #[serde(default = "default_max_failed_attempts")]
    pub max_failed_attempts: u32,
    /// Lockout duration in seconds after max failed attempts (default: 600 = 10 minutes)
    #[serde(default = "default_lockout_duration")]
    pub lockout_duration_seconds: u64,
}

fn default_host() -> String {
    "127.0.0.1".to_string()
}

fn default_port() -> u16 {
    7777
}

fn default_max_failed_attempts() -> u32 {
    3
}

fn default_lockout_duration() -> u64 {
    600 // 10 minutes
}

impl Config {
    pub fn load() -> anyhow::Result<Self> {
        // Config search order (first found wins):
        // 1. RUST_CACHE_CONFIG env var (explicit override)
        // 2. /etc/rust-cache/rust-cache.toml (system-wide)
        // 3. ~/.config/rust-cache/rust-cache.toml (user config)
        // 4. ./rust-cache.toml (local, current working directory)
        // 5. Defaults with auto-detected memory

        let config_paths = if let Ok(env_path) = std::env::var("RUST_CACHE_CONFIG") {
            vec![env_path]
        } else {
            let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
            vec![
                "/etc/rust-cache/rust-cache.conf".to_string(),
                format!("{}/.config/rust-cache/rust-cache.toml", home),
                "rust-cache.toml".to_string(),
            ]
        };

        for path in config_paths {
            if let Ok(text) = fs::read_to_string(&path) {
                let mut cfg: Config = toml::from_str(&text)
                    .with_context(|| format!("failed to parse config file at {path}"))?;

                if cfg.max_memory_mb.is_none() {
                    cfg.max_memory_mb = Self::auto_max_memory_mb();
                }

                return Ok(cfg);
            }
        }

        // No config file found: use defaults with auto-detected memory.
        Self::default_with_auto()
    }

    fn default_with_auto() -> anyhow::Result<Self> {
        let max_memory_mb = Self::auto_max_memory_mb();
        Ok(Config {
            host: default_host(),
            listen: None,
            port: default_port(),
            max_memory_mb,
            read_buffer_kb: None,
            username: None,
            password: None,
            max_failed_attempts: default_max_failed_attempts(),
            lockout_duration_seconds: default_lockout_duration(),
        })
    }

    fn auto_max_memory_mb() -> Option<u64> {
        detect_total_memory_bytes().map(|bytes| {
            // Use 50% of total RAM by default.
            (bytes / 2) / (1024 * 1024)
        })
    }

    pub fn addr(&self) -> SocketAddr {
        let host_str = self.listen.as_ref().unwrap_or(&self.host);
        let ip: IpAddr = host_str
            .parse()
            .unwrap_or_else(|_| IpAddr::V4(Ipv4Addr::LOCALHOST));
        SocketAddr::new(ip, self.port)
    }

    pub fn requires_auth(&self) -> bool {
        self.username.is_some() || self.password.is_some()
    }

    pub fn check_auth(&self, username: Option<&str>, password: Option<&str>) -> bool {
        if !self.requires_auth() {
            return true;
        }

        let username_ok = self.username.as_ref().map(|u| u.as_str()) == username;
        let password_ok = self.password.as_ref().map(|p| p.as_str()) == password;

        // If username is set, both must match. If only password is set, password must match.
        if self.username.is_some() {
            username_ok && password_ok
        } else {
            password_ok
        }
    }

    pub fn max_memory_bytes(&self) -> Option<u64> {
        self.max_memory_mb.map(|mb| mb * 1024 * 1024)
    }

    pub fn read_buffer_size(&self) -> usize {
        self.read_buffer_kb.unwrap_or(4) * 1024
    }
}

fn detect_total_memory_bytes() -> Option<u64> {
    #[cfg(target_os = "linux")]
    {
        let contents = fs::read_to_string("/proc/meminfo").ok()?;
        for line in contents.lines() {
            if let Some(rest) = line.strip_prefix("MemTotal:") {
                // Example line: "MemTotal:       16367452 kB"
                let kb_str = rest
                    .split_whitespace()
                    .next()
                    .unwrap_or_default();
                if let Ok(kb) = kb_str.parse::<u64>() {
                    return Some(kb * 1024);
                }
            }
        }
        None
    }
    #[cfg(not(target_os = "linux"))]
    {
        // Fallback: 1 GiB
        Some(1024 * 1024 * 1024)
    }
}