1use crate::QuicBackend;
2use crate::crypto;
3use anyhow::Context;
4use std::path::PathBuf;
5use std::{net, sync::Arc};
6use url::Url;
7
8#[derive(Clone, Default, Debug, clap::Args, serde::Serialize, serde::Deserialize)]
10#[serde(default, deny_unknown_fields)]
11#[non_exhaustive]
12pub struct ClientTls {
13 #[serde(skip_serializing_if = "Vec::is_empty")]
18 #[arg(id = "tls-root", long = "tls-root", env = "MOQ_CLIENT_TLS_ROOT")]
19 pub root: Vec<PathBuf>,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
25 #[arg(
26 id = "tls-disable-verify",
27 long = "tls-disable-verify",
28 env = "MOQ_CLIENT_TLS_DISABLE_VERIFY",
29 default_missing_value = "true",
30 num_args = 0..=1,
31 require_equals = true,
32 value_parser = clap::value_parser!(bool),
33 )]
34 pub disable_verify: Option<bool>,
35}
36
37#[derive(Clone, Debug, clap::Parser, serde::Serialize, serde::Deserialize)]
39#[serde(deny_unknown_fields, default)]
40#[non_exhaustive]
41pub struct ClientConfig {
42 #[arg(
44 id = "client-bind",
45 long = "client-bind",
46 default_value = "[::]:0",
47 env = "MOQ_CLIENT_BIND"
48 )]
49 pub bind: net::SocketAddr,
50
51 #[arg(id = "client-backend", long = "client-backend", env = "MOQ_CLIENT_BACKEND")]
54 pub backend: Option<QuicBackend>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
58 #[arg(
59 id = "client-max-streams",
60 long = "client-max-streams",
61 env = "MOQ_CLIENT_MAX_STREAMS"
62 )]
63 pub max_streams: Option<u64>,
64
65 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 #[arg(id = "client-version", long = "client-version", env = "MOQ_CLIENT_VERSION")]
74 pub version: Vec<moq_lite::Version>,
75
76 #[command(flatten)]
77 #[serde(default)]
78 pub tls: ClientTls,
79
80 #[cfg(feature = "websocket")]
81 #[command(flatten)]
82 #[serde(default)]
83 pub websocket: super::ClientWebSocket,
84}
85
86impl ClientConfig {
87 pub fn init(self) -> anyhow::Result<Client> {
88 Client::new(self)
89 }
90
91 pub fn versions(&self) -> moq_lite::Versions {
93 if self.version.is_empty() {
94 moq_lite::Versions::all()
95 } else {
96 moq_lite::Versions::from(self.version.clone())
97 }
98 }
99}
100
101impl Default for ClientConfig {
102 fn default() -> Self {
103 Self {
104 bind: "[::]:0".parse().unwrap(),
105 backend: None,
106 max_streams: None,
107 version: Vec::new(),
108 tls: ClientTls::default(),
109 #[cfg(feature = "websocket")]
110 websocket: super::ClientWebSocket::default(),
111 }
112 }
113}
114
115#[derive(Clone)]
119pub struct Client {
120 moq: moq_lite::Client,
121 versions: moq_lite::Versions,
122 #[cfg(feature = "websocket")]
123 websocket: super::ClientWebSocket,
124 tls: rustls::ClientConfig,
125 #[cfg(feature = "noq")]
126 noq: Option<crate::noq::NoqClient>,
127 #[cfg(feature = "quinn")]
128 quinn: Option<crate::quinn::QuinnClient>,
129 #[cfg(feature = "quiche")]
130 quiche: Option<crate::quiche::QuicheClient>,
131 #[cfg(feature = "iroh")]
132 iroh: Option<web_transport_iroh::iroh::Endpoint>,
133 #[cfg(feature = "iroh")]
134 iroh_addrs: Vec<std::net::SocketAddr>,
135}
136
137impl Client {
138 #[cfg(not(any(feature = "noq", feature = "quinn", feature = "quiche")))]
139 pub fn new(_config: ClientConfig) -> anyhow::Result<Self> {
140 anyhow::bail!("no QUIC backend compiled; enable noq, quinn, or quiche feature");
141 }
142
143 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
145 pub fn new(config: ClientConfig) -> anyhow::Result<Self> {
146 let backend = config.backend.clone().unwrap_or({
147 #[cfg(feature = "quinn")]
148 {
149 QuicBackend::Quinn
150 }
151 #[cfg(all(feature = "noq", not(feature = "quinn")))]
152 {
153 QuicBackend::Noq
154 }
155 #[cfg(all(feature = "quiche", not(feature = "quinn"), not(feature = "noq")))]
156 {
157 QuicBackend::Quiche
158 }
159 #[cfg(all(not(feature = "quiche"), not(feature = "quinn"), not(feature = "noq")))]
160 panic!("no QUIC backend compiled; enable noq, quinn, or quiche feature");
161 });
162
163 let provider = crypto::provider();
164
165 let mut roots = rustls::RootCertStore::empty();
167
168 if config.tls.root.is_empty() {
169 let native = rustls_native_certs::load_native_certs();
170
171 for err in native.errors {
173 tracing::warn!(%err, "failed to load root cert");
174 }
175
176 for cert in native.certs {
178 roots.add(cert).context("failed to add root cert")?;
179 }
180 } else {
181 for root in &config.tls.root {
183 let root = std::fs::File::open(root).context("failed to open root cert file")?;
184 let mut root = std::io::BufReader::new(root);
185
186 let root = rustls_pemfile::certs(&mut root)
187 .next()
188 .context("no roots found")?
189 .context("failed to read root cert")?;
190
191 roots.add(root).context("failed to add root cert")?;
192 }
193 }
194
195 let mut tls = rustls::ClientConfig::builder_with_provider(provider.clone())
199 .with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])?
200 .with_root_certificates(roots)
201 .with_no_client_auth();
202
203 if config.tls.disable_verify.unwrap_or_default() {
205 tracing::warn!("TLS server certificate verification is disabled; A man-in-the-middle attack is possible.");
206
207 let noop = NoCertificateVerification(provider.clone());
208 tls.dangerous().set_certificate_verifier(Arc::new(noop));
209 }
210
211 #[cfg(feature = "noq")]
212 #[allow(unreachable_patterns)]
213 let noq = match backend {
214 QuicBackend::Noq => Some(crate::noq::NoqClient::new(&config)?),
215 _ => None,
216 };
217
218 #[cfg(feature = "quinn")]
219 #[allow(unreachable_patterns)]
220 let quinn = match backend {
221 QuicBackend::Quinn => Some(crate::quinn::QuinnClient::new(&config)?),
222 _ => None,
223 };
224
225 #[cfg(feature = "quiche")]
226 let quiche = match backend {
227 QuicBackend::Quiche => Some(crate::quiche::QuicheClient::new(&config)?),
228 _ => None,
229 };
230
231 let versions = config.versions();
232 Ok(Self {
233 moq: moq_lite::Client::new().with_versions(versions.clone()),
234 versions,
235 #[cfg(feature = "websocket")]
236 websocket: config.websocket,
237 tls,
238 #[cfg(feature = "noq")]
239 noq,
240 #[cfg(feature = "quinn")]
241 quinn,
242 #[cfg(feature = "quiche")]
243 quiche,
244 #[cfg(feature = "iroh")]
245 iroh: None,
246 #[cfg(feature = "iroh")]
247 iroh_addrs: Vec::new(),
248 })
249 }
250
251 #[cfg(feature = "iroh")]
252 pub fn with_iroh(mut self, iroh: Option<web_transport_iroh::iroh::Endpoint>) -> Self {
253 self.iroh = iroh;
254 self
255 }
256
257 #[cfg(feature = "iroh")]
262 pub fn with_iroh_addrs(mut self, addrs: Vec<std::net::SocketAddr>) -> Self {
263 self.iroh_addrs = addrs;
264 self
265 }
266
267 pub fn with_publish(mut self, publish: impl Into<Option<moq_lite::OriginConsumer>>) -> Self {
268 self.moq = self.moq.with_publish(publish);
269 self
270 }
271
272 pub fn with_consume(mut self, consume: impl Into<Option<moq_lite::OriginProducer>>) -> Self {
273 self.moq = self.moq.with_consume(consume);
274 self
275 }
276
277 #[cfg(not(any(feature = "noq", feature = "quinn", feature = "quiche", feature = "iroh")))]
278 pub async fn connect(&self, _url: Url) -> anyhow::Result<moq_lite::Session> {
279 anyhow::bail!("no QUIC backend compiled; enable noq, quinn, quiche, or iroh feature");
280 }
281
282 #[cfg(any(feature = "noq", feature = "quinn", feature = "quiche", feature = "iroh"))]
283 pub async fn connect(&self, url: Url) -> anyhow::Result<moq_lite::Session> {
284 #[cfg(feature = "iroh")]
285 if url.scheme() == "iroh" {
286 let endpoint = self.iroh.as_ref().context("Iroh support is not enabled")?;
287 let session = crate::iroh::connect(endpoint, url, self.iroh_addrs.iter().copied()).await?;
288 let session = self.moq.connect(session).await?;
289 return Ok(session);
290 }
291
292 #[cfg(feature = "noq")]
293 if let Some(noq) = self.noq.as_ref() {
294 let tls = self.tls.clone();
295 let quic_url = url.clone();
296 let quic_handle = async {
297 let res = noq.connect(&tls, quic_url).await;
298 if let Err(err) = &res {
299 tracing::warn!(%err, "QUIC connection failed");
300 }
301 res
302 };
303
304 #[cfg(feature = "websocket")]
305 {
306 let alpns = self.versions.alpns();
307 let ws_handle = crate::websocket::race_handle(&self.websocket, &self.tls, url, &alpns);
308
309 return Ok(tokio::select! {
310 Ok(quic) = quic_handle => self.moq.connect(quic).await?,
311 Some(Ok(ws)) = ws_handle => self.moq.connect(ws).await?,
312 else => anyhow::bail!("failed to connect to server"),
313 });
314 }
315
316 #[cfg(not(feature = "websocket"))]
317 {
318 let session = quic_handle.await?;
319 return Ok(self.moq.connect(session).await?);
320 }
321 }
322
323 #[cfg(feature = "quinn")]
324 if let Some(quinn) = self.quinn.as_ref() {
325 let tls = self.tls.clone();
326 let quic_url = url.clone();
327 let quic_handle = async {
328 let res = quinn.connect(&tls, quic_url).await;
329 if let Err(err) = &res {
330 tracing::warn!(%err, "QUIC connection failed");
331 }
332 res
333 };
334
335 #[cfg(feature = "websocket")]
336 {
337 let alpns = self.versions.alpns();
338 let ws_handle = crate::websocket::race_handle(&self.websocket, &self.tls, url, &alpns);
339
340 return Ok(tokio::select! {
341 Ok(quic) = quic_handle => self.moq.connect(quic).await?,
342 Some(Ok(ws)) = ws_handle => self.moq.connect(ws).await?,
343 else => anyhow::bail!("failed to connect to server"),
344 });
345 }
346
347 #[cfg(not(feature = "websocket"))]
348 {
349 let session = quic_handle.await?;
350 return Ok(self.moq.connect(session).await?);
351 }
352 }
353
354 #[cfg(feature = "quiche")]
355 if let Some(quiche) = self.quiche.as_ref() {
356 let quic_url = url.clone();
357 let quic_handle = async {
358 let res = quiche.connect(quic_url).await;
359 if let Err(err) = &res {
360 tracing::warn!(%err, "QUIC connection failed");
361 }
362 res
363 };
364
365 #[cfg(feature = "websocket")]
366 {
367 let alpns = self.versions.alpns();
368 let ws_handle = crate::websocket::race_handle(&self.websocket, &self.tls, url, &alpns);
369
370 return Ok(tokio::select! {
371 Ok(quic) = quic_handle => self.moq.connect(quic).await?,
372 Some(Ok(ws)) = ws_handle => self.moq.connect(ws).await?,
373 else => anyhow::bail!("failed to connect to server"),
374 });
375 }
376
377 #[cfg(not(feature = "websocket"))]
378 {
379 let session = quic_handle.await?;
380 return Ok(self.moq.connect(session).await?);
381 }
382 }
383
384 anyhow::bail!("no QUIC backend compiled; enable noq, quinn, or quiche feature");
385 }
386}
387
388use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
389
390#[derive(Debug)]
391struct NoCertificateVerification(crypto::Provider);
392
393impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
394 fn verify_server_cert(
395 &self,
396 _end_entity: &CertificateDer<'_>,
397 _intermediates: &[CertificateDer<'_>],
398 _server_name: &ServerName<'_>,
399 _ocsp: &[u8],
400 _now: UnixTime,
401 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
402 Ok(rustls::client::danger::ServerCertVerified::assertion())
403 }
404
405 fn verify_tls12_signature(
406 &self,
407 message: &[u8],
408 cert: &CertificateDer<'_>,
409 dss: &rustls::DigitallySignedStruct,
410 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
411 rustls::crypto::verify_tls12_signature(message, cert, dss, &self.0.signature_verification_algorithms)
412 }
413
414 fn verify_tls13_signature(
415 &self,
416 message: &[u8],
417 cert: &CertificateDer<'_>,
418 dss: &rustls::DigitallySignedStruct,
419 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
420 rustls::crypto::verify_tls13_signature(message, cert, dss, &self.0.signature_verification_algorithms)
421 }
422
423 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
424 self.0.signature_verification_algorithms.supported_schemes()
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use clap::Parser;
432
433 #[test]
434 fn test_toml_disable_verify_survives_update_from() {
435 let toml = r#"
436 tls.disable_verify = true
437 "#;
438
439 let mut config: ClientConfig = toml::from_str(toml).unwrap();
440 assert_eq!(config.tls.disable_verify, Some(true));
441
442 config.update_from(["test"]);
444 assert_eq!(config.tls.disable_verify, Some(true));
445 }
446
447 #[test]
448 fn test_cli_disable_verify_flag() {
449 let config = ClientConfig::parse_from(["test", "--tls-disable-verify"]);
450 assert_eq!(config.tls.disable_verify, Some(true));
451 }
452
453 #[test]
454 fn test_cli_disable_verify_explicit_false() {
455 let config = ClientConfig::parse_from(["test", "--tls-disable-verify=false"]);
456 assert_eq!(config.tls.disable_verify, Some(false));
457 }
458
459 #[test]
460 fn test_cli_disable_verify_explicit_true() {
461 let config = ClientConfig::parse_from(["test", "--tls-disable-verify=true"]);
462 assert_eq!(config.tls.disable_verify, Some(true));
463 }
464
465 #[test]
466 fn test_cli_no_disable_verify() {
467 let config = ClientConfig::parse_from(["test"]);
468 assert_eq!(config.tls.disable_verify, None);
469 }
470
471 #[test]
472 fn test_toml_version_survives_update_from() {
473 let toml = r#"
474 version = ["moq-lite-02"]
475 "#;
476
477 let mut config: ClientConfig = toml::from_str(toml).unwrap();
478 assert_eq!(
479 config.version,
480 vec!["moq-lite-02".parse::<moq_lite::Version>().unwrap()]
481 );
482
483 config.update_from(["test"]);
485 assert_eq!(
486 config.version,
487 vec!["moq-lite-02".parse::<moq_lite::Version>().unwrap()]
488 );
489 }
490
491 #[test]
492 fn test_cli_version() {
493 let config = ClientConfig::parse_from(["test", "--client-version", "moq-lite-03"]);
494 assert_eq!(
495 config.version,
496 vec!["moq-lite-03".parse::<moq_lite::Version>().unwrap()]
497 );
498 }
499
500 #[test]
501 fn test_cli_no_version_defaults_to_all() {
502 let config = ClientConfig::parse_from(["test"]);
503 assert!(config.version.is_empty());
504 assert_eq!(config.versions().alpns().len(), moq_lite::ALPNS.len());
506 }
507}