1use crate::Error;
4use std::env;
5use std::fmt;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9#[non_exhaustive]
10pub enum NtripVersion {
11 V1,
13 V2,
15 #[default]
17 Auto,
18}
19
20#[derive(Clone)]
22pub struct ProxyConfig {
23 pub host: String,
25 pub port: u16,
27 pub username: Option<String>,
29 pub password: Option<String>,
31}
32
33impl ProxyConfig {
34 pub fn new(host: impl Into<String>, port: u16) -> Self {
36 Self {
37 host: host.into(),
38 port,
39 username: None,
40 password: None,
41 }
42 }
43
44 pub fn with_credentials(
46 mut self,
47 username: impl Into<String>,
48 password: impl Into<String>,
49 ) -> Self {
50 self.username = Some(username.into());
51 self.password = Some(password.into());
52 self
53 }
54
55 pub fn from_url(url: &str) -> Option<Self> {
57 let url = url
59 .strip_prefix("http://")
60 .or_else(|| url.strip_prefix("https://"))
61 .unwrap_or(url);
62
63 let (auth, host_port) = if let Some(at_pos) = url.rfind('@') {
65 (Some(&url[..at_pos]), &url[at_pos + 1..])
66 } else {
67 (None, url)
68 };
69
70 let (host, port) = if let Some(colon_pos) = host_port.rfind(':') {
72 let port_str = &host_port[colon_pos + 1..];
73 let port: u16 = port_str.parse().ok()?;
74 (&host_port[..colon_pos], port)
75 } else {
76 (host_port, 8080) };
78
79 if host.is_empty() {
80 return None;
81 }
82
83 let mut config = ProxyConfig::new(host, port);
84
85 if let Some(auth) = auth {
87 if let Some(colon_pos) = auth.find(':') {
88 let username = &auth[..colon_pos];
89 let password = &auth[colon_pos + 1..];
90 config = config.with_credentials(username, password);
91 }
92 }
93
94 Some(config)
95 }
96
97 pub fn from_env() -> Option<Self> {
102 env::var("HTTP_PROXY")
103 .or_else(|_| env::var("http_proxy"))
104 .ok()
105 .and_then(|url| Self::from_url(&url))
106 }
107}
108
109impl fmt::Debug for ProxyConfig {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 f.debug_struct("ProxyConfig")
112 .field("host", &self.host)
113 .field("port", &self.port)
114 .field("username", &self.username)
115 .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
116 .finish()
117 }
118}
119
120#[derive(Debug, Clone)]
122pub struct ConnectionConfig {
123 pub timeout_secs: u32,
125 pub read_timeout_secs: u32,
127 pub max_reconnect_attempts: u32,
129 pub reconnect_delay_ms: u64,
131}
132
133impl Default for ConnectionConfig {
134 fn default() -> Self {
135 Self {
136 timeout_secs: 15,
137 read_timeout_secs: 30,
138 max_reconnect_attempts: 3,
139 reconnect_delay_ms: 1000,
140 }
141 }
142}
143
144#[derive(Clone)]
146pub struct NtripConfig {
147 pub host: String,
149 pub port: u16,
151 pub mountpoint: String,
153 pub username: Option<String>,
155 pub password: Option<String>,
157 pub use_tls: bool,
159 pub tls_skip_verify: bool,
161 pub ntrip_version: NtripVersion,
163 pub connection: ConnectionConfig,
165 pub proxy: Option<ProxyConfig>,
167}
168
169impl fmt::Debug for NtripConfig {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 f.debug_struct("NtripConfig")
172 .field("host", &self.host)
173 .field("port", &self.port)
174 .field("mountpoint", &self.mountpoint)
175 .field("username", &self.username)
176 .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
177 .field("use_tls", &self.use_tls)
178 .field("tls_skip_verify", &self.tls_skip_verify)
179 .field("ntrip_version", &self.ntrip_version)
180 .field("connection", &self.connection)
181 .field("proxy", &self.proxy)
182 .finish()
183 }
184}
185
186impl NtripConfig {
187 pub fn new(host: impl Into<String>, port: u16, mountpoint: impl Into<String>) -> Self {
189 Self {
190 host: host.into(),
191 port,
192 mountpoint: mountpoint.into(),
193 username: None,
194 password: None,
195 use_tls: false,
196 tls_skip_verify: false,
197 ntrip_version: NtripVersion::Auto,
198 connection: ConnectionConfig::default(),
199 proxy: None,
200 }
201 }
202
203 pub fn with_credentials(
205 mut self,
206 username: impl Into<String>,
207 password: impl Into<String>,
208 ) -> Self {
209 self.username = Some(username.into());
210 self.password = Some(password.into());
211 self
212 }
213
214 pub fn with_tls(mut self) -> Self {
216 self.use_tls = true;
217 self
218 }
219
220 pub fn with_tls_skip_verify(mut self) -> Self {
222 self.tls_skip_verify = true;
223 self
224 }
225
226 pub fn with_version(mut self, version: NtripVersion) -> Self {
228 self.ntrip_version = version;
229 self
230 }
231
232 pub fn with_timeout(mut self, timeout_secs: u32) -> Self {
234 self.connection.timeout_secs = timeout_secs;
235 self
236 }
237
238 pub fn with_read_timeout(mut self, read_timeout_secs: u32) -> Self {
240 self.connection.read_timeout_secs = read_timeout_secs;
241 self
242 }
243
244 pub fn with_reconnect(mut self, max_attempts: u32, delay_ms: u64) -> Self {
246 self.connection.max_reconnect_attempts = max_attempts;
247 self.connection.reconnect_delay_ms = delay_ms;
248 self
249 }
250
251 pub fn without_reconnect(mut self) -> Self {
253 self.connection.max_reconnect_attempts = 0;
254 self
255 }
256
257 pub fn with_proxy(mut self, proxy: ProxyConfig) -> Self {
262 self.proxy = Some(proxy);
263 self
264 }
265
266 pub fn with_proxy_from_env(mut self) -> Self {
280 self.proxy = ProxyConfig::from_env();
281 self
282 }
283
284 pub fn validate(&self) -> Result<(), Error> {
286 if self.host.is_empty() {
287 return Err(Error::InvalidConfig {
288 message: "Host cannot be empty".to_string(),
289 });
290 }
291 if self.port == 0 {
292 return Err(Error::InvalidConfig {
293 message: "Port cannot be 0".to_string(),
294 });
295 }
296 Self::validate_no_control_chars(&self.host, "host")?;
298 Self::validate_no_control_chars(&self.mountpoint, "mountpoint")?;
299 if let Some(ref u) = self.username {
300 Self::validate_no_control_chars(u, "username")?;
301 }
302 if let Some(ref p) = self.password {
303 Self::validate_no_control_chars(p, "password")?;
304 }
305 Ok(())
306 }
307
308 fn validate_no_control_chars(s: &str, field_name: &str) -> Result<(), Error> {
310 if s.bytes().any(|b| b < 0x20 || b == 0x7F) {
311 return Err(Error::InvalidConfig {
312 message: format!(
313 "{} contains invalid control characters (possible header injection)",
314 field_name
315 ),
316 });
317 }
318 Ok(())
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_proxy_config_from_url_simple() {
328 let proxy = ProxyConfig::from_url("proxy.example.com:8080").unwrap();
329 assert_eq!(proxy.host, "proxy.example.com");
330 assert_eq!(proxy.port, 8080);
331 assert!(proxy.username.is_none());
332 assert!(proxy.password.is_none());
333 }
334
335 #[test]
336 fn test_proxy_config_from_url_with_http_prefix() {
337 let proxy = ProxyConfig::from_url("http://proxy.example.com:3128").unwrap();
338 assert_eq!(proxy.host, "proxy.example.com");
339 assert_eq!(proxy.port, 3128);
340 }
341
342 #[test]
343 fn test_proxy_config_from_url_with_credentials() {
344 let proxy = ProxyConfig::from_url("http://user:pass@proxy.example.com:8080").unwrap();
345 assert_eq!(proxy.host, "proxy.example.com");
346 assert_eq!(proxy.port, 8080);
347 assert_eq!(proxy.username.as_deref(), Some("user"));
348 assert_eq!(proxy.password.as_deref(), Some("pass"));
349 }
350
351 #[test]
352 fn test_proxy_config_from_url_default_port() {
353 let proxy = ProxyConfig::from_url("proxy.example.com").unwrap();
354 assert_eq!(proxy.host, "proxy.example.com");
355 assert_eq!(proxy.port, 8080); }
357
358 #[test]
359 fn test_proxy_config_from_url_empty_returns_none() {
360 assert!(ProxyConfig::from_url("").is_none());
361 }
362
363 #[test]
364 fn test_proxy_config_builder() {
365 let proxy = ProxyConfig::new("proxy.local", 8888).with_credentials("admin", "secret");
366 assert_eq!(proxy.host, "proxy.local");
367 assert_eq!(proxy.port, 8888);
368 assert_eq!(proxy.username.as_deref(), Some("admin"));
369 assert_eq!(proxy.password.as_deref(), Some("secret"));
370 }
371
372 #[test]
373 fn test_proxy_password_redacted_in_debug() {
374 let proxy =
375 ProxyConfig::new("proxy.local", 8080).with_credentials("user", "secret_password");
376 let debug_output = format!("{:?}", proxy);
377 assert!(!debug_output.contains("secret_password"));
378 assert!(debug_output.contains("[REDACTED]"));
379 }
380
381 #[test]
382 fn test_ntrip_config_with_proxy() {
383 let proxy = ProxyConfig::new("proxy.local", 8080);
384 let config = NtripConfig::new("caster.example.com", 2101, "MOUNT").with_proxy(proxy);
385 assert!(config.proxy.is_some());
386 assert_eq!(config.proxy.as_ref().unwrap().host, "proxy.local");
387 }
388}