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 #[command(flatten)]
66 #[serde(default)]
67 pub tls: ClientTls,
68
69 #[cfg(feature = "websocket")]
70 #[command(flatten)]
71 #[serde(default)]
72 pub websocket: super::ClientWebSocket,
73}
74
75impl ClientConfig {
76 pub fn init(self) -> anyhow::Result<Client> {
77 Client::new(self)
78 }
79}
80
81impl Default for ClientConfig {
82 fn default() -> Self {
83 Self {
84 bind: "[::]:0".parse().unwrap(),
85 backend: None,
86 max_streams: None,
87 tls: ClientTls::default(),
88 #[cfg(feature = "websocket")]
89 websocket: super::ClientWebSocket::default(),
90 }
91 }
92}
93
94#[derive(Clone)]
98pub struct Client {
99 moq: moq_lite::Client,
100 #[cfg(feature = "websocket")]
101 websocket: super::ClientWebSocket,
102 tls: rustls::ClientConfig,
103 #[cfg(feature = "quinn")]
104 quinn: Option<crate::quinn::QuinnClient>,
105 #[cfg(feature = "quiche")]
106 quiche: Option<crate::quiche::QuicheClient>,
107 #[cfg(feature = "iroh")]
108 iroh: Option<web_transport_iroh::iroh::Endpoint>,
109}
110
111impl Client {
112 #[cfg(not(any(feature = "quinn", feature = "quiche")))]
113 pub fn new(_config: ClientConfig) -> anyhow::Result<Self> {
114 anyhow::bail!("no QUIC backend compiled; enable quinn or quiche feature");
115 }
116
117 #[cfg(any(feature = "quinn", feature = "quiche"))]
119 pub fn new(config: ClientConfig) -> anyhow::Result<Self> {
120 let backend = config.backend.clone().unwrap_or({
121 #[cfg(feature = "quinn")]
122 {
123 QuicBackend::Quinn
124 }
125 #[cfg(all(feature = "quiche", not(feature = "quinn")))]
126 {
127 QuicBackend::Quiche
128 }
129 #[cfg(all(not(feature = "quiche"), not(feature = "quinn")))]
130 panic!("no QUIC backend compiled; enable quinn or quiche feature");
131 });
132
133 let provider = crypto::provider();
134
135 let mut roots = rustls::RootCertStore::empty();
137
138 if config.tls.root.is_empty() {
139 let native = rustls_native_certs::load_native_certs();
140
141 for err in native.errors {
143 tracing::warn!(%err, "failed to load root cert");
144 }
145
146 for cert in native.certs {
148 roots.add(cert).context("failed to add root cert")?;
149 }
150 } else {
151 for root in &config.tls.root {
153 let root = std::fs::File::open(root).context("failed to open root cert file")?;
154 let mut root = std::io::BufReader::new(root);
155
156 let root = rustls_pemfile::certs(&mut root)
157 .next()
158 .context("no roots found")?
159 .context("failed to read root cert")?;
160
161 roots.add(root).context("failed to add root cert")?;
162 }
163 }
164
165 let mut tls = rustls::ClientConfig::builder_with_provider(provider.clone())
167 .with_protocol_versions(&[&rustls::version::TLS13])?
168 .with_root_certificates(roots)
169 .with_no_client_auth();
170
171 if config.tls.disable_verify.unwrap_or_default() {
173 tracing::warn!("TLS server certificate verification is disabled; A man-in-the-middle attack is possible.");
174
175 let noop = NoCertificateVerification(provider.clone());
176 tls.dangerous().set_certificate_verifier(Arc::new(noop));
177 }
178
179 #[cfg(feature = "quinn")]
180 #[allow(unreachable_patterns)]
181 let quinn = match backend {
182 QuicBackend::Quinn => Some(crate::quinn::QuinnClient::new(&config)?),
183 _ => None,
184 };
185
186 #[cfg(feature = "quiche")]
187 let quiche = match backend {
188 QuicBackend::Quiche => Some(crate::quiche::QuicheClient::new(&config)?),
189 _ => None,
190 };
191
192 Ok(Self {
193 moq: moq_lite::Client::new(),
194 #[cfg(feature = "websocket")]
195 websocket: config.websocket,
196 tls,
197 #[cfg(feature = "quinn")]
198 quinn,
199 #[cfg(feature = "quiche")]
200 quiche,
201 #[cfg(feature = "iroh")]
202 iroh: None,
203 })
204 }
205
206 #[cfg(feature = "iroh")]
207 pub fn with_iroh(mut self, iroh: Option<web_transport_iroh::iroh::Endpoint>) -> Self {
208 self.iroh = iroh;
209 self
210 }
211
212 pub fn with_publish(mut self, publish: impl Into<Option<moq_lite::OriginConsumer>>) -> Self {
213 self.moq = self.moq.with_publish(publish);
214 self
215 }
216
217 pub fn with_consume(mut self, consume: impl Into<Option<moq_lite::OriginProducer>>) -> Self {
218 self.moq = self.moq.with_consume(consume);
219 self
220 }
221
222 #[cfg(not(any(feature = "quinn", feature = "quiche", feature = "iroh")))]
223 pub async fn connect(&self, _url: Url) -> anyhow::Result<moq_lite::Session> {
224 anyhow::bail!("no QUIC backend compiled; enable quinn, quiche, or iroh feature");
225 }
226
227 #[cfg(any(feature = "quinn", feature = "quiche", feature = "iroh"))]
228 pub async fn connect(&self, url: Url) -> anyhow::Result<moq_lite::Session> {
229 #[cfg(feature = "iroh")]
230 if url.scheme() == "iroh" {
231 let endpoint = self.iroh.as_ref().context("Iroh support is not enabled")?;
232 let session = crate::iroh::connect(endpoint, url).await?;
233 let session = self.moq.connect(session).await?;
234 return Ok(session);
235 }
236
237 #[cfg(feature = "quinn")]
238 if let Some(quinn) = self.quinn.as_ref() {
239 let tls = self.tls.clone();
240 let quic_url = url.clone();
241 let quic_handle = async {
242 let res = quinn.connect(&tls, quic_url).await;
243 if let Err(err) = &res {
244 tracing::warn!(%err, "QUIC connection failed");
245 }
246 res
247 };
248
249 #[cfg(feature = "websocket")]
250 {
251 let ws_handle = crate::websocket::race_handle(&self.websocket, &self.tls, url);
252
253 return Ok(tokio::select! {
254 Ok(quic) = quic_handle => self.moq.connect(quic).await?,
255 Some(Ok(ws)) = ws_handle => self.moq.connect(ws).await?,
256 else => anyhow::bail!("failed to connect to server"),
257 });
258 }
259
260 #[cfg(not(feature = "websocket"))]
261 {
262 let session = quic_handle.await?;
263 return Ok(self.moq.connect(session).await?);
264 }
265 }
266
267 #[cfg(feature = "quiche")]
268 if let Some(quiche) = self.quiche.as_ref() {
269 let quic_url = url.clone();
270 let quic_handle = async {
271 let res = quiche.connect(quic_url).await;
272 if let Err(err) = &res {
273 tracing::warn!(%err, "QUIC connection failed");
274 }
275 res
276 };
277
278 #[cfg(feature = "websocket")]
279 {
280 let ws_handle = crate::websocket::race_handle(&self.websocket, &self.tls, url);
281
282 return Ok(tokio::select! {
283 Ok(quic) = quic_handle => self.moq.connect(quic).await?,
284 Some(Ok(ws)) = ws_handle => self.moq.connect(ws).await?,
285 else => anyhow::bail!("failed to connect to server"),
286 });
287 }
288
289 #[cfg(not(feature = "websocket"))]
290 {
291 let session = quic_handle.await?;
292 return Ok(self.moq.connect(session).await?);
293 }
294 }
295
296 anyhow::bail!("no QUIC backend compiled; enable quinn or quiche feature");
297 }
298}
299
300use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
301
302#[derive(Debug)]
303struct NoCertificateVerification(crypto::Provider);
304
305impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
306 fn verify_server_cert(
307 &self,
308 _end_entity: &CertificateDer<'_>,
309 _intermediates: &[CertificateDer<'_>],
310 _server_name: &ServerName<'_>,
311 _ocsp: &[u8],
312 _now: UnixTime,
313 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
314 Ok(rustls::client::danger::ServerCertVerified::assertion())
315 }
316
317 fn verify_tls12_signature(
318 &self,
319 message: &[u8],
320 cert: &CertificateDer<'_>,
321 dss: &rustls::DigitallySignedStruct,
322 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
323 rustls::crypto::verify_tls12_signature(message, cert, dss, &self.0.signature_verification_algorithms)
324 }
325
326 fn verify_tls13_signature(
327 &self,
328 message: &[u8],
329 cert: &CertificateDer<'_>,
330 dss: &rustls::DigitallySignedStruct,
331 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
332 rustls::crypto::verify_tls13_signature(message, cert, dss, &self.0.signature_verification_algorithms)
333 }
334
335 fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
336 self.0.signature_verification_algorithms.supported_schemes()
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use clap::Parser;
344
345 #[test]
346 fn test_toml_disable_verify_survives_update_from() {
347 let toml = r#"
348 tls.disable_verify = true
349 "#;
350
351 let mut config: ClientConfig = toml::from_str(toml).unwrap();
352 assert_eq!(config.tls.disable_verify, Some(true));
353
354 config.update_from(["test"]);
356 assert_eq!(config.tls.disable_verify, Some(true));
357 }
358
359 #[test]
360 fn test_cli_disable_verify_flag() {
361 let config = ClientConfig::parse_from(["test", "--tls-disable-verify"]);
362 assert_eq!(config.tls.disable_verify, Some(true));
363 }
364
365 #[test]
366 fn test_cli_disable_verify_explicit_false() {
367 let config = ClientConfig::parse_from(["test", "--tls-disable-verify=false"]);
368 assert_eq!(config.tls.disable_verify, Some(false));
369 }
370
371 #[test]
372 fn test_cli_disable_verify_explicit_true() {
373 let config = ClientConfig::parse_from(["test", "--tls-disable-verify=true"]);
374 assert_eq!(config.tls.disable_verify, Some(true));
375 }
376
377 #[test]
378 fn test_cli_no_disable_verify() {
379 let config = ClientConfig::parse_from(["test"]);
380 assert_eq!(config.tls.disable_verify, None);
381 }
382}