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 set_signed_urls(&mut self, value: bool) {
122 match self {
123 HostTransport::Null { signed_urls, .. } => *signed_urls = value,
124 #[cfg(feature = "native-transport")]
125 HostTransport::Native(t) => t.signed_urls = value,
126 }
127 }
128
129 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
132 match self {
133 HostTransport::Null { .. } => drop(headers),
134 #[cfg(feature = "native-transport")]
135 HostTransport::Native(t) => t.set_headers(headers),
136 }
137 }
138
139 pub fn take_inbound(&mut self) -> Vec<Inbound> {
141 match self {
142 HostTransport::Null { inbound, .. } => inbound.take(),
143 #[cfg(feature = "native-transport")]
144 HostTransport::Native(t) => t.inbound.take(),
145 }
146 }
147
148 pub fn shutdown(&mut self) {
150 match self {
151 HostTransport::Null { .. } => {}
152 #[cfg(feature = "native-transport")]
153 HostTransport::Native(t) => t.shutdown(),
154 }
155 }
156}
157
158fn unavailable(op: &str) -> TransportError {
159 TransportError::new(
160 "transport.unavailable",
161 format!("{op} needs the native transport (build with --features native-transport)"),
162 )
163}
164
165#[cfg_attr(not(feature = "native-transport"), allow(unused_variables))]
168impl Transport for HostTransport {
169 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
170 match self {
171 HostTransport::Null { .. } => Err(unavailable("sync")),
172 #[cfg(feature = "native-transport")]
173 HostTransport::Native(t) => t.sync(request),
174 }
175 }
176
177 fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
178 match self {
179 HostTransport::Null { .. } => Err(unavailable("remoteOperation")),
180 #[cfg(feature = "native-transport")]
181 HostTransport::Native(t) => t.remote_operation(request),
182 }
183 }
184
185 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
186 match self {
187 HostTransport::Null { .. } => Err(unavailable("realtimeSync")),
188 #[cfg(feature = "native-transport")]
189 HostTransport::Native(t) => t.realtime_sync(request),
190 }
191 }
192
193 fn download_segment(
194 &mut self,
195 request: &SegmentRequest,
196 on_progress: &mut dyn FnMut(u64),
197 ) -> Result<Vec<u8>, TransportError> {
198 match self {
199 HostTransport::Null { .. } => Err(unavailable("downloadSegment")),
200 #[cfg(feature = "native-transport")]
201 HostTransport::Native(t) => t.download_segment(request, on_progress),
202 }
203 }
204
205 fn supports_url_fetch(&self) -> bool {
206 match self {
207 HostTransport::Null { signed_urls, .. } => *signed_urls,
208 #[cfg(feature = "native-transport")]
209 HostTransport::Native(t) => t.signed_urls,
210 }
211 }
212
213 fn fetch_url(
214 &mut self,
215 url: &str,
216 on_progress: &mut dyn FnMut(u64),
217 ) -> Result<Vec<u8>, TransportError> {
218 match self {
219 HostTransport::Null { .. } => Err(unavailable("fetchUrl")),
220 #[cfg(feature = "native-transport")]
221 HostTransport::Native(t) => t.fetch_url(url, on_progress),
222 }
223 }
224
225 fn blob_upload(
226 &mut self,
227 blob_id: &str,
228 bytes: &[u8],
229 media_type: Option<&str>,
230 ) -> Result<(), TransportError> {
231 match self {
232 HostTransport::Null { .. } => Err(unavailable("blobUpload")),
233 #[cfg(feature = "native-transport")]
234 HostTransport::Native(t) => t.blob_upload(blob_id, bytes, media_type),
235 }
236 }
237
238 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
239 match self {
240 HostTransport::Null { .. } => Err(unavailable("blobDownload")),
241 #[cfg(feature = "native-transport")]
242 HostTransport::Native(t) => t.blob_download(blob_id),
243 }
244 }
245
246 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
247 match self {
248 HostTransport::Null { .. } => Err(unavailable("fetchBlobUrl")),
249 #[cfg(feature = "native-transport")]
250 HostTransport::Native(t) => t.fetch_blob_url(url),
251 }
252 }
253
254 fn blob_upload_grant(
255 &mut self,
256 blob_id: &str,
257 byte_length: u64,
258 media_type: Option<&str>,
259 ) -> Result<BlobUploadGrant, TransportError> {
260 match self {
261 HostTransport::Null { .. } => Ok(BlobUploadGrant::None),
264 #[cfg(feature = "native-transport")]
265 HostTransport::Native(t) => t.blob_upload_grant(blob_id, byte_length, media_type),
266 }
267 }
268
269 fn blob_put_url(
270 &mut self,
271 url: &str,
272 bytes: &[u8],
273 media_type: Option<&str>,
274 ) -> Result<(), TransportError> {
275 match self {
276 HostTransport::Null { .. } => Err(unavailable("blobPutUrl")),
277 #[cfg(feature = "native-transport")]
278 HostTransport::Native(t) => t.blob_put_url(url, bytes, media_type),
279 }
280 }
281
282 fn realtime_connect(&mut self) -> Result<(), TransportError> {
283 match self {
284 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
285 #[cfg(feature = "native-transport")]
286 HostTransport::Native(t) => {
287 t.set_realtime_client_id(None);
288 t.realtime_connect()
289 }
290 }
291 }
292
293 fn realtime_connect_for_client(&mut self, client_id: &str) -> Result<(), TransportError> {
294 match self {
295 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
296 #[cfg(feature = "native-transport")]
297 HostTransport::Native(t) => {
298 t.set_realtime_client_id(Some(client_id));
299 t.realtime_connect()
300 }
301 }
302 }
303
304 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
305 match self {
306 HostTransport::Null { .. } => Err(unavailable("realtimeSend")),
307 #[cfg(feature = "native-transport")]
308 HostTransport::Native(t) => t.realtime_send(text),
309 }
310 }
311
312 fn realtime_close(&mut self) -> Result<(), TransportError> {
313 match self {
314 HostTransport::Null { .. } => Ok(()),
315 #[cfg(feature = "native-transport")]
316 HostTransport::Native(t) => t.realtime_close(),
317 }
318 }
319}
320
321#[cfg(feature = "native-transport")]
322mod native {
323 use std::net::TcpStream;
335 use std::sync::atomic::{AtomicBool, Ordering};
336 use std::sync::{mpsc, Arc, Condvar, Mutex};
337 use std::thread::JoinHandle;
338 use std::time::Duration;
339
340 use polling::{Event, Events, Poller};
341 use tungstenite::stream::MaybeTlsStream;
342 use tungstenite::Message;
343
344 use super::{Inbound, InboundBuffer};
345 use crate::{
346 BlobDownload, BlobUploadGrant, RealtimeRound, RoundInbound, SegmentRequest, Transport,
347 TransportError,
348 };
349
350 struct SocketRegistration {
352 poller: Arc<Poller>,
353 stream: TcpStream,
354 }
355
356 impl Drop for SocketRegistration {
357 fn drop(&mut self) {
358 let _ = self.poller.delete(&self.stream);
359 }
360 }
361
362 struct SocketSender {
363 queue: mpsc::SyncSender<Outgoing>,
364 poller: Arc<Poller>,
365 }
366
367 struct Outgoing {
368 message: Message,
369 completed: mpsc::Sender<Result<(), TransportError>>,
370 }
371
372 const ROUND_TIMEOUT: Duration = Duration::from_secs(30);
376 #[derive(Default)]
383 pub(super) struct RoundChannel {
384 state: Mutex<RoundState>,
385 ready: Condvar,
386 }
387
388 #[derive(Default)]
389 struct RoundState {
390 round: RealtimeRound,
391 outcome: Option<Result<Vec<u8>, TransportError>>,
393 }
394
395 impl RoundChannel {
396 fn begin(&self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
400 let mut state = self.state.lock().expect("round lock");
401 state.outcome = None;
402 state.round.begin(request)
403 }
404
405 fn route_binary(&self, frame: &[u8]) -> Option<Vec<u8>> {
409 let mut state = self.state.lock().expect("round lock");
410 match state.round.route_binary(frame) {
411 Ok(RoundInbound::Delta(body)) => Some(body),
412 Ok(RoundInbound::RoundProgress) | Ok(RoundInbound::Ignored) => None,
413 Ok(RoundInbound::RoundComplete(bytes)) => {
414 state.outcome = Some(Ok(bytes));
415 self.ready.notify_all();
416 None
417 }
418 Err(error) => {
419 state.outcome = Some(Err(error));
420 self.ready.notify_all();
421 None
422 }
423 }
424 }
425
426 fn fail_in_flight(&self, error: TransportError) {
428 let mut state = self.state.lock().expect("round lock");
429 if state.round.in_flight() && state.outcome.is_none() {
430 state.round.abort();
431 state.outcome = Some(Err(error));
432 self.ready.notify_all();
433 }
434 }
435
436 fn wait(&self) -> Result<Vec<u8>, TransportError> {
438 let mut state = self.state.lock().expect("round lock");
439 let deadline = std::time::Instant::now() + ROUND_TIMEOUT;
440 while state.outcome.is_none() {
441 let now = std::time::Instant::now();
442 if now >= deadline {
443 state.round.abort();
444 return Err(TransportError::new(
445 "sync.transport_failed",
446 "realtime sync round timed out (§8.7)",
447 ));
448 }
449 let (guard, _timeout) = self
450 .ready
451 .wait_timeout(state, deadline - now)
452 .expect("round wait");
453 state = guard;
454 }
455 state.outcome.take().expect("outcome present")
456 }
457 }
458
459 fn http_err(op: &str, e: impl std::fmt::Display) -> TransportError {
460 TransportError::new("transport.failed", format!("{op}: {e}"))
461 }
462
463 fn segment_error(
464 path: Option<&str>,
465 http_status: Option<u16>,
466 cause: impl std::fmt::Display,
467 ) -> TransportError {
468 let mut details = serde_json::json!({ "causeMessage": cause.to_string() });
469 if let Some(path) = path {
470 details["path"] = path.into();
471 }
472 if let Some(status) = http_status {
473 details["httpStatus"] = status.into();
474 }
475 let mut error = TransportError::new("sync.transport_failed", "segment transfer failed");
476 error.details = Some(details);
477 error
478 }
479
480 fn is_would_block(e: &tungstenite::Error) -> bool {
482 matches!(
483 e,
484 tungstenite::Error::Io(io) if matches!(
485 io.kind(),
486 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
487 )
488 )
489 }
490
491 pub struct NativeTransport {
492 base_url: String,
493 ws_url: String,
494 headers: Vec<(String, String)>,
496 agent: ureq::Agent,
497 pub signed_urls: bool,
498 pub inbound: Arc<InboundBuffer>,
499 outgoing: Option<Box<SocketSender>>,
501 reader: Option<JoinHandle<()>>,
502 reader_stop: Arc<AtomicBool>,
503 round: Arc<RoundChannel>,
505 realtime_client_id: Option<String>,
506 }
507
508 fn derive_ws_url(base_url: &str) -> String {
509 let ws = if let Some(rest) = base_url.strip_prefix("https://") {
512 format!("wss://{rest}")
513 } else if let Some(rest) = base_url.strip_prefix("http://") {
514 format!("ws://{rest}")
515 } else {
516 base_url.to_owned()
517 };
518 let trimmed = ws.trim_end_matches('/');
519 format!("{trimmed}/realtime")
520 }
521
522 impl NativeTransport {
523 pub fn new(
524 base_url: &str,
525 config: &serde_json::Value,
526 notify: Option<Arc<dyn Fn() + Send + Sync>>,
527 ) -> Result<Self, String> {
528 let mut headers = Vec::new();
529 if let Some(map) = config.get("headers").and_then(|v| v.as_object()) {
530 for (k, v) in map {
531 if let Some(s) = v.as_str() {
532 headers.push((k.clone(), s.to_owned()));
533 }
534 }
535 }
536 let ws_url = config
537 .get("wsUrl")
538 .and_then(|v| v.as_str())
539 .map(str::to_owned)
540 .unwrap_or_else(|| derive_ws_url(base_url));
541 Ok(NativeTransport {
542 base_url: base_url.trim_end_matches('/').to_owned(),
543 ws_url,
544 headers,
545 agent: ureq::Agent::new_with_defaults(),
546 signed_urls: false,
547 inbound: Arc::new(match notify {
548 Some(notify) => InboundBuffer::with_notify(notify),
549 None => InboundBuffer::default(),
550 }),
551 outgoing: None,
552 reader: None,
553 reader_stop: Arc::new(AtomicBool::new(false)),
554 round: Arc::new(RoundChannel::default()),
555 realtime_client_id: None,
556 })
557 }
558
559 fn post_sync(&self, path: &str, body: &[u8]) -> Result<Vec<u8>, TransportError> {
560 let url = format!("{}{}", self.base_url, path);
561 let mut req = self
564 .agent
565 .post(&url)
566 .header("content-type", "application/vnd.syncular.sync.v2");
567 for (k, v) in &self.headers {
568 req = req.header(k.as_str(), v.as_str());
569 }
570 let resp = req.send(body).map_err(|e| http_err("POST", e))?;
571 read_body(resp, None)
572 }
573
574 fn post_operation(&self, body: &[u8]) -> Result<Vec<u8>, TransportError> {
575 let url = format!("{}/operations", self.base_url);
576 let mut req = self.agent.post(&url).header(
577 "content-type",
578 "application/vnd.syncular.operations.v1+json",
579 );
580 for (key, value) in &self.headers {
581 req = req.header(key.as_str(), value.as_str());
582 }
583 let response = req
584 .send(body)
585 .map_err(|error| http_err("POST operation", error))?;
586 read_body(response, None)
587 }
588
589 fn get_bytes(&self, url: &str, with_headers: bool) -> Result<Vec<u8>, TransportError> {
590 let mut req = self.agent.get(url);
591 if with_headers {
592 for (k, v) in &self.headers {
593 req = req.header(k.as_str(), v.as_str());
594 }
595 }
596 let resp = req.call().map_err(|e| http_err("GET", e))?;
597 read_body(resp, None)
598 }
599
600 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
601 self.headers = headers;
602 }
603
604 pub fn set_realtime_client_id(&mut self, client_id: Option<&str>) {
605 self.realtime_client_id = client_id.map(str::to_owned);
606 }
607
608 pub fn shutdown(&mut self) {
609 self.reader_stop.store(true, Ordering::SeqCst);
610 self.round.fail_in_flight(TransportError::new(
613 "sync.transport_failed",
614 "realtime disconnected mid-round (§8.7)",
615 ));
616 if let Some(outgoing) = &self.outgoing {
617 let _ = outgoing.poller.notify();
618 }
619 if let Some(handle) = self.reader.take() {
620 let _ = handle.join();
621 }
622 self.outgoing = None;
623 }
624
625 fn send_message(&mut self, message: Message) -> Result<(), TransportError> {
626 let result = (|| {
627 let Some(outgoing) = &self.outgoing else {
628 return Err(TransportError::new(
629 "transport.failed",
630 "realtime not connected",
631 ));
632 };
633 let (completed, result) = mpsc::channel();
634 outgoing
635 .queue
636 .try_send(Outgoing { message, completed })
637 .map_err(|_| {
638 TransportError::new("transport.failed", "realtime send queue unavailable")
639 })?;
640 outgoing
641 .poller
642 .notify()
643 .map_err(|e| http_err("ws wake", e))?;
644 result.recv_timeout(ROUND_TIMEOUT).map_err(|_| {
645 TransportError::new("transport.failed", "realtime send did not complete")
646 })?
647 })();
648 if result.is_err() {
651 self.shutdown();
652 }
653 result
654 }
655 }
656
657 fn read_body(
658 resp: ureq::http::Response<ureq::Body>,
659 mut progress: Option<&mut dyn FnMut(u64)>,
660 ) -> Result<Vec<u8>, TransportError> {
661 use std::io::Read;
662 let mut reader = resp.into_body().into_with_config().reader();
663 let mut bytes = Vec::new();
664 let mut chunk = [0u8; 64 * 1024];
665 let mut reported = 0;
666 loop {
667 let n = reader.read(&mut chunk).map_err(|e| http_err("read", e))?;
668 if n == 0 {
669 break;
670 }
671 bytes.extend_from_slice(&chunk[..n]);
672 if bytes.len() - reported >= 64 * 1024 {
673 if let Some(callback) = progress.as_mut() {
674 callback(bytes.len() as u64);
675 }
676 reported = bytes.len();
677 }
678 }
679 if bytes.len() != reported {
680 if let Some(callback) = progress {
681 callback(bytes.len() as u64);
682 }
683 }
684 Ok(bytes)
685 }
686
687 impl Transport for NativeTransport {
688 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
689 self.post_sync("/sync", request)
690 }
691
692 fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
693 self.post_operation(request)
694 }
695
696 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
697 if self.outgoing.is_none() {
707 return self.post_sync("/sync", request);
708 }
709 let framed = self.round.begin(request)?;
710 if let Err(error) = self.send_message(Message::Binary(framed.into())) {
711 self.round.fail_in_flight(error);
712 }
713 self.round.wait()
714 }
715
716 fn download_segment(
717 &mut self,
718 request: &SegmentRequest,
719 on_progress: &mut dyn FnMut(u64),
720 ) -> Result<Vec<u8>, TransportError> {
721 let url = format!("{}/segments/{}", self.base_url, request.segment_id);
728 let parsed = url::Url::parse(&url).map_err(|error| segment_error(None, None, error))?;
729 let mut req = self
730 .agent
731 .get(&url)
732 .header("x-syncular-scopes", &request.requested_scopes_json);
733 for (k, v) in &self.headers {
734 req = req.header(k.as_str(), v.as_str());
735 }
736 let resp = req.call().map_err(|error| {
737 let status = match &error {
738 ureq::Error::StatusCode(status) => Some(*status),
739 _ => None,
740 };
741 segment_error(Some(parsed.path()), status, error)
742 })?;
743 let status = resp.status().as_u16();
744 read_body(resp, Some(on_progress))
745 .map_err(|error| segment_error(Some(parsed.path()), Some(status), error.message))
746 }
747
748 fn supports_url_fetch(&self) -> bool {
749 self.signed_urls
750 }
751
752 fn fetch_url(
753 &mut self,
754 url: &str,
755 on_progress: &mut dyn FnMut(u64),
756 ) -> Result<Vec<u8>, TransportError> {
757 let parsed = url::Url::parse(url).map_err(|error| segment_error(None, None, error))?;
759 let resp = self.agent.get(url).call().map_err(|error| {
760 let status = match &error {
761 ureq::Error::StatusCode(status) => Some(*status),
762 _ => None,
763 };
764 segment_error(Some(parsed.path()), status, error)
765 })?;
766 let status = resp.status().as_u16();
767 read_body(resp, Some(on_progress))
768 .map_err(|error| segment_error(Some(parsed.path()), Some(status), error.message))
769 }
770
771 fn blob_upload(
772 &mut self,
773 blob_id: &str,
774 bytes: &[u8],
775 media_type: Option<&str>,
776 ) -> Result<(), TransportError> {
777 let url = format!("{}/blobs/{}", self.base_url, blob_id);
780 let mut req = self.agent.put(&url).header(
781 "content-type",
782 media_type.unwrap_or("application/octet-stream"),
783 );
784 for (k, v) in &self.headers {
785 req = req.header(k.as_str(), v.as_str());
786 }
787 req.send(bytes).map_err(|e| http_err("PUT blob", e))?;
788 Ok(())
789 }
790
791 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
792 let url = format!("{}/blobs/{}", self.base_url, blob_id);
795 let mut req = self.agent.get(&url);
796 for (k, v) in &self.headers {
797 req = req.header(k.as_str(), v.as_str());
798 }
799 let resp = req.call().map_err(|e| {
800 let e = http_err("GET blob", e);
801 if e.code == "transport.failed" {
803 TransportError::new("blob.not_found", e.message)
804 } else {
805 e
806 }
807 })?;
808 let is_json = resp
811 .headers()
812 .get(ureq::http::header::CONTENT_TYPE)
813 .and_then(|value| value.to_str().ok())
814 .is_some_and(|value| value.contains("application/json"));
815 let body = read_body(resp, None)?;
816 if is_json {
817 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&body) {
818 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
819 return Ok(BlobDownload::Url {
820 url: u.to_owned(),
821 url_expires_at_ms: parsed
822 .get("urlExpiresAtMs")
823 .and_then(|v| v.as_i64()),
824 });
825 }
826 }
827 }
828 Ok(BlobDownload::Bytes(body))
829 }
830
831 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
832 self.get_bytes(url, false)
834 }
835
836 fn blob_upload_grant(
837 &mut self,
838 blob_id: &str,
839 byte_length: u64,
840 media_type: Option<&str>,
841 ) -> Result<BlobUploadGrant, TransportError> {
842 let url = format!("{}/blobs/{}/upload-grant", self.base_url, blob_id);
843 let mut req = self
844 .agent
845 .post(&url)
846 .header("content-type", "application/json");
847 for (k, v) in &self.headers {
848 req = req.header(k.as_str(), v.as_str());
849 }
850 let body = serde_json::json!({
851 "byteLength": byte_length,
852 "mediaType": media_type,
853 });
854 let resp = req
855 .send(body.to_string())
856 .map_err(|e| http_err("POST upload-grant", e))?;
857 let grant_body = read_body(resp, None)?;
858 let parsed: serde_json::Value = serde_json::from_slice(&grant_body)
859 .map_err(|e| TransportError::new("transport.failed", format!("read grant: {e}")))?;
860 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
861 return Ok(BlobUploadGrant::Url {
862 url: u.to_owned(),
863 url_expires_at_ms: parsed.get("urlExpiresAtMs").and_then(|v| v.as_i64()),
864 });
865 }
866 if parsed.get("present").and_then(|v| v.as_bool()) == Some(true) {
867 return Ok(BlobUploadGrant::Present);
868 }
869 Ok(BlobUploadGrant::None)
870 }
871
872 fn blob_put_url(
873 &mut self,
874 url: &str,
875 bytes: &[u8],
876 media_type: Option<&str>,
877 ) -> Result<(), TransportError> {
878 let req = self.agent.put(url).header(
880 "content-type",
881 media_type.unwrap_or("application/octet-stream"),
882 );
883 req.send(bytes).map_err(|e| http_err("PUT blob url", e))?;
884 Ok(())
885 }
886
887 fn realtime_connect(&mut self) -> Result<(), TransportError> {
888 if self.outgoing.is_some() {
889 return Ok(());
890 }
891 use tungstenite::client::IntoClientRequest;
897 let mut url = url::Url::parse(&self.ws_url)
898 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
899 if let Some(client_id) = self.realtime_client_id.as_deref() {
900 let retained_query: Vec<(String, String)> = url
901 .query_pairs()
902 .filter(|(key, _)| key != "clientId")
903 .map(|(key, value)| (key.into_owned(), value.into_owned()))
904 .collect();
905 url.set_query(None);
906 url.query_pairs_mut()
907 .extend_pairs(retained_query)
908 .append_pair("clientId", client_id);
909 }
910 let mut request = url
911 .as_str()
912 .into_client_request()
913 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
914 {
915 let out = request.headers_mut();
916 for (k, v) in &self.headers {
917 if let (Ok(name), Ok(value)) = (
918 tungstenite::http::HeaderName::try_from(k.as_str()),
919 tungstenite::http::HeaderValue::try_from(v.as_str()),
920 ) {
921 out.insert(name, value);
922 }
923 }
924 }
925 let (mut ws, _resp) = tungstenite::connect(request)
926 .map_err(|e| TransportError::new("transport.failed", format!("ws connect: {e}")))?;
927 let stream = match ws.get_mut() {
928 MaybeTlsStream::Plain(stream) => stream.try_clone(),
929 MaybeTlsStream::Rustls(stream) => stream.get_ref().try_clone(),
930 _ => {
931 return Err(TransportError::new(
932 "transport.failed",
933 "unsupported websocket stream",
934 ))
935 }
936 }
937 .map_err(|e| http_err("ws clone", e))?;
938 stream
939 .set_nonblocking(true)
940 .map_err(|e| http_err("ws nonblocking", e))?;
941 let poller = Arc::new(Poller::new().map_err(|e| http_err("ws poller", e))?);
942 unsafe { poller.add(&stream, Event::readable(0)) }
944 .map_err(|e| http_err("ws register", e))?;
945 let registration = SocketRegistration {
946 poller: Arc::clone(&poller),
947 stream,
948 };
949 let (outgoing, queued) = mpsc::sync_channel::<Outgoing>(1);
950 self.outgoing = Some(Box::new(SocketSender {
951 queue: outgoing,
952 poller,
953 }));
954 self.reader_stop.store(false, Ordering::SeqCst);
955 let inbound = Arc::clone(&self.inbound);
956 let stop = Arc::clone(&self.reader_stop);
957 let round = Arc::clone(&self.round);
958 self.reader = Some(std::thread::spawn(move || {
959 let mut events = Events::new();
960 let mut pending: Option<mpsc::Sender<Result<(), TransportError>>> = None;
961 let failure = 'io: loop {
962 if stop.load(Ordering::SeqCst) {
963 let _ = ws.close(None);
964 let _ = ws.flush();
965 break TransportError::new(
966 "sync.transport_failed",
967 "realtime disconnected mid-round (§8.7)",
968 );
969 }
970 if pending.is_none() {
971 if let Ok(outgoing) = queued.try_recv() {
972 pending = Some(outgoing.completed);
973 if let Err(error) = ws.write(outgoing.message) {
975 if !is_would_block(&error) {
976 break http_err("ws write", error);
977 }
978 }
979 }
980 }
981 let mut drained = false;
983 for _ in 0..64 {
984 match ws.read() {
985 Ok(Message::Text(text)) => {
986 inbound.push(Inbound::Text(text.to_string()))
987 }
988 Ok(Message::Binary(bytes)) => {
989 if let Some(delta) = round.route_binary(&bytes) {
990 inbound.push(Inbound::Binary(delta));
991 }
992 }
993 Err(error) if is_would_block(&error) => {
994 drained = true;
995 break;
996 }
997 Ok(Message::Close(_)) => {
998 let _ = ws.flush();
999 break 'io TransportError::new(
1000 "sync.transport_failed",
1001 "realtime disconnected mid-round (§8.7)",
1002 );
1003 }
1004 Err(_) => {
1005 break 'io TransportError::new(
1006 "sync.transport_failed",
1007 "realtime disconnected mid-round (§8.7)",
1008 );
1009 }
1010 Ok(_) => {}
1011 }
1012 }
1013 let interest = match ws.flush() {
1014 Ok(()) => {
1015 if let Some(completed) = pending.take() {
1016 let _ = completed.send(Ok(()));
1017 }
1018 Event::readable(0)
1019 }
1020 Err(error) if is_would_block(&error) => Event::all(0),
1021 Err(error) => break http_err("ws flush", error),
1022 };
1023 if !drained {
1024 continue;
1025 }
1026 if let Err(error) = registration.poller.modify(®istration.stream, interest) {
1027 break http_err("ws rearm", error);
1028 }
1029 events.clear();
1030 if let Err(error) = registration.poller.wait(&mut events, None) {
1031 if error.kind() != std::io::ErrorKind::Interrupted {
1032 break http_err("ws wait", error);
1033 }
1034 }
1035 };
1036 if let Some(completed) = pending {
1037 let _ = completed.send(Err(failure.clone()));
1038 }
1039 while let Ok(outgoing) = queued.try_recv() {
1040 let _ = outgoing.completed.send(Err(failure.clone()));
1041 }
1042 round.fail_in_flight(failure);
1043 }));
1044 Ok(())
1045 }
1046
1047 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
1048 self.send_message(Message::Text(text.to_owned().into()))
1049 }
1050
1051 fn realtime_close(&mut self) -> Result<(), TransportError> {
1052 self.shutdown();
1053 Ok(())
1054 }
1055 }
1056}
1057
1058#[cfg(all(test, feature = "native-transport"))]
1059mod tests {
1060 use super::*;
1061 use std::io::{BufRead, BufReader, Write};
1062
1063 #[test]
1064 fn segment_http_failures_have_named_codes_and_request_details() {
1065 for signed in [false, true] {
1066 for body_failure in [false, true] {
1067 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1068 let base = format!("http://{}", listener.local_addr().unwrap());
1069 let server = std::thread::spawn(move || {
1070 let (mut socket, _) = listener.accept().unwrap();
1071 let mut reader = BufReader::new(socket.try_clone().unwrap());
1072 loop {
1073 let mut line = String::new();
1074 reader.read_line(&mut line).unwrap();
1075 if line == "\r\n" {
1076 break;
1077 }
1078 assert!(!line.is_empty());
1079 }
1080 socket.write_all(if body_failure {
1081 b"HTTP/1.1 200 OK\r\nContent-Length: 999\r\nConnection: close\r\n\r\nshort"
1082 } else {
1083 b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
1084 }).unwrap();
1085 });
1086 let mut transport =
1087 HostTransport::new_from_config(&serde_json::json!({"baseUrl": base})).unwrap();
1088 let mut progress = |_| {};
1089 let error = if signed {
1090 transport.fetch_url(&format!("{base}/signed?signature=secret"), &mut progress)
1091 } else {
1092 transport.download_segment(
1093 &SegmentRequest {
1094 segment_id: "sha256:test".into(),
1095 table: "tasks".into(),
1096 requested_scopes_json: "{}".into(),
1097 },
1098 &mut progress,
1099 )
1100 }
1101 .unwrap_err();
1102 assert_eq!(error.code, "sync.transport_failed");
1103 assert_eq!(error.message, "segment transfer failed");
1104 let details = error.details.unwrap();
1105 assert_eq!(
1106 details["path"],
1107 if signed {
1108 "/signed"
1109 } else {
1110 "/segments/sha256:test"
1111 }
1112 );
1113 assert_eq!(details["httpStatus"], if body_failure { 200 } else { 403 });
1114 server.join().unwrap();
1115 }
1116 }
1117 }
1118
1119 #[test]
1120 fn refused_segment_connection_has_a_named_code_without_an_invented_status() {
1121 for signed in [false, true] {
1122 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1123 let base = format!("http://{}", listener.local_addr().unwrap());
1124 drop(listener);
1125 let mut transport =
1126 HostTransport::new_from_config(&serde_json::json!({"baseUrl": base})).unwrap();
1127 let mut progress = |_| {};
1128 let error = if signed {
1129 transport.fetch_url(&format!("{base}/signed?signature=secret"), &mut progress)
1130 } else {
1131 transport.download_segment(
1132 &SegmentRequest {
1133 segment_id: "sha256:test".into(),
1134 table: "tasks".into(),
1135 requested_scopes_json: "{}".into(),
1136 },
1137 &mut progress,
1138 )
1139 }
1140 .unwrap_err();
1141 assert_eq!(error.code, "sync.transport_failed");
1142 assert!(error.details.as_ref().unwrap().get("httpStatus").is_none());
1143 }
1144 }
1145
1146 #[test]
1147 fn segment_http_reports_intermediate_bytes_for_direct_and_signed_urls() {
1148 for signed in [false, true] {
1149 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1150 let base = format!("http://{}", listener.local_addr().unwrap());
1151 let server = std::thread::spawn(move || {
1152 let (mut socket, _) = listener.accept().unwrap();
1153 let mut reader = BufReader::new(socket.try_clone().unwrap());
1154 let mut headers = String::new();
1155 loop {
1156 let mut line = String::new();
1157 reader.read_line(&mut line).unwrap();
1158 if line == "\r\n" {
1159 break;
1160 }
1161 assert!(!line.is_empty());
1162 headers.push_str(&line.to_ascii_lowercase());
1163 }
1164 assert_eq!(headers.contains("authorization: bearer test"), !signed);
1165 assert_eq!(headers.contains("x-syncular-scopes:"), !signed);
1166 let bytes = vec![7; 128 * 1024 + 17];
1167 write!(
1168 socket,
1169 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1170 bytes.len()
1171 )
1172 .unwrap();
1173 socket.write_all(&bytes).unwrap();
1174 });
1175 let mut transport = HostTransport::from_config(
1176 &serde_json::json!({"baseUrl": base, "headers": {"authorization": "Bearer test"}}),
1177 )
1178 .unwrap();
1179 let mut updates = Vec::new();
1180 let mut progress = |bytes| updates.push(bytes);
1181 let bytes = if signed {
1182 transport
1183 .fetch_url(&format!("{base}/signed"), &mut progress)
1184 .unwrap()
1185 } else {
1186 transport
1187 .download_segment(
1188 &SegmentRequest {
1189 segment_id: "sha256:test".into(),
1190 table: "tasks".into(),
1191 requested_scopes_json: "{}".into(),
1192 },
1193 &mut progress,
1194 )
1195 .unwrap()
1196 };
1197 assert_eq!(bytes.len(), 128 * 1024 + 17);
1198 assert!(updates.iter().any(|n| *n > 0 && *n < bytes.len() as u64));
1199 assert_eq!(updates.last(), Some(&(bytes.len() as u64)));
1200 assert!(updates.windows(2).all(|pair| pair[0] < pair[1]));
1201 server.join().unwrap();
1202 }
1203 }
1204}