Skip to main content

rust_cache_server/
config.rs

1use std::fs;
2use std::net::{IpAddr, Ipv4Addr, SocketAddr};
3
4use anyhow::Context;
5use serde::Deserialize;
6
7#[derive(Debug, Deserialize, Clone)]
8pub struct Config {
9    #[serde(default = "default_host")]
10    pub host: String,
11    /// Alias for host. If set, overrides host.
12    pub listen: Option<String>,
13    #[serde(default = "default_port")]
14    pub port: u16,
15    /// Maximum memory for values (in megabytes). If None, auto-detect from system.
16    pub max_memory_mb: Option<u64>,
17    /// Socket read buffer size (in kilobytes).
18    pub read_buffer_kb: Option<usize>,
19    /// Username for authentication (optional)
20    pub username: Option<String>,
21    /// Password for authentication (optional)
22    pub password: Option<String>,
23    /// Maximum failed login attempts before lockout (default: 3)
24    #[serde(default = "default_max_failed_attempts")]
25    pub max_failed_attempts: u32,
26    /// Lockout duration in seconds after max failed attempts (default: 600 = 10 minutes)
27    #[serde(default = "default_lockout_duration")]
28    pub lockout_duration_seconds: u64,
29}
30
31fn default_host() -> String {
32    "127.0.0.1".to_string()
33}
34
35fn default_port() -> u16 {
36    7777
37}
38
39fn default_max_failed_attempts() -> u32 {
40    3
41}
42
43fn default_lockout_duration() -> u64 {
44    600 // 10 minutes
45}
46
47impl Config {
48    pub fn load() -> anyhow::Result<Self> {
49        // Config search order (first found wins):
50        // 1. RUST_CACHE_CONFIG env var (explicit override)
51        // 2. /etc/rust-cache/rust-cache.toml (system-wide)
52        // 3. ~/.config/rust-cache/rust-cache.toml (user config)
53        // 4. ./rust-cache.toml (local, current working directory)
54        // 5. Defaults with auto-detected memory
55
56        let config_paths = if let Ok(env_path) = std::env::var("RUST_CACHE_CONFIG") {
57            vec![env_path]
58        } else {
59            let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
60            vec![
61                "/etc/rust-cache/rust-cache.conf".to_string(),
62                format!("{}/.config/rust-cache/rust-cache.toml", home),
63                "rust-cache.toml".to_string(),
64            ]
65        };
66
67        for path in config_paths {
68            if let Ok(text) = fs::read_to_string(&path) {
69                let mut cfg: Config = toml::from_str(&text)
70                    .with_context(|| format!("failed to parse config file at {path}"))?;
71
72                if cfg.max_memory_mb.is_none() {
73                    cfg.max_memory_mb = Self::auto_max_memory_mb();
74                }
75
76                return Ok(cfg);
77            }
78        }
79
80        // No config file found: use defaults with auto-detected memory.
81        Self::default_with_auto()
82    }
83
84    fn default_with_auto() -> anyhow::Result<Self> {
85        let max_memory_mb = Self::auto_max_memory_mb();
86        Ok(Config {
87            host: default_host(),
88            listen: None,
89            port: default_port(),
90            max_memory_mb,
91            read_buffer_kb: None,
92            username: None,
93            password: None,
94            max_failed_attempts: default_max_failed_attempts(),
95            lockout_duration_seconds: default_lockout_duration(),
96        })
97    }
98
99    fn auto_max_memory_mb() -> Option<u64> {
100        detect_total_memory_bytes().map(|bytes| {
101            // Use 50% of total RAM by default.
102            (bytes / 2) / (1024 * 1024)
103        })
104    }
105
106    pub fn addr(&self) -> SocketAddr {
107        let host_str = self.listen.as_ref().unwrap_or(&self.host);
108        let ip: IpAddr = host_str
109            .parse()
110            .unwrap_or_else(|_| IpAddr::V4(Ipv4Addr::LOCALHOST));
111        SocketAddr::new(ip, self.port)
112    }
113
114    pub fn requires_auth(&self) -> bool {
115        self.username.is_some() || self.password.is_some()
116    }
117
118    pub fn check_auth(&self, username: Option<&str>, password: Option<&str>) -> bool {
119        if !self.requires_auth() {
120            return true;
121        }
122
123        let username_ok = self.username.as_ref().map(|u| u.as_str()) == username;
124        let password_ok = self.password.as_ref().map(|p| p.as_str()) == password;
125
126        // If username is set, both must match. If only password is set, password must match.
127        if self.username.is_some() {
128            username_ok && password_ok
129        } else {
130            password_ok
131        }
132    }
133
134    pub fn max_memory_bytes(&self) -> Option<u64> {
135        self.max_memory_mb.map(|mb| mb * 1024 * 1024)
136    }
137
138    pub fn read_buffer_size(&self) -> usize {
139        self.read_buffer_kb.unwrap_or(4) * 1024
140    }
141}
142
143fn detect_total_memory_bytes() -> Option<u64> {
144    #[cfg(target_os = "linux")]
145    {
146        let contents = fs::read_to_string("/proc/meminfo").ok()?;
147        for line in contents.lines() {
148            if let Some(rest) = line.strip_prefix("MemTotal:") {
149                // Example line: "MemTotal:       16367452 kB"
150                let kb_str = rest
151                    .split_whitespace()
152                    .next()
153                    .unwrap_or_default();
154                if let Ok(kb) = kb_str.parse::<u64>() {
155                    return Some(kb * 1024);
156                }
157            }
158        }
159        None
160    }
161    #[cfg(not(target_os = "linux"))]
162    {
163        // Fallback: 1 GiB
164        Some(1024 * 1024 * 1024)
165    }
166}
167
168