Rust · 9814 bytes Raw Blame History
1 use anyhow::{Context, Result};
2 use serde::{Deserialize, Serialize};
3 use std::net::SocketAddr;
4 use std::path::{Path, PathBuf};
5
6 /// Configuration for ZephyrFS Node
7 ///
8 /// Transparency: All configuration options are clearly documented
9 /// Privacy: No sensitive data is logged or stored in plaintext
10 #[derive(Debug, Clone, Deserialize, Serialize)]
11 pub struct Config {
12 /// Node identification (generated if not provided)
13 pub node_id: Option<String>,
14
15 /// P2P networking configuration
16 pub network: NetworkConfig,
17
18 /// Storage configuration
19 pub storage: StorageConfig,
20
21 /// Coordinator connection
22 pub coordinator: CoordinatorConfig,
23
24 /// Security settings
25 pub security: SecurityConfig,
26 }
27
28 #[derive(Debug, Clone, Deserialize, Serialize)]
29 pub struct NetworkConfig {
30 /// P2P listening port (default: 4001)
31 pub p2p_port: u16,
32
33 /// API listening port (default: 8080)
34 pub api_port: u16,
35
36 /// Enable mDNS for local discovery (default: true)
37 pub enable_mdns: bool,
38
39 /// Bootstrap peers for initial connection
40 pub bootstrap_peers: Vec<String>,
41
42 /// Maximum number of peers to maintain (default: 50)
43 pub max_peers: usize,
44
45 /// NAT traversal settings
46 pub enable_nat_traversal: bool,
47 pub stun_servers: Vec<String>,
48 }
49
50 #[derive(Debug, Clone, Deserialize, Serialize)]
51 pub struct StorageConfig {
52 /// Data directory for storing chunks and metadata
53 pub data_dir: PathBuf,
54
55 /// Maximum storage to allocate (in bytes)
56 pub max_storage: u64,
57
58 /// Chunk size for file splitting (default: 1MB)
59 pub chunk_size: usize,
60
61 /// Enable storage encryption at rest
62 pub encrypt_at_rest: bool,
63 }
64
65 #[derive(Debug, Clone, Deserialize, Serialize)]
66 pub struct CoordinatorConfig {
67 /// Coordinator service URL
68 pub url: String,
69
70 /// Connection timeout (seconds)
71 pub timeout: u64,
72
73 /// Heartbeat interval (seconds)
74 pub heartbeat_interval: u64,
75 }
76
77 #[derive(Debug, Clone, Deserialize, Serialize)]
78 pub struct SecurityConfig {
79 /// Enable strict TLS verification
80 pub strict_tls: bool,
81
82 /// Minimum peer reputation score to accept connections
83 pub min_peer_reputation: f64,
84
85 /// Maximum connections from unknown peers
86 pub max_unknown_peers: usize,
87
88 /// Enable connection rate limiting
89 pub enable_rate_limiting: bool,
90 }
91
92 impl Default for Config {
93 fn default() -> Self {
94 Self {
95 node_id: None, // Generated at startup
96 network: NetworkConfig::default(),
97 storage: StorageConfig::default(),
98 coordinator: CoordinatorConfig::default(),
99 security: SecurityConfig::default(),
100 }
101 }
102 }
103
104 impl Default for NetworkConfig {
105 fn default() -> Self {
106 Self {
107 p2p_port: 4001,
108 api_port: 8080,
109 enable_mdns: true,
110 bootstrap_peers: vec![],
111 max_peers: 50,
112 enable_nat_traversal: true,
113 stun_servers: vec![
114 "stun:stun.l.google.com:19302".to_string(),
115 "stun:stun1.l.google.com:19302".to_string(),
116 ],
117 }
118 }
119 }
120
121 impl Default for StorageConfig {
122 fn default() -> Self {
123 Self {
124 data_dir: PathBuf::from("./data"),
125 max_storage: 10 * 1024 * 1024 * 1024, // 10GB
126 chunk_size: 1024 * 1024, // 1MB
127 encrypt_at_rest: true, // Privacy: Always encrypt by default
128 }
129 }
130 }
131
132 impl Default for CoordinatorConfig {
133 fn default() -> Self {
134 Self {
135 url: "http://localhost:8080".to_string(),
136 timeout: 30,
137 heartbeat_interval: 60,
138 }
139 }
140 }
141
142 impl Default for SecurityConfig {
143 fn default() -> Self {
144 Self {
145 strict_tls: true, // Safety: Always use strict TLS
146 min_peer_reputation: 0.5,
147 max_unknown_peers: 10,
148 enable_rate_limiting: true, // Safety: Prevent DoS attacks
149 }
150 }
151 }
152
153 impl Config {
154 /// Load configuration from file or environment variables
155 ///
156 /// Transparency: Configuration loading process is fully logged
157 pub fn load(config_path: Option<&Path>) -> Result<Self> {
158 let mut config = Config::default();
159
160 // Load from file if provided
161 if let Some(path) = config_path {
162 let contents = std::fs::read_to_string(path)
163 .with_context(|| format!("Failed to read config file: {:?}", path))?;
164
165 config = serde_yaml::from_str(&contents)
166 .with_context(|| format!("Failed to parse config file: {:?}", path))?;
167 }
168
169 // Override with environment variables (for Docker/container deployment)
170 config.apply_env_overrides()?;
171
172 // Generate node ID if not provided
173 if config.node_id.is_none() {
174 config.node_id = Some(generate_node_id());
175 }
176
177 // Validate configuration
178 config.validate()?;
179
180 Ok(config)
181 }
182
183 /// Apply environment variable overrides
184 ///
185 /// Transparency: All environment variables are documented
186 fn apply_env_overrides(&mut self) -> Result<()> {
187 if let Ok(node_id) = std::env::var("ZEPHYR_NODE_ID") {
188 self.node_id = Some(node_id);
189 }
190
191 if let Ok(p2p_port) = std::env::var("ZEPHYR_P2P_PORT") {
192 self.network.p2p_port = p2p_port.parse()
193 .context("Invalid ZEPHYR_P2P_PORT value")?;
194 }
195
196 if let Ok(api_port) = std::env::var("ZEPHYR_API_PORT") {
197 self.network.api_port = api_port.parse()
198 .context("Invalid ZEPHYR_API_PORT value")?;
199 }
200
201 if let Ok(coordinator_url) = std::env::var("ZEPHYR_COORDINATOR_URL") {
202 self.coordinator.url = coordinator_url;
203 }
204
205 if let Ok(max_storage) = std::env::var("ZEPHYR_MAX_STORAGE") {
206 self.storage.max_storage = parse_storage_size(&max_storage)
207 .context("Invalid ZEPHYR_MAX_STORAGE value")?;
208 }
209
210 Ok(())
211 }
212
213 /// Validate configuration for safety and security
214 ///
215 /// Safety: Comprehensive validation prevents misconfigurations
216 fn validate(&self) -> Result<()> {
217 // Validate ports are available
218 if self.network.p2p_port == self.network.api_port {
219 anyhow::bail!("P2P and API ports must be different");
220 }
221
222 // Validate storage limits
223 if self.storage.max_storage == 0 {
224 anyhow::bail!("Maximum storage must be greater than 0");
225 }
226
227 if self.storage.chunk_size == 0 || self.storage.chunk_size > 100 * 1024 * 1024 {
228 anyhow::bail!("Chunk size must be between 1 byte and 100MB");
229 }
230
231 // Validate coordinator URL
232 if self.coordinator.url.is_empty() {
233 anyhow::bail!("Coordinator URL must be provided");
234 }
235
236 // Validate security settings
237 if self.security.min_peer_reputation < 0.0 || self.security.min_peer_reputation > 1.0 {
238 anyhow::bail!("Minimum peer reputation must be between 0.0 and 1.0");
239 }
240
241 Ok(())
242 }
243
244 /// Get P2P listen address
245 pub fn p2p_listen_addr(&self) -> SocketAddr {
246 ([0, 0, 0, 0], self.network.p2p_port).into()
247 }
248
249 /// Get API listen address
250 pub fn api_listen_addr(&self) -> SocketAddr {
251 ([127, 0, 0, 1], self.network.api_port).into()
252 }
253 }
254
255 /// Generate a secure, unique node ID
256 ///
257 /// Privacy: Node ID doesn't reveal any personal information
258 fn generate_node_id() -> String {
259 use rand::Rng;
260 let mut rng = rand::thread_rng();
261 let id: [u8; 16] = rng.gen();
262 format!("zephyr-{}", hex::encode(id))
263 }
264
265 /// Parse storage size from human-readable format (e.g., "10GB", "500MB")
266 ///
267 /// Transparency: Clear error messages for invalid formats
268 fn parse_storage_size(size_str: &str) -> Result<u64> {
269 let size_str = size_str.to_uppercase();
270
271 if let Some(num_str) = size_str.strip_suffix("GB") {
272 let num: u64 = num_str.parse().context("Invalid number in storage size")?;
273 Ok(num * 1024 * 1024 * 1024)
274 } else if let Some(num_str) = size_str.strip_suffix("MB") {
275 let num: u64 = num_str.parse().context("Invalid number in storage size")?;
276 Ok(num * 1024 * 1024)
277 } else if let Some(num_str) = size_str.strip_suffix("KB") {
278 let num: u64 = num_str.parse().context("Invalid number in storage size")?;
279 Ok(num * 1024)
280 } else {
281 // Assume bytes if no suffix
282 size_str.parse().context("Invalid storage size format")
283 }
284 }
285
286 // Add hex dependency to Cargo.toml
287 #[cfg(test)]
288 mod tests {
289 use super::*;
290
291 #[test]
292 fn test_default_config_is_valid() {
293 let config = Config::default();
294 assert!(config.validate().is_ok());
295 }
296
297 #[test]
298 fn test_storage_size_parsing() {
299 assert_eq!(parse_storage_size("10GB").unwrap(), 10 * 1024 * 1024 * 1024);
300 assert_eq!(parse_storage_size("500MB").unwrap(), 500 * 1024 * 1024);
301 assert_eq!(parse_storage_size("1024KB").unwrap(), 1024 * 1024);
302 assert_eq!(parse_storage_size("1024").unwrap(), 1024);
303 assert!(parse_storage_size("invalid").is_err());
304 }
305
306 #[test]
307 fn test_config_validation() {
308 let mut config = Config::default();
309
310 // Test invalid port configuration
311 config.network.p2p_port = config.network.api_port;
312 assert!(config.validate().is_err());
313
314 // Test invalid storage
315 config = Config::default();
316 config.storage.max_storage = 0;
317 assert!(config.validate().is_err());
318 }
319 }