Skip to main content

moq_net/
server.rs

1use crate::origin;
2use crate::{
3	ALPN_14, ALPN_15, ALPN_16, ALPN_17, ALPN_18, ALPN_19, ALPN_LITE, ALPN_LITE_03, ALPN_LITE_04, ALPN_LITE_05,
4	ALPN_LITE_06_WIP, Consume, Driver, Error, NEGOTIATED, Role, Session, Version, Versions,
5	coding::{Decode, Encode, Stream},
6	ietf, lite, setup, stats,
7};
8
9/// A MoQ server session builder.
10#[derive(Default, Clone)]
11pub struct Server {
12	publish: Option<origin::Consumer>,
13	subscribe: Option<origin::Producer>,
14	stats: stats::Session,
15	versions: Versions,
16}
17
18impl Server {
19	/// A server that neither publishes nor subscribes until configured.
20	pub fn new() -> Self {
21		Default::default()
22	}
23
24	/// Publish to the connected client: the session reads from the given origin
25	/// (pass an [`origin::Producer`] or [`origin::Consumer`] by reference) and forwards
26	/// its announcements. Omit to publish nothing. Pre-scoped via
27	/// [`origin::Producer::scope`] for token-gated relays.
28	pub fn with_publisher(mut self, publish: impl Consume<origin::Consumer>) -> Self {
29		self.publish = Some(publish.consume());
30		self
31	}
32
33	/// Subscribe to the connected client: the session writes the broadcasts the
34	/// client announces into this [`origin::Producer`]. Omit to subscribe to nothing.
35	pub fn with_subscriber(mut self, subscribe: origin::Producer) -> Self {
36		self.subscribe = Some(subscribe);
37		self
38	}
39
40	/// Attach a per-connection [`stats::Session`] context. The session's publish
41	/// (egress) and subscribe (ingress) origin handles are tagged with it, so all
42	/// traffic counters are attributed through the model for this session's lifetime.
43	/// Pass [`stats::Session::default`] (a no-op context) to opt out.
44	pub fn with_stats(mut self, stats: stats::Session) -> Self {
45		self.stats = stats;
46		self
47	}
48
49	/// Set both publish and subscribe from one shared [`origin::Producer`].
50	pub fn with_origin(self, origin: origin::Producer) -> Self {
51		self.with_publisher(&origin).with_subscriber(origin)
52	}
53
54	/// Restrict which protocol versions to accept, in preference order.
55	/// Defaults to every version this crate supports.
56	pub fn with_versions(mut self, versions: Versions) -> Self {
57		self.versions = versions;
58		self
59	}
60
61	/// Perform the MoQ handshake as a server, returning the [`Session`] and the
62	/// [`Driver`] that runs its protocol work.
63	///
64	/// Convenience wrapper over [`accept_request`](Self::accept_request) that
65	/// completes the handshake immediately. Use `accept_request` when you need to
66	/// inspect the client's advertised path before deciding what to serve.
67	pub async fn accept<S: web_transport_trait::Session>(&self, session: S) -> Result<(Session, Driver), Error> {
68		self.accept_request(session).await?.ok().await
69	}
70
71	/// Begin the MoQ handshake, pausing once the client's request path is known so
72	/// the caller can authorize/scope before serving.
73	///
74	/// Reads the client's SETUP (the in-band path lives there on URL-less transports),
75	/// then returns a [`Request`]: inspect [`path`](Request::path), set the origins to
76	/// serve, and call [`ok`](Request::ok) or [`close`](Request::close). Session start
77	/// is deferred to `ok()`, so origins set on the `Request` always take effect.
78	///
79	/// The path is surfaced for moq-lite-05 and every moq-transport draft we speak;
80	/// it's empty on versions with no in-band request path (e.g. lite 01-04).
81	pub async fn accept_request<S: web_transport_trait::Session>(&self, session: S) -> Result<Request<S>, Error> {
82		// Regimes without a path to read defer to `ok()` without surfacing one, and
83		// carry no role or origin hint, so authorization is unchanged for them.
84		let deferred = |handshake| Request {
85			path: None,
86			role: None,
87			origin: None,
88			inner: Some(RequestInner {
89				server: self.clone(),
90				handshake,
91			}),
92		};
93
94		let (encoding, supported) = match session.protocol() {
95			Some(ALPN_19) => {
96				self.versions
97					.select(Version::Ietf(ietf::Version::Draft19))
98					.ok_or(Error::Version)?;
99				return self.accept_ietf_modern(session, ietf::Version::Draft19).await;
100			}
101			Some(ALPN_18) => {
102				self.versions
103					.select(Version::Ietf(ietf::Version::Draft18))
104					.ok_or(Error::Version)?;
105				return self.accept_ietf_modern(session, ietf::Version::Draft18).await;
106			}
107			Some(ALPN_17) => {
108				self.versions
109					.select(Version::Ietf(ietf::Version::Draft17))
110					.ok_or(Error::Version)?;
111				return self.accept_ietf_modern(session, ietf::Version::Draft17).await;
112			}
113			Some(ALPN_16) => {
114				let v = self
115					.versions
116					.select(Version::Ietf(ietf::Version::Draft16))
117					.ok_or(Error::Version)?;
118				(v, v.into())
119			}
120			Some(ALPN_15) => {
121				let v = self
122					.versions
123					.select(Version::Ietf(ietf::Version::Draft15))
124					.ok_or(Error::Version)?;
125				(v, v.into())
126			}
127			Some(ALPN_14) => {
128				let v = self
129					.versions
130					.select(Version::Ietf(ietf::Version::Draft14))
131					.ok_or(Error::Version)?;
132				(v, v.into())
133			}
134			Some(alpn @ (ALPN_LITE_05 | ALPN_LITE_06_WIP)) => {
135				let version = match alpn {
136					ALPN_LITE_06_WIP => lite::Version::Lite06Wip,
137					_ => lite::Version::Lite05,
138				};
139				self.versions.select(Version::Lite(version)).ok_or(Error::Version)?;
140
141				// Gate on the client's SETUP: read it before serving so the caller can
142				// scope by the advertised path. Seeded back into `start` on `ok()` so
143				// PROBE gating resolves without re-reading the (consumed) Setup Stream.
144				let client_setup = lite::accept_setup(&session, version).await?;
145				return Ok(Request {
146					path: client_setup.path.clone(),
147					role: client_setup.role,
148					origin: client_setup.origin,
149					inner: Some(RequestInner {
150						server: self.clone(),
151						handshake: Handshake::LiteSetup {
152							session,
153							version,
154							client_setup,
155						},
156					}),
157				});
158			}
159			Some(ALPN_LITE_04) => {
160				self.versions
161					.select(Version::Lite(lite::Version::Lite04))
162					.ok_or(Error::Version)?;
163				return Ok(deferred(Handshake::LiteBare {
164					session,
165					version: lite::Version::Lite04,
166				}));
167			}
168			Some(ALPN_LITE_03) => {
169				self.versions
170					.select(Version::Lite(lite::Version::Lite03))
171					.ok_or(Error::Version)?;
172				return Ok(deferred(Handshake::LiteBare {
173					session,
174					version: lite::Version::Lite03,
175				}));
176			}
177			Some(ALPN_LITE) | None => {
178				let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?;
179				(Version::Ietf(ietf::Version::Draft14), supported)
180			}
181			Some(p) => return Err(Error::UnknownAlpn(p.to_string())),
182		};
183
184		// Legacy bidi SETUP exchange (IETF 14-16, lite 01/02). Read the client's
185		// SETUP to choose the version; `ok()` sends the server SETUP and starts.
186		let mut stream = Stream::accept(&session, encoding).await?;
187		let mut client: setup::Client = stream.reader.decode().await?;
188
189		let version = client
190			.versions
191			.iter()
192			.flat_map(|v| Version::try_from(*v).ok())
193			.find(|v| supported.contains(v))
194			.ok_or(Error::Version)?;
195
196		// Pull the request path and max request ID out now (IETF only) so `ok()`
197		// doesn't re-decode the consumed parameters. moq-transport carries the path
198		// in its SETUP just like lite-05.
199		let (path, request_id_max, peer_declared) = match version {
200			Version::Ietf(v) => {
201				let params = ietf::Parameters::decode(&mut client.parameters, v)?;
202				let path = match params.get_bytes(ietf::ParameterBytes::Path) {
203					Some(bytes) => Some(
204						std::str::from_utf8(bytes)
205							.map_err(|_| Error::Decode(crate::DecodeError::InvalidValue))?
206							.to_owned(),
207					),
208					None => None,
209				};
210				let request_id_max = params
211					.get_varint(ietf::ParameterVarInt::MaxRequestId)
212					.map(ietf::RequestId);
213				let peer_declared = ietf::peer::Peer {
214					solicit: ietf::solicit::from_setup(&params, v)?,
215					..Default::default()
216				};
217				(path, request_id_max, peer_declared)
218			}
219			Version::Lite(_) => (None, None, ietf::peer::Peer::default()),
220		};
221
222		Ok(Request {
223			path,
224			role: None,
225			origin: None,
226			inner: Some(RequestInner {
227				server: self.clone(),
228				handshake: Handshake::Legacy {
229					session,
230					stream,
231					version,
232					request_id_max,
233					peer_declared,
234				},
235			}),
236		})
237	}
238
239	/// Read a draft-17/18 client's SETUP (with its request path) off its uni stream,
240	/// then pause. `ok()` starts the session and hands the stream back for GOAWAY.
241	async fn accept_ietf_modern<S: web_transport_trait::Session>(
242		&self,
243		session: S,
244		version: ietf::Version,
245	) -> Result<Request<S>, Error> {
246		let peer_setup = ietf::accept_setup(&session, version).await?;
247		Ok(Request {
248			path: peer_setup.path.clone(),
249			role: None,
250			// A moq-transport peer only has an identity if it negotiated the MoQ
251			// Cluster extension and declared a non-zero Hop ID.
252			origin: peer_setup
253				.declared
254				.cluster
255				.origin
256				.filter(|o| *o != crate::Origin::UNKNOWN),
257			inner: Some(RequestInner {
258				server: self.clone(),
259				handshake: Handshake::IetfModern {
260					session,
261					version,
262					peer_setup,
263				},
264			}),
265		})
266	}
267}
268
269/// A paused server-side handshake.
270///
271/// Returned by [`Server::accept_request`] once the peer's advertised
272/// [`path`](Self::path) is known but before the session is granted anything. Set
273/// the origins to serve, then call [`ok`](Self::ok) to complete the handshake, or
274/// [`close`](Self::close) to reject it. Modeled on the WebTransport `Request` in
275/// moq-native.
276pub struct Request<S: web_transport_trait::Session> {
277	path: Option<String>,
278	role: Option<Role>,
279	origin: Option<crate::Origin>,
280	// Taken by `ok`/`close`; `Drop` rejects the handshake if neither ran.
281	inner: Option<RequestInner<S>>,
282}
283
284/// The parts of a [`Request`] consumed by [`Request::ok`] / [`Request::close`].
285struct RequestInner<S: web_transport_trait::Session> {
286	server: Server,
287	handshake: Handshake<S>,
288}
289
290/// The handshake state captured at the pause point. Every variant defers its
291/// session start to [`Request::ok`] so origins set on the Request still apply.
292enum Handshake<S: web_transport_trait::Session> {
293	/// Modern IETF (17/18): the client's SETUP (with its request path) has been read
294	/// off its uni stream; `ok()` starts the session, handing that stream back for
295	/// GOAWAY monitoring.
296	IetfModern {
297		session: S,
298		version: ietf::Version,
299		peer_setup: ietf::PeerSetup<S>,
300	},
301	/// moq-lite 03/04: no Setup Stream.
302	LiteBare { session: S, version: lite::Version },
303	/// Legacy IETF (draft 14-16) and lite 01/02: the client SETUP has been read off
304	/// the bidi stream (including its request path) but the server SETUP hasn't been
305	/// sent. `ok()` finishes it.
306	Legacy {
307		session: S,
308		stream: Stream<S, Version>,
309		version: Version,
310		request_id_max: Option<ietf::RequestId>,
311		/// What the client's SETUP declared, for the options `ok()` acts on.
312		peer_declared: ietf::peer::Peer,
313	},
314	/// moq-lite 05+: the client's Setup Stream has been read. `ok()` starts the
315	/// session, seeding the SETUP back so PROBE gating resolves.
316	LiteSetup {
317		session: S,
318		version: lite::Version,
319		client_setup: lite::Setup,
320	},
321}
322
323impl<S: web_transport_trait::Session> Request<S> {
324	/// The request path the client advertised in its SETUP.
325	///
326	/// Empty when the client advertised none: either it sent an empty path, or the
327	/// version carries none in-band (lite 01-04). Those mean the same thing, so the
328	/// wire distinction isn't surfaced. Populated for moq-lite-05 and every
329	/// moq-transport draft we speak. See the note on [`Server::accept_request`].
330	pub fn path(&self) -> &str {
331		self.path.as_deref().unwrap_or("")
332	}
333
334	/// The single [`Role`] the client advertised in its SETUP, or `None` for a
335	/// bidirectional session.
336	///
337	/// Only moq-lite-05 carries a role, so `None` covers three cases that the wire
338	/// doesn't distinguish: an older version, a client that omitted the parameter, and a
339	/// client that explicitly advertised both directions. All three mean the same thing
340	/// (the client may publish and subscribe), so authorize on what the token grants.
341	/// See the note on [`Server::accept_request`].
342	pub fn role(&self) -> Option<Role> {
343		self.role
344	}
345
346	/// The origin identity declared by the peer, when the negotiated protocol carries one.
347	///
348	/// A moq-lite-05+ endpoint declares this when it attaches a publish or subscribe
349	/// origin; a `moqt-17`+ endpoint declares it via the MoQ Cluster extension. Older
350	/// versions and endpoints without one return `None`.
351	///
352	/// Self-declared, so treat it as a correlation hint rather than an
353	/// authenticated identity: authorize on the token or client certificate.
354	pub fn peer_origin(&self) -> Option<crate::Origin> {
355		self.origin
356	}
357
358	/// Publish to the connected client. Overrides any value from the [`Server`]
359	/// builder; typically set after inspecting [`path`](Self::path).
360	pub fn with_publisher(mut self, publish: impl Consume<origin::Consumer>) -> Self {
361		self.inner_mut().server.publish = Some(publish.consume());
362		self
363	}
364
365	/// Subscribe to the connected client. Overrides any value from the [`Server`] builder.
366	pub fn with_subscriber(mut self, subscribe: origin::Producer) -> Self {
367		self.inner_mut().server.subscribe = Some(subscribe);
368		self
369	}
370
371	/// Set the per-connection [`stats::Session`] context. Overrides any value from the
372	/// [`Server`] builder.
373	pub fn with_stats(mut self, stats: stats::Session) -> Self {
374		self.inner_mut().server.stats = stats;
375		self
376	}
377
378	fn inner_mut(&mut self) -> &mut RequestInner<S> {
379		self.inner.as_mut().expect("request already responded")
380	}
381
382	/// Accept the session, returning the [`Session`] and the [`Driver`] that runs
383	/// its protocol work.
384	pub async fn ok(mut self) -> Result<(Session, Driver), Error> {
385		let RequestInner { server, handshake } = self.inner.take().expect("request already responded");
386
387		// Tag the origin pair with the stats context so the model attributes reads
388		// (egress) and writes (ingress) for this session. One shared context across
389		// both halves keeps presence and viewer counts from double-attributing.
390		let publish = server.publish.map(|origin| origin.with_stats(server.stats.clone()));
391		let subscribe = server.subscribe.map(|origin| origin.with_stats(server.stats.clone()));
392
393		let (session, mut stream, version, request_id_max, peer_declared) = match handshake {
394			Handshake::IetfModern {
395				session,
396				version,
397				peer_setup,
398			} => {
399				// The client's SETUP was read in `accept_request`; hand the stream back
400				// for GOAWAY. A server never advertises a path, hence `None`.
401				let protocol = ietf::start(ietf::Config {
402					session: session.clone(),
403					setup: None,
404					request_id_max: None,
405					client: false,
406					publish,
407					subscribe,
408					peer_origin: None,
409					// Only the dialing side prices a link.
410					cost: None,
411					version,
412					path: None,
413					peer_setup_stream: Some(peer_setup.stream),
414					peer_declared: Some(peer_setup.declared),
415				})?;
416				tracing::debug!(?version, "connected");
417				return Ok(Session::new(session, version.into(), None, protocol));
418			}
419			Handshake::LiteBare { session, version } => {
420				let start = lite::start(lite::Config {
421					session: session.clone(),
422					setup_stream: None,
423					publish,
424					subscribe,
425					peer_origin: None,
426					version,
427					our_setup: lite::Setup::default(),
428					peer_setup: None,
429				})?;
430				return Ok(Session::new(
431					session,
432					version.into(),
433					start.recv_bandwidth,
434					start.driver,
435				));
436			}
437			Handshake::LiteSetup {
438				session,
439				version,
440				client_setup,
441			} => {
442				// We report send bitrate; a server never advertises a request Path or Role.
443				let our_setup = lite::Setup {
444					probe: lite::ProbeLevel::Report,
445					path: None,
446					role: None,
447					// The dialing side prices the link; we charge what its SETUP declared.
448					cost: None,
449					// Filled by `lite::start` from the attached origin handles.
450					origin: None,
451				};
452				let start = lite::start(lite::Config {
453					session: session.clone(),
454					setup_stream: None,
455					publish,
456					subscribe,
457					peer_origin: None,
458					version,
459					our_setup,
460					peer_setup: Some(client_setup),
461				})?;
462				return Ok(Session::new(
463					session,
464					version.into(),
465					start.recv_bandwidth,
466					start.driver,
467				));
468			}
469			Handshake::Legacy {
470				session,
471				stream,
472				version,
473				request_id_max,
474				peer_declared,
475			} => (session, stream, version, request_id_max, peer_declared),
476		};
477
478		// Encode parameters using the version-appropriate type.
479		let parameters = match version {
480			Version::Ietf(v) => {
481				let mut parameters = ietf::Parameters::default();
482				parameters.set_varint(ietf::ParameterVarInt::MaxRequestId, u32::MAX as u64);
483				parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
484				ietf::solicit::into_setup(&mut parameters, v);
485				parameters.encode_bytes(v)?
486			}
487			Version::Lite(v) => lite::Parameters::default().encode_bytes(v)?,
488		};
489
490		let server_setup = setup::Server {
491			version: version.into(),
492			parameters,
493		};
494		stream.writer.encode(&server_setup).await?;
495
496		let (recv_bw, protocol) = match version {
497			Version::Lite(v) => {
498				let stream = stream.with_version(v);
499				// Pre-lite-05: no Setup Stream, so nothing to advertise or seed.
500				let start = lite::start(lite::Config {
501					session: session.clone(),
502					setup_stream: Some(stream),
503					publish,
504					subscribe,
505					peer_origin: None,
506					version: v,
507					our_setup: lite::Setup::default(),
508					peer_setup: None,
509				})?;
510				(start.recv_bandwidth, start.driver)
511			}
512			Version::Ietf(v) => {
513				let stream = stream.with_version(v);
514				// Draft 14-16: path came in the bidi SETUP, no uni SETUP to hand back.
515				let protocol = ietf::start(ietf::Config {
516					session: session.clone(),
517					setup: Some(stream),
518					request_id_max,
519					client: false,
520					publish,
521					subscribe,
522					peer_origin: None,
523					cost: None,
524					version: v,
525					path: None,
526					peer_setup_stream: None,
527					peer_declared: Some(peer_declared),
528				})?;
529				(None, protocol)
530			}
531		};
532
533		Ok(Session::new(session, version, recv_bw, protocol))
534	}
535
536	/// Reject the session, closing the transport with `err`'s wire code.
537	pub fn close(mut self, err: Error) {
538		let inner = self.inner.take().expect("request already responded");
539		inner.close(err);
540	}
541}
542
543impl<S: web_transport_trait::Session> RequestInner<S> {
544	fn close(self, err: Error) {
545		let session = match self.handshake {
546			Handshake::IetfModern { session, .. } => session,
547			Handshake::LiteBare { session, .. } => session,
548			Handshake::Legacy { session, .. } => session,
549			Handshake::LiteSetup { session, .. } => session,
550		};
551		session.close(err.to_code(), &err.to_string());
552	}
553}
554
555impl<S: web_transport_trait::Session> Drop for Request<S> {
556	// A dropped request would otherwise leave the client hanging until its idle
557	// timeout: it already sent SETUP and is waiting on a response. Reject loudly.
558	fn drop(&mut self) {
559		if let Some(inner) = self.inner.take() {
560			tracing::warn!("Request dropped without ok() or close(); rejecting the session");
561			inner.close(Error::Cancel);
562		}
563	}
564}
565
566#[cfg(test)]
567mod tests {
568	use super::*;
569	use crate::Origin;
570	use std::{
571		collections::VecDeque,
572		sync::{Arc, Mutex},
573	};
574
575	use crate::ALPN_LITE_05;
576	use bytes::Bytes;
577
578	#[derive(Debug, Clone, Default)]
579	struct FakeError;
580	impl std::fmt::Display for FakeError {
581		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
582			write!(f, "fake transport error")
583		}
584	}
585	impl std::error::Error for FakeError {}
586	impl web_transport_trait::Error for FakeError {
587		fn session_error(&self) -> Option<(u32, String)> {
588			Some((0, "closed".to_string()))
589		}
590	}
591
592	/// A session that replays a queue of unidirectional streams (each a `Vec<u8>`) in
593	/// order from `accept_uni`; everything else is inert.
594	#[derive(Clone)]
595	struct FakeSession {
596		protocol: Option<&'static str>,
597		uni: Arc<Mutex<VecDeque<Vec<u8>>>>,
598	}
599
600	impl FakeSession {
601		fn new(protocol: &'static str, uni: impl IntoIterator<Item = Vec<u8>>) -> Self {
602			Self {
603				protocol: Some(protocol),
604				uni: Arc::new(Mutex::new(uni.into_iter().collect())),
605			}
606		}
607	}
608
609	impl web_transport_trait::Session for FakeSession {
610		type SendStream = FakeSend;
611		type RecvStream = FakeRecv;
612		type Error = FakeError;
613
614		async fn accept_uni(&self) -> Result<Self::RecvStream, Self::Error> {
615			// Drop the guard before any await so the future stays Send.
616			let data = self.uni.lock().unwrap().pop_front();
617			match data {
618				Some(data) => Ok(FakeRecv { data: data.into() }),
619				None => std::future::pending().await,
620			}
621		}
622		async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
623			std::future::pending().await
624		}
625		async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
626			std::future::pending().await
627		}
628		async fn open_uni(&self) -> Result<Self::SendStream, Self::Error> {
629			std::future::pending().await
630		}
631		fn send_datagram(&self, _payload: Bytes) -> Result<(), Self::Error> {
632			Ok(())
633		}
634		async fn recv_datagram(&self) -> Result<Bytes, Self::Error> {
635			std::future::pending().await
636		}
637		fn max_datagram_size(&self) -> usize {
638			1200
639		}
640		fn protocol(&self) -> Option<&str> {
641			self.protocol
642		}
643		fn close(&self, _code: u32, _reason: &str) {}
644		async fn closed(&self) -> Self::Error {
645			std::future::pending().await
646		}
647	}
648
649	#[derive(Clone, Default)]
650	struct FakeSend;
651	impl web_transport_trait::SendStream for FakeSend {
652		type Error = FakeError;
653		async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
654			Ok(buf.len())
655		}
656		fn set_priority(&mut self, _order: u8) {}
657		fn finish(&mut self) -> Result<(), Self::Error> {
658			Ok(())
659		}
660		fn reset(&mut self, _code: u32) {}
661		async fn closed(&mut self) -> Result<(), Self::Error> {
662			Ok(())
663		}
664	}
665
666	struct FakeRecv {
667		data: VecDeque<u8>,
668	}
669	impl web_transport_trait::RecvStream for FakeRecv {
670		type Error = FakeError;
671		async fn read(&mut self, dst: &mut [u8]) -> Result<Option<usize>, Self::Error> {
672			if self.data.is_empty() {
673				return Ok(None);
674			}
675			let size = dst.len().min(self.data.len());
676			for slot in dst.iter_mut().take(size) {
677				*slot = self.data.pop_front().unwrap();
678			}
679			Ok(Some(size))
680		}
681		fn stop(&mut self, _code: u32) {}
682		async fn closed(&mut self) -> Result<(), Self::Error> {
683			Ok(())
684		}
685	}
686
687	/// Encode a lite-05 Setup Stream: the `DataType::Setup` tag then the SETUP message.
688	fn lite05_setup(path: Option<&str>, role: Option<Role>, origin: Option<Origin>) -> Vec<u8> {
689		let v = lite::Version::Lite05;
690		let mut buf = Vec::new();
691		lite::DataType::Setup.encode(&mut buf, v).unwrap();
692		lite::Setup {
693			probe: lite::ProbeLevel::None,
694			path: path.map(str::to_string),
695			role,
696			cost: None,
697			origin,
698		}
699		.encode(&mut buf, v)
700		.unwrap();
701		buf
702	}
703
704	/// Encode a draft-17+ Setup Stream: the unified SETUP message, whose parameters
705	/// carry the request path the same way lite-05's does.
706	fn ietf_setup(version: ietf::Version, path: Option<&str>) -> Vec<u8> {
707		let mut params = ietf::Parameters::default();
708		if let Some(path) = path {
709			params.set_bytes(ietf::ParameterBytes::Path, path.as_bytes().to_vec());
710		}
711		let parameters = params.encode_bytes(version).unwrap();
712
713		let mut buf = Vec::new();
714		setup::Setup { parameters }
715			.encode(&mut buf, crate::Version::Ietf(version))
716			.unwrap();
717		buf
718	}
719
720	#[tokio::test(start_paused = true)]
721	async fn accept_request_reads_ietf_path() {
722		// Every draft-17+ version gates on the SETUP stream before starting, so the
723		// path is known at authorization time just like lite-05.
724		for (alpn, version) in [
725			(ALPN_17, ietf::Version::Draft17),
726			(ALPN_18, ietf::Version::Draft18),
727			(ALPN_19, ietf::Version::Draft19),
728		] {
729			let session = FakeSession::new(alpn, [ietf_setup(version, Some("/team/room"))]);
730			let request = Server::new().accept_request(session).await.unwrap();
731			assert_eq!(request.path(), "/team/room", "{alpn}");
732		}
733	}
734
735	#[tokio::test(start_paused = true)]
736	async fn accept_request_ietf_without_path_is_empty() {
737		let session = FakeSession::new(ALPN_19, [ietf_setup(ietf::Version::Draft19, None)]);
738		let request = Server::new().accept_request(session).await.unwrap();
739		assert_eq!(request.path(), "");
740	}
741
742	#[tokio::test(start_paused = true)]
743	async fn accept_request_ietf_empty_path_is_accepted() {
744		let session = FakeSession::new(ALPN_19, [ietf_setup(ietf::Version::Draft19, Some(""))]);
745		let request = Server::new().accept_request(session).await.unwrap();
746		assert_eq!(request.path(), "");
747	}
748
749	/// Encode a lite-05 GROUP uni stream header (just the `DataType::Group` tag).
750	fn lite05_group() -> Vec<u8> {
751		let mut buf = Vec::new();
752		lite::DataType::Group.encode(&mut buf, lite::Version::Lite05).unwrap();
753		buf
754	}
755
756	#[tokio::test(start_paused = true)]
757	async fn accept_request_reads_lite05_path() {
758		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(Some("/team/room"), None, None)]);
759		let request = Server::new().accept_request(session).await.unwrap();
760		assert_eq!(request.path(), "/team/room");
761		assert_eq!(request.role(), None, "a client that omits the role is bidirectional");
762	}
763
764	#[tokio::test(start_paused = true)]
765	async fn accept_request_lite05_without_path_is_empty() {
766		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(None, None, None)]);
767		let request = Server::new().accept_request(session).await.unwrap();
768		assert_eq!(request.path(), "");
769	}
770
771	#[tokio::test(start_paused = true)]
772	async fn accept_request_lite05_empty_path_is_accepted() {
773		// An empty path is valid on the wire and means the same as omitting it, so a
774		// client that wants the root doesn't have to special-case the parameter.
775		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(Some(""), None, None)]);
776		let request = Server::new().accept_request(session).await.unwrap();
777		assert_eq!(request.path(), "");
778	}
779
780	#[tokio::test(start_paused = true)]
781	async fn accept_request_reads_lite05_role() {
782		let session = FakeSession::new(
783			ALPN_LITE_05,
784			[lite05_setup(Some("/team/room"), Some(Role::Publisher), None)],
785		);
786		let request = Server::new().accept_request(session).await.unwrap();
787		assert_eq!(request.role(), Some(Role::Publisher));
788	}
789
790	#[tokio::test(start_paused = true)]
791	async fn accept_request_skips_uni_stream_before_setup() {
792		// A GROUP racing ahead of the SETUP is STOP_SENDING-ed and skipped; the gate
793		// keeps reading until it finds the SETUP.
794		let session = FakeSession::new(
795			ALPN_LITE_05,
796			[lite05_group(), lite05_setup(Some("/team/room"), None, None)],
797		);
798		let request = Server::new().accept_request(session).await.unwrap();
799		assert_eq!(request.path(), "/team/room");
800	}
801
802	#[tokio::test(start_paused = true)]
803	async fn accept_request_reads_lite05_peer_origin() {
804		let origin = Origin::new(42).unwrap();
805		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(None, None, Some(origin))]);
806		let request = Server::new().accept_request(session).await.unwrap();
807		assert_eq!(request.peer_origin(), Some(origin));
808	}
809}