1use crate::Error;
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
8#[non_exhaustive]
9pub enum NtripVersion {
10 V1,
12 V2,
14 #[default]
16 Auto,
17}
18
19#[derive(Debug, Clone)]
21pub struct ConnectionConfig {
22 pub timeout_secs: u32,
24 pub read_timeout_secs: u32,
26 pub max_reconnect_attempts: u32,
28 pub reconnect_delay_ms: u64,
30}
31
32impl Default for ConnectionConfig {
33 fn default() -> Self {
34 Self {
35 timeout_secs: 15,
36 read_timeout_secs: 30,
37 max_reconnect_attempts: 3,
38 reconnect_delay_ms: 1000,
39 }
40 }
41}
42
43#[derive(Clone)]
45pub struct NtripConfig {
46 pub host: String,
48 pub port: u16,
50 pub mountpoint: String,
52 pub username: Option<String>,
54 pub password: Option<String>,
56 pub use_tls: bool,
58 pub tls_skip_verify: bool,
60 pub ntrip_version: NtripVersion,
62 pub connection: ConnectionConfig,
64}
65
66impl fmt::Debug for NtripConfig {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.debug_struct("NtripConfig")
69 .field("host", &self.host)
70 .field("port", &self.port)
71 .field("mountpoint", &self.mountpoint)
72 .field("username", &self.username)
73 .field("password", &self.password.as_ref().map(|_| "[REDACTED]"))
74 .field("use_tls", &self.use_tls)
75 .field("tls_skip_verify", &self.tls_skip_verify)
76 .field("ntrip_version", &self.ntrip_version)
77 .field("connection", &self.connection)
78 .finish()
79 }
80}
81
82impl NtripConfig {
83 pub fn new(host: impl Into<String>, port: u16, mountpoint: impl Into<String>) -> Self {
85 Self {
86 host: host.into(),
87 port,
88 mountpoint: mountpoint.into(),
89 username: None,
90 password: None,
91 use_tls: false,
92 tls_skip_verify: false,
93 ntrip_version: NtripVersion::Auto,
94 connection: ConnectionConfig::default(),
95 }
96 }
97
98 pub fn with_credentials(
100 mut self,
101 username: impl Into<String>,
102 password: impl Into<String>,
103 ) -> Self {
104 self.username = Some(username.into());
105 self.password = Some(password.into());
106 self
107 }
108
109 pub fn with_tls(mut self) -> Self {
111 self.use_tls = true;
112 self
113 }
114
115 pub fn with_tls_skip_verify(mut self) -> Self {
117 self.tls_skip_verify = true;
118 self
119 }
120
121 pub fn with_version(mut self, version: NtripVersion) -> Self {
123 self.ntrip_version = version;
124 self
125 }
126
127 pub fn with_timeout(mut self, timeout_secs: u32) -> Self {
129 self.connection.timeout_secs = timeout_secs;
130 self
131 }
132
133 pub fn with_read_timeout(mut self, read_timeout_secs: u32) -> Self {
135 self.connection.read_timeout_secs = read_timeout_secs;
136 self
137 }
138
139 pub fn with_reconnect(mut self, max_attempts: u32, delay_ms: u64) -> Self {
141 self.connection.max_reconnect_attempts = max_attempts;
142 self.connection.reconnect_delay_ms = delay_ms;
143 self
144 }
145
146 pub fn without_reconnect(mut self) -> Self {
148 self.connection.max_reconnect_attempts = 0;
149 self
150 }
151
152 pub fn validate(&self) -> Result<(), Error> {
154 if self.host.is_empty() {
155 return Err(Error::InvalidConfig {
156 message: "Host cannot be empty".to_string(),
157 });
158 }
159 if self.port == 0 {
160 return Err(Error::InvalidConfig {
161 message: "Port cannot be 0".to_string(),
162 });
163 }
164 Self::validate_no_control_chars(&self.host, "host")?;
166 Self::validate_no_control_chars(&self.mountpoint, "mountpoint")?;
167 if let Some(ref u) = self.username {
168 Self::validate_no_control_chars(u, "username")?;
169 }
170 if let Some(ref p) = self.password {
171 Self::validate_no_control_chars(p, "password")?;
172 }
173 Ok(())
174 }
175
176 fn validate_no_control_chars(s: &str, field_name: &str) -> Result<(), Error> {
178 if s.bytes().any(|b| b < 0x20 || b == 0x7F) {
179 return Err(Error::InvalidConfig {
180 message: format!(
181 "{} contains invalid control characters (possible header injection)",
182 field_name
183 ),
184 });
185 }
186 Ok(())
187 }
188}