Skip to main content

moq_net/
client.rs

1use crate::{
2	ALPN_14, ALPN_15, ALPN_16, ALPN_17, ALPN_18, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05_WIP, Error,
3	NEGOTIATED, OriginConsumer, OriginProducer, Session, StatsHandle, Version, Versions,
4	coding::{self, Decode, Encode, Stream},
5	ietf, lite, setup,
6};
7
8/// A MoQ client session builder.
9#[derive(Default, Clone)]
10pub struct Client {
11	publish: Option<OriginConsumer>,
12	consume: Option<OriginProducer>,
13	stats: StatsHandle,
14	versions: Versions,
15	path: Option<String>,
16}
17
18impl Client {
19	pub fn new() -> Self {
20		Default::default()
21	}
22
23	pub fn with_publish(mut self, publish: impl Into<Option<OriginConsumer>>) -> Self {
24		self.publish = publish.into();
25		self
26	}
27
28	pub fn with_consume(mut self, consume: impl Into<Option<OriginProducer>>) -> Self {
29		self.consume = consume.into();
30		self
31	}
32
33	/// Attach a tier-scoped [`StatsHandle`]. Per-broadcast and per-subscription
34	/// counters will be bumped through this handle for the lifetime of the session.
35	/// Pass [`StatsHandle::default`] (a no-op handle) to opt out.
36	pub fn with_stats(mut self, stats: StatsHandle) -> Self {
37		self.stats = stats;
38		self
39	}
40
41	/// Set both publish and consume from an `OriginProducer`.
42	///
43	/// This is equivalent to calling `with_publish(origin.consume())` and `with_consume(origin)`.
44	pub fn with_origin(self, origin: OriginProducer) -> Self {
45		let consumer = origin.consume();
46		self.with_publish(consumer).with_consume(origin)
47	}
48
49	pub fn with_versions(mut self, versions: Versions) -> Self {
50		self.versions = versions;
51		self
52	}
53
54	/// Set the request path to advertise in the SETUP (moq-lite-05).
55	///
56	/// Required on transports that carry no request URI (native QUIC, qmux over
57	/// TCP/TLS/UDS) so the server learns which path the client wants. Omit it on
58	/// bindings that already carry a URI (WebTransport). Ignored by versions with no
59	/// Setup stream (moq-lite-01 through 04). The value is normalized to an absolute
60	/// path (empty becomes `/`, a leading `/` is prepended).
61	pub fn with_path(mut self, path: impl Into<String>) -> Self {
62		let path = path.into();
63		self.path = Some(if path.is_empty() {
64			"/".to_string()
65		} else if path.starts_with('/') {
66			path
67		} else {
68			format!("/{path}")
69		});
70		self
71	}
72
73	/// Perform the MoQ handshake as a client negotiating the version.
74	pub async fn connect<S: web_transport_trait::Session>(&self, session: S) -> Result<Session, Error> {
75		if self.publish.is_none() && self.consume.is_none() {
76			tracing::warn!("not publishing or consuming anything");
77		}
78
79		// If ALPN was used to negotiate the version, use the appropriate encoding.
80		// Default to IETF 14 if no ALPN was used and we'll negotiate the version later.
81		let (encoding, supported) = match session.protocol() {
82			Some(ALPN_18) => {
83				let v = self
84					.versions
85					.select(Version::Ietf(ietf::Version::Draft18))
86					.ok_or(Error::Version)?;
87
88				// Draft-17+: SETUP is exchanged in the background by the session.
89				ietf::start(
90					session.clone(),
91					None,
92					None,
93					true,
94					self.publish.clone(),
95					self.consume.clone(),
96					self.stats.clone(),
97					ietf::Version::Draft18,
98				)?;
99
100				tracing::debug!(version = ?v, "connected");
101				return Ok(Session::new(session, v, None));
102			}
103			Some(ALPN_17) => {
104				let v = self
105					.versions
106					.select(Version::Ietf(ietf::Version::Draft17))
107					.ok_or(Error::Version)?;
108
109				// Draft-17+: SETUP is exchanged in the background by the session.
110				ietf::start(
111					session.clone(),
112					None,
113					None,
114					true,
115					self.publish.clone(),
116					self.consume.clone(),
117					self.stats.clone(),
118					ietf::Version::Draft17,
119				)?;
120
121				tracing::debug!(version = ?v, "connected");
122				return Ok(Session::new(session, v, None));
123			}
124			Some(ALPN_16) => {
125				let v = self
126					.versions
127					.select(Version::Ietf(ietf::Version::Draft16))
128					.ok_or(Error::Version)?;
129				(v, v.into())
130			}
131			Some(ALPN_15) => {
132				let v = self
133					.versions
134					.select(Version::Ietf(ietf::Version::Draft15))
135					.ok_or(Error::Version)?;
136				(v, v.into())
137			}
138			Some(ALPN_14) => {
139				let v = self
140					.versions
141					.select(Version::Ietf(ietf::Version::Draft14))
142					.ok_or(Error::Version)?;
143				(v, v.into())
144			}
145			Some(ALPN_LITE_05_WIP) => {
146				self.versions
147					.select(Version::Lite(lite::Version::Lite05Wip))
148					.ok_or(Error::Version)?;
149
150				let setup = lite::Setup {
151					path: self.path.clone(),
152				};
153				let recv_bw = lite::start(
154					session.clone(),
155					None,
156					self.publish.clone(),
157					self.consume.clone(),
158					self.stats.clone(),
159					lite::Version::Lite05Wip,
160					setup,
161				)?;
162
163				return Ok(Session::new(session, lite::Version::Lite05Wip.into(), recv_bw));
164			}
165			Some(ALPN_LITE_04) => {
166				self.versions
167					.select(Version::Lite(lite::Version::Lite04))
168					.ok_or(Error::Version)?;
169
170				let recv_bw = lite::start(
171					session.clone(),
172					None,
173					self.publish.clone(),
174					self.consume.clone(),
175					self.stats.clone(),
176					lite::Version::Lite04,
177					lite::Setup::default(),
178				)?;
179
180				return Ok(Session::new(session, lite::Version::Lite04.into(), recv_bw));
181			}
182			Some(ALPN_LITE_03) => {
183				self.versions
184					.select(Version::Lite(lite::Version::Lite03))
185					.ok_or(Error::Version)?;
186
187				// Starting with draft-03, there's no more SETUP control stream.
188				let recv_bw = lite::start(
189					session.clone(),
190					None,
191					self.publish.clone(),
192					self.consume.clone(),
193					self.stats.clone(),
194					lite::Version::Lite03,
195					lite::Setup::default(),
196				)?;
197
198				return Ok(Session::new(session, lite::Version::Lite03.into(), recv_bw));
199			}
200			Some(ALPN_LITE) | None => {
201				let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?;
202				(Version::Ietf(ietf::Version::Draft14), supported)
203			}
204			Some(p) => return Err(Error::UnknownAlpn(p.to_string())),
205		};
206
207		let mut stream = Stream::open(&session, encoding).await?;
208
209		// The encoding is always an IETF version for SETUP negotiation.
210		let ietf_encoding = ietf::Version::try_from(encoding).map_err(|_| Error::Version)?;
211
212		let mut parameters = ietf::Parameters::default();
213		parameters.set_varint(ietf::ParameterVarInt::MaxRequestId, u32::MAX as u64);
214		parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
215		let parameters = parameters.encode_bytes(ietf_encoding)?;
216
217		let client = setup::Client {
218			versions: supported.clone().into(),
219			parameters,
220		};
221
222		stream.writer.encode(&client).await?;
223
224		let mut server: setup::Server = stream.reader.decode().await?;
225
226		let version = supported
227			.iter()
228			.find(|v| coding::Version::from(**v) == server.version)
229			.copied()
230			.ok_or(Error::Version)?;
231
232		let recv_bw = match version {
233			Version::Lite(v) => {
234				let stream = stream.with_version(v);
235				// This path only negotiates lite-01/02, which have no Setup stream.
236				lite::start(
237					session.clone(),
238					Some(stream),
239					self.publish.clone(),
240					self.consume.clone(),
241					self.stats.clone(),
242					v,
243					lite::Setup::default(),
244				)?
245			}
246			Version::Ietf(v) => {
247				// Decode the parameters to get the initial request ID.
248				let parameters = ietf::Parameters::decode(&mut server.parameters, v)?;
249				let request_id_max = parameters
250					.get_varint(ietf::ParameterVarInt::MaxRequestId)
251					.map(ietf::RequestId);
252
253				let stream = stream.with_version(v);
254				ietf::start(
255					session.clone(),
256					Some(stream),
257					request_id_max,
258					true,
259					self.publish.clone(),
260					self.consume.clone(),
261					self.stats.clone(),
262					v,
263				)?;
264				None
265			}
266		};
267
268		Ok(Session::new(session, version, recv_bw))
269	}
270}
271
272#[cfg(test)]
273mod tests {
274	use super::*;
275	use std::{
276		collections::VecDeque,
277		sync::{Arc, Mutex},
278	};
279
280	use crate::coding::{Decode, Encode};
281	use bytes::{BufMut, Bytes};
282
283	#[derive(Debug, Clone, Default)]
284	struct FakeError;
285
286	impl std::fmt::Display for FakeError {
287		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288			write!(f, "fake transport error")
289		}
290	}
291
292	impl std::error::Error for FakeError {}
293
294	impl web_transport_trait::Error for FakeError {
295		fn session_error(&self) -> Option<(u32, String)> {
296			Some((0, "closed".to_string()))
297		}
298	}
299
300	#[derive(Clone, Default)]
301	struct FakeSession {
302		state: Arc<FakeSessionState>,
303	}
304
305	#[derive(Default)]
306	struct FakeSessionState {
307		protocol: Option<&'static str>,
308		control_stream: Mutex<Option<(FakeSendStream, FakeRecvStream)>>,
309		close_events: Mutex<Vec<(u32, String)>>,
310		close_notify: tokio::sync::Notify,
311		control_writes: Arc<Mutex<Vec<u8>>>,
312	}
313
314	impl FakeSession {
315		fn new(protocol: Option<&'static str>, server_control_bytes: Vec<u8>) -> Self {
316			let writes = Arc::new(Mutex::new(Vec::new()));
317			let send = FakeSendStream { writes: writes.clone() };
318			let recv = FakeRecvStream {
319				data: VecDeque::from(server_control_bytes),
320			};
321			let state = FakeSessionState {
322				protocol,
323				control_stream: Mutex::new(Some((send, recv))),
324				close_events: Mutex::new(Vec::new()),
325				close_notify: tokio::sync::Notify::new(),
326				control_writes: writes,
327			};
328			Self { state: Arc::new(state) }
329		}
330
331		fn control_writes(&self) -> Vec<u8> {
332			self.state.control_writes.lock().unwrap().clone()
333		}
334
335		async fn wait_for_first_close(&self) -> (u32, String) {
336			loop {
337				let notified = self.state.close_notify.notified();
338				if let Some(close) = self.state.close_events.lock().unwrap().first().cloned() {
339					return close;
340				}
341				notified.await;
342			}
343		}
344	}
345
346	impl web_transport_trait::Session for FakeSession {
347		type SendStream = FakeSendStream;
348		type RecvStream = FakeRecvStream;
349		type Error = FakeError;
350
351		async fn accept_uni(&self) -> Result<Self::RecvStream, Self::Error> {
352			std::future::pending().await
353		}
354
355		async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
356			std::future::pending().await
357		}
358
359		async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
360			self.state.control_stream.lock().unwrap().take().ok_or(FakeError)
361		}
362
363		async fn open_uni(&self) -> Result<Self::SendStream, Self::Error> {
364			std::future::pending().await
365		}
366
367		fn send_datagram(&self, _payload: Bytes) -> Result<(), Self::Error> {
368			Ok(())
369		}
370
371		async fn recv_datagram(&self) -> Result<Bytes, Self::Error> {
372			std::future::pending().await
373		}
374
375		fn max_datagram_size(&self) -> usize {
376			1200
377		}
378
379		fn protocol(&self) -> Option<&str> {
380			self.state.protocol
381		}
382
383		fn close(&self, code: u32, reason: &str) {
384			self.state.close_events.lock().unwrap().push((code, reason.to_string()));
385			self.state.close_notify.notify_waiters();
386		}
387
388		async fn closed(&self) -> Self::Error {
389			self.state.close_notify.notified().await;
390			FakeError
391		}
392	}
393
394	#[derive(Clone, Default)]
395	struct FakeSendStream {
396		writes: Arc<Mutex<Vec<u8>>>,
397	}
398
399	impl web_transport_trait::SendStream for FakeSendStream {
400		type Error = FakeError;
401
402		async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
403			self.writes.lock().unwrap().put_slice(buf);
404			Ok(buf.len())
405		}
406
407		fn set_priority(&mut self, _order: u8) {}
408
409		fn finish(&mut self) -> Result<(), Self::Error> {
410			Ok(())
411		}
412
413		fn reset(&mut self, _code: u32) {}
414
415		async fn closed(&mut self) -> Result<(), Self::Error> {
416			Ok(())
417		}
418	}
419
420	struct FakeRecvStream {
421		data: VecDeque<u8>,
422	}
423
424	impl web_transport_trait::RecvStream for FakeRecvStream {
425		type Error = FakeError;
426
427		async fn read(&mut self, dst: &mut [u8]) -> Result<Option<usize>, Self::Error> {
428			if self.data.is_empty() {
429				return Ok(None);
430			}
431
432			let size = dst.len().min(self.data.len());
433			for slot in dst.iter_mut().take(size) {
434				*slot = self.data.pop_front().unwrap();
435			}
436			Ok(Some(size))
437		}
438
439		fn stop(&mut self, _code: u32) {}
440
441		async fn closed(&mut self) -> Result<(), Self::Error> {
442			Ok(())
443		}
444	}
445
446	fn mock_server_setup(negotiated: Version) -> Vec<u8> {
447		let mut encoded = Vec::new();
448		let server = setup::Server {
449			version: negotiated.into(),
450			parameters: Bytes::new(),
451		};
452		server
453			.encode(&mut encoded, Version::Ietf(ietf::Version::Draft14))
454			.unwrap();
455
456		// Add a setup-stream SessionInfo frame using the negotiated Lite version.
457		let info = lite::SessionInfo { bitrate: Some(1) };
458		let lite_v = lite::Version::try_from(negotiated).unwrap();
459		info.encode(&mut encoded, lite_v).unwrap();
460
461		encoded
462	}
463
464	async fn run_alpn_lite_fallback_case(protocol: Option<&'static str>) {
465		let fake = FakeSession::new(protocol, mock_server_setup(Version::Lite(lite::Version::Lite01)));
466		let client = Client::new().with_versions(
467			[
468				Version::Lite(lite::Version::Lite03),
469				Version::Lite(lite::Version::Lite02),
470				Version::Lite(lite::Version::Lite01),
471				Version::Ietf(ietf::Version::Draft14),
472			]
473			.into(),
474		);
475
476		let _session = client.connect(fake.clone()).await.unwrap();
477
478		// Verify the client setup was encoded using Draft14 framing (ALPN_LITE fallback path).
479		let mut setup_bytes = Bytes::from(fake.control_writes());
480		let setup = setup::Client::decode(&mut setup_bytes, Version::Ietf(ietf::Version::Draft14)).unwrap();
481		let advertised: Vec<Version> = setup.versions.iter().map(|v| Version::try_from(*v).unwrap()).collect();
482		assert_eq!(
483			advertised,
484			vec![
485				Version::Lite(lite::Version::Lite02),
486				Version::Lite(lite::Version::Lite01),
487				Version::Ietf(ietf::Version::Draft14),
488			]
489		);
490
491		// The first close comes from the background lite session task.
492		// Code 0 ("cancelled") means SessionInfo decoded successfully after set_version().
493		let (code, _) = fake.wait_for_first_close().await;
494		assert_eq!(code, Error::Cancel.to_code());
495	}
496
497	#[tokio::test(start_paused = true)]
498	async fn alpn_lite_falls_back_to_draft14_and_switches_version_post_setup() {
499		run_alpn_lite_fallback_case(Some(ALPN_LITE)).await;
500	}
501
502	#[tokio::test(start_paused = true)]
503	async fn no_alpn_falls_back_to_draft14_and_switches_version_post_setup() {
504		run_alpn_lite_fallback_case(None).await;
505	}
506}