1use std::sync::{Arc, Mutex};
20
21use crate::{BlobDownload, BlobUploadGrant, SegmentRequest, Transport, TransportError};
22
23pub enum Inbound {
25 Text(String),
26 Binary(Vec<u8>),
27}
28
29pub struct InboundBuffer {
33 frames: Mutex<Vec<Inbound>>,
34 notify: Option<Arc<dyn Fn() + Send + Sync>>,
35}
36
37impl Default for InboundBuffer {
38 fn default() -> Self {
39 Self {
40 frames: Mutex::new(Vec::new()),
41 notify: None,
42 }
43 }
44}
45
46impl InboundBuffer {
47 pub fn with_notify(notify: Arc<dyn Fn() + Send + Sync>) -> Self {
48 Self {
49 frames: Mutex::new(Vec::new()),
50 notify: Some(notify),
51 }
52 }
53
54 pub fn push(&self, frame: Inbound) {
55 self.frames.lock().expect("inbound lock").push(frame);
56 if let Some(notify) = &self.notify {
57 notify();
58 }
59 }
60 fn take(&self) -> Vec<Inbound> {
61 std::mem::take(&mut *self.frames.lock().expect("inbound lock"))
62 }
63}
64
65pub enum HostTransport {
66 Null {
68 signed_urls: bool,
69 inbound: Arc<InboundBuffer>,
70 },
71 #[cfg(feature = "native-transport")]
72 Native(native::NativeTransport),
73}
74
75impl HostTransport {
76 pub fn from_config(config: &serde_json::Value) -> Result<Self, String> {
79 Self::new_from_config(config)
80 }
81
82 pub fn new_from_config(config: &serde_json::Value) -> Result<Self, String> {
85 Self::from_config_with_notify(config, None)
86 }
87
88 pub fn from_config_with_notify(
92 config: &serde_json::Value,
93 notify: Option<Arc<dyn Fn() + Send + Sync>>,
94 ) -> Result<Self, String> {
95 #[cfg(feature = "native-transport")]
96 {
97 if let Some(base_url) = config.get("baseUrl").and_then(|v| v.as_str()) {
98 return Ok(HostTransport::Native(native::NativeTransport::new(
99 base_url, config, notify,
100 )?));
101 }
102 }
103 #[cfg(not(feature = "native-transport"))]
104 {
105 if config.get("baseUrl").is_some() {
106 return Err(
107 "this build has no native transport (rebuild with --features native-transport)"
108 .to_owned(),
109 );
110 }
111 }
112 Ok(HostTransport::Null {
113 signed_urls: false,
114 inbound: Arc::new(match notify {
115 Some(notify) => InboundBuffer::with_notify(notify),
116 None => InboundBuffer::default(),
117 }),
118 })
119 }
120
121 pub fn fork_round(&self) -> Self {
124 match self {
125 Self::Null {
126 signed_urls,
127 inbound,
128 } => Self::Null {
129 signed_urls: *signed_urls,
130 inbound: inbound.clone(),
131 },
132 #[cfg(feature = "native-transport")]
133 Self::Native(t) => Self::Native(t.fork_round()),
134 }
135 }
136
137 pub fn set_signed_urls(&mut self, value: bool) {
138 match self {
139 HostTransport::Null { signed_urls, .. } => *signed_urls = value,
140 #[cfg(feature = "native-transport")]
141 HostTransport::Native(t) => t.signed_urls = value,
142 }
143 }
144
145 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
148 match self {
149 HostTransport::Null { .. } => drop(headers),
150 #[cfg(feature = "native-transport")]
151 HostTransport::Native(t) => t.set_headers(headers),
152 }
153 }
154
155 pub fn take_inbound(&mut self) -> Vec<Inbound> {
157 match self {
158 HostTransport::Null { inbound, .. } => inbound.take(),
159 #[cfg(feature = "native-transport")]
160 HostTransport::Native(t) => t.inbound.take(),
161 }
162 }
163
164 pub fn shutdown(&mut self) {
166 match self {
167 HostTransport::Null { .. } => {}
168 #[cfg(feature = "native-transport")]
169 HostTransport::Native(t) => t.shutdown(),
170 }
171 }
172}
173
174fn unavailable(op: &str) -> TransportError {
175 TransportError::new(
176 "transport.unavailable",
177 format!("{op} needs the native transport (build with --features native-transport)"),
178 )
179}
180
181#[cfg_attr(not(feature = "native-transport"), allow(unused_variables))]
184impl Transport for HostTransport {
185 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
186 match self {
187 HostTransport::Null { .. } => Err(unavailable("sync")),
188 #[cfg(feature = "native-transport")]
189 HostTransport::Native(t) => t.sync(request),
190 }
191 }
192
193 fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
194 match self {
195 HostTransport::Null { .. } => Err(unavailable("remoteOperation")),
196 #[cfg(feature = "native-transport")]
197 HostTransport::Native(t) => t.remote_operation(request),
198 }
199 }
200
201 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
202 match self {
203 HostTransport::Null { .. } => Err(unavailable("realtimeSync")),
204 #[cfg(feature = "native-transport")]
205 HostTransport::Native(t) => t.realtime_sync(request),
206 }
207 }
208
209 fn download_segment(
210 &mut self,
211 request: &SegmentRequest,
212 on_progress: &mut dyn FnMut(u64),
213 ) -> Result<Vec<u8>, TransportError> {
214 match self {
215 HostTransport::Null { .. } => Err(unavailable("downloadSegment")),
216 #[cfg(feature = "native-transport")]
217 HostTransport::Native(t) => t.download_segment(request, on_progress),
218 }
219 }
220
221 fn supports_url_fetch(&self) -> bool {
222 match self {
223 HostTransport::Null { signed_urls, .. } => *signed_urls,
224 #[cfg(feature = "native-transport")]
225 HostTransport::Native(t) => t.signed_urls,
226 }
227 }
228
229 fn fetch_url(
230 &mut self,
231 url: &str,
232 on_progress: &mut dyn FnMut(u64),
233 ) -> Result<Vec<u8>, TransportError> {
234 match self {
235 HostTransport::Null { .. } => Err(unavailable("fetchUrl")),
236 #[cfg(feature = "native-transport")]
237 HostTransport::Native(t) => t.fetch_url(url, on_progress),
238 }
239 }
240
241 fn blob_upload(
242 &mut self,
243 blob_id: &str,
244 bytes: &[u8],
245 media_type: Option<&str>,
246 ) -> Result<(), TransportError> {
247 match self {
248 HostTransport::Null { .. } => Err(unavailable("blobUpload")),
249 #[cfg(feature = "native-transport")]
250 HostTransport::Native(t) => t.blob_upload(blob_id, bytes, media_type),
251 }
252 }
253
254 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
255 match self {
256 HostTransport::Null { .. } => Err(unavailable("blobDownload")),
257 #[cfg(feature = "native-transport")]
258 HostTransport::Native(t) => t.blob_download(blob_id),
259 }
260 }
261
262 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
263 match self {
264 HostTransport::Null { .. } => Err(unavailable("fetchBlobUrl")),
265 #[cfg(feature = "native-transport")]
266 HostTransport::Native(t) => t.fetch_blob_url(url),
267 }
268 }
269
270 fn blob_upload_grant(
271 &mut self,
272 blob_id: &str,
273 byte_length: u64,
274 media_type: Option<&str>,
275 ) -> Result<BlobUploadGrant, TransportError> {
276 match self {
277 HostTransport::Null { .. } => Ok(BlobUploadGrant::None),
280 #[cfg(feature = "native-transport")]
281 HostTransport::Native(t) => t.blob_upload_grant(blob_id, byte_length, media_type),
282 }
283 }
284
285 fn blob_put_url(
286 &mut self,
287 url: &str,
288 bytes: &[u8],
289 media_type: Option<&str>,
290 ) -> Result<(), TransportError> {
291 match self {
292 HostTransport::Null { .. } => Err(unavailable("blobPutUrl")),
293 #[cfg(feature = "native-transport")]
294 HostTransport::Native(t) => t.blob_put_url(url, bytes, media_type),
295 }
296 }
297
298 fn realtime_connect(&mut self) -> Result<(), TransportError> {
299 match self {
300 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
301 #[cfg(feature = "native-transport")]
302 HostTransport::Native(t) => {
303 t.set_realtime_client_id(None);
304 t.realtime_connect()
305 }
306 }
307 }
308
309 fn realtime_connect_for_client(&mut self, client_id: &str) -> Result<(), TransportError> {
310 match self {
311 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
312 #[cfg(feature = "native-transport")]
313 HostTransport::Native(t) => {
314 t.set_realtime_client_id(Some(client_id));
315 t.realtime_connect()
316 }
317 }
318 }
319
320 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
321 match self {
322 HostTransport::Null { .. } => Err(unavailable("realtimeSend")),
323 #[cfg(feature = "native-transport")]
324 HostTransport::Native(t) => t.realtime_send(text),
325 }
326 }
327
328 fn realtime_close(&mut self) -> Result<(), TransportError> {
329 match self {
330 HostTransport::Null { .. } => Ok(()),
331 #[cfg(feature = "native-transport")]
332 HostTransport::Native(t) => t.realtime_close(),
333 }
334 }
335}
336
337#[cfg(feature = "native-transport")]
338mod native {
339 use std::net::TcpStream;
351 use std::sync::atomic::{AtomicBool, Ordering};
352 use std::sync::{mpsc, Arc, Condvar, Mutex};
353 use std::thread::JoinHandle;
354 use std::time::Duration;
355
356 use polling::{Event, Events, Poller};
357 use tungstenite::stream::MaybeTlsStream;
358 use tungstenite::Message;
359
360 use super::{Inbound, InboundBuffer};
361 use crate::{
362 BlobDownload, BlobUploadGrant, RealtimeRound, RoundInbound, SegmentRequest, Transport,
363 TransportError,
364 };
365
366 struct SocketRegistration {
368 poller: Arc<Poller>,
369 stream: TcpStream,
370 }
371
372 impl Drop for SocketRegistration {
373 fn drop(&mut self) {
374 let _ = self.poller.delete(&self.stream);
375 }
376 }
377
378 #[derive(Clone)]
379 struct SocketSender {
380 queue: mpsc::SyncSender<Outgoing>,
381 poller: Arc<Poller>,
382 }
383
384 struct Outgoing {
385 message: Message,
386 completed: mpsc::Sender<Result<(), TransportError>>,
387 }
388
389 const ROUND_TIMEOUT: Duration = Duration::from_secs(30);
393 #[derive(Default)]
400 pub(super) struct RoundChannel {
401 state: Mutex<RoundState>,
402 ready: Condvar,
403 }
404
405 #[derive(Default)]
406 struct RoundState {
407 round: RealtimeRound,
408 outcome: Option<Result<Vec<u8>, TransportError>>,
410 }
411
412 impl RoundChannel {
413 fn begin(&self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
417 let mut state = self.state.lock().expect("round lock");
418 state.outcome = None;
419 state.round.begin(request)
420 }
421
422 fn route_binary(&self, frame: &[u8]) -> Option<Vec<u8>> {
426 let mut state = self.state.lock().expect("round lock");
427 match state.round.route_binary(frame) {
428 Ok(RoundInbound::Delta(body)) => Some(body),
429 Ok(RoundInbound::RoundProgress) | Ok(RoundInbound::Ignored) => None,
430 Ok(RoundInbound::RoundComplete(bytes)) => {
431 state.outcome = Some(Ok(bytes));
432 self.ready.notify_all();
433 None
434 }
435 Err(error) => {
436 state.outcome = Some(Err(error));
437 self.ready.notify_all();
438 None
439 }
440 }
441 }
442
443 fn fail_in_flight(&self, error: TransportError) {
445 let mut state = self.state.lock().expect("round lock");
446 if state.round.in_flight() && state.outcome.is_none() {
447 state.round.abort();
448 state.outcome = Some(Err(error));
449 self.ready.notify_all();
450 }
451 }
452
453 fn wait(&self) -> Result<Vec<u8>, TransportError> {
455 let mut state = self.state.lock().expect("round lock");
456 let deadline = std::time::Instant::now() + ROUND_TIMEOUT;
457 while state.outcome.is_none() {
458 let now = std::time::Instant::now();
459 if now >= deadline {
460 state.round.abort();
461 return Err(TransportError::new(
462 "sync.transport_failed",
463 "realtime sync round timed out (§8.7)",
464 ));
465 }
466 let (guard, _timeout) = self
467 .ready
468 .wait_timeout(state, deadline - now)
469 .expect("round wait");
470 state = guard;
471 }
472 state.outcome.take().expect("outcome present")
473 }
474 }
475
476 fn http_err(op: &str, e: impl std::fmt::Display) -> TransportError {
477 TransportError::new("transport.failed", format!("{op}: {e}"))
478 }
479
480 fn segment_error(
481 path: Option<&str>,
482 http_status: Option<u16>,
483 cause: impl std::fmt::Display,
484 ) -> TransportError {
485 let mut details = serde_json::json!({ "causeMessage": cause.to_string() });
486 if let Some(path) = path {
487 details["path"] = path.into();
488 }
489 if let Some(status) = http_status {
490 details["httpStatus"] = status.into();
491 }
492 let mut error = TransportError::new("sync.transport_failed", "segment transfer failed");
493 error.details = Some(details);
494 error
495 }
496
497 fn is_would_block(e: &tungstenite::Error) -> bool {
499 matches!(
500 e,
501 tungstenite::Error::Io(io) if matches!(
502 io.kind(),
503 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
504 )
505 )
506 }
507
508 pub struct NativeTransport {
509 base_url: String,
510 ws_url: String,
511 headers: Vec<(String, String)>,
513 agent: ureq::Agent,
514 pub signed_urls: bool,
515 pub inbound: Arc<InboundBuffer>,
516 outgoing: Option<Box<SocketSender>>,
518 reader: Option<JoinHandle<()>>,
519 reader_stop: Arc<AtomicBool>,
520 round: Arc<RoundChannel>,
522 realtime_client_id: Option<String>,
523 }
524
525 fn derive_ws_url(base_url: &str) -> String {
526 let ws = if let Some(rest) = base_url.strip_prefix("https://") {
529 format!("wss://{rest}")
530 } else if let Some(rest) = base_url.strip_prefix("http://") {
531 format!("ws://{rest}")
532 } else {
533 base_url.to_owned()
534 };
535 let trimmed = ws.trim_end_matches('/');
536 format!("{trimmed}/realtime")
537 }
538
539 impl NativeTransport {
540 pub fn fork_round(&self) -> Self {
541 Self {
542 base_url: self.base_url.clone(),
543 ws_url: self.ws_url.clone(),
544 headers: self.headers.clone(),
545 agent: self.agent.clone(),
546 signed_urls: self.signed_urls,
547 inbound: self.inbound.clone(),
548 outgoing: self.outgoing.clone(),
549 reader: None,
550 reader_stop: self.reader_stop.clone(),
551 round: self.round.clone(),
552 realtime_client_id: self.realtime_client_id.clone(),
553 }
554 }
555
556 pub fn new(
557 base_url: &str,
558 config: &serde_json::Value,
559 notify: Option<Arc<dyn Fn() + Send + Sync>>,
560 ) -> Result<Self, String> {
561 let mut headers = Vec::new();
562 if let Some(map) = config.get("headers").and_then(|v| v.as_object()) {
563 for (k, v) in map {
564 if let Some(s) = v.as_str() {
565 headers.push((k.clone(), s.to_owned()));
566 }
567 }
568 }
569 let ws_url = config
570 .get("wsUrl")
571 .and_then(|v| v.as_str())
572 .map(str::to_owned)
573 .unwrap_or_else(|| derive_ws_url(base_url));
574 Ok(NativeTransport {
575 base_url: base_url.trim_end_matches('/').to_owned(),
576 ws_url,
577 headers,
578 agent: ureq::Agent::new_with_defaults(),
579 signed_urls: false,
580 inbound: Arc::new(match notify {
581 Some(notify) => InboundBuffer::with_notify(notify),
582 None => InboundBuffer::default(),
583 }),
584 outgoing: None,
585 reader: None,
586 reader_stop: Arc::new(AtomicBool::new(false)),
587 round: Arc::new(RoundChannel::default()),
588 realtime_client_id: None,
589 })
590 }
591
592 fn post_sync(&self, path: &str, body: &[u8]) -> Result<Vec<u8>, TransportError> {
593 let url = format!("{}{}", self.base_url, path);
594 let mut req = self
597 .agent
598 .post(&url)
599 .header("content-type", "application/vnd.syncular.sync.v2");
600 for (k, v) in &self.headers {
601 req = req.header(k.as_str(), v.as_str());
602 }
603 let resp = req.send(body).map_err(|e| http_err("POST", e))?;
604 read_body(resp, None)
605 }
606
607 fn post_operation(&self, body: &[u8]) -> Result<Vec<u8>, TransportError> {
608 let url = format!("{}/operations", self.base_url);
609 let mut req = self.agent.post(&url).header(
610 "content-type",
611 "application/vnd.syncular.operations.v1+json",
612 );
613 for (key, value) in &self.headers {
614 req = req.header(key.as_str(), value.as_str());
615 }
616 let response = req
617 .send(body)
618 .map_err(|error| http_err("POST operation", error))?;
619 read_body(response, None)
620 }
621
622 fn get_bytes(&self, url: &str, with_headers: bool) -> Result<Vec<u8>, TransportError> {
623 let mut req = self.agent.get(url);
624 if with_headers {
625 for (k, v) in &self.headers {
626 req = req.header(k.as_str(), v.as_str());
627 }
628 }
629 let resp = req.call().map_err(|e| http_err("GET", e))?;
630 read_body(resp, None)
631 }
632
633 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
634 self.headers = headers;
635 }
636
637 pub fn set_realtime_client_id(&mut self, client_id: Option<&str>) {
638 self.realtime_client_id = client_id.map(str::to_owned);
639 }
640
641 pub fn shutdown(&mut self) {
642 self.reader_stop.store(true, Ordering::SeqCst);
643 self.round.fail_in_flight(TransportError::new(
646 "sync.transport_failed",
647 "realtime disconnected mid-round (§8.7)",
648 ));
649 if let Some(outgoing) = &self.outgoing {
650 let _ = outgoing.poller.notify();
651 }
652 if let Some(handle) = self.reader.take() {
653 let _ = handle.join();
654 }
655 self.outgoing = None;
656 }
657
658 fn send_message(&mut self, message: Message) -> Result<(), TransportError> {
659 let result = (|| {
660 let Some(outgoing) = &self.outgoing else {
661 return Err(TransportError::new(
662 "transport.failed",
663 "realtime not connected",
664 ));
665 };
666 let (completed, result) = mpsc::channel();
667 outgoing
668 .queue
669 .try_send(Outgoing { message, completed })
670 .map_err(|_| {
671 TransportError::new("transport.failed", "realtime send queue unavailable")
672 })?;
673 outgoing
674 .poller
675 .notify()
676 .map_err(|e| http_err("ws wake", e))?;
677 result.recv_timeout(ROUND_TIMEOUT).map_err(|_| {
678 TransportError::new("transport.failed", "realtime send did not complete")
679 })?
680 })();
681 if result.is_err() {
684 self.shutdown();
685 }
686 result
687 }
688 }
689
690 fn read_body(
691 resp: ureq::http::Response<ureq::Body>,
692 mut progress: Option<&mut dyn FnMut(u64)>,
693 ) -> Result<Vec<u8>, TransportError> {
694 use std::io::Read;
695 let mut reader = resp.into_body().into_with_config().reader();
696 let mut bytes = Vec::new();
697 let mut chunk = [0u8; 64 * 1024];
698 let mut reported = 0;
699 loop {
700 let n = reader.read(&mut chunk).map_err(|e| http_err("read", e))?;
701 if n == 0 {
702 break;
703 }
704 bytes.extend_from_slice(&chunk[..n]);
705 if bytes.len() - reported >= 64 * 1024 {
706 if let Some(callback) = progress.as_mut() {
707 callback(bytes.len() as u64);
708 }
709 reported = bytes.len();
710 }
711 }
712 if bytes.len() != reported {
713 if let Some(callback) = progress {
714 callback(bytes.len() as u64);
715 }
716 }
717 Ok(bytes)
718 }
719
720 impl Transport for NativeTransport {
721 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
722 self.post_sync("/sync", request)
723 }
724
725 fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
726 self.post_operation(request)
727 }
728
729 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
730 if self.outgoing.is_none() {
740 return self.post_sync("/sync", request);
741 }
742 let framed = self.round.begin(request)?;
743 if let Err(error) = self.send_message(Message::Binary(framed.into())) {
744 self.round.fail_in_flight(error);
745 }
746 self.round.wait()
747 }
748
749 fn download_segment(
750 &mut self,
751 request: &SegmentRequest,
752 on_progress: &mut dyn FnMut(u64),
753 ) -> Result<Vec<u8>, TransportError> {
754 let url = format!("{}/segments/{}", self.base_url, request.segment_id);
761 let parsed = url::Url::parse(&url).map_err(|error| segment_error(None, None, error))?;
762 let mut req = self
763 .agent
764 .get(&url)
765 .header("x-syncular-scopes", &request.requested_scopes_json);
766 for (k, v) in &self.headers {
767 req = req.header(k.as_str(), v.as_str());
768 }
769 let resp = req.call().map_err(|error| {
770 let status = match &error {
771 ureq::Error::StatusCode(status) => Some(*status),
772 _ => None,
773 };
774 segment_error(Some(parsed.path()), status, error)
775 })?;
776 let status = resp.status().as_u16();
777 read_body(resp, Some(on_progress))
778 .map_err(|error| segment_error(Some(parsed.path()), Some(status), error.message))
779 }
780
781 fn supports_url_fetch(&self) -> bool {
782 self.signed_urls
783 }
784
785 fn fetch_url(
786 &mut self,
787 url: &str,
788 on_progress: &mut dyn FnMut(u64),
789 ) -> Result<Vec<u8>, TransportError> {
790 let parsed = url::Url::parse(url).map_err(|error| segment_error(None, None, error))?;
792 let resp = self.agent.get(url).call().map_err(|error| {
793 let status = match &error {
794 ureq::Error::StatusCode(status) => Some(*status),
795 _ => None,
796 };
797 segment_error(Some(parsed.path()), status, error)
798 })?;
799 let status = resp.status().as_u16();
800 read_body(resp, Some(on_progress))
801 .map_err(|error| segment_error(Some(parsed.path()), Some(status), error.message))
802 }
803
804 fn blob_upload(
805 &mut self,
806 blob_id: &str,
807 bytes: &[u8],
808 media_type: Option<&str>,
809 ) -> Result<(), TransportError> {
810 let url = format!("{}/blobs/{}", self.base_url, blob_id);
813 let mut req = self.agent.put(&url).header(
814 "content-type",
815 media_type.unwrap_or("application/octet-stream"),
816 );
817 for (k, v) in &self.headers {
818 req = req.header(k.as_str(), v.as_str());
819 }
820 req.send(bytes).map_err(|e| http_err("PUT blob", e))?;
821 Ok(())
822 }
823
824 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
825 let url = format!("{}/blobs/{}", self.base_url, blob_id);
828 let mut req = self.agent.get(&url);
829 for (k, v) in &self.headers {
830 req = req.header(k.as_str(), v.as_str());
831 }
832 let resp = req.call().map_err(|e| {
833 let e = http_err("GET blob", e);
834 if e.code == "transport.failed" {
836 TransportError::new("blob.not_found", e.message)
837 } else {
838 e
839 }
840 })?;
841 let is_json = resp
844 .headers()
845 .get(ureq::http::header::CONTENT_TYPE)
846 .and_then(|value| value.to_str().ok())
847 .is_some_and(|value| value.contains("application/json"));
848 let body = read_body(resp, None)?;
849 if is_json {
850 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&body) {
851 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
852 return Ok(BlobDownload::Url {
853 url: u.to_owned(),
854 url_expires_at_ms: parsed
855 .get("urlExpiresAtMs")
856 .and_then(|v| v.as_i64()),
857 });
858 }
859 }
860 }
861 Ok(BlobDownload::Bytes(body))
862 }
863
864 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
865 self.get_bytes(url, false)
867 }
868
869 fn blob_upload_grant(
870 &mut self,
871 blob_id: &str,
872 byte_length: u64,
873 media_type: Option<&str>,
874 ) -> Result<BlobUploadGrant, TransportError> {
875 let url = format!("{}/blobs/{}/upload-grant", self.base_url, blob_id);
876 let mut req = self
877 .agent
878 .post(&url)
879 .header("content-type", "application/json");
880 for (k, v) in &self.headers {
881 req = req.header(k.as_str(), v.as_str());
882 }
883 let body = serde_json::json!({
884 "byteLength": byte_length,
885 "mediaType": media_type,
886 });
887 let resp = req
888 .send(body.to_string())
889 .map_err(|e| http_err("POST upload-grant", e))?;
890 let grant_body = read_body(resp, None)?;
891 let parsed: serde_json::Value = serde_json::from_slice(&grant_body)
892 .map_err(|e| TransportError::new("transport.failed", format!("read grant: {e}")))?;
893 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
894 return Ok(BlobUploadGrant::Url {
895 url: u.to_owned(),
896 url_expires_at_ms: parsed.get("urlExpiresAtMs").and_then(|v| v.as_i64()),
897 });
898 }
899 if parsed.get("present").and_then(|v| v.as_bool()) == Some(true) {
900 return Ok(BlobUploadGrant::Present);
901 }
902 Ok(BlobUploadGrant::None)
903 }
904
905 fn blob_put_url(
906 &mut self,
907 url: &str,
908 bytes: &[u8],
909 media_type: Option<&str>,
910 ) -> Result<(), TransportError> {
911 let req = self.agent.put(url).header(
913 "content-type",
914 media_type.unwrap_or("application/octet-stream"),
915 );
916 req.send(bytes).map_err(|e| http_err("PUT blob url", e))?;
917 Ok(())
918 }
919
920 fn realtime_connect(&mut self) -> Result<(), TransportError> {
921 if self.outgoing.is_some() {
922 if self.reader_stop.load(Ordering::SeqCst)
923 || self
924 .reader
925 .as_ref()
926 .is_some_and(|reader| reader.is_finished())
927 {
928 self.shutdown();
929 } else {
930 return Ok(());
931 }
932 }
933 use tungstenite::client::IntoClientRequest;
939 let mut url = url::Url::parse(&self.ws_url)
940 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
941 if let Some(client_id) = self.realtime_client_id.as_deref() {
942 let retained_query: Vec<(String, String)> = url
943 .query_pairs()
944 .filter(|(key, _)| key != "clientId")
945 .map(|(key, value)| (key.into_owned(), value.into_owned()))
946 .collect();
947 url.set_query(None);
948 url.query_pairs_mut()
949 .extend_pairs(retained_query)
950 .append_pair("clientId", client_id);
951 }
952 let mut request = url
953 .as_str()
954 .into_client_request()
955 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
956 {
957 let out = request.headers_mut();
958 for (k, v) in &self.headers {
959 if let (Ok(name), Ok(value)) = (
960 tungstenite::http::HeaderName::try_from(k.as_str()),
961 tungstenite::http::HeaderValue::try_from(v.as_str()),
962 ) {
963 out.insert(name, value);
964 }
965 }
966 }
967 let (mut ws, _resp) = tungstenite::connect(request)
968 .map_err(|e| TransportError::new("transport.failed", format!("ws connect: {e}")))?;
969 let stream = match ws.get_mut() {
970 MaybeTlsStream::Plain(stream) => stream.try_clone(),
971 MaybeTlsStream::Rustls(stream) => stream.get_ref().try_clone(),
972 _ => {
973 return Err(TransportError::new(
974 "transport.failed",
975 "unsupported websocket stream",
976 ))
977 }
978 }
979 .map_err(|e| http_err("ws clone", e))?;
980 stream
981 .set_nonblocking(true)
982 .map_err(|e| http_err("ws nonblocking", e))?;
983 let poller = Arc::new(Poller::new().map_err(|e| http_err("ws poller", e))?);
984 unsafe { poller.add(&stream, Event::readable(0)) }
986 .map_err(|e| http_err("ws register", e))?;
987 let registration = SocketRegistration {
988 poller: Arc::clone(&poller),
989 stream,
990 };
991 let (outgoing, queued) = mpsc::sync_channel::<Outgoing>(1);
992 self.outgoing = Some(Box::new(SocketSender {
993 queue: outgoing,
994 poller,
995 }));
996 self.reader_stop = Arc::new(AtomicBool::new(false));
997 self.round = Arc::new(RoundChannel::default());
998 let inbound = Arc::clone(&self.inbound);
999 let stop = Arc::clone(&self.reader_stop);
1000 let round = Arc::clone(&self.round);
1001 self.reader = Some(std::thread::spawn(move || {
1002 let mut events = Events::new();
1003 let mut pending: Option<mpsc::Sender<Result<(), TransportError>>> = None;
1004 let failure = 'io: loop {
1005 if stop.load(Ordering::SeqCst) {
1006 let _ = ws.close(None);
1007 let _ = ws.flush();
1008 break TransportError::new(
1009 "sync.transport_failed",
1010 "realtime disconnected mid-round (§8.7)",
1011 );
1012 }
1013 if pending.is_none() {
1014 if let Ok(outgoing) = queued.try_recv() {
1015 pending = Some(outgoing.completed);
1016 if let Err(error) = ws.write(outgoing.message) {
1018 if !is_would_block(&error) {
1019 break http_err("ws write", error);
1020 }
1021 }
1022 }
1023 }
1024 let mut drained = false;
1026 for _ in 0..64 {
1027 match ws.read() {
1028 Ok(Message::Text(text)) => {
1029 inbound.push(Inbound::Text(text.to_string()))
1030 }
1031 Ok(Message::Binary(bytes)) => {
1032 if let Some(delta) = round.route_binary(&bytes) {
1033 inbound.push(Inbound::Binary(delta));
1034 }
1035 }
1036 Err(error) if is_would_block(&error) => {
1037 drained = true;
1038 break;
1039 }
1040 Ok(Message::Close(_)) => {
1041 let _ = ws.flush();
1042 break 'io TransportError::new(
1043 "sync.transport_failed",
1044 "realtime disconnected mid-round (§8.7)",
1045 );
1046 }
1047 Err(_) => {
1048 break 'io TransportError::new(
1049 "sync.transport_failed",
1050 "realtime disconnected mid-round (§8.7)",
1051 );
1052 }
1053 Ok(_) => {}
1054 }
1055 }
1056 let interest = match ws.flush() {
1057 Ok(()) => {
1058 if let Some(completed) = pending.take() {
1059 let _ = completed.send(Ok(()));
1060 }
1061 Event::readable(0)
1062 }
1063 Err(error) if is_would_block(&error) => Event::all(0),
1064 Err(error) => break http_err("ws flush", error),
1065 };
1066 if !drained {
1067 continue;
1068 }
1069 if let Err(error) = registration.poller.modify(®istration.stream, interest) {
1070 break http_err("ws rearm", error);
1071 }
1072 events.clear();
1073 if let Err(error) = registration.poller.wait(&mut events, None) {
1074 if error.kind() != std::io::ErrorKind::Interrupted {
1075 break http_err("ws wait", error);
1076 }
1077 }
1078 };
1079 if let Some(completed) = pending {
1080 let _ = completed.send(Err(failure.clone()));
1081 }
1082 while let Ok(outgoing) = queued.try_recv() {
1083 let _ = outgoing.completed.send(Err(failure.clone()));
1084 }
1085 round.fail_in_flight(failure);
1086 }));
1087 Ok(())
1088 }
1089
1090 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
1091 self.send_message(Message::Text(text.to_owned().into()))
1092 }
1093
1094 fn realtime_close(&mut self) -> Result<(), TransportError> {
1095 self.shutdown();
1096 Ok(())
1097 }
1098 }
1099}
1100
1101#[cfg(all(test, feature = "native-transport"))]
1102mod tests {
1103 use super::*;
1104 use std::io::{BufRead, BufReader, Write};
1105
1106 #[test]
1107 fn detached_round_uses_the_owned_socket_and_shutdown_releases_it() {
1108 use ssp2::model::{Frame, Message, MsgKind};
1109 use std::sync::mpsc::channel;
1110 for cancelled in [false, true] {
1111 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1112 let base = format!("http://{}", listener.local_addr().unwrap());
1113 let (entered_tx, entered_rx) = channel();
1114 let (release_tx, release_rx) = channel();
1115 let server = std::thread::spawn(move || {
1116 let (socket, _) = listener.accept().unwrap();
1117 let mut socket = tungstenite::accept(socket).unwrap();
1118 let request = socket.read().unwrap().into_data();
1119 assert_eq!(request[0], crate::REALTIME_TAG_ROUND);
1120 assert_eq!(
1121 ssp2::decode_message(&request[1..]).unwrap().msg_kind,
1122 MsgKind::Request
1123 );
1124 entered_tx.send(()).unwrap();
1125 release_rx.recv().unwrap();
1126 if !cancelled {
1127 let response = ssp2::encode_message(&Message {
1128 wire_version: ssp2::decode::WIRE_VERSION,
1129 msg_kind: MsgKind::Response,
1130 frames: vec![Frame::RespHeader {
1131 required_schema_version: None,
1132 latest_schema_version: None,
1133 log_epoch: Some("epoch".into()),
1134 reset_required: Some(true),
1135 }],
1136 });
1137 let mut chunk = vec![crate::REALTIME_TAG_ROUND];
1138 chunk.extend(response);
1139 socket
1140 .send(tungstenite::Message::Binary(chunk.into()))
1141 .unwrap();
1142 }
1143 });
1144 let mut owner =
1145 HostTransport::new_from_config(&serde_json::json!({"baseUrl":base})).unwrap();
1146 owner.realtime_connect_for_client("detached-round").unwrap();
1147 let mut detached = owner.fork_round();
1148 let network = std::thread::spawn(move || {
1149 let request = ssp2::encode_message(&Message {
1150 wire_version: ssp2::decode::WIRE_VERSION,
1151 msg_kind: MsgKind::Request,
1152 frames: vec![
1153 Frame::ReqHeader {
1154 client_id: "detached-round".into(),
1155 schema_version: 1,
1156 log_epoch: None,
1157 },
1158 Frame::PullHeader {
1159 limit_commits: 0,
1160 limit_snapshot_rows: 0,
1161 max_snapshot_pages: 0,
1162 accept: 3,
1163 },
1164 ],
1165 });
1166 detached.realtime_sync(&request)
1167 });
1168 entered_rx.recv().unwrap();
1169 owner.set_headers(vec![("authorization".into(), "Bearer rotated".into())]);
1170 assert!(owner.take_inbound().is_empty());
1171 if cancelled {
1172 owner.shutdown();
1173 }
1174 release_tx.send(()).unwrap();
1175 let response = network.join().unwrap();
1176 if cancelled {
1177 assert!(response.is_err());
1178 } else {
1179 assert_eq!(
1180 ssp2::decode_message(&response.unwrap()).unwrap().msg_kind,
1181 MsgKind::Response
1182 );
1183 owner.shutdown();
1184 }
1185 server.join().unwrap();
1186 }
1187 }
1188
1189 #[test]
1190 fn segment_http_failures_have_named_codes_and_request_details() {
1191 for signed in [false, true] {
1192 for body_failure in [false, true] {
1193 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1194 let base = format!("http://{}", listener.local_addr().unwrap());
1195 let server = std::thread::spawn(move || {
1196 let (mut socket, _) = listener.accept().unwrap();
1197 let mut reader = BufReader::new(socket.try_clone().unwrap());
1198 loop {
1199 let mut line = String::new();
1200 reader.read_line(&mut line).unwrap();
1201 if line == "\r\n" {
1202 break;
1203 }
1204 assert!(!line.is_empty());
1205 }
1206 socket.write_all(if body_failure {
1207 b"HTTP/1.1 200 OK\r\nContent-Length: 999\r\nConnection: close\r\n\r\nshort"
1208 } else {
1209 b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
1210 }).unwrap();
1211 });
1212 let mut transport =
1213 HostTransport::new_from_config(&serde_json::json!({"baseUrl": base})).unwrap();
1214 let mut progress = |_| {};
1215 let error = if signed {
1216 transport.fetch_url(&format!("{base}/signed?signature=secret"), &mut progress)
1217 } else {
1218 transport.download_segment(
1219 &SegmentRequest {
1220 segment_id: "sha256:test".into(),
1221 table: "tasks".into(),
1222 requested_scopes_json: "{}".into(),
1223 },
1224 &mut progress,
1225 )
1226 }
1227 .unwrap_err();
1228 assert_eq!(error.code, "sync.transport_failed");
1229 assert_eq!(error.message, "segment transfer failed");
1230 let details = error.details.unwrap();
1231 assert_eq!(
1232 details["path"],
1233 if signed {
1234 "/signed"
1235 } else {
1236 "/segments/sha256:test"
1237 }
1238 );
1239 assert_eq!(details["httpStatus"], if body_failure { 200 } else { 403 });
1240 server.join().unwrap();
1241 }
1242 }
1243 }
1244
1245 #[test]
1246 fn refused_segment_connection_has_a_named_code_without_an_invented_status() {
1247 for signed in [false, true] {
1248 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1249 let base = format!("http://{}", listener.local_addr().unwrap());
1250 drop(listener);
1251 let mut transport =
1252 HostTransport::new_from_config(&serde_json::json!({"baseUrl": base})).unwrap();
1253 let mut progress = |_| {};
1254 let error = if signed {
1255 transport.fetch_url(&format!("{base}/signed?signature=secret"), &mut progress)
1256 } else {
1257 transport.download_segment(
1258 &SegmentRequest {
1259 segment_id: "sha256:test".into(),
1260 table: "tasks".into(),
1261 requested_scopes_json: "{}".into(),
1262 },
1263 &mut progress,
1264 )
1265 }
1266 .unwrap_err();
1267 assert_eq!(error.code, "sync.transport_failed");
1268 assert!(error.details.as_ref().unwrap().get("httpStatus").is_none());
1269 }
1270 }
1271
1272 #[test]
1273 fn segment_http_reports_intermediate_bytes_for_direct_and_signed_urls() {
1274 for signed in [false, true] {
1275 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1276 let base = format!("http://{}", listener.local_addr().unwrap());
1277 let server = std::thread::spawn(move || {
1278 let (mut socket, _) = listener.accept().unwrap();
1279 let mut reader = BufReader::new(socket.try_clone().unwrap());
1280 let mut headers = String::new();
1281 loop {
1282 let mut line = String::new();
1283 reader.read_line(&mut line).unwrap();
1284 if line == "\r\n" {
1285 break;
1286 }
1287 assert!(!line.is_empty());
1288 headers.push_str(&line.to_ascii_lowercase());
1289 }
1290 assert_eq!(headers.contains("authorization: bearer test"), !signed);
1291 assert_eq!(headers.contains("x-syncular-scopes:"), !signed);
1292 let bytes = vec![7; 128 * 1024 + 17];
1293 write!(
1294 socket,
1295 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1296 bytes.len()
1297 )
1298 .unwrap();
1299 socket.write_all(&bytes).unwrap();
1300 });
1301 let mut transport = HostTransport::from_config(
1302 &serde_json::json!({"baseUrl": base, "headers": {"authorization": "Bearer test"}}),
1303 )
1304 .unwrap();
1305 let mut updates = Vec::new();
1306 let mut progress = |bytes| updates.push(bytes);
1307 let bytes = if signed {
1308 transport
1309 .fetch_url(&format!("{base}/signed"), &mut progress)
1310 .unwrap()
1311 } else {
1312 transport
1313 .download_segment(
1314 &SegmentRequest {
1315 segment_id: "sha256:test".into(),
1316 table: "tasks".into(),
1317 requested_scopes_json: "{}".into(),
1318 },
1319 &mut progress,
1320 )
1321 .unwrap()
1322 };
1323 assert_eq!(bytes.len(), 128 * 1024 + 17);
1324 assert!(updates.iter().any(|n| *n > 0 && *n < bytes.len() as u64));
1325 assert_eq!(updates.last(), Some(&(bytes.len() as u64)));
1326 assert!(updates.windows(2).all(|pair| pair[0] < pair[1]));
1327 server.join().unwrap();
1328 }
1329 }
1330}