Skip to main content

moq_net/
client.rs

1use crate::{
2	ALPN_14, ALPN_15, ALPN_16, ALPN_17, ALPN_18, ALPN_19, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05_WIP,
3	Error, 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_19) => {
83				let v = self
84					.versions
85					.select(Version::Ietf(ietf::Version::Draft19))
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::Draft19,
98				)?;
99
100				tracing::debug!(version = ?v, "connected");
101				return Ok(Session::new(session, v, None));
102			}
103			Some(ALPN_18) => {
104				let v = self
105					.versions
106					.select(Version::Ietf(ietf::Version::Draft18))
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::Draft18,
119				)?;
120
121				tracing::debug!(version = ?v, "connected");
122				return Ok(Session::new(session, v, None));
123			}
124			Some(ALPN_17) => {
125				let v = self
126					.versions
127					.select(Version::Ietf(ietf::Version::Draft17))
128					.ok_or(Error::Version)?;
129
130				// Draft-17+: SETUP is exchanged in the background by the session.
131				ietf::start(
132					session.clone(),
133					None,
134					None,
135					true,
136					self.publish.clone(),
137					self.consume.clone(),
138					self.stats.clone(),
139					ietf::Version::Draft17,
140				)?;
141
142				tracing::debug!(version = ?v, "connected");
143				return Ok(Session::new(session, v, None));
144			}
145			Some(ALPN_16) => {
146				let v = self
147					.versions
148					.select(Version::Ietf(ietf::Version::Draft16))
149					.ok_or(Error::Version)?;
150				(v, v.into())
151			}
152			Some(ALPN_15) => {
153				let v = self
154					.versions
155					.select(Version::Ietf(ietf::Version::Draft15))
156					.ok_or(Error::Version)?;
157				(v, v.into())
158			}
159			Some(ALPN_14) => {
160				let v = self
161					.versions
162					.select(Version::Ietf(ietf::Version::Draft14))
163					.ok_or(Error::Version)?;
164				(v, v.into())
165			}
166			Some(ALPN_LITE_05_WIP) => {
167				self.versions
168					.select(Version::Lite(lite::Version::Lite05Wip))
169					.ok_or(Error::Version)?;
170
171				let setup = lite::Setup {
172					path: self.path.clone(),
173				};
174				let recv_bw = lite::start(
175					session.clone(),
176					None,
177					self.publish.clone(),
178					self.consume.clone(),
179					self.stats.clone(),
180					lite::Version::Lite05Wip,
181					setup,
182				)?;
183
184				return Ok(Session::new(session, lite::Version::Lite05Wip.into(), recv_bw));
185			}
186			Some(ALPN_LITE_04) => {
187				self.versions
188					.select(Version::Lite(lite::Version::Lite04))
189					.ok_or(Error::Version)?;
190
191				let recv_bw = lite::start(
192					session.clone(),
193					None,
194					self.publish.clone(),
195					self.consume.clone(),
196					self.stats.clone(),
197					lite::Version::Lite04,
198					lite::Setup::default(),
199				)?;
200
201				return Ok(Session::new(session, lite::Version::Lite04.into(), recv_bw));
202			}
203			Some(ALPN_LITE_03) => {
204				self.versions
205					.select(Version::Lite(lite::Version::Lite03))
206					.ok_or(Error::Version)?;
207
208				// Starting with draft-03, there's no more SETUP control stream.
209				let recv_bw = lite::start(
210					session.clone(),
211					None,
212					self.publish.clone(),
213					self.consume.clone(),
214					self.stats.clone(),
215					lite::Version::Lite03,
216					lite::Setup::default(),
217				)?;
218
219				return Ok(Session::new(session, lite::Version::Lite03.into(), recv_bw));
220			}
221			Some(ALPN_LITE) | None => {
222				let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?;
223				(Version::Ietf(ietf::Version::Draft14), supported)
224			}
225			Some(p) => return Err(Error::UnknownAlpn(p.to_string())),
226		};
227
228		let mut stream = Stream::open(&session, encoding).await?;
229
230		// The encoding is always an IETF version for SETUP negotiation.
231		let ietf_encoding = ietf::Version::try_from(encoding).map_err(|_| Error::Version)?;
232
233		let mut parameters = ietf::Parameters::default();
234		parameters.set_varint(ietf::ParameterVarInt::MaxRequestId, u32::MAX as u64);
235		parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
236		let parameters = parameters.encode_bytes(ietf_encoding)?;
237
238		let client = setup::Client {
239			versions: supported.clone().into(),
240			parameters,
241		};
242
243		stream.writer.encode(&client).await?;
244
245		let mut server: setup::Server = stream.reader.decode().await?;
246
247		let version = supported
248			.iter()
249			.find(|v| coding::Version::from(**v) == server.version)
250			.copied()
251			.ok_or(Error::Version)?;
252
253		let recv_bw = match version {
254			Version::Lite(v) => {
255				let stream = stream.with_version(v);
256				// This path only negotiates lite-01/02, which have no Setup stream.
257				lite::start(
258					session.clone(),
259					Some(stream),
260					self.publish.clone(),
261					self.consume.clone(),
262					self.stats.clone(),
263					v,
264					lite::Setup::default(),
265				)?
266			}
267			Version::Ietf(v) => {
268				// Decode the parameters to get the initial request ID.
269				let parameters = ietf::Parameters::decode(&mut server.parameters, v)?;
270				let request_id_max = parameters
271					.get_varint(ietf::ParameterVarInt::MaxRequestId)
272					.map(ietf::RequestId);
273
274				let stream = stream.with_version(v);
275				ietf::start(
276					session.clone(),
277					Some(stream),
278					request_id_max,
279					true,
280					self.publish.clone(),
281					self.consume.clone(),
282					self.stats.clone(),
283					v,
284				)?;
285				None
286			}
287		};
288
289		Ok(Session::new(session, version, recv_bw))
290	}
291}
292
293#[cfg(test)]
294mod tests {
295	use super::*;
296	use std::{
297		collections::VecDeque,
298		sync::{Arc, Mutex},
299	};
300
301	use crate::coding::{Decode, Encode};
302	use bytes::{BufMut, Bytes};
303
304	#[derive(Debug, Clone, Default)]
305	struct FakeError;
306
307	impl std::fmt::Display for FakeError {
308		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309			write!(f, "fake transport error")
310		}
311	}
312
313	impl std::error::Error for FakeError {}
314
315	impl web_transport_trait::Error for FakeError {
316		fn session_error(&self) -> Option<(u32, String)> {
317			Some((0, "closed".to_string()))
318		}
319	}
320
321	#[derive(Clone, Default)]
322	struct FakeSession {
323		state: Arc<FakeSessionState>,
324	}
325
326	#[derive(Default)]
327	struct FakeSessionState {
328		protocol: Option<&'static str>,
329		control_stream: Mutex<Option<(FakeSendStream, FakeRecvStream)>>,
330		close_events: Mutex<Vec<(u32, String)>>,
331		close_notify: tokio::sync::Notify,
332		control_writes: Arc<Mutex<Vec<u8>>>,
333	}
334
335	impl FakeSession {
336		fn new(protocol: Option<&'static str>, server_control_bytes: Vec<u8>) -> Self {
337			let writes = Arc::new(Mutex::new(Vec::new()));
338			let send = FakeSendStream { writes: writes.clone() };
339			let recv = FakeRecvStream {
340				data: VecDeque::from(server_control_bytes),
341			};
342			let state = FakeSessionState {
343				protocol,
344				control_stream: Mutex::new(Some((send, recv))),
345				close_events: Mutex::new(Vec::new()),
346				close_notify: tokio::sync::Notify::new(),
347				control_writes: writes,
348			};
349			Self { state: Arc::new(state) }
350		}
351
352		fn control_writes(&self) -> Vec<u8> {
353			self.state.control_writes.lock().unwrap().clone()
354		}
355
356		async fn wait_for_first_close(&self) -> (u32, String) {
357			loop {
358				let notified = self.state.close_notify.notified();
359				if let Some(close) = self.state.close_events.lock().unwrap().first().cloned() {
360					return close;
361				}
362				notified.await;
363			}
364		}
365	}
366
367	impl web_transport_trait::Session for FakeSession {
368		type SendStream = FakeSendStream;
369		type RecvStream = FakeRecvStream;
370		type Error = FakeError;
371
372		async fn accept_uni(&self) -> Result<Self::RecvStream, Self::Error> {
373			std::future::pending().await
374		}
375
376		async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
377			std::future::pending().await
378		}
379
380		async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
381			self.state.control_stream.lock().unwrap().take().ok_or(FakeError)
382		}
383
384		async fn open_uni(&self) -> Result<Self::SendStream, Self::Error> {
385			std::future::pending().await
386		}
387
388		fn send_datagram(&self, _payload: Bytes) -> Result<(), Self::Error> {
389			Ok(())
390		}
391
392		async fn recv_datagram(&self) -> Result<Bytes, Self::Error> {
393			std::future::pending().await
394		}
395
396		fn max_datagram_size(&self) -> usize {
397			1200
398		}
399
400		fn protocol(&self) -> Option<&str> {
401			self.state.protocol
402		}
403
404		fn close(&self, code: u32, reason: &str) {
405			self.state.close_events.lock().unwrap().push((code, reason.to_string()));
406			self.state.close_notify.notify_waiters();
407		}
408
409		async fn closed(&self) -> Self::Error {
410			self.state.close_notify.notified().await;
411			FakeError
412		}
413	}
414
415	#[derive(Clone, Default)]
416	struct FakeSendStream {
417		writes: Arc<Mutex<Vec<u8>>>,
418	}
419
420	impl web_transport_trait::SendStream for FakeSendStream {
421		type Error = FakeError;
422
423		async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
424			self.writes.lock().unwrap().put_slice(buf);
425			Ok(buf.len())
426		}
427
428		fn set_priority(&mut self, _order: u8) {}
429
430		fn finish(&mut self) -> Result<(), Self::Error> {
431			Ok(())
432		}
433
434		fn reset(&mut self, _code: u32) {}
435
436		async fn closed(&mut self) -> Result<(), Self::Error> {
437			Ok(())
438		}
439	}
440
441	struct FakeRecvStream {
442		data: VecDeque<u8>,
443	}
444
445	impl web_transport_trait::RecvStream for FakeRecvStream {
446		type Error = FakeError;
447
448		async fn read(&mut self, dst: &mut [u8]) -> Result<Option<usize>, Self::Error> {
449			if self.data.is_empty() {
450				return Ok(None);
451			}
452
453			let size = dst.len().min(self.data.len());
454			for slot in dst.iter_mut().take(size) {
455				*slot = self.data.pop_front().unwrap();
456			}
457			Ok(Some(size))
458		}
459
460		fn stop(&mut self, _code: u32) {}
461
462		async fn closed(&mut self) -> Result<(), Self::Error> {
463			Ok(())
464		}
465	}
466
467	fn mock_server_setup(negotiated: Version) -> Vec<u8> {
468		let mut encoded = Vec::new();
469		let server = setup::Server {
470			version: negotiated.into(),
471			parameters: Bytes::new(),
472		};
473		server
474			.encode(&mut encoded, Version::Ietf(ietf::Version::Draft14))
475			.unwrap();
476
477		// Add a setup-stream SessionInfo frame using the negotiated Lite version.
478		let info = lite::SessionInfo { bitrate: Some(1) };
479		let lite_v = lite::Version::try_from(negotiated).unwrap();
480		info.encode(&mut encoded, lite_v).unwrap();
481
482		encoded
483	}
484
485	async fn run_alpn_lite_fallback_case(protocol: Option<&'static str>) {
486		let fake = FakeSession::new(protocol, mock_server_setup(Version::Lite(lite::Version::Lite01)));
487		let client = Client::new().with_versions(
488			[
489				Version::Lite(lite::Version::Lite03),
490				Version::Lite(lite::Version::Lite02),
491				Version::Lite(lite::Version::Lite01),
492				Version::Ietf(ietf::Version::Draft14),
493			]
494			.into(),
495		);
496
497		let _session = client.connect(fake.clone()).await.unwrap();
498
499		// Verify the client setup was encoded using Draft14 framing (ALPN_LITE fallback path).
500		let mut setup_bytes = Bytes::from(fake.control_writes());
501		let setup = setup::Client::decode(&mut setup_bytes, Version::Ietf(ietf::Version::Draft14)).unwrap();
502		let advertised: Vec<Version> = setup.versions.iter().map(|v| Version::try_from(*v).unwrap()).collect();
503		assert_eq!(
504			advertised,
505			vec![
506				Version::Lite(lite::Version::Lite02),
507				Version::Lite(lite::Version::Lite01),
508				Version::Ietf(ietf::Version::Draft14),
509			]
510		);
511
512		// The first close comes from the background lite session task.
513		// Code 0 ("cancelled") means SessionInfo decoded successfully after set_version().
514		let (code, _) = fake.wait_for_first_close().await;
515		assert_eq!(code, Error::Cancel.to_code());
516	}
517
518	#[tokio::test(start_paused = true)]
519	async fn alpn_lite_falls_back_to_draft14_and_switches_version_post_setup() {
520		run_alpn_lite_fallback_case(Some(ALPN_LITE)).await;
521	}
522
523	#[tokio::test(start_paused = true)]
524	async fn no_alpn_falls_back_to_draft14_and_switches_version_post_setup() {
525		run_alpn_lite_fallback_case(None).await;
526	}
527}