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