1use crate::{Backoff, Error, QuicBackend, Reconnect};
2#[cfg(all(feature = "websocket", any(feature = "noq", feature = "quinn", feature = "quiche")))]
3use std::future::Future;
4use std::net;
5use url::Url;
6
7const DEFAULT_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
8
9pub(crate) const DEFAULT_FAILOVER_DELAY: std::time::Duration = std::time::Duration::from_millis(250);
17
18pub(crate) const DEFAULT_RESOLUTION_DELAY: std::time::Duration = std::time::Duration::from_millis(50);
24
25#[derive(Clone, Debug, clap::Parser, serde::Serialize, serde::Deserialize)]
27#[serde(deny_unknown_fields, default)]
28#[non_exhaustive]
29pub struct ClientConfig {
30 #[serde(skip_serializing_if = "Option::is_none")]
38 #[arg(id = "client-connect", long = "client-connect", env = "MOQ_CLIENT_CONNECT")]
39 pub connect: Option<Url>,
40
41 #[arg(
43 id = "client-bind",
44 long = "client-bind",
45 default_value = "[::]:0",
46 env = "MOQ_CLIENT_BIND"
47 )]
48 pub bind: net::SocketAddr,
49
50 #[arg(id = "client-backend", long = "client-backend", env = "MOQ_CLIENT_BACKEND")]
53 pub backend: Option<QuicBackend>,
54
55 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
65 #[arg(
66 id = "client-failover-delay",
67 long = "client-failover-delay",
68 env = "MOQ_CLIENT_FAILOVER_DELAY",
69 value_parser = humantime::parse_duration,
70 )]
71 pub failover_delay: Option<std::time::Duration>,
72
73 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
81 #[arg(
82 id = "client-resolution-delay",
83 long = "client-resolution-delay",
84 env = "MOQ_CLIENT_RESOLUTION_DELAY",
85 value_parser = humantime::parse_duration,
86 )]
87 pub resolution_delay: Option<std::time::Duration>,
88
89 #[arg(
99 id = "client-connect-timeout",
100 long = "client-connect-timeout",
101 env = "MOQ_CLIENT_CONNECT_TIMEOUT",
102 value_parser = humantime::parse_duration,
103 )]
104 #[serde(default, skip_serializing_if = "Option::is_none", with = "humantime_serde::option")]
105 pub timeout: Option<std::time::Duration>,
106
107 #[command(flatten)]
109 #[serde(default)]
110 pub quic: crate::quic::Client,
111
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
118 #[arg(
119 id = "client-version",
120 long = "client-version",
121 env = "MOQ_CLIENT_VERSION",
122 value_parser = crate::version_parser(),
123 )]
124 pub version: Vec<moq_net::Version>,
125
126 #[command(flatten)]
128 #[serde(default)]
129 pub tls: crate::tls::Client,
130
131 #[command(flatten)]
133 #[serde(default)]
134 pub backoff: Backoff,
135
136 #[cfg(feature = "websocket")]
139 #[command(flatten)]
140 #[serde(default)]
141 pub websocket: crate::websocket::Client,
142}
143
144impl ClientConfig {
145 pub fn init(self) -> crate::Result<Client> {
147 Client::new(self)
148 }
149
150 pub fn versions(&self) -> moq_net::Versions {
152 if self.version.is_empty() {
153 moq_net::Versions::all()
154 } else {
155 moq_net::Versions::from(self.version.clone())
156 }
157 }
158
159 pub fn resolved_failover_delay(&self) -> std::time::Duration {
166 self.failover_delay.unwrap_or(DEFAULT_FAILOVER_DELAY)
167 }
168
169 pub fn resolved_resolution_delay(&self) -> std::time::Duration {
173 self.resolution_delay.unwrap_or(DEFAULT_RESOLUTION_DELAY)
174 }
175
176 pub fn resolved_connect_timeout(&self) -> std::time::Duration {
179 self.timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT)
180 }
181}
182
183impl Default for ClientConfig {
184 fn default() -> Self {
185 Self {
186 connect: None,
187 bind: "[::]:0".parse().unwrap(),
188 backend: None,
189 failover_delay: None,
190 resolution_delay: None,
191 timeout: None,
192 quic: crate::quic::Client::default(),
193 version: Vec::new(),
194 tls: crate::tls::Client::default(),
195 backoff: Backoff::default(),
196 #[cfg(feature = "websocket")]
197 websocket: crate::websocket::Client::default(),
198 }
199 }
200}
201
202#[derive(Clone)]
206pub struct Client {
207 moq: moq_net::Client,
208 #[cfg(any(
214 feature = "noq",
215 feature = "quinn",
216 feature = "quiche",
217 feature = "websocket",
218 feature = "tcp",
219 feature = "uds"
220 ))]
221 versions: moq_net::Versions,
222 connect: Option<Url>,
224 timeout: std::time::Duration,
226 backoff: Backoff,
227 #[cfg(feature = "tcp")]
230 failover_delay: std::time::Duration,
231 #[cfg(feature = "tcp")]
232 resolution_delay: std::time::Duration,
233 #[cfg(feature = "websocket")]
234 websocket: crate::websocket::Client,
235 #[cfg(any(feature = "noq", feature = "quinn", feature = "websocket"))]
238 tls: rustls::ClientConfig,
239 #[cfg(feature = "noq")]
240 noq: Option<crate::noq::NoqClient>,
241 #[cfg(feature = "quinn")]
242 quinn: Option<crate::quinn::QuinnClient>,
243 #[cfg(feature = "quiche")]
244 quiche: Option<crate::quiche::QuicheClient>,
245 #[cfg(feature = "iroh")]
246 iroh: Option<crate::iroh::Endpoint>,
247 #[cfg(feature = "iroh")]
248 iroh_addrs: Vec<std::net::SocketAddr>,
249}
250
251impl Client {
252 #[cfg(not(any(
256 feature = "noq",
257 feature = "quinn",
258 feature = "quiche",
259 feature = "iroh",
260 feature = "websocket",
261 feature = "tcp",
262 feature = "uds"
263 )))]
264 pub fn new(_config: ClientConfig) -> crate::Result<Self> {
265 Err(Error::NoBackend(
266 "no backend compiled; enable noq, quinn, quiche, iroh, websocket, tcp, or uds feature",
267 ))
268 }
269
270 #[cfg(any(
272 feature = "noq",
273 feature = "quinn",
274 feature = "quiche",
275 feature = "iroh",
276 feature = "websocket",
277 feature = "tcp",
278 feature = "uds"
279 ))]
280 pub fn new(config: ClientConfig) -> crate::Result<Self> {
281 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
282 let backend = config.backend.clone().unwrap_or_else(crate::default_quic_backend);
283
284 config.quic.validate()?;
285 config.backoff.validate()?;
286
287 #[cfg(any(feature = "noq", feature = "quinn", feature = "websocket"))]
290 let tls = config.tls.build()?;
291
292 #[cfg(feature = "noq")]
293 #[allow(unreachable_patterns)]
294 let noq = match backend {
295 QuicBackend::Noq => Some(crate::noq::NoqClient::new(&config)?),
296 _ => None,
297 };
298
299 #[cfg(feature = "quinn")]
300 #[allow(unreachable_patterns)]
301 let quinn = match backend {
302 QuicBackend::Quinn => Some(crate::quinn::QuinnClient::new(&config)?),
303 _ => None,
304 };
305
306 #[cfg(feature = "quiche")]
307 #[allow(unreachable_patterns)]
308 let quiche = match backend {
309 QuicBackend::Quiche => Some(crate::quiche::QuicheClient::new(&config)?),
310 _ => None,
311 };
312
313 let versions = config.versions();
314 #[cfg(feature = "tcp")]
316 let failover_delay = config.resolved_failover_delay();
317 #[cfg(feature = "tcp")]
318 let resolution_delay = config.resolved_resolution_delay();
319 let timeout = config.resolved_connect_timeout();
320
321 Ok(Self {
322 moq: moq_net::Client::new().with_versions(versions.clone()),
323 #[cfg(any(
324 feature = "noq",
325 feature = "quinn",
326 feature = "quiche",
327 feature = "websocket",
328 feature = "tcp",
329 feature = "uds"
330 ))]
331 versions,
332 connect: config.connect,
333 timeout,
334 backoff: config.backoff,
335 #[cfg(feature = "tcp")]
336 failover_delay,
337 #[cfg(feature = "tcp")]
338 resolution_delay,
339 #[cfg(feature = "websocket")]
340 websocket: config.websocket,
341 #[cfg(any(feature = "noq", feature = "quinn", feature = "websocket"))]
342 tls,
343 #[cfg(feature = "noq")]
344 noq,
345 #[cfg(feature = "quinn")]
346 quinn,
347 #[cfg(feature = "quiche")]
348 quiche,
349 #[cfg(feature = "iroh")]
350 iroh: None,
351 #[cfg(feature = "iroh")]
352 iroh_addrs: Vec::new(),
353 })
354 }
355
356 #[cfg(feature = "iroh")]
361 pub fn with_iroh(mut self, iroh: crate::iroh::Endpoint) -> Self {
362 self.iroh = Some(iroh);
363 self
364 }
365
366 #[cfg(feature = "iroh")]
371 pub fn with_iroh_addrs(mut self, addrs: Vec<std::net::SocketAddr>) -> Self {
372 self.iroh_addrs = addrs;
373 self
374 }
375
376 pub fn with_publisher(mut self, publish: impl moq_net::Consume<moq_net::origin::Consumer>) -> Self {
378 self.moq = self.moq.with_publisher(publish);
379 self
380 }
381
382 pub fn with_subscriber(mut self, subscribe: moq_net::origin::Producer) -> Self {
384 self.moq = self.moq.with_subscriber(subscribe);
385 self
386 }
387
388 pub fn with_stats(mut self, stats: moq_net::stats::Session) -> Self {
391 self.moq = self.moq.with_stats(stats);
392 self
393 }
394
395 pub fn with_cost(mut self, cost: u64) -> Self {
397 self.moq = self.moq.with_cost(cost);
398 self
399 }
400
401 pub fn with_peer_origin(mut self, origin: moq_net::Origin) -> Self {
404 self.moq = self.moq.with_peer_origin(origin);
405 self
406 }
407
408 pub fn reconnect(&self, url: Url) -> Reconnect {
413 Reconnect::new(self.clone(), url, self.backoff.clone())
414 }
415
416 pub fn publish(self, origin: moq_net::origin::Consumer) -> Option<Reconnect> {
422 let url = self.connect.clone()?;
423 Some(self.with_publisher(origin).reconnect(url))
424 }
425
426 pub fn consume(self, origin: moq_net::origin::Producer) -> Option<Reconnect> {
438 let url = self.connect.clone()?;
439 let origin = origin.with_linger(self.backoff.linger());
440 Some(self.with_subscriber(origin).reconnect(url))
441 }
442
443 #[cfg(not(any(
447 feature = "noq",
448 feature = "quinn",
449 feature = "quiche",
450 feature = "iroh",
451 feature = "websocket",
452 feature = "tcp",
453 feature = "uds"
454 )))]
455 pub async fn connect(&self, _url: Url) -> crate::Result<moq_net::Session> {
456 Err(Error::NoBackend(
457 "no backend compiled; enable noq, quinn, quiche, iroh, websocket, tcp, or uds feature",
458 ))
459 }
460
461 #[cfg(any(
468 feature = "noq",
469 feature = "quinn",
470 feature = "quiche",
471 feature = "iroh",
472 feature = "websocket",
473 feature = "tcp",
474 feature = "uds"
475 ))]
476 pub async fn connect(&self, url: Url) -> crate::Result<moq_net::Session> {
477 let attempt = Box::pin(self.connect_inner(url));
480
481 let pair = match self.timeout.is_zero() {
485 true => attempt.await?,
486 false => match tokio::time::timeout(self.timeout, attempt).await {
487 Ok(res) => res?,
488 Err(_) => return Err(Error::ConnectTimeout(self.timeout)),
489 },
490 };
491
492 tracing::info!(version = %pair.0.version(), "connected");
493 Ok(crate::spawn_session(pair))
494 }
495
496 #[cfg(any(
498 feature = "noq",
499 feature = "quinn",
500 feature = "quiche",
501 feature = "iroh",
502 feature = "websocket",
503 feature = "tcp",
504 feature = "uds"
505 ))]
506 fn moq_with_path(&self, path: Option<String>) -> moq_net::Client {
507 match path {
508 Some(path) => self.moq.clone().with_path(path),
509 None => self.moq.clone(),
510 }
511 }
512
513 #[cfg(any(
514 feature = "noq",
515 feature = "quinn",
516 feature = "quiche",
517 feature = "iroh",
518 feature = "websocket",
519 feature = "tcp",
520 feature = "uds"
521 ))]
522 async fn connect_inner(&self, url: Url) -> crate::Result<(moq_net::Session, moq_net::Driver)> {
523 #[allow(unused_variables)]
528 let moq = self.moq_with_path(setup_path(&url));
529
530 #[cfg(feature = "tcp")]
533 if url.scheme() == "tcp" {
534 let session =
535 crate::tcp::connect(url, &self.versions.alpns(), self.failover_delay, self.resolution_delay).await?;
536 return Ok(moq.connect(session).await?);
537 }
538
539 #[cfg(all(feature = "uds", unix))]
542 if url.scheme() == "unix" {
543 let session = crate::unix::connect(url, &self.versions.alpns()).await?;
544 return Ok(moq.connect(session).await?);
545 }
546
547 #[cfg(feature = "iroh")]
552 if url.scheme() == "iroh" {
553 let endpoint = self.iroh.as_ref().ok_or(Error::IrohDisabled)?;
554 let target = request_target(&url);
555 let (session, binding) = crate::iroh::connect(endpoint, url, self.iroh_addrs.iter().copied()).await?;
556
557 let moq = match binding {
558 crate::iroh::Binding::Raw => self.moq_with_path(target),
559 crate::iroh::Binding::H3 => self.moq.clone(),
560 };
561
562 return Ok(moq.connect(session).await?);
563 }
564
565 #[cfg(feature = "noq")]
566 if let Some(noq) = self.noq.as_ref() {
567 let tls = self.tls.clone();
568 let quic_url = url.clone();
569 let quic_handle = async { noq.connect(&tls, quic_url, &self.versions).await.map_err(Error::from) };
570
571 #[cfg(feature = "websocket")]
572 {
573 return self.race_moq_connect(&moq, url, quic_handle).await;
574 }
575
576 #[cfg(not(feature = "websocket"))]
577 {
578 let session = quic_handle.await?;
579 return Ok(moq.connect(session).await?);
580 }
581 }
582
583 #[cfg(feature = "quinn")]
584 if let Some(quinn) = self.quinn.as_ref() {
585 let tls = self.tls.clone();
586 let quic_url = url.clone();
587 let quic_handle = async { quinn.connect(&tls, quic_url, &self.versions).await.map_err(Error::from) };
588
589 #[cfg(feature = "websocket")]
590 {
591 return self.race_moq_connect(&moq, url, quic_handle).await;
592 }
593
594 #[cfg(not(feature = "websocket"))]
595 {
596 let session = quic_handle.await?;
597 return Ok(moq.connect(session).await?);
598 }
599 }
600
601 #[cfg(feature = "quiche")]
602 if let Some(quiche) = self.quiche.as_ref() {
603 let quic_url = url.clone();
604 let quic_handle = async { quiche.connect(quic_url, &self.versions).await.map_err(Error::from) };
605
606 #[cfg(feature = "websocket")]
607 {
608 return self.race_moq_connect(&moq, url, quic_handle).await;
609 }
610
611 #[cfg(not(feature = "websocket"))]
612 {
613 let session = quic_handle.await?;
614 return Ok(moq.connect(session).await?);
615 }
616 }
617
618 #[cfg(feature = "websocket")]
619 {
620 let alpns = self.versions.alpns();
621 let session = crate::websocket::connect(&self.websocket, &self.tls, url, &alpns).await?;
622 return Ok(moq.connect(session).await?);
623 }
624
625 #[cfg(not(feature = "websocket"))]
626 return Err(Error::NoBackend("no QUIC backend matched; this should not happen"));
627 }
628
629 #[cfg(all(feature = "websocket", any(feature = "noq", feature = "quinn", feature = "quiche")))]
638 async fn race_moq_connect<Q, S>(
639 &self,
640 moq: &moq_net::Client,
641 url: Url,
642 quic: Q,
643 ) -> crate::Result<(moq_net::Session, moq_net::Driver)>
644 where
645 Q: Future<Output = crate::Result<S>>,
646 S: web_transport_trait::Session,
647 {
648 let alpns = self.versions.alpns();
649 let ws_config = self.websocket.clone();
650 let ws_tls = self.tls.clone();
651 let websocket = async move {
652 crate::websocket::race_handle(&ws_config, &ws_tls, url, &alpns)
653 .await
654 .map(|res| res.map_err(Error::from))
655 };
656
657 match race_transport_connect(quic, websocket).await? {
658 TransportRace::Quic(quic) => Ok(moq.connect(quic).await?),
659 TransportRace::WebSocket(websocket) => Ok(self.moq.connect(websocket).await?),
660 }
661 }
662}
663
664#[cfg(any(
671 feature = "noq",
672 feature = "quinn",
673 feature = "quiche",
674 feature = "iroh",
675 feature = "websocket",
676 feature = "tcp",
677 feature = "uds"
678))]
679fn request_target(url: &Url) -> Option<String> {
680 let target = match url.query().filter(|query| !query.is_empty()) {
684 Some(query) => format!("{}?{}", url.path(), query),
685 None => url.path().to_owned(),
686 };
687
688 (!target.is_empty()).then_some(target)
689}
690
691#[cfg(any(
699 feature = "noq",
700 feature = "quinn",
701 feature = "quiche",
702 feature = "iroh",
703 feature = "websocket",
704 feature = "tcp",
705 feature = "uds"
706))]
707fn setup_path(url: &Url) -> Option<String> {
708 match url.scheme() {
709 "unix" => url
713 .query_pairs()
714 .find(|(k, _)| k == "path")
715 .map(|(_, v)| v.into_owned())
716 .filter(|path| !path.is_empty()),
717 "moqt" | "moql" | "tcp" => request_target(url),
720 _ => None,
721 }
722}
723
724#[cfg(all(feature = "websocket", any(feature = "noq", feature = "quinn", feature = "quiche")))]
725#[derive(Debug, PartialEq, Eq)]
726enum TransportRace<Q, W> {
727 Quic(Q),
728 WebSocket(W),
729}
730
731#[cfg(all(feature = "websocket", any(feature = "noq", feature = "quinn", feature = "quiche")))]
732async fn race_transport_connect<Q, W, QT, WT>(quic: Q, websocket: W) -> crate::Result<TransportRace<QT, WT>>
733where
734 Q: Future<Output = crate::Result<QT>>,
735 W: Future<Output = Option<crate::Result<WT>>>,
736{
737 tokio::pin!(quic);
738 tokio::pin!(websocket);
739
740 let mut quic_err = None;
741 let mut websocket_err = None;
742 let mut quic_done = false;
743 let mut websocket_done = false;
744
745 loop {
746 tokio::select! {
747 res = &mut quic, if !quic_done => {
748 match res {
749 Ok(session) => return Ok(TransportRace::Quic(session)),
750 Err(err) if err.is_auth() => return Err(err),
751 Err(err) => {
752 tracing::warn!(%err, "QUIC connection failed");
753 quic_err = Some(err);
754 quic_done = true;
755 }
756 }
757 }
758 res = &mut websocket, if !websocket_done => {
759 match res {
760 Some(Ok(session)) => return Ok(TransportRace::WebSocket(session)),
761 Some(Err(err)) if err.is_auth() => return Err(err),
762 Some(Err(err)) => {
763 tracing::warn!(%err, "WebSocket connection failed");
764 websocket_err = Some(err);
765 websocket_done = true;
766 }
767 None => {
768 websocket_done = true;
769 }
770 }
771 }
772 else => break,
773 }
774
775 if quic_done && websocket_done {
776 break;
777 }
778 }
779
780 match (quic_err, websocket_err) {
781 (Some(quic), Some(websocket)) => Err(Error::TransportRace {
782 quic: std::sync::Arc::new(quic),
783 websocket: std::sync::Arc::new(websocket),
784 }),
785 (Some(err), None) | (None, Some(err)) => Err(err),
786 (None, None) => Err(Error::ConnectFailed),
787 }
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793 use clap::{CommandFactory, Parser};
794
795 #[cfg(any(
796 feature = "noq",
797 feature = "quinn",
798 feature = "quiche",
799 feature = "iroh",
800 feature = "websocket",
801 feature = "tcp",
802 feature = "uds"
803 ))]
804 #[test]
805 fn setup_path_covers_the_uri_less_transports() {
806 let cases = [
809 ("unix:///run/moq.sock?path=/room", Some("/room")),
810 ("unix:///run/moq.sock?path=/room%3Fjwt%3Dabc", Some("/room?jwt=abc")),
813 ("unix:///run/moq.sock?path=", None),
814 ("unix:///run/moq.sock", None),
815 ("tcp://localhost:4443/room", Some("/room")),
816 ("tcp://localhost:4443/room?jwt=abc", Some("/room?jwt=abc")),
817 ("tcp://localhost:4443", None),
818 ("moqt://relay.example.com/anon", Some("/anon")),
821 ("moqt://relay.example.com/anon?jwt=abc", Some("/anon?jwt=abc")),
822 ("moql://relay.example.com/anon?jwt=abc", Some("/anon?jwt=abc")),
823 ("moqt://relay.example.com", None),
824 ("moqt://relay.example.com/anon?jwt=abc#pos:12", Some("/anon?jwt=abc")),
826 ("moqt://relay.example.com/anon#pos:12", Some("/anon")),
827 ("moqt://relay.example.com/anon?", Some("/anon")),
829 ("moqt://relay.example.com?", None),
830 ("https://relay.example.com/anon?jwt=abc", None),
833 ("http://relay.example.com/anon", None),
834 ("wss://relay.example.com/anon?jwt=abc", None),
835 ("iroh://k5lnrlndqpqcgh4d5nhbnbnhcyrgvw6ttxwrsvsu4nlt6foorxaa/anon", None),
837 ];
838
839 for (url, want) in cases {
840 let url = Url::parse(url).unwrap();
841 let got = setup_path(&url);
842 assert_eq!(got.as_deref(), want, "{url}");
843 }
844 }
845
846 #[cfg(any(
849 feature = "noq",
850 feature = "quinn",
851 feature = "quiche",
852 feature = "iroh",
853 feature = "websocket",
854 feature = "tcp",
855 feature = "uds"
856 ))]
857 #[test]
858 fn request_target_joins_the_path_and_query() {
859 const PEER: &str = "k5lnrlndqpqcgh4d5nhbnbnhcyrgvw6ttxwrsvsu4nlt6foorxaa";
860
861 let cases = [
862 (format!("iroh://{PEER}/room?jwt=abc"), Some("/room?jwt=abc")),
863 (format!("iroh://{PEER}/room"), Some("/room")),
864 (format!("iroh://{PEER}"), None),
865 (format!("iroh://{PEER}/"), Some("/")),
866 ];
867
868 for (url, want) in cases {
869 let url = Url::parse(&url).unwrap();
870 let got = request_target(&url);
871 assert_eq!(got.as_deref(), want, "{url}");
872 }
873 }
874
875 #[test]
876 fn test_toml_disable_verify_survives_update_from() {
877 let toml = r#"
878 tls.disable_verify = true
879 "#;
880
881 let mut config: ClientConfig = toml::from_str(toml).unwrap();
882 assert_eq!(config.tls.disable_verify, Some(true));
883
884 config.update_from(["test"]);
886 assert_eq!(config.tls.disable_verify, Some(true));
887 }
888
889 #[test]
890 fn test_cli_disable_verify_flag() {
891 let config = ClientConfig::parse_from(["test", "--client-tls-disable-verify"]);
892 assert_eq!(config.tls.disable_verify, Some(true));
893 }
894
895 #[test]
896 fn test_cli_disable_verify_explicit_false() {
897 let config = ClientConfig::parse_from(["test", "--client-tls-disable-verify=false"]);
898 assert_eq!(config.tls.disable_verify, Some(false));
899 }
900
901 #[test]
902 fn test_cli_disable_verify_explicit_true() {
903 let config = ClientConfig::parse_from(["test", "--client-tls-disable-verify=true"]);
904 assert_eq!(config.tls.disable_verify, Some(true));
905 }
906
907 #[test]
908 fn test_cli_deprecated_tls_flags_fold_into_canonical() {
909 let config = ClientConfig::parse_from(["test", "--tls-disable-verify=true", "--tls-fingerprint", "abcd1234"]);
913 assert_eq!(
914 config.tls.disable_verify, None,
915 "deprecated flag must not set the canonical field"
916 );
917 assert_eq!(config.tls.effective_disable_verify(), Some(true));
918 assert_eq!(config.tls.effective_fingerprint(), vec!["abcd1234"]);
919 }
920
921 #[test]
922 fn test_canonical_tls_flag_wins_over_deprecated() {
923 let config = ClientConfig::parse_from([
925 "test",
926 "--client-tls-disable-verify=false",
927 "--tls-disable-verify=true",
928 "--client-tls-fingerprint",
929 "aaaa",
930 "--tls-fingerprint",
931 "bbbb",
932 ]);
933 assert_eq!(config.tls.effective_disable_verify(), Some(false));
934 assert_eq!(config.tls.effective_fingerprint(), vec!["aaaa", "bbbb"]);
935 }
936
937 #[test]
938 fn test_cli_no_disable_verify() {
939 let config = ClientConfig::parse_from(["test"]);
940 assert_eq!(config.tls.disable_verify, None);
941 }
942
943 #[test]
944 fn test_toml_failover_delay_survives_update_from() {
945 let toml = r#"
946 failover_delay = "1s"
947 "#;
948
949 let mut config: ClientConfig = toml::from_str(toml).unwrap();
950 assert_eq!(config.failover_delay, Some(std::time::Duration::from_secs(1)));
951
952 config.update_from(["test"]);
954 assert_eq!(config.failover_delay, Some(std::time::Duration::from_secs(1)));
955 }
956
957 #[test]
958 fn test_cli_failover_delay() {
959 let config = ClientConfig::parse_from(["test", "--client-failover-delay", "50ms"]);
960 assert_eq!(config.failover_delay, Some(std::time::Duration::from_millis(50)));
961 }
962
963 #[test]
964 fn test_toml_resolution_delay_survives_update_from() {
965 let toml = r#"
966 resolution_delay = "10ms"
967 "#;
968
969 let mut config: ClientConfig = toml::from_str(toml).unwrap();
970 assert_eq!(config.resolution_delay, Some(std::time::Duration::from_millis(10)));
971
972 config.update_from(["test"]);
974 assert_eq!(config.resolution_delay, Some(std::time::Duration::from_millis(10)));
975 }
976
977 #[test]
978 fn test_cli_resolution_delay() {
979 let config = ClientConfig::parse_from(["test", "--client-resolution-delay", "0s"]);
980 assert_eq!(config.resolution_delay, Some(std::time::Duration::ZERO));
981 assert_eq!(config.resolved_resolution_delay(), std::time::Duration::ZERO);
982 }
983
984 #[test]
985 fn resolution_delay_defaults_to_the_rfc_value() {
986 let config = ClientConfig::parse_from(["test"]);
987 assert_eq!(config.resolution_delay, None);
988 assert_eq!(config.resolved_resolution_delay(), std::time::Duration::from_millis(50));
989 }
990
991 #[test]
992 fn test_toml_fingerprint_survives_update_from() {
993 let toml = r#"
994 tls.fingerprint = ["abcd1234", "ef567890"]
995 "#;
996
997 let mut config: ClientConfig = toml::from_str(toml).unwrap();
998 assert_eq!(config.tls.fingerprint, vec!["abcd1234", "ef567890"]);
999
1000 config.update_from(["test"]);
1002 assert_eq!(config.tls.fingerprint, vec!["abcd1234", "ef567890"]);
1003 }
1004
1005 #[test]
1006 fn test_toml_fingerprint_accepts_single_string() {
1007 let toml = r#"
1008 tls.fingerprint = "abcd1234"
1009 "#;
1010
1011 let config: ClientConfig = toml::from_str(toml).unwrap();
1012 assert_eq!(config.tls.fingerprint, vec!["abcd1234"]);
1013 }
1014
1015 #[test]
1016 fn test_cli_fingerprint() {
1017 let config = ClientConfig::parse_from(["test", "--client-tls-fingerprint", "abcd1234"]);
1018 assert_eq!(config.tls.fingerprint, vec!["abcd1234"]);
1019 }
1020
1021 #[test]
1022 fn test_toml_version_survives_update_from() {
1023 let toml = r#"
1024 version = ["moq-lite-02"]
1025 "#;
1026
1027 let mut config: ClientConfig = toml::from_str(toml).unwrap();
1028 assert_eq!(config.version, vec!["moq-lite-02".parse::<moq_net::Version>().unwrap()]);
1029
1030 config.update_from(["test"]);
1032 assert_eq!(config.version, vec!["moq-lite-02".parse::<moq_net::Version>().unwrap()]);
1033 }
1034
1035 #[test]
1036 fn test_cli_version() {
1037 let config = ClientConfig::parse_from(["test", "--client-version", "moq-lite-03"]);
1038 assert_eq!(config.version, vec!["moq-lite-03".parse::<moq_net::Version>().unwrap()]);
1039 }
1040
1041 #[test]
1042 fn test_cli_version_help_lists_every_parseable_name() {
1043 let help = ClientConfig::command().render_long_help().to_string();
1044 for name in moq_net::Version::names() {
1045 assert!(help.contains(name), "missing {name} from --client-version help");
1046 }
1047 }
1048
1049 #[test]
1050 fn test_toml_connect_survives_update_from() {
1051 let toml = r#"
1052 connect = "https://relay.example.com/anon"
1053 "#;
1054
1055 let mut config: ClientConfig = toml::from_str(toml).unwrap();
1056 assert_eq!(
1057 config.connect.as_ref().unwrap().as_str(),
1058 "https://relay.example.com/anon"
1059 );
1060
1061 config.update_from(["test"]);
1063 assert_eq!(
1064 config.connect.as_ref().unwrap().as_str(),
1065 "https://relay.example.com/anon"
1066 );
1067 }
1068
1069 #[test]
1070 fn test_cli_connect() {
1071 let config = ClientConfig::parse_from(["test", "--client-connect", "https://relay.example.com/anon"]);
1072 assert_eq!(
1073 config.connect.as_ref().unwrap().as_str(),
1074 "https://relay.example.com/anon"
1075 );
1076 }
1077
1078 #[test]
1079 fn test_toml_host_name_survives_update_from() {
1080 let toml = r#"
1081 tls.host_name = "example.host"
1082 "#;
1083
1084 let mut config: ClientConfig = toml::from_str(toml).unwrap();
1085 assert_eq!(config.tls.host_name.as_deref(), Some("example.host"));
1086
1087 config.update_from(["test"]);
1089 assert_eq!(config.tls.host_name.as_deref(), Some("example.host"));
1090 }
1091
1092 #[test]
1093 fn test_cli_host_name() {
1094 let config = ClientConfig::parse_from(["test", "--client-tls-host-name", "override.example"]);
1095 assert_eq!(config.tls.host_name.as_deref(), Some("override.example"));
1096 }
1097
1098 #[test]
1099 fn test_cli_no_version_defaults_to_all() {
1100 let config = ClientConfig::parse_from(["test"]);
1101 assert!(config.version.is_empty());
1102 assert_eq!(config.versions().alpns().len(), moq_net::ALPNS.len());
1104 }
1105
1106 #[cfg(all(feature = "websocket", any(feature = "noq", feature = "quinn", feature = "quiche")))]
1107 #[tokio::test]
1108 async fn race_transport_connect_stops_on_quic_auth_error() {
1109 let quic = async { Err::<usize, _>(crate::ConnectError::Unauthorized.into()) };
1110 let websocket = async {
1111 tokio::task::yield_now().await;
1113 Some(Ok(1usize))
1114 };
1115
1116 let err = super::race_transport_connect(quic, websocket).await.unwrap_err();
1117 assert_eq!(err.connect_error(), Some(crate::ConnectError::Unauthorized));
1118 }
1119
1120 #[cfg(all(feature = "websocket", any(feature = "noq", feature = "quinn", feature = "quiche")))]
1121 #[tokio::test]
1122 async fn race_transport_connect_keeps_websocket_after_quic_non_auth_error() {
1123 let quic = async { Err::<usize, _>(Error::ConnectFailed) };
1124 let websocket = async { Some(Ok(7usize)) };
1125
1126 let value = super::race_transport_connect(quic, websocket).await.unwrap();
1127 assert_eq!(value, super::TransportRace::WebSocket(7));
1128 }
1129
1130 #[cfg(all(feature = "websocket", any(feature = "noq", feature = "quinn", feature = "quiche")))]
1131 #[tokio::test]
1132 async fn race_transport_connect_returns_when_quic_transport_connects() {
1133 let quic = async { Ok("quic") };
1134 let websocket = std::future::pending::<Option<crate::Result<&str>>>();
1135
1136 let value = tokio::time::timeout(
1137 std::time::Duration::from_secs(1),
1138 super::race_transport_connect(quic, websocket),
1139 )
1140 .await
1141 .expect("race waited for WebSocket after QUIC transport connected")
1142 .unwrap();
1143 assert_eq!(value, super::TransportRace::Quic("quic"));
1144 }
1145
1146 #[test]
1149 fn failover_delay_defaults_to_the_rfc_8305_stagger() {
1150 let config = ClientConfig::parse_from(["test"]);
1151 assert_eq!(config.failover_delay, None);
1152 assert_eq!(config.resolved_failover_delay(), std::time::Duration::from_millis(250));
1153 }
1154
1155 #[cfg(all(
1158 feature = "iroh",
1159 not(any(feature = "noq", feature = "quinn", feature = "websocket"))
1160 ))]
1161 #[test]
1162 fn iroh_only_client_does_not_require_a_tls_provider() {
1163 ClientConfig::default().init().expect("iroh-only client");
1164 }
1165
1166 #[test]
1167 fn connect_timeout_defaults_to_thirty_seconds() {
1168 let config = ClientConfig::parse_from(["test"]);
1169 assert_eq!(config.timeout, None);
1170 assert_eq!(config.resolved_connect_timeout(), DEFAULT_CONNECT_TIMEOUT);
1171 }
1172
1173 #[cfg(feature = "websocket")]
1181 #[tokio::test]
1182 async fn connect_times_out_against_a_peer_that_never_speaks() {
1183 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1184 let addr = listener.local_addr().unwrap();
1185
1186 let timeout = DEFAULT_CONNECT_TIMEOUT;
1187 let mut config = ClientConfig {
1188 timeout: Some(timeout),
1189 ..Default::default()
1190 };
1191 config.websocket.delay = Some(std::time::Duration::ZERO);
1192 let client = config.init().unwrap();
1193
1194 let url: Url = format!("https://127.0.0.1:{}/", addr.port()).parse().unwrap();
1197
1198 let mut attempt = Box::pin(client.connect(url));
1199 let _silent = tokio::select! {
1200 res = &mut attempt => match res {
1201 Err(err) => panic!("connect failed before the silent peer accepted it: {err}"),
1202 Ok(_) => panic!("connected to a peer that never spoke"),
1203 },
1204 res = listener.accept() => res.unwrap().0,
1205 };
1206
1207 tokio::time::pause();
1210 tokio::time::advance(timeout).await;
1211
1212 let err = match attempt.await {
1213 Err(err) => err,
1214 Ok(_) => panic!("connected to a peer that never spoke"),
1215 };
1216
1217 assert!(matches!(err, Error::ConnectTimeout(_)), "unexpected error: {err}");
1218 }
1219}