1use serde::Deserialize;
2use std::fs;
3use std::net::{IpAddr, SocketAddr};
4use std::path::Path;
5
6use crate::error::{AppError, ConfigError, Result};
7
8#[derive(Debug, Deserialize, Clone)]
10pub struct ConfigFile {
11 pub server: Option<ServerSection>,
12 #[serde(default)]
13 pub proxies: Vec<ProxySection>,
14 pub token: Option<String>,
15 pub totp_secret: Option<String>,
16 #[serde(default)]
17 pub reverse_proxies: Vec<ReverseProxySection>,
18
19 pub reverse_proxy_server: Option<ReverseProxyServerSection>,
21
22 pub reverse_proxy_client: Option<ReverseProxyClientSection>,
24}
25
26#[derive(Debug, Deserialize, Clone)]
28pub struct ServerSection {
29 pub bind_ip: Option<String>,
30 pub debug: Option<bool>,
31}
32
33impl ServerSection {
34 }
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum AuthMode {
41 None, Single, Multiple, }
45
46impl Default for ServerSection {
47 fn default() -> Self {
48 Self {
49 bind_ip: Some("127.0.0.1".to_string()),
50 debug: Some(false),
51 }
52 }
53}
54
55#[derive(Debug, Deserialize, Clone)]
57pub struct ProxySection {
58 pub local: String, pub remote: String, pub source_ip: Option<String>,
61 pub allow_ips: Option<Vec<String>>,
62 pub max_conns_per_ip: Option<usize>,
63 pub cps_limit: Option<f64>, }
65
66impl ProxySection {
67 pub fn get_local_port(&self) -> Result<u16> {
69 if self.local.contains(':') {
70 let parts: Vec<&str> = self.local.split(':').collect();
72 if parts.len() != 2 {
73 return Err(AppError::Config(ConfigError::InvalidConfigValue {
74 path: "local".to_string(),
75 reason: format!(
76 "Invalid local format '{}', expected 'IP:PORT' or 'PORT'",
77 self.local
78 ),
79 }));
80 }
81 parts[1].parse().map_err(|_| {
82 AppError::Config(ConfigError::InvalidConfigValue {
83 path: "local".to_string(),
84 reason: format!("Invalid port number in '{}'", self.local),
85 })
86 })
87 } else {
88 self.local.parse().map_err(|_| {
90 AppError::Config(ConfigError::InvalidConfigValue {
91 path: "local".to_string(),
92 reason: format!("Invalid port number '{}'", self.local),
93 })
94 })
95 }
96 }
97
98 pub fn get_local_ip(&self) -> Option<String> {
100 if self.local.contains(':') {
101 let parts: Vec<&str> = self.local.split(':').collect();
102 if parts.len() == 2 {
103 Some(parts[0].to_string())
104 } else {
105 None
106 }
107 } else {
108 None }
110 }
111
112 pub fn get_remote_host(&self) -> Result<String> {
114 if self.remote.contains(':') {
115 let parts: Vec<&str> = self.remote.split(':').collect();
117 if parts.len() < 2 {
118 return Err(AppError::Config(ConfigError::InvalidConfigValue {
119 path: "remote".to_string(),
120 reason: format!(
121 "Invalid remote format '{}', expected 'HOST:PORT' or 'PORT'",
122 self.remote
123 ),
124 }));
125 }
126 Ok(parts[0..parts.len() - 1].join(":")) } else {
128 Ok("localhost".to_string())
130 }
131 }
132
133 pub fn get_remote_port(&self) -> Result<u16> {
135 if self.remote.contains(':') {
136 let parts: Vec<&str> = self.remote.rsplit(':').collect();
138 if parts.len() < 2 {
139 return Err(AppError::Config(ConfigError::InvalidConfigValue {
140 path: "remote".to_string(),
141 reason: format!("Invalid remote format '{}'", self.remote),
142 }));
143 }
144 parts[0].parse().map_err(|_| {
145 AppError::Config(ConfigError::InvalidConfigValue {
146 path: "remote".to_string(),
147 reason: format!("Invalid port number in '{}'", self.remote),
148 })
149 })
150 } else {
151 self.remote.parse().map_err(|_| {
153 AppError::Config(ConfigError::InvalidConfigValue {
154 path: "remote".to_string(),
155 reason: format!("Invalid port number '{}'", self.remote),
156 })
157 })
158 }
159 }
160}
161
162#[derive(Debug, Deserialize, Clone)]
164pub struct ReverseProxySection {
165 pub server: String,
167 pub local: String,
169 pub source_ip: Option<String>,
170}
171
172impl ReverseProxySection {
173 pub fn get_server_port(&self) -> Result<u16> {
175 if self.server.contains(':') {
176 let parts: Vec<&str> = self.server.rsplit(':').collect();
178 if parts.len() < 2 {
179 return Err(AppError::Config(ConfigError::InvalidConfigValue {
180 path: "server".to_string(),
181 reason: format!("Invalid server format '{}'", self.server),
182 }));
183 }
184 parts[0].parse().map_err(|_| {
185 AppError::Config(ConfigError::InvalidConfigValue {
186 path: "server".to_string(),
187 reason: format!("Invalid port number in '{}'", self.server),
188 })
189 })
190 } else {
191 self.server.parse().map_err(|_| {
193 AppError::Config(ConfigError::InvalidConfigValue {
194 path: "server".to_string(),
195 reason: format!("Invalid port number '{}'", self.server),
196 })
197 })
198 }
199 }
200
201 pub fn get_server_ip(&self) -> Option<String> {
203 if self.server.contains(':') {
204 let parts: Vec<&str> = self.server.split(':').collect();
205 if parts.len() >= 2 {
206 Some(parts[0..parts.len() - 1].join(":")) } else {
208 None
209 }
210 } else {
211 None }
213 }
214
215 pub fn get_local_port(&self) -> Result<u16> {
217 if self.local.contains(':') {
218 let parts: Vec<&str> = self.local.rsplit(':').collect();
220 if parts.len() < 2 {
221 return Err(AppError::Config(ConfigError::InvalidConfigValue {
222 path: "local".to_string(),
223 reason: format!("Invalid local format '{}'", self.local),
224 }));
225 }
226 parts[0].parse().map_err(|_| {
227 AppError::Config(ConfigError::InvalidConfigValue {
228 path: "local".to_string(),
229 reason: format!("Invalid port number in '{}'", self.local),
230 })
231 })
232 } else {
233 self.local.parse().map_err(|_| {
235 AppError::Config(ConfigError::InvalidConfigValue {
236 path: "local".to_string(),
237 reason: format!("Invalid port number '{}'", self.local),
238 })
239 })
240 }
241 }
242
243 pub fn get_local_host(&self) -> Option<String> {
245 if self.local.contains(':') {
246 let parts: Vec<&str> = self.local.split(':').collect();
247 if parts.len() >= 2 {
248 Some(parts[0..parts.len() - 1].join(":")) } else {
250 Some("localhost".to_string())
251 }
252 } else {
253 Some("localhost".to_string()) }
255 }
256}
257
258#[derive(Debug, Deserialize, Clone)]
260pub struct ReverseProxyServerSection {
261 pub port: u16,
263 #[serde(default)]
264 pub allowed_tokens: Vec<String>, pub totp_secret: Option<String>, }
267
268impl Default for ReverseProxyServerSection {
269 fn default() -> Self {
270 Self {
271 port: 9001,
272 allowed_tokens: Vec::new(),
273 totp_secret: None,
274 }
275 }
276}
277
278#[derive(Debug, Deserialize, Clone)]
280pub struct ReverseProxyClientSection {
281 pub server: String,
283 pub token: Option<String>,
284 pub totp_secret: Option<String>,
285}
286
287impl ConfigFile {
288 pub fn get_runtime_mode(&self) -> String {
291 let has_forward = !self.proxies.is_empty();
292 let has_reverse_server = self.reverse_proxy_server.is_some();
293 let has_reverse_client = self.reverse_proxy_client.is_some();
294
295 if has_reverse_client {
297 "reverse_client".to_string()
298 } else if has_reverse_server {
299 "reverse_server".to_string()
300 } else if has_forward {
301 "forward".to_string()
302 } else {
303 "unknown".to_string()
304 }
305 }
306
307 pub fn validate(&mut self) -> std::result::Result<Vec<String>, ConfigError> {
309 use std::collections::HashSet;
310
311 let mut warnings = Vec::new();
312 let mut errors = Vec::new();
313
314 if !self.reverse_proxies.is_empty() {
316 let has_server = self.reverse_proxy_server.is_some();
317 let has_client = self.reverse_proxy_client.is_some();
318
319 if !has_server && !has_client {
320 errors.push("Reverse proxies configured but neither reverse_proxy_server nor reverse_proxy_client specified".to_string());
321 }
322 }
323
324 if let Some(server) = self.server.as_mut() {
325 if let Some(current) = server.bind_ip.clone() {
326 let trimmed = current.trim();
327 if trimmed.is_empty() {
328 warnings.push(
329 "server.bind_ip is empty, falling back to default 127.0.0.1".to_string(),
330 );
331 server.bind_ip = Some("127.0.0.1".to_string());
332 } else if trimmed.parse::<IpAddr>().is_err() {
333 errors.push(format!(
334 "server.bind_ip '{}' is not a valid IP address",
335 trimmed
336 ));
337 } else if trimmed != current {
338 server.bind_ip = Some(trimmed.to_string());
339 }
340 }
341 }
342
343 let mut local_ports = HashSet::new();
344
345 for (index, proxy) in self.proxies.iter_mut().enumerate() {
347 let prefix = format!("proxies[{}]", index);
348
349 let local_port = match proxy.get_local_port() {
351 Ok(port) => port,
352 Err(e) => {
353 errors.push(format!("{}.local: {}", prefix, e));
354 continue;
355 }
356 };
357
358 if local_port == 0 {
359 errors.push(format!("{}.local port must be between 1 and 65535", prefix));
360 }
361
362 let _remote_host = match proxy.get_remote_host() {
364 Ok(host) => host,
365 Err(e) => {
366 errors.push(format!("{}.remote: {}", prefix, e));
367 continue;
368 }
369 };
370
371 let remote_port = match proxy.get_remote_port() {
372 Ok(port) => port,
373 Err(e) => {
374 errors.push(format!("{}.remote: {}", prefix, e));
375 continue;
376 }
377 };
378
379 if remote_port == 0 {
380 errors.push(format!(
381 "{}.remote port must be between 1 and 65535",
382 prefix
383 ));
384 }
385
386 if !local_ports.insert(local_port) {
388 errors.push(format!(
389 "Duplicate local port {} detected in {}",
390 local_port, prefix
391 ));
392 }
393
394 if let Some(ref source_ip) = proxy.source_ip {
396 let trimmed = source_ip.trim();
397
398 if trimmed.is_empty() {
399 warnings.push(format!("{}.source_ip is empty and will be ignored", prefix));
400 proxy.source_ip = None;
401 } else if trimmed.eq_ignore_ascii_case("null") {
402 warnings.push(format!(
403 "{}.source_ip contains 'null' value; will be ignored",
404 prefix
405 ));
406 proxy.source_ip = None;
407 } else if trimmed.parse::<IpAddr>().is_err() {
408 errors.push(format!(
409 "{}.source_ip '{}' is not a valid IP address",
410 prefix, trimmed
411 ));
412 }
413 }
414 }
415
416 let mut server_ports = HashSet::new();
418
419 for (index, rproxy) in self.reverse_proxies.iter().enumerate() {
420 let prefix = format!("reverse_proxies[{}]", index);
421
422 let server_port = match rproxy.get_server_port() {
424 Ok(port) => port,
425 Err(e) => {
426 errors.push(format!("{}.server: {}", prefix, e));
427 continue;
428 }
429 };
430
431 if server_port == 0 {
432 errors.push(format!(
433 "{}.server port must be between 1 and 65535",
434 prefix
435 ));
436 }
437
438 if !server_ports.insert(server_port) {
440 errors.push(format!(
441 "Duplicate server port {} detected in {}",
442 server_port, prefix
443 ));
444 }
445
446 let local_port = match rproxy.get_local_port() {
448 Ok(port) => port,
449 Err(e) => {
450 errors.push(format!("{}.local: {}", prefix, e));
451 continue;
452 }
453 };
454
455 if local_port == 0 {
456 errors.push(format!("{}.local port must be between 1 and 65535", prefix));
457 }
458
459 if let Some(ref source_ip) = rproxy.source_ip {
461 let trimmed = source_ip.trim();
462
463 if trimmed.is_empty() {
464 warnings.push(format!("{}.source_ip is empty and will be ignored", prefix));
465 } else if trimmed.eq_ignore_ascii_case("null") {
466 warnings.push(format!(
467 "{}.source_ip contains 'null' value; will be ignored",
468 prefix
469 ));
470 } else if trimmed.parse::<IpAddr>().is_err() {
471 errors.push(format!(
472 "{}.source_ip '{}' is not a valid IP address",
473 prefix, trimmed
474 ));
475 }
476 }
477 }
478
479 if !errors.is_empty() {
480 return Err(ConfigError::InvalidConfigValue {
481 path: "config".to_string(),
482 reason: errors.join("; "),
483 });
484 }
485
486 Ok(warnings)
487 }
488}
489
490pub struct ConfigLoader;
492
493impl ConfigLoader {
494 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<ConfigFile> {
496 let path_ref = path.as_ref();
497 let content = fs::read_to_string(path_ref).map_err(|e| {
498 if e.kind() == std::io::ErrorKind::NotFound {
499 AppError::Config(ConfigError::ConfigFileNotFound(
500 path_ref.to_string_lossy().to_string(),
501 ))
502 } else {
503 AppError::Config(ConfigError::ReadFailed(format!(
504 "Failed to read config file '{}': {}",
505 path_ref.display(),
506 e
507 )))
508 }
509 })?;
510
511 let (config, warnings) = Self::load_from_str_with_warnings(&content)?;
512 Self::emit_warnings(&warnings);
513 Ok(config)
514 }
515
516 pub fn load_from_str(content: &str) -> Result<ConfigFile> {
518 let (config, warnings) =
519 Self::load_from_str_with_warnings(content).map_err(AppError::from)?;
520 Self::emit_warnings(&warnings);
521 Ok(config)
522 }
523
524 fn load_from_str_with_warnings(
526 content: &str,
527 ) -> std::result::Result<(ConfigFile, Vec<String>), ConfigError> {
528 let (sanitized, mut warnings) = Self::sanitize_special_values(content);
529
530 let mut config = toml::from_str::<ConfigFile>(&sanitized)
531 .map_err(|err| Self::map_toml_error(&sanitized, err))?;
532 let mut validation_warnings = config.validate()?;
533 warnings.append(&mut validation_warnings);
534
535 Ok((config, warnings))
536 }
537
538 fn emit_warnings(warnings: &[String]) {
539 for warning in warnings {
540 eprintln!("⚠️ Configuration Warning: {}", warning);
541 }
542 }
543
544 pub fn parse_ip_address(ip_str: &str) -> Result<IpAddr> {
546 let trimmed = ip_str.trim();
547 Ok(trimmed
548 .parse::<IpAddr>()
549 .map_err(|_| ConfigError::InvalidIpAddress(trimmed.to_string()))?)
550 }
551
552 pub fn create_socket_addr(host: &str, port: u16) -> Result<SocketAddr> {
555 host.parse::<IpAddr>()
557 .map(|ip| SocketAddr::new(ip, port))
558 .map_err(|_| ConfigError::InvalidIpAddress(format!(
559 "Invalid IP address '{}'. Tunnel proxy requires IP addresses, not hostnames. Use nslookup or dig to resolve hostnames manually.",
560 host
561 )))
562 .map_err(AppError::Config)
563 }
564
565 pub fn config_file_exists<P: AsRef<Path>>(path: P) -> bool {
567 path.as_ref().exists()
568 }
569
570 fn sanitize_special_values(content: &str) -> (String, Vec<String>) {
571 let mut sanitized = String::with_capacity(content.len());
572 let warnings = Vec::new();
573
574 for raw_line in content.split_inclusive('\n') {
575 let (line_body, newline) = match raw_line.strip_suffix('\n') {
576 Some(body) => (body, "\n"),
577 None => (raw_line, ""),
578 };
579
580 let mut replaced_line = line_body.to_string();
581
582 if let Some(eq_index) = line_body.find('=') {
583 let key_part = &line_body[..eq_index];
584 if key_part.trim().eq_ignore_ascii_case("source_ip") {
585 let prefix = &line_body[..=eq_index];
586 let rest = &line_body[eq_index + 1..];
587 let trimmed_rest = rest.trim_start();
588
589 if trimmed_rest.len() >= 4 && trimmed_rest[..4].eq_ignore_ascii_case("null") {
590 let remainder = &trimmed_rest[4..];
591 if remainder.is_empty()
592 || remainder.starts_with(|c: char| c.is_whitespace() || c == '#')
593 {
594 let whitespace_len = rest.len() - trimmed_rest.len();
595 let whitespace = &rest[..whitespace_len];
596 let suffix = &rest[whitespace_len + 4..];
597 replaced_line = format!("{}{}\"null\"{}", prefix, whitespace, suffix);
598 }
599 }
600 }
601 }
602
603 sanitized.push_str(&replaced_line);
604 sanitized.push_str(newline);
605 }
606
607 if !content.ends_with('\n') && sanitized.ends_with('\n') {
608 sanitized.pop();
609 }
610
611 (sanitized, warnings)
612 }
613
614 fn map_toml_error(content: &str, error: toml::de::Error) -> ConfigError {
615 let message = error.message().to_string();
616
617 if let Some(field) = message.strip_prefix("missing field `") {
618 if let Some(end) = field.find('`') {
619 let field_name = &field[..end];
620 return ConfigError::MissingRequiredField(field_name.to_string());
621 }
622 }
623
624 let (line, column) = error
625 .span()
626 .map(|span| Self::offset_to_line_col(content, span.start))
627 .unwrap_or((0, 0));
628 ConfigError::InvalidTomlFormat(format!(
629 "TOML parse error at line {}, column {}: {}\n\
630 Tip: Check for syntax errors like 'null' values (should be omitted), \
631 missing quotes, or invalid data types",
632 line, column, message
633 ))
634 }
635
636 fn offset_to_line_col(content: &str, offset: usize) -> (usize, usize) {
637 let mut line = 1usize;
638 let mut column = 1usize;
639 let upto = offset.min(content.len());
640
641 for byte in &content.as_bytes()[..upto] {
642 match byte {
643 b'\n' => {
644 line += 1;
645 column = 1;
646 }
647 b'\r' => column = 1,
648 _ => column += 1,
649 }
650 }
651
652 (line, column)
653 }
654
655 pub fn load_with_search() -> Result<(ConfigFile, std::path::PathBuf)> {
657 if let Some(path) = Self::get_cli_config_path() {
659 if path.exists() {
660 match Self::load_from_file(&path) {
661 Ok(config) => return Ok((config, path)),
662 Err(e) => {
663 eprintln!(
664 "❌ Failed to load config specified by CLI '{:?}': {}",
665 path, e
666 );
667 return Err(e);
670 }
671 }
672 } else {
673 return Err(AppError::Config(ConfigError::ConfigFileNotFound(format!(
674 "CLI specified config file not found: {:?}",
675 path
676 ))));
677 }
678 }
679
680 let paths = Self::get_config_search_paths();
682
683 for path in &paths {
684 if path.exists() {
685 match Self::load_from_file(path) {
686 Ok(config) => return Ok((config, path.clone())),
687 Err(e) => eprintln!("⚠️ Failed to load config from {}: {}", path.display(), e),
688 }
689 }
690 }
691
692 Err(AppError::Config(ConfigError::ConfigFileNotFound(format!(
693 "No configuration file found. Searched: {:?}",
694 paths
695 ))))
696 }
697
698 fn get_cli_config_path() -> Option<std::path::PathBuf> {
700 let args: Vec<String> = std::env::args().collect();
701 for i in 1..args.len() {
702 if (args[i] == "-c" || args[i] == "--config") && i + 1 < args.len() {
703 return Some(std::path::PathBuf::from(&args[i + 1]));
704 }
705 }
706 None
707 }
708
709 pub fn get_config_search_paths() -> Vec<std::path::PathBuf> {
711 let mut paths = Vec::new();
712 paths.push(std::path::PathBuf::from("default.toml"));
713 paths.push(std::path::PathBuf::from("config.toml"));
714 paths.push(std::path::PathBuf::from("gsc-fq.toml"));
715 paths
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722 use std::fs;
723 use std::net::{Ipv4Addr, Ipv6Addr};
724 use tempfile::NamedTempFile;
725
726 #[test]
727 fn test_validate_detects_invalid_source_ip() {
728 let mut config = ConfigFile {
729 server: None,
730 proxies: vec![ProxySection {
731 local: "8080".to_string(),
732 remote: "example.com:80".to_string(),
733 source_ip: Some("invalid-ip".to_string()),
734 allow_ips: None,
735 max_conns_per_ip: None,
736 cps_limit: None,
737 }],
738 token: Some("default".to_string()),
739 totp_secret: None,
740 reverse_proxies: vec![],
741 reverse_proxy_server: None,
742 reverse_proxy_client: None,
743 };
744
745 let result = config.validate();
746 assert!(result.is_err());
747 if let Err(ConfigError::InvalidConfigValue { reason, .. }) = result {
748 assert!(reason.contains("not a valid IP address"));
749 } else {
750 panic!("Expected InvalidConfigValue error");
751 }
752 }
753
754 #[test]
755 fn test_validate_handles_null_source_ip() {
756 let content = r#"
757[[proxies]]
758local = "9000"
759remote = "example.com:443"
760source_ip = "null"
761"#;
762
763 let (config, warnings) =
764 ConfigLoader::load_from_str_with_warnings(content).expect("Should load config");
765
766 assert!(warnings
767 .iter()
768 .any(|w| w.contains("source_ip") && w.contains("null")));
769 assert!(config.proxies[0].source_ip.is_none());
770 }
771
772 #[test]
773 fn test_sanitize_unquoted_null_source_ip() {
774 let content = r#"
775[[proxies]]
776local = "8000"
777remote = "example.org:80"
778source_ip = null
779"#;
780
781 let (config, warnings) =
782 ConfigLoader::load_from_str_with_warnings(content).expect("Should load config");
783
784 assert!(warnings
785 .iter()
786 .any(|w| w.contains("source_ip") && w.contains("null")));
787 assert!(config.proxies[0].source_ip.is_none());
788 }
789
790 #[test]
791 fn test_missing_required_field_error() {
792 let content = r#"
793[[proxies]]
794local = "7000"
795# Missing remote field on purpose
796"#;
797
798 let err = ConfigLoader::load_from_str_with_warnings(content).unwrap_err();
799 match err {
800 ConfigError::MissingRequiredField(field) => {
801 assert_eq!(field, "remote");
802 }
803 other => panic!("Expected MissingRequiredField, got {:?}", other),
804 }
805 }
806
807 #[test]
808 fn test_duplicate_local_port_detection() {
809 let mut config = ConfigFile {
810 server: None,
811 proxies: vec![
812 ProxySection {
813 local: "8080".to_string(),
814 remote: "example.com:80".to_string(),
815 source_ip: None,
816 allow_ips: None,
817 max_conns_per_ip: None,
818 cps_limit: None,
819 },
820 ProxySection {
821 local: "8080".to_string(),
822 remote: "example.net:8080".to_string(),
823 source_ip: None,
824 allow_ips: None,
825 max_conns_per_ip: None,
826 cps_limit: None,
827 },
828 ],
829 token: Some("default".to_string()),
830 totp_secret: None,
831 reverse_proxies: vec![],
832 reverse_proxy_server: None,
833 reverse_proxy_client: None,
834 };
835
836 let result = config.validate();
837 assert!(result.is_err());
838 if let Err(ConfigError::InvalidConfigValue { reason, .. }) = result {
839 assert!(reason.contains("Duplicate local port"));
840 } else {
841 panic!("Expected InvalidConfigValue error for duplicate port");
842 }
843 }
844
845 #[test]
846 fn test_config_file_not_found() {
847 let err = ConfigLoader::load_from_file("does_not_exist.toml").unwrap_err();
848 match err {
849 AppError::Config(ConfigError::ConfigFileNotFound(path)) => {
850 assert!(path.contains("does_not_exist.toml"));
851 }
852 other => panic!("Expected ConfigFileNotFound, got {:?}", other),
853 }
854 }
855
856 #[test]
857 fn test_read_and_validate_from_file() {
858 let file = NamedTempFile::new().expect("create temp file");
859 let content = r#"
860[[proxies]]
861local = "8100"
862remote = "example.com:8101"
863source_ip = null
864"#;
865 fs::write(file.path(), content).expect("write config");
866
867 let config = ConfigLoader::load_from_file(file.path()).expect("load config");
868 assert!(config.proxies[0].source_ip.is_none());
869 }
870
871 #[test]
872 fn test_server_bind_ip_empty_defaults() {
873 let content = r#"[server]
874bind_ip = ""
875allowed_tokens = []
876
877[[proxies]]
878local = "8200"
879remote = "example.com:8201"
880"#;
881
882 let (config, warnings) =
883 ConfigLoader::load_from_str_with_warnings(content).expect("load config");
884
885 assert!(warnings.iter().any(|w| w.contains("bind_ip")));
886 let server = config.server.as_ref().expect("server section");
887 assert_eq!(server.bind_ip.as_deref(), Some("127.0.0.1"));
888 }
889
890 #[test]
891 fn test_ip_parsing() {
892 let ipv4 = ConfigLoader::parse_ip_address("192.168.1.1").unwrap();
893 assert_eq!(ipv4, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
894
895 let ipv6 = ConfigLoader::parse_ip_address("::1").unwrap();
896 assert_eq!(ipv6, IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)));
897
898 let trimmed = ConfigLoader::parse_ip_address(" 127.0.0.1 ").unwrap();
899 assert_eq!(trimmed, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
900
901 let invalid = ConfigLoader::parse_ip_address("invalid");
902 assert!(invalid.is_err());
903 }
904
905 #[test]
906 fn test_load_from_str() {
907 let toml_content = r#"[server]
908bind_ip = " 127.0.0.1 "
909allowed_tokens = []
910
911[[proxies]]
912local = "8080"
913remote = "example.com:80"
914"#;
915
916 let (config, warnings) =
917 ConfigLoader::load_from_str_with_warnings(toml_content).expect("load config");
918 assert!(warnings.is_empty());
919
920 let server = config.server.as_ref().expect("Server section should exist");
921 assert_eq!(server.bind_ip.as_deref(), Some("127.0.0.1"));
922 assert_eq!(config.proxies.len(), 1);
923 assert_eq!(config.proxies[0].local, "8080");
924 assert_eq!(config.proxies[0].remote, "example.com:80");
925 }
926}