1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::time::Duration;
4
5pub const DEFAULT_PROXY_SOURCES: &[&str] = &[
8 "https://letllmrun.codeberg.page/scatter-proxy/socks5.txt",
10];
11
12#[derive(Clone)]
14pub struct ScatterProxyConfig {
15 pub sources: Vec<String>,
18 pub source_refresh_interval: Duration,
20 pub rate_limit: RateLimitConfig,
22 pub proxy_timeout: Duration,
24 pub max_concurrent_per_request: usize,
26 pub max_inflight: usize,
28 pub task_pool_capacity: usize,
30 pub health_window: usize,
32 pub cooldown_base: Duration,
34 pub cooldown_max: Duration,
36 pub cooldown_consecutive_fails: usize,
38 pub eviction_min_samples: usize,
40 pub state_file: Option<PathBuf>,
42 pub state_save_interval: Duration,
44 pub metrics_log_interval: Duration,
46 pub prefer_remote_dns: bool,
48 pub name: Option<String>,
56 pub max_requests_per_proxy: Option<u32>,
62 pub challenge_cooldown: Duration,
66}
67
68impl Default for ScatterProxyConfig {
69 fn default() -> Self {
70 Self {
71 sources: Vec::new(),
72 source_refresh_interval: Duration::from_secs(600),
73 rate_limit: RateLimitConfig::default(),
74 proxy_timeout: Duration::from_secs(8),
75 max_concurrent_per_request: 3,
76 max_inflight: 100,
77 task_pool_capacity: 1000,
78 health_window: 30,
79 cooldown_base: Duration::from_secs(30),
80 cooldown_max: Duration::from_secs(300),
81 cooldown_consecutive_fails: 3,
82 eviction_min_samples: 30,
83 state_file: None,
84 state_save_interval: Duration::from_secs(300),
85 metrics_log_interval: Duration::from_secs(30),
86 prefer_remote_dns: true,
87 name: None,
88 max_requests_per_proxy: None,
89 challenge_cooldown: Duration::from_secs(60),
90 }
91 }
92}
93
94#[derive(Clone)]
96pub struct RateLimitConfig {
97 pub default_interval: Duration,
99 pub host_overrides: HashMap<String, Duration>,
101}
102
103impl Default for RateLimitConfig {
104 fn default() -> Self {
105 Self {
106 default_interval: Duration::from_millis(500),
107 host_overrides: HashMap::new(),
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_scatter_proxy_config_defaults() {
118 let cfg = ScatterProxyConfig::default();
119
120 assert!(cfg.sources.is_empty());
121 assert_eq!(cfg.source_refresh_interval, Duration::from_secs(600));
122 assert_eq!(cfg.proxy_timeout, Duration::from_secs(8));
123 assert_eq!(cfg.max_concurrent_per_request, 3);
124 assert_eq!(cfg.max_inflight, 100);
125 assert_eq!(cfg.task_pool_capacity, 1000);
126 assert_eq!(cfg.health_window, 30);
127 assert_eq!(cfg.cooldown_base, Duration::from_secs(30));
128 assert_eq!(cfg.cooldown_max, Duration::from_secs(300));
129 assert_eq!(cfg.cooldown_consecutive_fails, 3);
130 assert_eq!(cfg.eviction_min_samples, 30);
131 assert!(cfg.state_file.is_none());
132 assert_eq!(cfg.state_save_interval, Duration::from_secs(300));
133 assert_eq!(cfg.metrics_log_interval, Duration::from_secs(30));
134 assert!(cfg.prefer_remote_dns);
135 assert!(cfg.max_requests_per_proxy.is_none());
136 assert_eq!(cfg.challenge_cooldown, Duration::from_secs(60));
137 }
138
139 #[test]
140 fn test_rate_limit_config_defaults() {
141 let rl = RateLimitConfig::default();
142
143 assert_eq!(rl.default_interval, Duration::from_millis(500));
144 assert!(rl.host_overrides.is_empty());
145 }
146
147 #[test]
148 fn test_rate_limit_config_nested_in_scatter_proxy_config() {
149 let cfg = ScatterProxyConfig::default();
150
151 assert_eq!(cfg.rate_limit.default_interval, Duration::from_millis(500));
152 assert!(cfg.rate_limit.host_overrides.is_empty());
153 }
154
155 #[test]
156 fn test_config_can_be_customised() {
157 let cfg = ScatterProxyConfig {
158 sources: vec!["https://example.com/proxies.txt".into()],
159 max_concurrent_per_request: 5,
160 max_inflight: 200,
161 state_file: Some(PathBuf::from("/tmp/scatter.json")),
162 prefer_remote_dns: false,
163 max_requests_per_proxy: Some(100),
164 challenge_cooldown: Duration::from_secs(30),
165 rate_limit: RateLimitConfig {
166 default_interval: Duration::from_millis(250),
167 host_overrides: {
168 let mut m = HashMap::new();
169 m.insert("slow.example.com".into(), Duration::from_secs(2));
170 m
171 },
172 },
173 ..ScatterProxyConfig::default()
174 };
175
176 assert_eq!(cfg.sources.len(), 1);
177 assert_eq!(cfg.max_concurrent_per_request, 5);
178 assert_eq!(cfg.max_inflight, 200);
179 assert_eq!(cfg.state_file, Some(PathBuf::from("/tmp/scatter.json")));
180 assert!(!cfg.prefer_remote_dns);
181 assert_eq!(cfg.max_requests_per_proxy, Some(100));
182 assert_eq!(cfg.challenge_cooldown, Duration::from_secs(30));
183 assert_eq!(cfg.rate_limit.default_interval, Duration::from_millis(250));
184 assert_eq!(
185 cfg.rate_limit.host_overrides.get("slow.example.com"),
186 Some(&Duration::from_secs(2))
187 );
188 assert_eq!(cfg.proxy_timeout, Duration::from_secs(8));
190 assert_eq!(cfg.health_window, 30);
191 }
192
193 #[test]
194 fn test_max_requests_per_proxy_default_is_none() {
195 let cfg = ScatterProxyConfig::default();
196 assert!(cfg.max_requests_per_proxy.is_none());
197 }
198
199 #[test]
200 fn test_challenge_cooldown_default_is_60s() {
201 let cfg = ScatterProxyConfig::default();
202 assert_eq!(cfg.challenge_cooldown, Duration::from_secs(60));
203 }
204
205 #[test]
206 fn test_cooldown_max_gte_cooldown_base() {
207 let cfg = ScatterProxyConfig::default();
208 assert!(cfg.cooldown_max >= cfg.cooldown_base);
209 }
210
211 #[test]
212 fn test_source_refresh_interval_is_10_minutes() {
213 let cfg = ScatterProxyConfig::default();
214 assert_eq!(cfg.source_refresh_interval.as_secs(), 10 * 60);
215 }
216
217 #[test]
218 fn test_state_save_interval_is_5_minutes() {
219 let cfg = ScatterProxyConfig::default();
220 assert_eq!(cfg.state_save_interval.as_secs(), 5 * 60);
221 }
222
223 #[test]
224 fn test_default_proxy_sources_not_empty() {
225 assert!(DEFAULT_PROXY_SOURCES
226 .iter()
227 .any(|source| source.contains("codeberg.page/scatter-proxy/socks5.txt")));
228 for source in DEFAULT_PROXY_SOURCES {
229 assert!(source.starts_with("https://"));
230 }
231 }
232}