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}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(default)]
64pub struct NetworkConfig {
65 pub bootstrap_nodes: Vec<String>,
67 pub listen_address: String,
69 pub public_address: Option<String>,
71 pub ipv6_enabled: bool,
73 pub max_connections: usize,
75 pub connection_timeout: u64,
77 pub keepalive_interval: u64,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(default)]
84pub struct SecurityConfig {
85 pub rate_limit: u32,
87 pub connection_limit: u32,
89 pub encryption_enabled: bool,
91 pub min_tls_version: String,
93 pub identity_security_level: String,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(default)]
100pub struct StorageConfig {
101 pub path: PathBuf,
103 pub max_size: String,
105 pub cache_size: u64,
107 pub compression_enabled: bool,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(default)]
114pub struct DhtConfig {
115 pub replication_factor: u8,
117 pub alpha: u8,
119 pub beta: u8,
121 pub record_ttl: u64,
123 pub adaptive_routing: bool,
125
126 pub trust_selection_enabled: bool,
130
131 pub trust_weight: f64,
135
136 pub min_trust_threshold: f64,
140
141 pub exclude_untrusted_for_storage: bool,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(default)]
150pub struct TransportConfig {
151 pub protocol: String,
153 pub quic_enabled: bool,
155 pub tcp_enabled: bool,
157 pub webrtc_enabled: bool,
159 pub buffer_size: usize,
161 pub server_name: String,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(default)]
168pub struct IdentityConfig {
169 pub derivation_path: String,
171 pub rotation_interval: u32,
173 pub backup_enabled: bool,
175 pub backup_interval: u32,
177}
178
179impl Default for NetworkConfig {
182 fn default() -> Self {
183 Self {
184 bootstrap_nodes: vec![],
185 listen_address: "0.0.0.0:9000".to_string(),
187 public_address: None,
188 ipv6_enabled: true,
189 max_connections: 10000,
190 connection_timeout: 30,
191 keepalive_interval: 60,
192 }
193 }
194}
195
196impl Default for SecurityConfig {
197 fn default() -> Self {
198 Self {
199 rate_limit: 1000,
200 connection_limit: 100,
201 encryption_enabled: true,
202 min_tls_version: "1.3".to_string(),
203 identity_security_level: "High".to_string(),
204 }
205 }
206}
207
208impl Default for StorageConfig {
209 fn default() -> Self {
210 Self {
211 path: PathBuf::from("./data"),
212 max_size: "10GB".to_string(),
213 cache_size: 256,
214 compression_enabled: true,
215 }
216 }
217}
218
219impl Default for DhtConfig {
220 fn default() -> Self {
221 Self {
222 replication_factor: 8,
223 alpha: 3,
224 beta: 1,
225 record_ttl: 3600,
226 adaptive_routing: true,
227 trust_selection_enabled: true,
229 trust_weight: 0.3,
230 min_trust_threshold: 0.1,
231 exclude_untrusted_for_storage: false,
232 }
233 }
234}
235
236impl Default for TransportConfig {
237 fn default() -> Self {
238 Self {
239 protocol: "quic".to_string(),
240 quic_enabled: true,
241 tcp_enabled: true,
242 webrtc_enabled: false,
243 buffer_size: 65536,
244 server_name: "p2p.local".to_string(),
245 }
246 }
247}
248
249impl Default for IdentityConfig {
250 fn default() -> Self {
251 Self {
252 derivation_path: "m/44'/0'/0'/0/0".to_string(),
253 rotation_interval: 90,
254 backup_enabled: true,
255 backup_interval: 24,
256 }
257 }
258}
259
260impl Config {
261 pub fn load() -> Result<Self> {
280 Self::load_with_path::<&str>(None)
281 }
282
283 pub fn load_with_path<P: AsRef<Path>>(path: Option<P>) -> Result<Self> {
299 let mut config = Self::default();
301
302 if let Some(path) = path {
304 config = Self::load_from_file(path)?;
305 } else {
306 for location in &["saorsa.toml", "config.toml", "/etc/saorsa/config.toml"] {
308 if Path::new(location).exists() {
309 info!("Loading config from: {}", location);
310 config = Self::load_from_file(location)?;
311 break;
312 }
313 }
314 }
315
316 config.apply_env_overrides()?;
318
319 config.validate()?;
321
322 Ok(config)
323 }
324
325 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
327 let content = fs::read_to_string(&path).map_err(|e| {
328 P2PError::Config(ConfigError::IoError {
329 path: path.as_ref().to_string_lossy().to_string().into(),
330 source: e,
331 })
332 })?;
333
334 toml::from_str(&content)
335 .map_err(|e| P2PError::Config(ConfigError::ParseError(e.to_string().into())))
336 }
337
338 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
340 let content = toml::to_string_pretty(self)
341 .map_err(|e| P2PError::Config(ConfigError::ParseError(e.to_string().into())))?;
342
343 fs::write(&path, content).map_err(|e| {
344 P2PError::Config(ConfigError::IoError {
345 path: path.as_ref().to_string_lossy().to_string().into(),
346 source: e,
347 })
348 })?;
349
350 Ok(())
351 }
352
353 fn apply_env_overrides(&mut self) -> Result<()> {
355 if let Ok(val) = env::var("SAORSA_LISTEN_ADDRESS") {
357 self.network.listen_address = val;
358 }
359 if let Ok(val) = env::var("SAORSA_PUBLIC_ADDRESS") {
360 self.network.public_address = Some(val);
361 }
362 if let Ok(val) = env::var("SAORSA_BOOTSTRAP_NODES") {
363 self.network.bootstrap_nodes = val.split(',').map(String::from).collect();
364 }
365 if let Ok(val) = env::var("SAORSA_MAX_CONNECTIONS") {
366 self.network.max_connections = val.parse().map_err(|_| {
367 P2PError::Config(ConfigError::InvalidValue {
368 field: "max_connections".to_string().into(),
369 reason: "Invalid value".to_string().into(),
370 })
371 })?;
372 }
373
374 if let Ok(val) = env::var("SAORSA_RATE_LIMIT") {
376 self.security.rate_limit = val.parse().map_err(|_| {
377 P2PError::Config(ConfigError::InvalidValue {
378 field: "rate_limit".to_string().into(),
379 reason: "Invalid value".to_string().into(),
380 })
381 })?;
382 }
383 if let Ok(val) = env::var("SAORSA_ENCRYPTION_ENABLED") {
384 self.security.encryption_enabled = val.parse().map_err(|_| {
385 P2PError::Config(ConfigError::InvalidValue {
386 field: "encryption_enabled".to_string().into(),
387 reason: "Invalid value".to_string().into(),
388 })
389 })?;
390 }
391
392 if let Ok(val) = env::var("SAORSA_DATA_PATH") {
394 self.storage.path = PathBuf::from(val);
395 }
396 if let Ok(val) = env::var("SAORSA_MAX_STORAGE") {
397 self.storage.max_size = val;
398 }
399
400 Ok(())
401 }
402
403 pub fn validate(&self) -> Result<()> {
405 let mut errors = Vec::new();
406
407 if let Err(e) = self.validate_address(&self.network.listen_address, "listen_address") {
409 errors.push(e);
410 }
411
412 if let Some(addr) = &self.network.public_address
413 && let Err(e) = self.validate_address(addr, "public_address")
414 {
415 errors.push(e);
416 }
417
418 for (i, node) in self.network.bootstrap_nodes.iter().enumerate() {
419 if let Err(e) = self.validate_address(node, &format!("bootstrap_node[{}]", i)) {
420 errors.push(e);
421 }
422 }
423
424 if let Err(e) = validate_config_value(
426 &self.network.max_connections.to_string(),
427 Some(1_usize),
428 Some(100_000_usize),
429 ) {
430 errors.push(P2PError::Config(ConfigError::InvalidValue {
431 field: "max_connections".to_string().into(),
432 reason: e.to_string().into(),
433 }));
434 }
435
436 if let Err(e) = validate_config_value(
437 &self.security.rate_limit.to_string(),
438 Some(1_u32),
439 Some(1_000_000_u32),
440 ) {
441 errors.push(P2PError::Config(ConfigError::InvalidValue {
442 field: "rate_limit".to_string().into(),
443 reason: e.to_string().into(),
444 }));
445 }
446
447 if self.storage.path.exists()
449 && let Err(e) = validate_file_path(&self.storage.path)
450 {
451 errors.push(P2PError::Config(ConfigError::InvalidValue {
452 field: "storage.path".to_string().into(),
453 reason: format!("{:?}: {}", self.storage.path, e).into(),
454 }));
455 }
456
457 if !self.validate_size_format(&self.storage.max_size) {
459 errors.push(P2PError::Config(ConfigError::InvalidValue {
460 field: "max_size".to_string().into(),
461 reason: format!("Invalid storage size format: {}", self.storage.max_size).into(),
462 }));
463 }
464
465 match self.transport.protocol.as_str() {
467 "quic" | "tcp" | "webrtc" => {}
468 _ => errors.push(P2PError::Config(ConfigError::InvalidValue {
469 field: "protocol".to_string().into(),
470 reason: format!("Invalid transport protocol: {}", self.transport.protocol).into(),
471 })),
472 }
473
474 if errors.is_empty() {
475 Ok(())
476 } else {
477 Err(errors.into_iter().next().unwrap_or_else(|| {
479 P2PError::Config(ConfigError::InvalidValue {
480 field: "unknown".to_string().into(),
481 reason: "Validation failed with unknown error".to_string().into(),
482 })
483 }))
484 }
485 }
486
487 fn validate_address(&self, addr: &str, field: &str) -> Result<()> {
489 if let Ok(socket_addr) = SocketAddr::from_str(addr) {
491 let ctx = ValidationContext::default()
493 .allow_localhost() .allow_private_ips(); return validate_network_address(&socket_addr, &ctx).map_err(|e| {
497 P2PError::Config(ConfigError::InvalidValue {
498 field: field.to_string().into(),
499 reason: e.to_string().into(),
500 })
501 });
502 }
503
504 if let Ok(network_addr) = crate::NetworkAddress::from_four_words(addr) {
506 let ctx = ValidationContext::default()
508 .allow_localhost()
509 .allow_private_ips();
510
511 return validate_network_address(&network_addr.socket_addr(), &ctx).map_err(|e| {
512 P2PError::Config(ConfigError::InvalidValue {
513 field: field.to_string().into(),
514 reason: e.to_string().into(),
515 })
516 });
517 }
518
519 if addr.starts_with("/ip4/") || addr.starts_with("/ip6/") {
521 return Ok(());
523 }
524
525 Err(P2PError::Config(ConfigError::InvalidValue {
526 field: field.to_string().into(),
527 reason: format!("Invalid address format: {}", addr).into(),
528 }))
529 }
530
531 fn validate_size_format(&self, size: &str) -> bool {
533 thread_local! {
534 static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^\d+(?:\.\d+)?\s*(?:B|KB|MB|GB|TB)$")
536 .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
537 }
538 SIZE_REGEX.with(|re| re.as_ref().ok().map(|r| r.is_match(size)).unwrap_or(false))
539 }
540
541 pub fn development() -> Self {
543 let mut config = Self::default();
544 config.network.listen_address = "127.0.0.1:9000".to_string();
545 config.security.rate_limit = 10000;
546 config.security.connection_limit = 1000;
547 config.storage.path = PathBuf::from("./dev-data");
548 config
549 }
550
551 pub fn production() -> Self {
553 let mut config = Self::default();
554 config.network.listen_address =
556 env::var("SAORSA_LISTEN_ADDRESS").unwrap_or_else(|_| "0.0.0.0:9000".to_string());
557 config.security.rate_limit = 1000;
558 config.security.connection_limit = 100;
559 config.storage.path = PathBuf::from("/var/lib/saorsa");
560 config.transport.buffer_size = 131072;
562 config
563 }
564
565 pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
567 SocketAddr::from_str(&self.network.listen_address).map_err(|e| {
568 P2PError::Config(ConfigError::InvalidValue {
569 field: "listen_address".to_string().into(),
570 reason: format!("Invalid address: {}", e).into(),
571 })
572 })
573 }
574
575 pub fn bootstrap_addrs(&self) -> Result<Vec<NetworkAddress>> {
577 self.network
578 .bootstrap_nodes
579 .iter()
580 .map(|addr| {
581 addr.parse::<NetworkAddress>().map_err(|e| {
582 P2PError::Config(ConfigError::InvalidValue {
583 field: "bootstrap_nodes".to_string().into(),
584 reason: format!("Invalid address: {}", e).into(),
585 })
586 })
587 })
588 .collect()
589 }
590
591 pub fn parse_size(size: &str) -> Result<u64> {
603 thread_local! {
604 static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$")
605 .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
606 }
607
608 SIZE_REGEX.with(|re| -> Result<u64> {
609 let re = match re {
610 Ok(r) => r,
611 Err(e) => {
612 return Err(P2PError::Config(ConfigError::InvalidValue {
613 field: "size".to_string().into(),
614 reason: e.to_string().into(),
615 }));
616 }
617 };
618 if let Some(captures) = re.captures(size) {
619 let value: f64 = captures
620 .get(1)
621 .and_then(|m| m.as_str().parse().ok())
622 .ok_or_else(|| {
623 P2PError::Config(ConfigError::InvalidValue {
624 field: "size".to_string().into(),
625 reason: "Invalid numeric value".to_string().into(),
626 })
627 })?;
628
629 let unit = captures.get(2).map(|m| m.as_str()).unwrap_or("B");
630 let multiplier = match unit {
631 "B" => 1u64,
632 "KB" => 1024,
633 "MB" => 1024 * 1024,
634 "GB" => 1024 * 1024 * 1024,
635 "TB" => 1024u64.pow(4),
636 _ => {
637 return Err(P2PError::Config(ConfigError::InvalidValue {
638 field: "size".to_string().into(),
639 reason: format!("Unknown unit: {}", unit).into(),
640 }));
641 }
642 };
643
644 Ok((value * multiplier as f64) as u64)
645 } else {
646 Err(P2PError::Config(ConfigError::InvalidValue {
647 field: "size".to_string().into(),
648 reason: format!("Invalid size format: {}", size).into(),
649 }))
650 }
651 })
652 }
653
654 pub fn storage_max_size_bytes(&self) -> Result<u64> {
656 Self::parse_size(&self.storage.max_size)
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use super::*;
663 use tempfile::NamedTempFile;
664
665 #[test]
666 fn test_default_config() {
667 let config = Config::default();
668 assert_eq!(config.network.listen_address, "0.0.0.0:9000");
669 assert_eq!(config.security.rate_limit, 1000);
670 assert!(config.security.encryption_enabled);
671 }
672
673 #[test]
674 fn test_development_config() {
675 let config = Config::development();
676 assert_eq!(config.network.listen_address, "127.0.0.1:9000");
677 assert_eq!(config.security.rate_limit, 10000);
678 }
679
680 #[test]
681 fn test_production_config() {
682 let config = Config::production();
683 assert_eq!(config.transport.buffer_size, 131072);
685 assert!(config.network.listen_address.contains(':'));
687 }
688
689 #[test]
690 fn test_config_validation() {
691 let mut config = Config::default();
692 assert!(config.validate().is_ok());
693
694 config.network.listen_address = "invalid".to_string();
696 assert!(config.validate().is_err());
697
698 config.network.listen_address = "/ip4/127.0.0.1/tcp/9000".to_string();
700 assert!(config.validate().is_ok());
701 }
702
703 #[test]
704 fn test_save_and_load_config() {
705 let config = Config::development();
706 let file = NamedTempFile::new().unwrap();
707
708 config.save_to_file(file.path()).unwrap();
709
710 let loaded = Config::load_from_file(file.path()).unwrap();
711 assert_eq!(loaded.network.listen_address, config.network.listen_address);
712 }
713
714 #[test]
715 #[serial_test::serial]
716 #[allow(unsafe_code)] fn test_env_overrides() {
718 use std::sync::Mutex;
719
720 static ENV_MUTEX: Mutex<()> = Mutex::new(());
722 let _guard = ENV_MUTEX.lock().unwrap();
723
724 let orig_listen = env::var("SAORSA_LISTEN_ADDRESS").ok();
726 let orig_rate = env::var("SAORSA_RATE_LIMIT").ok();
727
728 unsafe {
730 env::set_var("SAORSA_LISTEN_ADDRESS", "127.0.0.1:8000");
731 env::set_var("SAORSA_RATE_LIMIT", "5000");
732 }
733
734 let config = Config::load().unwrap();
735 assert_eq!(config.network.listen_address, "127.0.0.1:8000");
736 assert_eq!(config.security.rate_limit, 5000);
737
738 unsafe {
740 match orig_listen {
741 Some(val) => env::set_var("SAORSA_LISTEN_ADDRESS", val),
742 None => env::remove_var("SAORSA_LISTEN_ADDRESS"),
743 }
744 match orig_rate {
745 Some(val) => env::set_var("SAORSA_RATE_LIMIT", val),
746 None => env::remove_var("SAORSA_RATE_LIMIT"),
747 }
748 }
749 }
750
751 #[test]
752 fn test_size_validation() {
753 let config = Config::default();
754 assert!(config.validate_size_format("10GB"));
755 assert!(config.validate_size_format("500MB"));
756 assert!(config.validate_size_format("1.5TB"));
757 assert!(!config.validate_size_format("10XB"));
758 assert!(!config.validate_size_format("invalid"));
759 }
760
761 #[test]
762 fn test_size_parsing() {
763 assert_eq!(Config::parse_size("10B").unwrap(), 10);
764 assert_eq!(Config::parse_size("1KB").unwrap(), 1024);
765 assert_eq!(Config::parse_size("5MB").unwrap(), 5 * 1024 * 1024);
766 assert_eq!(Config::parse_size("1GB").unwrap(), 1024 * 1024 * 1024);
767 assert_eq!(Config::parse_size("1.5GB").unwrap(), 1610612736);
768 assert_eq!(Config::parse_size("1TB").unwrap(), 1024u64.pow(4));
769
770 assert!(Config::parse_size("invalid").is_err());
772 assert!(Config::parse_size("10XB").is_err());
773 assert!(Config::parse_size("GB").is_err());
774 }
775}