1use crate::address::NetworkAddress;
27use crate::error::ConfigError;
28use crate::validation::{
29 ValidationContext, validate_config_value, validate_file_path, validate_network_address,
30};
31use crate::{P2PError, Result};
32use regex::Regex;
33use serde::{Deserialize, Serialize};
34use std::env;
35use std::fs;
36use std::net::SocketAddr;
37use std::path::{Path, PathBuf};
38use std::str::FromStr;
39use tracing::info;
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(default)]
44#[derive(Default)]
45pub struct Config {
46 pub network: NetworkConfig,
48 pub security: SecurityConfig,
50 pub storage: StorageConfig,
52
53 pub dht: DhtConfig,
55 pub transport: TransportConfig,
57 pub identity: IdentityConfig,
59 #[serde(default)]
61 pub attestation: crate::attestation::AttestationConfig,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(default)]
67pub struct NetworkConfig {
68 pub bootstrap_nodes: Vec<String>,
70 pub listen_address: String,
72 pub public_address: Option<String>,
74 pub ipv6_enabled: bool,
76 pub max_connections: usize,
78 pub connection_timeout: u64,
80 pub keepalive_interval: u64,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86#[serde(default)]
87pub struct SecurityConfig {
88 pub rate_limit: u32,
90 pub connection_limit: u32,
92 pub encryption_enabled: bool,
94 pub min_tls_version: String,
96 pub identity_security_level: String,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102#[serde(default)]
103pub struct StorageConfig {
104 pub path: PathBuf,
106 pub max_size: String,
108 pub cache_size: u64,
110 pub compression_enabled: bool,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(default)]
117pub struct DhtConfig {
118 pub replication_factor: u8,
120 pub alpha: u8,
122 pub beta: u8,
124 pub record_ttl: u64,
126 pub adaptive_routing: bool,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(default)]
133pub struct TransportConfig {
134 pub protocol: String,
136 pub quic_enabled: bool,
138 pub tcp_enabled: bool,
140 pub webrtc_enabled: bool,
142 pub buffer_size: usize,
144 pub server_name: String,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(default)]
151pub struct IdentityConfig {
152 pub derivation_path: String,
154 pub rotation_interval: u32,
156 pub backup_enabled: bool,
158 pub backup_interval: u32,
160}
161
162impl Default for NetworkConfig {
165 fn default() -> Self {
166 Self {
167 bootstrap_nodes: vec![],
168 listen_address: "0.0.0.0:9000".to_string(),
170 public_address: None,
171 ipv6_enabled: true,
172 max_connections: 10000,
173 connection_timeout: 30,
174 keepalive_interval: 60,
175 }
176 }
177}
178
179impl Default for SecurityConfig {
180 fn default() -> Self {
181 Self {
182 rate_limit: 1000,
183 connection_limit: 100,
184 encryption_enabled: true,
185 min_tls_version: "1.3".to_string(),
186 identity_security_level: "High".to_string(),
187 }
188 }
189}
190
191impl Default for StorageConfig {
192 fn default() -> Self {
193 Self {
194 path: PathBuf::from("./data"),
195 max_size: "10GB".to_string(),
196 cache_size: 256,
197 compression_enabled: true,
198 }
199 }
200}
201
202impl Default for DhtConfig {
203 fn default() -> Self {
204 Self {
205 replication_factor: 8,
206 alpha: 3,
207 beta: 1,
208 record_ttl: 3600,
209 adaptive_routing: true,
210 }
211 }
212}
213
214impl Default for TransportConfig {
215 fn default() -> Self {
216 Self {
217 protocol: "quic".to_string(),
218 quic_enabled: true,
219 tcp_enabled: true,
220 webrtc_enabled: false,
221 buffer_size: 65536,
222 server_name: "p2p.local".to_string(),
223 }
224 }
225}
226
227impl Default for IdentityConfig {
228 fn default() -> Self {
229 Self {
230 derivation_path: "m/44'/0'/0'/0/0".to_string(),
231 rotation_interval: 90,
232 backup_enabled: true,
233 backup_interval: 24,
234 }
235 }
236}
237
238impl Config {
239 pub fn load() -> Result<Self> {
258 Self::load_with_path::<&str>(None)
259 }
260
261 pub fn load_with_path<P: AsRef<Path>>(path: Option<P>) -> Result<Self> {
277 let mut config = Self::default();
279
280 if let Some(path) = path {
282 config = Self::load_from_file(path)?;
283 } else {
284 for location in &["saorsa.toml", "config.toml", "/etc/saorsa/config.toml"] {
286 if Path::new(location).exists() {
287 info!("Loading config from: {}", location);
288 config = Self::load_from_file(location)?;
289 break;
290 }
291 }
292 }
293
294 config.apply_env_overrides()?;
296
297 config.validate()?;
299
300 Ok(config)
301 }
302
303 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
305 let content = fs::read_to_string(&path).map_err(|e| {
306 P2PError::Config(ConfigError::IoError {
307 path: path.as_ref().to_string_lossy().to_string().into(),
308 source: e,
309 })
310 })?;
311
312 toml::from_str(&content)
313 .map_err(|e| P2PError::Config(ConfigError::ParseError(e.to_string().into())))
314 }
315
316 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
318 let content = toml::to_string_pretty(self)
319 .map_err(|e| P2PError::Config(ConfigError::ParseError(e.to_string().into())))?;
320
321 fs::write(&path, content).map_err(|e| {
322 P2PError::Config(ConfigError::IoError {
323 path: path.as_ref().to_string_lossy().to_string().into(),
324 source: e,
325 })
326 })?;
327
328 Ok(())
329 }
330
331 fn apply_env_overrides(&mut self) -> Result<()> {
333 if let Ok(val) = env::var("SAORSA_LISTEN_ADDRESS") {
335 self.network.listen_address = val;
336 }
337 if let Ok(val) = env::var("SAORSA_PUBLIC_ADDRESS") {
338 self.network.public_address = Some(val);
339 }
340 if let Ok(val) = env::var("SAORSA_BOOTSTRAP_NODES") {
341 self.network.bootstrap_nodes = val.split(',').map(String::from).collect();
342 }
343 if let Ok(val) = env::var("SAORSA_MAX_CONNECTIONS") {
344 self.network.max_connections = val.parse().map_err(|_| {
345 P2PError::Config(ConfigError::InvalidValue {
346 field: "max_connections".to_string().into(),
347 reason: "Invalid value".to_string().into(),
348 })
349 })?;
350 }
351
352 if let Ok(val) = env::var("SAORSA_RATE_LIMIT") {
354 self.security.rate_limit = val.parse().map_err(|_| {
355 P2PError::Config(ConfigError::InvalidValue {
356 field: "rate_limit".to_string().into(),
357 reason: "Invalid value".to_string().into(),
358 })
359 })?;
360 }
361 if let Ok(val) = env::var("SAORSA_ENCRYPTION_ENABLED") {
362 self.security.encryption_enabled = val.parse().map_err(|_| {
363 P2PError::Config(ConfigError::InvalidValue {
364 field: "encryption_enabled".to_string().into(),
365 reason: "Invalid value".to_string().into(),
366 })
367 })?;
368 }
369
370 if let Ok(val) = env::var("SAORSA_DATA_PATH") {
372 self.storage.path = PathBuf::from(val);
373 }
374 if let Ok(val) = env::var("SAORSA_MAX_STORAGE") {
375 self.storage.max_size = val;
376 }
377
378 Ok(())
379 }
380
381 pub fn validate(&self) -> Result<()> {
383 let mut errors = Vec::new();
384
385 if let Err(e) = self.validate_address(&self.network.listen_address, "listen_address") {
387 errors.push(e);
388 }
389
390 if let Some(addr) = &self.network.public_address
391 && let Err(e) = self.validate_address(addr, "public_address")
392 {
393 errors.push(e);
394 }
395
396 for (i, node) in self.network.bootstrap_nodes.iter().enumerate() {
397 if let Err(e) = self.validate_address(node, &format!("bootstrap_node[{}]", i)) {
398 errors.push(e);
399 }
400 }
401
402 if let Err(e) = validate_config_value(
404 &self.network.max_connections.to_string(),
405 Some(1_usize),
406 Some(100_000_usize),
407 ) {
408 errors.push(P2PError::Config(ConfigError::InvalidValue {
409 field: "max_connections".to_string().into(),
410 reason: e.to_string().into(),
411 }));
412 }
413
414 if let Err(e) = validate_config_value(
415 &self.security.rate_limit.to_string(),
416 Some(1_u32),
417 Some(1_000_000_u32),
418 ) {
419 errors.push(P2PError::Config(ConfigError::InvalidValue {
420 field: "rate_limit".to_string().into(),
421 reason: e.to_string().into(),
422 }));
423 }
424
425 if self.storage.path.exists()
427 && let Err(e) = validate_file_path(&self.storage.path)
428 {
429 errors.push(P2PError::Config(ConfigError::InvalidValue {
430 field: "storage.path".to_string().into(),
431 reason: format!("{:?}: {}", self.storage.path, e).into(),
432 }));
433 }
434
435 if !self.validate_size_format(&self.storage.max_size) {
437 errors.push(P2PError::Config(ConfigError::InvalidValue {
438 field: "max_size".to_string().into(),
439 reason: format!("Invalid storage size format: {}", self.storage.max_size).into(),
440 }));
441 }
442
443 match self.transport.protocol.as_str() {
445 "quic" | "tcp" | "webrtc" => {}
446 _ => errors.push(P2PError::Config(ConfigError::InvalidValue {
447 field: "protocol".to_string().into(),
448 reason: format!("Invalid transport protocol: {}", self.transport.protocol).into(),
449 })),
450 }
451
452 if errors.is_empty() {
453 Ok(())
454 } else {
455 Err(errors.into_iter().next().unwrap_or_else(|| {
457 P2PError::Config(ConfigError::InvalidValue {
458 field: "unknown".to_string().into(),
459 reason: "Validation failed with unknown error".to_string().into(),
460 })
461 }))
462 }
463 }
464
465 fn validate_address(&self, addr: &str, field: &str) -> Result<()> {
467 if let Ok(socket_addr) = SocketAddr::from_str(addr) {
469 let ctx = ValidationContext::default()
471 .allow_localhost() .allow_private_ips(); return validate_network_address(&socket_addr, &ctx).map_err(|e| {
475 P2PError::Config(ConfigError::InvalidValue {
476 field: field.to_string().into(),
477 reason: e.to_string().into(),
478 })
479 });
480 }
481
482 if let Ok(network_addr) = crate::NetworkAddress::from_four_words(addr) {
484 let ctx = ValidationContext::default()
486 .allow_localhost()
487 .allow_private_ips();
488
489 return validate_network_address(&network_addr.socket_addr(), &ctx).map_err(|e| {
490 P2PError::Config(ConfigError::InvalidValue {
491 field: field.to_string().into(),
492 reason: e.to_string().into(),
493 })
494 });
495 }
496
497 if addr.starts_with("/ip4/") || addr.starts_with("/ip6/") {
499 return Ok(());
501 }
502
503 Err(P2PError::Config(ConfigError::InvalidValue {
504 field: field.to_string().into(),
505 reason: format!("Invalid address format: {}", addr).into(),
506 }))
507 }
508
509 fn validate_size_format(&self, size: &str) -> bool {
511 thread_local! {
512 static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^\d+(?:\.\d+)?\s*(?:B|KB|MB|GB|TB)$")
514 .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
515 }
516 SIZE_REGEX.with(|re| re.as_ref().ok().map(|r| r.is_match(size)).unwrap_or(false))
517 }
518
519 pub fn development() -> Self {
521 let mut config = Self::default();
522 config.network.listen_address = "127.0.0.1:9000".to_string();
523 config.security.rate_limit = 10000;
524 config.security.connection_limit = 1000;
525 config.storage.path = PathBuf::from("./dev-data");
526 config
527 }
528
529 pub fn production() -> Self {
531 let mut config = Self::default();
532 config.network.listen_address =
534 env::var("SAORSA_LISTEN_ADDRESS").unwrap_or_else(|_| "0.0.0.0:9000".to_string());
535 config.security.rate_limit = 1000;
536 config.security.connection_limit = 100;
537 config.storage.path = PathBuf::from("/var/lib/saorsa");
538 config.transport.buffer_size = 131072;
540 config
541 }
542
543 pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
545 SocketAddr::from_str(&self.network.listen_address).map_err(|e| {
546 P2PError::Config(ConfigError::InvalidValue {
547 field: "listen_address".to_string().into(),
548 reason: format!("Invalid address: {}", e).into(),
549 })
550 })
551 }
552
553 pub fn bootstrap_addrs(&self) -> Result<Vec<NetworkAddress>> {
555 self.network
556 .bootstrap_nodes
557 .iter()
558 .map(|addr| {
559 addr.parse::<NetworkAddress>().map_err(|e| {
560 P2PError::Config(ConfigError::InvalidValue {
561 field: "bootstrap_nodes".to_string().into(),
562 reason: format!("Invalid address: {}", e).into(),
563 })
564 })
565 })
566 .collect()
567 }
568
569 pub fn parse_size(size: &str) -> Result<u64> {
581 thread_local! {
582 static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$")
583 .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
584 }
585
586 SIZE_REGEX.with(|re| -> Result<u64> {
587 let re = match re {
588 Ok(r) => r,
589 Err(e) => {
590 return Err(P2PError::Config(ConfigError::InvalidValue {
591 field: "size".to_string().into(),
592 reason: e.to_string().into(),
593 }));
594 }
595 };
596 if let Some(captures) = re.captures(size) {
597 let value: f64 = captures
598 .get(1)
599 .and_then(|m| m.as_str().parse().ok())
600 .ok_or_else(|| {
601 P2PError::Config(ConfigError::InvalidValue {
602 field: "size".to_string().into(),
603 reason: "Invalid numeric value".to_string().into(),
604 })
605 })?;
606
607 let unit = captures.get(2).map(|m| m.as_str()).unwrap_or("B");
608 let multiplier = match unit {
609 "B" => 1u64,
610 "KB" => 1024,
611 "MB" => 1024 * 1024,
612 "GB" => 1024 * 1024 * 1024,
613 "TB" => 1024u64.pow(4),
614 _ => {
615 return Err(P2PError::Config(ConfigError::InvalidValue {
616 field: "size".to_string().into(),
617 reason: format!("Unknown unit: {}", unit).into(),
618 }));
619 }
620 };
621
622 Ok((value * multiplier as f64) as u64)
623 } else {
624 Err(P2PError::Config(ConfigError::InvalidValue {
625 field: "size".to_string().into(),
626 reason: format!("Invalid size format: {}", size).into(),
627 }))
628 }
629 })
630 }
631
632 pub fn storage_max_size_bytes(&self) -> Result<u64> {
634 Self::parse_size(&self.storage.max_size)
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use tempfile::NamedTempFile;
642
643 #[test]
644 fn test_default_config() {
645 let config = Config::default();
646 assert_eq!(config.network.listen_address, "0.0.0.0:9000");
647 assert_eq!(config.security.rate_limit, 1000);
648 assert!(config.security.encryption_enabled);
649 }
650
651 #[test]
652 fn test_development_config() {
653 let config = Config::development();
654 assert_eq!(config.network.listen_address, "127.0.0.1:9000");
655 assert_eq!(config.security.rate_limit, 10000);
656 }
657
658 #[test]
659 fn test_production_config() {
660 let config = Config::production();
661 assert_eq!(config.transport.buffer_size, 131072);
663 assert!(config.network.listen_address.contains(':'));
665 }
666
667 #[test]
668 fn test_config_validation() {
669 let mut config = Config::default();
670 assert!(config.validate().is_ok());
671
672 config.network.listen_address = "invalid".to_string();
674 assert!(config.validate().is_err());
675
676 config.network.listen_address = "/ip4/127.0.0.1/tcp/9000".to_string();
678 assert!(config.validate().is_ok());
679 }
680
681 #[test]
682 fn test_save_and_load_config() {
683 let config = Config::development();
684 let file = NamedTempFile::new().unwrap();
685
686 config.save_to_file(file.path()).unwrap();
687
688 let loaded = Config::load_from_file(file.path()).unwrap();
689 assert_eq!(loaded.network.listen_address, config.network.listen_address);
690 }
691
692 #[test]
693 #[serial_test::serial]
694 #[allow(unsafe_code)] fn test_env_overrides() {
696 use std::sync::Mutex;
697
698 static ENV_MUTEX: Mutex<()> = Mutex::new(());
700 let _guard = ENV_MUTEX.lock().unwrap();
701
702 let orig_listen = env::var("SAORSA_LISTEN_ADDRESS").ok();
704 let orig_rate = env::var("SAORSA_RATE_LIMIT").ok();
705
706 unsafe {
708 env::set_var("SAORSA_LISTEN_ADDRESS", "127.0.0.1:8000");
709 env::set_var("SAORSA_RATE_LIMIT", "5000");
710 }
711
712 let config = Config::load().unwrap();
713 assert_eq!(config.network.listen_address, "127.0.0.1:8000");
714 assert_eq!(config.security.rate_limit, 5000);
715
716 unsafe {
718 match orig_listen {
719 Some(val) => env::set_var("SAORSA_LISTEN_ADDRESS", val),
720 None => env::remove_var("SAORSA_LISTEN_ADDRESS"),
721 }
722 match orig_rate {
723 Some(val) => env::set_var("SAORSA_RATE_LIMIT", val),
724 None => env::remove_var("SAORSA_RATE_LIMIT"),
725 }
726 }
727 }
728
729 #[test]
730 fn test_size_validation() {
731 let config = Config::default();
732 assert!(config.validate_size_format("10GB"));
733 assert!(config.validate_size_format("500MB"));
734 assert!(config.validate_size_format("1.5TB"));
735 assert!(!config.validate_size_format("10XB"));
736 assert!(!config.validate_size_format("invalid"));
737 }
738
739 #[test]
740 fn test_size_parsing() {
741 assert_eq!(Config::parse_size("10B").unwrap(), 10);
742 assert_eq!(Config::parse_size("1KB").unwrap(), 1024);
743 assert_eq!(Config::parse_size("5MB").unwrap(), 5 * 1024 * 1024);
744 assert_eq!(Config::parse_size("1GB").unwrap(), 1024 * 1024 * 1024);
745 assert_eq!(Config::parse_size("1.5GB").unwrap(), 1610612736);
746 assert_eq!(Config::parse_size("1TB").unwrap(), 1024u64.pow(4));
747
748 assert!(Config::parse_size("invalid").is_err());
750 assert!(Config::parse_size("10XB").is_err());
751 assert!(Config::parse_size("GB").is_err());
752 }
753}