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
127#[derive(Debug, Clone, Serialize, Deserialize)]
129#[serde(default)]
130pub struct TransportConfig {
131 pub protocol: String,
133 pub quic_enabled: bool,
135 pub tcp_enabled: bool,
137 pub webrtc_enabled: bool,
139 pub buffer_size: usize,
141 pub server_name: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(default)]
148pub struct IdentityConfig {
149 pub derivation_path: String,
151 pub rotation_interval: u32,
153 pub backup_enabled: bool,
155 pub backup_interval: u32,
157}
158
159impl Default for NetworkConfig {
162 fn default() -> Self {
163 Self {
164 bootstrap_nodes: vec![],
165 listen_address: "0.0.0.0:9000".to_string(),
167 public_address: None,
168 ipv6_enabled: true,
169 max_connections: 10000,
170 connection_timeout: 30,
171 keepalive_interval: 60,
172 }
173 }
174}
175
176impl Default for SecurityConfig {
177 fn default() -> Self {
178 Self {
179 rate_limit: 1000,
180 connection_limit: 100,
181 encryption_enabled: true,
182 min_tls_version: "1.3".to_string(),
183 identity_security_level: "High".to_string(),
184 }
185 }
186}
187
188impl Default for StorageConfig {
189 fn default() -> Self {
190 Self {
191 path: PathBuf::from("./data"),
192 max_size: "10GB".to_string(),
193 cache_size: 256,
194 compression_enabled: true,
195 }
196 }
197}
198
199impl Default for DhtConfig {
200 fn default() -> Self {
201 Self {
202 replication_factor: 8,
203 alpha: 3,
204 beta: 1,
205 record_ttl: 3600,
206 adaptive_routing: true,
207 }
208 }
209}
210
211impl Default for TransportConfig {
212 fn default() -> Self {
213 Self {
214 protocol: "quic".to_string(),
215 quic_enabled: true,
216 tcp_enabled: true,
217 webrtc_enabled: false,
218 buffer_size: 65536,
219 server_name: "p2p.local".to_string(),
220 }
221 }
222}
223
224impl Default for IdentityConfig {
225 fn default() -> Self {
226 Self {
227 derivation_path: "m/44'/0'/0'/0/0".to_string(),
228 rotation_interval: 90,
229 backup_enabled: true,
230 backup_interval: 24,
231 }
232 }
233}
234
235impl Config {
236 pub fn load() -> Result<Self> {
255 Self::load_with_path::<&str>(None)
256 }
257
258 pub fn load_with_path<P: AsRef<Path>>(path: Option<P>) -> Result<Self> {
274 let mut config = Self::default();
276
277 if let Some(path) = path {
279 config = Self::load_from_file(path)?;
280 } else {
281 for location in &["saorsa.toml", "config.toml", "/etc/saorsa/config.toml"] {
283 if Path::new(location).exists() {
284 info!("Loading config from: {}", location);
285 config = Self::load_from_file(location)?;
286 break;
287 }
288 }
289 }
290
291 config.apply_env_overrides()?;
293
294 config.validate()?;
296
297 Ok(config)
298 }
299
300 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
302 let content = fs::read_to_string(&path).map_err(|e| {
303 P2PError::Config(ConfigError::IoError {
304 path: path.as_ref().to_string_lossy().to_string().into(),
305 source: e,
306 })
307 })?;
308
309 toml::from_str(&content)
310 .map_err(|e| P2PError::Config(ConfigError::ParseError(e.to_string().into())))
311 }
312
313 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
315 let content = toml::to_string_pretty(self)
316 .map_err(|e| P2PError::Config(ConfigError::ParseError(e.to_string().into())))?;
317
318 fs::write(&path, content).map_err(|e| {
319 P2PError::Config(ConfigError::IoError {
320 path: path.as_ref().to_string_lossy().to_string().into(),
321 source: e,
322 })
323 })?;
324
325 Ok(())
326 }
327
328 fn apply_env_overrides(&mut self) -> Result<()> {
330 if let Ok(val) = env::var("SAORSA_LISTEN_ADDRESS") {
332 self.network.listen_address = val;
333 }
334 if let Ok(val) = env::var("SAORSA_PUBLIC_ADDRESS") {
335 self.network.public_address = Some(val);
336 }
337 if let Ok(val) = env::var("SAORSA_BOOTSTRAP_NODES") {
338 self.network.bootstrap_nodes = val.split(',').map(String::from).collect();
339 }
340 if let Ok(val) = env::var("SAORSA_MAX_CONNECTIONS") {
341 self.network.max_connections = val.parse().map_err(|_| {
342 P2PError::Config(ConfigError::InvalidValue {
343 field: "max_connections".to_string().into(),
344 reason: "Invalid value".to_string().into(),
345 })
346 })?;
347 }
348
349 if let Ok(val) = env::var("SAORSA_RATE_LIMIT") {
351 self.security.rate_limit = val.parse().map_err(|_| {
352 P2PError::Config(ConfigError::InvalidValue {
353 field: "rate_limit".to_string().into(),
354 reason: "Invalid value".to_string().into(),
355 })
356 })?;
357 }
358 if let Ok(val) = env::var("SAORSA_ENCRYPTION_ENABLED") {
359 self.security.encryption_enabled = val.parse().map_err(|_| {
360 P2PError::Config(ConfigError::InvalidValue {
361 field: "encryption_enabled".to_string().into(),
362 reason: "Invalid value".to_string().into(),
363 })
364 })?;
365 }
366
367 if let Ok(val) = env::var("SAORSA_DATA_PATH") {
369 self.storage.path = PathBuf::from(val);
370 }
371 if let Ok(val) = env::var("SAORSA_MAX_STORAGE") {
372 self.storage.max_size = val;
373 }
374
375 Ok(())
376 }
377
378 pub fn validate(&self) -> Result<()> {
380 let mut errors = Vec::new();
381
382 if let Err(e) = self.validate_address(&self.network.listen_address, "listen_address") {
384 errors.push(e);
385 }
386
387 if let Some(addr) = &self.network.public_address
388 && let Err(e) = self.validate_address(addr, "public_address")
389 {
390 errors.push(e);
391 }
392
393 for (i, node) in self.network.bootstrap_nodes.iter().enumerate() {
394 if let Err(e) = self.validate_address(node, &format!("bootstrap_node[{}]", i)) {
395 errors.push(e);
396 }
397 }
398
399 if let Err(e) = validate_config_value(
401 &self.network.max_connections.to_string(),
402 Some(1_usize),
403 Some(100_000_usize),
404 ) {
405 errors.push(P2PError::Config(ConfigError::InvalidValue {
406 field: "max_connections".to_string().into(),
407 reason: e.to_string().into(),
408 }));
409 }
410
411 if let Err(e) = validate_config_value(
412 &self.security.rate_limit.to_string(),
413 Some(1_u32),
414 Some(1_000_000_u32),
415 ) {
416 errors.push(P2PError::Config(ConfigError::InvalidValue {
417 field: "rate_limit".to_string().into(),
418 reason: e.to_string().into(),
419 }));
420 }
421
422 if self.storage.path.exists()
424 && let Err(e) = validate_file_path(&self.storage.path)
425 {
426 errors.push(P2PError::Config(ConfigError::InvalidValue {
427 field: "storage.path".to_string().into(),
428 reason: format!("{:?}: {}", self.storage.path, e).into(),
429 }));
430 }
431
432 if !self.validate_size_format(&self.storage.max_size) {
434 errors.push(P2PError::Config(ConfigError::InvalidValue {
435 field: "max_size".to_string().into(),
436 reason: format!("Invalid storage size format: {}", self.storage.max_size).into(),
437 }));
438 }
439
440 match self.transport.protocol.as_str() {
442 "quic" | "tcp" | "webrtc" => {}
443 _ => errors.push(P2PError::Config(ConfigError::InvalidValue {
444 field: "protocol".to_string().into(),
445 reason: format!("Invalid transport protocol: {}", self.transport.protocol).into(),
446 })),
447 }
448
449 if errors.is_empty() {
450 Ok(())
451 } else {
452 Err(errors.into_iter().next().unwrap_or_else(|| {
454 P2PError::Config(ConfigError::InvalidValue {
455 field: "unknown".to_string().into(),
456 reason: "Validation failed with unknown error".to_string().into(),
457 })
458 }))
459 }
460 }
461
462 fn validate_address(&self, addr: &str, field: &str) -> Result<()> {
464 if let Ok(socket_addr) = SocketAddr::from_str(addr) {
466 let ctx = ValidationContext::default()
468 .allow_localhost() .allow_private_ips(); return validate_network_address(&socket_addr, &ctx).map_err(|e| {
472 P2PError::Config(ConfigError::InvalidValue {
473 field: field.to_string().into(),
474 reason: e.to_string().into(),
475 })
476 });
477 }
478
479 if let Ok(network_addr) = crate::NetworkAddress::from_four_words(addr) {
481 let ctx = ValidationContext::default()
483 .allow_localhost()
484 .allow_private_ips();
485
486 return validate_network_address(&network_addr.socket_addr(), &ctx).map_err(|e| {
487 P2PError::Config(ConfigError::InvalidValue {
488 field: field.to_string().into(),
489 reason: e.to_string().into(),
490 })
491 });
492 }
493
494 if addr.starts_with("/ip4/") || addr.starts_with("/ip6/") {
496 return Ok(());
498 }
499
500 Err(P2PError::Config(ConfigError::InvalidValue {
501 field: field.to_string().into(),
502 reason: format!("Invalid address format: {}", addr).into(),
503 }))
504 }
505
506 fn validate_size_format(&self, size: &str) -> bool {
508 thread_local! {
509 static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^\d+(?:\.\d+)?\s*(?:B|KB|MB|GB|TB)$")
511 .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
512 }
513 SIZE_REGEX.with(|re| re.as_ref().ok().map(|r| r.is_match(size)).unwrap_or(false))
514 }
515
516 pub fn development() -> Self {
518 let mut config = Self::default();
519 config.network.listen_address = "127.0.0.1:9000".to_string();
520 config.security.rate_limit = 10000;
521 config.security.connection_limit = 1000;
522 config.storage.path = PathBuf::from("./dev-data");
523 config
524 }
525
526 pub fn production() -> Self {
528 let mut config = Self::default();
529 config.network.listen_address =
531 env::var("SAORSA_LISTEN_ADDRESS").unwrap_or_else(|_| "0.0.0.0:9000".to_string());
532 config.security.rate_limit = 1000;
533 config.security.connection_limit = 100;
534 config.storage.path = PathBuf::from("/var/lib/saorsa");
535 config.transport.buffer_size = 131072;
537 config
538 }
539
540 pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
542 SocketAddr::from_str(&self.network.listen_address).map_err(|e| {
543 P2PError::Config(ConfigError::InvalidValue {
544 field: "listen_address".to_string().into(),
545 reason: format!("Invalid address: {}", e).into(),
546 })
547 })
548 }
549
550 pub fn bootstrap_addrs(&self) -> Result<Vec<NetworkAddress>> {
552 self.network
553 .bootstrap_nodes
554 .iter()
555 .map(|addr| {
556 addr.parse::<NetworkAddress>().map_err(|e| {
557 P2PError::Config(ConfigError::InvalidValue {
558 field: "bootstrap_nodes".to_string().into(),
559 reason: format!("Invalid address: {}", e).into(),
560 })
561 })
562 })
563 .collect()
564 }
565
566 pub fn parse_size(size: &str) -> Result<u64> {
578 thread_local! {
579 static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$")
580 .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
581 }
582
583 SIZE_REGEX.with(|re| -> Result<u64> {
584 let re = match re {
585 Ok(r) => r,
586 Err(e) => {
587 return Err(P2PError::Config(ConfigError::InvalidValue {
588 field: "size".to_string().into(),
589 reason: e.to_string().into(),
590 }));
591 }
592 };
593 if let Some(captures) = re.captures(size) {
594 let value: f64 = captures
595 .get(1)
596 .and_then(|m| m.as_str().parse().ok())
597 .ok_or_else(|| {
598 P2PError::Config(ConfigError::InvalidValue {
599 field: "size".to_string().into(),
600 reason: "Invalid numeric value".to_string().into(),
601 })
602 })?;
603
604 let unit = captures.get(2).map(|m| m.as_str()).unwrap_or("B");
605 let multiplier = match unit {
606 "B" => 1u64,
607 "KB" => 1024,
608 "MB" => 1024 * 1024,
609 "GB" => 1024 * 1024 * 1024,
610 "TB" => 1024u64.pow(4),
611 _ => {
612 return Err(P2PError::Config(ConfigError::InvalidValue {
613 field: "size".to_string().into(),
614 reason: format!("Unknown unit: {}", unit).into(),
615 }));
616 }
617 };
618
619 Ok((value * multiplier as f64) as u64)
620 } else {
621 Err(P2PError::Config(ConfigError::InvalidValue {
622 field: "size".to_string().into(),
623 reason: format!("Invalid size format: {}", size).into(),
624 }))
625 }
626 })
627 }
628
629 pub fn storage_max_size_bytes(&self) -> Result<u64> {
631 Self::parse_size(&self.storage.max_size)
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638 use tempfile::NamedTempFile;
639
640 #[test]
641 fn test_default_config() {
642 let config = Config::default();
643 assert_eq!(config.network.listen_address, "0.0.0.0:9000");
644 assert_eq!(config.security.rate_limit, 1000);
645 assert!(config.security.encryption_enabled);
646 }
647
648 #[test]
649 fn test_development_config() {
650 let config = Config::development();
651 assert_eq!(config.network.listen_address, "127.0.0.1:9000");
652 assert_eq!(config.security.rate_limit, 10000);
653 }
654
655 #[test]
656 fn test_production_config() {
657 let config = Config::production();
658 assert_eq!(config.transport.buffer_size, 131072);
660 assert!(config.network.listen_address.contains(':'));
662 }
663
664 #[test]
665 fn test_config_validation() {
666 let mut config = Config::default();
667 assert!(config.validate().is_ok());
668
669 config.network.listen_address = "invalid".to_string();
671 assert!(config.validate().is_err());
672
673 config.network.listen_address = "/ip4/127.0.0.1/tcp/9000".to_string();
675 assert!(config.validate().is_ok());
676 }
677
678 #[test]
679 fn test_save_and_load_config() {
680 let config = Config::development();
681 let file = NamedTempFile::new().unwrap();
682
683 config.save_to_file(file.path()).unwrap();
684
685 let loaded = Config::load_from_file(file.path()).unwrap();
686 assert_eq!(loaded.network.listen_address, config.network.listen_address);
687 }
688
689 #[test]
690 #[serial_test::serial]
691 #[allow(unsafe_code)] fn test_env_overrides() {
693 use std::sync::Mutex;
694
695 static ENV_MUTEX: Mutex<()> = Mutex::new(());
697 let _guard = ENV_MUTEX.lock().unwrap();
698
699 let orig_listen = env::var("SAORSA_LISTEN_ADDRESS").ok();
701 let orig_rate = env::var("SAORSA_RATE_LIMIT").ok();
702
703 unsafe {
705 env::set_var("SAORSA_LISTEN_ADDRESS", "127.0.0.1:8000");
706 env::set_var("SAORSA_RATE_LIMIT", "5000");
707 }
708
709 let config = Config::load().unwrap();
710 assert_eq!(config.network.listen_address, "127.0.0.1:8000");
711 assert_eq!(config.security.rate_limit, 5000);
712
713 unsafe {
715 match orig_listen {
716 Some(val) => env::set_var("SAORSA_LISTEN_ADDRESS", val),
717 None => env::remove_var("SAORSA_LISTEN_ADDRESS"),
718 }
719 match orig_rate {
720 Some(val) => env::set_var("SAORSA_RATE_LIMIT", val),
721 None => env::remove_var("SAORSA_RATE_LIMIT"),
722 }
723 }
724 }
725
726 #[test]
727 fn test_size_validation() {
728 let config = Config::default();
729 assert!(config.validate_size_format("10GB"));
730 assert!(config.validate_size_format("500MB"));
731 assert!(config.validate_size_format("1.5TB"));
732 assert!(!config.validate_size_format("10XB"));
733 assert!(!config.validate_size_format("invalid"));
734 }
735
736 #[test]
737 fn test_size_parsing() {
738 assert_eq!(Config::parse_size("10B").unwrap(), 10);
739 assert_eq!(Config::parse_size("1KB").unwrap(), 1024);
740 assert_eq!(Config::parse_size("5MB").unwrap(), 5 * 1024 * 1024);
741 assert_eq!(Config::parse_size("1GB").unwrap(), 1024 * 1024 * 1024);
742 assert_eq!(Config::parse_size("1.5GB").unwrap(), 1610612736);
743 assert_eq!(Config::parse_size("1TB").unwrap(), 1024u64.pow(4));
744
745 assert!(Config::parse_size("invalid").is_err());
747 assert!(Config::parse_size("10XB").is_err());
748 assert!(Config::parse_size("GB").is_err());
749 }
750}