1use std::sync::{Arc, Mutex};
20use std::time::Duration;
21
22use crate::{
23 BlobDownload, BlobUploadGrant, SegmentFetch, SegmentRequest, Transport, TransportError,
24};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum RedirectPolicy {
29 #[default]
34 Deny,
35 Follow,
45}
46
47#[derive(Debug, Clone, Default)]
50pub struct HostTransportPolicy {
51 pub request_timeout: Option<Duration>,
53 pub round_deadline: Option<Duration>,
58 pub max_request_bytes: Option<u64>,
61 pub max_response_bytes: Option<u64>,
64 pub redirects: RedirectPolicy,
65}
66
67impl HostTransportPolicy {
68 pub fn validate(&self) -> Result<(), String> {
72 let now = std::time::Instant::now();
73 if let Some(timeout) = self.request_timeout {
74 if timeout.is_zero() || now.checked_add(timeout).is_none() {
75 return Err(
76 "sync.invalid_request: transport config requestTimeoutMs is out of range"
77 .to_owned(),
78 );
79 }
80 }
81 if let Some(deadline) = self.round_deadline {
82 if deadline.is_zero() || now.checked_add(deadline).is_none() {
83 return Err(
84 "sync.invalid_request: transport config roundDeadlineMs is out of range"
85 .to_owned(),
86 );
87 }
88 }
89 if self.max_request_bytes == Some(0) {
90 return Err(
91 "sync.invalid_request: transport config maxRequestBytes must be a positive integer"
92 .to_owned(),
93 );
94 }
95 if self.max_response_bytes == Some(0) {
96 return Err("sync.invalid_request: transport config maxResponseBytes must be a positive integer".to_owned());
97 }
98 Ok(())
99 }
100
101 pub fn from_config(config: &serde_json::Value) -> Result<Self, String> {
104 let bound = |key: &str, message: &'static str| -> Result<Option<u64>, String> {
105 match config.get(key) {
106 None | Some(serde_json::Value::Null) => Ok(None),
107 Some(value) => match value.as_u64() {
108 Some(number) if number >= 1 => Ok(Some(number)),
109 _ => Err(message.to_owned()),
110 },
111 }
112 };
113 let redirects = match config.get("redirects") {
114 None | Some(serde_json::Value::Null) => RedirectPolicy::Deny,
115 Some(serde_json::Value::String(value)) if value == "deny" => RedirectPolicy::Deny,
116 Some(serde_json::Value::String(value)) if value == "follow" => RedirectPolicy::Follow,
117 Some(_) => return Err(
118 "sync.invalid_request: transport config redirects must be \"deny\" or \"follow\""
119 .to_owned(),
120 ),
121 };
122 let policy = Self {
123 request_timeout: bound(
124 "requestTimeoutMs",
125 "sync.invalid_request: transport config requestTimeoutMs must be a positive integer",
126 )?
127 .map(Duration::from_millis),
128 round_deadline: bound(
129 "roundDeadlineMs",
130 "sync.invalid_request: transport config roundDeadlineMs must be a positive integer",
131 )?
132 .map(Duration::from_millis),
133 max_request_bytes: bound(
134 "maxRequestBytes",
135 "sync.invalid_request: transport config maxRequestBytes must be a positive integer",
136 )?,
137 max_response_bytes: bound(
138 "maxResponseBytes",
139 "sync.invalid_request: transport config maxResponseBytes must be a positive integer",
140 )?,
141 redirects,
142 };
143 policy.validate()?;
144 Ok(policy)
145 }
146}
147
148pub enum Inbound {
150 Text(String),
151 Binary(Vec<u8>),
152 Disconnected(TransportError),
155}
156
157pub struct InboundBuffer {
161 frames: Mutex<Vec<Inbound>>,
162 notify: Option<Arc<dyn Fn() + Send + Sync>>,
163}
164
165impl Default for InboundBuffer {
166 fn default() -> Self {
167 Self {
168 frames: Mutex::new(Vec::new()),
169 notify: None,
170 }
171 }
172}
173
174impl InboundBuffer {
175 pub fn with_notify(notify: Arc<dyn Fn() + Send + Sync>) -> Self {
176 Self {
177 frames: Mutex::new(Vec::new()),
178 notify: Some(notify),
179 }
180 }
181
182 pub fn push(&self, frame: Inbound) {
183 self.frames.lock().expect("inbound lock").push(frame);
184 if let Some(notify) = &self.notify {
185 notify();
186 }
187 }
188 fn take(&self) -> Vec<Inbound> {
189 std::mem::take(&mut *self.frames.lock().expect("inbound lock"))
190 }
191}
192
193#[allow(clippy::large_enum_variant)]
196pub enum HostTransport {
197 Null {
199 signed_urls: bool,
200 inbound: Arc<InboundBuffer>,
201 },
202 #[cfg(feature = "native-transport")]
203 Native(native::NativeTransport),
204}
205
206impl HostTransport {
207 pub fn from_config(config: &serde_json::Value) -> Result<Self, String> {
210 Self::new_from_config(config)
211 }
212
213 pub fn new_from_config(config: &serde_json::Value) -> Result<Self, String> {
216 Self::from_config_with_notify(config, None)
217 }
218
219 pub fn from_config_with_notify(
223 config: &serde_json::Value,
224 notify: Option<Arc<dyn Fn() + Send + Sync>>,
225 ) -> Result<Self, String> {
226 let policy = HostTransportPolicy::from_config(config)?;
227 Self::from_config_with_policy(config, policy, notify)
228 }
229
230 pub fn from_config_with_policy(
233 config: &serde_json::Value,
234 policy: HostTransportPolicy,
235 notify: Option<Arc<dyn Fn() + Send + Sync>>,
236 ) -> Result<Self, String> {
237 #[cfg(feature = "native-transport")]
238 {
239 if let Some(base_url) = config.get("baseUrl").and_then(|v| v.as_str()) {
240 return Ok(HostTransport::Native(native::NativeTransport::with_policy(
241 base_url, config, policy, notify,
242 )?));
243 }
244 }
245 #[cfg(not(feature = "native-transport"))]
246 {
247 policy.validate()?;
248 if config.get("baseUrl").is_some() {
249 return Err(
250 "this build has no native transport (rebuild with --features native-transport)"
251 .to_owned(),
252 );
253 }
254 }
255 Ok(HostTransport::Null {
256 signed_urls: false,
257 inbound: Arc::new(match notify {
258 Some(notify) => InboundBuffer::with_notify(notify),
259 None => InboundBuffer::default(),
260 }),
261 })
262 }
263
264 pub fn fork_round(&self) -> Self {
267 match self {
268 Self::Null {
269 signed_urls,
270 inbound,
271 } => Self::Null {
272 signed_urls: *signed_urls,
273 inbound: inbound.clone(),
274 },
275 #[cfg(feature = "native-transport")]
276 Self::Native(t) => Self::Native(t.fork_round()),
277 }
278 }
279
280 pub fn set_signed_urls(&mut self, value: bool) {
281 match self {
282 HostTransport::Null { signed_urls, .. } => *signed_urls = value,
283 #[cfg(feature = "native-transport")]
284 HostTransport::Native(t) => t.signed_urls = value,
285 }
286 }
287
288 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
291 match self {
292 HostTransport::Null { .. } => drop(headers),
293 #[cfg(feature = "native-transport")]
294 HostTransport::Native(t) => t.set_headers(headers),
295 }
296 }
297
298 pub fn set_policy(&mut self, policy: HostTransportPolicy) -> Result<(), String> {
303 policy.validate()?;
304 match self {
305 HostTransport::Null { .. } => {}
306 #[cfg(feature = "native-transport")]
307 HostTransport::Native(t) => t.set_policy(policy)?,
308 }
309 Ok(())
310 }
311
312 pub fn take_inbound(&mut self) -> Vec<Inbound> {
314 match self {
315 HostTransport::Null { inbound, .. } => inbound.take(),
316 #[cfg(feature = "native-transport")]
317 HostTransport::Native(t) => t.inbound.take(),
318 }
319 }
320
321 pub fn shutdown(&mut self) {
323 match self {
324 HostTransport::Null { .. } => {}
325 #[cfg(feature = "native-transport")]
326 HostTransport::Native(t) => t.shutdown(),
327 }
328 }
329}
330
331fn unavailable(op: &str) -> TransportError {
332 TransportError::new(
333 "transport.unavailable",
334 format!("{op} needs the native transport (build with --features native-transport)"),
335 )
336}
337
338#[cfg_attr(not(feature = "native-transport"), allow(unused_variables))]
341impl Transport for HostTransport {
342 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
343 match self {
344 HostTransport::Null { .. } => Err(unavailable("sync")),
345 #[cfg(feature = "native-transport")]
346 HostTransport::Native(t) => t.sync(request),
347 }
348 }
349
350 fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
351 match self {
352 HostTransport::Null { .. } => Err(unavailable("remoteOperation")),
353 #[cfg(feature = "native-transport")]
354 HostTransport::Native(t) => t.remote_operation(request),
355 }
356 }
357
358 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
359 match self {
360 HostTransport::Null { .. } => Err(unavailable("realtimeSync")),
361 #[cfg(feature = "native-transport")]
362 HostTransport::Native(t) => t.realtime_sync(request),
363 }
364 }
365
366 fn download_segment(
367 &mut self,
368 request: &SegmentRequest,
369 on_progress: &mut dyn FnMut(u64),
370 ) -> Result<Vec<u8>, TransportError> {
371 match self {
372 HostTransport::Null { .. } => Err(unavailable("downloadSegment")),
373 #[cfg(feature = "native-transport")]
374 HostTransport::Native(t) => t.download_segment(request, on_progress),
375 }
376 }
377
378 fn supports_url_fetch(&self) -> bool {
379 match self {
380 HostTransport::Null { signed_urls, .. } => *signed_urls,
381 #[cfg(feature = "native-transport")]
382 HostTransport::Native(t) => t.signed_urls,
383 }
384 }
385
386 fn download_segments(
387 &mut self,
388 fetches: &[SegmentFetch],
389 on_progress: &mut (dyn FnMut(usize, u64) + Send),
390 ) -> Vec<Result<Vec<u8>, TransportError>> {
391 match self {
392 HostTransport::Null { .. } => fetches
393 .first()
394 .map(|_| Err(unavailable("downloadSegments")))
395 .into_iter()
396 .collect(),
397 #[cfg(feature = "native-transport")]
398 HostTransport::Native(t) => t.download_segments(fetches, on_progress),
399 }
400 }
401
402 fn fetch_url(
403 &mut self,
404 url: &str,
405 on_progress: &mut dyn FnMut(u64),
406 ) -> Result<Vec<u8>, TransportError> {
407 match self {
408 HostTransport::Null { .. } => Err(unavailable("fetchUrl")),
409 #[cfg(feature = "native-transport")]
410 HostTransport::Native(t) => t.fetch_url(url, on_progress),
411 }
412 }
413
414 fn blob_upload(
415 &mut self,
416 blob_id: &str,
417 bytes: &[u8],
418 media_type: Option<&str>,
419 ) -> Result<(), TransportError> {
420 match self {
421 HostTransport::Null { .. } => Err(unavailable("blobUpload")),
422 #[cfg(feature = "native-transport")]
423 HostTransport::Native(t) => t.blob_upload(blob_id, bytes, media_type),
424 }
425 }
426
427 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
428 match self {
429 HostTransport::Null { .. } => Err(unavailable("blobDownload")),
430 #[cfg(feature = "native-transport")]
431 HostTransport::Native(t) => t.blob_download(blob_id),
432 }
433 }
434
435 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
436 match self {
437 HostTransport::Null { .. } => Err(unavailable("fetchBlobUrl")),
438 #[cfg(feature = "native-transport")]
439 HostTransport::Native(t) => t.fetch_blob_url(url),
440 }
441 }
442
443 fn blob_upload_grant(
444 &mut self,
445 blob_id: &str,
446 byte_length: u64,
447 media_type: Option<&str>,
448 ) -> Result<BlobUploadGrant, TransportError> {
449 match self {
450 HostTransport::Null { .. } => Ok(BlobUploadGrant::None),
453 #[cfg(feature = "native-transport")]
454 HostTransport::Native(t) => t.blob_upload_grant(blob_id, byte_length, media_type),
455 }
456 }
457
458 fn blob_put_url(
459 &mut self,
460 url: &str,
461 bytes: &[u8],
462 media_type: Option<&str>,
463 ) -> Result<(), TransportError> {
464 match self {
465 HostTransport::Null { .. } => Err(unavailable("blobPutUrl")),
466 #[cfg(feature = "native-transport")]
467 HostTransport::Native(t) => t.blob_put_url(url, bytes, media_type),
468 }
469 }
470
471 fn realtime_connect(&mut self) -> Result<(), TransportError> {
472 match self {
473 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
474 #[cfg(feature = "native-transport")]
475 HostTransport::Native(t) => {
476 t.set_realtime_client_id(None);
477 t.realtime_connect()
478 }
479 }
480 }
481
482 fn realtime_connect_for_client(&mut self, client_id: &str) -> Result<(), TransportError> {
483 match self {
484 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
485 #[cfg(feature = "native-transport")]
486 HostTransport::Native(t) => {
487 t.set_realtime_client_id(Some(client_id));
488 t.realtime_connect()
489 }
490 }
491 }
492
493 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
494 match self {
495 HostTransport::Null { .. } => Err(unavailable("realtimeSend")),
496 #[cfg(feature = "native-transport")]
497 HostTransport::Native(t) => t.realtime_send(text),
498 }
499 }
500
501 fn realtime_close(&mut self) -> Result<(), TransportError> {
502 match self {
503 HostTransport::Null { .. } => Ok(()),
504 #[cfg(feature = "native-transport")]
505 HostTransport::Native(t) => t.realtime_close(),
506 }
507 }
508
509 fn round_deadline(&self) -> Option<std::time::Duration> {
510 match self {
511 HostTransport::Null { .. } => None,
512 #[cfg(feature = "native-transport")]
513 HostTransport::Native(t) => t.round_deadline(),
514 }
515 }
516
517 fn set_round_deadline(&mut self, deadline: Option<std::time::Instant>) {
518 match self {
519 HostTransport::Null { .. } => {}
520 #[cfg(feature = "native-transport")]
521 HostTransport::Native(t) => t.set_round_deadline(deadline),
522 }
523 }
524}
525
526#[cfg(feature = "native-transport")]
527mod native {
528 use std::io::{self, Read, Write};
540 use std::net::{TcpStream, ToSocketAddrs};
541 use std::sync::atomic::{AtomicBool, Ordering};
542 use std::sync::{mpsc, Arc, Condvar, Mutex};
543 use std::thread::JoinHandle;
544 use std::time::{Duration, Instant};
545
546 use polling::{Event, Events, Poller};
547 use tungstenite::stream::MaybeTlsStream;
548 use tungstenite::Message;
549
550 use super::{HostTransportPolicy, Inbound, InboundBuffer, RedirectPolicy};
551 use crate::{
552 BlobDownload, BlobUploadGrant, RealtimeRound, RoundInbound, SegmentFetch, SegmentRequest,
553 Transport, TransportError,
554 };
555
556 struct SocketRegistration {
558 poller: Arc<Poller>,
559 stream: TcpStream,
560 }
561
562 impl Drop for SocketRegistration {
563 fn drop(&mut self) {
564 let _ = self.poller.delete(&self.stream);
565 }
566 }
567
568 #[derive(Clone)]
569 struct SocketSender {
570 queue: mpsc::SyncSender<Outgoing>,
571 poller: Arc<Poller>,
572 }
573
574 struct Outgoing {
575 message: Message,
576 completed: mpsc::Sender<Result<(), TransportError>>,
577 }
578
579 const ROUND_TIMEOUT: Duration = Duration::from_secs(30);
583 #[derive(Default)]
590 pub(super) struct RoundChannel {
591 state: Mutex<RoundState>,
592 ready: Condvar,
593 }
594
595 #[derive(Default)]
596 struct RoundState {
597 round: RealtimeRound,
598 outcome: Option<Result<Vec<u8>, TransportError>>,
600 }
601
602 impl RoundChannel {
603 fn begin(&self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
607 let mut state = self.state.lock().expect("round lock");
608 state.outcome = None;
609 state.round.begin(request)
610 }
611
612 fn route_binary(&self, frame: &[u8]) -> Option<Vec<u8>> {
616 let mut state = self.state.lock().expect("round lock");
617 match state.round.route_binary(frame) {
618 Ok(RoundInbound::Delta(body)) => Some(body),
619 Ok(RoundInbound::RoundProgress) | Ok(RoundInbound::Ignored) => None,
620 Ok(RoundInbound::RoundComplete(bytes)) => {
621 state.outcome = Some(Ok(bytes));
622 self.ready.notify_all();
623 None
624 }
625 Err(error) => {
626 state.outcome = Some(Err(error));
627 self.ready.notify_all();
628 None
629 }
630 }
631 }
632
633 fn fail_in_flight(&self, error: TransportError) {
635 let mut state = self.state.lock().expect("round lock");
636 if state.round.in_flight() && state.outcome.is_none() {
637 state.round.abort();
638 state.outcome = Some(Err(error));
639 self.ready.notify_all();
640 }
641 }
642
643 fn wait(
647 &self,
648 round_deadline: Option<std::time::Instant>,
649 ) -> Result<Vec<u8>, TransportError> {
650 let mut state = self.state.lock().expect("round lock");
651 let hard = std::time::Instant::now() + ROUND_TIMEOUT;
652 let round_binding = round_deadline.is_some_and(|round| round < hard);
653 let deadline = match round_deadline {
654 Some(round) if round_binding => round,
655 _ => hard,
656 };
657 while state.outcome.is_none() {
658 let now = std::time::Instant::now();
659 if now >= deadline {
660 state.round.abort();
661 return Err(if round_binding {
662 timeout_error("sync round deadline exceeded", "round_deadline")
663 } else {
664 TransportError::new(
665 "sync.transport_failed",
666 "realtime sync round timed out (§8.7)",
667 )
668 });
669 }
670 let (guard, _timeout) = self
671 .ready
672 .wait_timeout(state, deadline - now)
673 .expect("round wait");
674 state = guard;
675 }
676 state.outcome.take().expect("outcome present")
677 }
678 }
679
680 fn transfer_error(
681 message: &'static str,
682 cause_kind: &'static str,
683 http_status: Option<u16>,
684 ) -> TransportError {
685 let mut details = serde_json::json!({ "causeKind": cause_kind });
686 if let Some(status) = http_status {
687 details["httpStatus"] = status.into();
688 }
689 let mut error = TransportError::new("transport.failed", message);
690 error.details = Some(details);
691 error
692 }
693
694 fn timeout_error(message: &'static str, cause_kind: &'static str) -> TransportError {
697 let mut error = TransportError::new("transport.timeout", message);
698 error.details = Some(serde_json::json!({ "causeKind": cause_kind }));
699 error
700 }
701
702 fn redirect_error(message: &'static str, http_status: Option<u16>) -> TransportError {
704 let mut details = serde_json::json!({ "causeKind": "redirect" });
705 if let Some(status) = http_status {
706 details["httpStatus"] = status.into();
707 }
708 let mut error = TransportError::new("transport.redirect", message);
709 error.details = Some(details);
710 error
711 }
712
713 fn redirect_refused(resp: &ureq::http::Response<ureq::Body>) -> Result<(), TransportError> {
718 let status = resp.status().as_u16();
719 if (300..400).contains(&status) {
720 return Err(redirect_error("redirect refused", Some(status)));
721 }
722 Ok(())
723 }
724
725 fn is_timeout_io(error: &std::io::Error) -> bool {
731 if error.kind() == std::io::ErrorKind::TimedOut {
732 return true;
733 }
734 let Some(inner) = error.get_ref() else {
735 return false;
736 };
737 let Some(ureq_error) = inner.downcast_ref::<ureq::Error>() else {
738 return false;
739 };
740 match ureq_error {
741 ureq::Error::Timeout(_) => true,
742 ureq::Error::Io(nested) => is_timeout_io(nested),
743 ureq::Error::Decompress(_, nested) => is_timeout_io(nested),
744 _ => false,
745 }
746 }
747
748 fn http_err(message: &'static str, error: ureq::Error) -> TransportError {
749 let (kind, status) = match error {
750 ureq::Error::StatusCode(status) if (300..400).contains(&status) => {
751 return redirect_error(message, Some(status))
752 }
753 ureq::Error::StatusCode(status) => ("status", Some(status)),
754 ureq::Error::Timeout(_) => return timeout_error(message, "timeout"),
755 ureq::Error::Io(error) => return io_err(message, error),
756 ureq::Error::HostNotFound
757 | ureq::Error::ConnectionFailed
758 | ureq::Error::ConnectProxyFailed(_) => ("connect", None),
759 ureq::Error::Tls(_)
760 | ureq::Error::Rustls(_)
761 | ureq::Error::Pem(_)
762 | ureq::Error::TlsRequired => ("tls", None),
763 ureq::Error::BadUri(_)
764 | ureq::Error::Http(_)
765 | ureq::Error::RequireHttpsOnly(_)
766 | ureq::Error::InvalidProxyUrl => ("request", None),
767 ureq::Error::Protocol(_) => ("protocol", None),
768 ureq::Error::BodyExceedsLimit(_)
769 | ureq::Error::BodyStalled
770 | ureq::Error::Decompress(_, _) => ("body", None),
771 ureq::Error::RedirectFailed | ureq::Error::TooManyRedirects => {
772 return redirect_error(message, None)
773 }
774 _ => ("unknown", None),
775 };
776 transfer_error(message, kind, status)
777 }
778
779 fn io_err(message: &'static str, error: std::io::Error) -> TransportError {
780 if is_timeout_io(&error) {
781 return timeout_error(message, "timeout");
782 }
783 let kind = match error.kind() {
784 std::io::ErrorKind::ConnectionRefused
785 | std::io::ErrorKind::ConnectionAborted
786 | std::io::ErrorKind::ConnectionReset
787 | std::io::ErrorKind::NotConnected => "connect",
788 _ => "io",
789 };
790 transfer_error(message, kind, None)
791 }
792
793 fn ws_err(message: &'static str, error: tungstenite::Error) -> TransportError {
794 match error {
795 tungstenite::Error::Io(error) => io_err(message, error),
796 tungstenite::Error::Tls(_) => transfer_error(message, "tls", None),
797 tungstenite::Error::Http(response) if response.status().is_redirection() => {
800 redirect_error(message, Some(response.status().as_u16()))
801 }
802 tungstenite::Error::Http(response) => {
803 transfer_error(message, "status", Some(response.status().as_u16()))
804 }
805 tungstenite::Error::Url(_) | tungstenite::Error::HttpFormat(_) => {
806 transfer_error(message, "request", None)
807 }
808 _ => transfer_error(message, "protocol", None),
809 }
810 }
811
812 const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
813
814 fn remaining(deadline: Instant) -> io::Result<Duration> {
815 deadline
816 .checked_duration_since(Instant::now())
817 .filter(|left| !left.is_zero())
818 .ok_or_else(|| {
819 io::Error::new(
820 io::ErrorKind::TimedOut,
821 "websocket establishment deadline exceeded",
822 )
823 })
824 }
825
826 struct EstablishingStream {
827 socket: TcpStream,
828 deadline: Option<Instant>,
829 }
830
831 impl EstablishingStream {
832 fn timeout(&self) -> io::Result<Option<Duration>> {
833 self.deadline.map(remaining).transpose()
834 }
835 fn established(&mut self) -> io::Result<TcpStream> {
836 self.deadline = None;
837 self.socket.set_read_timeout(None)?;
838 self.socket.set_write_timeout(None)?;
839 self.socket.try_clone()
840 }
841 }
842
843 fn establishment_io<T>(result: io::Result<T>, establishing: bool) -> io::Result<T> {
844 result.map_err(|error| {
845 if establishing
846 && matches!(
847 error.kind(),
848 io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
849 )
850 {
851 io::Error::new(
852 io::ErrorKind::TimedOut,
853 "websocket establishment deadline exceeded",
854 )
855 } else {
856 error
857 }
858 })
859 }
860
861 impl Read for EstablishingStream {
862 fn read(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
863 if self.deadline.is_some() {
864 self.socket.set_read_timeout(self.timeout()?)?;
865 }
866 establishment_io(self.socket.read(bytes), self.deadline.is_some())
867 }
868 }
869 impl Write for EstablishingStream {
870 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
871 if self.deadline.is_some() {
872 self.socket.set_write_timeout(self.timeout()?)?;
873 }
874 establishment_io(self.socket.write(bytes), self.deadline.is_some())
875 }
876 fn flush(&mut self) -> io::Result<()> {
877 if self.deadline.is_some() {
878 self.socket.set_write_timeout(self.timeout()?)?;
879 }
880 establishment_io(self.socket.flush(), self.deadline.is_some())
881 }
882 }
883
884 fn connect_websocket(
885 request: tungstenite::handshake::client::Request,
886 deadline: Instant,
887 ) -> Result<tungstenite::WebSocket<MaybeTlsStream<EstablishingStream>>, TransportError> {
888 let host = request
889 .uri()
890 .host()
891 .ok_or_else(|| transfer_error("realtime URL invalid", "request", None))?;
892 let host = host.trim_start_matches('[').trim_end_matches(']');
893 let port = match request.uri().scheme_str() {
894 Some("ws") => request.uri().port_u16().unwrap_or(80),
895 Some("wss") => request.uri().port_u16().unwrap_or(443),
896 _ => return Err(transfer_error("realtime URL invalid", "request", None)),
897 };
898 let host = host.to_owned();
901 let (resolved, addresses) = mpsc::sync_channel(1);
902 std::thread::Builder::new()
903 .name("syncular-dns".to_owned())
904 .spawn(move || {
905 let result = (host.as_str(), port)
906 .to_socket_addrs()
907 .map(|items| items.collect::<Vec<_>>());
908 let _ = resolved.send(result);
909 })
910 .map_err(|e| io_err("realtime DNS failed", e))?;
911 let addresses = addresses
912 .recv_timeout(remaining(deadline).map_err(|e| io_err("realtime DNS failed", e))?)
913 .map_err(|e| match e {
914 mpsc::RecvTimeoutError::Timeout => {
915 timeout_error("realtime DNS timed out", "timeout")
916 }
917 mpsc::RecvTimeoutError::Disconnected => {
918 transfer_error("realtime DNS failed", "io", None)
919 }
920 })?
921 .map_err(|e| io_err("realtime DNS failed", e))?;
922 let mut last_error = io::Error::new(io::ErrorKind::NotFound, "DNS returned no addresses");
923 let mut socket = None;
924 for address in addresses {
925 match TcpStream::connect_timeout(
926 &address,
927 remaining(deadline).map_err(|e| io_err("realtime TCP connect failed", e))?,
928 ) {
929 Ok(stream) => {
930 socket = Some(stream);
931 break;
932 }
933 Err(error) => last_error = error,
934 }
935 }
936 let socket = socket.ok_or_else(|| io_err("realtime TCP connect failed", last_error))?;
937 socket
938 .set_nodelay(true)
939 .map_err(|e| io_err("realtime TCP connect failed", e))?;
940 let stream = EstablishingStream {
941 socket,
942 deadline: Some(deadline),
943 };
944 let (ws, _) = tungstenite::client_tls_with_config(request, stream, None, None).map_err(
947 |e| match e {
948 tungstenite::HandshakeError::Failure(error) => {
949 ws_err("realtime connection failed", error)
950 }
951 tungstenite::HandshakeError::Interrupted(_) => {
954 transfer_error("realtime connection failed", "protocol", None)
955 }
956 },
957 )?;
958 remaining(deadline).map_err(|e| io_err("realtime connection failed", e))?;
959 Ok(ws)
960 }
961
962 #[cfg(test)]
963 mod establishment_tests {
964 use super::*;
965 use std::net::{Shutdown, TcpListener};
966 use tungstenite::client::IntoClientRequest;
967
968 fn withheld_response(tls: bool, old: bool) {
971 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
972 let url = format!(
973 "{}://{}/realtime",
974 if tls { "wss" } else { "ws" },
975 listener.local_addr().unwrap()
976 );
977 let (accepted_tx, accepted_rx) = mpsc::channel();
978 let (release_tx, release_rx) = mpsc::channel();
979 let peer = std::thread::spawn(move || {
980 let (mut stream, _) = listener.accept().unwrap();
981 stream
982 .set_read_timeout(Some(Duration::from_secs(2)))
983 .unwrap();
984 let mut bytes = [0; 4096];
985 assert!(stream.read(&mut bytes).unwrap() > 0);
986 accepted_tx.send(()).unwrap();
987 release_rx.recv_timeout(Duration::from_secs(3)).unwrap();
988 let _ = stream.shutdown(Shutdown::Both);
989 });
990 let (done_tx, done_rx) = mpsc::channel();
991 let worker = std::thread::spawn(move || {
992 let result = if old {
993 tungstenite::connect(url)
994 .map(|_| ())
995 .map_err(|e| e.to_string())
996 } else {
997 connect_websocket(
998 url.into_client_request().unwrap(),
999 Instant::now() + Duration::from_millis(250),
1000 )
1001 .map(|_| ())
1002 .map_err(|e| format!("{}: {}", e.code, e.message))
1003 };
1004 done_tx.send(result).unwrap();
1005 });
1006 accepted_rx.recv_timeout(Duration::from_secs(2)).unwrap();
1007 if old {
1008 assert!(
1009 matches!(
1010 done_rx.recv_timeout(Duration::from_millis(500)),
1011 Err(mpsc::RecvTimeoutError::Timeout)
1012 ),
1013 "original connect unexpectedly had a deadline"
1014 );
1015 release_tx.send(()).unwrap();
1016 assert!(done_rx
1017 .recv_timeout(Duration::from_secs(2))
1018 .unwrap()
1019 .is_err());
1020 } else {
1021 let error = done_rx
1022 .recv_timeout(Duration::from_secs(2))
1023 .expect("establishment remained blocked")
1024 .unwrap_err();
1025 assert_eq!(error, "transport.timeout: realtime connection failed");
1026 eprintln!(
1027 "withheld {} produces {error}",
1028 if tls { "TLS" } else { "upgrade" }
1029 );
1030 release_tx.send(()).unwrap();
1031 }
1032 worker.join().unwrap();
1033 peer.join().unwrap();
1034 }
1035
1036 #[test]
1037 fn original_connect_blocks_on_withheld_upgrade_and_tls() {
1038 withheld_response(false, true);
1039 withheld_response(true, true);
1040 }
1041 #[test]
1042 fn establishment_deadline_errors_on_withheld_upgrade_and_tls() {
1043 withheld_response(false, false);
1044 withheld_response(true, false);
1045 }
1046
1047 #[test]
1048 fn slow_upgrade_bytes_cannot_renew_the_absolute_deadline() {
1049 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1050 let url = format!("ws://{}", listener.local_addr().unwrap());
1051 let peer = std::thread::spawn(move || {
1052 let (mut stream, _) = listener.accept().unwrap();
1053 let mut bytes = [0; 4096];
1054 let _ = stream.read(&mut bytes).unwrap();
1055 for byte in b"HTTP/1.1 101 Switching Protocols\r\n" {
1056 if stream.write_all(&[*byte]).is_err() {
1057 break;
1058 }
1059 std::thread::sleep(Duration::from_millis(40));
1060 }
1061 });
1062 let started = Instant::now();
1063 assert!(connect_websocket(
1064 url.into_client_request().unwrap(),
1065 started + Duration::from_millis(250)
1066 )
1067 .is_err());
1068 assert!(started.elapsed() < Duration::from_secs(1));
1069 peer.join().unwrap();
1070 }
1071
1072 #[test]
1073 fn saturated_tcp_accept_queue_returns_an_observable_error() {
1074 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1077 let address = listener.local_addr().unwrap();
1078 let mut queued = Vec::new();
1079 loop {
1080 match TcpStream::connect_timeout(&address, Duration::from_millis(50)) {
1081 Ok(stream) => queued.push(stream),
1082 Err(error) => {
1083 assert!(
1084 matches!(
1085 error.kind(),
1086 io::ErrorKind::TimedOut
1087 | io::ErrorKind::WouldBlock
1088 | io::ErrorKind::ConnectionRefused
1089 | io::ErrorKind::ConnectionReset
1090 ),
1091 "{error:?}"
1092 );
1093 break;
1094 }
1095 }
1096 assert!(queued.len() < 4096, "listener queue did not saturate");
1097 }
1098 let started = Instant::now();
1099 let url = format!("ws://{address}");
1100 let error = match connect_websocket(
1101 url.into_client_request().unwrap(),
1102 started + Duration::from_millis(250),
1103 ) {
1104 Err(error) => error,
1105 Ok(_) => panic!("full accept queue unexpectedly connected"),
1106 };
1107 assert!(
1108 (error.code == "transport.timeout" || error.code == "transport.failed")
1109 && (error.message == "realtime TCP connect failed"
1110 || error.message == "realtime connection failed"),
1111 "{}: {}",
1112 error.code,
1113 error.message
1114 );
1115 assert!(started.elapsed() < Duration::from_secs(2));
1116 }
1117 }
1118
1119 fn segment_error(mut error: TransportError) -> TransportError {
1123 if error.code == "transport.failed" {
1124 error.code = "sync.transport_failed".into();
1125 error.message = "segment transfer failed".into();
1126 }
1127 error
1128 }
1129
1130 fn is_would_block(e: &tungstenite::Error) -> bool {
1132 matches!(
1133 e,
1134 tungstenite::Error::Io(io) if matches!(
1135 io.kind(),
1136 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
1137 )
1138 )
1139 }
1140
1141 pub struct NativeTransport {
1142 base_url: String,
1143 ws_url: String,
1144 headers: Vec<(String, String)>,
1146 base_url_has_userinfo: bool,
1150 agent: ureq::Agent,
1151 policy: HostTransportPolicy,
1152 round_deadline_at: Option<std::time::Instant>,
1156 pub signed_urls: bool,
1157 pub inbound: Arc<InboundBuffer>,
1158 outgoing: Option<Box<SocketSender>>,
1160 reader: Option<JoinHandle<()>>,
1161 reader_stop: Arc<AtomicBool>,
1162 round: Arc<RoundChannel>,
1164 realtime_client_id: Option<String>,
1165 }
1166
1167 fn build_agent(policy: &HostTransportPolicy) -> ureq::Agent {
1168 ureq::Agent::config_builder()
1169 .timeout_global(policy.request_timeout)
1170 .max_redirects(match policy.redirects {
1171 RedirectPolicy::Deny => 0,
1172 RedirectPolicy::Follow => 10,
1173 })
1174 .max_redirects_will_error(matches!(policy.redirects, RedirectPolicy::Follow))
1175 .redirect_auth_headers(ureq::config::RedirectAuthHeaders::Never)
1176 .build()
1177 .into()
1178 }
1179
1180 fn derive_ws_url(base_url: &str) -> String {
1181 let ws = if let Some(rest) = base_url.strip_prefix("https://") {
1184 format!("wss://{rest}")
1185 } else if let Some(rest) = base_url.strip_prefix("http://") {
1186 format!("ws://{rest}")
1187 } else {
1188 base_url.to_owned()
1189 };
1190 let trimmed = ws.trim_end_matches('/');
1191 format!("{trimmed}/realtime")
1192 }
1193
1194 impl NativeTransport {
1195 pub fn fork_round(&self) -> Self {
1196 Self {
1197 base_url: self.base_url.clone(),
1198 ws_url: self.ws_url.clone(),
1199 headers: self.headers.clone(),
1200 base_url_has_userinfo: self.base_url_has_userinfo,
1201 agent: self.agent.clone(),
1202 policy: self.policy.clone(),
1203 round_deadline_at: None,
1204 signed_urls: self.signed_urls,
1205 inbound: self.inbound.clone(),
1206 outgoing: self.outgoing.clone(),
1207 reader: None,
1208 reader_stop: self.reader_stop.clone(),
1209 round: self.round.clone(),
1210 realtime_client_id: self.realtime_client_id.clone(),
1211 }
1212 }
1213
1214 pub fn new(
1215 base_url: &str,
1216 config: &serde_json::Value,
1217 notify: Option<Arc<dyn Fn() + Send + Sync>>,
1218 ) -> Result<Self, String> {
1219 let policy = HostTransportPolicy::from_config(config)?;
1220 Self::with_policy(base_url, config, policy, notify)
1221 }
1222
1223 pub fn with_policy(
1224 base_url: &str,
1225 config: &serde_json::Value,
1226 policy: HostTransportPolicy,
1227 notify: Option<Arc<dyn Fn() + Send + Sync>>,
1228 ) -> Result<Self, String> {
1229 policy.validate()?;
1230 let mut headers = Vec::new();
1231 if let Some(map) = config.get("headers").and_then(|v| v.as_object()) {
1232 for (k, v) in map {
1233 if let Some(s) = v.as_str() {
1234 headers.push((k.clone(), s.to_owned()));
1235 }
1236 }
1237 }
1238 let ws_url = config
1239 .get("wsUrl")
1240 .and_then(|v| v.as_str())
1241 .map(str::to_owned)
1242 .unwrap_or_else(|| derive_ws_url(base_url));
1243 let base_url_has_userinfo = url::Url::parse(base_url)
1246 .map(|url| !url.username().is_empty() || url.password().is_some())
1247 .unwrap_or(false);
1248 Ok(NativeTransport {
1249 base_url: base_url.trim_end_matches('/').to_owned(),
1250 ws_url,
1251 headers,
1252 base_url_has_userinfo,
1253 agent: build_agent(&policy),
1254 policy,
1255 round_deadline_at: None,
1256 signed_urls: false,
1257 inbound: Arc::new(match notify {
1258 Some(notify) => InboundBuffer::with_notify(notify),
1259 None => InboundBuffer::default(),
1260 }),
1261 outgoing: None,
1262 reader: None,
1263 reader_stop: Arc::new(AtomicBool::new(false)),
1264 round: Arc::new(RoundChannel::default()),
1265 realtime_client_id: None,
1266 })
1267 }
1268
1269 pub fn set_policy(&mut self, policy: HostTransportPolicy) -> Result<(), String> {
1273 policy.validate()?;
1274 self.agent = build_agent(&policy);
1275 self.policy = policy;
1276 Ok(())
1277 }
1278
1279 pub fn round_deadline(&self) -> Option<Duration> {
1280 self.policy.round_deadline
1281 }
1282
1283 pub fn set_round_deadline(&mut self, deadline: Option<std::time::Instant>) {
1284 self.round_deadline_at = deadline;
1285 }
1286
1287 fn effective_timeout(&self) -> Result<Option<Duration>, TransportError> {
1291 let remaining = match self.round_deadline_at {
1292 Some(deadline) => {
1293 match deadline.checked_duration_since(std::time::Instant::now()) {
1294 Some(remaining) => Some(remaining),
1295 None => {
1296 return Err(timeout_error(
1297 "sync round deadline exceeded",
1298 "round_deadline",
1299 ))
1300 }
1301 }
1302 }
1303 None => None,
1304 };
1305 Ok(match (self.policy.request_timeout, remaining) {
1306 (Some(request), Some(round)) => Some(request.min(round)),
1307 (Some(request), None) => Some(request),
1308 (None, Some(round)) => Some(round),
1309 (None, None) => None,
1310 })
1311 }
1312
1313 fn credentialed(&self) -> bool {
1316 !self.headers.is_empty() || self.base_url_has_userinfo
1317 }
1318
1319 fn finish_request<B>(
1325 &self,
1326 req: ureq::RequestBuilder<B>,
1327 credentialed: bool,
1328 ) -> Result<ureq::RequestBuilder<B>, TransportError> {
1329 let timeout = self.effective_timeout()?;
1330 let follow = self.policy.redirects == RedirectPolicy::Follow && !credentialed;
1331 Ok(req
1332 .config()
1333 .timeout_global(timeout)
1334 .max_redirects(if follow { 10 } else { 0 })
1335 .max_redirects_will_error(true)
1336 .build())
1337 }
1338
1339 fn check_request_bytes(&self, len: u64) -> Result<(), TransportError> {
1341 match self.policy.max_request_bytes {
1342 Some(max) if len > max => {
1343 let mut error = TransportError::new(
1344 "transport.request_too_large",
1345 "request body exceeds the configured transport limit",
1346 );
1347 error.details = Some(serde_json::json!({ "limit": max, "size": len }));
1348 Err(error)
1349 }
1350 _ => Ok(()),
1351 }
1352 }
1353
1354 fn post_sync(&self, path: &str, body: &[u8]) -> Result<Vec<u8>, TransportError> {
1355 self.check_request_bytes(body.len() as u64)?;
1356 let credentialed = self.credentialed();
1357 let url = format!("{}{}", self.base_url, path);
1358 let mut req = self
1361 .agent
1362 .post(&url)
1363 .header("content-type", "application/vnd.syncular.sync.v2");
1364 for (k, v) in &self.headers {
1365 req = req.header(k.as_str(), v.as_str());
1366 }
1367 let req = self.finish_request(req, credentialed)?;
1368 let resp = req
1369 .send(body)
1370 .map_err(|e| http_err("sync request failed", e))?;
1371 read_body(resp, None, self.policy.max_response_bytes)
1372 }
1373
1374 fn post_operation(&self, body: &[u8]) -> Result<Vec<u8>, TransportError> {
1375 self.check_request_bytes(body.len() as u64)?;
1376 let credentialed = self.credentialed();
1377 let url = format!("{}/operations", self.base_url);
1378 let mut req = self.agent.post(&url).header(
1379 "content-type",
1380 "application/vnd.syncular.operations.v1+json",
1381 );
1382 for (key, value) in &self.headers {
1383 req = req.header(key.as_str(), value.as_str());
1384 }
1385 let req = self.finish_request(req, credentialed)?;
1386 let response = req
1387 .send(body)
1388 .map_err(|error| http_err("remote operation request failed", error))?;
1389 read_body(response, None, self.policy.max_response_bytes)
1390 }
1391
1392 fn get_bytes(&self, url: &str, with_headers: bool) -> Result<Vec<u8>, TransportError> {
1393 let mut req = self.agent.get(url);
1396 if with_headers {
1397 for (k, v) in &self.headers {
1398 req = req.header(k.as_str(), v.as_str());
1399 }
1400 }
1401 let req = self.finish_request(req, true)?;
1402 let resp = req.call().map_err(|e| http_err("URL fetch failed", e))?;
1403 read_body(resp, None, self.policy.max_response_bytes)
1404 }
1405
1406 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
1407 self.headers = headers;
1408 }
1409
1410 pub fn set_realtime_client_id(&mut self, client_id: Option<&str>) {
1411 self.realtime_client_id = client_id.map(str::to_owned);
1412 }
1413
1414 pub fn shutdown(&mut self) {
1415 self.reader_stop.store(true, Ordering::SeqCst);
1416 self.round.fail_in_flight(TransportError::new(
1419 "sync.transport_failed",
1420 "realtime disconnected mid-round (§8.7)",
1421 ));
1422 if let Some(outgoing) = &self.outgoing {
1423 let _ = outgoing.poller.notify();
1424 }
1425 if let Some(handle) = self.reader.take() {
1426 let _ = handle.join();
1427 }
1428 self.outgoing = None;
1429 }
1430
1431 fn send_message(&mut self, message: Message) -> Result<(), TransportError> {
1432 let (wait, round_bound) = match self.round_deadline_at {
1437 Some(deadline) => {
1438 match deadline.checked_duration_since(std::time::Instant::now()) {
1439 Some(remaining) if remaining < ROUND_TIMEOUT => (remaining, true),
1440 Some(_) => (ROUND_TIMEOUT, false),
1441 None => {
1444 return Err(timeout_error("realtime send timed out", "round_deadline"));
1445 }
1446 }
1447 }
1448 None => (ROUND_TIMEOUT, false),
1449 };
1450 let result = (|| {
1451 let Some(outgoing) = &self.outgoing else {
1452 return Err(TransportError::new(
1453 "transport.failed",
1454 "realtime not connected",
1455 ));
1456 };
1457 let (completed, result) = mpsc::channel();
1458 outgoing
1459 .queue
1460 .try_send(Outgoing { message, completed })
1461 .map_err(|_| {
1462 TransportError::new("transport.failed", "realtime send queue unavailable")
1463 })?;
1464 outgoing
1465 .poller
1466 .notify()
1467 .map_err(|e| io_err("realtime wake failed", e))?;
1468 result.recv_timeout(wait).map_err(|_| {
1469 if round_bound {
1470 timeout_error("realtime send timed out", "round_deadline")
1471 } else {
1472 TransportError::new("transport.failed", "realtime send did not complete")
1473 }
1474 })?
1475 })();
1476 if let Err(error) = &result {
1479 self.shutdown();
1480 self.inbound.push(Inbound::Disconnected(error.clone()));
1483 }
1484 result
1485 }
1486 }
1487
1488 fn read_body(
1489 resp: ureq::http::Response<ureq::Body>,
1490 mut progress: Option<&mut dyn FnMut(u64)>,
1491 max: Option<u64>,
1492 ) -> Result<Vec<u8>, TransportError> {
1493 redirect_refused(&resp)?;
1494 use std::io::Read;
1495 let status = resp.status().as_u16();
1496 let mut reader = resp.into_body().into_with_config().reader();
1497 let mut bytes = Vec::new();
1498 let mut chunk = [0u8; 64 * 1024];
1499 let mut reported = 0;
1500 loop {
1501 let n = reader.read(&mut chunk).map_err(|error| {
1502 if is_timeout_io(&error) {
1503 timeout_error("response body read times out", "timeout")
1504 } else {
1505 transfer_error("response body read failed", "body", Some(status))
1506 }
1507 })?;
1508 if n == 0 {
1509 break;
1510 }
1511 if let Some(max) = max {
1514 if bytes.len() as u64 + n as u64 > max {
1515 return Err(response_too_large(max));
1516 }
1517 }
1518 bytes.extend_from_slice(&chunk[..n]);
1519 if bytes.len() - reported >= 64 * 1024 {
1520 if let Some(callback) = progress.as_mut() {
1521 callback(bytes.len() as u64);
1522 }
1523 reported = bytes.len();
1524 }
1525 }
1526 if bytes.len() != reported {
1527 if let Some(callback) = progress {
1528 callback(bytes.len() as u64);
1529 }
1530 }
1531 Ok(bytes)
1532 }
1533
1534 fn response_too_large(max: u64) -> TransportError {
1535 let mut error = TransportError::new(
1536 "transport.response_too_large",
1537 "response body exceeds the configured transport limit",
1538 );
1539 error.details = Some(serde_json::json!({ "limit": max }));
1540 error
1541 }
1542
1543 impl Transport for NativeTransport {
1544 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
1545 self.post_sync("/sync", request)
1546 }
1547
1548 fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
1549 self.post_operation(request)
1550 }
1551
1552 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
1553 if self.outgoing.is_none() {
1563 return self.post_sync("/sync", request);
1564 }
1565 self.effective_timeout()?;
1566 let framed = self.round.begin(request)?;
1567 if let Err(error) = self.send_message(Message::Binary(framed.into())) {
1568 self.round.fail_in_flight(error);
1569 }
1570 self.round.wait(self.round_deadline_at)
1571 }
1572
1573 fn download_segment(
1574 &mut self,
1575 request: &SegmentRequest,
1576 on_progress: &mut dyn FnMut(u64),
1577 ) -> Result<Vec<u8>, TransportError> {
1578 let url = format!("{}/segments/{}", self.base_url, request.segment_id);
1585 let credentialed = self.credentialed();
1586 let mut req = self
1587 .agent
1588 .get(&url)
1589 .header("x-syncular-scopes", &request.requested_scopes_json);
1590 for (k, v) in &self.headers {
1591 req = req.header(k.as_str(), v.as_str());
1592 }
1593 let req = self.finish_request(req, credentialed)?;
1594 let resp = req
1595 .call()
1596 .map_err(|error| segment_error(http_err("segment request failed", error)))?;
1597 read_body(resp, Some(on_progress), self.policy.max_response_bytes)
1598 .map_err(segment_error)
1599 }
1600
1601 fn supports_url_fetch(&self) -> bool {
1602 self.signed_urls
1603 }
1604
1605 fn download_segments(
1606 &mut self,
1607 fetches: &[SegmentFetch],
1608 on_progress: &mut (dyn FnMut(usize, u64) + Send),
1609 ) -> Vec<Result<Vec<u8>, TransportError>> {
1610 let workers = (0..fetches.len().min(4))
1619 .map(|_| {
1620 let mut worker = self.fork_round();
1621 worker.round_deadline_at = self.round_deadline_at;
1622 worker
1623 })
1624 .collect::<Vec<_>>();
1625 let slots = fetches.iter().map(|_| Mutex::new(None)).collect::<Vec<_>>();
1626 let progress = Mutex::new(on_progress);
1627 let next = std::sync::atomic::AtomicUsize::new(0);
1628 std::thread::scope(|scope| {
1629 for worker in workers {
1630 let (slots, progress, next) = (&slots, &progress, &next);
1631 scope.spawn(move || {
1632 let mut worker = worker;
1633 loop {
1634 let index = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1635 let Some(fetch) = fetches.get(index) else {
1636 break;
1637 };
1638 let mut report = |bytes: u64| {
1639 (*progress.lock().expect("segment progress lock"))(index, bytes);
1640 };
1641 let result = match fetch {
1642 SegmentFetch::Direct(request) => {
1643 worker.download_segment(request, &mut report)
1644 }
1645 SegmentFetch::Url(url) => worker.fetch_url(url, &mut report),
1646 };
1647 *slots[index].lock().expect("segment result slot") = Some(result);
1648 }
1649 });
1650 }
1651 });
1652 slots
1653 .into_iter()
1654 .map(|slot| slot.into_inner().expect("segment result slot"))
1655 .map(|result| result.expect("every fetch is attempted"))
1656 .collect()
1657 }
1658
1659 fn fetch_url(
1660 &mut self,
1661 url: &str,
1662 on_progress: &mut dyn FnMut(u64),
1663 ) -> Result<Vec<u8>, TransportError> {
1664 let req = self.finish_request(self.agent.get(url), true)?;
1666 let resp = req
1667 .call()
1668 .map_err(|error| segment_error(http_err("segment request failed", error)))?;
1669 read_body(resp, Some(on_progress), self.policy.max_response_bytes)
1670 .map_err(segment_error)
1671 }
1672
1673 fn blob_upload(
1674 &mut self,
1675 blob_id: &str,
1676 bytes: &[u8],
1677 media_type: Option<&str>,
1678 ) -> Result<(), TransportError> {
1679 self.check_request_bytes(bytes.len() as u64)?;
1682 let credentialed = self.credentialed();
1683 let url = format!("{}/blobs/{}", self.base_url, blob_id);
1684 let mut req = self.agent.put(&url).header(
1685 "content-type",
1686 media_type.unwrap_or("application/octet-stream"),
1687 );
1688 for (k, v) in &self.headers {
1689 req = req.header(k.as_str(), v.as_str());
1690 }
1691 let req = self.finish_request(req, credentialed)?;
1692 let resp = req
1693 .send(bytes)
1694 .map_err(|e| http_err("blob upload failed", e))?;
1695 redirect_refused(&resp)?;
1696 Ok(())
1697 }
1698
1699 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
1700 let credentialed = self.credentialed();
1703 let url = format!("{}/blobs/{}", self.base_url, blob_id);
1704 let mut req = self.agent.get(&url);
1705 for (k, v) in &self.headers {
1706 req = req.header(k.as_str(), v.as_str());
1707 }
1708 let req = self.finish_request(req, credentialed)?;
1709 let resp = req.call().map_err(|cause| {
1710 let status = match &cause {
1711 ureq::Error::StatusCode(status) => Some(*status),
1712 _ => None,
1713 };
1714 let mut error = http_err("blob download failed", cause);
1715 match status {
1716 Some(404) => {
1717 error.code = "blob.not_found".into();
1718 error.message = "blob not found".into();
1719 }
1720 Some(403) => {
1721 error.code = "blob.forbidden".into();
1722 error.message = "blob download forbidden".into();
1723 }
1724 Some(401) => {
1725 error.code = "sync.auth_required".into();
1726 error.message = "host authentication required".into();
1727 }
1728 _ => {}
1729 }
1730 error
1731 })?;
1732 let is_json = resp
1735 .headers()
1736 .get(ureq::http::header::CONTENT_TYPE)
1737 .and_then(|value| value.to_str().ok())
1738 .is_some_and(|value| value.contains("application/json"));
1739 let body = read_body(resp, None, self.policy.max_response_bytes)?;
1740 if is_json {
1741 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&body) {
1742 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
1743 return Ok(BlobDownload::Url {
1744 url: u.to_owned(),
1745 url_expires_at_ms: parsed
1746 .get("urlExpiresAtMs")
1747 .and_then(|v| v.as_i64()),
1748 });
1749 }
1750 }
1751 }
1752 Ok(BlobDownload::Bytes(body))
1753 }
1754
1755 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
1756 self.get_bytes(url, false)
1758 }
1759
1760 fn blob_upload_grant(
1761 &mut self,
1762 blob_id: &str,
1763 byte_length: u64,
1764 media_type: Option<&str>,
1765 ) -> Result<BlobUploadGrant, TransportError> {
1766 let url = format!("{}/blobs/{}/upload-grant", self.base_url, blob_id);
1767 let body = serde_json::json!({
1768 "byteLength": byte_length,
1769 "mediaType": media_type,
1770 })
1771 .to_string();
1772 self.check_request_bytes(body.len() as u64)?;
1773 let credentialed = self.credentialed();
1774 let mut req = self
1775 .agent
1776 .post(&url)
1777 .header("content-type", "application/json");
1778 for (k, v) in &self.headers {
1779 req = req.header(k.as_str(), v.as_str());
1780 }
1781 let req = self.finish_request(req, credentialed)?;
1782 let resp = req
1783 .send(body)
1784 .map_err(|e| http_err("blob upload grant request failed", e))?;
1785 let grant_body = read_body(resp, None, self.policy.max_response_bytes)?;
1786 let parsed: serde_json::Value = serde_json::from_slice(&grant_body)
1787 .map_err(|_| transfer_error("blob upload grant decode failed", "decode", None))?;
1788 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
1789 return Ok(BlobUploadGrant::Url {
1790 url: u.to_owned(),
1791 url_expires_at_ms: parsed.get("urlExpiresAtMs").and_then(|v| v.as_i64()),
1792 });
1793 }
1794 if parsed.get("present").and_then(|v| v.as_bool()) == Some(true) {
1795 return Ok(BlobUploadGrant::Present);
1796 }
1797 Ok(BlobUploadGrant::None)
1798 }
1799
1800 fn blob_put_url(
1801 &mut self,
1802 url: &str,
1803 bytes: &[u8],
1804 media_type: Option<&str>,
1805 ) -> Result<(), TransportError> {
1806 self.check_request_bytes(bytes.len() as u64)?;
1808 let req = self.finish_request(
1809 self.agent.put(url).header(
1810 "content-type",
1811 media_type.unwrap_or("application/octet-stream"),
1812 ),
1813 true,
1814 )?;
1815 let resp = req
1816 .send(bytes)
1817 .map_err(|e| http_err("signed blob upload failed", e))?;
1818 redirect_refused(&resp)?;
1819 Ok(())
1820 }
1821
1822 fn realtime_connect(&mut self) -> Result<(), TransportError> {
1823 if self.outgoing.is_some() {
1824 if self.reader_stop.load(Ordering::SeqCst)
1825 || self
1826 .reader
1827 .as_ref()
1828 .is_some_and(|reader| reader.is_finished())
1829 {
1830 self.shutdown();
1831 } else {
1832 return Ok(());
1833 }
1834 }
1835 use tungstenite::client::IntoClientRequest;
1841 let mut url = url::Url::parse(&self.ws_url)
1842 .map_err(|_| transfer_error("realtime URL invalid", "request", None))?;
1843 if let Some(client_id) = self.realtime_client_id.as_deref() {
1844 let retained_query: Vec<(String, String)> = url
1845 .query_pairs()
1846 .filter(|(key, _)| key != "clientId")
1847 .map(|(key, value)| (key.into_owned(), value.into_owned()))
1848 .collect();
1849 url.set_query(None);
1850 url.query_pairs_mut()
1851 .extend_pairs(retained_query)
1852 .append_pair("clientId", client_id);
1853 }
1854 let mut request = url
1855 .as_str()
1856 .into_client_request()
1857 .map_err(|error| ws_err("realtime request failed", error))?;
1858 {
1859 let out = request.headers_mut();
1860 for (k, v) in &self.headers {
1861 if let (Ok(name), Ok(value)) = (
1862 tungstenite::http::HeaderName::try_from(k.as_str()),
1863 tungstenite::http::HeaderValue::try_from(v.as_str()),
1864 ) {
1865 out.insert(name, value);
1866 }
1867 }
1868 }
1869 if self.policy.redirects == RedirectPolicy::Follow {
1872 return Err(redirect_error(
1873 "realtime establishment does not follow redirects",
1874 None,
1875 ));
1876 }
1877 let mut ws = connect_websocket(request, Instant::now() + CONNECT_TIMEOUT)?;
1878 let stream = match ws.get_mut() {
1879 MaybeTlsStream::Plain(stream) => stream.established(),
1880 MaybeTlsStream::Rustls(stream) => stream.get_mut().established(),
1881 _ => {
1882 return Err(TransportError::new(
1883 "transport.failed",
1884 "unsupported websocket stream",
1885 ))
1886 }
1887 }
1888 .map_err(|e| io_err("realtime clone failed", e))?;
1889 stream
1890 .set_nonblocking(true)
1891 .map_err(|e| io_err("realtime nonblocking failed", e))?;
1892 let poller = Arc::new(Poller::new().map_err(|e| io_err("realtime poller failed", e))?);
1893 unsafe { poller.add(&stream, Event::readable(0)) }
1895 .map_err(|e| io_err("realtime register failed", e))?;
1896 let registration = SocketRegistration {
1897 poller: Arc::clone(&poller),
1898 stream,
1899 };
1900 let (outgoing, queued) = mpsc::sync_channel::<Outgoing>(1);
1901 self.outgoing = Some(Box::new(SocketSender {
1902 queue: outgoing,
1903 poller,
1904 }));
1905 self.reader_stop = Arc::new(AtomicBool::new(false));
1906 self.round = Arc::new(RoundChannel::default());
1907 let inbound = Arc::clone(&self.inbound);
1908 let stop = Arc::clone(&self.reader_stop);
1909 let round = Arc::clone(&self.round);
1910 self.reader = Some(std::thread::spawn(move || {
1911 let mut events = Events::new();
1912 let mut pending: Option<mpsc::Sender<Result<(), TransportError>>> = None;
1913 let failure = 'io: loop {
1914 if stop.load(Ordering::SeqCst) {
1915 let _ = ws.close(None);
1916 let _ = ws.flush();
1917 break TransportError::new(
1918 "sync.transport_failed",
1919 "realtime disconnected mid-round (§8.7)",
1920 );
1921 }
1922 if pending.is_none() {
1923 if let Ok(outgoing) = queued.try_recv() {
1924 pending = Some(outgoing.completed);
1925 if let Err(error) = ws.write(outgoing.message) {
1927 if !is_would_block(&error) {
1928 break ws_err("realtime write failed", error);
1929 }
1930 }
1931 }
1932 }
1933 let mut drained = false;
1935 for _ in 0..64 {
1936 match ws.read() {
1937 Ok(Message::Text(text)) => {
1938 inbound.push(Inbound::Text(text.to_string()))
1939 }
1940 Ok(Message::Binary(bytes)) => {
1941 if let Some(delta) = round.route_binary(&bytes) {
1942 inbound.push(Inbound::Binary(delta));
1943 }
1944 }
1945 Err(error) if is_would_block(&error) => {
1946 drained = true;
1947 break;
1948 }
1949 Ok(Message::Close(_)) => {
1950 let _ = ws.flush();
1951 break 'io TransportError::new(
1952 "sync.transport_failed",
1953 "realtime disconnected mid-round (§8.7)",
1954 );
1955 }
1956 Err(_) => {
1957 break 'io TransportError::new(
1958 "sync.transport_failed",
1959 "realtime disconnected mid-round (§8.7)",
1960 );
1961 }
1962 Ok(_) => {}
1963 }
1964 }
1965 let interest = match ws.flush() {
1966 Ok(()) => {
1967 if let Some(completed) = pending.take() {
1968 let _ = completed.send(Ok(()));
1969 }
1970 Event::readable(0)
1971 }
1972 Err(error) if is_would_block(&error) => Event::all(0),
1973 Err(error) => break ws_err("realtime flush failed", error),
1974 };
1975 if !drained {
1976 continue;
1977 }
1978 if let Err(error) = registration.poller.modify(®istration.stream, interest) {
1979 break io_err("realtime rearm failed", error);
1980 }
1981 events.clear();
1982 if let Err(error) = registration.poller.wait(&mut events, None) {
1983 if error.kind() != std::io::ErrorKind::Interrupted {
1984 break io_err("realtime wait failed", error);
1985 }
1986 }
1987 };
1988 if !stop.load(Ordering::SeqCst) {
1993 inbound.push(Inbound::Disconnected(failure.clone()));
1994 }
1995 if let Some(completed) = pending {
1996 let _ = completed.send(Err(failure.clone()));
1997 }
1998 while let Ok(outgoing) = queued.try_recv() {
1999 let _ = outgoing.completed.send(Err(failure.clone()));
2000 }
2001 round.fail_in_flight(failure);
2002 }));
2003 Ok(())
2004 }
2005
2006 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
2007 self.send_message(Message::Text(text.to_owned().into()))
2008 }
2009
2010 fn realtime_close(&mut self) -> Result<(), TransportError> {
2011 self.shutdown();
2012 Ok(())
2013 }
2014
2015 fn round_deadline(&self) -> Option<Duration> {
2016 NativeTransport::round_deadline(self)
2017 }
2018
2019 fn set_round_deadline(&mut self, deadline: Option<std::time::Instant>) {
2020 NativeTransport::set_round_deadline(self, deadline);
2021 }
2022 }
2023 #[cfg(test)]
2024 mod transfer_tests {
2025 use super::*;
2026 use ureq::unversioned::resolver::DefaultResolver;
2027 use ureq::unversioned::transport::{ConnectionDetails, Connector};
2028
2029 const SECRET: &str = "https://user:password@cdn.example/secret-capability-path?secret-query=value#secret-fragment";
2030
2031 #[derive(Debug)]
2032 struct SecretError;
2033 impl std::fmt::Display for SecretError {
2034 fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2035 panic!("transport error must never be formatted")
2036 }
2037 }
2038 impl std::error::Error for SecretError {}
2039
2040 #[derive(Debug)]
2041 struct FailingConnector(&'static str);
2042 impl Connector for FailingConnector {
2043 type Out = ();
2044 fn connect(
2045 &self,
2046 _: &ConnectionDetails,
2047 _: Option<()>,
2048 ) -> Result<Option<()>, ureq::Error> {
2049 Err(match self.0 {
2050 "io" => ureq::Error::Io(std::io::Error::new(
2051 std::io::ErrorKind::ConnectionRefused,
2052 format!("404 {SECRET}"),
2053 )),
2054 "tls" => ureq::Error::Tls(SECRET),
2055 "request" => ureq::Error::BadUri(SECRET.into()),
2056 "timeout" => ureq::Error::Timeout(ureq::Timeout::Global),
2057 "404" => ureq::Error::StatusCode(404),
2058 "401" => ureq::Error::StatusCode(401),
2059 "403" => ureq::Error::StatusCode(403),
2060 "503" => ureq::Error::StatusCode(503),
2061 _ => ureq::Error::Other(Box::new(SecretError)),
2062 })
2063 }
2064 }
2065
2066 #[test]
2067 fn injected_http_failures_never_expose_capabilities_or_format_the_cause() {
2068 for kind in [
2069 "io", "tls", "request", "timeout", "unknown", "404", "401", "403", "503",
2070 ] {
2071 for operation in [
2072 "sync",
2073 "remote",
2074 "segment",
2075 "signed_segment",
2076 "blob",
2077 "signed_blob",
2078 "upload",
2079 "grant",
2080 "signed_put",
2081 ] {
2082 let mut transport =
2083 NativeTransport::new("http://127.0.0.1", &serde_json::json!({}), None)
2084 .unwrap();
2085 transport.agent = ureq::Agent::with_parts(
2086 ureq::Agent::config_builder().build(),
2087 FailingConnector(kind),
2088 DefaultResolver::default(),
2089 );
2090 let mut progress = |_| {};
2091 let result = match operation {
2092 "sync" => transport.sync(&[]).map(|_| ()),
2093 "remote" => transport.remote_operation(&[]).map(|_| ()),
2094 "segment" => transport.download_segment(&SegmentRequest { segment_id: "sha256:test".into(), table: "tasks".into(), requested_scopes_json: "{}".into() }, &mut progress).map(|_| ()),
2095 "signed_segment" => transport.fetch_url("http://127.0.0.1/secret-capability-path?secret-query=value#secret-fragment", &mut progress).map(|_| ()),
2096 "blob" => transport.blob_download("sha256:test").map(|_| ()),
2097 "signed_blob" => transport.fetch_blob_url("http://127.0.0.1/secret-capability-path?secret-query=value#secret-fragment").map(|_| ()),
2098 "upload" => transport.blob_upload("sha256:test", &[], None),
2099 "grant" => transport.blob_upload_grant("sha256:test", 0, None).map(|_| ()),
2100 _ => transport.blob_put_url("http://127.0.0.1/secret-capability-path?secret-query=value#secret-fragment", &[], None),
2101 };
2102 let error = result.unwrap_err();
2103 let expected_code = match (operation, kind) {
2104 (_, "timeout") => "transport.timeout",
2105 ("segment" | "signed_segment", _) => "sync.transport_failed",
2106 ("blob", "404") => "blob.not_found",
2107 ("blob", "401") => "sync.auth_required",
2108 ("blob", "403") => "blob.forbidden",
2109 _ => "transport.failed",
2110 };
2111 assert_eq!(error.code, expected_code, "{operation} {kind}");
2112 let details = error.details.unwrap();
2113 assert_eq!(
2114 details["causeKind"],
2115 match kind {
2116 "io" => "connect",
2117 "404" | "401" | "403" | "503" => "status",
2118 other => other,
2119 }
2120 );
2121 assert_eq!(
2122 details.get("httpStatus").and_then(|value| value.as_u64()),
2123 kind.parse::<u64>().ok()
2124 );
2125 let rendered = format!("{}{}", error.message, details);
2126 for secret in [
2127 "http://",
2128 "https://",
2129 "password",
2130 "secret-capability-path",
2131 "secret-query",
2132 "secret-fragment",
2133 ] {
2134 assert!(!rendered.contains(secret), "{operation} {kind}");
2135 }
2136 assert!(details.get("causeMessage").is_none());
2137 assert!(details.get("path").is_none());
2138 }
2139 }
2140 }
2141
2142 #[test]
2143 fn injected_body_and_websocket_errors_do_not_expose_payloads_or_source_chains() {
2144 struct FailingRead;
2145 impl std::io::Read for FailingRead {
2146 fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
2147 Err(std::io::Error::other(SECRET))
2148 }
2149 }
2150 let response = ureq::http::Response::builder()
2151 .status(200)
2152 .body(ureq::Body::builder().reader(FailingRead))
2153 .unwrap();
2154 let error = segment_error(read_body(response, None, None).unwrap_err());
2155 assert_eq!(error.code, "sync.transport_failed");
2156 assert_eq!(error.message, "segment transfer failed");
2157 assert_eq!(
2158 error.details,
2159 Some(serde_json::json!({ "causeKind": "body", "httpStatus": 200 }))
2160 );
2161 for cause in [
2162 tungstenite::Error::Io(std::io::Error::other(SecretError)),
2163 tungstenite::Error::Http(Box::new(
2164 tungstenite::http::Response::builder()
2165 .status(403)
2166 .header("location", SECRET)
2167 .body(Some(SECRET.as_bytes().to_vec()))
2168 .unwrap(),
2169 )),
2170 ] {
2171 let error = ws_err("realtime connection failed", cause);
2172 assert_eq!(error.message, "realtime connection failed");
2173 let details = error.details.unwrap();
2174 assert!(details.get("path").is_none());
2175 assert!(details.get("causeMessage").is_none());
2176 assert!(!details.to_string().contains("secret"));
2177 }
2178 }
2179
2180 #[test]
2181 fn body_read_timeouts_are_classified_through_ureq_wrapping() {
2182 let direct = ureq::Error::Timeout(ureq::Timeout::Global).into_io();
2184 assert!(is_timeout_io(&direct));
2185 let wrapped = ureq::Error::Decompress(
2187 "gzip",
2188 ureq::Error::Timeout(ureq::Timeout::Global).into_io(),
2189 )
2190 .into_io();
2191 assert!(is_timeout_io(&wrapped));
2192 assert!(is_timeout_io(&std::io::Error::new(
2194 std::io::ErrorKind::TimedOut,
2195 "plain"
2196 )));
2197 assert!(!is_timeout_io(&std::io::Error::other("other")));
2198 assert!(!is_timeout_io(&ureq::Error::StatusCode(500).into_io()));
2199 }
2200
2201 #[test]
2202 fn the_configured_request_timeout_is_imposed_on_each_call() {
2203 #[derive(Debug)]
2204 #[allow(clippy::type_complexity)]
2205 struct TimeoutRecording(Arc<Mutex<Vec<(Option<Duration>, bool)>>>);
2206 impl Connector for TimeoutRecording {
2207 type Out = ();
2208 fn connect(
2209 &self,
2210 details: &ConnectionDetails,
2211 _: Option<()>,
2212 ) -> Result<Option<()>, ureq::Error> {
2213 self.0
2214 .lock()
2215 .unwrap()
2216 .push((details.config.timeouts().global, details.request_level));
2217 Err(ureq::Error::ConnectionFailed)
2218 }
2219 }
2220 let seen = Arc::new(Mutex::new(Vec::new()));
2221 let mut transport =
2222 NativeTransport::new("http://127.0.0.1", &serde_json::json!({}), None).unwrap();
2223 transport
2224 .set_policy(HostTransportPolicy {
2225 request_timeout: Some(Duration::from_millis(1234)),
2226 ..Default::default()
2227 })
2228 .unwrap();
2229 transport.agent = ureq::Agent::with_parts(
2230 ureq::Agent::config_builder().build(),
2231 TimeoutRecording(Arc::clone(&seen)),
2232 DefaultResolver::default(),
2233 );
2234 let _ = transport.blob_upload("sha256:test", &[0u8; 4], None);
2235 let seen = seen.lock().unwrap();
2236 assert_eq!(seen.len(), 1);
2237 assert_eq!(
2238 seen[0],
2239 (Some(Duration::from_millis(1234)), true),
2240 "the request-level config carries the policy timeout"
2241 );
2242 }
2243
2244 #[test]
2245 fn body_read_wrapped_timeouts_surface_the_typed_code() {
2246 struct TimeoutRead(bool);
2247 impl std::io::Read for TimeoutRead {
2248 fn read(&mut self, _: &mut [u8]) -> std::io::Result<usize> {
2249 Err(if self.0 {
2250 ureq::Error::Timeout(ureq::Timeout::Global).into_io()
2251 } else {
2252 ureq::Error::Decompress(
2253 "gzip",
2254 ureq::Error::Timeout(ureq::Timeout::Global).into_io(),
2255 )
2256 .into_io()
2257 })
2258 }
2259 }
2260 for compressed in [false, true] {
2261 let response = ureq::http::Response::builder()
2262 .status(200)
2263 .body(ureq::Body::builder().reader(TimeoutRead(compressed)))
2264 .unwrap();
2265 let error = read_body(response, None, None).unwrap_err();
2266 assert_eq!(error.code, "transport.timeout", "{compressed}");
2267 assert_eq!(
2268 error.details,
2269 Some(serde_json::json!({ "causeKind": "timeout" }))
2270 );
2271 }
2272 }
2273
2274 #[test]
2275 fn policy_validation_rejects_zero_and_unrepresentable_bounds() {
2276 assert_eq!(
2277 HostTransportPolicy {
2278 request_timeout: Some(Duration::ZERO),
2279 ..Default::default()
2280 }
2281 .validate()
2282 .unwrap_err(),
2283 "sync.invalid_request: transport config requestTimeoutMs is out of range"
2284 );
2285 assert_eq!(
2286 HostTransportPolicy {
2287 round_deadline: Some(Duration::MAX),
2288 ..Default::default()
2289 }
2290 .validate()
2291 .unwrap_err(),
2292 "sync.invalid_request: transport config roundDeadlineMs is out of range"
2293 );
2294 assert!(HostTransportPolicy {
2295 max_request_bytes: Some(0),
2296 ..Default::default()
2297 }
2298 .validate()
2299 .is_err());
2300 assert!(HostTransportPolicy {
2301 max_response_bytes: Some(0),
2302 ..Default::default()
2303 }
2304 .validate()
2305 .is_err());
2306 assert!(HostTransportPolicy::default().validate().is_ok());
2309 assert_eq!(
2310 HostTransportPolicy::from_config(&serde_json::json!({ "requestTimeoutMs": 0 }))
2311 .unwrap_err(),
2312 "sync.invalid_request: transport config requestTimeoutMs must be a positive integer"
2313 );
2314 }
2315
2316 #[test]
2317 fn set_policy_rejects_invalid_and_preserves_the_current_policy() {
2318 let mut transport =
2319 NativeTransport::new("http://127.0.0.1", &serde_json::json!({}), None).unwrap();
2320 assert_eq!(transport.policy.max_response_bytes, None);
2321 let error = transport
2322 .set_policy(HostTransportPolicy {
2323 max_response_bytes: Some(0),
2324 ..Default::default()
2325 })
2326 .unwrap_err();
2327 assert_eq!(
2328 error,
2329 "sync.invalid_request: transport config maxResponseBytes must be a positive integer"
2330 );
2331 assert_eq!(transport.policy.max_response_bytes, None);
2332 }
2333
2334 #[test]
2335 fn an_expired_round_deadline_is_typed_and_cleared_for_the_next_round() {
2336 let mut transport =
2337 NativeTransport::new("http://127.0.0.1:9", &serde_json::json!({}), None).unwrap();
2338 transport.set_round_deadline(Some(std::time::Instant::now() - Duration::from_secs(1)));
2339 let error = transport.blob_download("sha256:test").unwrap_err();
2340 assert_eq!(error.code, "transport.timeout");
2341 assert_eq!(
2342 error.details,
2343 Some(serde_json::json!({ "causeKind": "round_deadline" }))
2344 );
2345 transport.set_round_deadline(None);
2347 assert!(transport.effective_timeout().is_ok());
2348 }
2349
2350 #[test]
2351 fn an_expired_round_deadline_refuses_before_queuing_a_realtime_send() {
2352 let mut transport =
2353 NativeTransport::new("http://127.0.0.1:9", &serde_json::json!({}), None).unwrap();
2354 transport.set_round_deadline(Some(std::time::Instant::now() - Duration::from_secs(1)));
2355 let error = transport.realtime_send("blocked").unwrap_err();
2359 assert_eq!(error.code, "transport.timeout");
2360 assert_eq!(
2361 error.details,
2362 Some(serde_json::json!({ "causeKind": "round_deadline" }))
2363 );
2364 transport.set_round_deadline(None);
2367 let error = transport.realtime_send("blocked").unwrap_err();
2368 assert_eq!(error.code, "transport.failed");
2369 }
2370
2371 #[test]
2372 fn an_expired_round_deadline_bounds_the_socket_wait() {
2373 let channel = RoundChannel::default();
2374 let error = channel
2375 .wait(Some(std::time::Instant::now() - Duration::from_secs(1)))
2376 .unwrap_err();
2377 assert_eq!(error.code, "transport.timeout");
2378 assert_eq!(
2379 error.details,
2380 Some(serde_json::json!({ "causeKind": "round_deadline" }))
2381 );
2382 }
2383 }
2384}
2385
2386#[cfg(all(test, feature = "native-transport"))]
2387mod tests {
2388 use super::*;
2389 use std::io::{BufRead, BufReader, Write};
2390
2391 #[test]
2392 fn detached_round_uses_the_owned_socket_and_shutdown_releases_it() {
2393 use ssp2::model::{Frame, Message, MsgKind};
2394 use std::sync::mpsc::channel;
2395 for cancelled in [false, true] {
2396 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2397 let base = format!("http://{}", listener.local_addr().unwrap());
2398 let (entered_tx, entered_rx) = channel();
2399 let (release_tx, release_rx) = channel();
2400 let server = std::thread::spawn(move || {
2401 let (socket, _) = listener.accept().unwrap();
2402 let mut socket = tungstenite::accept(socket).unwrap();
2403 let request = socket.read().unwrap().into_data();
2404 assert_eq!(request[0], crate::REALTIME_TAG_ROUND);
2405 assert_eq!(
2406 ssp2::decode_message(&request[1..]).unwrap().msg_kind,
2407 MsgKind::Request
2408 );
2409 entered_tx.send(()).unwrap();
2410 release_rx.recv().unwrap();
2411 if !cancelled {
2412 let response = ssp2::encode_message(&Message {
2413 wire_version: ssp2::decode::WIRE_VERSION,
2414 msg_kind: MsgKind::Response,
2415 frames: vec![Frame::RespHeader {
2416 required_schema_version: None,
2417 latest_schema_version: None,
2418 log_epoch: Some("epoch".into()),
2419 reset_required: Some(true),
2420 }],
2421 });
2422 let mut chunk = vec![crate::REALTIME_TAG_ROUND];
2423 chunk.extend(response);
2424 socket
2425 .send(tungstenite::Message::Binary(chunk.into()))
2426 .unwrap();
2427 }
2428 });
2429 let mut owner =
2430 HostTransport::new_from_config(&serde_json::json!({"baseUrl":base})).unwrap();
2431 owner.realtime_connect_for_client("detached-round").unwrap();
2432 let mut detached = owner.fork_round();
2433 let network = std::thread::spawn(move || {
2434 let request = ssp2::encode_message(&Message {
2435 wire_version: ssp2::decode::WIRE_VERSION,
2436 msg_kind: MsgKind::Request,
2437 frames: vec![
2438 Frame::ReqHeader {
2439 client_id: "detached-round".into(),
2440 schema_version: 1,
2441 log_epoch: None,
2442 },
2443 Frame::PullHeader {
2444 limit_commits: 0,
2445 limit_snapshot_rows: 0,
2446 max_snapshot_pages: 0,
2447 accept: 3,
2448 },
2449 ],
2450 });
2451 detached.realtime_sync(&request)
2452 });
2453 entered_rx.recv().unwrap();
2454 owner.set_headers(vec![("authorization".into(), "Bearer rotated".into())]);
2455 assert!(owner.take_inbound().is_empty());
2456 if cancelled {
2457 owner.shutdown();
2458 }
2459 release_tx.send(()).unwrap();
2460 let response = network.join().unwrap();
2461 if cancelled {
2462 assert!(response.is_err());
2463 } else {
2464 assert_eq!(
2465 ssp2::decode_message(&response.unwrap()).unwrap().msg_kind,
2466 MsgKind::Response
2467 );
2468 owner.shutdown();
2469 }
2470 server.join().unwrap();
2471 }
2472 }
2473
2474 #[test]
2475 fn segment_http_failures_have_named_codes_and_safe_cause_details() {
2476 for signed in [false, true] {
2477 for body_failure in [false, true] {
2478 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2479 let base = format!("http://{}", listener.local_addr().unwrap());
2480 let server = std::thread::spawn(move || {
2481 let (mut socket, _) = listener.accept().unwrap();
2482 let mut reader = BufReader::new(socket.try_clone().unwrap());
2483 loop {
2484 let mut line = String::new();
2485 reader.read_line(&mut line).unwrap();
2486 if line == "\r\n" {
2487 break;
2488 }
2489 assert!(!line.is_empty());
2490 }
2491 socket.write_all(if body_failure {
2492 b"HTTP/1.1 200 OK\r\nContent-Length: 999\r\nConnection: close\r\n\r\nshort"
2493 } else {
2494 b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
2495 }).unwrap();
2496 });
2497 let mut transport =
2498 HostTransport::new_from_config(&serde_json::json!({"baseUrl": base})).unwrap();
2499 let mut progress = |_| {};
2500 let error = if signed {
2501 transport.fetch_url(&format!("{base}/signed?signature=secret"), &mut progress)
2502 } else {
2503 transport.download_segment(
2504 &SegmentRequest {
2505 segment_id: "sha256:test".into(),
2506 table: "tasks".into(),
2507 requested_scopes_json: "{}".into(),
2508 },
2509 &mut progress,
2510 )
2511 }
2512 .unwrap_err();
2513 assert_eq!(error.code, "sync.transport_failed");
2514 assert_eq!(error.message, "segment transfer failed");
2515 let details = error.details.unwrap();
2516 assert!(details.get("path").is_none());
2517 assert!(details.get("causeMessage").is_none());
2518 assert_eq!(
2519 details["causeKind"],
2520 if body_failure { "body" } else { "status" }
2521 );
2522 assert_eq!(details["httpStatus"], if body_failure { 200 } else { 403 });
2523 server.join().unwrap();
2524 }
2525 }
2526 }
2527
2528 #[test]
2529 fn blob_status_classification_uses_the_actual_http_response() {
2530 for (status, expected) in [
2531 (404, "blob.not_found"),
2532 (401, "sync.auth_required"),
2533 (403, "blob.forbidden"),
2534 (500, "transport.failed"),
2535 ] {
2536 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2537 let base = format!("http://{}", listener.local_addr().unwrap());
2538 let server = std::thread::spawn(move || {
2539 let (mut socket, _) = listener.accept().unwrap();
2540 let mut reader = BufReader::new(socket.try_clone().unwrap());
2541 loop {
2542 let mut line = String::new();
2543 reader.read_line(&mut line).unwrap();
2544 if line == "\r\n" {
2545 break;
2546 }
2547 assert!(!line.is_empty());
2548 }
2549 let body = r#"{"code":"blob.not_found","message":"https://host/secret-path?secret-query#secret-fragment","retryable":false}"#;
2550 write!(socket, "HTTP/1.1 {status} Error\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).unwrap();
2551 });
2552 let mut transport =
2553 HostTransport::new_from_config(&serde_json::json!({"baseUrl":base})).unwrap();
2554 let error = transport.blob_download("sha256:test").unwrap_err();
2555 assert_eq!(error.code, expected);
2556 assert_eq!(
2557 error.details,
2558 Some(serde_json::json!({ "causeKind":"status", "httpStatus":status }))
2559 );
2560 assert!(!error.message.contains("secret"));
2561 assert!(!error.message.contains("http"));
2562 server.join().unwrap();
2563 }
2564 }
2565
2566 #[test]
2567 fn refused_segment_connection_has_a_named_code_without_an_invented_status() {
2568 for signed in [false, true] {
2569 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2570 let base = format!("http://{}", listener.local_addr().unwrap());
2571 drop(listener);
2572 let mut transport =
2573 HostTransport::new_from_config(&serde_json::json!({"baseUrl": base})).unwrap();
2574 let mut progress = |_| {};
2575 let error = if signed {
2576 transport.fetch_url(&format!("{base}/signed?signature=secret"), &mut progress)
2577 } else {
2578 transport.download_segment(
2579 &SegmentRequest {
2580 segment_id: "sha256:test".into(),
2581 table: "tasks".into(),
2582 requested_scopes_json: "{}".into(),
2583 },
2584 &mut progress,
2585 )
2586 }
2587 .unwrap_err();
2588 assert_eq!(error.code, "sync.transport_failed");
2589 assert!(error.details.as_ref().unwrap().get("httpStatus").is_none());
2590 }
2591 }
2592
2593 #[test]
2594 fn segment_http_reports_intermediate_bytes_for_direct_and_signed_urls() {
2595 for signed in [false, true] {
2596 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2597 let base = format!("http://{}", listener.local_addr().unwrap());
2598 let server = std::thread::spawn(move || {
2599 let (mut socket, _) = listener.accept().unwrap();
2600 let mut reader = BufReader::new(socket.try_clone().unwrap());
2601 let mut headers = String::new();
2602 loop {
2603 let mut line = String::new();
2604 reader.read_line(&mut line).unwrap();
2605 if line == "\r\n" {
2606 break;
2607 }
2608 assert!(!line.is_empty());
2609 headers.push_str(&line.to_ascii_lowercase());
2610 }
2611 assert_eq!(headers.contains("authorization: bearer test"), !signed);
2612 assert_eq!(headers.contains("x-syncular-scopes:"), !signed);
2613 let bytes = vec![7; 128 * 1024 + 17];
2614 write!(
2615 socket,
2616 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
2617 bytes.len()
2618 )
2619 .unwrap();
2620 socket.write_all(&bytes).unwrap();
2621 });
2622 let mut transport = HostTransport::from_config(
2623 &serde_json::json!({"baseUrl": base, "headers": {"authorization": "Bearer test"}}),
2624 )
2625 .unwrap();
2626 let mut updates = Vec::new();
2627 let mut progress = |bytes| updates.push(bytes);
2628 let bytes = if signed {
2629 transport
2630 .fetch_url(&format!("{base}/signed"), &mut progress)
2631 .unwrap()
2632 } else {
2633 transport
2634 .download_segment(
2635 &SegmentRequest {
2636 segment_id: "sha256:test".into(),
2637 table: "tasks".into(),
2638 requested_scopes_json: "{}".into(),
2639 },
2640 &mut progress,
2641 )
2642 .unwrap()
2643 };
2644 assert_eq!(bytes.len(), 128 * 1024 + 17);
2645 assert!(updates.iter().any(|n| *n > 0 && *n < bytes.len() as u64));
2646 assert_eq!(updates.last(), Some(&(bytes.len() as u64)));
2647 assert!(updates.windows(2).all(|pair| pair[0] < pair[1]));
2648 server.join().unwrap();
2649 }
2650 }
2651
2652 #[test]
2653 fn request_over_cap_is_refused_before_network() {
2654 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2655 listener.set_nonblocking(true).unwrap();
2656 let base = format!("http://{}", listener.local_addr().unwrap());
2657 let mut transport =
2658 HostTransport::from_config(&serde_json::json!({"baseUrl": base, "maxRequestBytes": 4}))
2659 .unwrap();
2660 let error = transport
2661 .blob_upload("sha256:test", &[0u8; 8], None)
2662 .unwrap_err();
2663 assert_eq!(error.code, "transport.request_too_large");
2664 assert_eq!(
2665 error.details,
2666 Some(serde_json::json!({ "limit": 4, "size": 8 }))
2667 );
2668 assert!(
2669 matches!(listener.accept(), Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock),
2670 "the request cap is checked before any connection"
2671 );
2672 }
2673
2674 #[test]
2675 fn credential_bearing_redirects_are_refused_without_contacting_the_destination() {
2676 for credentials in ["headers", "userinfo", "password"] {
2677 for redirects in ["deny", "follow"] {
2678 let destination = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2679 destination.set_nonblocking(true).unwrap();
2680 let location = format!("http://{}/landed", destination.local_addr().unwrap());
2681 let origin = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2682 let host = origin.local_addr().unwrap();
2683 let (base, headers) = match credentials {
2684 "headers" => (
2685 format!("http://{host}"),
2686 serde_json::json!({"authorization": "Bearer secret"}),
2687 ),
2688 "userinfo" => (format!("http://user:secret@{host}"), serde_json::json!({})),
2689 _ => (format!("http://:secret@{host}"), serde_json::json!({})),
2690 };
2691 let server = std::thread::spawn(move || {
2692 let (mut socket, _) = origin.accept().unwrap();
2693 let mut reader = BufReader::new(socket.try_clone().unwrap());
2694 loop {
2695 let mut line = String::new();
2696 reader.read_line(&mut line).unwrap();
2697 if line == "\r\n" {
2698 break;
2699 }
2700 }
2701 let _ = write!(
2702 socket,
2703 "HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
2704 );
2705 });
2706 let mut transport = HostTransport::from_config(&serde_json::json!({
2707 "baseUrl": base,
2708 "redirects": redirects,
2709 "headers": headers,
2710 }))
2711 .unwrap();
2712 let error = transport.blob_download("sha256:test").unwrap_err();
2713 assert_eq!(
2714 error.code, "transport.redirect",
2715 "{credentials} {redirects}"
2716 );
2717 let details = error.details.unwrap();
2718 assert_eq!(details["causeKind"], "redirect");
2719 assert_eq!(details["httpStatus"], 302);
2720 assert!(
2721 matches!(destination.accept(), Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock),
2722 "{credentials} {redirects}: the destination is never contacted"
2723 );
2724 server.join().unwrap();
2725 }
2726 }
2727 }
2728
2729 #[test]
2730 fn websocket_handshake_redirects_obey_the_redirect_policy() {
2731 for credential in ["headers", "userinfo", "query", "none"] {
2738 let redirects = "deny";
2739 let destination = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2740 let destination_addr = destination.local_addr().unwrap();
2741 let location = format!("ws://{destination_addr}/realtime");
2742 let landed = Arc::new(Mutex::new(Vec::new()));
2746 let landed_writer = Arc::clone(&landed);
2747 let destination_thread = std::thread::spawn(move || loop {
2748 let (mut socket, _) = destination.accept().unwrap();
2749 let mut reader = BufReader::new(socket.try_clone().unwrap());
2750 let mut head = String::new();
2751 loop {
2752 let mut line = String::new();
2753 reader.read_line(&mut line).unwrap();
2754 if line == "\r\n" {
2755 break;
2756 }
2757 head.push_str(&line);
2758 }
2759 let sentinel = head.starts_with("GET /sentinel");
2760 landed_writer.lock().unwrap().push(head);
2761 let _ = write!(
2762 socket,
2763 "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
2764 );
2765 if sentinel {
2766 break;
2767 }
2768 });
2769 let origin = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2770 let origin_addr = origin.local_addr().unwrap();
2771 let (headers, ws_url) = match credential {
2772 "headers" => (serde_json::json!({"authorization": "Bearer secret"}), None),
2773 "userinfo" => (
2774 serde_json::json!({}),
2775 Some(format!("ws://user:secret@{origin_addr}/realtime")),
2776 ),
2777 "query" => (
2778 serde_json::json!({}),
2779 Some(format!("ws://{origin_addr}/realtime?token=secret")),
2780 ),
2781 _ => (serde_json::json!({}), None),
2782 };
2783 let (head_tx, head_rx) = std::sync::mpsc::channel();
2784 let origin_thread = std::thread::spawn(move || {
2785 let (mut socket, _) = origin.accept().unwrap();
2786 let mut reader = BufReader::new(socket.try_clone().unwrap());
2787 let mut head = String::new();
2788 loop {
2789 let mut line = String::new();
2790 reader.read_line(&mut line).unwrap();
2791 if line == "\r\n" {
2792 break;
2793 }
2794 head.push_str(&line);
2795 }
2796 let _ = head_tx.send(head);
2797 let _ = write!(
2798 socket,
2799 "HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
2800 );
2801 });
2802 let mut config = serde_json::json!({
2803 "baseUrl": format!("http://{origin_addr}"),
2804 "headers": headers,
2805 "redirects": redirects,
2806 });
2807 if let Some(ws_url) = &ws_url {
2808 config["wsUrl"] = serde_json::json!(ws_url);
2809 }
2810 let mut transport = HostTransport::from_config(&config).unwrap();
2811 let result = transport.realtime_connect();
2812 let mut sentinel = std::net::TcpStream::connect(destination_addr).unwrap();
2815 let _ = write!(sentinel, "GET /sentinel HTTP/1.1\r\nHost: sentinel\r\n\r\n");
2816 destination_thread.join().unwrap();
2817 let landed = landed.lock().unwrap().clone();
2818 origin_thread.join().unwrap();
2819 let error = result.unwrap_err();
2820 assert_eq!(
2821 error.code, "transport.redirect",
2822 "{credential}: a refused handshake redirect surfaces the redirect code"
2823 );
2824 let details = error.details.unwrap();
2825 assert_eq!(details["causeKind"], "redirect");
2826 assert_eq!(details["httpStatus"], 302);
2827 assert_eq!(
2828 landed.len(),
2829 1,
2830 "{credential}: the redirect target is never contacted: {landed:?}"
2831 );
2832 assert!(landed[0].starts_with("GET /sentinel"), "{landed:?}");
2833 let head = head_rx.recv().unwrap();
2834 if credential == "userinfo" {
2835 assert!(
2836 !head.contains("secret"),
2837 "the handshake request line drops URL userinfo"
2838 );
2839 }
2840 if credential == "query" {
2841 assert!(
2842 head.contains("token=secret"),
2843 "the handshake request line carries the realtime query"
2844 );
2845 }
2846 }
2847 }
2848
2849 const GZIP_BOMB: [u8; 548] = [
2852 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xed, 0xc9, 0xb1, 0x11, 0x80,
2853 0x20, 0x10, 0x00, 0xb0, 0x59, 0x1c, 0x80, 0xa1, 0xfe, 0x90, 0xb3, 0x41, 0xf0, 0x7c, 0x2d,
2854 0xd8, 0xde, 0x09, 0xdc, 0x20, 0x69, 0x93, 0x6b, 0xd4, 0xb7, 0xc7, 0x5d, 0xb2, 0x1d, 0x67,
2855 0x1b, 0x4f, 0xb9, 0x62, 0xf5, 0x19, 0xfb, 0x96, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2856 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2857 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2858 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2859 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2860 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2861 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2862 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2863 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2864 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2865 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2866 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2867 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2868 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2869 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2870 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2871 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2872 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2873 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2874 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2875 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2876 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2877 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2878 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2879 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2880 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2881 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2882 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2883 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2884 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2885 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2886 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08,
2887 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0x10, 0x42, 0x08, 0x21, 0x84, 0xf8, 0x8b, 0x0f,
2888 0xdb, 0x82, 0xf4, 0xa9, 0x40, 0x0d, 0x03, 0x00,
2889 ];
2890
2891 #[test]
2892 fn decoded_response_cap_counts_decompressed_bytes() {
2893 for cap in [200000u64, 199999] {
2894 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2895 let base = format!("http://{}", listener.local_addr().unwrap());
2896 let server = std::thread::spawn(move || {
2897 let (mut socket, _) = listener.accept().unwrap();
2898 let mut reader = BufReader::new(socket.try_clone().unwrap());
2899 loop {
2900 let mut line = String::new();
2901 reader.read_line(&mut line).unwrap();
2902 if line == "\r\n" {
2903 break;
2904 }
2905 }
2906 let _ = write!(
2907 socket,
2908 "HTTP/1.1 200 OK\r\nContent-Encoding: gzip\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
2909 GZIP_BOMB.len()
2910 );
2911 let _ = socket.write_all(&GZIP_BOMB);
2912 });
2913 let mut transport = HostTransport::from_config(
2914 &serde_json::json!({"baseUrl": base, "maxResponseBytes": cap}),
2915 )
2916 .unwrap();
2917 let result = transport.fetch_blob_url(&format!("{base}/signed"));
2918 if cap == 200000 {
2919 let body = result.expect("a cap equal to the decoded size admits the body");
2920 assert_eq!(body.len(), 200000);
2921 } else {
2922 let error = result.unwrap_err();
2923 assert_eq!(error.code, "transport.response_too_large");
2924 assert_eq!(error.details, Some(serde_json::json!({ "limit": cap })));
2925 }
2926 server.join().unwrap();
2927 }
2928 }
2929}