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_20, 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			assigned_origin: crate::Origin::random(),
89			inner: Some(RequestInner {
90				server: self.clone(),
91				handshake,
92			}),
93		};
94
95		let (encoding, supported) = match session.protocol() {
96			Some(ALPN_20) => {
97				self.versions
98					.select(Version::Ietf(ietf::Version::Draft20))
99					.ok_or(Error::Version)?;
100				return self.accept_ietf_modern(session, ietf::Version::Draft20).await;
101			}
102			Some(ALPN_19) => {
103				self.versions
104					.select(Version::Ietf(ietf::Version::Draft19))
105					.ok_or(Error::Version)?;
106				return self.accept_ietf_modern(session, ietf::Version::Draft19).await;
107			}
108			Some(ALPN_18) => {
109				self.versions
110					.select(Version::Ietf(ietf::Version::Draft18))
111					.ok_or(Error::Version)?;
112				return self.accept_ietf_modern(session, ietf::Version::Draft18).await;
113			}
114			Some(ALPN_17) => {
115				self.versions
116					.select(Version::Ietf(ietf::Version::Draft17))
117					.ok_or(Error::Version)?;
118				return self.accept_ietf_modern(session, ietf::Version::Draft17).await;
119			}
120			Some(ALPN_16) => {
121				let v = self
122					.versions
123					.select(Version::Ietf(ietf::Version::Draft16))
124					.ok_or(Error::Version)?;
125				(v, v.into())
126			}
127			Some(ALPN_15) => {
128				let v = self
129					.versions
130					.select(Version::Ietf(ietf::Version::Draft15))
131					.ok_or(Error::Version)?;
132				(v, v.into())
133			}
134			Some(ALPN_14) => {
135				let v = self
136					.versions
137					.select(Version::Ietf(ietf::Version::Draft14))
138					.ok_or(Error::Version)?;
139				(v, v.into())
140			}
141			Some(alpn @ (ALPN_LITE_05 | ALPN_LITE_06_WIP)) => {
142				let version = match alpn {
143					ALPN_LITE_06_WIP => lite::Version::Lite06Wip,
144					_ => lite::Version::Lite05,
145				};
146				self.versions.select(Version::Lite(version)).ok_or(Error::Version)?;
147
148				// Gate on the client's SETUP: read it before serving so the caller can
149				// scope by the advertised path. Seeded back into `start` on `ok()` so
150				// PROBE gating resolves without re-reading the (consumed) Setup Stream.
151				let client_setup = lite::accept_setup(&session, version).await?;
152				return Ok(Request {
153					path: client_setup.path.clone(),
154					role: client_setup.role,
155					origin: client_setup.origin,
156					assigned_origin: crate::Origin::random(),
157					inner: Some(RequestInner {
158						server: self.clone(),
159						handshake: Handshake::LiteSetup {
160							session,
161							version,
162							client_setup,
163						},
164					}),
165				});
166			}
167			Some(ALPN_LITE_04) => {
168				self.versions
169					.select(Version::Lite(lite::Version::Lite04))
170					.ok_or(Error::Version)?;
171				return Ok(deferred(Handshake::LiteBare {
172					session,
173					version: lite::Version::Lite04,
174				}));
175			}
176			Some(ALPN_LITE_03) => {
177				self.versions
178					.select(Version::Lite(lite::Version::Lite03))
179					.ok_or(Error::Version)?;
180				return Ok(deferred(Handshake::LiteBare {
181					session,
182					version: lite::Version::Lite03,
183				}));
184			}
185			Some(ALPN_LITE) | None => {
186				let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?;
187				(Version::Ietf(ietf::Version::Draft14), supported)
188			}
189			Some(p) => return Err(Error::UnknownAlpn(p.to_string())),
190		};
191
192		// Legacy bidi SETUP exchange (IETF 14-16, lite 01/02). Read the client's
193		// SETUP to choose the version; `ok()` sends the server SETUP and starts.
194		let mut stream = Stream::accept(&session, encoding).await?;
195		let mut client: setup::Client = stream.reader.decode().await?;
196
197		let version = client
198			.versions
199			.iter()
200			.flat_map(|v| Version::try_from(*v).ok())
201			.find(|v| supported.contains(v))
202			.ok_or(Error::Version)?;
203
204		// Pull the request path and max request ID out now (IETF only) so `ok()`
205		// doesn't re-decode the consumed parameters. moq-transport carries the path
206		// in its SETUP just like lite-05.
207		let (path, request_id_max, peer_declared) = match version {
208			Version::Ietf(v) => {
209				let params = ietf::Parameters::decode(&mut client.parameters, v)?;
210				let path = match params.get_bytes(ietf::ParameterBytes::Path) {
211					Some(bytes) => Some(
212						std::str::from_utf8(bytes)
213							.map_err(|_| Error::Decode(crate::DecodeError::InvalidValue))?
214							.to_owned(),
215					),
216					None => None,
217				};
218				let request_id_max = params
219					.get_varint(ietf::ParameterVarInt::MaxRequestId)
220					.map(ietf::RequestId);
221				let peer_declared = ietf::peer::Peer {
222					solicit: ietf::solicit::from_setup(&params, v)?,
223					..Default::default()
224				};
225				(path, request_id_max, peer_declared)
226			}
227			Version::Lite(_) => (None, None, ietf::peer::Peer::default()),
228		};
229
230		Ok(Request {
231			path,
232			role: None,
233			origin: None,
234			assigned_origin: crate::Origin::random(),
235			inner: Some(RequestInner {
236				server: self.clone(),
237				handshake: Handshake::Legacy {
238					session,
239					stream,
240					version,
241					request_id_max,
242					peer_declared,
243				},
244			}),
245		})
246	}
247
248	/// Read a draft-17/18 client's SETUP (with its request path) off its uni stream,
249	/// then pause. `ok()` starts the session and hands the stream back for GOAWAY.
250	async fn accept_ietf_modern<S: web_transport_trait::Session>(
251		&self,
252		session: S,
253		version: ietf::Version,
254	) -> Result<Request<S>, Error> {
255		let peer_setup = ietf::accept_setup(&session, version).await?;
256		Ok(Request {
257			path: peer_setup.path.clone(),
258			role: None,
259			// A moq-transport peer only has an identity if it negotiated the MoQ
260			// Cluster extension and declared a non-zero Hop ID.
261			origin: peer_setup
262				.declared
263				.cluster
264				.origin
265				.filter(|o| *o != crate::Origin::UNKNOWN),
266			assigned_origin: crate::Origin::random(),
267			inner: Some(RequestInner {
268				server: self.clone(),
269				handshake: Handshake::IetfModern {
270					session,
271					version,
272					peer_setup,
273				},
274			}),
275		})
276	}
277}
278
279/// A paused server-side handshake.
280///
281/// Returned by [`Server::accept_request`] once the peer's advertised
282/// [`path`](Self::path) is known but before the session is granted anything. Set
283/// the origins to serve, then call [`ok`](Self::ok) to complete the handshake, or
284/// [`close`](Self::close) to reject it. Modeled on the WebTransport `Request` in
285/// moq-native.
286pub struct Request<S: web_transport_trait::Session> {
287	path: Option<String>,
288	role: Option<Role>,
289	origin: Option<crate::Origin>,
290	/// The identity this session's routes are stamped with when the peer declares none
291	/// on the wire. Fresh per request unless the caller overrides it
292	/// ([`Request::with_peer_origin`]).
293	assigned_origin: crate::Origin,
294	// Taken by `ok`/`close`; `Drop` rejects the handshake if neither ran.
295	inner: Option<RequestInner<S>>,
296}
297
298/// The parts of a [`Request`] consumed by [`Request::ok`] / [`Request::close`].
299struct RequestInner<S: web_transport_trait::Session> {
300	server: Server,
301	handshake: Handshake<S>,
302}
303
304/// The handshake state captured at the pause point. Every variant defers its
305/// session start to [`Request::ok`] so origins set on the Request still apply.
306enum Handshake<S: web_transport_trait::Session> {
307	/// Modern IETF (17/18): the client's SETUP (with its request path) has been read
308	/// off its uni stream; `ok()` starts the session, handing that stream back for
309	/// GOAWAY monitoring.
310	IetfModern {
311		session: S,
312		version: ietf::Version,
313		peer_setup: ietf::PeerSetup<S>,
314	},
315	/// moq-lite 03/04: no Setup Stream.
316	LiteBare { session: S, version: lite::Version },
317	/// Legacy IETF (draft 14-16) and lite 01/02: the client SETUP has been read off
318	/// the bidi stream (including its request path) but the server SETUP hasn't been
319	/// sent. `ok()` finishes it.
320	Legacy {
321		session: S,
322		stream: Stream<S, Version>,
323		version: Version,
324		request_id_max: Option<ietf::RequestId>,
325		/// What the client's SETUP declared, for the options `ok()` acts on.
326		peer_declared: ietf::peer::Peer,
327	},
328	/// moq-lite 05+: the client's Setup Stream has been read. `ok()` starts the
329	/// session, seeding the SETUP back so PROBE gating resolves.
330	LiteSetup {
331		session: S,
332		version: lite::Version,
333		client_setup: lite::Setup,
334	},
335}
336
337impl<S: web_transport_trait::Session> Request<S> {
338	/// The request path the client advertised in its SETUP.
339	///
340	/// Empty when the client advertised none: either it sent an empty path, or the
341	/// version carries none in-band (lite 01-04). Those mean the same thing, so the
342	/// wire distinction isn't surfaced. Populated for moq-lite-05 and every
343	/// moq-transport draft we speak. See the note on [`Server::accept_request`].
344	pub fn path(&self) -> &str {
345		self.path.as_deref().unwrap_or("")
346	}
347
348	/// The single [`Role`] the client advertised in its SETUP, or `None` for a
349	/// bidirectional session.
350	///
351	/// Only moq-lite-05 carries a role, so `None` covers three cases that the wire
352	/// doesn't distinguish: an older version, a client that omitted the parameter, and a
353	/// client that explicitly advertised both directions. All three mean the same thing
354	/// (the client may publish and subscribe), so authorize on what the token grants.
355	/// See the note on [`Server::accept_request`].
356	pub fn role(&self) -> Option<Role> {
357		self.role
358	}
359
360	/// The origin identity declared by the peer, when the negotiated protocol carries one.
361	///
362	/// A moq-lite-05+ endpoint declares this when it attaches a publish or subscribe
363	/// origin; a `moqt-17`+ endpoint declares it via the MoQ Cluster extension. Older
364	/// versions and endpoints without one return `None`.
365	///
366	/// Self-declared, so treat it as a correlation hint rather than an
367	/// authenticated identity: authorize on the token or client certificate.
368	pub fn peer_origin(&self) -> Option<crate::Origin> {
369		self.origin
370	}
371
372	/// Publish to the connected client. Overrides any value from the [`Server`]
373	/// builder; typically set after inspecting [`path`](Self::path).
374	pub fn with_publisher(mut self, publish: impl Consume<origin::Consumer>) -> Self {
375		self.inner_mut().server.publish = Some(publish.consume());
376		self
377	}
378
379	/// Subscribe to the connected client. Overrides any value from the [`Server`] builder.
380	pub fn with_subscriber(mut self, subscribe: origin::Producer) -> Self {
381		self.inner_mut().server.subscribe = Some(subscribe);
382		self
383	}
384
385	/// Assign the identity this peer's routes are attributed to, overriding the fresh
386	/// per-session default.
387	///
388	/// Only for a peer whose identity the server has actually established, such as one
389	/// authenticated by mTLS or a token ([`crate::Client::with_peer_origin`] is the
390	/// dialing-side equivalent). An identity the peer declares on the wire still wins.
391	///
392	/// Two sessions given the same origin are treated as one endpoint: routes learned
393	/// from either are kept off both, and content arriving on either is interchangeable
394	/// with the other's. That is the point when they really are one peer reconnecting or
395	/// running redundant links, and a bug otherwise. Derive it from the authenticated
396	/// identity, never from something coarser like the remote address.
397	pub fn with_peer_origin(mut self, origin: crate::Origin) -> Self {
398		self.assigned_origin = origin;
399		self
400	}
401
402	/// Set the per-connection [`stats::Session`] context. Overrides any value from the
403	/// [`Server`] builder.
404	pub fn with_stats(mut self, stats: stats::Session) -> Self {
405		self.inner_mut().server.stats = stats;
406		self
407	}
408
409	fn inner_mut(&mut self) -> &mut RequestInner<S> {
410		self.inner.as_mut().expect("request already responded")
411	}
412
413	/// Accept the session, returning the [`Session`] and the [`Driver`] that runs
414	/// its protocol work.
415	pub async fn ok(mut self) -> Result<(Session, Driver), Error> {
416		let peer_origin = Some(self.assigned_origin);
417		let RequestInner { server, handshake } = self.inner.take().expect("request already responded");
418
419		// Tag the origin pair with the stats context so the model attributes reads
420		// (egress) and writes (ingress) for this session. One shared context across
421		// both halves keeps presence and viewer counts from double-attributing.
422		let publish = server.publish.map(|origin| origin.with_stats(server.stats.clone()));
423		let subscribe = server.subscribe.map(|origin| origin.with_stats(server.stats.clone()));
424
425		let (session, mut stream, version, request_id_max, peer_declared) = match handshake {
426			Handshake::IetfModern {
427				session,
428				version,
429				peer_setup,
430			} => {
431				// The client's SETUP was read in `accept_request`; hand the stream back
432				// for GOAWAY. A server never advertises a path, hence `None`.
433				let protocol = ietf::start(ietf::Config {
434					session: session.clone(),
435					setup: None,
436					request_id_max: None,
437					client: false,
438					publish,
439					subscribe,
440					peer_origin,
441					// Only the dialing side prices a link.
442					cost: None,
443					version,
444					path: None,
445					peer_setup_stream: Some(peer_setup.stream),
446					peer_declared: Some(peer_setup.declared),
447				})?;
448				tracing::debug!(?version, "connected");
449				return Ok(Session::new(session, version.into(), None, protocol));
450			}
451			Handshake::LiteBare { session, version } => {
452				let start = lite::start(lite::Config {
453					session: session.clone(),
454					setup_stream: None,
455					publish,
456					subscribe,
457					peer_origin,
458					version,
459					our_setup: lite::Setup::default(),
460					peer_setup: None,
461				})?;
462				return Ok(Session::new(
463					session,
464					version.into(),
465					start.recv_bandwidth,
466					start.driver,
467				));
468			}
469			Handshake::LiteSetup {
470				session,
471				version,
472				client_setup,
473			} => {
474				// We report what the transport actually measures; a server never
475				// advertises a request Path or Role.
476				let our_setup = lite::Setup {
477					probe: lite::ProbeLevel::detect(&session),
478					path: None,
479					role: None,
480					// The dialing side prices the link; we charge what its SETUP declared.
481					cost: None,
482					// Filled by `lite::start` from the attached origin handles.
483					origin: None,
484				};
485				let start = lite::start(lite::Config {
486					session: session.clone(),
487					setup_stream: None,
488					publish,
489					subscribe,
490					peer_origin,
491					version,
492					our_setup,
493					peer_setup: Some(client_setup),
494				})?;
495				return Ok(Session::new(
496					session,
497					version.into(),
498					start.recv_bandwidth,
499					start.driver,
500				));
501			}
502			Handshake::Legacy {
503				session,
504				stream,
505				version,
506				request_id_max,
507				peer_declared,
508			} => (session, stream, version, request_id_max, peer_declared),
509		};
510
511		// Encode parameters using the version-appropriate type.
512		let parameters = match version {
513			Version::Ietf(v) => {
514				let mut parameters = ietf::Parameters::default();
515				parameters.set_varint(ietf::ParameterVarInt::MaxRequestId, u32::MAX as u64);
516				parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
517				ietf::solicit::into_setup(&mut parameters, v);
518				parameters.encode_bytes(v)?
519			}
520			Version::Lite(v) => lite::Parameters::default().encode_bytes(v)?,
521		};
522
523		let server_setup = setup::Server {
524			version: version.into(),
525			parameters,
526		};
527		stream.writer.encode(&server_setup).await?;
528
529		let (recv_bw, protocol) = match version {
530			Version::Lite(v) => {
531				let stream = stream.with_version(v);
532				// Pre-lite-05: no Setup Stream, so nothing to advertise or seed.
533				let start = lite::start(lite::Config {
534					session: session.clone(),
535					setup_stream: Some(stream),
536					publish,
537					subscribe,
538					peer_origin,
539					version: v,
540					our_setup: lite::Setup::default(),
541					peer_setup: None,
542				})?;
543				(start.recv_bandwidth, start.driver)
544			}
545			Version::Ietf(v) => {
546				let stream = stream.with_version(v);
547				// Draft 14-16: path came in the bidi SETUP, no uni SETUP to hand back.
548				let protocol = ietf::start(ietf::Config {
549					session: session.clone(),
550					setup: Some(stream),
551					request_id_max,
552					client: false,
553					publish,
554					subscribe,
555					peer_origin,
556					cost: None,
557					version: v,
558					path: None,
559					peer_setup_stream: None,
560					peer_declared: Some(peer_declared),
561				})?;
562				(None, protocol)
563			}
564		};
565
566		Ok(Session::new(session, version, recv_bw, protocol))
567	}
568
569	/// Reject the session, closing the transport with `err`'s wire code.
570	pub fn close(mut self, err: Error) {
571		let inner = self.inner.take().expect("request already responded");
572		inner.close(err);
573	}
574}
575
576impl<S: web_transport_trait::Session> RequestInner<S> {
577	fn close(self, err: Error) {
578		let session = match self.handshake {
579			Handshake::IetfModern { session, .. } => session,
580			Handshake::LiteBare { session, .. } => session,
581			Handshake::Legacy { session, .. } => session,
582			Handshake::LiteSetup { session, .. } => session,
583		};
584		session.close(err.to_code(), &err.to_string());
585	}
586}
587
588impl<S: web_transport_trait::Session> Drop for Request<S> {
589	// A dropped request would otherwise leave the client hanging until its idle
590	// timeout: it already sent SETUP and is waiting on a response. Reject loudly.
591	fn drop(&mut self) {
592		if let Some(inner) = self.inner.take() {
593			tracing::warn!("Request dropped without ok() or close(); rejecting the session");
594			inner.close(Error::Cancel);
595		}
596	}
597}
598
599#[cfg(test)]
600mod tests {
601	use super::*;
602	use crate::Origin;
603	use std::{
604		collections::VecDeque,
605		sync::{Arc, Mutex},
606	};
607
608	use crate::ALPN_LITE_05;
609	use bytes::Bytes;
610
611	fn occurrences(log: &crate::lite::test_transport::Log, needle: &[u8]) -> usize {
612		let writes = log.writes.lock().unwrap();
613		writes.windows(needle.len()).filter(|window| *window == needle).count()
614	}
615
616	#[derive(Debug, Clone, Default)]
617	struct FakeError;
618	impl std::fmt::Display for FakeError {
619		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620			write!(f, "fake transport error")
621		}
622	}
623	impl std::error::Error for FakeError {}
624	impl web_transport_trait::Error for FakeError {
625		fn session_error(&self) -> Option<(u32, String)> {
626			Some((0, "closed".to_string()))
627		}
628	}
629
630	/// A session that replays a queue of unidirectional streams (each a `Vec<u8>`) in
631	/// order from `accept_uni`; everything else is inert.
632	#[derive(Clone)]
633	struct FakeSession {
634		protocol: Option<&'static str>,
635		uni: Arc<Mutex<VecDeque<Vec<u8>>>>,
636	}
637
638	impl FakeSession {
639		fn new(protocol: &'static str, uni: impl IntoIterator<Item = Vec<u8>>) -> Self {
640			Self {
641				protocol: Some(protocol),
642				uni: Arc::new(Mutex::new(uni.into_iter().collect())),
643			}
644		}
645	}
646
647	impl web_transport_trait::Session for FakeSession {
648		type SendStream = FakeSend;
649		type RecvStream = FakeRecv;
650		type Error = FakeError;
651
652		async fn accept_uni(&self) -> Result<Self::RecvStream, Self::Error> {
653			// Drop the guard before any await so the future stays Send.
654			let data = self.uni.lock().unwrap().pop_front();
655			match data {
656				Some(data) => Ok(FakeRecv { data: data.into() }),
657				None => std::future::pending().await,
658			}
659		}
660		async fn accept_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
661			std::future::pending().await
662		}
663		async fn open_bi(&self) -> Result<(Self::SendStream, Self::RecvStream), Self::Error> {
664			std::future::pending().await
665		}
666		async fn open_uni(&self) -> Result<Self::SendStream, Self::Error> {
667			std::future::pending().await
668		}
669		fn send_datagram(&self, _payload: Bytes) -> Result<(), Self::Error> {
670			Ok(())
671		}
672		async fn recv_datagram(&self) -> Result<Bytes, Self::Error> {
673			std::future::pending().await
674		}
675		fn max_datagram_size(&self) -> usize {
676			1200
677		}
678		fn protocol(&self) -> Option<&str> {
679			self.protocol
680		}
681		fn close(&self, _code: u32, _reason: &str) {}
682		async fn closed(&self) -> Self::Error {
683			std::future::pending().await
684		}
685	}
686
687	#[derive(Clone, Default)]
688	struct FakeSend;
689	impl web_transport_trait::SendStream for FakeSend {
690		type Error = FakeError;
691		async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
692			Ok(buf.len())
693		}
694		fn set_priority(&mut self, _order: u8) {}
695		fn finish(&mut self) -> Result<(), Self::Error> {
696			Ok(())
697		}
698		fn reset(&mut self, _code: u32) {}
699		async fn closed(&mut self) -> Result<(), Self::Error> {
700			Ok(())
701		}
702	}
703
704	struct FakeRecv {
705		data: VecDeque<u8>,
706	}
707	impl web_transport_trait::RecvStream for FakeRecv {
708		type Error = FakeError;
709		async fn read(&mut self, dst: &mut [u8]) -> Result<Option<usize>, Self::Error> {
710			if self.data.is_empty() {
711				return Ok(None);
712			}
713			let size = dst.len().min(self.data.len());
714			for slot in dst.iter_mut().take(size) {
715				*slot = self.data.pop_front().unwrap();
716			}
717			Ok(Some(size))
718		}
719		fn stop(&mut self, _code: u32) {}
720		async fn closed(&mut self) -> Result<(), Self::Error> {
721			Ok(())
722		}
723	}
724
725	/// Encode a lite-05 Setup Stream: the `DataType::Setup` tag then the SETUP message.
726	fn lite05_setup(path: Option<&str>, role: Option<Role>, origin: Option<Origin>) -> Vec<u8> {
727		let v = lite::Version::Lite05;
728		let mut buf = Vec::new();
729		lite::DataType::Setup.encode(&mut buf, v).unwrap();
730		lite::Setup {
731			probe: lite::ProbeLevel::None,
732			path: path.map(str::to_string),
733			role,
734			cost: None,
735			origin,
736		}
737		.encode(&mut buf, v)
738		.unwrap();
739		buf
740	}
741
742	/// Encode a draft-17+ Setup Stream: the unified SETUP message, whose parameters
743	/// carry the request path the same way lite-05's does.
744	fn ietf_setup(version: ietf::Version, path: Option<&str>) -> Vec<u8> {
745		let mut params = ietf::Parameters::default();
746		if let Some(path) = path {
747			params.set_bytes(ietf::ParameterBytes::Path, path.as_bytes().to_vec());
748		}
749		let parameters = params.encode_bytes(version).unwrap();
750
751		let mut buf = Vec::new();
752		setup::Setup { parameters }
753			.encode(&mut buf, crate::Version::Ietf(version))
754			.unwrap();
755		buf
756	}
757
758	#[tokio::test(start_paused = true)]
759	async fn accept_request_reads_ietf_path() {
760		// Every draft-17+ version gates on the SETUP stream before starting, so the
761		// path is known at authorization time just like lite-05.
762		for (alpn, version) in [
763			(ALPN_17, ietf::Version::Draft17),
764			(ALPN_18, ietf::Version::Draft18),
765			(ALPN_19, ietf::Version::Draft19),
766		] {
767			let session = FakeSession::new(alpn, [ietf_setup(version, Some("/team/room"))]);
768			let request = Server::new().accept_request(session).await.unwrap();
769			assert_eq!(request.path(), "/team/room", "{alpn}");
770		}
771	}
772
773	#[tokio::test(start_paused = true)]
774	async fn accept_request_ietf_without_path_is_empty() {
775		let session = FakeSession::new(ALPN_19, [ietf_setup(ietf::Version::Draft19, 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_ietf_empty_path_is_accepted() {
782		let session = FakeSession::new(ALPN_19, [ietf_setup(ietf::Version::Draft19, Some(""))]);
783		let request = Server::new().accept_request(session).await.unwrap();
784		assert_eq!(request.path(), "");
785	}
786
787	/// Encode a lite-05 GROUP uni stream header (just the `DataType::Group` tag).
788	fn lite05_group() -> Vec<u8> {
789		let mut buf = Vec::new();
790		lite::DataType::Group.encode(&mut buf, lite::Version::Lite05).unwrap();
791		buf
792	}
793
794	#[tokio::test(start_paused = true)]
795	async fn accept_request_reads_lite05_path() {
796		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(Some("/team/room"), None, None)]);
797		let request = Server::new().accept_request(session).await.unwrap();
798		assert_eq!(request.path(), "/team/room");
799		assert_eq!(request.role(), None, "a client that omits the role is bidirectional");
800	}
801
802	#[tokio::test(start_paused = true)]
803	async fn accept_request_lite05_without_path_is_empty() {
804		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(None, None, None)]);
805		let request = Server::new().accept_request(session).await.unwrap();
806		assert_eq!(request.path(), "");
807	}
808
809	#[tokio::test(start_paused = true)]
810	async fn accept_request_lite05_empty_path_is_accepted() {
811		// An empty path is valid on the wire and means the same as omitting it, so a
812		// client that wants the root doesn't have to special-case the parameter.
813		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(Some(""), None, None)]);
814		let request = Server::new().accept_request(session).await.unwrap();
815		assert_eq!(request.path(), "");
816	}
817
818	#[tokio::test(start_paused = true)]
819	async fn accept_request_reads_lite05_role() {
820		let session = FakeSession::new(
821			ALPN_LITE_05,
822			[lite05_setup(Some("/team/room"), Some(Role::Publisher), None)],
823		);
824		let request = Server::new().accept_request(session).await.unwrap();
825		assert_eq!(request.role(), Some(Role::Publisher));
826	}
827
828	#[tokio::test(start_paused = true)]
829	async fn accept_request_skips_uni_stream_before_setup() {
830		// A GROUP racing ahead of the SETUP is STOP_SENDING-ed and skipped; the gate
831		// keeps reading until it finds the SETUP.
832		let session = FakeSession::new(
833			ALPN_LITE_05,
834			[lite05_group(), lite05_setup(Some("/team/room"), None, None)],
835		);
836		let request = Server::new().accept_request(session).await.unwrap();
837		assert_eq!(request.path(), "/team/room");
838	}
839
840	#[tokio::test(start_paused = true)]
841	async fn accept_request_reads_lite05_peer_origin() {
842		let origin = Origin::new(42).unwrap();
843		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(None, None, Some(origin))]);
844		let request = Server::new().accept_request(session).await.unwrap();
845		assert_eq!(request.peer_origin(), Some(origin));
846	}
847
848	#[tokio::test(start_paused = true)]
849	async fn anonymous_peer_origin_filters_routes_from_server_session() {
850		let other = Origin::new(778).unwrap();
851		let origin = crate::origin::Info::new(Origin::new(1).unwrap()).produce();
852
853		let gate = kio::Producer::new(true);
854		let transport = crate::lite::test_transport::SinkSession::gated_bi(gate.consume());
855		let log = transport.log.clone();
856		let version = ietf::Version::Draft18;
857		let request = Request {
858			path: None,
859			role: None,
860			origin: None,
861			assigned_origin: Origin::random(),
862			inner: Some(RequestInner {
863				server: Server::new().with_publisher(&origin),
864				handshake: Handshake::IetfModern {
865					session: transport,
866					version,
867					peer_setup: ietf::PeerSetup {
868						stream: crate::coding::Reader::new(
869							crate::lite::test_transport::PendingRecv,
870							Version::Ietf(version),
871						),
872						path: None,
873						declared: ietf::peer::Peer::default(),
874					},
875				},
876			}),
877		};
878		let assigned = request.assigned_origin;
879
880		let mut echoed_hops = crate::OriginList::new();
881		echoed_hops.push(assigned).unwrap();
882		let _echoed = origin
883			.create_broadcast(
884				"echoed-route",
885				crate::broadcast::Route::new()
886					.with_hops(echoed_hops)
887					.with_announce(true),
888			)
889			.unwrap();
890
891		let mut local_hops = crate::OriginList::new();
892		local_hops.push(other).unwrap();
893		let _local = origin
894			.create_broadcast(
895				"local-route",
896				crate::broadcast::Route::new().with_hops(local_hops).with_announce(true),
897			)
898			.unwrap();
899
900		let (session, driver) = request.ok().await.unwrap();
901		let _driver = tokio::spawn(driver);
902
903		for _ in 0..100 {
904			if occurrences(&log, b"local-route") > 0 {
905				break;
906			}
907			tokio::time::sleep(std::time::Duration::from_millis(1)).await;
908		}
909
910		assert_eq!(occurrences(&log, b"echoed-route"), 0);
911		assert_eq!(occurrences(&log, b"local-route"), 1);
912		drop(session);
913	}
914}