Skip to main content

moq_net/
server.rs

1//! Accepting a MoQ session, including the paused handshake that inspects the
2//! peer's SETUP before granting origins.
3
4use web_transport_trait::{MaybeSend, MaybeSync};
5
6use crate::origin;
7use crate::time::{Clock, Instant};
8use crate::{
9	ALPN_14, ALPN_15, ALPN_16, ALPN_17, ALPN_18, ALPN_19, ALPN_20, ALPN_21, ALPN_22, ALPN_LITE, ALPN_LITE_03,
10	ALPN_LITE_04, ALPN_LITE_05, ALPN_LITE_06, ALPN_LITE_07_WIP, Consume, Error, NEGOTIATED, Role, Session,
11	SessionError, Version, Versions,
12	coding::{Decode, Encode, Stream},
13	ietf, lite, setup, stats,
14};
15
16/// A MoQ server session builder.
17#[derive(Default, Clone)]
18pub struct Server {
19	publish: Option<origin::Consumer>,
20	subscribe: Option<origin::Producer>,
21	stats: stats::Session,
22	versions: Versions,
23}
24
25impl Server {
26	/// A server that neither publishes nor subscribes until configured.
27	pub fn new() -> Self {
28		Default::default()
29	}
30
31	/// Publish to the connected client: the session reads from the given origin
32	/// (pass an [`origin::Producer`] or [`origin::Consumer`] by reference) and forwards
33	/// its announcements. Omit to publish nothing. Pre-scoped via
34	/// [`origin::Producer::scope`] for token-gated relays.
35	pub fn with_publisher(mut self, publish: impl Consume<origin::Consumer>) -> Self {
36		self.publish = Some(publish.consume());
37		self
38	}
39
40	/// Subscribe to the connected client: the session writes the broadcasts the
41	/// client announces into this [`origin::Producer`]. Omit to subscribe to nothing.
42	pub fn with_subscriber(mut self, subscribe: origin::Producer) -> Self {
43		self.subscribe = Some(subscribe);
44		self
45	}
46
47	/// Attach a per-connection [`stats::Session`] context. The session's publish
48	/// (egress) and subscribe (ingress) origin handles are tagged with it, so all
49	/// traffic counters are attributed through the model for this session's lifetime.
50	/// Pass [`stats::Session::default`] (a no-op context) to opt out.
51	pub fn with_stats(mut self, stats: stats::Session) -> Self {
52		self.stats = stats;
53		self
54	}
55
56	/// Set both publish and subscribe from one shared [`origin::Producer`].
57	pub fn with_origin(self, origin: origin::Producer) -> Self {
58		self.with_publisher(&origin).with_subscriber(origin)
59	}
60
61	/// Restrict which protocol versions to accept, in preference order.
62	/// Defaults to every version this crate supports.
63	pub fn with_versions(mut self, versions: Versions) -> Self {
64		self.versions = versions;
65		self
66	}
67
68	/// The configured origin pair, each tagged with the stats context so the
69	/// model attributes reads (egress) and writes (ingress) for this session.
70	/// One shared context across both halves keeps presence and viewer counts
71	/// from double-attributing.
72	fn stat_tagged_origins(&self) -> (Option<origin::Consumer>, Option<origin::Producer>) {
73		let publish = self.publish.clone().map(|origin| origin.with_stats(self.stats.clone()));
74		let subscribe = self
75			.subscribe
76			.clone()
77			.map(|origin| origin.with_stats(self.stats.clone()));
78		(publish, subscribe)
79	}
80
81	/// Start a lite session on an accepted transport: wire the origins, answer
82	/// with our SETUP, and return the session and its driver.
83	fn start_lite<S>(
84		&self,
85		runtime: Clock,
86		session: S,
87		version: lite::Version,
88		client_setup: Option<lite::Setup>,
89		peer_hop: Option<crate::Hop>,
90	) -> Result<(Session, crate::Driver<S>), Error>
91	where
92		S: crate::transport::poll::Session,
93	{
94		let (publish, subscribe) = self.stat_tagged_origins();
95
96		// We report what the transport actually measures; a server never
97		// advertises a request Path or Role, and only the dialing side prices a
98		// link. Versions without a Setup Stream have nothing to advertise.
99		let our_setup = if version.has_setup_stream() {
100			lite::Setup {
101				probe: lite::ProbeLevel::detect(&session),
102				path: None,
103				role: None,
104				cost: None,
105				// Filled by `lite::start` from the attached origin handles.
106				hop: None,
107			}
108		} else {
109			lite::Setup::default()
110		};
111
112		let start = lite::start(lite::Config {
113			runtime: runtime.clone(),
114			session: session.clone(),
115			setup_stream: None,
116			publish,
117			subscribe,
118			peer_hop,
119			version,
120			our_setup,
121			peer_setup: client_setup,
122		})?;
123
124		Ok(Session::new(
125			runtime,
126			session,
127			version.into(),
128			start.recv_bandwidth,
129			crate::driver::Protocol::Lite(Box::new(start.driver)),
130			start.goaway,
131		))
132	}
133
134	/// Perform the MoQ handshake for moq-lite only, over any transport.
135	///
136	/// Same trade as [`Client::connect_lite`](crate::Client::connect_lite): no
137	/// thread-affinity bound on the transport, so a pinned `!Send` transport
138	/// works, and only a moq-lite ALPN is accepted (anything else is refused
139	/// with [`Error::Version`]). Completes the handshake immediately; a caller
140	/// gating on the advertised path uses
141	/// [`accept_request_lite`](Self::accept_request_lite) instead.
142	pub async fn accept_lite<S>(&self, now: Instant, session: S) -> Result<(Session, crate::Driver<S>), Error>
143	where
144		S: crate::transport::poll::Session,
145	{
146		self.accept_request_lite(now, session).await?.ok().await
147	}
148
149	/// Begin the moq-lite handshake, pausing like
150	/// [`accept_request`](Self::accept_request) but for moq-lite ALPNs only,
151	/// which is what drops the thread-affinity bounds: a pinned `!Send`
152	/// transport can gate on the advertised path too. Anything but a moq-lite
153	/// ALPN is refused with [`Error::Version`].
154	pub async fn accept_request_lite<S>(&self, now: Instant, mut session: S) -> Result<Handshake<S>, Error>
155	where
156		S: crate::transport::poll::Session,
157	{
158		let runtime = Clock::new(now);
159		let (path, role, origin, handshake) = match session.protocol() {
160			Some(alpn @ (ALPN_LITE_05 | ALPN_LITE_06 | ALPN_LITE_07_WIP)) => {
161				let version = match alpn {
162					ALPN_LITE_07_WIP => lite::Version::Lite07,
163					ALPN_LITE_06 => lite::Version::Lite06,
164					_ => lite::Version::Lite05,
165				};
166				self.versions.select(Version::Lite(version)).ok_or(Error::Version)?;
167				// Gate on the client's SETUP: read it before serving so the
168				// caller can scope by the advertised path. Seeded back into
169				// `start` on `ok()` so PROBE gating resolves without
170				// re-reading the (consumed) Setup Stream.
171				let client_setup = lite::accept_setup(&mut session, version).await?;
172				(
173					client_setup.path.clone(),
174					client_setup.role,
175					client_setup.hop,
176					PausedHandshake::LiteSetup {
177						session,
178						version,
179						client_setup,
180					},
181				)
182			}
183			Some(ALPN_LITE_04) => {
184				self.versions
185					.select(Version::Lite(lite::Version::Lite04))
186					.ok_or(Error::Version)?;
187				(
188					None,
189					None,
190					None,
191					PausedHandshake::LiteBare {
192						session,
193						version: lite::Version::Lite04,
194					},
195				)
196			}
197			Some(ALPN_LITE_03) => {
198				self.versions
199					.select(Version::Lite(lite::Version::Lite03))
200					.ok_or(Error::Version)?;
201				(
202					None,
203					None,
204					None,
205					PausedHandshake::LiteBare {
206						session,
207						version: lite::Version::Lite03,
208					},
209				)
210			}
211			_ => return Err(Error::Version),
212		};
213
214		Ok(Handshake {
215			path,
216			role,
217			origin,
218			assigned_hop: crate::Hop::random(),
219			inner: Some(RequestInner {
220				server: self.clone(),
221				runtime,
222				handshake,
223			}),
224		})
225	}
226
227	/// Perform the MoQ handshake as a server, returning the [`Session`] and its [`Driver`](crate::Driver).
228	///
229	/// Poll the returned driver with nondecreasing time, starting at `now`.
230	///
231	/// Convenience wrapper over [`accept_request`](Self::accept_request) that
232	/// completes the handshake immediately. Use `accept_request` when you need to
233	/// inspect the client's advertised path before deciding what to serve.
234	pub async fn accept<S>(&self, now: Instant, session: S) -> Result<(Session, crate::Driver<S>), Error>
235	where
236		S: crate::transport::poll::Boxable,
237		S::SendStream: MaybeSync,
238		S::RecvStream: MaybeSync,
239	{
240		self.accept_request(now, session).await?.ok().await
241	}
242
243	/// Begin the MoQ handshake, pausing once the client's request path is known so
244	/// the caller can authorize/scope before serving.
245	///
246	/// Reads the client's SETUP (the in-band path lives there on URL-less transports),
247	/// then returns a [`Handshake`]: inspect [`path`](Handshake::path), set the origins to
248	/// serve, and call [`ok`](Handshake::ok) or [`close`](Handshake::close). Session start
249	/// is deferred to `ok()`, so origins set on the handshake always take effect.
250	///
251	/// The path is surfaced for moq-lite-05 and newer, and every moq-transport
252	/// draft we speak; it's empty on versions with no in-band request path (lite 01-04).
253	pub async fn accept_request<S>(&self, now: Instant, mut session: S) -> Result<Handshake<S>, Error>
254	where
255		S: crate::transport::poll::Boxable,
256		S::SendStream: MaybeSync,
257		S::RecvStream: MaybeSync,
258	{
259		let runtime = Clock::new(now);
260		let (encoding, supported) = match session.protocol() {
261			Some(alpn @ (ALPN_22 | ALPN_21 | ALPN_20 | ALPN_19 | ALPN_18 | ALPN_17)) => {
262				let draft = match alpn {
263					ALPN_22 => ietf::Version::Draft22,
264					ALPN_21 => ietf::Version::Draft21,
265					ALPN_20 => ietf::Version::Draft20,
266					ALPN_19 => ietf::Version::Draft19,
267					ALPN_18 => ietf::Version::Draft18,
268					_ => ietf::Version::Draft17,
269				};
270
271				self.versions.select(Version::Ietf(draft)).ok_or(Error::Version)?;
272				return self.accept_ietf_modern(runtime, session, draft).await;
273			}
274			Some(ALPN_16) => {
275				let v = self
276					.versions
277					.select(Version::Ietf(ietf::Version::Draft16))
278					.ok_or(Error::Version)?;
279				(v, v.into())
280			}
281			Some(ALPN_15) => {
282				let v = self
283					.versions
284					.select(Version::Ietf(ietf::Version::Draft15))
285					.ok_or(Error::Version)?;
286				(v, v.into())
287			}
288			Some(ALPN_14) => {
289				let v = self
290					.versions
291					.select(Version::Ietf(ietf::Version::Draft14))
292					.ok_or(Error::Version)?;
293				(v, v.into())
294			}
295			// Every lite ALPN goes through the same entry point, which is also
296			// what a `!Send` transport calls directly.
297			Some(ALPN_LITE_07_WIP | ALPN_LITE_06 | ALPN_LITE_05 | ALPN_LITE_04 | ALPN_LITE_03) => {
298				return self.accept_request_lite(now, session).await;
299			}
300			Some(ALPN_LITE) | None => {
301				let supported = self.versions.filter(&NEGOTIATED.into()).ok_or(Error::Version)?;
302				(Version::Ietf(ietf::Version::Draft14), supported)
303			}
304			Some(p) => return Err(Error::UnknownAlpn(p.to_string())),
305		};
306
307		// Legacy bidi SETUP exchange (IETF 14-16, lite 01/02). Read the client's
308		// SETUP to choose the version; `ok()` sends the server SETUP and starts.
309		let mut stream = Stream::accept(&mut session, encoding).await?;
310		let mut client: setup::Client = stream.reader.decode().await?;
311
312		let version = client
313			.versions
314			.iter()
315			.flat_map(|v| Version::try_from(*v).ok())
316			.find(|v| supported.contains(v))
317			.ok_or(Error::Version)?;
318
319		// Pull the request path and max request ID out now (IETF only) so `ok()`
320		// doesn't re-decode the consumed parameters. moq-transport carries the path
321		// in its SETUP just like lite-05.
322		let (path, request_id_max, peer_declared) = match version {
323			Version::Ietf(v) => {
324				let params = ietf::Parameters::decode(&mut client.parameters, v)?;
325				let path = match params.get_bytes(ietf::ParameterBytes::Path) {
326					Some(bytes) => Some(
327						std::str::from_utf8(bytes)
328							.map_err(|_| Error::Decode(crate::DecodeError::InvalidValue))?
329							.to_owned(),
330					),
331					None => None,
332				};
333				let request_id_max = params
334					.get_varint(ietf::ParameterVarInt::MaxRequestId)
335					.map(ietf::RequestId);
336				let peer_declared = ietf::peer::Peer {
337					solicit: ietf::solicit::from_setup(&params, v)?,
338					hidden: ietf::hidden::from_setup(&params, v),
339					..Default::default()
340				};
341				(path, request_id_max, peer_declared)
342			}
343			Version::Lite(_) => (None, None, ietf::peer::Peer::default()),
344		};
345
346		Ok(Handshake {
347			path,
348			role: None,
349			origin: None,
350			assigned_hop: crate::Hop::random(),
351			inner: Some(RequestInner {
352				server: self.clone(),
353				runtime,
354				handshake: PausedHandshake::Boxed(Box::new(PausedLegacy {
355					session,
356					stream,
357					version,
358					request_id_max,
359					peer_declared,
360				})),
361			}),
362		})
363	}
364
365	/// Read a draft-17/18 client's SETUP (with its request path) off its uni stream,
366	/// then pause. `ok()` starts the session and hands the stream back for GOAWAY.
367	async fn accept_ietf_modern<S>(
368		&self,
369		runtime: Clock,
370		mut session: S,
371		version: ietf::Version,
372	) -> Result<Handshake<S>, Error>
373	where
374		S: crate::transport::poll::Boxable,
375		S::SendStream: MaybeSync,
376		S::RecvStream: MaybeSync,
377	{
378		let peer_setup = ietf::accept_setup(&mut session, version).await?;
379		Ok(Handshake {
380			path: peer_setup.path.clone(),
381			role: None,
382			// A moq-transport peer only has an identity if it negotiated the MoQ
383			// Cluster extension and declared a non-zero Hop ID.
384			origin: peer_setup.declared.cluster.hop.filter(|h| *h != crate::Hop::UNKNOWN),
385			assigned_hop: crate::Hop::random(),
386			inner: Some(RequestInner {
387				server: self.clone(),
388				runtime,
389				handshake: PausedHandshake::Boxed(Box::new(PausedIetfModern {
390					session,
391					version,
392					peer_setup,
393				})),
394			}),
395		})
396	}
397}
398
399/// A paused server-side handshake.
400///
401/// Returned by [`Server::accept_request`] once the peer's advertised
402/// [`path`](Self::path) is known but before the session is granted anything. Set
403/// the origins to serve, then call [`ok`](Self::ok) to complete the handshake, or
404/// [`close`](Self::close) to reject it. Modeled on the WebTransport `Request` in
405/// moq-tokio.
406pub struct Handshake<S: crate::transport::poll::Session> {
407	path: Option<String>,
408	role: Option<Role>,
409	origin: Option<crate::Hop>,
410	/// The identity this session's routes are stamped with when the peer declares none
411	/// on the wire. Fresh per request unless the caller overrides it
412	/// ([`Handshake::with_peer_hop`]).
413	assigned_hop: crate::Hop,
414	// Taken by `ok`/`close`; `Drop` rejects the handshake if neither ran.
415	inner: Option<RequestInner<S>>,
416}
417
418/// The parts of a [`Handshake`] consumed by [`Handshake::ok`] / [`Handshake::close`].
419struct RequestInner<S: crate::transport::poll::Session> {
420	server: Server,
421	/// Supplies the clock and timers for the accepted session.
422	runtime: Clock,
423	handshake: PausedHandshake<S>,
424}
425
426/// The handshake state captured at the pause point. Every variant defers its
427/// session start to [`Handshake::ok`] so origins set on the handshake still apply.
428enum PausedHandshake<S: crate::transport::poll::Session> {
429	/// moq-lite 03/04: no Setup Stream.
430	LiteBare { session: S, version: lite::Version },
431	/// moq-lite 05+: the client's Setup Stream has been read. `ok()` starts the
432	/// session, seeding the SETUP back so PROBE gating resolves.
433	LiteSetup {
434		session: S,
435		version: lite::Version,
436		client_setup: lite::Setup,
437	},
438	/// An IETF (or legacy bidi-SETUP) handshake, boxed where its
439	/// thread-affinity bounds held. The boxing is what keeps [`Handshake`] and
440	/// its lite path free of those bounds: the ietf machinery erases its
441	/// futures, which forces a per-target `Send` choice a pinned `!Send`
442	/// transport cannot satisfy, so the choice is made here, at construction,
443	/// where the caller proved the bounds.
444	Boxed(Box<dyn Paused<S>>),
445}
446
447type Accept<S> = crate::util::MaybeSendBox<'static, Result<(Session, crate::Driver<S>), Error>>;
448
449/// A paused non-lite handshake. See [`PausedHandshake::Boxed`] for why this is a
450/// trait object.
451///
452/// `MaybeSync` is not decoration: a caller holding a [`Handshake`] across an
453/// await behind `&self` (moq-relay authenticates that way) needs
454/// `&Handshake: Send`, which is `Handshake: Sync`, which is this.
455trait Paused<S: crate::transport::poll::Session>: MaybeSend + MaybeSync {
456	/// Complete the handshake with the final server config.
457	fn ok(self: Box<Self>, server: Server, runtime: Clock, peer_hop: Option<crate::Hop>) -> Accept<S>;
458
459	/// Reject the handshake, closing the transport with `err`'s wire code.
460	fn close(self: Box<Self>, err: Error);
461}
462
463/// Modern IETF (17/18): the client's SETUP (with its request path) has been
464/// read off its uni stream; `ok` starts the session, handing that stream back
465/// for GOAWAY monitoring.
466struct PausedIetfModern<S: crate::transport::poll::Session> {
467	session: S,
468	version: ietf::Version,
469	peer_setup: ietf::PeerSetup<S>,
470}
471
472impl<S> Paused<S> for PausedIetfModern<S>
473where
474	S: crate::transport::poll::Boxable,
475	S::SendStream: MaybeSync,
476	S::RecvStream: MaybeSync,
477{
478	fn ok(self: Box<Self>, server: Server, runtime: Clock, peer_hop: Option<crate::Hop>) -> Accept<S> {
479		use crate::util::MaybeBoxedExt as _;
480		async move {
481			let Self {
482				session,
483				version,
484				peer_setup,
485			} = *self;
486			let (publish, subscribe) = server.stat_tagged_origins();
487
488			// The client's SETUP was read at the pause; hand the stream back
489			// for GOAWAY. A server never advertises a path, hence `None`.
490			let (protocol, goaway) = ietf::start(ietf::Config {
491				runtime: runtime.clone(),
492				session: session.clone(),
493				setup: None,
494				request_id_max: None,
495				client: false,
496				publish,
497				subscribe,
498				peer_hop,
499				// Only the dialing side prices a link.
500				cost: None,
501				version,
502				path: None,
503				peer_setup_stream: Some(peer_setup.stream),
504				peer_declared: Some(peer_setup.declared),
505			})?;
506			tracing::debug!(?version, "connected");
507			Ok(Session::new(
508				runtime,
509				session,
510				version.into(),
511				None,
512				crate::driver::Protocol::Ietf(protocol),
513				goaway,
514			))
515		}
516		.maybe_boxed()
517	}
518
519	fn close(self: Box<Self>, err: Error) {
520		let mut session = self.session;
521		session.close(SessionError::from(&err).to_code(), &err.to_string());
522	}
523}
524
525/// Legacy IETF (draft 14-16) and lite 01/02: the client SETUP has been read
526/// off the bidi stream (including its request path) but the server SETUP
527/// hasn't been sent; `ok` finishes it.
528struct PausedLegacy<S: crate::transport::poll::Session> {
529	session: S,
530	stream: Stream<S, Version>,
531	version: Version,
532	request_id_max: Option<ietf::RequestId>,
533	/// What the client's SETUP declared, for the options `ok` acts on.
534	peer_declared: ietf::peer::Peer,
535}
536
537impl<S> Paused<S> for PausedLegacy<S>
538where
539	S: crate::transport::poll::Boxable,
540	S::SendStream: MaybeSync,
541	S::RecvStream: MaybeSync,
542{
543	fn ok(self: Box<Self>, server: Server, runtime: Clock, peer_hop: Option<crate::Hop>) -> Accept<S> {
544		use crate::util::MaybeBoxedExt as _;
545		async move {
546			let Self {
547				session,
548				mut stream,
549				version,
550				request_id_max,
551				peer_declared,
552			} = *self;
553			let (publish, subscribe) = server.stat_tagged_origins();
554
555			// Encode parameters using the version-appropriate type.
556			let parameters = match version {
557				Version::Ietf(v) => {
558					let mut parameters = ietf::Parameters::default();
559					parameters.set_varint(ietf::ParameterVarInt::MaxRequestId, u32::MAX as u64);
560					parameters.set_bytes(ietf::ParameterBytes::Implementation, b"moq-lite-rs".to_vec());
561					ietf::solicit::into_setup(&mut parameters, v);
562					ietf::hidden::into_setup(&mut parameters, v);
563					parameters.encode_bytes(v)?
564				}
565				Version::Lite(v) => lite::Parameters::default().encode_bytes(v)?,
566			};
567
568			let server_setup = setup::Server {
569				version: version.into(),
570				parameters,
571			};
572			stream.writer.encode(&server_setup).await?;
573
574			let (recv_bw, protocol, goaway) = match version {
575				Version::Lite(v) => {
576					let stream = stream.with_version(v);
577					// Pre-lite-05: no Setup Stream, so nothing to advertise or seed.
578					let start = lite::start(lite::Config {
579						runtime: runtime.clone(),
580						session: session.clone(),
581						setup_stream: Some(stream),
582						publish,
583						subscribe,
584						peer_hop,
585						version: v,
586						our_setup: lite::Setup::default(),
587						peer_setup: None,
588					})?;
589					(
590						start.recv_bandwidth,
591						crate::driver::Protocol::Lite(Box::new(start.driver)),
592						start.goaway,
593					)
594				}
595				Version::Ietf(v) => {
596					let stream = stream.with_version(v);
597					// Draft 14-16: path came in the bidi SETUP, no uni SETUP to hand back.
598					let (protocol, goaway) = ietf::start(ietf::Config {
599						runtime: runtime.clone(),
600						session: session.clone(),
601						setup: Some(stream),
602						request_id_max,
603						client: false,
604						publish,
605						subscribe,
606						peer_hop,
607						cost: None,
608						version: v,
609						path: None,
610						peer_setup_stream: None,
611						peer_declared: Some(peer_declared),
612					})?;
613					(None, crate::driver::Protocol::Ietf(protocol), goaway)
614				}
615			};
616
617			Ok(Session::new(runtime, session, version, recv_bw, protocol, goaway))
618		}
619		.maybe_boxed()
620	}
621
622	fn close(self: Box<Self>, err: Error) {
623		let mut session = self.session;
624		session.close(SessionError::from(&err).to_code(), &err.to_string());
625	}
626}
627
628impl<S> Handshake<S>
629where
630	S: crate::transport::poll::Session,
631{
632	/// The request path the client advertised in its SETUP.
633	///
634	/// Empty when the client advertised none: either it sent an empty path, or the
635	/// version carries none in-band (lite 01-04). Those mean the same thing, so the
636	/// wire distinction isn't surfaced. Populated for moq-lite-05 and newer,
637	/// and every moq-transport draft we speak. See the note on [`Server::accept_request`].
638	pub fn path(&self) -> &str {
639		self.path.as_deref().unwrap_or("")
640	}
641
642	/// The single [`Role`] the client advertised in its SETUP, or `None` for a
643	/// bidirectional session.
644	///
645	/// Only moq-lite-05 and newer carry a role, so `None` covers three cases
646	/// that the wire doesn't distinguish: an older version, a client that omitted the parameter, and a
647	/// client that explicitly advertised both directions. All three mean the same thing
648	/// (the client may publish and subscribe), so authorize on what the token grants.
649	/// See the note on [`Server::accept_request`].
650	pub fn role(&self) -> Option<Role> {
651		self.role
652	}
653
654	/// The Hop ID declared by the peer, when the negotiated protocol carries one.
655	///
656	/// A moq-lite-05+ endpoint declares this when it attaches a publish or subscribe
657	/// origin; a `moqt-17`+ endpoint declares it via the MoQ Cluster extension. Older
658	/// versions and endpoints without one return `None`.
659	///
660	/// Self-declared, so treat it as a correlation hint rather than an
661	/// authenticated identity: authorize on the token or client certificate.
662	pub fn peer_hop(&self) -> Option<crate::Hop> {
663		self.origin
664	}
665
666	/// Publish to the connected client. Overrides any value from the [`Server`]
667	/// builder; typically set after inspecting [`path`](Self::path).
668	pub fn with_publisher(mut self, publish: impl Consume<origin::Consumer>) -> Self {
669		self.inner_mut().server.publish = Some(publish.consume());
670		self
671	}
672
673	/// Subscribe to the connected client. Overrides any value from the [`Server`] builder.
674	pub fn with_subscriber(mut self, subscribe: origin::Producer) -> Self {
675		self.inner_mut().server.subscribe = Some(subscribe);
676		self
677	}
678
679	/// Assign the identity this peer's routes are attributed to, overriding the fresh
680	/// per-session default.
681	///
682	/// Only for a peer whose identity the server has actually established, such as one
683	/// authenticated by mTLS or a token ([`crate::Client::with_peer_hop`] is the
684	/// dialing-side equivalent). An identity the peer declares on the wire still wins.
685	///
686	/// Two sessions given the same origin are treated as one endpoint: routes learned
687	/// from either are kept off both, and content arriving on either is interchangeable
688	/// with the other's. That is the point when they really are one peer reconnecting or
689	/// running redundant links, and a bug otherwise. Derive it from the authenticated
690	/// identity, never from something coarser like the remote address.
691	pub fn with_peer_hop(mut self, hop: crate::Hop) -> Self {
692		self.assigned_hop = hop;
693		self
694	}
695
696	/// Set the per-connection [`stats::Session`] context. Overrides any value from the
697	/// [`Server`] builder.
698	pub fn with_stats(mut self, stats: stats::Session) -> Self {
699		self.inner_mut().server.stats = stats;
700		self
701	}
702
703	fn inner_mut(&mut self) -> &mut RequestInner<S> {
704		self.inner.as_mut().expect("request already responded")
705	}
706
707	/// Accept the session, returning the [`Session`] and its [`Driver`](crate::Driver).
708	///
709	/// Poll or spawn the returned driver to run the session.
710	pub async fn ok(mut self) -> Result<(Session, crate::Driver<S>), Error> {
711		let peer_hop = Some(self.assigned_hop);
712		let RequestInner {
713			server,
714			runtime,
715			handshake,
716		} = self.inner.take().expect("request already responded");
717
718		match handshake {
719			PausedHandshake::LiteBare { session, version } => {
720				server.start_lite(runtime, session, version, None, peer_hop)
721			}
722			PausedHandshake::LiteSetup {
723				session,
724				version,
725				client_setup,
726			} => server.start_lite(runtime, session, version, Some(client_setup), peer_hop),
727			PausedHandshake::Boxed(paused) => paused.ok(server, runtime, peer_hop).await,
728		}
729	}
730
731	/// Reject the session, closing the transport with `err`'s wire code.
732	pub fn close(mut self, err: Error) {
733		let inner = self.inner.take().expect("request already responded");
734		inner.close(err);
735	}
736}
737
738impl<S: crate::transport::poll::Session> RequestInner<S> {
739	fn close(self, err: Error) {
740		let mut session = match self.handshake {
741			PausedHandshake::LiteBare { session, .. } => session,
742			PausedHandshake::LiteSetup { session, .. } => session,
743			PausedHandshake::Boxed(paused) => return paused.close(err),
744		};
745		session.close(SessionError::from(&err).to_code(), &err.to_string());
746	}
747}
748
749impl<S: crate::transport::poll::Session> Drop for Handshake<S> {
750	// A dropped request would otherwise leave the client hanging until its idle
751	// timeout: it already sent SETUP and is waiting on a response. Reject loudly.
752	fn drop(&mut self) {
753		if let Some(inner) = self.inner.take() {
754			tracing::warn!("Handshake dropped without ok() or close(); rejecting the session");
755			inner.close(Error::Cancel);
756		}
757	}
758}
759
760#[cfg(test)]
761mod tests {
762	use super::*;
763	use crate::Hop;
764	use crate::model::ProduceTest;
765	use std::{
766		collections::VecDeque,
767		sync::{Arc, Mutex},
768	};
769
770	use crate::ALPN_LITE_05;
771	use bytes::Bytes;
772
773	fn occurrences(log: &crate::lite::test_transport::Log, needle: &[u8]) -> usize {
774		let writes = log.writes.lock().unwrap();
775		writes.windows(needle.len()).filter(|window| *window == needle).count()
776	}
777
778	#[derive(Debug, Clone, Default)]
779	struct FakeError;
780	impl std::fmt::Display for FakeError {
781		fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
782			write!(f, "fake transport error")
783		}
784	}
785	impl std::error::Error for FakeError {}
786	impl web_transport_trait::Error for FakeError {
787		fn session_error(&self) -> Option<(u32, String)> {
788			Some((0, "closed".to_string()))
789		}
790	}
791
792	/// A session that replays a queue of unidirectional streams (each a `Vec<u8>`) in
793	/// order from `accept_uni`; everything else is inert.
794	#[derive(Clone)]
795	struct FakeSession {
796		protocol: Option<&'static str>,
797		uni: Arc<Mutex<VecDeque<Vec<u8>>>>,
798	}
799
800	impl FakeSession {
801		fn new(protocol: &'static str, uni: impl IntoIterator<Item = Vec<u8>>) -> Self {
802			Self {
803				protocol: Some(protocol),
804				uni: Arc::new(Mutex::new(uni.into_iter().collect())),
805			}
806		}
807	}
808
809	impl web_transport_trait::poll::Session for FakeSession {
810		type SendStream = FakeSend;
811		type RecvStream = FakeRecv;
812		type Error = FakeError;
813
814		fn poll_accept_uni(
815			&mut self,
816			_cx: &mut std::task::Context<'_>,
817		) -> std::task::Poll<Result<Self::RecvStream, Self::Error>> {
818			match self.uni.lock().unwrap().pop_front() {
819				Some(data) => std::task::Poll::Ready(Ok(FakeRecv { data: data.into() })),
820				None => std::task::Poll::Pending,
821			}
822		}
823		fn poll_accept_bi(
824			&mut self,
825			_cx: &mut std::task::Context<'_>,
826		) -> std::task::Poll<Result<(Self::SendStream, Self::RecvStream), Self::Error>> {
827			std::task::Poll::Pending
828		}
829		fn poll_open_bi(
830			&mut self,
831			_cx: &mut std::task::Context<'_>,
832		) -> std::task::Poll<Result<(Self::SendStream, Self::RecvStream), Self::Error>> {
833			std::task::Poll::Pending
834		}
835		fn poll_open_uni(
836			&mut self,
837			_cx: &mut std::task::Context<'_>,
838		) -> std::task::Poll<Result<Self::SendStream, Self::Error>> {
839			std::task::Poll::Pending
840		}
841		fn poll_send_datagram(
842			&mut self,
843			_cx: &mut std::task::Context<'_>,
844			_payload: &[u8],
845		) -> std::task::Poll<Result<(), Self::Error>> {
846			std::task::Poll::Ready(Ok(()))
847		}
848		fn poll_recv_datagram(
849			&mut self,
850			_cx: &mut std::task::Context<'_>,
851		) -> std::task::Poll<Result<Bytes, Self::Error>> {
852			std::task::Poll::Pending
853		}
854		fn max_datagram_size(&self) -> usize {
855			1200
856		}
857		fn protocol(&self) -> Option<&str> {
858			self.protocol
859		}
860		fn close(&mut self, _code: u32, _reason: &str) {}
861		fn poll_closed(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Error> {
862			std::task::Poll::Pending
863		}
864		fn stats(&self) -> impl web_transport_trait::Stats {
865			web_transport_trait::StatsUnavailable
866		}
867	}
868
869	#[derive(Clone, Default)]
870	struct FakeSend;
871	impl web_transport_trait::poll::SendStream for FakeSend {
872		type Error = FakeError;
873		fn poll_write(
874			&mut self,
875			_cx: &mut std::task::Context<'_>,
876			buf: &[u8],
877		) -> std::task::Poll<Result<usize, Self::Error>> {
878			std::task::Poll::Ready(Ok(buf.len()))
879		}
880		fn set_priority(&mut self, _order: u8) {}
881		fn finish(&mut self) -> Result<(), Self::Error> {
882			Ok(())
883		}
884		fn reset(&mut self, _code: u32) {}
885		fn poll_closed(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
886			std::task::Poll::Ready(Ok(()))
887		}
888	}
889
890	struct FakeRecv {
891		data: VecDeque<u8>,
892	}
893	impl web_transport_trait::poll::RecvStream for FakeRecv {
894		type Error = FakeError;
895		fn poll_read(
896			&mut self,
897			_cx: &mut std::task::Context<'_>,
898			dst: &mut [u8],
899		) -> std::task::Poll<Result<Option<usize>, Self::Error>> {
900			if self.data.is_empty() {
901				return std::task::Poll::Ready(Ok(None));
902			}
903			let size = dst.len().min(self.data.len());
904			for slot in dst.iter_mut().take(size) {
905				*slot = self.data.pop_front().unwrap();
906			}
907			std::task::Poll::Ready(Ok(Some(size)))
908		}
909		fn stop(&mut self, _code: u32) {}
910		fn poll_closed(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
911			std::task::Poll::Ready(Ok(()))
912		}
913	}
914
915	/// Encode a lite-05 Setup Stream: the `DataType::Setup` tag then the SETUP message.
916	fn lite05_setup(path: Option<&str>, role: Option<Role>, hop: Option<Hop>) -> Vec<u8> {
917		let v = lite::Version::Lite05;
918		let mut buf = Vec::new();
919		lite::DataType::Setup.encode(&mut buf, v).unwrap();
920		lite::Setup {
921			probe: lite::ProbeLevel::None,
922			path: path.map(str::to_string),
923			role,
924			cost: None,
925			hop,
926		}
927		.encode(&mut buf, v)
928		.unwrap();
929		buf
930	}
931
932	/// Encode a draft-17+ Setup Stream: the unified SETUP message, whose parameters
933	/// carry the request path the same way lite-05's does.
934	fn ietf_setup(version: ietf::Version, path: Option<&str>) -> Vec<u8> {
935		let mut params = ietf::Parameters::default();
936		if let Some(path) = path {
937			params.set_bytes(ietf::ParameterBytes::Path, path.as_bytes().to_vec());
938		}
939		let parameters = params.encode_bytes(version).unwrap();
940
941		let mut buf = Vec::new();
942		setup::Setup { parameters }
943			.encode(&mut buf, crate::Version::Ietf(version))
944			.unwrap();
945		buf
946	}
947
948	#[tokio::test(start_paused = true)]
949	async fn accept_request_reads_ietf_path() {
950		// Every draft-17+ version gates on the SETUP stream before starting, so the
951		// path is known at authorization time just like lite-05.
952		for (alpn, version) in [
953			(ALPN_17, ietf::Version::Draft17),
954			(ALPN_18, ietf::Version::Draft18),
955			(ALPN_19, ietf::Version::Draft19),
956		] {
957			let session = FakeSession::new(alpn, [ietf_setup(version, Some("/team/room"))]);
958			let request = Server::new()
959				.accept_request(tokio::time::Instant::now().into_std(), session)
960				.await
961				.unwrap();
962			assert_eq!(request.path(), "/team/room", "{alpn}");
963		}
964	}
965
966	#[tokio::test(start_paused = true)]
967	async fn accept_request_ietf_without_path_is_empty() {
968		let session = FakeSession::new(ALPN_19, [ietf_setup(ietf::Version::Draft19, None)]);
969		let request = Server::new()
970			.accept_request(tokio::time::Instant::now().into_std(), session)
971			.await
972			.unwrap();
973		assert_eq!(request.path(), "");
974	}
975
976	#[tokio::test(start_paused = true)]
977	async fn accept_request_ietf_empty_path_is_accepted() {
978		let session = FakeSession::new(ALPN_19, [ietf_setup(ietf::Version::Draft19, Some(""))]);
979		let request = Server::new()
980			.accept_request(tokio::time::Instant::now().into_std(), session)
981			.await
982			.unwrap();
983		assert_eq!(request.path(), "");
984	}
985
986	/// Encode a lite-05 GROUP uni stream header (just the `DataType::Group` tag).
987	fn lite05_group() -> Vec<u8> {
988		let mut buf = Vec::new();
989		lite::DataType::Group.encode(&mut buf, lite::Version::Lite05).unwrap();
990		buf
991	}
992
993	#[tokio::test(start_paused = true)]
994	async fn accept_request_reads_lite05_path() {
995		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(Some("/team/room"), None, None)]);
996		let request = Server::new()
997			.accept_request(tokio::time::Instant::now().into_std(), session)
998			.await
999			.unwrap();
1000		assert_eq!(request.path(), "/team/room");
1001		assert_eq!(request.role(), None, "a client that omits the role is bidirectional");
1002	}
1003
1004	#[tokio::test(start_paused = true)]
1005	async fn accept_request_lite05_without_path_is_empty() {
1006		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(None, None, None)]);
1007		let request = Server::new()
1008			.accept_request(tokio::time::Instant::now().into_std(), session)
1009			.await
1010			.unwrap();
1011		assert_eq!(request.path(), "");
1012	}
1013
1014	#[tokio::test(start_paused = true)]
1015	async fn accept_request_lite05_empty_path_is_accepted() {
1016		// An empty path is valid on the wire and means the same as omitting it, so a
1017		// client that wants the root doesn't have to special-case the parameter.
1018		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(Some(""), None, None)]);
1019		let request = Server::new()
1020			.accept_request(tokio::time::Instant::now().into_std(), session)
1021			.await
1022			.unwrap();
1023		assert_eq!(request.path(), "");
1024	}
1025
1026	#[tokio::test(start_paused = true)]
1027	async fn accept_request_reads_lite05_role() {
1028		let session = FakeSession::new(
1029			ALPN_LITE_05,
1030			[lite05_setup(Some("/team/room"), Some(Role::Publisher), None)],
1031		);
1032		let request = Server::new()
1033			.accept_request(tokio::time::Instant::now().into_std(), session)
1034			.await
1035			.unwrap();
1036		assert_eq!(request.role(), Some(Role::Publisher));
1037	}
1038
1039	#[tokio::test(start_paused = true)]
1040	async fn accept_request_skips_uni_stream_before_setup() {
1041		// A GROUP racing ahead of the SETUP is STOP_SENDING-ed and skipped; the gate
1042		// keeps reading until it finds the SETUP.
1043		let session = FakeSession::new(
1044			ALPN_LITE_05,
1045			[lite05_group(), lite05_setup(Some("/team/room"), None, None)],
1046		);
1047		let request = Server::new()
1048			.accept_request(tokio::time::Instant::now().into_std(), session)
1049			.await
1050			.unwrap();
1051		assert_eq!(request.path(), "/team/room");
1052	}
1053
1054	#[tokio::test(start_paused = true)]
1055	async fn accept_request_reads_lite05_peer_hop() {
1056		let hop = Hop::new(42).unwrap();
1057		let session = FakeSession::new(ALPN_LITE_05, [lite05_setup(None, None, Some(hop))]);
1058		let request = Server::new()
1059			.accept_request(tokio::time::Instant::now().into_std(), session)
1060			.await
1061			.unwrap();
1062		assert_eq!(request.peer_hop(), Some(hop));
1063	}
1064
1065	#[tokio::test(start_paused = true)]
1066	async fn anonymous_peer_hop_filters_routes_from_server_session() {
1067		let other = Hop::new(778).unwrap();
1068		let origin = crate::origin::Config::new(Hop::new(1).unwrap()).produce();
1069
1070		let gate = kio::Producer::new(true);
1071		let transport = crate::lite::test_transport::SinkSession::gated_bi(gate.consume());
1072		let log = transport.log.clone();
1073		let version = ietf::Version::Draft18;
1074		let request = Handshake {
1075			path: None,
1076			role: None,
1077			origin: None,
1078			assigned_hop: Hop::random(),
1079			inner: Some(RequestInner {
1080				server: Server::new().with_publisher(&origin),
1081				runtime: Clock::new(tokio::time::Instant::now().into_std()),
1082				handshake: PausedHandshake::Boxed(Box::new(PausedIetfModern {
1083					session: transport,
1084					version,
1085					peer_setup: ietf::PeerSetup {
1086						stream: crate::coding::Reader::new(
1087							crate::lite::test_transport::PendingRecv,
1088							Version::Ietf(version),
1089						),
1090						path: None,
1091						declared: ietf::peer::Peer::default(),
1092					},
1093				})),
1094			}),
1095		};
1096		let assigned = request.assigned_hop;
1097
1098		let mut echoed_hops = crate::Hops::new();
1099		echoed_hops.push(crate::Hop::UNKNOWN).unwrap();
1100		let _echoed = origin
1101			.announce(
1102				"echoed-route",
1103				crate::origin::Route::default()
1104					.with_hops(echoed_hops)
1105					.with_via(assigned),
1106			)
1107			.unwrap();
1108
1109		let mut local_hops = crate::Hops::new();
1110		local_hops.push(other).unwrap();
1111		let _local = origin
1112			.announce("local-route", crate::origin::Route::default().with_hops(local_hops))
1113			.unwrap();
1114
1115		let (session, driver) = request.ok().await.unwrap();
1116		tokio::spawn(crate::time::run(driver));
1117
1118		for _ in 0..100 {
1119			if occurrences(&log, b"local-route") > 0 {
1120				break;
1121			}
1122			tokio::time::sleep(std::time::Duration::from_millis(1)).await;
1123		}
1124
1125		assert_eq!(occurrences(&log, b"echoed-route"), 0);
1126		assert_eq!(occurrences(&log, b"local-route"), 1);
1127		drop(session);
1128	}
1129}