Skip to main content

moq_net/
server.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::{Decode, Encode, Stream},
5	ietf, lite, setup,
6};
7
8/// A MoQ server session builder.
9#[derive(Default, Clone)]
10pub struct Server {
11	publish: Option<OriginConsumer>,
12	consume: Option<OriginProducer>,
13	stats: StatsHandle,
14	versions: Versions,
15}
16
17impl Server {
18	pub fn new() -> Self {
19		Default::default()
20	}
21
22	pub fn with_publish(mut self, publish: impl Into<Option<OriginConsumer>>) -> Self {
23		self.publish = publish.into();
24		self
25	}
26
27	pub fn with_consume(mut self, consume: impl Into<Option<OriginProducer>>) -> Self {
28		self.consume = consume.into();
29		self
30	}
31
32	/// Attach a tier-scoped [`StatsHandle`]. Per-broadcast and per-subscription
33	/// counters will be bumped through this handle for the lifetime of the session.
34	/// Pass [`StatsHandle::default`] (a no-op handle) to opt out.
35	pub fn with_stats(mut self, stats: StatsHandle) -> Self {
36		self.stats = stats;
37		self
38	}
39
40	/// Set both publish and consume from an `OriginProducer`.
41	///
42	/// This is equivalent to calling `with_publish(origin.consume())` and `with_consume(origin)`.
43	pub fn with_origin(self, origin: OriginProducer) -> Self {
44		let consumer = origin.consume();
45		self.with_publish(consumer).with_consume(origin)
46	}
47
48	pub fn with_versions(mut self, versions: Versions) -> Self {
49		self.versions = versions;
50		self
51	}
52
53	/// Perform the MoQ handshake as a server, returning the established [`Session`].
54	///
55	/// Convenience wrapper over [`accept_request`](Self::accept_request) that completes
56	/// the handshake immediately. Use `accept_request` when you need to inspect the
57	/// client's advertised path before deciding what to serve.
58	pub async fn accept<S: web_transport_trait::Session>(&self, session: S) -> Result<Session, Error> {
59		self.accept_request(session).await?.ok().await
60	}
61
62	/// Begin the MoQ handshake, pausing once the client's request path is known so the
63	/// caller can authorize or scope before serving.
64	///
65	/// Reads the client's SETUP (the in-band path lives there on URL-less transports),
66	/// then returns a [`Request`]: inspect [`path`](Request::path), set the origins to
67	/// serve, and call [`ok`](Request::ok) or [`close`](Request::close). Starting the
68	/// session is deferred to `ok()`, so origins set on the `Request` take effect.
69	///
70	/// The path is surfaced for moq-lite-05; it is `None` on versions with no in-band
71	/// request path (lite 01-04, and the IETF drafts in this build).
72	pub async fn accept_request<S: web_transport_trait::Session>(&self, session: S) -> Result<Request<S>, Error> {
73		// Regimes without an in-band path defer to `ok()` without surfacing one.
74		let deferred = |handshake| Request {
75			server: self.clone(),
76			path: None,
77			handshake,
78		};
79
80		let (encoding, supported) = match session.protocol() {
81			Some(ALPN_18) => {
82				self.versions
83					.select(Version::Ietf(ietf::Version::Draft18))
84					.ok_or(Error::Version)?;
85				return Ok(deferred(Handshake::IetfModern {
86					session,
87					version: ietf::Version::Draft18,
88				}));
89			}
90			Some(ALPN_17) => {
91				self.versions
92					.select(Version::Ietf(ietf::Version::Draft17))
93					.ok_or(Error::Version)?;
94				return Ok(deferred(Handshake::IetfModern {
95					session,
96					version: ietf::Version::Draft17,
97				}));
98			}
99			Some(ALPN_16) => {
100				let v = self
101					.versions
102					.select(Version::Ietf(ietf::Version::Draft16))
103					.ok_or(Error::Version)?;
104				(v, v.into())
105			}
106			Some(ALPN_15) => {
107				let v = self
108					.versions
109					.select(Version::Ietf(ietf::Version::Draft15))
110					.ok_or(Error::Version)?;
111				(v, v.into())
112			}
113			Some(ALPN_14) => {
114				let v = self
115					.versions
116					.select(Version::Ietf(ietf::Version::Draft14))
117					.ok_or(Error::Version)?;
118				(v, v.into())
119			}
120			Some(ALPN_LITE_05_WIP) => {
121				self.versions
122					.select(Version::Lite(lite::Version::Lite05Wip))
123					.ok_or(Error::Version)?;
124
125				// Gate on the client's SETUP: read it before serving so the caller can
126				// scope by the advertised path.
127				let client_setup = lite::accept_setup(&session, lite::Version::Lite05Wip).await?;
128				return Ok(Request {
129					server: self.clone(),
130					path: client_setup.path,
131					handshake: Handshake::Lite05 { session },
132				});
133			}
134			Some(ALPN_LITE_04) => {
135				self.versions
136					.select(Version::Lite(lite::Version::Lite04))
137					.ok_or(Error::Version)?;
138				return Ok(deferred(Handshake::LiteBare {
139					session,
140					version: lite::Version::Lite04,
141				}));
142			}
143			Some(ALPN_LITE_03) => {
144				self.versions
145					.select(Version::Lite(lite::Version::Lite03))
146					.ok_or(Error::Version)?;
147				return Ok(deferred(Handshake::LiteBare {
148					session,
149					version: lite::Version::Lite03,
150				}));
151			}
152			Some(ALPN_LITE) | None => {
153				let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?;
154				(Version::Ietf(ietf::Version::Draft14), supported)
155			}
156			Some(p) => return Err(Error::UnknownAlpn(p.to_string())),
157		};
158
159		// Legacy bidi SETUP exchange (IETF 14-16, lite 01/02). Read the client's SETUP
160		// to choose the version; `ok()` sends the server SETUP and starts the session.
161		let mut stream = Stream::accept(&session, encoding).await?;
162		let mut client: setup::Client = stream.reader.decode().await?;
163
164		let version = client
165			.versions
166			.iter()
167			.flat_map(|v| Version::try_from(*v).ok())
168			.find(|v| supported.contains(v))
169			.ok_or(Error::Version)?;
170
171		// Pull the max request ID out now (IETF only) so `ok()` doesn't re-decode the
172		// consumed parameters.
173		let request_id_max = match version {
174			Version::Ietf(v) => {
175				let params = ietf::Parameters::decode(&mut client.parameters, v)?;
176				params
177					.get_varint(ietf::ParameterVarInt::MaxRequestId)
178					.map(ietf::RequestId)
179			}
180			Version::Lite(_) => None,
181		};
182
183		Ok(deferred(Handshake::Legacy {
184			session,
185			stream,
186			version,
187			request_id_max,
188		}))
189	}
190}
191
192/// A paused server-side handshake.
193///
194/// Returned by [`Server::accept_request`] once the client's advertised
195/// [`path`](Self::path) is known but before the session is granted anything. Set the
196/// origins to serve, then call [`ok`](Self::ok) to complete the handshake, or
197/// [`close`](Self::close) to reject it. Modeled on the WebTransport `Request`.
198pub struct Request<S: web_transport_trait::Session> {
199	server: Server,
200	path: Option<String>,
201	handshake: Handshake<S>,
202}
203
204/// The handshake state captured at the pause point. Every variant defers its session
205/// start to [`Request::ok`] so origins set on the `Request` still apply.
206enum Handshake<S: web_transport_trait::Session> {
207	/// Modern IETF (17/18): SETUP is exchanged in the background by the session.
208	IetfModern { session: S, version: ietf::Version },
209	/// moq-lite 03/04: no Setup stream.
210	LiteBare { session: S, version: lite::Version },
211	/// moq-lite 05+: the client's Setup stream has already been read.
212	Lite05 { session: S },
213	/// Legacy IETF (14-16) and lite 01/02: the client SETUP has been read off the bidi
214	/// stream but the server SETUP hasn't been sent. `ok()` finishes it.
215	Legacy {
216		session: S,
217		stream: Stream<S, Version>,
218		version: Version,
219		request_id_max: Option<ietf::RequestId>,
220	},
221}
222
223impl<S: web_transport_trait::Session> Request<S> {
224	/// The request path the client advertised in its SETUP, if any.
225	///
226	/// Populated for moq-lite-05; `None` on versions without an in-band request path.
227	/// See the note on [`Server::accept_request`].
228	pub fn path(&self) -> Option<&str> {
229		self.path.as_deref()
230	}
231
232	/// Publish to the connected client. Overrides any value from the [`Server`]
233	/// builder; typically set after inspecting [`path`](Self::path).
234	pub fn with_publish(mut self, publish: impl Into<Option<OriginConsumer>>) -> Self {
235		self.server.publish = publish.into();
236		self
237	}
238
239	/// Subscribe to the connected client. Overrides any value from the [`Server`] builder.
240	pub fn with_consume(mut self, consume: impl Into<Option<OriginProducer>>) -> Self {
241		self.server.consume = consume.into();
242		self
243	}
244
245	/// Set the tier-scoped stats handle. Overrides any value from the [`Server`] builder.
246	pub fn with_stats(mut self, stats: StatsHandle) -> Self {
247		self.server.stats = stats;
248		self
249	}
250
251	/// Accept the session, completing the handshake.
252	pub async fn ok(self) -> Result<Session, Error> {
253		let server = self.server;
254
255		// Warn here, not in `accept_request`: callers attach origins on the Request
256		// (after inspecting the path), so checking earlier gives false positives.
257		if server.publish.is_none() && server.consume.is_none() {
258			tracing::warn!("not publishing or consuming anything");
259		}
260
261		let (session, mut stream, version, request_id_max) = match self.handshake {
262			Handshake::IetfModern { session, version } => {
263				ietf::start(
264					session.clone(),
265					None,
266					None,
267					false,
268					server.publish,
269					server.consume,
270					server.stats,
271					version,
272				)?;
273				tracing::debug!(?version, "connected");
274				return Ok(Session::new(session, version.into(), None));
275			}
276			Handshake::LiteBare { session, version } => {
277				let recv_bw = lite::start(
278					session.clone(),
279					None,
280					server.publish,
281					server.consume,
282					server.stats,
283					version,
284					lite::Setup::default(),
285				)?;
286				return Ok(Session::new(session, version.into(), recv_bw));
287			}
288			Handshake::Lite05 { session } => {
289				// A server never advertises a request path.
290				let recv_bw = lite::start(
291					session.clone(),
292					None,
293					server.publish,
294					server.consume,
295					server.stats,
296					lite::Version::Lite05Wip,
297					lite::Setup::default(),
298				)?;
299				return Ok(Session::new(session, lite::Version::Lite05Wip.into(), recv_bw));
300			}
301			Handshake::Legacy {
302				session,
303				stream,
304				version,
305				request_id_max,
306			} => (session, stream, version, request_id_max),
307		};
308
309		// Encode parameters using the version-appropriate type.
310		let parameters = match version {
311			Version::Ietf(v) => {
312				let mut parameters = ietf::Parameters::default();
313				parameters.set_varint(ietf::ParameterVarInt::MaxRequestId, u32::MAX as u64);
314				parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
315				parameters.encode_bytes(v)?
316			}
317			Version::Lite(v) => lite::Parameters::default().encode_bytes(v)?,
318		};
319
320		let server_setup = setup::Server {
321			version: version.into(),
322			parameters,
323		};
324		stream.writer.encode(&server_setup).await?;
325
326		let recv_bw = match version {
327			Version::Lite(v) => {
328				let stream = stream.with_version(v);
329				// Pre-lite-05: no Setup stream, so nothing to advertise.
330				lite::start(
331					session.clone(),
332					Some(stream),
333					server.publish,
334					server.consume,
335					server.stats,
336					v,
337					lite::Setup::default(),
338				)?
339			}
340			Version::Ietf(v) => {
341				let stream = stream.with_version(v);
342				ietf::start(
343					session.clone(),
344					Some(stream),
345					request_id_max,
346					false,
347					server.publish,
348					server.consume,
349					server.stats,
350					v,
351				)?;
352				None
353			}
354		};
355
356		Ok(Session::new(session, version, recv_bw))
357	}
358
359	/// Reject the session, closing the transport with `err`'s wire code.
360	pub fn close(self, err: Error) {
361		let session = match self.handshake {
362			Handshake::IetfModern { session, .. } => session,
363			Handshake::LiteBare { session, .. } => session,
364			Handshake::Lite05 { session } => session,
365			Handshake::Legacy { session, .. } => session,
366		};
367		session.close(err.to_code(), &err.to_string());
368	}
369}
370
371#[cfg(test)]
372mod tests {
373	use super::*;
374	use std::{
375		collections::VecDeque,
376		sync::{Arc, Mutex},
377	};
378
379	use bytes::Bytes;
380
381	#[derive(Debug, Clone, Default)]
382	struct FakeError;
383	impl std::fmt::Display for FakeError {
384		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385			write!(f, "fake transport error")
386		}
387	}
388	impl std::error::Error for FakeError {}
389	impl web_transport_trait::Error for FakeError {
390		fn session_error(&self) -> Option<(u32, String)> {
391			Some((0, "closed".to_string()))
392		}
393	}
394
395	/// A session that replays a queue of unidirectional streams (each a `Vec<u8>`) in
396	/// order from `accept_uni`; everything else is inert.
397	#[derive(Clone)]
398	struct FakeSession {
399		protocol: Option<&'static str>,
400		uni: Arc<Mutex<VecDeque<Vec<u8>>>>,
401	}
402
403	impl FakeSession {
404		fn new(protocol: &'static str, uni: impl IntoIterator<Item = Vec<u8>>) -> Self {
405			Self {
406				protocol: Some(protocol),
407				uni: Arc::new(Mutex::new(uni.into_iter().collect())),
408			}
409		}
410	}
411
412	impl web_transport_trait::Session for FakeSession {
413		type SendStream = FakeSend;
414		type RecvStream = FakeRecv;
415		type Error = FakeError;
416
417		async fn accept_uni(&self) -> Result<Self::RecvStream, Self::Error> {
418			// Drop the guard before any await so the future stays Send.
419			let data = self.uni.lock().unwrap().pop_front();
420			match data {
421				Some(data) => Ok(FakeRecv { data: data.into() }),
422				None => std::future::pending().await,
423			}
424		}
425		async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
426			std::future::pending().await
427		}
428		async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
429			std::future::pending().await
430		}
431		async fn open_uni(&self) -> Result<Self::SendStream, Self::Error> {
432			std::future::pending().await
433		}
434		fn send_datagram(&self, _payload: Bytes) -> Result<(), Self::Error> {
435			Ok(())
436		}
437		async fn recv_datagram(&self) -> Result<Bytes, Self::Error> {
438			std::future::pending().await
439		}
440		fn max_datagram_size(&self) -> usize {
441			1200
442		}
443		fn protocol(&self) -> Option<&str> {
444			self.protocol
445		}
446		fn close(&self, _code: u32, _reason: &str) {}
447		async fn closed(&self) -> Self::Error {
448			std::future::pending().await
449		}
450	}
451
452	#[derive(Clone, Default)]
453	struct FakeSend;
454	impl web_transport_trait::SendStream for FakeSend {
455		type Error = FakeError;
456		async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
457			Ok(buf.len())
458		}
459		fn set_priority(&mut self, _order: u8) {}
460		fn finish(&mut self) -> Result<(), Self::Error> {
461			Ok(())
462		}
463		fn reset(&mut self, _code: u32) {}
464		async fn closed(&mut self) -> Result<(), Self::Error> {
465			Ok(())
466		}
467	}
468
469	struct FakeRecv {
470		data: VecDeque<u8>,
471	}
472	impl web_transport_trait::RecvStream for FakeRecv {
473		type Error = FakeError;
474		async fn read(&mut self, dst: &mut [u8]) -> Result<Option<usize>, Self::Error> {
475			if self.data.is_empty() {
476				return Ok(None);
477			}
478			let size = dst.len().min(self.data.len());
479			for slot in dst.iter_mut().take(size) {
480				*slot = self.data.pop_front().unwrap();
481			}
482			Ok(Some(size))
483		}
484		fn stop(&mut self, _code: u32) {}
485		async fn closed(&mut self) -> Result<(), Self::Error> {
486			Ok(())
487		}
488	}
489
490	/// Encode a lite-05 Setup stream: the `DataType::Setup` tag then the SETUP message.
491	fn lite05_setup(path: Option<&str>) -> Vec<u8> {
492		let v = lite::Version::Lite05Wip;
493		let mut buf = Vec::new();
494		lite::DataType::Setup.encode(&mut buf, v).unwrap();
495		lite::Setup {
496			path: path.map(str::to_string),
497		}
498		.encode(&mut buf, v)
499		.unwrap();
500		buf
501	}
502
503	/// Encode a lite-05 Group uni stream header (just the `DataType::Group` tag).
504	fn lite05_group() -> Vec<u8> {
505		let mut buf = Vec::new();
506		lite::DataType::Group
507			.encode(&mut buf, lite::Version::Lite05Wip)
508			.unwrap();
509		buf
510	}
511
512	#[tokio::test(start_paused = true)]
513	async fn accept_request_reads_lite05_path() {
514		let session = FakeSession::new(ALPN_LITE_05_WIP, [lite05_setup(Some("/team/room"))]);
515		let request = Server::new()
516			.with_versions(Version::Lite(lite::Version::Lite05Wip).into())
517			.accept_request(session)
518			.await
519			.unwrap();
520		assert_eq!(request.path(), Some("/team/room"));
521	}
522
523	#[tokio::test(start_paused = true)]
524	async fn accept_request_lite05_without_path_is_none() {
525		let session = FakeSession::new(ALPN_LITE_05_WIP, [lite05_setup(None)]);
526		let request = Server::new()
527			.with_versions(Version::Lite(lite::Version::Lite05Wip).into())
528			.accept_request(session)
529			.await
530			.unwrap();
531		assert_eq!(request.path(), None);
532	}
533
534	#[tokio::test(start_paused = true)]
535	async fn accept_request_skips_uni_stream_before_setup() {
536		// A Group racing ahead of the SETUP is reset and skipped; the gate keeps
537		// reading until it finds the SETUP.
538		let session = FakeSession::new(ALPN_LITE_05_WIP, [lite05_group(), lite05_setup(Some("/team/room"))]);
539		let request = Server::new()
540			.with_versions(Version::Lite(lite::Version::Lite05Wip).into())
541			.accept_request(session)
542			.await
543			.unwrap();
544		assert_eq!(request.path(), Some("/team/room"));
545	}
546}