feox_server/
config.rs

1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::path::Path;
4
5/// Server configuration
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Config {
8    /// Bind address
9    pub bind_addr: String,
10
11    /// Port to listen on
12    pub port: u16,
13
14    /// Number of worker threads
15    pub threads: usize,
16
17    /// Path to FeOx data file (None for memory-only)
18    pub data_path: Option<String>,
19
20    /// Maximum connections per thread
21    pub max_connections_per_thread: usize,
22
23    /// Connection buffer size (per connection)
24    pub connection_buffer_size: usize,
25
26    /// TCP nodelay
27    pub tcp_nodelay: bool,
28
29    /// Pipeline queue depth
30    pub max_pipeline_depth: usize,
31
32    /// Enable NUMA awareness
33    pub numa_aware: bool,
34
35    /// Maximum memory for FeOx store (per shard)
36    pub max_memory_per_shard: Option<usize>,
37
38    /// Enable TTL support
39    pub enable_ttl: bool,
40
41    /// File size for persistent storage (in bytes)
42    /// Only used when data_path is set
43    pub file_size: Option<u64>,
44
45    /// Log level
46    pub log_level: String,
47
48    /// Password for AUTH command (Redis-compatible)
49    /// None means no authentication required
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub requirepass: Option<String>,
52}
53
54impl Default for Config {
55    fn default() -> Self {
56        Self {
57            bind_addr: "127.0.0.1".to_string(),
58            port: 6379,
59            threads: num_cpus::get(),
60            data_path: None,
61            max_connections_per_thread: 10000,
62            connection_buffer_size: 16 * 1024, // 16KB
63            tcp_nodelay: true,
64            max_pipeline_depth: 1000,
65            numa_aware: false,
66            max_memory_per_shard: Some(1024 * 1024 * 1024), // 1GB per shard
67            enable_ttl: true,
68            file_size: Some(10 * 1024 * 1024 * 1024), // 10GB default for persistent storage
69            log_level: "info".to_string(),
70            requirepass: None,
71        }
72    }
73}
74
75impl Config {
76    /// Load configuration from a TOML file
77    ///
78    /// # Example
79    ///
80    /// ```no_run
81    /// use feox_server::Config;
82    ///
83    /// # fn main() -> anyhow::Result<()> {
84    /// let config = Config::from_file("config.toml")?;
85    /// # Ok(())
86    /// # }
87    /// ```
88    pub fn from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
89        let contents = fs::read_to_string(path)?;
90        let mut config: Config = toml::from_str(&contents)?;
91
92        // Load password from environment if not in config file
93        if config.requirepass.is_none() {
94            if let Ok(password) = std::env::var("FEOX_AUTH_PASSWORD") {
95                config.requirepass = Some(password);
96                // Clear from environment after reading
97                std::env::remove_var("FEOX_AUTH_PASSWORD");
98            }
99        }
100
101        Ok(config)
102    }
103
104    /// Save configuration to a TOML file
105    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
106        let contents = toml::to_string_pretty(self)?;
107        fs::write(path, contents)?;
108        Ok(())
109    }
110
111    /// Validate configuration
112    pub fn validate(&self) -> anyhow::Result<()> {
113        if self.threads == 0 {
114            anyhow::bail!("threads must be > 0");
115        }
116
117        if self.port == 0 {
118            anyhow::bail!("port must be > 0");
119        }
120
121        if self.connection_buffer_size < 1024 {
122            anyhow::bail!("connection_buffer_size must be >= 1024");
123        }
124
125        Ok(())
126    }
127
128    /// Check if authentication is required
129    pub fn auth_required(&self) -> bool {
130        self.requirepass.is_some()
131    }
132
133    /// Validate password (constant-time comparison)
134    pub fn check_password(&self, password: &str) -> bool {
135        match &self.requirepass {
136            Some(correct) => constant_time_eq(password.as_bytes(), correct.as_bytes()),
137            None => false,
138        }
139    }
140}
141
142/// Constant-time string comparison to prevent timing attacks
143fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
144    if a.len() != b.len() {
145        return false;
146    }
147
148    let mut result = 0u8;
149    for (byte_a, byte_b) in a.iter().zip(b.iter()) {
150        result |= byte_a ^ byte_b;
151    }
152    result == 0
153}