Skip to main content

moq_uring/quic/
web.rs

1//! WebTransport over the worker's QUIC path, for browser peers.
2//!
3//! A browser cannot speak raw QUIC: it dials with ALPN `h3` and expects an
4//! HTTP/3 CONNECT handshake before streams flow, with every stream and
5//! datagram prefixed to name the session. [`Request::accept`] runs that
6//! handshake over a [`Connection`] (settings exchange, CONNECT, subprotocol
7//! selection) using [`web_transport_proto`]'s state machines, and
8//! [`Request::respond`] yields a [`Session`].
9//!
10//! [`Session`] supports both raw QUIC and WebTransport on the worker:
11//! [`Session::raw`] wraps a raw-QUIC connection in the same type with the
12//! WebTransport layering disabled, so native peers and browsers run the same
13//! machinery. Stream and session error codes map through the HTTP/3 error
14//! space ([`web_transport_proto::error_to_http3`]) in web mode and pass
15//! through untouched in raw mode.
16
17use std::cell::RefCell;
18use std::collections::VecDeque;
19use std::rc::Rc;
20use std::task::{Context, Poll, ready};
21
22use bytes::{Buf, Bytes, BytesMut};
23use web_transport_proto as proto;
24
25use super::{Connection, Error};
26
27/// The frame type WebTransport bidirectional streams lead with.
28const FRAME_WEBTRANSPORT: u64 = 0x41;
29/// The stream type WebTransport unidirectional streams lead with.
30const STREAM_WEBTRANSPORT: u64 = 0x54;
31/// H3 unidirectional stream types a peer legitimately opens and we must keep
32/// alive without reading: the control stream and the QPACK pair.
33const STREAM_CONTROL: u64 = 0x00;
34const STREAM_QPACK_ENCODER: u64 = 0x02;
35const STREAM_QPACK_DECODER: u64 = 0x03;
36/// The HTTP/3 DATA frame carrying capsules on the CONNECT stream.
37const FRAME_DATA: u64 = 0x00;
38/// How long a graceful close waits for the peer to act on the
39/// `CloseWebTransportSession` capsule before closing the connection itself.
40const CLOSE_GRACE: std::time::Duration = std::time::Duration::from_secs(1);
41/// How much of one HTTP/3 message to buffer while waiting for the rest.
42///
43/// A peer declares a frame's length before sending its body, and reading what
44/// does arrive replenishes its flow-control credit, so an unauthenticated
45/// client could otherwise announce an enormous SETTINGS, CONNECT, or capsule
46/// frame and dribble it until the process runs out of memory. Everything
47/// parsed here is a few hundred bytes; no real peer meets this.
48const MESSAGE_LIMIT: usize = 64 * 1024;
49/// How many unidirectional streams a peer may open before its control stream
50/// arrives.
51///
52/// The handshake holds each arrival (QPACK, the control stream) or parks it as
53/// an early WebTransport stream, so without a cap a peer that never sends a
54/// control stream could open them without end. A real client opens three
55/// before its CONNECT, plus whatever it pipelines.
56const HANDSHAKE_STREAMS: usize = 64;
57/// How many of a peer's critical streams to hold open once the session is
58/// running. HTTP/3 defines one control stream and two QPACK streams; the
59/// slack is for a peer that greases, and anything past it is dropped rather
60/// than retained.
61const HELD_STREAMS: usize = 8;
62/// `H3_GENERAL_PROTOCOL_ERROR`, for closing a connection whose HTTP/3
63/// handshake never produced a session.
64const H3_GENERAL_PROTOCOL_ERROR: u64 = 0x0101;
65/// `H3_NO_ERROR`, for closing a connection that did what it came to do: a
66/// rejection the peer has been told about.
67const H3_NO_ERROR: u64 = 0x0100;
68
69/// An incoming WebTransport handshake: the CONNECT request, ready to answer.
70///
71/// Produced by [`accept`](Self::accept) on a connection whose ALPN negotiated
72/// `h3`. Answer with [`respond`](Self::respond) (or [`ok`](Self::ok)) to get
73/// the [`Session`], or [`reject`](Self::reject) to refuse it.
74pub struct Request {
75	conn: Connection,
76	/// Closes the connection on every way out of here that is not an answer.
77	guard: Guard,
78	request: proto::ConnectRequest,
79	send: super::SendStream,
80	recv: super::RecvStream,
81	/// The peer's control and QPACK streams, held open for the session's
82	/// life: closing them reads as tearing the H3 connection down.
83	held: Vec<super::RecvStream>,
84	/// Our control stream, same deal.
85	control: super::SendStream,
86	/// WebTransport streams a pipelining peer opened before its CONNECT was
87	/// answered, headers consumed, keyed by the session id they claimed.
88	early: Vec<(u64, super::RecvStream)>,
89	/// Streams that arrived during the handshake and are still mid-header;
90	/// the session goes on classifying them.
91	pending: Vec<PendingUni>,
92}
93
94/// Closes a connection whose handshake was abandoned, disarmed once the peer
95/// has an answer.
96///
97/// Dropping a [`Connection`] does not close QUIC: the endpoint keeps it, its
98/// routes, and its driver task until the driver sees a terminal state, and the
99/// backlog stopped counting it at accept. The peer picks the path it asks for
100/// and the subprotocols it offers, so it decides which of [`Request::respond`]'s
101/// early returns the server takes; a guard covers every way out rather than
102/// each one remembering.
103struct Guard {
104	conn: Option<Connection>,
105}
106
107impl Guard {
108	fn new(conn: Connection) -> Self {
109		Self { conn: Some(conn) }
110	}
111
112	/// The peer got an answer, so the connection is the answer's to close.
113	fn disarm(&mut self) {
114		self.conn = None;
115	}
116
117	fn close(&mut self, reason: &str) {
118		if let Some(conn) = self.conn.take() {
119			conn.close_code(H3_GENERAL_PROTOCOL_ERROR, reason);
120		}
121	}
122}
123
124impl Drop for Guard {
125	fn drop(&mut self) {
126		self.close("webtransport handshake abandoned");
127	}
128}
129
130impl Request {
131	/// Run the server side of the HTTP/3 handshake: exchange SETTINGS, then
132	/// take the CONNECT request.
133	///
134	/// Everything the session later spawns runs on the worker already driving
135	/// `conn`. There is no timeout here; a peer that stalls mid-handshake is
136	/// bounded by the connection's idle timeout.
137	pub async fn accept(conn: Connection) -> Result<Self, Error> {
138		// The endpoint and driver retain this connection after the future is
139		// dropped. Own its close before the first await; the returned Request
140		// takes over that duty once the handshake succeeds.
141		let mut guard = Guard::new(conn.clone());
142		match Self::handshake(conn).await {
143			Ok(request) => {
144				guard.disarm();
145				Ok(request)
146			}
147			Err(err) => {
148				guard.close(&err.to_string());
149				Err(err)
150			}
151		}
152	}
153
154	async fn handshake(mut conn: Connection) -> Result<Self, Error> {
155		// Our control stream: the SETTINGS advertising WebTransport support.
156		let mut control = open_uni(&mut conn).await?;
157		let mut settings = proto::Settings::default();
158		settings.enable_webtransport(1);
159		let mut buf = Vec::new();
160		settings.encode(&mut buf);
161		write_all(&mut control, &buf).await?;
162
163		// The peer's control stream carries its SETTINGS, but its QPACK
164		// streams race it, and an eager client's WebTransport streams can
165		// arrive ahead of everything, so classify every arrival at once.
166		// Taking them one at a time would let a stream that sends its type
167		// byte and then stalls hold off a control stream that has already
168		// fully arrived, for as long as the peer keeps the connection alive.
169		let mut pending: Vec<PendingUni> = Vec::new();
170		let mut held = Vec::new();
171		let mut early = Vec::new();
172		// Every arrival counts, not just the ones kept: a stream of unknown
173		// type is dropped here, and a dropped stream returns its credit, so
174		// counting what we retain would let a peer loop this forever while
175		// never sending a control stream.
176		let mut arrivals = 0usize;
177		let mut peer_control = None;
178		std::future::poll_fn(|cx| {
179			loop {
180				// Stop adopting past the cap, but do not give up on what is
181				// already here: the control stream may be sitting at the head
182				// of a queue a pipelining peer filled behind it, and refusing
183				// that peer is the bug the cap is not for. The one that tips
184				// it over is kept rather than dropped, since dropping it would
185				// cancel a legitimate stream on a handshake that then succeeds.
186				let mut over = false;
187				while !over {
188					match web_transport_trait::poll::Session::poll_accept_uni(&mut conn, cx) {
189						Poll::Ready(Ok(recv)) => {
190							arrivals += 1;
191							pending.push(PendingUni::new(recv));
192							over = arrivals > HANDSHAKE_STREAMS;
193						}
194						Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),
195						Poll::Pending => break,
196					}
197				}
198
199				// Each mid-header stream parks the caller in its own stream's
200				// waiters, so progress on any of them re-polls us.
201				let mut progressed = false;
202				let mut index = 0;
203				while index < pending.len() {
204					let Poll::Ready(class) = pending[index].poll_classify(cx) else {
205						index += 1;
206						continue;
207					};
208					// Whatever was last takes this slot, so it is polled by
209					// this same pass rather than waited for.
210					let stream = pending.swap_remove(index);
211					progressed = true;
212					match class {
213						UniClass::Control => {
214							peer_control = Some(stream.recv);
215							return Poll::Ready(Ok(()));
216						}
217						UniClass::Qpack => held.push(stream.recv),
218						UniClass::Web(session) => early.push((session, stream.recv)),
219						UniClass::Unknown => {}
220					}
221				}
222
223				// Only now, with everything adopted classified: the peer sent
224				// this many streams and none of them was a control stream.
225				if over {
226					return Poll::Ready(Err(Error::Web("too many streams before the control stream".into())));
227				}
228				if !progressed {
229					return Poll::Pending;
230				}
231			}
232		})
233		.await?;
234
235		let mut peer_control = peer_control.expect("the loop only ends with a control stream");
236		let settings = read_settings(&mut peer_control).await?;
237		if settings.supports_webtransport() == 0 {
238			return Err(Error::Web("peer does not support WebTransport".into()));
239		}
240		held.push(peer_control);
241
242		// The CONNECT request rides the client's first bidirectional stream,
243		// which arrives first: QUIC creates lower-numbered streams
244		// implicitly, and the accept queue is in id order.
245		let (send, mut recv) = accept_bi(&mut conn).await?;
246		let request = read_connect(&mut recv).await?;
247
248		Ok(Self {
249			guard: Guard::new(conn.clone()),
250			conn,
251			request,
252			send,
253			recv,
254			held,
255			control,
256			early,
257			pending,
258		})
259	}
260
261	/// The URL the peer connected to; the path and query are what a server
262	/// routes and authenticates on. The `url` crate is
263	/// [re-exported](crate::url) so naming this type needs no dependency of
264	/// your own.
265	pub fn url(&self) -> &url::Url {
266		&self.request.url
267	}
268
269	/// The subprotocols the peer offered, the WebTransport equivalent of ALPN.
270	/// Pick one and name it in [`respond`](Self::respond).
271	pub fn protocols(&self) -> &[String] {
272		&self.request.protocols
273	}
274
275	/// Accept with a `200`, answering as `response` describes.
276	pub async fn respond(mut self, response: Response) -> Result<Session, Error> {
277		let Response { protocol } = response;
278
279		let mut encoded = proto::ConnectResponse::OK;
280		if let Some(protocol) = &protocol {
281			if !self.request.protocols.iter().any(|offered| offered == protocol) {
282				return Err(Error::Web(format!("subprotocol {protocol:?} was not offered")));
283			}
284			encoded = encoded.with_protocol(protocol);
285		}
286		let mut buf = Vec::new();
287		encoded.encode(&mut buf).map_err(|err| Error::Web(err.to_string()))?;
288		write_all(&mut self.send, &buf).await?;
289		self.guard.disarm();
290
291		Ok(Session::establish(self, protocol))
292	}
293
294	/// Accept with a `200` and no subprotocol.
295	pub async fn ok(self) -> Result<Session, Error> {
296		self.respond(Response::default()).await
297	}
298
299	/// Refuse with `reason`, ending the handshake.
300	///
301	/// Returns once the peer has the response, or after a grace period if it
302	/// never acknowledges one, and closes the connection deliberately. The
303	/// HTTP/3 critical streams (the peer's control and QPACK streams, and
304	/// ours) stay open until then: RFC 9114 makes closing one a connection
305	/// error, so tearing them down here would show the peer an H3 failure
306	/// instead of the status it was sent.
307	pub async fn reject(mut self, reason: Rejected) -> Result<(), Error> {
308		let response = proto::ConnectResponse::new(reason.status());
309		let mut buf = Vec::new();
310		response.encode(&mut buf).map_err(|err| Error::Web(err.to_string()))?;
311		write_all(&mut self.send, &buf).await?;
312		web_transport_trait::poll::SendStream::finish(&mut self.send)?;
313
314		// The guard stays armed across the wait below. Cancelling this future
315		// mid-grace would otherwise skip the deliberate close and leak the
316		// connection, which is the very thing the guard is here to prevent.
317		let mut deadline = self.conn.owner().after(CLOSE_GRACE);
318		let send = &mut self.send;
319		kio::wait(|waiter| {
320			let mut cx = Context::from_waker(waiter.waker());
321			if web_transport_trait::poll::SendStream::poll_closed(send, &mut cx).is_ready() {
322				return Poll::Ready(());
323			}
324			deadline.poll(waiter)
325		})
326		.await;
327
328		// The peer has the response, so this close is the deliberate one; the
329		// guard's abrupt `H3_GENERAL_PROTOCOL_ERROR` would have raced it out.
330		self.guard.disarm();
331		self.conn.close_code(H3_NO_ERROR, "");
332		Ok(())
333	}
334}
335
336/// How to answer a CONNECT the server is accepting.
337///
338/// Built with [`default`](Self::default) and the setters below, so a knob
339/// added later stays additive.
340#[derive(Clone, Debug, Default)]
341pub struct Response {
342	protocol: Option<String>,
343}
344
345impl Response {
346	/// Select a subprotocol from the ones the peer
347	/// [offered](Request::protocols), the WebTransport equivalent of ALPN.
348	/// Answering with one the peer did not offer is an error.
349	pub fn with_protocol(mut self, protocol: impl Into<String>) -> Self {
350		self.protocol = Some(protocol.into());
351		self
352	}
353}
354
355/// Why a CONNECT is being refused.
356///
357/// Named rather than numeric so callers need no HTTP crate of their own, and
358/// so this stays a contract of moq-uring's rather than of whichever `http`
359/// version it happens to build against.
360#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361#[non_exhaustive]
362pub enum Rejected {
363	/// The peer is not allowed here (`401`).
364	Unauthorized,
365	/// The peer is known and still not allowed here (`403`).
366	Forbidden,
367	/// Nothing is served at the requested path (`404`).
368	NotFound,
369	/// The request was malformed (`400`).
370	BadRequest,
371	/// The server cannot take it right now (`503`).
372	Unavailable,
373}
374
375impl Rejected {
376	fn status(self) -> http::StatusCode {
377		match self {
378			Self::Unauthorized => http::StatusCode::UNAUTHORIZED,
379			Self::Forbidden => http::StatusCode::FORBIDDEN,
380			Self::NotFound => http::StatusCode::NOT_FOUND,
381			Self::BadRequest => http::StatusCode::BAD_REQUEST,
382			Self::Unavailable => http::StatusCode::SERVICE_UNAVAILABLE,
383		}
384	}
385}
386
387impl std::fmt::Debug for Request {
388	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389		f.debug_struct("Request").field("url", &self.request.url).finish()
390	}
391}
392
393/// The WebTransport layering shared by a session's clones.
394struct Web {
395	/// The CONNECT stream's id: what every stream header and datagram carries.
396	session_id: u64,
397	/// Precomputed per-kind prefixes carrying the session id.
398	header_uni: Bytes,
399	header_bi: Bytes,
400	header_datagram: Bytes,
401	state: RefCell<State>,
402	/// The session's terminal error, in WebTransport code space: a received
403	/// `CloseWebTransportSession` capsule, or the local [`Session::close`].
404	/// First writer wins.
405	closed: RefCell<Option<Error>>,
406}
407
408struct State {
409	/// Incoming streams whose headers are still being read.
410	pending_uni: Vec<PendingUni>,
411	pending_bi: Vec<PendingBi>,
412	/// Classified application streams nobody has accepted yet.
413	ready_uni: VecDeque<super::RecvStream>,
414	ready_bi: VecDeque<(super::SendStream, super::RecvStream)>,
415	/// Streams held open, never read: control and QPACK, ours and theirs.
416	held_recv: Vec<super::RecvStream>,
417	_control_send: Option<super::SendStream>,
418	/// The CONNECT stream's send half; taken exactly once by the close path.
419	connect_send: Option<super::SendStream>,
420}
421
422/// A MoQ transport over the worker: raw QUIC, or a WebTransport session.
423///
424/// [`Request::respond`] builds the web
425/// flavor; [`Session::raw`] wraps a raw-QUIC [`Connection`] with the layering
426/// disabled. Clones share the session.
427pub struct Session {
428	conn: Connection,
429	web: Option<Rc<Web>>,
430	/// What [`protocol`](web_transport_trait::poll::Session::protocol)
431	/// reports: the selected subprotocol in web mode, the ALPN in raw mode.
432	protocol: Option<String>,
433}
434
435impl Session {
436	/// Wrap a raw-QUIC connection in a session, with no
437	/// WebTransport layering: streams and datagrams pass through untouched.
438	pub fn raw(conn: Connection) -> Self {
439		let protocol = web_transport_trait::poll::Session::protocol(&conn).map(str::to_owned);
440		Self {
441			conn,
442			web: None,
443			protocol,
444		}
445	}
446
447	/// Assemble the web flavor and spawn its capsule reader.
448	fn establish(request: Request, protocol: Option<String>) -> Self {
449		let Request {
450			conn,
451			guard: _,
452			request: _,
453			send,
454			recv,
455			held,
456			control,
457			early,
458			pending,
459		} = request;
460
461		let session_id = send.id();
462		let mut header_uni = Vec::new();
463		encode_varint(STREAM_WEBTRANSPORT, &mut header_uni);
464		encode_varint(session_id, &mut header_uni);
465		let mut header_bi = Vec::new();
466		encode_varint(FRAME_WEBTRANSPORT, &mut header_bi);
467		encode_varint(session_id, &mut header_bi);
468		let mut header_datagram = Vec::new();
469		encode_varint(session_id, &mut header_datagram);
470
471		// A pipelining peer's streams claimed a session id before this one
472		// existed; only its own id could have been meant.
473		let ready_uni = early
474			.into_iter()
475			.filter_map(|(session, recv)| (session == session_id).then_some(recv))
476			.collect();
477
478		let web = Rc::new(Web {
479			session_id,
480			header_uni: header_uni.into(),
481			header_bi: header_bi.into(),
482			header_datagram: header_datagram.into(),
483			state: RefCell::new(State {
484				// Still mid-header when the control stream turned up; the
485				// session finishes classifying them, so a pipelined
486				// WebTransport stream is not lost to the handshake ending.
487				pending_uni: pending,
488				pending_bi: Vec::new(),
489				ready_uni,
490				ready_bi: VecDeque::new(),
491				held_recv: held,
492				_control_send: Some(control),
493				connect_send: Some(send),
494			}),
495			closed: RefCell::new(None),
496		});
497
498		// The peer signals session close with a capsule on the CONNECT
499		// stream; read it so the close code survives the H3 mapping.
500		let capsules = web.clone();
501		let capsule_conn = conn.clone();
502		conn.owner()
503			.spawn(async move { read_capsules(capsules, capsule_conn, recv).await });
504
505		Self {
506			conn,
507			web: Some(web),
508			protocol,
509		}
510	}
511
512	/// Rewrite error codes out of the HTTP/3 mapping in web mode.
513	fn map_err(&self, err: Error) -> Error {
514		match self.web {
515			Some(_) => unmap_err(err),
516			None => err,
517		}
518	}
519}
520
521impl Clone for Session {
522	fn clone(&self) -> Self {
523		Self {
524			conn: self.conn.clone(),
525			web: self.web.clone(),
526			protocol: self.protocol.clone(),
527		}
528	}
529}
530
531impl std::fmt::Debug for Session {
532	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
533		f.debug_struct("Session")
534			.field("web", &self.web.is_some())
535			.field("protocol", &self.protocol)
536			.finish()
537	}
538}
539
540impl web_transport_trait::poll::Session for Session {
541	type SendStream = SendStream;
542	type RecvStream = RecvStream;
543	type Error = Error;
544
545	fn poll_accept_uni(&mut self, cx: &mut Context<'_>) -> Poll<Result<Self::RecvStream, Self::Error>> {
546		let Some(web) = self.web.clone() else {
547			let inner = ready!(web_transport_trait::poll::Session::poll_accept_uni(&mut self.conn, cx))?;
548			return Poll::Ready(Ok(RecvStream { inner, web: false }));
549		};
550
551		loop {
552			if let Some(inner) = web.state.borrow_mut().ready_uni.pop_front() {
553				return Poll::Ready(Ok(RecvStream { inner, web: true }));
554			}
555
556			// Adopt every new arrival, then try to classify the pending set.
557			// Each mid-header stream parks the caller in its own stream's
558			// waiters, so progress on any of them re-polls us.
559			loop {
560				match web_transport_trait::poll::Session::poll_accept_uni(&mut self.conn, cx) {
561					Poll::Ready(Ok(recv)) => web.state.borrow_mut().pending_uni.push(PendingUni::new(recv)),
562					Poll::Ready(Err(err)) => return Poll::Ready(Err(unmap_err(err))),
563					Poll::Pending => break,
564				}
565			}
566
567			if !classify_uni(&web, cx) {
568				return Poll::Pending;
569			}
570		}
571	}
572
573	fn poll_accept_bi(
574		&mut self,
575		cx: &mut Context<'_>,
576	) -> Poll<Result<(Self::SendStream, Self::RecvStream), Self::Error>> {
577		let Some(web) = self.web.clone() else {
578			let (send, recv) = ready!(web_transport_trait::poll::Session::poll_accept_bi(&mut self.conn, cx))?;
579			return Poll::Ready(Ok((
580				SendStream {
581					inner: send,
582					prefix: Bytes::new(),
583					finishing: false,
584					web: false,
585				},
586				RecvStream {
587					inner: recv,
588					web: false,
589				},
590			)));
591		};
592
593		loop {
594			if let Some((send, recv)) = web.state.borrow_mut().ready_bi.pop_front() {
595				return Poll::Ready(Ok((
596					SendStream {
597						inner: send,
598						prefix: Bytes::new(),
599						finishing: false,
600						web: true,
601					},
602					RecvStream { inner: recv, web: true },
603				)));
604			}
605
606			loop {
607				match web_transport_trait::poll::Session::poll_accept_bi(&mut self.conn, cx) {
608					Poll::Ready(Ok((send, recv))) => web.state.borrow_mut().pending_bi.push(PendingBi::new(send, recv)),
609					Poll::Ready(Err(err)) => return Poll::Ready(Err(unmap_err(err))),
610					Poll::Pending => break,
611				}
612			}
613
614			if !classify_bi(&web, cx) {
615				return Poll::Pending;
616			}
617		}
618	}
619
620	fn poll_open_uni(&mut self, cx: &mut Context<'_>) -> Poll<Result<Self::SendStream, Self::Error>> {
621		let inner = ready!(web_transport_trait::poll::Session::poll_open_uni(&mut self.conn, cx))
622			.map_err(|err| self.map_err(err))?;
623		let prefix = self.web.as_ref().map(|web| web.header_uni.clone()).unwrap_or_default();
624		Poll::Ready(Ok(SendStream {
625			inner,
626			prefix,
627			finishing: false,
628			web: self.web.is_some(),
629		}))
630	}
631
632	fn poll_open_bi(
633		&mut self,
634		cx: &mut Context<'_>,
635	) -> Poll<Result<(Self::SendStream, Self::RecvStream), Self::Error>> {
636		let (send, recv) = ready!(web_transport_trait::poll::Session::poll_open_bi(&mut self.conn, cx))
637			.map_err(|err| self.map_err(err))?;
638		let prefix = self.web.as_ref().map(|web| web.header_bi.clone()).unwrap_or_default();
639		Poll::Ready(Ok((
640			SendStream {
641				inner: send,
642				prefix,
643				finishing: false,
644				web: self.web.is_some(),
645			},
646			RecvStream {
647				inner: recv,
648				web: self.web.is_some(),
649			},
650		)))
651	}
652
653	fn poll_send_datagram(&mut self, cx: &mut Context<'_>, payload: &[u8]) -> Poll<Result<(), Self::Error>> {
654		let Some(web) = &self.web else {
655			return web_transport_trait::poll::Session::poll_send_datagram(&mut self.conn, cx, payload);
656		};
657		let mut framed = Vec::with_capacity(web.header_datagram.len() + payload.len());
658		framed.extend_from_slice(&web.header_datagram);
659		framed.extend_from_slice(payload);
660		web_transport_trait::poll::Session::poll_send_datagram(&mut self.conn, cx, &framed).map_err(unmap_err)
661	}
662
663	fn poll_recv_datagram(&mut self, cx: &mut Context<'_>) -> Poll<Result<Bytes, Self::Error>> {
664		let Some(web) = self.web.clone() else {
665			return web_transport_trait::poll::Session::poll_recv_datagram(&mut self.conn, cx);
666		};
667		loop {
668			let datagram = ready!(web_transport_trait::poll::Session::poll_recv_datagram(
669				&mut self.conn,
670				cx
671			))
672			.map_err(unmap_err)?;
673			// The prefix names the session; anything else is not ours.
674			let mut peek: &[u8] = &datagram;
675			match decode_varint(&mut peek) {
676				Some(id) if id == web.session_id => {
677					let start = datagram.len() - peek.len();
678					return Poll::Ready(Ok(datagram.slice(start..)));
679				}
680				_ => tracing::debug!("dropping a datagram for an unknown session"),
681			}
682		}
683	}
684
685	fn max_datagram_size(&self) -> usize {
686		let inner = web_transport_trait::poll::Session::max_datagram_size(&self.conn);
687		match &self.web {
688			Some(web) => inner.saturating_sub(web.header_datagram.len()),
689			None => inner,
690		}
691	}
692
693	fn protocol(&self) -> Option<&str> {
694		self.protocol.as_deref()
695	}
696
697	fn close(&mut self, code: u32, reason: &str) {
698		let Some(web) = &self.web else {
699			return web_transport_trait::poll::Session::close(&mut self.conn, code, reason);
700		};
701
702		{
703			let mut closed = web.closed.borrow_mut();
704			if closed.is_some() {
705				return;
706			}
707			*closed = Some(Error::App {
708				code: u64::from(code),
709				reason: reason.to_string(),
710			});
711		}
712
713		let connect_send = web.state.borrow_mut().connect_send.take();
714		let http3 = proto::error_to_http3(code);
715		let Some(mut send) = connect_send else {
716			self.conn.close_code(http3, reason);
717			return;
718		};
719
720		// The capsule is what carries the code and reason to a browser
721		// (`WebTransport.closed`); the connection close alone would lose the
722		// reason and squash the code through the H3 mapping.
723		let capsule = proto::Capsule::CloseWebTransportSession {
724			code,
725			reason: reason.to_string(),
726		};
727		let mut payload = Vec::new();
728		capsule.encode(&mut payload);
729		let mut frame = Vec::new();
730		encode_varint(FRAME_DATA, &mut frame);
731		encode_varint(payload.len() as u64, &mut frame);
732		frame.extend_from_slice(&payload);
733
734		// Finish the capsule and then give the peer a moment to act on it,
735		// closing either way once the grace period is up. The write goes in
736		// the task rather than here because flow control can take it in
737		// pieces, and abandoning a partial frame would leave the browser with
738		// neither the code nor the reason.
739		let mut deadline = self.conn.owner().after(CLOSE_GRACE);
740		let reason = reason.to_string();
741		let mut conn = self.conn.clone();
742		self.conn.owner().spawn(async move {
743			let mut offset = 0;
744			kio::wait(|waiter| {
745				let mut cx = Context::from_waker(waiter.waker());
746
747				// Stop writing once the connection is gone; the deadline is the
748				// only other thing that ends this.
749				if web_transport_trait::poll::Session::poll_closed(&mut conn, &mut cx).is_ready() {
750					return Poll::Ready(());
751				}
752				while offset < frame.len() {
753					match web_transport_trait::poll::SendStream::poll_write(&mut send, &mut cx, &frame[offset..]) {
754						Poll::Ready(Ok(n)) => offset += n,
755						// The stream is unusable, so the connection close below
756						// is all the peer is going to get.
757						Poll::Ready(Err(_)) => return Poll::Ready(()),
758						Poll::Pending => break,
759					}
760					if offset == frame.len() {
761						let _ = web_transport_trait::poll::SendStream::finish(&mut send);
762					}
763				}
764
765				deadline.poll(waiter)
766			})
767			.await;
768			conn.close_code(http3, &reason);
769		});
770	}
771
772	fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll<Self::Error> {
773		let err = ready!(web_transport_trait::poll::Session::poll_closed(&mut self.conn, cx));
774		let Some(web) = &self.web else {
775			return Poll::Ready(err);
776		};
777		// A recorded close (capsule or local) is the truth; the connection
778		// error is just its H3-mangled echo.
779		if let Some(recorded) = web.closed.borrow().clone() {
780			return Poll::Ready(recorded);
781		}
782		Poll::Ready(unmap_err(err))
783	}
784
785	fn stats(&self) -> impl web_transport_trait::Stats {
786		web_transport_trait::poll::Session::stats(&self.conn)
787	}
788}
789
790/// An outgoing stream, WebTransport-framed when its session is.
791///
792/// [`finish`](web_transport_trait::poll::SendStream::finish) can leave the
793/// WebTransport header owed, when the connection has no flow-control credit
794/// for it. The FIN then goes out on a later
795/// [`poll_closed`](web_transport_trait::poll::SendStream::poll_closed), or on
796/// `Drop` if credit has returned by then. Dropping immediately after
797/// finishing, with no poll in between, leaves no moment for it to return and
798/// cancels the stream instead, so pair the two when a clean end matters.
799pub struct SendStream {
800	inner: super::SendStream,
801	/// Header bytes still owed to the wire before any payload.
802	prefix: Bytes,
803	/// [`finish`](web_transport_trait::poll::SendStream::finish) ran with the
804	/// header still owed, so [`poll_closed`](web_transport_trait::poll::SendStream::poll_closed)
805	/// writes the rest and finishes then.
806	finishing: bool,
807	web: bool,
808}
809
810impl SendStream {
811	fn map(&self, err: Error) -> Error {
812		match self.web {
813			true => unmap_err(err),
814			false => err,
815		}
816	}
817}
818
819impl web_transport_trait::poll::SendStream for SendStream {
820	type Error = Error;
821
822	fn poll_write(&mut self, cx: &mut Context<'_>, buf: &[u8]) -> Poll<Result<usize, Self::Error>> {
823		// The FIN is owed the moment the header lands, so there is no room
824		// left for a payload; the inner stream refuses a post-FIN write the
825		// same way.
826		if self.finishing {
827			return Poll::Ready(Err(Error::Quic("stream already finished".to_string())));
828		}
829		while !self.prefix.is_empty() {
830			let n = ready!(web_transport_trait::poll::SendStream::poll_write(
831				&mut self.inner,
832				cx,
833				&self.prefix
834			))
835			.map_err(|err| self.map(err))?;
836			self.prefix.advance(n);
837		}
838		match web_transport_trait::poll::SendStream::poll_write(&mut self.inner, cx, buf) {
839			Poll::Ready(Err(err)) => Poll::Ready(Err(self.map(err))),
840			other => other,
841		}
842	}
843
844	fn poll_write_buf<B: Buf>(&mut self, cx: &mut Context<'_>, buf: &mut B) -> Poll<Result<usize, Self::Error>> {
845		if self.finishing {
846			return Poll::Ready(Err(Error::Quic("stream already finished".to_string())));
847		}
848		while !self.prefix.is_empty() {
849			ready!(web_transport_trait::poll::SendStream::poll_write_buf(
850				&mut self.inner,
851				cx,
852				&mut self.prefix
853			))
854			.map_err(|err| self.map(err))?;
855		}
856		match web_transport_trait::poll::SendStream::poll_write_buf(&mut self.inner, cx, buf) {
857			Poll::Ready(Err(err)) => Poll::Ready(Err(self.map(err))),
858			other => other,
859		}
860	}
861
862	fn set_priority(&mut self, order: u8) {
863		web_transport_trait::poll::SendStream::set_priority(&mut self.inner, order);
864	}
865
866	fn finish(&mut self) -> Result<(), Self::Error> {
867		// A stream finished before any write still owes its header, or the
868		// peer sees an unframed (and thus invalid) stream.
869		if !self.prefix.is_empty() {
870			let n = self.inner.try_write(&self.prefix);
871			self.prefix.advance(n);
872			if !self.prefix.is_empty() {
873				// Zero capacity here is ordinary flow control, which clears
874				// once the peer reads. Reporting it terminally would have the
875				// caller drop (and so reset) a stream that is finishing
876				// cleanly, so the FIN becomes this stream's debt: `poll_closed`
877				// writes the rest, and `Drop` pays what it can if nobody polls.
878				self.finishing = true;
879				return Ok(());
880			}
881		}
882		web_transport_trait::poll::SendStream::finish(&mut self.inner).map_err(|err| self.map(err))
883	}
884
885	fn reset(&mut self, code: u32) {
886		match self.web {
887			true => self.inner.reset_code(proto::error_to_http3(code)),
888			false => web_transport_trait::poll::SendStream::reset(&mut self.inner, code),
889		}
890	}
891
892	fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
893		// `finish` left the header owed, so finishing is still this call's job.
894		while self.finishing {
895			match ready!(web_transport_trait::poll::SendStream::poll_write(
896				&mut self.inner,
897				cx,
898				&self.prefix
899			)) {
900				Ok(n) => self.prefix.advance(n),
901				Err(err) => return Poll::Ready(Err(self.map(err))),
902			}
903			if self.prefix.is_empty() {
904				self.finishing = false;
905				if let Err(err) = web_transport_trait::poll::SendStream::finish(&mut self.inner) {
906					return Poll::Ready(Err(self.map(err)));
907				}
908			}
909		}
910		match web_transport_trait::poll::SendStream::poll_closed(&mut self.inner, cx) {
911			Poll::Ready(Err(err)) => Poll::Ready(Err(self.map(err))),
912			other => other,
913		}
914	}
915}
916
917impl Drop for SendStream {
918	fn drop(&mut self) {
919		// `finish` returned `Ok` with the header still owed, so the FIN is
920		// this stream's debt rather than the caller's. Pay it if the credit
921		// has since arrived, instead of letting a stream the caller finished
922		// cleanly go out as a cancellation.
923		if self.finishing {
924			let n = self.inner.try_write(&self.prefix);
925			self.prefix.advance(n);
926			if self.prefix.is_empty() {
927				let _ = web_transport_trait::poll::SendStream::finish(&mut self.inner);
928			}
929		}
930		// The inner `Drop` resets with a raw 0, which reads to a browser as an
931		// HTTP/3 stream error rather than the WebTransport cancellation that
932		// dropping a stream means. moq cancels subscriptions by dropping, so
933		// this is the ordinary path.
934		if self.web && !self.inner.ended() {
935			self.inner.reset_code(proto::error_to_http3(0));
936		}
937	}
938}
939
940impl std::fmt::Debug for SendStream {
941	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
942		self.inner.fmt(f)
943	}
944}
945
946/// An incoming stream; its WebTransport header was consumed on accept.
947pub struct RecvStream {
948	inner: super::RecvStream,
949	web: bool,
950}
951
952impl RecvStream {
953	fn map(&self, err: Error) -> Error {
954		match self.web {
955			true => unmap_err(err),
956			false => err,
957		}
958	}
959}
960
961impl web_transport_trait::poll::RecvStream for RecvStream {
962	type Error = Error;
963
964	fn poll_read(&mut self, cx: &mut Context<'_>, dst: &mut [u8]) -> Poll<Result<Option<usize>, Self::Error>> {
965		match web_transport_trait::poll::RecvStream::poll_read(&mut self.inner, cx, dst) {
966			Poll::Ready(Err(err)) => Poll::Ready(Err(self.map(err))),
967			other => other,
968		}
969	}
970
971	fn stop(&mut self, code: u32) {
972		match self.web {
973			true => self.inner.stop_code(proto::error_to_http3(code)),
974			false => web_transport_trait::poll::RecvStream::stop(&mut self.inner, code),
975		}
976	}
977
978	fn poll_closed(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
979		match web_transport_trait::poll::RecvStream::poll_closed(&mut self.inner, cx) {
980			Poll::Ready(Err(err)) => Poll::Ready(Err(self.map(err))),
981			other => other,
982		}
983	}
984}
985
986impl Drop for RecvStream {
987	fn drop(&mut self) {
988		// Same as the send side: the inner `Drop` stops with a raw 0.
989		if self.web && !self.inner.ended() {
990			self.inner.stop_code(proto::error_to_http3(0));
991		}
992	}
993}
994
995impl std::fmt::Debug for RecvStream {
996	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
997		self.inner.fmt(f)
998	}
999}
1000
1001/// Rewrite HTTP/3-mapped error codes back into WebTransport code space.
1002///
1003/// Only a sparse range of HTTP/3 codes names a WebTransport error. Anything
1004/// else is HTTP/3's own failure rather than a code the peer's application
1005/// chose, so it becomes [`Error::Http3`]: keeping the `App`/`Reset`/`Stop`
1006/// variant would advertise it through `session_error()`/`stream_error()` as
1007/// the very thing this mapping exists to tell it apart from.
1008fn unmap_err(err: Error) -> Error {
1009	fn unmap(code: u64) -> Option<u64> {
1010		proto::error_from_http3(code).map(u64::from)
1011	}
1012	match err {
1013		Error::Reset(code) => match unmap(code) {
1014			Some(code) => Error::Reset(code),
1015			None => Error::Http3 {
1016				code,
1017				reason: String::new(),
1018			},
1019		},
1020		Error::Stop(code) => match unmap(code) {
1021			Some(code) => Error::Stop(code),
1022			None => Error::Http3 {
1023				code,
1024				reason: String::new(),
1025			},
1026		},
1027		Error::App { code, reason } => match unmap(code) {
1028			Some(code) => Error::App { code, reason },
1029			None => Error::Http3 { code, reason },
1030		},
1031		other => other,
1032	}
1033}
1034
1035// ── Incoming stream classification ──────────────────────────────────
1036
1037/// Incrementally reads one QUIC varint off a stream, never past it.
1038#[derive(Default)]
1039struct VarRead {
1040	buf: [u8; 8],
1041	have: usize,
1042}
1043
1044enum VarPoll {
1045	Value(u64),
1046	/// The stream ended (or died) before the varint did.
1047	End,
1048}
1049
1050impl VarRead {
1051	fn poll(&mut self, cx: &mut Context<'_>, recv: &mut super::RecvStream) -> Poll<VarPoll> {
1052		loop {
1053			let need = match self.have {
1054				0 => 1,
1055				_ => 1usize << (self.buf[0] >> 6),
1056			};
1057			if self.have >= need {
1058				let mut value = u64::from(self.buf[0] & 0x3f);
1059				for byte in &self.buf[1..need] {
1060					value = (value << 8) | u64::from(*byte);
1061				}
1062				return Poll::Ready(VarPoll::Value(value));
1063			}
1064			match ready!(web_transport_trait::poll::RecvStream::poll_read(
1065				recv,
1066				cx,
1067				&mut self.buf[self.have..need]
1068			)) {
1069				Ok(Some(n)) => self.have += n,
1070				Ok(None) | Err(_) => return Poll::Ready(VarPoll::End),
1071			}
1072		}
1073	}
1074}
1075
1076/// Encode one QUIC varint.
1077fn encode_varint(value: u64, buf: &mut Vec<u8>) {
1078	if value < 1 << 6 {
1079		buf.push(value as u8);
1080	} else if value < 1 << 14 {
1081		buf.extend_from_slice(&((value as u16) | 0x4000).to_be_bytes());
1082	} else if value < 1 << 30 {
1083		buf.extend_from_slice(&((value as u32) | 0x8000_0000).to_be_bytes());
1084	} else {
1085		buf.extend_from_slice(&(value | 0xc000_0000_0000_0000).to_be_bytes());
1086	}
1087}
1088
1089/// Decode one QUIC varint off the front of a slice, advancing past it.
1090fn decode_varint(buf: &mut &[u8]) -> Option<u64> {
1091	let first = *buf.first()?;
1092	let len = 1usize << (first >> 6);
1093	if buf.len() < len {
1094		return None;
1095	}
1096	let mut value = u64::from(first & 0x3f);
1097	for byte in &buf[1..len] {
1098		value = (value << 8) | u64::from(*byte);
1099	}
1100	*buf = &buf[len..];
1101	Some(value)
1102}
1103
1104/// What an incoming unidirectional stream's header says it is.
1105enum UniClass {
1106	/// The H3 control stream, positioned right after its type varint.
1107	Control,
1108	/// A QPACK stream: keep it open, never read it.
1109	Qpack,
1110	/// A WebTransport stream claiming this session id.
1111	Web(u64),
1112	/// Noise (GREASE included); dropping it stops it.
1113	Unknown,
1114}
1115
1116/// An incoming unidirectional stream mid-header.
1117struct PendingUni {
1118	recv: super::RecvStream,
1119	typ: VarRead,
1120	session: VarRead,
1121	typ_value: Option<u64>,
1122}
1123
1124impl PendingUni {
1125	fn new(recv: super::RecvStream) -> Self {
1126		Self {
1127			recv,
1128			typ: VarRead::default(),
1129			session: VarRead::default(),
1130			typ_value: None,
1131		}
1132	}
1133
1134	fn poll_classify(&mut self, cx: &mut Context<'_>) -> Poll<UniClass> {
1135		let typ = match self.typ_value {
1136			Some(typ) => typ,
1137			None => match ready!(self.typ.poll(cx, &mut self.recv)) {
1138				VarPoll::Value(typ) => {
1139					self.typ_value = Some(typ);
1140					typ
1141				}
1142				VarPoll::End => return Poll::Ready(UniClass::Unknown),
1143			},
1144		};
1145		match typ {
1146			STREAM_WEBTRANSPORT => match ready!(self.session.poll(cx, &mut self.recv)) {
1147				VarPoll::Value(session) => Poll::Ready(UniClass::Web(session)),
1148				VarPoll::End => Poll::Ready(UniClass::Unknown),
1149			},
1150			STREAM_CONTROL => Poll::Ready(UniClass::Control),
1151			STREAM_QPACK_ENCODER | STREAM_QPACK_DECODER => Poll::Ready(UniClass::Qpack),
1152			_ => {
1153				tracing::debug!(typ, "ignoring an unknown unidirectional stream");
1154				Poll::Ready(UniClass::Unknown)
1155			}
1156		}
1157	}
1158}
1159
1160/// Progress every pending unidirectional stream; whether any became ready.
1161fn classify_uni(web: &Rc<Web>, cx: &mut Context<'_>) -> bool {
1162	// Taken out of the RefCell so classification can push into the other
1163	// queues without a re-entrant borrow; everything is single-threaded.
1164	let pending = std::mem::take(&mut web.state.borrow_mut().pending_uni);
1165	let mut keep = Vec::new();
1166	let mut progressed = false;
1167
1168	for mut stream in pending {
1169		match stream.poll_classify(cx) {
1170			Poll::Pending => keep.push(stream),
1171			Poll::Ready(UniClass::Web(session)) if session == web.session_id => {
1172				web.state.borrow_mut().ready_uni.push_back(stream.recv);
1173				progressed = true;
1174			}
1175			// A second control stream (or a stray QPACK one) is bogus but
1176			// harmless; holding it beats closing it, which the peer would
1177			// read as tearing down the H3 connection. Only up to a point: a
1178			// finished stream returns its credit, so a peer that keeps opening
1179			// them would grow this vector for as long as it cared to.
1180			Poll::Ready(UniClass::Control | UniClass::Qpack) => {
1181				let mut state = web.state.borrow_mut();
1182				if state.held_recv.len() < HELD_STREAMS {
1183					state.held_recv.push(stream.recv);
1184				}
1185			}
1186			Poll::Ready(_) => {}
1187		}
1188	}
1189
1190	let mut state = web.state.borrow_mut();
1191	state.pending_uni.extend(keep);
1192	progressed
1193}
1194
1195/// An incoming bidirectional stream mid-header: frame type, then session id.
1196struct PendingBi {
1197	send: super::SendStream,
1198	recv: super::RecvStream,
1199	typ: VarRead,
1200	session: VarRead,
1201	typ_value: Option<u64>,
1202}
1203
1204impl PendingBi {
1205	fn new(send: super::SendStream, recv: super::RecvStream) -> Self {
1206		Self {
1207			send,
1208			recv,
1209			typ: VarRead::default(),
1210			session: VarRead::default(),
1211			typ_value: None,
1212		}
1213	}
1214
1215	/// `Some(session)` for a WebTransport stream, `None` for anything else.
1216	fn poll_classify(&mut self, cx: &mut Context<'_>) -> Poll<Option<u64>> {
1217		let typ = match self.typ_value {
1218			Some(typ) => typ,
1219			None => match ready!(self.typ.poll(cx, &mut self.recv)) {
1220				VarPoll::Value(typ) => {
1221					self.typ_value = Some(typ);
1222					typ
1223				}
1224				VarPoll::End => return Poll::Ready(None),
1225			},
1226		};
1227		if typ != FRAME_WEBTRANSPORT {
1228			tracing::debug!(typ, "ignoring an unknown bidirectional stream");
1229			return Poll::Ready(None);
1230		}
1231		match ready!(self.session.poll(cx, &mut self.recv)) {
1232			VarPoll::Value(session) => Poll::Ready(Some(session)),
1233			VarPoll::End => Poll::Ready(None),
1234		}
1235	}
1236}
1237
1238/// Progress every pending bidirectional stream; whether any became ready.
1239fn classify_bi(web: &Rc<Web>, cx: &mut Context<'_>) -> bool {
1240	let pending = std::mem::take(&mut web.state.borrow_mut().pending_bi);
1241	let mut keep = Vec::new();
1242	let mut progressed = false;
1243
1244	for mut stream in pending {
1245		match stream.poll_classify(cx) {
1246			Poll::Pending => keep.push(stream),
1247			Poll::Ready(Some(session)) if session == web.session_id => {
1248				web.state.borrow_mut().ready_bi.push_back((stream.send, stream.recv));
1249				progressed = true;
1250			}
1251			Poll::Ready(_) => {}
1252		}
1253	}
1254
1255	let mut state = web.state.borrow_mut();
1256	state.pending_bi.extend(keep);
1257	progressed
1258}
1259
1260// ── Handshake plumbing ──────────────────────────────────────────────
1261
1262async fn open_uni(conn: &mut Connection) -> Result<super::SendStream, Error> {
1263	std::future::poll_fn(|cx| web_transport_trait::poll::Session::poll_open_uni(conn, cx)).await
1264}
1265
1266async fn accept_bi(conn: &mut Connection) -> Result<(super::SendStream, super::RecvStream), Error> {
1267	std::future::poll_fn(|cx| web_transport_trait::poll::Session::poll_accept_bi(conn, cx)).await
1268}
1269
1270async fn write_all(send: &mut super::SendStream, mut buf: &[u8]) -> Result<(), Error> {
1271	while !buf.is_empty() {
1272		let n = std::future::poll_fn(|cx| web_transport_trait::poll::SendStream::poll_write(send, cx, buf)).await?;
1273		buf = &buf[n..];
1274	}
1275	Ok(())
1276}
1277
1278/// Read another chunk into `buf`; `false` once the stream has ended.
1279///
1280/// Fails rather than letting `buf` grow past [`MESSAGE_LIMIT`], which is what
1281/// bounds a peer that declares a huge frame and never finishes it.
1282async fn read_some(recv: &mut super::RecvStream, buf: &mut BytesMut) -> Result<bool, Error> {
1283	let mut chunk = [0u8; 4096];
1284	let n = std::future::poll_fn(|cx| web_transport_trait::poll::RecvStream::poll_read(recv, cx, &mut chunk)).await?;
1285	match n {
1286		Some(n) => {
1287			if buf.len() + n > MESSAGE_LIMIT {
1288				return Err(Error::Web("an HTTP/3 message exceeded the buffer limit".into()));
1289			}
1290			buf.extend_from_slice(&chunk[..n]);
1291			Ok(true)
1292		}
1293		None => Ok(false),
1294	}
1295}
1296
1297/// Read the SETTINGS off a control stream whose type varint was already
1298/// consumed by classification.
1299async fn read_settings(recv: &mut super::RecvStream) -> Result<proto::Settings, Error> {
1300	let mut buf = BytesMut::new();
1301	// The decoder expects the stream to start with its type; re-seed the one
1302	// byte classification took.
1303	buf.extend_from_slice(&[STREAM_CONTROL as u8]);
1304	loop {
1305		let mut peek: &[u8] = &buf;
1306		match proto::Settings::decode(&mut peek) {
1307			Ok(settings) => return Ok(settings),
1308			Err(proto::SettingsError::UnexpectedEnd) => {}
1309			Err(err) => return Err(Error::Web(err.to_string())),
1310		}
1311		if !read_some(recv, &mut buf).await? {
1312			return Err(Error::Web("control stream ended before SETTINGS".into()));
1313		}
1314	}
1315}
1316
1317/// Read the CONNECT request off the first bidirectional stream.
1318async fn read_connect(recv: &mut super::RecvStream) -> Result<proto::ConnectRequest, Error> {
1319	let mut buf = BytesMut::new();
1320	loop {
1321		let mut peek: &[u8] = &buf;
1322		match proto::ConnectRequest::decode(&mut peek) {
1323			Ok(request) => return Ok(request),
1324			Err(proto::ConnectError::UnexpectedEnd) => {}
1325			Err(err) => return Err(Error::Web(err.to_string())),
1326		}
1327		if !read_some(recv, &mut buf).await? {
1328			return Err(Error::Web("stream ended before the CONNECT request".into()));
1329		}
1330	}
1331}
1332
1333// ── Session close capsules ──────────────────────────────────────────
1334
1335/// Read the CONNECT stream until it ends, surfacing the peer's
1336/// `CloseWebTransportSession` capsule (carried in HTTP/3 DATA frames) as the
1337/// session's error, then close the connection.
1338async fn read_capsules(web: Rc<Web>, conn: Connection, mut recv: super::RecvStream) {
1339	let mut capsules = Capsules::default();
1340	let capsule = loop {
1341		match capsules.take() {
1342			Ok(Some(proto::Capsule::CloseWebTransportSession { code, reason })) => {
1343				break Some((code, reason));
1344			}
1345			// GREASE and anything else defined later: skip it and keep
1346			// reading, since the close capsule may still be behind it.
1347			Ok(Some(_)) => continue,
1348			Ok(None) => {}
1349			Err(err) => {
1350				tracing::debug!(%err, "failed to parse a capsule on the CONNECT stream");
1351				break None;
1352			}
1353		}
1354		match read_some(&mut recv, &mut capsules.frames).await {
1355			Ok(true) => {}
1356			// A clean FIN without a capsule, or the connection died under
1357			// the stream; either way the session is over.
1358			Ok(false) | Err(_) => break None,
1359		}
1360	};
1361
1362	match capsule {
1363		Some((code, reason)) => {
1364			web.closed.borrow_mut().get_or_insert(Error::App {
1365				code: u64::from(code),
1366				reason: reason.clone(),
1367			});
1368			conn.close_code(proto::error_to_http3(code), &reason);
1369		}
1370		// The CONNECT stream ending closes the session with no error.
1371		None => conn.close_code(proto::error_to_http3(0), ""),
1372	}
1373}
1374
1375/// The CONNECT stream's capsule reader.
1376///
1377/// HTTP/3 framing and the Capsule Protocol are independent layers: a capsule
1378/// may span DATA frames and one DATA frame may carry several, so the payloads
1379/// are concatenated and parsed as one continuous byte stream rather than
1380/// frame by frame.
1381#[derive(Default)]
1382struct Capsules {
1383	/// Stream bytes whose HTTP/3 framing has not been split off yet.
1384	frames: BytesMut,
1385	/// The DATA payloads, concatenated: the capsule stream itself.
1386	body: BytesMut,
1387}
1388
1389impl Capsules {
1390	/// Take the next whole capsule, or `None` until one has fully arrived.
1391	fn take(&mut self) -> Result<Option<proto::Capsule>, Error> {
1392		self.demux()?;
1393
1394		let mut peek: &[u8] = &self.body;
1395		match proto::Capsule::decode(&mut peek) {
1396			Ok(capsule) => {
1397				let consumed = self.body.len() - peek.len();
1398				self.body.advance(consumed);
1399				Ok(Some(capsule))
1400			}
1401			// A short header and a short body are both just "not yet".
1402			Err(proto::CapsuleError::UnexpectedEnd | proto::CapsuleError::VarInt(_)) => Ok(None),
1403			Err(err) => Err(Error::Web(err.to_string())),
1404		}
1405	}
1406
1407	/// Move every whole DATA payload into [`body`](Self::body), skipping other
1408	/// frame types entirely.
1409	fn demux(&mut self) -> Result<(), Error> {
1410		loop {
1411			let mut peek: &[u8] = &self.frames;
1412			let Some(typ) = decode_varint(&mut peek) else {
1413				return Ok(());
1414			};
1415			let Some(len) = decode_varint(&mut peek) else {
1416				return Ok(());
1417			};
1418			let len = usize::try_from(len).map_err(|_| Error::Web("oversized HTTP/3 frame".into()))?;
1419			if peek.len() < len {
1420				return Ok(());
1421			}
1422			let header = self.frames.len() - peek.len();
1423
1424			if typ == FRAME_DATA {
1425				// Bounded like the frame buffer: a capsule nothing ever
1426				// completes must not grow here either.
1427				if self.body.len() + len > MESSAGE_LIMIT {
1428					return Err(Error::Web("a capsule exceeded the buffer limit".into()));
1429				}
1430				self.body.extend_from_slice(&peek[..len]);
1431			}
1432			self.frames.advance(header + len);
1433		}
1434	}
1435}
1436
1437#[cfg(test)]
1438mod tests {
1439	use super::*;
1440
1441	/// A code with a WebTransport meaning comes back out as itself; one
1442	/// without is HTTP/3's own failure, and must stop advertising itself as an
1443	/// application error through the trait accessors.
1444	#[test]
1445	fn an_unmappable_code_is_not_an_application_error() {
1446		use web_transport_trait::Error as _;
1447
1448		let app = unmap_err(Error::App {
1449			code: proto::error_to_http3(7),
1450			reason: "seven".into(),
1451		});
1452		assert!(
1453			matches!(&app, Error::App { code: 7, reason } if reason == "seven"),
1454			"got {app:?}"
1455		);
1456		assert_eq!(app.session_error(), Some((7, "seven".to_string())));
1457
1458		// H3_NO_ERROR: HTTP/3's, and no WebTransport code at all.
1459		let h3 = unmap_err(Error::App {
1460			code: 0x100,
1461			reason: "done".into(),
1462		});
1463		assert!(
1464			matches!(&h3, Error::Http3 { code: 0x100, reason } if reason == "done"),
1465			"got {h3:?}"
1466		);
1467		assert_eq!(h3.session_error(), None, "not a code the peer's application chose");
1468
1469		let reset = unmap_err(Error::Reset(0x100));
1470		assert!(matches!(reset, Error::Http3 { code: 0x100, .. }), "got {reset:?}");
1471		assert_eq!(reset.stream_error(), None, "not a MoQ stream code");
1472		assert!(matches!(
1473			unmap_err(Error::Stop(proto::error_to_http3(3))),
1474			Error::Stop(3)
1475		));
1476	}
1477
1478	/// Wrap `payload` in an HTTP/3 frame of type `typ`.
1479	fn frame(typ: u64, payload: &[u8]) -> Vec<u8> {
1480		let mut buf = Vec::new();
1481		encode_varint(typ, &mut buf);
1482		encode_varint(payload.len() as u64, &mut buf);
1483		buf.extend_from_slice(payload);
1484		buf
1485	}
1486
1487	fn close_capsule(code: u32, reason: &str) -> Vec<u8> {
1488		let mut buf = Vec::new();
1489		proto::Capsule::CloseWebTransportSession {
1490			code,
1491			reason: reason.to_string(),
1492		}
1493		.encode(&mut buf);
1494		buf
1495	}
1496
1497	/// The Capsule Protocol runs across DATA frames, so a capsule cut in half
1498	/// by the framing still has to come out whole.
1499	#[test]
1500	fn a_capsule_spans_data_frames() {
1501		let capsule = close_capsule(42, "split");
1502		let (head, tail) = capsule.split_at(capsule.len() / 2);
1503
1504		let mut capsules = Capsules::default();
1505		capsules.frames.extend_from_slice(&frame(FRAME_DATA, head));
1506		assert!(capsules.take().expect("parse").is_none(), "half a capsule is not one");
1507
1508		capsules.frames.extend_from_slice(&frame(FRAME_DATA, tail));
1509		let capsule = capsules.take().expect("parse").expect("the second half completes it");
1510		assert_eq!(
1511			capsule,
1512			proto::Capsule::CloseWebTransportSession {
1513				code: 42,
1514				reason: "split".to_string()
1515			}
1516		);
1517	}
1518
1519	/// One DATA frame may carry several capsules, and reading the first must
1520	/// not discard the rest.
1521	#[test]
1522	fn one_frame_carries_several_capsules() {
1523		let mut payload = close_capsule(1, "first");
1524		payload.extend_from_slice(&close_capsule(2, "second"));
1525
1526		let mut capsules = Capsules::default();
1527		capsules.frames.extend_from_slice(&frame(FRAME_DATA, &payload));
1528
1529		for (code, reason) in [(1, "first"), (2, "second")] {
1530			let capsule = capsules.take().expect("parse").expect("a whole capsule");
1531			assert_eq!(
1532				capsule,
1533				proto::Capsule::CloseWebTransportSession {
1534					code,
1535					reason: reason.to_string()
1536				}
1537			);
1538		}
1539		assert!(capsules.take().expect("parse").is_none(), "only two were written");
1540	}
1541
1542	/// Frames that are not DATA carry no capsules; skipping one must not
1543	/// disturb the capsule stream around it.
1544	#[test]
1545	fn a_non_data_frame_is_skipped() {
1546		let capsule = close_capsule(7, "after");
1547		let (head, tail) = capsule.split_at(1);
1548
1549		let mut capsules = Capsules::default();
1550		capsules.frames.extend_from_slice(&frame(FRAME_DATA, head));
1551		// 0x07 is GOAWAY: legal on the stream, and not a capsule carrier.
1552		capsules.frames.extend_from_slice(&frame(0x07, b"\x00"));
1553		capsules.frames.extend_from_slice(&frame(FRAME_DATA, tail));
1554
1555		let capsule = capsules.take().expect("parse").expect("a whole capsule");
1556		assert_eq!(
1557			capsule,
1558			proto::Capsule::CloseWebTransportSession {
1559				code: 7,
1560				reason: "after".to_string()
1561			}
1562		);
1563	}
1564
1565	/// A peer that declares a capsule payload it never sends must not grow the
1566	/// buffer without end.
1567	#[test]
1568	fn a_capsule_stream_is_bounded() {
1569		// A close capsule whose declared length never arrives. 65536 is the
1570		// largest the decoder entertains, so it keeps asking for more rather
1571		// than refusing the length outright.
1572		let mut header = Vec::new();
1573		encode_varint(0x2843, &mut header);
1574		encode_varint(65536, &mut header);
1575
1576		let mut capsules = Capsules::default();
1577		capsules.frames.extend_from_slice(&frame(FRAME_DATA, &header));
1578
1579		let chunk = vec![0u8; 8 * 1024];
1580		let err = loop {
1581			match capsules.take() {
1582				Ok(None) => {}
1583				Ok(Some(capsule)) => panic!("the payload never arrived, got {capsule:?}"),
1584				Err(err) => break err,
1585			}
1586			capsules.frames.extend_from_slice(&frame(FRAME_DATA, &chunk));
1587		};
1588		assert!(matches!(err, Error::Web(_)), "refused with {err}");
1589		assert!(capsules.body.len() <= MESSAGE_LIMIT, "the buffer stayed bounded");
1590	}
1591}