1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::Duration;
4
5pub const DEFAULT_PROXY_SOURCES: &[&str] = &[
8 "https://cdn.jsdelivr.net/gh/letllmrun/scatter-proxy@main/docs/socks5.txt",
10 "https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt",
12 "https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/socks5.txt",
13];
14
15pub struct ScatterProxyConfig {
17 pub sources: Vec<String>,
20 pub source_refresh_interval: Duration,
22 pub rate_limit: RateLimitConfig,
24 pub proxy_timeout: Duration,
26 pub max_concurrent_per_request: usize,
28 pub max_inflight: usize,
30 pub task_pool_capacity: usize,
32 pub health_window: usize,
34 pub cooldown_base: Duration,
36 pub cooldown_max: Duration,
38 pub cooldown_consecutive_fails: usize,
40 pub eviction_min_samples: usize,
42 pub state_file: Option<PathBuf>,
44 pub state_save_interval: Duration,
46 pub metrics_log_interval: Duration,
48 pub prefer_remote_dns: bool,
50}
51
52impl Default for ScatterProxyConfig {
53 fn default() -> Self {
54 Self {
55 sources: Vec::new(),
56 source_refresh_interval: Duration::from_secs(600),
57 rate_limit: RateLimitConfig::default(),
58 proxy_timeout: Duration::from_secs(8),
59 max_concurrent_per_request: 3,
60 max_inflight: 100,
61 task_pool_capacity: 1000,
62 health_window: 30,
63 cooldown_base: Duration::from_secs(30),
64 cooldown_max: Duration::from_secs(300),
65 cooldown_consecutive_fails: 3,
66 eviction_min_samples: 30,
67 state_file: None,
68 state_save_interval: Duration::from_secs(300),
69 metrics_log_interval: Duration::from_secs(30),
70 prefer_remote_dns: true,
71 }
72 }
73}
74
75pub struct RateLimitConfig {
77 pub default_interval: Duration,
79 pub host_overrides: HashMap<String, Duration>,
81}
82
83impl Default for RateLimitConfig {
84 fn default() -> Self {
85 Self {
86 default_interval: Duration::from_millis(500),
87 host_overrides: HashMap::new(),
88 }
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn test_scatter_proxy_config_defaults() {
98 let cfg = ScatterProxyConfig::default();
99
100 assert!(cfg.sources.is_empty());
101 assert_eq!(cfg.source_refresh_interval, Duration::from_secs(600));
102 assert_eq!(cfg.proxy_timeout, Duration::from_secs(8));
103 assert_eq!(cfg.max_concurrent_per_request, 3);
104 assert_eq!(cfg.max_inflight, 100);
105 assert_eq!(cfg.task_pool_capacity, 1000);
106 assert_eq!(cfg.health_window, 30);
107 assert_eq!(cfg.cooldown_base, Duration::from_secs(30));
108 assert_eq!(cfg.cooldown_max, Duration::from_secs(300));
109 assert_eq!(cfg.cooldown_consecutive_fails, 3);
110 assert_eq!(cfg.eviction_min_samples, 30);
111 assert!(cfg.state_file.is_none());
112 assert_eq!(cfg.state_save_interval, Duration::from_secs(300));
113 assert_eq!(cfg.metrics_log_interval, Duration::from_secs(30));
114 assert!(cfg.prefer_remote_dns);
115 }
116
117 #[test]
118 fn test_rate_limit_config_defaults() {
119 let rl = RateLimitConfig::default();
120
121 assert_eq!(rl.default_interval, Duration::from_millis(500));
122 assert!(rl.host_overrides.is_empty());
123 }
124
125 #[test]
126 fn test_rate_limit_config_nested_in_scatter_proxy_config() {
127 let cfg = ScatterProxyConfig::default();
128
129 assert_eq!(cfg.rate_limit.default_interval, Duration::from_millis(500));
130 assert!(cfg.rate_limit.host_overrides.is_empty());
131 }
132
133 #[test]
134 fn test_config_can_be_customised() {
135 let cfg = ScatterProxyConfig {
136 sources: vec!["https://example.com/proxies.txt".into()],
137 max_concurrent_per_request: 5,
138 max_inflight: 200,
139 state_file: Some(PathBuf::from("/tmp/scatter.json")),
140 prefer_remote_dns: false,
141 rate_limit: RateLimitConfig {
142 default_interval: Duration::from_millis(250),
143 host_overrides: {
144 let mut m = HashMap::new();
145 m.insert("slow.example.com".into(), Duration::from_secs(2));
146 m
147 },
148 },
149 ..ScatterProxyConfig::default()
150 };
151
152 assert_eq!(cfg.sources.len(), 1);
153 assert_eq!(cfg.max_concurrent_per_request, 5);
154 assert_eq!(cfg.max_inflight, 200);
155 assert_eq!(cfg.state_file, Some(PathBuf::from("/tmp/scatter.json")));
156 assert!(!cfg.prefer_remote_dns);
157 assert_eq!(cfg.rate_limit.default_interval, Duration::from_millis(250));
158 assert_eq!(
159 cfg.rate_limit.host_overrides.get("slow.example.com"),
160 Some(&Duration::from_secs(2))
161 );
162 assert_eq!(cfg.proxy_timeout, Duration::from_secs(8));
164 assert_eq!(cfg.health_window, 30);
165 }
166
167 #[test]
168 fn test_cooldown_max_gte_cooldown_base() {
169 let cfg = ScatterProxyConfig::default();
170 assert!(cfg.cooldown_max >= cfg.cooldown_base);
171 }
172
173 #[test]
174 fn test_source_refresh_interval_is_10_minutes() {
175 let cfg = ScatterProxyConfig::default();
176 assert_eq!(cfg.source_refresh_interval.as_secs(), 10 * 60);
177 }
178
179 #[test]
180 fn test_state_save_interval_is_5_minutes() {
181 let cfg = ScatterProxyConfig::default();
182 assert_eq!(cfg.state_save_interval.as_secs(), 5 * 60);
183 }
184
185 #[test]
186 fn test_default_proxy_sources_not_empty() {
187 assert!(!DEFAULT_PROXY_SOURCES.is_empty());
188 for source in DEFAULT_PROXY_SOURCES {
189 assert!(source.starts_with("https://"));
190 }
191 }
192}