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