1use eyre::{Context, Result};
39use std::str::FromStr;
40
41pub struct RobustNtripClientOptions {
43 pub max_backoff_duration: std::time::Duration,
45
46 pub timeout: Option<std::time::Duration>,
48}
49
50impl std::default::Default for RobustNtripClientOptions {
51 fn default() -> Self {
52 Self {
53 max_backoff_duration: std::time::Duration::from_secs(30),
54 timeout: Some(std::time::Duration::from_secs(10)),
55 }
56 }
57}
58
59pub struct RobustNtripClient {
62 request_url: String,
63 user_pass: Option<(String, String)>,
64 client: reqwest::Client,
65 timeout: Option<std::time::Duration>,
66 max_backoff_duration: std::time::Duration,
67
68 response: reqwest::Response,
69}
70
71impl RobustNtripClient {
72 pub async fn new(url: &str, opts: RobustNtripClientOptions) -> Result<Self> {
74 let uri: http::Uri = url
75 .parse()
76 .with_context(|| format!("While parsing NTRIP URL \"{url}\"."))?;
77
78 let (need_tls, default_port) = if let Some(scheme) = uri.scheme() {
79 let ntrip = http::uri::Scheme::from_str("ntrip").unwrap();
80 let http = http::uri::Scheme::from_str("http").unwrap();
81 let https = http::uri::Scheme::from_str("https").unwrap();
82 if scheme == &ntrip {
83 (false, Some(2101))
84 } else if scheme == &http {
85 (false, None)
86 } else if scheme == &https {
87 (true, None)
88 } else {
89 eyre::bail!("Unexpected URI scheme (found \"{scheme}\").");
90 }
91 } else {
92 eyre::bail!("No URI scheme.");
95 };
96
97 let parts = uri.into_parts();
98 let (host_port, user_pass) = if let Some(auth) = &parts.authority {
99 parse_authority(auth)?
100 } else {
101 eyre::bail!("No authority section of URL");
102 };
103 let auth = http::uri::Authority::from_maybe_shared(host_port)?;
104
105 let host = auth.host();
106 let port = auth.port_u16().or(default_port);
107 let mountpoint = if let Some(pq) = parts.path_and_query {
108 pq.path().to_string()
109 } else {
110 "/".to_string()
111 };
112
113 let scheme = if need_tls { "https" } else { "http" };
114 let port = if let Some(port) = port {
115 format!(":{port}")
116 } else {
117 "".to_string()
118 };
119 let request_url = format!("{scheme}://{host}{port}{mountpoint}");
120
121 let mut headers = reqwest::header::HeaderMap::new();
122 headers.insert(
124 "Ntrip-Version",
125 reqwest::header::HeaderValue::from_static("ntrip/2.0"),
126 );
127
128 let client = reqwest::ClientBuilder::new()
129 .tcp_keepalive(std::time::Duration::from_secs(5))
130 .default_headers(headers)
131 .user_agent(format!(
132 "NTRIP {}/{}",
133 env!("CARGO_PKG_NAME"),
134 env!("CARGO_PKG_VERSION")
135 ))
136 .build()?;
137
138 let max_backoff_duration = opts.max_backoff_duration;
139 let timeout = opts.timeout;
140 let response = establish_connection(
141 &client,
142 &request_url,
143 user_pass.as_ref(),
144 max_backoff_duration,
145 )
146 .await?;
147
148 Ok(Self {
149 request_url,
150 user_pass,
151 client,
152 timeout,
153 max_backoff_duration,
154
155 response,
156 })
157 }
158
159 pub async fn chunk(&mut self) -> Result<bytes::Bytes> {
161 if let Some(duration) = self.timeout {
162 self.next_chunk_with_timeout(duration).await
163 } else {
164 self.next_chunk_infinite_wait().await
165 }
166 }
167
168 async fn next_chunk_with_timeout(
169 &mut self,
170 duration: std::time::Duration,
171 ) -> Result<bytes::Bytes> {
172 match tokio::time::timeout(duration, self.next_chunk_infinite_wait()).await {
173 Ok(next) => next, Err(_) => {
175 tracing::warn!("Reconnecting due to timeout elapsed.");
176 self.reconnect_and_get_first_chunk().await
177 }
178 }
179 }
180
181 async fn next_chunk_infinite_wait(&mut self) -> Result<bytes::Bytes> {
182 match self.response.chunk().await {
183 Ok(Some(next)) => Ok(next), Ok(None) => {
185 tracing::warn!("Reconnecting due to end of HTTP stream.");
186 self.reconnect_and_get_first_chunk().await
187 }
188 Err(_) => {
189 tracing::warn!("Reconnecting due to error with HTTP stream.");
190 self.reconnect_and_get_first_chunk().await
191 }
192 }
193 }
194
195 async fn reconnect_and_get_first_chunk(&mut self) -> Result<bytes::Bytes> {
196 let mut backoff = min_dur(std::time::Duration::from_secs(1), self.max_backoff_duration);
197 loop {
198 self.response = establish_connection(
199 &self.client,
200 &self.request_url,
201 self.user_pass.as_ref(),
202 self.max_backoff_duration,
203 )
204 .await?;
205
206 match self.response.chunk().await {
207 Ok(Some(chunk)) => return Ok(chunk),
208 Ok(None) => {
209 tracing::warn!(
210 "NTRIP stream ended before yielding data after reconnect; retrying."
211 );
212 }
213 Err(error) => {
214 tracing::warn!(
215 %error,
216 "Error reading NTRIP stream after reconnect; retrying."
217 );
218 }
219 }
220
221 tokio::time::sleep(backoff).await;
222 backoff = min_dur(backoff * 2, self.max_backoff_duration);
223 }
224 }
225}
226
227async fn establish_connection(
228 client: &reqwest::Client,
229 request_url: &str,
230 user_pass: Option<&(String, String)>,
231 max_backoff_duration: std::time::Duration,
232) -> Result<reqwest::Response> {
233 let mut backoff = std::time::Duration::from_secs(1);
234 loop {
235 tracing::info!("Establishing connection to {request_url}.");
236 let mut req_builder = client.get(request_url);
237 if let Some((username, password)) = &user_pass {
238 req_builder = req_builder.basic_auth(username, Some(password));
239 }
240 let result_response = req_builder.send().await;
241 match result_response {
242 Ok(response) => {
243 tracing::debug!("Sent request");
244
245 if !response.status().is_success() {
246 eyre::bail!("Error getting NTRIP URL: HTTP status {}", response.status());
247 }
248
249 return Ok(response);
250 }
251 Err(e) => {
252 let error = eyre::Report::from(e);
253 let mut err_msg = format!("Could not open NTRIP URL: {error}");
254 for cause in error.chain() {
255 err_msg = format!("{err_msg}\n cause: {cause}");
256 }
257 tracing::warn!("{err_msg}");
258
259 tokio::time::sleep(backoff).await;
260 backoff = min_dur(backoff * 2, max_backoff_duration);
261 }
262 }
263 }
264}
265
266fn min_dur(a: std::time::Duration, b: std::time::Duration) -> std::time::Duration {
267 if a < b { a } else { b }
268}
269
270fn parse_authority(auth: &http::uri::Authority) -> Result<(String, Option<(String, String)>)> {
271 let auth_vec = auth.as_str().split("@").collect::<Vec<_>>();
273 match auth_vec.len() {
274 1 => {
275 let host_port = auth_vec[0].to_string();
277 Ok((host_port, None))
278 }
279 2 => {
280 let user_pass = auth_vec[0];
282 let host_port = auth_vec[1].to_string();
283 let up = user_pass.split(":").collect::<Vec<_>>();
284 if up.len() != 2 {
285 eyre::bail!("Could not parse username and password from URL");
286 }
287 let username = up[0].to_string();
288 let password = up[1].to_string();
289 Ok((host_port, Some((username, password))))
290 }
291 _ => {
292 eyre::bail!("Expected zero or one '@' symbols in authority");
293 }
294 }
295}
296
297#[test]
298fn test_parse_example_url() {
299 let uri: http::Uri = "ntrip://hostname.com:2101/mountpoint".parse().unwrap();
300 let my_scheme = http::uri::Scheme::from_str("ntrip").unwrap();
301 assert_eq!(uri.scheme(), Some(&my_scheme));
302 let authority = uri.authority().unwrap();
303 assert_eq!(authority.host(), "hostname.com");
304 assert_eq!(authority.port_u16(), Some(2101));
305 let (host_port, user_pass) = parse_authority(authority).unwrap();
306 assert!(user_pass.is_none());
307 assert_eq!(host_port, "hostname.com:2101");
308 let path_and_query = uri.path_and_query().unwrap();
309 assert_eq!(path_and_query.path(), "/mountpoint");
310}
311
312#[test]
313fn test_parse_example_url_with_user_pass() {
314 let uri: http::Uri = "ntrip://username:password@hostname.com:2101/mountpoint"
315 .parse()
316 .unwrap();
317 let my_scheme = http::uri::Scheme::from_str("ntrip").unwrap();
318 assert_eq!(uri.scheme(), Some(&my_scheme));
319 let authority = uri.authority().unwrap();
320 assert_eq!(authority.host(), "hostname.com");
321 assert_eq!(authority.port_u16(), Some(2101));
322 let (host_port, user_pass) = parse_authority(authority).unwrap();
323 let (username, password) = user_pass.unwrap();
324 assert_eq!(username, "username");
325 assert_eq!(password, "password");
326 assert_eq!(host_port, "hostname.com:2101");
327 let path_and_query = uri.path_and_query().unwrap();
328 assert_eq!(path_and_query.path(), "/mountpoint");
329}
330
331#[cfg(test)]
332#[tokio::test]
333async fn reconnect_retries_a_truncated_first_chunk() {
334 use std::io::{Read, Write};
335
336 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
337 let address = listener.local_addr().unwrap();
338 let server = std::thread::spawn(move || {
339 let responses: [&[u8]; 3] = [
340 b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
341 b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\ninvalid\r\n",
342 b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n0\r\n\r\n",
343 ];
344
345 for response in responses {
346 let (mut stream, _) = listener.accept().unwrap();
347 let mut request = Vec::new();
348 let mut byte = [0_u8; 1];
349 while !request.ends_with(b"\r\n\r\n") {
350 stream.read_exact(&mut byte).unwrap();
351 request.push(byte[0]);
352 }
353 stream.write_all(response).unwrap();
354 }
355 });
356
357 let mut client = RobustNtripClient::new(
358 &format!("http://{address}/mountpoint"),
359 RobustNtripClientOptions {
360 max_backoff_duration: std::time::Duration::ZERO,
361 timeout: None,
362 },
363 )
364 .await
365 .unwrap();
366
367 let chunk = client.reconnect_and_get_first_chunk().await.unwrap();
368 assert_eq!(chunk, "test");
369 server.join().unwrap();
370}
371
372pub struct FrameData {
374 frame_data: bytes::BytesMut,
375 message_number: u16,
376}
377
378impl FrameData {
379 pub fn frame_data(&self) -> &[u8] {
381 &self.frame_data
382 }
383 pub fn message_number(&self) -> u16 {
385 self.message_number
386 }
387}
388
389impl From<FrameData> for Vec<u8> {
390 fn from(val: FrameData) -> Self {
391 val.frame_data.into()
392 }
393}
394
395pub struct ParsingNtripClient {
397 client: RobustNtripClient,
398 buf: bytes::BytesMut,
399}
400
401impl ParsingNtripClient {
402 pub fn new(client: RobustNtripClient) -> Self {
404 let buf = bytes::BytesMut::new();
405 Self { client, buf }
406 }
407
408 pub async fn next(&mut self) -> Result<FrameData> {
410 loop {
411 let mut advance_info = None;
412 for (i, start_byte) in (&self.buf).into_iter().enumerate() {
413 if *start_byte == 0xd3 {
414 match rtcm_rs::MessageFrame::new(&self.buf[i..]) {
415 Ok(m) => {
416 tracing::debug!(
417 "Found RTCM message {} frame with length {}",
418 m.message_number().unwrap(),
419 m.frame_len()
420 );
421 advance_info = Some((
422 i,
423 false,
424 Some((m.frame_len(), m.message_number().unwrap())),
425 ));
426 break;
427 }
428 Err(rtcm_rs::rtcm_error::RtcmError::Incomplete) => {
429 advance_info = Some((i, true, None)); break;
431 }
432 Err(rtcm_rs::rtcm_error::RtcmError::NotValid) => {
433 advance_info = Some((i + 1, false, None)); break;
435 }
436 _ => unreachable!(),
437 }
438 }
439 }
440
441 let (n_discard, do_read_more, msg_info) = if let Some(x) = advance_info {
442 x
443 } else {
444 (self.buf.len(), true, None)
446 };
447
448 let _discard_bytes = self.buf.split_to(n_discard);
449 if let Some((frame_len, message_number)) = msg_info {
450 assert!(!do_read_more);
451 let frame_data = self.buf.split_to(frame_len);
452 return Ok(FrameData {
453 frame_data,
454 message_number,
455 });
456 }
457
458 if do_read_more {
459 let this_buf = self.client.chunk().await?;
461 self.buf.extend_from_slice(&this_buf);
462 }
463 }
464 }
465}