moqtap_client/draft16/connection.rs
1use std::collections::VecDeque;
2use std::sync::Mutex;
3
4use bytes::{Buf, Bytes, BytesMut};
5
6use crate::draft16::endpoint::{Endpoint, EndpointError};
7use crate::draft16::event::{ClientEvent, Direction, StreamKind};
8use crate::draft16::observer::ConnectionObserver;
9use crate::draft16::session::request_id::Role;
10use crate::draft16::session::setup;
11use crate::malformed_tracks::MalformedTrackCondition;
12use crate::track_locations::{ObjectLocation, ObjectRole, TrackObjects};
13use crate::transport::{RecvStream, SendStream, Transport, TransportError};
14use moqtap_codec::dispatch::{
15 AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
16};
17use moqtap_codec::draft16::data_stream::{
18 FetchHeader, FetchObjectHeader, SubgroupObject, SubgroupObjectReader,
19};
20use moqtap_codec::draft16::error_codes::DataStreamResetErrorCode;
21use moqtap_codec::draft16::message::{ControlMessage, MessageType, RequestError, RequestOk};
22use moqtap_codec::error::CodecError;
23use moqtap_codec::kvp::KeyValuePair;
24use moqtap_codec::types::*;
25use moqtap_codec::varint::VarInt;
26use moqtap_codec::version::DraftVersion;
27
28/// The ALPN identifier draft-16 uses on raw QUIC, `moqt-16`.
29///
30/// Drafts 07 to 14 share one ALPN, `moq-00`, and a peer that offers it has
31/// said nothing about which of the eight it speaks. Draft-15 ended that:
32/// from there each draft has an ALPN of its own, so the version is settled
33/// by the TLS handshake before a byte of MoQT is written.
34///
35/// This is [`DraftVersion::Draft16`]'s own
36/// [`quic_alpn`](DraftVersion::quic_alpn), which is what
37/// [`ClientConfig::alpn`] offers; the test below holds the two together.
38pub const MOQT_ALPN: &[u8] = b"moqt-16";
39
40/// Errors from the connection layer.
41#[derive(Debug, thiserror::Error)]
42pub enum ConnectionError {
43 /// Endpoint state machine error.
44 #[error("endpoint error: {0}")]
45 Endpoint(#[from] EndpointError),
46 /// Wire codec error.
47 #[error("codec error: {0}")]
48 Codec(#[from] CodecError),
49 /// Transport-level error.
50 #[error("transport error: {0}")]
51 Transport(#[from] TransportError),
52 /// Variable-length integer decoding error.
53 #[error("varint error: {0}")]
54 VarInt(#[from] moqtap_codec::varint::VarIntError),
55 /// Control stream was not opened.
56 #[error("control stream not open")]
57 NoControlStream,
58 /// Stream ended before a complete message was read.
59 #[error("unexpected end of stream")]
60 UnexpectedEnd,
61 /// Stream was finished by the peer.
62 #[error("stream finished")]
63 StreamFinished,
64 /// Invalid server address string.
65 #[error("invalid server address: {0}")]
66 InvalidAddress(String),
67 /// TLS configuration error.
68 #[error("TLS config error: {0}")]
69 TlsConfig(String),
70 /// Data stream used out of order (e.g. object before header).
71 #[error("data stream state error: {0}")]
72 DataStreamState(&'static str),
73 /// A control message this build decoded for draft-16 and then could not
74 /// narrow to draft-16's own message type.
75 ///
76 /// Unreachable, and that is not the same as harmless. `read_control`
77 /// decodes with this connection's own draft, so the `AnyControlMessage` it
78 /// hands back can only carry this draft's variant — but the narrowing arm
79 /// is compiled in every configuration anyway, under
80 /// `#[allow(unreachable_patterns)]` rather than a `cfg` naming the other
81 /// drafts, because such a list has to be edited in every per-draft
82 /// module whenever a draft is added and a copy that omits one leaves the
83 /// match non-exhaustive.
84 ///
85 /// Spelled as `CodecError::UnknownMessageType(0)` it would not stay inert:
86 /// every draft's
87 /// [`codec_session_error_code`](Connection::codec_session_error_code)
88 /// answers that variant `Some(PROTOCOL_VIOLATION)`. So the day the
89 /// narrowing did fail, this build's own defect would reach a caller as *the
90 /// peer sent a control message type this draft does not assign, and the
91 /// session must be closed with a Protocol Violation* — carrying `0x00` as
92 /// the codepoint that proved it. A conformance report reading that
93 /// publishes a named, well-evidenced accusation against a relay for
94 /// something no relay did.
95 ///
96 /// A variant of its own is what stops that.
97 /// [`draft_specific_cause`](Connection::draft_specific_cause) answers it
98 /// [`LocalRefusal`], the facade turns that into [`ErrorCause::Facade`], and
99 /// nothing downstream can read a rule out of a cause that says nothing
100 /// reached the wire. What is pinned is the consequence rather than the
101 /// unreachability: nothing pins the arm's reachability, which is exactly
102 /// why the consequence must not be an accusation.
103 ///
104 /// [`LocalRefusal`]: crate::above_codec_rules::DraftSpecificCause::LocalRefusal
105 /// [`ErrorCause::Facade`]: crate::dispatch::ErrorCause::Facade
106 #[error(
107 "a control message decoded for draft-16 did not narrow to draft-16: a defect in this build, and evidence about nothing the peer did"
108 )]
109 ControlMessageNarrowing,
110 /// A bidirectional stream the peer opened began with a message type other
111 /// than SUBSCRIBE_NAMESPACE.
112 ///
113 /// Draft-16 Section 3.3: "This specification only specifies two uses of
114 /// bidirectional streams, the control stream, which begins with
115 /// CLIENT_SETUP, and SUBSCRIBE_NAMESPACE. Bidirectional streams MUST NOT
116 /// begin with any other message type unless negotiated. If they do, the
117 /// peer MUST close the Session with a Protocol Violation." The session has
118 /// already been closed on the wire by the time this is returned, and the
119 /// offending stream reset.
120 #[error(
121 "a bidirectional stream the peer opened began with {0:?}, which does not begin a namespace subscription; the session was closed"
122 )]
123 NonSubscribeNamespaceOnBidiStream(MessageType),
124 /// A `respond_*` helper was called on a namespace subscription this
125 /// endpoint opened.
126 ///
127 /// The answer to a SUBSCRIBE_NAMESPACE is owed by whoever received it, so
128 /// only a stream that arrived through
129 /// [`Connection::accept_namespace_stream`] can be answered here. Nothing
130 /// was written and no state moved.
131 #[error("request {0} was made by this endpoint, so there is nothing here to answer")]
132 NotOursToAnswer(u64),
133 /// An Object arrived carrying extension headers on a status that is not
134 /// Normal.
135 /// Draft-16 Section 10.2.1.2: "Any Object with status Normal can have
136 /// extension headers", with a reference to Section 2.5 inside the sentence,
137 /// and "If an endpoint receives extension headers on Objects with status
138 /// that is not Normal, it MUST close the session with a
139 /// PROTOCOL_VIOLATION."
140 ///
141 /// The codec's `read_object` refuses such an Object as a bare
142 /// `InvalidField`. Its `read_object_meta` frames it, which is how the
143 /// receive path names the rule here and stays in step with the wire.
144 ///
145 /// [`Connection::close_for_data_stream`] performs the close the sentence
146 /// above requires. It is a separate call because the reader that raises
147 /// this holds no connection, and because a deliberately permissive caller
148 /// should be able to read a violating stream and report it without tearing
149 /// the session down.
150 #[error(
151 "object {object_id} carries {extensions_len} bytes of extension headers on status {status:?}, which is not Normal"
152 )]
153 ExtensionsOnNonNormalStatus {
154 /// The Object ID the extension headers arrived on.
155 object_id: u64,
156 /// Length in bytes of the extension-header block.
157 extensions_len: usize,
158 /// The Object's status, resolved through the encoding's elision rule.
159 ///
160 /// Spelled out in full because the glob import of `moqtap_codec::types`
161 /// brings a different `ObjectStatus` into this module.
162 status: moqtap_codec::draft16::types::ObjectStatus,
163 },
164 /// A datagram whose Type announces extension headers arrived with an
165 /// Extension Headers Length of 0.
166 ///
167 /// draft-16 Section 10.3.1: "If an endpoint receives a datagram with the
168 /// EXTENSIONS bit set and an Extension Headers Length of 0, it MUST close
169 /// the session with a PROTOCOL_VIOLATION."
170 ///
171 /// The codec's header decoder refuses such a datagram as a bare
172 /// `InvalidField`. [`Connection::recv_datagram`] names it here, and
173 /// [`Connection::close_for_data_stream`] closes the session over it.
174 #[error(
175 "the datagram for object {object_id} announces extension headers and has an Extension Headers Length of 0"
176 )]
177 EmptyDatagramExtensions {
178 /// The Object ID the datagram named.
179 object_id: u64,
180 },
181}
182
183/// [`ConnectionError::EmptyDatagramExtensions`] for a datagram the header
184/// decoder refused as `InvalidField`, when its empty extension block was the
185/// fault.
186///
187/// draft-16 Section 10.3.1 gives a Type's fields by bit: 0x01 EXTENSIONS,
188/// 0x04 no Object ID, 0x08 no Publisher Priority. The codec refuses a Type
189/// spelled in more than one byte first, so only a one-byte Type is read.
190fn empty_datagram_extensions(data: &[u8]) -> Option<ConnectionError> {
191 use crate::datagram_prefix::{empty_extension_block, Prefix};
192 empty_extension_block(data, |datagram_type, width| {
193 (width == 1 && datagram_type & 0x01 != 0).then_some(Prefix {
194 object_id: datagram_type & 0x04 == 0,
195 priority: datagram_type & 0x08 == 0,
196 })
197 })
198 .map(|object_id| ConnectionError::EmptyDatagramExtensions { object_id })
199}
200
201impl From<crate::transport::DialError> for ConnectionError {
202 /// Maps a dial failure onto the variants this error already has, so a
203 /// caller matches `InvalidAddress` or `TlsConfig`.
204 ///
205 /// A `LocalSocket` failure maps to `InvalidAddress`, which the facade reads
206 /// as this side's failure
207 /// ([`ErrorCause::Facade`](crate::dispatch::ErrorCause::Facade)). The dial
208 /// phase stays on [`DialError::phase`](crate::transport::DialError::phase).
209 fn from(e: crate::transport::DialError) -> Self {
210 match e {
211 // Two variants, one arm, deliberately — see above.
212 crate::transport::DialError::InvalidAddress(s)
213 | crate::transport::DialError::LocalSocket(s) => ConnectionError::InvalidAddress(s),
214 crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
215 crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
216 }
217 }
218}
219
220/// Transport type for the connection.
221#[derive(Debug, Clone)]
222pub enum TransportType {
223 /// Raw QUIC via quinn. The `addr` field should be `host:port`.
224 Quic,
225 /// WebTransport via wtransport. The `url` field is the WebTransport URL.
226 WebTransport {
227 /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
228 url: String,
229 },
230}
231
232/// Configuration for a MoQT client connection.
233///
234/// Both `draft` and `transport` are required -- there is no `Default` impl.
235pub struct ClientConfig {
236 /// The MoQT draft version to use (primary, determines codec/framing).
237 pub draft: DraftVersion,
238 /// The transport type (QUIC or WebTransport).
239 pub transport: TransportType,
240 /// Whether to skip TLS certificate verification (for testing).
241 pub skip_cert_verification: bool,
242 /// Custom CA certificates to trust (DER-encoded).
243 pub ca_certs: Vec<Vec<u8>>,
244 /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
245 pub setup_parameters: Vec<KeyValuePair>,
246}
247
248impl ClientConfig {
249 /// Returns the ALPN protocol identifiers for the transport.
250 pub fn alpn(&self) -> Vec<Vec<u8>> {
251 match &self.transport {
252 TransportType::Quic => vec![self.draft.quic_alpn().to_vec()],
253 TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
254 }
255 }
256}
257
258/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
259pub struct FramedSendStream {
260 inner: SendStream,
261 draft: DraftVersion,
262 /// Stateful subgroup object writer.
263 subgroup_io: Option<SubgroupObjectReader>,
264}
265
266impl FramedSendStream {
267 /// Create a new framed send stream for the given draft version.
268 pub fn new(inner: SendStream, draft: DraftVersion) -> Self {
269 Self { inner, draft, subgroup_io: None }
270 }
271
272 /// Get the transport-level stream ID.
273 pub fn stream_id(&self) -> u64 {
274 self.inner.stream_id()
275 }
276
277 /// Write a control message to the stream with type+length framing.
278 /// Returns the raw bytes that were written (for event capture).
279 pub async fn write_control(
280 &mut self,
281 msg: &AnyControlMessage,
282 ) -> Result<Vec<u8>, ConnectionError> {
283 let mut buf = Vec::new();
284 msg.encode(&mut buf)?;
285 self.inner.write_all(&buf).await?;
286 Ok(buf)
287 }
288
289 /// Write a subgroup stream header. Also initializes the internal
290 /// delta-encoding state used by
291 /// [`FramedSendStream::write_subgroup_object`].
292 ///
293 /// The header is refused, and nothing is written, if its fields disagree
294 /// with its own stream type. That check has to happen here rather than at
295 /// the first object: the type is what every object after it is framed
296 /// against, so a header that went out saying the wrong thing cannot be
297 /// taken back.
298 pub async fn write_subgroup_header(
299 &mut self,
300 header: &AnySubgroupHeader,
301 ) -> Result<(), ConnectionError> {
302 let mut buf = Vec::new();
303 header.encode_stream_checked(&mut buf)?;
304 self.inner.write_all(&buf).await?;
305 // Clippy would rather see these two arms as an `if let`, and rustc rejects
306 // that in a single-draft build, where the pattern is irrefutable. Only a
307 // `match` satisfies both.
308 #[allow(clippy::single_match)]
309 match header {
310 AnySubgroupHeader::Draft16(ref d16) => {
311 self.subgroup_io = Some(SubgroupObjectReader::new(d16));
312 }
313 // Only this draft's header seeds the object reader. With draft 16 the only enabled
314 // draft `AnySubgroupHeader` has a single variant, the arm above is exhaustive and this
315 // one unreachable. Compiled in every configuration with the lint allowed, rather than
316 // gated on a `cfg` naming the other drafts: such a list has to be edited in
317 // every draft module whenever a draft is added, and a copy that omits one leaves this
318 // match non-exhaustive.
319 #[allow(unreachable_patterns)]
320 _ => {}
321 }
322 Ok(())
323 }
324
325 /// Write a fetch response header.
326 pub async fn write_fetch_header(
327 &mut self,
328 header: &AnyFetchHeader,
329 ) -> Result<(), ConnectionError> {
330 let mut buf = Vec::new();
331 header.encode_stream(&mut buf);
332 self.inner.write_all(&buf).await?;
333 Ok(())
334 }
335
336 /// Append a draft-16 subgroup object to the stream using the
337 /// stateful writer seeded from
338 /// [`FramedSendStream::write_subgroup_header`].
339 pub async fn write_subgroup_object(
340 &mut self,
341 object: &SubgroupObject,
342 ) -> Result<(), ConnectionError> {
343 let writer = self
344 .subgroup_io
345 .as_mut()
346 .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
347 let mut buf = Vec::new();
348 writer.write_object(object, &mut buf)?;
349 self.inner.write_all(&buf).await?;
350 Ok(())
351 }
352
353 /// Append a fetch object to the stream.
354 ///
355 /// The fetch stream had a header writer and no object writer, so a caller
356 /// could open one and put nothing on it through this type. The subgroup
357 /// stream has had both since the writer was introduced.
358 ///
359 /// The declared length comes from the payload rather than from the caller's
360 /// field: a header that disagrees with the bytes beside it desynchronises
361 /// every object after it on the stream, and nothing downstream can recover.
362 ///
363 /// # Errors
364 ///
365 /// [`ConnectionError::Codec`] if the header's fields disagree with the
366 /// Serialization Flags that announce them, which the encoder refuses rather
367 /// than writing a frame its own reader cannot take apart.
368 pub async fn write_fetch_object(
369 &mut self,
370 header: &FetchObjectHeader,
371 payload: &[u8],
372 ) -> Result<(), ConnectionError> {
373 let mut header = header.clone();
374 header.payload_length = VarInt::from_usize(payload.len());
375 let mut buf = Vec::new();
376 header.encode(&mut buf)?;
377 buf.extend_from_slice(payload);
378 self.inner.write_all(&buf).await?;
379 Ok(())
380 }
381
382 /// Finish the stream (send FIN).
383 pub async fn finish(&mut self) -> Result<(), ConnectionError> {
384 self.inner.finish()?;
385 Ok(())
386 }
387
388 /// Abandon the stream, handing the peer `code` as the `RESET_STREAM`
389 /// application error code.
390 ///
391 /// Dropping a send stream sends a FIN, which claims the stream ended
392 /// cleanly; this is the only way to say the opposite. See
393 /// [`SendStream::reset`].
394 pub fn reset(&mut self, code: u64) -> Result<(), ConnectionError> {
395 self.inner.reset(code)?;
396 Ok(())
397 }
398
399 /// Returns the draft version this stream is framed for.
400 pub fn draft(&self) -> DraftVersion {
401 self.draft
402 }
403}
404
405/// What an Object Status makes of an object here.
406///
407/// Two answers where drafts 08 through 13 have three, and the missing one is
408/// the point: the end-of-track status settles where the track ended and is
409/// judged against nothing, because the rule about where one may be placed is
410/// not in this draft. `a_track_may_end_where_it_has_already_been.rs` asserts
411/// that acceptance.
412///
413/// Every other status is a statement about objects rather than one of them.
414fn object_role(status: Option<u64>) -> ObjectRole {
415 match status {
416 None | Some(0x0) => ObjectRole::Produced,
417 Some(0x4) => ObjectRole::EndsTrack(None),
418 _ => ObjectRole::Neither,
419 }
420}
421
422/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
423pub struct FramedRecvStream {
424 inner: RecvStream,
425 buf: BytesMut,
426 draft: DraftVersion,
427 /// Stateful subgroup object reader.
428 subgroup_io: Option<SubgroupObjectReader>,
429 /// The record this stream's objects are measured against, and the Group ID
430 /// its header named.
431 ///
432 /// One group for the whole stream: a subgroup header names it once and no
433 /// object header repeats it. `None` on a stream that was never given one -
434 /// a stream for an alias no live binding names, and every stream built
435 /// outside [`Connection::accept_subgroup_stream`] - and such a stream reads
436 /// without being measured, because `note_subgroup_object` has nothing to
437 /// measure it against.
438 tracking: Option<(TrackObjects, u64)>,
439}
440
441impl FramedRecvStream {
442 /// Create a new framed receive stream for the given draft version.
443 pub fn new(inner: RecvStream, draft: DraftVersion) -> Self {
444 Self { inner, buf: BytesMut::with_capacity(4096), draft, subgroup_io: None, tracking: None }
445 }
446
447 /// Get the transport-level stream ID.
448 pub fn stream_id(&self) -> u64 {
449 self.inner.stream_id()
450 }
451
452 /// Measure this stream's objects against `objects`, all of them in `group`.
453 ///
454 /// Called by [`Connection::accept_subgroup_stream`] once the header has
455 /// been read, which is the only point at which both the track and the group
456 /// are known.
457 fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
458 self.tracking = Some((objects, group));
459 }
460
461 /// Judge one object this stream carried against where its track ended.
462 ///
463 /// The object's Group ID is the stream's and its Object ID is its own,
464 /// already resolved from the delta the wire carries; what they are measured
465 /// against is the end an end-of-track object settled on any stream.
466 fn note_subgroup_object(
467 &self,
468 object: u64,
469 status: Option<u64>,
470 ) -> Result<(), ConnectionError> {
471 let Some((objects, group)) = &self.tracking else { return Ok(()) };
472 let at = ObjectLocation { group: *group, object };
473 objects.note_past_final(at, object_role(status)).map_err(|end| {
474 ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject {
475 alias: objects.alias(),
476 group: at.group,
477 object: at.object,
478 final_group: end.group,
479 final_object: end.object,
480 })
481 })
482 }
483
484 /// Read more data from the stream into the internal buffer.
485 async fn fill(&mut self) -> Result<bool, ConnectionError> {
486 let mut tmp = [0u8; 4096];
487 match self.inner.read(&mut tmp).await {
488 Ok(Some(n)) => {
489 self.buf.extend_from_slice(&tmp[..n]);
490 Ok(true)
491 }
492 Ok(None) => Ok(false),
493 Err(e) => Err(ConnectionError::Transport(e)),
494 }
495 }
496
497 /// Ensure at least `n` bytes are available in the buffer.
498 async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
499 while self.buf.len() < n {
500 if !self.fill().await? {
501 return Err(ConnectionError::UnexpectedEnd);
502 }
503 }
504 Ok(())
505 }
506
507 /// Stop reading, telling the peer to stop transmitting with `code` as the
508 /// `STOP_SENDING` application error code, discarding anything unread.
509 ///
510 /// Dropping a receive stream also stops it, but with a hard-coded 0. See
511 /// [`RecvStream::stop`].
512 pub fn stop(&mut self, code: u64) -> Result<(), ConnectionError> {
513 self.inner.stop(code)?;
514 Ok(())
515 }
516
517 /// Wait for the peer to reset this stream, consuming nothing.
518 ///
519 /// See [`RecvStream::received_reset`] for what `Ok(None)` means and why a
520 /// caller must not re-poll after it.
521 pub async fn received_reset(&mut self) -> Result<Option<u64>, ConnectionError> {
522 Ok(self.inner.received_reset().await?)
523 }
524
525 /// Read the next control message, or report that the peer finished the
526 /// stream at a message boundary.
527 ///
528 /// `Ok(None)` is a clean end and not an error: on a namespace
529 /// subscription's stream it is one of the two ways Section 6.1 withdraws
530 /// the subscription — "closing the stream with either a FIN or
531 /// RESET_STREAM" — and the other is a reset, which surfaces as
532 /// [`TransportError::StreamReset`] out of the read below.
533 ///
534 /// A stream that ends *inside* a message is a different thing and stays
535 /// [`ConnectionError::UnexpectedEnd`]: the buffer is empty only at a
536 /// boundary.
537 pub async fn read_control_or_end(
538 &mut self,
539 capture_raw: bool,
540 ) -> Result<Option<(AnyControlMessage, Option<Vec<u8>>)>, ConnectionError> {
541 if self.buf.is_empty() && !self.fill().await? {
542 return Ok(None);
543 }
544 self.read_control(capture_raw).await.map(Some)
545 }
546
547 /// Read a control message from the stream.
548 ///
549 /// When `capture_raw` is true, the returned tuple includes a clone of the
550 /// framed wire bytes (for observer emission). When false, the second
551 /// element is `None` and the payload clone is skipped.
552 pub async fn read_control(
553 &mut self,
554 capture_raw: bool,
555 ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
556 // Read type ID varint
557 self.ensure(1).await?;
558 let type_len = varint_len(self.buf[0]);
559 self.ensure(type_len).await?;
560
561 let mut cursor = &self.buf[..type_len];
562 let _type_id = VarInt::decode(&mut cursor)?;
563
564 // Draft-16: 16-bit BE payload length
565 let (payload_len, len_field_size) = if self.draft.uses_fixed_length_framing() {
566 self.ensure(type_len + 2).await?;
567 let hi = self.buf[type_len] as usize;
568 let lo = self.buf[type_len + 1] as usize;
569 ((hi << 8) | lo, 2)
570 } else {
571 self.ensure(type_len + 1).await?;
572 let payload_len_start = type_len;
573 let payload_len_varint_len = varint_len(self.buf[payload_len_start]);
574 self.ensure(type_len + payload_len_varint_len).await?;
575 let mut cursor = &self.buf[payload_len_start..type_len + payload_len_varint_len];
576 let payload_len = VarInt::decode(&mut cursor)?.into_inner() as usize;
577 (payload_len, payload_len_varint_len)
578 };
579
580 // Read full payload
581 let total = type_len + len_field_size + payload_len;
582 self.ensure(total).await?;
583
584 // Capture raw bytes only if requested (observer attached).
585 let raw = capture_raw.then(|| self.buf[..total].to_vec());
586
587 // Now decode the whole message
588 let mut frame = &self.buf[..total];
589 let msg = AnyControlMessage::decode(self.draft, &mut frame)?;
590 self.buf.advance(total);
591 Ok((msg, raw))
592 }
593
594 /// Read a subgroup stream header. Also initializes the internal
595 /// delta-decoding state.
596 pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
597 self.ensure(1).await?;
598 loop {
599 let mut cursor = &self.buf[..];
600 match AnySubgroupHeader::decode(self.draft, &mut cursor) {
601 Ok(header) => {
602 let consumed = self.buf.len() - cursor.remaining();
603 self.buf.advance(consumed);
604 // Clippy would rather see these two arms as an `if let`, and rustc rejects
605 // that in a single-draft build, where the pattern is irrefutable. Only a
606 // `match` satisfies both.
607 #[allow(clippy::single_match)]
608 match header {
609 AnySubgroupHeader::Draft16(ref d16) => {
610 self.subgroup_io = Some(SubgroupObjectReader::new(d16));
611 }
612 // Only this draft's header seeds the object reader. With draft 16 the only
613 // enabled draft `AnySubgroupHeader` has a single variant, the arm above is
614 // exhaustive and this one unreachable. Compiled in every configuration with
615 // the lint allowed, rather than gated on a `cfg` naming every other
616 // draft: such a list has to be edited in every draft module whenever a
617 // draft is added, and a copy that omits one leaves this match
618 // non-exhaustive.
619 #[allow(unreachable_patterns)]
620 _ => {}
621 }
622 return Ok(header);
623 }
624 Err(e) if e.is_incomplete() => {
625 if !self.fill().await? {
626 return Err(ConnectionError::UnexpectedEnd);
627 }
628 }
629 Err(e) => return Err(ConnectionError::Codec(e)),
630 }
631 }
632 }
633
634 /// Read a fetch response header.
635 pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
636 self.ensure(1).await?;
637 loop {
638 let mut cursor = &self.buf[..];
639 match AnyFetchHeader::decode(self.draft, &mut cursor) {
640 Ok(header) => {
641 let consumed = self.buf.len() - cursor.remaining();
642 self.buf.advance(consumed);
643 return Ok(header);
644 }
645 Err(e) if e.is_incomplete() => {
646 if !self.fill().await? {
647 return Err(ConnectionError::UnexpectedEnd);
648 }
649 }
650 Err(e) => return Err(ConnectionError::Codec(e)),
651 }
652 }
653 }
654
655 /// Read the next draft-16 subgroup object from this stream using
656 /// the stateful reader seeded by
657 /// [`FramedRecvStream::read_subgroup_header`].
658 ///
659 /// Errors with [`ConnectionError::ExtensionsOnNonNormalStatus`] on an
660 /// Object that carries extension headers on a status other than Normal,
661 /// which draft-16 Section 10.2.1.2 answers with a session close. The Object
662 /// is consumed from the stream before the check, so the reader stays in
663 /// step with the wire and a caller that reports the violation and reads on
664 /// sees the following Object rather than a re-parse of this one.
665 pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
666 if self.subgroup_io.is_none() {
667 return Err(ConnectionError::DataStreamState("subgroup header not read yet"));
668 }
669 loop {
670 let reader = self.subgroup_io.as_mut().unwrap();
671 let mut probe = reader.clone();
672 let mut cursor = &self.buf[..];
673 match probe.read_object(&mut cursor) {
674 Ok(obj) => {
675 let consumed = self.buf.len() - cursor.remaining();
676 self.buf.advance(consumed);
677 *reader = probe;
678 if !obj.extensions_permitted() {
679 return Err(ConnectionError::ExtensionsOnNonNormalStatus {
680 object_id: obj.object_id.into_inner(),
681 extensions_len: obj.extension_headers.len(),
682 status: obj.status(),
683 });
684 }
685 self.note_subgroup_object(
686 obj.object_id.into_inner(),
687 obj.object_status.map(|s| s as u64),
688 )?;
689 return Ok(obj);
690 }
691 Err(e) if e.is_incomplete() => {
692 if !self.fill().await? {
693 return Err(ConnectionError::UnexpectedEnd);
694 }
695 }
696 // The codec refuses extension headers on a non-Normal status as
697 // `InvalidField`, one of many readings of that variant. Its
698 // framing-only reader accepts them, which tells this case apart
699 // from the others and keeps the stream in step.
700 Err(CodecError::InvalidField) => {
701 let mut framing = reader.clone();
702 let mut cursor = &self.buf[..];
703 if let Ok(meta) = framing.read_object_meta(&mut cursor) {
704 let status = meta
705 .status
706 .and_then(moqtap_codec::draft16::types::ObjectStatus::from_u64);
707 if let Some(status) = status.filter(|s| {
708 meta.extension_headers_len > 0
709 && *s != moqtap_codec::draft16::types::ObjectStatus::Normal
710 }) {
711 let consumed = self.buf.len() - cursor.remaining();
712 self.buf.advance(consumed);
713 *reader = framing;
714 return Err(ConnectionError::ExtensionsOnNonNormalStatus {
715 object_id: meta.object_id,
716 extensions_len: meta.extension_headers_len as usize,
717 status,
718 });
719 }
720 }
721 return Err(ConnectionError::Codec(CodecError::InvalidField));
722 }
723 Err(e) => return Err(ConnectionError::Codec(e)),
724 }
725 }
726 }
727
728 /// Read the next draft-16 fetch header from this stream.
729 pub async fn read_fetch_stream_header(&mut self) -> Result<FetchHeader, ConnectionError> {
730 loop {
731 let mut cursor = &self.buf[..];
732 match FetchHeader::decode(&mut cursor) {
733 Ok(hdr) => {
734 let consumed = self.buf.len() - cursor.remaining();
735 self.buf.advance(consumed);
736 return Ok(hdr);
737 }
738 Err(e) if e.is_incomplete() => {
739 if !self.fill().await? {
740 return Err(ConnectionError::UnexpectedEnd);
741 }
742 }
743 Err(e) => return Err(ConnectionError::Codec(e)),
744 }
745 }
746 }
747
748 /// Read the next draft-16 fetch object's header and payload.
749 ///
750 /// The mirror of [`FramedSendStream::write_fetch_object`], and the payload
751 /// comes back with the header for the reason the codec leaves it on the
752 /// wire: `payload_length` says how many bytes follow, and a reader that
753 /// takes the wrong number of them desynchronises every later object on the
754 /// stream. Doing it here is the only place that count and the buffer are
755 /// both in hand.
756 ///
757 /// Stateless: the header comes back with its elided fields still absent —
758 /// `group_id`, `object_id` and the Subgroup ID mode say what each Object
759 /// inherited rather than what it is. Resolving them against the Object
760 /// before takes a running
761 /// [`FetchObjectReader`](moqtap_codec::draft16::data_stream::FetchObjectReader),
762 /// which this draft's codec does offer and which this stream does not hold —
763 /// where drafts 15 and 17 through 20 keep one on the stream itself.
764 ///
765 /// So a caller that wants Locations rather than inheritances carries the
766 /// reader beside the stream and calls
767 /// [`resolve`](moqtap_codec::draft16::data_stream::FetchObjectReader::resolve)
768 /// on each header. `AnyConnection::accept_fetch` is that caller, and
769 /// `Draft16FetchStream` is where it keeps the state — draft-16 is the one
770 /// arm of the facade's fetch reader that is not a bare stream.
771 ///
772 /// # Errors
773 ///
774 /// [`ConnectionError::UnexpectedEnd`] when the stream ends inside the header
775 /// or inside the payload it declared, and [`ConnectionError::Codec`] on a
776 /// Serialization Flags value the draft does not define.
777 pub async fn read_fetch_object(
778 &mut self,
779 ) -> Result<(FetchObjectHeader, Vec<u8>), ConnectionError> {
780 let header = loop {
781 let mut cursor = &self.buf[..];
782 match FetchObjectHeader::decode(&mut cursor) {
783 Ok(header) => {
784 let consumed = self.buf.len() - cursor.remaining();
785 self.buf.advance(consumed);
786 break header;
787 }
788 Err(e) if e.is_incomplete() => {
789 if !self.fill().await? {
790 return Err(ConnectionError::UnexpectedEnd);
791 }
792 }
793 Err(e) => return Err(ConnectionError::Codec(e)),
794 }
795 };
796 let payload = self.read_object_payload(&header.payload_length).await?;
797 Ok((header, payload))
798 }
799
800 /// Take the `length` payload bytes that follow a fetch object's header.
801 ///
802 /// Separate from the header read because the header is decoded from a probe
803 /// cursor that may have to be retried after a fill, and the payload is a
804 /// flat byte count that never is.
805 async fn read_object_payload(&mut self, length: &VarInt) -> Result<Vec<u8>, ConnectionError> {
806 let length = length.into_inner() as usize;
807 self.ensure(length).await?;
808 let payload = self.buf[..length].to_vec();
809 self.buf.advance(length);
810 Ok(payload)
811 }
812
813 /// Returns the draft version this stream is framed for.
814 pub fn draft(&self) -> DraftVersion {
815 self.draft
816 }
817}
818
819/// Which side opened the bidirectional stream a namespace subscription
820/// travels on.
821///
822/// Section 6.1 does not say who may subscribe to a namespace, and a relay
823/// subscribing to what a client publishes is the ordinary case, so the stream
824/// arrives in both directions. The two are not symmetric — one side owes an
825/// answer and the other is waiting for it — so a [`NamespaceStream`] carries
826/// this to say which side of that it is on.
827#[derive(Debug, Clone, Copy, PartialEq, Eq)]
828pub enum RequestOrigin {
829 /// This endpoint opened the stream and wrote the SUBSCRIBE_NAMESPACE on
830 /// it. What comes back is the answer and the namespaces that follow it.
831 Local,
832 /// The peer opened the stream; this endpoint owes it an answer and writes
833 /// the namespaces on it afterwards.
834 Peer,
835}
836
837/// The application error code a namespace subscription's stream is abandoned
838/// with when this endpoint never served it: `INTERNAL_ERROR`, 0x0.
839///
840/// Draft-16 assigns no code for this. Its only registry of stream error codes
841/// is Section 13.4.4, "Data Stream Reset Error Codes", and every entry there is
842/// specified by Section 10.4.3, which is about closing subgroup streams — a
843/// namespace subscription's stream is not a data stream. Section 6.1 offers a
844/// FIN as the alternative and names no number for the other form.
845///
846/// So this is a choice rather than a citation, and it is the one that claims
847/// least: `INTERNAL_ERROR` is "an implementation specific error", which is
848/// exactly what a stream abandoned mid-accept is. It is taken from the codec's
849/// own registry rather than written as a literal so a renumbering in a later
850/// draft cannot be missed here.
851///
852/// Only the paths that give up on a stream before it carries a subscription
853/// use it — a request that could not be built, a first message that could not
854/// be read, a Request ID the peer may not use. A caller cancelling a live
855/// subscription picks its own code, or uses the FIN form and picks none.
856const STREAM_ABANDONED: u64 = DataStreamResetErrorCode::InternalError as u64;
857
858/// The largest value a QUIC application error code can carry, `2^62 - 1`.
859///
860/// Checked by [`NamespaceStream::cancel`] before either half of the stream is
861/// touched, so an unrepresentable code cannot half-cancel a subscription.
862const MAX_QUIC_VARINT: u64 = (1u64 << 62) - 1;
863
864/// One SUBSCRIBE_NAMESPACE and everything answering it, on a bidirectional
865/// stream of their own.
866///
867/// Draft-16 Section 3.3: "This specification only specifies two uses of
868/// bidirectional streams, the control stream, which begins with CLIENT_SETUP,
869/// and SUBSCRIBE_NAMESPACE." This is the second use, and the only request on
870/// this draft that has a stream at all — every other one is still written on
871/// the control stream and identified by its Request ID.
872///
873/// The stream matters because two of the four messages that travel on it carry
874/// no Request ID. Section 9.21 puts NAMESPACE "on the response stream of a
875/// SUBSCRIBE_NAMESPACE request" and Section 9.23 says the same of
876/// NAMESPACE_DONE; both carry a Track Namespace **Suffix**, relative to a
877/// prefix only this subscription knows. Without the stream they name nothing.
878///
879/// # Reading and writing go through the connection
880///
881/// This handle owns both halves of the stream but not the session, so the
882/// endpoint state machine and the observer stay where they were. Read with
883/// [`Connection::recv_on_namespace_stream`], answer the peer with
884/// [`Connection::respond_ok_on_namespace_stream`] or
885/// [`Connection::respond_error_on_namespace_stream`], report namespaces with
886/// [`Connection::send_on_namespace_stream`], and withdraw with
887/// [`Connection::cancel_namespace_stream`] or
888/// [`Connection::finish_namespace_stream`].
889///
890/// [`cancel`](Self::cancel), [`finish`](Self::finish) and
891/// [`peer_cancelled`](Self::peer_cancelled) are on the handle because a caller
892/// may hold one without the connection. None of them moves the endpoint's
893/// record of the subscription, which is why the connection carries a wrapper
894/// for each.
895///
896/// # Dropping this cancels the subscription, and correctly
897///
898/// Section 6.1: "A SUBSCRIBE_NAMESPACE can be cancelled by closing the stream
899/// with either a FIN or RESET_STREAM." Dropping a send stream sends a FIN and
900/// dropping a receive stream sends `STOP_SENDING`, so a handle that falls out
901/// of scope performs the first of those two forms exactly. That is why there
902/// is no [`Drop`] impl here: on this draft the default *is* the cancellation,
903/// and drafts 17 to 19 need one only because they made a FIN mean something
904/// else.
905///
906/// What a drop cannot do is say so at the endpoint. It holds the stream and
907/// not the session, so the subscription stays where it was in the endpoint's
908/// record while the stream it travelled on is gone. Call
909/// [`Connection::finish_namespace_stream`] wherever that record matters.
910///
911/// All fields are private so the shape can grow without breaking callers.
912#[must_use = "dropping a namespace stream cancels the subscription; hold it while it is live"]
913pub struct NamespaceStream {
914 send: FramedSendStream,
915 recv: FramedRecvStream,
916 request_id: VarInt,
917 draft: DraftVersion,
918 stream_id: u64,
919 origin: RequestOrigin,
920 /// Whether this handle has already closed the stream, by either form.
921 closed: bool,
922 /// Whether a `respond_*` helper has written the answer on this stream.
923 /// Only ever true on a [`RequestOrigin::Peer`] stream.
924 responded: bool,
925}
926
927impl NamespaceStream {
928 /// The Request ID the SUBSCRIBE_NAMESPACE on this stream carries.
929 pub fn request_id(&self) -> VarInt {
930 self.request_id
931 }
932
933 /// The transport-level stream identifier, the same one
934 /// [`ClientEvent::StreamOpened`] reports.
935 pub fn stream_id(&self) -> u64 {
936 self.stream_id
937 }
938
939 /// The draft version this stream is framed for.
940 pub fn draft(&self) -> DraftVersion {
941 self.draft
942 }
943
944 /// Which side opened this stream.
945 ///
946 /// [`RequestOrigin::Peer`] means this endpoint owes the answer and the
947 /// `respond_*` helpers apply; [`RequestOrigin::Local`] means it is waiting
948 /// for one.
949 pub fn origin(&self) -> RequestOrigin {
950 self.origin
951 }
952
953 /// Whether the answer has been written on this stream by one of the
954 /// `respond_*` helpers.
955 ///
956 /// Always false on a [`RequestOrigin::Local`] stream, which is answered by
957 /// the peer rather than here.
958 pub fn responded(&self) -> bool {
959 self.responded
960 }
961
962 /// Whether [`cancel`](Self::cancel) or [`finish`](Self::finish) has
963 /// already run on this handle.
964 ///
965 /// Says nothing about the peer: a peer's cancel is learned from
966 /// [`peer_cancelled`](Self::peer_cancelled) or from the next read.
967 pub fn is_closed(&self) -> bool {
968 self.closed
969 }
970
971 /// Cancel the subscription by resetting the stream, handing the peer
972 /// `code`.
973 ///
974 /// The second of the two forms Section 6.1 allows. Both halves are shut —
975 /// a QUIC bidirectional stream has two independent halves, so resetting
976 /// only the send half would leave the peer free to keep writing namespaces
977 /// nobody will read. The send half is reset with `code` and the receive
978 /// half is stopped with the same value.
979 ///
980 /// `code` is a plain `u64` and has no default here, because draft-16
981 /// assigns none: its only registry of stream error codes is titled "Data
982 /// Stream Reset Error Codes" and every entry in it is specified by Section
983 /// 10.4.3, which is about closing subgroup streams. A namespace
984 /// subscription's stream is not a data stream, so a caller that wants to
985 /// end one without choosing a number should use [`finish`](Self::finish),
986 /// the form that carries none.
987 ///
988 /// **This is the stream and nothing else.** The endpoint's record of the
989 /// subscription does not move, so a namespace already in flight is still
990 /// accepted after this returns. [`Connection::cancel_namespace_stream`]
991 /// does both and is what a caller holding a connection should reach for.
992 ///
993 /// Idempotent, and errors from a stream that was already reset or finished
994 /// are swallowed: the subscription is cancelled either way.
995 ///
996 /// # Errors
997 ///
998 /// [`ConnectionError::Transport`] carrying [`TransportError::Write`] if
999 /// `code` is outside the QUIC varint range (`0..2^62`). Nothing is sent in
1000 /// that case and the handle is *not* marked closed, so a caller can retry
1001 /// with a representable code.
1002 pub fn cancel(&mut self, code: u64) -> Result<(), ConnectionError> {
1003 if self.closed {
1004 return Ok(());
1005 }
1006 // Rejected before either half is touched, so a failed call leaves the
1007 // stream exactly as it was.
1008 if code > MAX_QUIC_VARINT {
1009 return Err(ConnectionError::Transport(TransportError::Write(format!(
1010 "error code {code} exceeds the varint range"
1011 ))));
1012 }
1013 self.closed = true;
1014 // Already-finished or already-reset halves report StreamClosed; the
1015 // subscription ends regardless, so neither is worth raising.
1016 let _ = self.send.reset(code);
1017 let _ = self.recv.stop(code);
1018 Ok(())
1019 }
1020
1021 /// Cancel the subscription by finishing the send half cleanly.
1022 ///
1023 /// The first of the two forms Section 6.1 allows, and the one that needs
1024 /// no error code. The receive half is left open on purpose: a publisher
1025 /// that has already written namespaces has them in flight, and stopping
1026 /// the half they arrive on would discard what was sent before the FIN.
1027 ///
1028 /// Like [`cancel`](Self::cancel), this is the stream and nothing else.
1029 /// [`Connection::finish_namespace_stream`] is the same act with the
1030 /// endpoint's record attached.
1031 ///
1032 /// Idempotent.
1033 pub async fn finish(&mut self) -> Result<(), ConnectionError> {
1034 if self.closed {
1035 return Ok(());
1036 }
1037 self.closed = true;
1038 self.send.finish().await
1039 }
1040
1041 /// Wait for the peer to reset this stream, consuming nothing.
1042 ///
1043 /// This sees one of Section 6.1's two forms and not the other: a reset
1044 /// arrives here, a FIN arrives as `Ok(None)` from
1045 /// [`Connection::recv_on_namespace_stream`]. A caller that wants to
1046 /// observe both has to read.
1047 ///
1048 /// Returns `Ok(Some(code))` with the peer's application error code, or
1049 /// `Ok(None)` meaning **no reset is observable, now or ever — stop
1050 /// asking**. A caller that re-polls after `Ok(None)` spins.
1051 ///
1052 /// Records nothing at the endpoint;
1053 /// [`Connection::peer_cancelled_on_namespace_stream`] is the same wait
1054 /// with the record attached. Cancel-safe, and it grants no flow-control
1055 /// credit.
1056 ///
1057 /// On WebTransport this always answers `Ok(None)`: `wtransport` exposes no
1058 /// reset-only observable, so a WebTransport caller learns of a peer reset
1059 /// on its next read and not before.
1060 pub async fn peer_cancelled(&mut self) -> Result<Option<u64>, ConnectionError> {
1061 self.recv.received_reset().await
1062 }
1063}
1064
1065/// Holds a peer-opened stream pair while its first message is being read, and
1066/// puts it back on the connection's queue if that read is abandoned.
1067///
1068/// [`Connection::accept_namespace_stream`] awaits a whole control message, and
1069/// a caller may drop that future — a `select!` against a shutdown signal is
1070/// the ordinary reason. Without this the stream, and every byte already read
1071/// off it into the reader's buffer, would go with the future: the peer would
1072/// see its subscription reset for no reason it could act on.
1073///
1074/// [`Drop`] is the only place this can run, because a cancelled future is
1075/// never polled again. Every path that finishes — success or error — takes the
1076/// pair out first, so a pair still present when this drops was cancelled.
1077struct PendingInbound<'a> {
1078 pair: Option<(FramedSendStream, FramedRecvStream)>,
1079 queue: &'a Mutex<VecDeque<(FramedSendStream, FramedRecvStream)>>,
1080}
1081
1082impl Drop for PendingInbound<'_> {
1083 fn drop(&mut self) {
1084 if let Some(pair) = self.pair.take() {
1085 // Front, not back: this stream arrived before anything still
1086 // queued behind it, and a partially read message must not be
1087 // handed out after a stream that arrived later.
1088 self.queue.lock().unwrap_or_else(|p| p.into_inner()).push_front(pair);
1089 }
1090 }
1091}
1092
1093/// A live MoQT connection over QUIC or WebTransport, combining the endpoint
1094/// state machine with actual network I/O.
1095pub struct Connection {
1096 transport: Transport,
1097 endpoint: Endpoint,
1098 draft: DraftVersion,
1099 /// The control stream's write half, behind an async lock.
1100 ///
1101 /// A lock rather than `&mut self` because Section 2.4.2's answer to a
1102 /// Malformed Track is a control message, and the condition is detected
1103 /// where objects arrive - on a datagram read that takes `&self`, and on a
1104 /// stream the caller holds, whose reader has no connection at all.
1105 control_send: Option<tokio::sync::Mutex<FramedSendStream>>,
1106 control_recv: Option<FramedRecvStream>,
1107 observer: Option<Box<dyn ConnectionObserver>>,
1108 /// Setup events buffered during `connect()` and replayed when an
1109 /// observer attaches via `set_observer` — without this, an observer
1110 /// attached after `connect` returns would never see the handshake.
1111 pending_events: Vec<ClientEvent>,
1112 /// The server's half of the setup handshake, kept whole.
1113 ///
1114 /// The endpoint acts on the parameters it recognises and retains none of
1115 /// them, and which parameters a server sends — in what order, with what
1116 /// values — is the sharpest thing a session says about the implementation
1117 /// behind it.
1118 server_setup: AnyControlMessage,
1119 /// The framed wire bytes of [`Self::server_setup`].
1120 server_setup_raw: Option<Vec<u8>>,
1121 /// Bidirectional streams the peer opened that
1122 /// [`accept_namespace_stream`](Connection::accept_namespace_stream) took
1123 /// off the transport but did not finish reading a first message from,
1124 /// because its future was dropped. In arrival order.
1125 ///
1126 /// Without this a caller could not put `accept_namespace_stream` in a
1127 /// `select!` at all: losing the race would lose a stream the peer had
1128 /// already opened and, with it, whatever of the request had arrived.
1129 ///
1130 /// Behind a mutex because the lock is only ever held for a push or a pop,
1131 /// never across an await.
1132 pending_inbound: Mutex<VecDeque<(FramedSendStream, FramedRecvStream)>>,
1133}
1134
1135impl Connection {
1136 /// Connect to a MoQT server as a client.
1137 ///
1138 /// Establishes a QUIC or WebTransport connection (based on
1139 /// `config.transport`), opens a bidirectional control stream,
1140 /// performs the CLIENT_SETUP / SERVER_SETUP handshake, and returns
1141 /// a ready-to-use connection.
1142 pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
1143 // PATH is for native QUIC only, and the transport is known here and
1144 // nowhere further in. Refusing before dialling means a session that
1145 // the server would close on sight is never opened.
1146 setup::validate_client_path_transport(
1147 &config.setup_parameters,
1148 matches!(config.transport, TransportType::WebTransport { .. }),
1149 )
1150 .map_err(EndpointError::from)?;
1151
1152 let transport = match &config.transport {
1153 TransportType::Quic => Self::connect_quic(addr, &config).await?,
1154 TransportType::WebTransport { url } => {
1155 let url = url.clone();
1156 Self::connect_webtransport(&url, &config).await?
1157 }
1158 };
1159
1160 Self::adopt(transport, config).await
1161 }
1162
1163 /// Run the MoQT setup handshake over a transport somebody else established.
1164 ///
1165 /// For choosing the draft from what the server selected: dial once through
1166 /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
1167 /// connection to the module its answer names. [`Self::connect`] cannot do
1168 /// this — it derives its single ALPN from the draft it was given.
1169 ///
1170 /// `config.draft` must match this module. The transport is adopted as
1171 /// given; nothing here re-checks the ALPN it was negotiated with.
1172 pub async fn adopt(
1173 transport: Transport,
1174 config: ClientConfig,
1175 ) -> Result<Self, ConnectionError> {
1176 let draft = config.draft;
1177 // PATH is for native QUIC only, and the transport is known here and
1178 // nowhere further in. Refusing before dialling means a session that
1179 // the server would close on sight is never opened.
1180 setup::validate_client_path_transport(
1181 &config.setup_parameters,
1182 matches!(config.transport, TransportType::WebTransport { .. }),
1183 )
1184 .map_err(EndpointError::from)?;
1185
1186 // Open bidirectional control stream
1187 let (send, recv) = transport.open_bi().await?;
1188 let mut control_send = FramedSendStream::new(send, draft);
1189 let mut control_recv = FramedRecvStream::new(recv, draft);
1190
1191 // Perform setup handshake (draft-16: no versions)
1192 let mut endpoint = Endpoint::new(Role::Client);
1193 endpoint.connect()?;
1194 let setup_msg = endpoint.send_client_setup(config.setup_parameters.clone())?;
1195 let any_setup = AnyControlMessage::Draft16(setup_msg);
1196 let raw_setup = control_send.write_control(&any_setup).await?;
1197
1198 let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
1199 // Unwrap to draft-16 for the endpoint
1200 match &server_setup {
1201 AnyControlMessage::Draft16(ControlMessage::ServerSetup(ref ss)) => {
1202 endpoint.receive_server_setup(ss)?;
1203 }
1204 _ => {
1205 return Err(ConnectionError::Endpoint(EndpointError::NotActive));
1206 }
1207 }
1208
1209 let pending_events = vec![
1210 ClientEvent::ControlMessage {
1211 direction: Direction::Send,
1212 message: any_setup,
1213 stream_id: None,
1214 raw: Some(raw_setup),
1215 },
1216 ClientEvent::ControlMessage {
1217 direction: Direction::Receive,
1218 message: server_setup.clone(),
1219 stream_id: None,
1220 raw: raw_server_setup.clone(),
1221 },
1222 ClientEvent::SetupComplete { negotiated_version: 0xff000000 + 16 },
1223 ];
1224
1225 Ok(Self {
1226 transport,
1227 endpoint,
1228 draft,
1229 control_send: Some(tokio::sync::Mutex::new(control_send)),
1230 control_recv: Some(control_recv),
1231 observer: None,
1232 pending_events,
1233 server_setup,
1234 server_setup_raw: raw_server_setup,
1235 pending_inbound: Mutex::new(VecDeque::new()),
1236 })
1237 }
1238
1239 /// Establish a raw QUIC connection.
1240 ///
1241 /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
1242 /// TLS and endpoint setup.
1243 async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
1244 let (transport, _negotiated) = crate::transport::dial_quic(
1245 addr,
1246 &crate::transport::QuicDialOptions {
1247 skip_cert_verification: config.skip_cert_verification,
1248 ca_certs: config.ca_certs.clone(),
1249 ..crate::transport::QuicDialOptions::new(config.alpn())
1250 },
1251 )
1252 .await?;
1253 Ok(transport)
1254 }
1255
1256 /// Establish a WebTransport connection.
1257 ///
1258 /// [`crate::transport::dial_webtransport`] holds the TLS and endpoint
1259 /// setup, exactly as `connect_quic` above defers its own. That is not
1260 /// only deduplication: both dials must trust the same roots. Settling trust
1261 /// at this call site instead — from `wtransport`'s own builder settings, or
1262 /// from a second config of this draft's own — puts the decision in two
1263 /// places, where it can stop matching what the QUIC dial trusts, so one
1264 /// relay would pass on one transport and fail on the other and a caller's
1265 /// private CA would reach only the dials whose call site installed it.
1266 /// Both ask the same function what to trust.
1267 #[cfg(feature = "webtransport")]
1268 async fn connect_webtransport(
1269 url: &str,
1270 config: &ClientConfig,
1271 ) -> Result<Transport, ConnectionError> {
1272 Ok(crate::transport::dial_webtransport(
1273 url,
1274 &crate::transport::QuicDialOptions {
1275 skip_cert_verification: config.skip_cert_verification,
1276 ca_certs: config.ca_certs.clone(),
1277 // The draft's own protocol identifier. Section 3 gives this
1278 // draft two version-negotiation channels and one of them per
1279 // transport: an ALPN over QUIC, and the WT-Available-Protocols
1280 // header over WebTransport. `config.alpn()` above is `h3`,
1281 // which is the HTTP/3 name and settles no version, so without
1282 // this the draft would be named nowhere.
1283 wt_protocols: vec![config.draft.quic_alpn().to_vec()],
1284 ..crate::transport::QuicDialOptions::new(config.alpn())
1285 },
1286 )
1287 .await?)
1288 }
1289
1290 /// Stub for when the webtransport feature is not enabled.
1291 #[cfg(not(feature = "webtransport"))]
1292 async fn connect_webtransport(
1293 _url: &str,
1294 _config: &ClientConfig,
1295 ) -> Result<Transport, ConnectionError> {
1296 Err(ConnectionError::Transport(TransportError::Connect(
1297 "webtransport feature not enabled".into(),
1298 )))
1299 }
1300
1301 // -- Observer ---------------------------------------------------
1302
1303 /// Attach an observer. Buffered handshake events from `connect()` are
1304 /// flushed in arrival order before this returns.
1305 pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
1306 self.observer = Some(observer);
1307 for event in self.pending_events.drain(..) {
1308 if let Some(ref obs) = self.observer {
1309 obs.on_event_owned(event);
1310 }
1311 }
1312 }
1313
1314 /// Remove the observer.
1315 pub fn clear_observer(&mut self) {
1316 self.observer = None;
1317 }
1318
1319 /// Emit an event to the observer, if one is attached.
1320 fn emit(&self, event: ClientEvent) {
1321 if let Some(ref obs) = self.observer {
1322 obs.on_event_owned(event);
1323 }
1324 }
1325
1326 // -- Control message I/O ----------------------------------------
1327
1328 /// Send a control message on the control stream.
1329 ///
1330 /// Wraps the draft-16 message in `AnyControlMessage::Draft16` for
1331 /// framing.
1332 pub async fn send_control(&self, msg: &ControlMessage) -> Result<(), ConnectionError> {
1333 let any = AnyControlMessage::Draft16(msg.clone());
1334 let mut send =
1335 self.control_send.as_ref().ok_or(ConnectionError::NoControlStream)?.lock().await;
1336 let raw = send.write_control(&any).await?;
1337 drop(send);
1338 self.emit(ClientEvent::ControlMessage {
1339 direction: Direction::Send,
1340 message: any,
1341 stream_id: None,
1342 raw: Some(raw),
1343 });
1344 Ok(())
1345 }
1346
1347 /// Read the next control message from the control stream.
1348 ///
1349 /// Returns the `AnyControlMessage` and also extracts the draft-16
1350 /// `ControlMessage` for internal endpoint dispatch.
1351 pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
1352 let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
1353 let capture_raw = self.observer.is_some();
1354 let (any, raw) = match recv.read_control(capture_raw).await {
1355 Ok(v) => v,
1356 Err(e) => return Err(self.close_for_codec(e)),
1357 };
1358 if capture_raw {
1359 self.emit(ClientEvent::ControlMessage {
1360 direction: Direction::Receive,
1361 message: any.clone(),
1362 stream_id: None,
1363 raw,
1364 });
1365 }
1366 // Unwrap to draft-16 for the endpoint
1367 match any {
1368 AnyControlMessage::Draft16(msg) => Ok(msg),
1369 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 16 the
1370 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
1371 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
1372 // naming the other drafts: such a list has to be edited in every draft module
1373 // whenever a draft is added, and a copy that omits one leaves this match
1374 // non-exhaustive.
1375 #[allow(unreachable_patterns)]
1376 _ => Err(ConnectionError::ControlMessageNarrowing),
1377 }
1378 }
1379
1380 /// Read and dispatch the next incoming control message through the
1381 /// endpoint state machine. Returns the decoded message for inspection.
1382 pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
1383 let msg = self.recv_control().await?;
1384 self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
1385
1386 // Emit draining event if this was a GoAway
1387 if let ControlMessage::GoAway(ref ga) = msg {
1388 self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
1389 }
1390
1391 Ok(msg)
1392 }
1393
1394 // -- Subscribe flow ---------------------------------------------
1395
1396 /// Send a SUBSCRIBE and return the allocated request ID.
1397 pub async fn subscribe(
1398 &mut self,
1399 track_namespace: TrackNamespace,
1400 track_name: Vec<u8>,
1401 parameters: Vec<KeyValuePair>,
1402 ) -> Result<VarInt, ConnectionError> {
1403 let (req_id, msg) = self.endpoint.subscribe(track_namespace, track_name, parameters)?;
1404 self.send_control(&msg).await?;
1405 Ok(req_id)
1406 }
1407
1408 /// Send an UNSUBSCRIBE for the given request ID.
1409 pub async fn unsubscribe(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1410 let msg = self.endpoint.unsubscribe(request_id)?;
1411 self.send_control(&msg).await
1412 }
1413
1414 /// Accept a subscription the peer opened, sending SUBSCRIBE_OK and giving
1415 /// its track a Track Alias.
1416 ///
1417 /// The endpoint refuses an alias a live track of its own already holds and
1418 /// refuses a second answer to one SUBSCRIBE, so nothing is written on the
1419 /// wire when it does either.
1420 pub async fn subscribe_ok(
1421 &mut self,
1422 request_id: VarInt,
1423 track_alias: VarInt,
1424 track_extensions: Vec<KeyValuePair>,
1425 parameters: Vec<KeyValuePair>,
1426 ) -> Result<(), ConnectionError> {
1427 let msg = self.endpoint.send_subscribe_ok(
1428 request_id,
1429 track_alias,
1430 track_extensions,
1431 parameters,
1432 )?;
1433 self.send_control(&msg).await
1434 }
1435
1436 /// Refuse a request the peer opened, sending REQUEST_ERROR.
1437 ///
1438 /// One message refuses a SUBSCRIBE or a FETCH, and the endpoint finds
1439 /// which by the identifier. It refuses a second answer to either, and
1440 /// refuses a Joining Fetch's refusal under any code but the one the draft
1441 /// names for it, so nothing is written on the wire when it does.
1442 pub async fn request_error(
1443 &mut self,
1444 request_id: VarInt,
1445 error_code: VarInt,
1446 retry_interval: VarInt,
1447 reason_phrase: Vec<u8>,
1448 ) -> Result<(), ConnectionError> {
1449 let msg = self.endpoint.send_request_error(
1450 request_id,
1451 error_code,
1452 retry_interval,
1453 reason_phrase,
1454 )?;
1455 self.send_control(&msg).await
1456 }
1457
1458 /// Narrow a subscription this endpoint opened, sending REQUEST_UPDATE, and
1459 /// return the Request ID the update itself spent.
1460 pub async fn request_update(
1461 &mut self,
1462 existing_request_id: VarInt,
1463 parameters: Vec<KeyValuePair>,
1464 ) -> Result<VarInt, ConnectionError> {
1465 let (request_id, msg) = self.endpoint.request_update(existing_request_id, parameters)?;
1466 self.send_control(&msg).await?;
1467 Ok(request_id)
1468 }
1469
1470 /// Accept a PUBLISH the peer sent, which establishes the subscription it
1471 /// opened.
1472 ///
1473 /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1474 /// written on the wire when it does.
1475 pub async fn publish_ok(
1476 &mut self,
1477 request_id: VarInt,
1478 parameters: Vec<KeyValuePair>,
1479 ) -> Result<(), ConnectionError> {
1480 let msg = self.endpoint.send_publish_ok(request_id, parameters)?;
1481 self.send_control(&msg).await
1482 }
1483
1484 /// Reject a PUBLISH the peer sent, which ends the subscription it opened
1485 /// before it was established.
1486 ///
1487 /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1488 /// written on the wire when it does.
1489 pub async fn publish_error(
1490 &mut self,
1491 request_id: VarInt,
1492 error_code: VarInt,
1493 retry_interval: VarInt,
1494 reason_phrase: Vec<u8>,
1495 ) -> Result<(), ConnectionError> {
1496 let msg = self.endpoint.send_publish_error(
1497 request_id,
1498 error_code,
1499 retry_interval,
1500 reason_phrase,
1501 )?;
1502 self.send_control(&msg).await
1503 }
1504
1505 // -- Fetch flow -------------------------------------------------
1506
1507 /// Send a standalone FETCH and return the allocated request ID.
1508 #[allow(clippy::too_many_arguments)]
1509 pub async fn fetch(
1510 &mut self,
1511 track_namespace: TrackNamespace,
1512 track_name: Vec<u8>,
1513 start_group: VarInt,
1514 start_object: VarInt,
1515 end_group: VarInt,
1516 end_object: VarInt,
1517 parameters: Vec<KeyValuePair>,
1518 ) -> Result<VarInt, ConnectionError> {
1519 let (req_id, msg) = self.endpoint.fetch(
1520 track_namespace,
1521 track_name,
1522 start_group,
1523 start_object,
1524 end_group,
1525 end_object,
1526 parameters,
1527 )?;
1528 self.send_control(&msg).await?;
1529 Ok(req_id)
1530 }
1531
1532 /// Send a Relative Joining Fetch and return the allocated request ID.
1533 ///
1534 /// `joining_start` counts groups back from the subscription's largest
1535 /// group. To name the starting group outright, use
1536 /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
1537 pub async fn joining_fetch(
1538 &mut self,
1539 joining_request_id: VarInt,
1540 joining_start: VarInt,
1541 parameters: Vec<KeyValuePair>,
1542 ) -> Result<VarInt, ConnectionError> {
1543 let (req_id, msg) =
1544 self.endpoint.joining_fetch(joining_request_id, joining_start, parameters)?;
1545 self.send_control(&msg).await?;
1546 Ok(req_id)
1547 }
1548
1549 /// Send an Absolute Joining Fetch and return the allocated request ID.
1550 ///
1551 /// Here `joining_start` is the group to begin at rather than an offset,
1552 /// which is what an application that knows the group it wants has: draft-16
1553 /// Section 9.16.2.1 has the publisher set the Start Location to
1554 /// {Joining Start, 0}.
1555 pub async fn absolute_joining_fetch(
1556 &mut self,
1557 joining_request_id: VarInt,
1558 joining_start: VarInt,
1559 parameters: Vec<KeyValuePair>,
1560 ) -> Result<VarInt, ConnectionError> {
1561 let (req_id, msg) =
1562 self.endpoint.absolute_joining_fetch(joining_request_id, joining_start, parameters)?;
1563 self.send_control(&msg).await?;
1564 Ok(req_id)
1565 }
1566
1567 /// Send a FETCH_CANCEL for the given request ID.
1568 pub async fn fetch_cancel(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1569 let msg = self.endpoint.fetch_cancel(request_id)?;
1570 self.send_control(&msg).await
1571 }
1572
1573 /// Accept a fetch the peer opened, sending FETCH_OK.
1574 ///
1575 /// The endpoint refuses a Joining Fetch naming a subscription this session
1576 /// cannot join and refuses a second answer to one FETCH, so nothing is
1577 /// written on the wire when it does either.
1578 pub async fn fetch_ok(
1579 &mut self,
1580 request_id: VarInt,
1581 end_of_track: u8,
1582 end_group: VarInt,
1583 end_object: VarInt,
1584 parameters: Vec<KeyValuePair>,
1585 track_extensions: Vec<KeyValuePair>,
1586 ) -> Result<(), ConnectionError> {
1587 let msg = self.endpoint.send_fetch_ok(
1588 request_id,
1589 end_of_track,
1590 end_group,
1591 end_object,
1592 parameters,
1593 track_extensions,
1594 )?;
1595 self.send_control(&msg).await
1596 }
1597
1598 // -- Namespace flows --------------------------------------------
1599
1600 /// Send a SUBSCRIBE_NAMESPACE on a bidirectional stream of its own.
1601 ///
1602 /// Section 6.1: "The subscriber sends SUBSCRIBE_NAMESPACE on a new
1603 /// bidirectional stream and the publisher MUST send a single REQUEST_OK or
1604 /// REQUEST_ERROR as the first message on the bidirectional stream in
1605 /// response to a SUBSCRIBE_NAMESPACE." Every other draft-16 request is
1606 /// still written on the control stream; this is the one that is not.
1607 ///
1608 /// The returned [`NamespaceStream`] **must be held while the subscription
1609 /// is live**. Section 6.1 makes closing the stream the cancellation, so
1610 /// letting the handle fall out of scope withdraws the subscription — see
1611 /// the type's own note.
1612 ///
1613 /// `subscribe_options` selects what the publisher reports back: PUBLISH
1614 /// (0x00), NAMESPACE (0x01) or both (0x02), per Section 9.25.
1615 ///
1616 /// # Ordering
1617 ///
1618 /// The stream is opened before the Request ID is allocated, because a
1619 /// failed open would otherwise burn an id the endpoint cannot retract. If
1620 /// the endpoint refuses the request the stream is reset rather than
1621 /// dropped: dropping would FIN it, which on this draft says a
1622 /// subscription that was never made has been withdrawn.
1623 pub async fn subscribe_namespace(
1624 &mut self,
1625 namespace_prefix: TrackNamespace,
1626 subscribe_options: VarInt,
1627 parameters: Vec<KeyValuePair>,
1628 ) -> Result<NamespaceStream, ConnectionError> {
1629 let (send, recv) = self.transport.open_bi().await?;
1630 let mut send = FramedSendStream::new(send, self.draft);
1631 let mut recv = FramedRecvStream::new(recv, self.draft);
1632
1633 let (req_id, msg) = match self.endpoint.subscribe_namespace(
1634 namespace_prefix,
1635 subscribe_options,
1636 parameters,
1637 ) {
1638 Ok(built) => built,
1639 Err(e) => {
1640 let _ = send.reset(STREAM_ABANDONED);
1641 let _ = recv.stop(STREAM_ABANDONED);
1642 return Err(ConnectionError::Endpoint(e));
1643 }
1644 };
1645
1646 let stream_id = send.stream_id();
1647 self.emit(ClientEvent::StreamOpened {
1648 direction: Direction::Send,
1649 stream_kind: StreamKind::NamespaceSubscription,
1650 stream_id,
1651 });
1652 let any = AnyControlMessage::Draft16(msg);
1653 let raw = match send.write_control(&any).await {
1654 Ok(raw) => raw,
1655 Err(e) => {
1656 let _ = send.reset(STREAM_ABANDONED);
1657 let _ = recv.stop(STREAM_ABANDONED);
1658 return Err(e);
1659 }
1660 };
1661 self.emit(ClientEvent::ControlMessage {
1662 direction: Direction::Send,
1663 message: any,
1664 stream_id: Some(stream_id),
1665 raw: Some(raw),
1666 });
1667 Ok(NamespaceStream {
1668 send,
1669 recv,
1670 request_id: req_id,
1671 draft: self.draft,
1672 stream_id,
1673 origin: RequestOrigin::Local,
1674 closed: false,
1675 responded: false,
1676 })
1677 }
1678
1679 /// Accept the next bidirectional stream the peer opened, read the
1680 /// SUBSCRIBE_NAMESPACE it begins with, and hand back that request and a
1681 /// handle to answer it on.
1682 ///
1683 /// The mirror of [`subscribe_namespace`](Self::subscribe_namespace).
1684 /// Section 3.3 does not say who may open the second kind of bidirectional
1685 /// stream, and a relay subscribing to what a client publishes is the
1686 /// ordinary case, so a client that never calls this can never be asked for
1687 /// its namespaces.
1688 ///
1689 /// The returned [`NamespaceStream`] carries [`RequestOrigin::Peer`].
1690 /// Answer it with
1691 /// [`respond_ok_on_namespace_stream`](Self::respond_ok_on_namespace_stream)
1692 /// or
1693 /// [`respond_error_on_namespace_stream`](Self::respond_error_on_namespace_stream),
1694 /// and **hold it for as long as the subscription lasts** — every NAMESPACE
1695 /// and NAMESPACE_DONE is written on it, and dropping it ends the
1696 /// subscription.
1697 ///
1698 /// # Two refusals, two codes
1699 ///
1700 /// Section 3.3, on a stream that begins with the wrong type:
1701 /// "Bidirectional streams MUST NOT begin with any other message type
1702 /// unless negotiated. If they do, the peer MUST close the Session with a
1703 /// Protocol Violation." Section 9.1, on the Request ID: "If an endpoint
1704 /// receives a Request ID that is not valid for the peer, or a new request
1705 /// with a Request ID that is not the next in sequence or exceeds the
1706 /// received MAX_REQUEST_ID, it MUST close the session with
1707 /// INVALID_REQUEST_ID." Both are closes of the session on the
1708 /// wire, with different codes, and both happen before this returns — the
1709 /// error handed back reports a session that is already gone, not one the
1710 /// caller must remember to close.
1711 ///
1712 /// The refusal cannot be built without the acceptance. An endpoint that
1713 /// took a bidirectional stream only to refuse everything on it would close
1714 /// sessions over the SUBSCRIBE_NAMESPACE the same sentence permits.
1715 ///
1716 /// # Cancelling this future loses nothing
1717 ///
1718 /// A stream taken off the transport but not yet read is put back on an
1719 /// internal queue, and the next call takes it before accepting anything
1720 /// new — including whatever bytes of the request had already arrived,
1721 /// which live in the stream's own reader. So this is safe to `select!`
1722 /// against a shutdown signal or a timer. See
1723 /// [`pending_inbound_count`](Self::pending_inbound_count).
1724 ///
1725 /// What it is **not** safe to do is run concurrently with another method
1726 /// on the same connection: this takes `&mut self` because registering the
1727 /// peer's request moves endpoint state.
1728 ///
1729 /// # Ordering
1730 ///
1731 /// The endpoint is told about the request last, after every step that can
1732 /// fail or be cancelled, and building the handle afterwards cannot fail.
1733 /// Registering earlier would let a cancelled accept leave a state machine
1734 /// keyed to a stream nobody holds, with the peer's Request ID spent on a
1735 /// request nobody answers.
1736 ///
1737 /// # Errors
1738 ///
1739 /// - [`ConnectionError::NonSubscribeNamespaceOnBidiStream`] — the session
1740 /// has been closed with PROTOCOL_VIOLATION and the stream reset.
1741 /// - [`ConnectionError::Endpoint`] carrying `RequestId` — the session has
1742 /// been closed with INVALID_REQUEST_ID and the stream reset.
1743 /// - [`ConnectionError::Endpoint`] carrying `NotActive` or `Draining` —
1744 /// the stream is reset, the session is left alone.
1745 /// - [`ConnectionError::Transport`] or [`ConnectionError::Codec`] — the
1746 /// stream is reset, the session is left alone.
1747 pub async fn accept_namespace_stream(
1748 &mut self,
1749 ) -> Result<(ControlMessage, NamespaceStream), ConnectionError> {
1750 let pair = match self.take_pending_inbound() {
1751 Some(pair) => pair,
1752 None => {
1753 let (send, recv) = self.transport.accept_bi().await?;
1754 (FramedSendStream::new(send, self.draft), FramedRecvStream::new(recv, self.draft))
1755 }
1756 };
1757 let capture_raw = self.observer.is_some();
1758
1759 let (any, raw, mut send, mut recv) = {
1760 let mut pending = PendingInbound { pair: Some(pair), queue: &self.pending_inbound };
1761 let read = {
1762 let (_, recv) = pending.pair.as_mut().expect("set on construction");
1763 recv.read_control(capture_raw).await
1764 };
1765 // Taken out before anything can return, so the guard's Drop puts
1766 // the pair back for exactly one reason: this future was cancelled.
1767 let (mut send, mut recv) = pending.pair.take().expect("set on construction");
1768 match read {
1769 Ok((any, raw)) => (any, raw, send, recv),
1770 Err(e) => {
1771 // A stream whose first message could not be read is not
1772 // worth queueing: the next accept would fail on it the
1773 // same way. Reset rather than FIN — nothing was served.
1774 let _ = send.reset(STREAM_ABANDONED);
1775 let _ = recv.stop(STREAM_ABANDONED);
1776 return Err(e);
1777 }
1778 }
1779 };
1780
1781 // Reported once the request has actually arrived rather than when the
1782 // stream came off the transport, so a cancelled accept that is retried
1783 // does not report the same stream twice.
1784 let stream_id = send.stream_id();
1785 self.emit(ClientEvent::StreamOpened {
1786 direction: Direction::Receive,
1787 stream_kind: StreamKind::NamespaceSubscription,
1788 stream_id,
1789 });
1790 if capture_raw {
1791 self.emit(ClientEvent::ControlMessage {
1792 direction: Direction::Receive,
1793 message: any.clone(),
1794 stream_id: Some(stream_id),
1795 raw,
1796 });
1797 }
1798
1799 let msg = match any {
1800 AnyControlMessage::Draft16(msg) => msg,
1801 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 16 the
1802 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
1803 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
1804 // naming the other drafts: such a list has to be edited in every draft module
1805 // whenever a draft is added, and a copy that omits one leaves this match
1806 // non-exhaustive.
1807 #[allow(unreachable_patterns)]
1808 _ => {
1809 let _ = send.reset(STREAM_ABANDONED);
1810 let _ = recv.stop(STREAM_ABANDONED);
1811 return Err(ConnectionError::ControlMessageNarrowing);
1812 }
1813 };
1814
1815 let ty = msg.message_type();
1816 if ty != MessageType::SubscribeNamespace {
1817 let err = self.endpoint.refuse_non_subscribe_namespace(ty);
1818 self.close_for(&err);
1819 let _ = send.reset(STREAM_ABANDONED);
1820 let _ = recv.stop(STREAM_ABANDONED);
1821 return Err(ConnectionError::NonSubscribeNamespaceOnBidiStream(ty));
1822 }
1823
1824 let request_id = match self.endpoint.receive_subscribe_namespace_on_stream(&msg) {
1825 Ok(request_id) => request_id,
1826 Err(e) => {
1827 let _ = send.reset(STREAM_ABANDONED);
1828 let _ = recv.stop(STREAM_ABANDONED);
1829 return Err(self.close_if_session_fatal(e));
1830 }
1831 };
1832
1833 Ok((
1834 msg,
1835 NamespaceStream {
1836 send,
1837 recv,
1838 request_id,
1839 draft: self.draft,
1840 stream_id,
1841 origin: RequestOrigin::Peer,
1842 closed: false,
1843 responded: false,
1844 },
1845 ))
1846 }
1847
1848 /// Take the oldest stream pair a cancelled
1849 /// [`accept_namespace_stream`](Self::accept_namespace_stream) put back, if
1850 /// any.
1851 ///
1852 /// Synchronous on purpose: the guard is dropped before the caller awaits,
1853 /// so the lock is never held across a suspension point.
1854 fn take_pending_inbound(&self) -> Option<(FramedSendStream, FramedRecvStream)> {
1855 self.pending_inbound.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).pop_front()
1856 }
1857
1858 /// How many peer-opened namespace streams a cancelled
1859 /// [`accept_namespace_stream`](Self::accept_namespace_stream) put back and
1860 /// a later call has not yet taken.
1861 ///
1862 /// Zero unless an accept future was dropped mid-read.
1863 pub fn pending_inbound_count(&self) -> usize {
1864 self.pending_inbound.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).len()
1865 }
1866
1867 /// Read the next message off a namespace subscription's stream and
1868 /// dispatch it through the endpoint.
1869 ///
1870 /// Three things can come back, and each is one of the shapes Section 6.1
1871 /// and Section 9.25 describe:
1872 ///
1873 /// - `Ok(Some(msg))` — a REQUEST_OK or REQUEST_ERROR answering the
1874 /// subscription, or a NAMESPACE or NAMESPACE_DONE reporting on it.
1875 /// - `Ok(None)` — the peer finished its half with a FIN. Section 6.1 makes
1876 /// that a cancellation, and it is recorded here.
1877 /// - `Err` carrying [`TransportError::StreamReset`] — the peer reset the
1878 /// stream, the other form of the same cancellation, also recorded.
1879 ///
1880 /// This blocks until a whole message has arrived. Backpressure is per
1881 /// subscription: a stream nobody reads stays unread, and the peer stays
1882 /// flow controlled on it alone.
1883 ///
1884 /// # A stream the peer opened reports but does not dispatch
1885 ///
1886 /// Draft-16 places nothing after the SUBSCRIBE_NAMESPACE on the
1887 /// subscriber's half, so on a [`RequestOrigin::Peer`] stream there is no
1888 /// state for a second message to move and none is attempted; what a
1889 /// responder reads for is the peer's FIN. A message that arrives anyway is
1890 /// handed back rather than refused, because no sentence in this draft
1891 /// forbids it.
1892 ///
1893 /// # Errors
1894 ///
1895 /// [`ConnectionError::Endpoint`] if the message does not fit the
1896 /// subscription's state, or names a different request than this stream
1897 /// carries. The message has already been emitted to the observer by
1898 /// then — what arrived is reported whether or not the endpoint accepts it.
1899 pub async fn recv_on_namespace_stream(
1900 &mut self,
1901 stream: &mut NamespaceStream,
1902 ) -> Result<Option<ControlMessage>, ConnectionError> {
1903 let capture_raw = self.observer.is_some();
1904 let read = match stream.recv.read_control_or_end(capture_raw).await {
1905 Ok(read) => read,
1906 Err(e) => {
1907 // A peer that reset this stream cancelled the subscription on
1908 // it, and this is where a caller reading normally learns of
1909 // it. The record is made and its verdict dropped: the read's
1910 // own error is what the caller has to act on, and returning a
1911 // state error in its place would hide a reset behind it.
1912 if matches!(e, ConnectionError::Transport(TransportError::StreamReset(_))) {
1913 let _ = self.endpoint.cancel_namespace_subscription(stream.request_id);
1914 }
1915 return Err(e);
1916 }
1917 };
1918 let Some((any, raw)) = read else {
1919 // The other half of Section 6.1's sentence. Recorded the same way
1920 // and for the same reason as the reset above.
1921 let _ = self.endpoint.cancel_namespace_subscription(stream.request_id);
1922 return Ok(None);
1923 };
1924 if capture_raw {
1925 self.emit(ClientEvent::ControlMessage {
1926 direction: Direction::Receive,
1927 message: any.clone(),
1928 stream_id: Some(stream.stream_id),
1929 raw,
1930 });
1931 }
1932 let msg = match any {
1933 AnyControlMessage::Draft16(msg) => Ok::<_, ConnectionError>(msg),
1934 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 16 the
1935 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
1936 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
1937 // naming the other drafts: such a list has to be edited in every draft module
1938 // whenever a draft is added, and a copy that omits one leaves this match
1939 // non-exhaustive.
1940 #[allow(unreachable_patterns)]
1941 _ => Err(ConnectionError::ControlMessageNarrowing),
1942 }?;
1943 if stream.origin == RequestOrigin::Local {
1944 self.endpoint
1945 .receive_on_namespace_stream(stream.request_id, &msg)
1946 .map_err(|e| self.close_if_session_fatal(e))?;
1947 }
1948 Ok(Some(msg))
1949 }
1950
1951 /// Write a message on an open namespace subscription stream.
1952 ///
1953 /// This is for what follows the answer: Section 9.25 says the publisher
1954 /// "will send matching NAMESPACE messages on the response stream if they
1955 /// are requested", and NAMESPACE_DONE withdraws one of them on the same
1956 /// stream. The answer itself has its own helpers, which drive the endpoint
1957 /// as well as the wire.
1958 ///
1959 /// It does not refuse any message type: which messages may follow the
1960 /// answer is not something this implementation can settle, so the choice
1961 /// is left to the caller rather than guessed at.
1962 pub async fn send_on_namespace_stream(
1963 &mut self,
1964 stream: &mut NamespaceStream,
1965 msg: &ControlMessage,
1966 ) -> Result<(), ConnectionError> {
1967 let any = AnyControlMessage::Draft16(msg.clone());
1968 let raw = stream.send.write_control(&any).await?;
1969 self.emit(ClientEvent::ControlMessage {
1970 direction: Direction::Send,
1971 message: any,
1972 stream_id: Some(stream.stream_id),
1973 raw: Some(raw),
1974 });
1975 Ok(())
1976 }
1977
1978 /// Accept the peer's SUBSCRIBE_NAMESPACE with a REQUEST_OK on its own
1979 /// stream.
1980 ///
1981 /// Section 6.1: "the publisher MUST send a single REQUEST_OK or
1982 /// REQUEST_ERROR as the first message on the bidirectional stream in
1983 /// response to a SUBSCRIBE_NAMESPACE." The Request ID is taken from the
1984 /// stream rather than from the caller, which is what makes the correlation
1985 /// unforgeable.
1986 ///
1987 /// The endpoint goes first and the message is written only if it agrees.
1988 ///
1989 /// # Errors
1990 ///
1991 /// [`ConnectionError::NotOursToAnswer`] if `stream` was opened by this
1992 /// endpoint, and [`ConnectionError::Endpoint`] if the subscription has
1993 /// already been answered or has ended.
1994 pub async fn respond_ok_on_namespace_stream(
1995 &mut self,
1996 stream: &mut NamespaceStream,
1997 parameters: Vec<KeyValuePair>,
1998 ) -> Result<(), ConnectionError> {
1999 self.respond_on_namespace_stream(
2000 stream,
2001 ControlMessage::RequestOk(RequestOk { request_id: stream.request_id, parameters }),
2002 )
2003 .await
2004 }
2005
2006 /// Refuse the peer's SUBSCRIBE_NAMESPACE with a REQUEST_ERROR on its own
2007 /// stream, and finish the stream.
2008 ///
2009 /// Section 9.25 says what follows the refusal: "If it is an error, the
2010 /// stream will be immediately closed via FIN." So this writes and then
2011 /// finishes, and the handle is closed when it returns.
2012 ///
2013 /// # Errors
2014 ///
2015 /// As [`respond_ok_on_namespace_stream`](Self::respond_ok_on_namespace_stream).
2016 pub async fn respond_error_on_namespace_stream(
2017 &mut self,
2018 stream: &mut NamespaceStream,
2019 error_code: VarInt,
2020 retry_interval: VarInt,
2021 reason_phrase: Vec<u8>,
2022 ) -> Result<(), ConnectionError> {
2023 self.respond_on_namespace_stream(
2024 stream,
2025 ControlMessage::RequestError(RequestError {
2026 request_id: stream.request_id,
2027 error_code,
2028 retry_interval,
2029 reason_phrase,
2030 }),
2031 )
2032 .await?;
2033 stream.finish().await
2034 }
2035
2036 /// Drive the endpoint, then write the answer.
2037 ///
2038 /// The order is the one every request path here uses: a caller acts on a
2039 /// stream after the endpoint has accepted the step, never before.
2040 async fn respond_on_namespace_stream(
2041 &mut self,
2042 stream: &mut NamespaceStream,
2043 msg: ControlMessage,
2044 ) -> Result<(), ConnectionError> {
2045 if stream.origin != RequestOrigin::Peer {
2046 return Err(ConnectionError::NotOursToAnswer(stream.request_id.into_inner()));
2047 }
2048 // Which of the two answers this is, and the code it carries, are both
2049 // in the message, so nothing else has to be told them.
2050 let refusal_code = match &msg {
2051 ControlMessage::RequestError(e) => Some(e.error_code),
2052 _ => None,
2053 };
2054 let driven = self.endpoint.respond_on_namespace_stream(stream.request_id, refusal_code);
2055 driven.map_err(|e| self.close_if_session_fatal(e))?;
2056 let any = AnyControlMessage::Draft16(msg);
2057 let raw = stream.send.write_control(&any).await?;
2058 stream.responded = true;
2059 self.emit(ClientEvent::ControlMessage {
2060 direction: Direction::Send,
2061 message: any,
2062 stream_id: Some(stream.stream_id),
2063 raw: Some(raw),
2064 });
2065 Ok(())
2066 }
2067
2068 /// Withdraw a namespace subscription by resetting its stream: record it at
2069 /// the endpoint, then reset.
2070 ///
2071 /// Section 6.1 puts the withdrawal at the stream — "A SUBSCRIBE_NAMESPACE
2072 /// can be cancelled by closing the stream with either a FIN or
2073 /// RESET_STREAM" — while the subscription's own state lives in the
2074 /// endpoint, so the two have to move together. This and
2075 /// [`finish_namespace_stream`](Self::finish_namespace_stream) are the only
2076 /// places that move both.
2077 ///
2078 /// The endpoint goes first and the stream is reset only if it agrees. A
2079 /// refused withdrawal therefore leaves the stream exactly as it was, and
2080 /// [`NamespaceStream::cancel`] is still there for a caller that wants the
2081 /// stream reset regardless.
2082 ///
2083 /// Idempotent from both ends: a subscription that has already ended
2084 /// accepts it and stays where it is, and a handle that is already closed
2085 /// resets nothing a second time.
2086 ///
2087 /// # Errors
2088 ///
2089 /// [`ConnectionError::Endpoint`] if no namespace subscription carries this
2090 /// stream's id or nothing was ever written on it, and
2091 /// [`ConnectionError::Transport`] if `code` is outside the QUIC varint
2092 /// range — see [`NamespaceStream::cancel`], which is what sends it.
2093 pub fn cancel_namespace_stream(
2094 &mut self,
2095 stream: &mut NamespaceStream,
2096 code: u64,
2097 ) -> Result<(), ConnectionError> {
2098 let recorded = self.endpoint.cancel_namespace_subscription(stream.request_id);
2099 recorded.map_err(|e| self.close_if_session_fatal(e))?;
2100 stream.cancel(code)
2101 }
2102
2103 /// Withdraw a namespace subscription by finishing its stream: record it at
2104 /// the endpoint, then FIN.
2105 ///
2106 /// The other form Section 6.1 allows, and the one that needs no error
2107 /// code. See [`cancel_namespace_stream`](Self::cancel_namespace_stream)
2108 /// for the ordering and the idempotence, which are the same.
2109 pub async fn finish_namespace_stream(
2110 &mut self,
2111 stream: &mut NamespaceStream,
2112 ) -> Result<(), ConnectionError> {
2113 let recorded = self.endpoint.cancel_namespace_subscription(stream.request_id);
2114 recorded.map_err(|e| self.close_if_session_fatal(e))?;
2115 stream.finish().await
2116 }
2117
2118 /// Wait for the peer to reset this subscription's stream, and record it if
2119 /// it does.
2120 ///
2121 /// [`NamespaceStream::peer_cancelled`] with the endpoint's record
2122 /// attached. A caller applying backpressure is deliberately not calling
2123 /// [`recv_on_namespace_stream`](Self::recv_on_namespace_stream), which is
2124 /// the other place a peer reset surfaces, so without this the subscription
2125 /// would end on the wire and stay open in the endpoint's record for as
2126 /// long as the backpressure lasts.
2127 ///
2128 /// It sees a reset and not a FIN — see
2129 /// [`NamespaceStream::peer_cancelled`]. Cancel-safe, and it grants no
2130 /// flow-control credit.
2131 pub async fn peer_cancelled_on_namespace_stream(
2132 &mut self,
2133 stream: &mut NamespaceStream,
2134 ) -> Result<Option<u64>, ConnectionError> {
2135 let code = stream.peer_cancelled().await?;
2136 if code.is_some() {
2137 // Discarded for the reason the read path discards it: the peer has
2138 // ended the subscription whatever the record said, and a state
2139 // error here would replace the answer the caller asked for.
2140 let _ = self.endpoint.cancel_namespace_subscription(stream.request_id);
2141 }
2142 Ok(code)
2143 }
2144
2145 /// Send a PUBLISH_NAMESPACE and return the request ID.
2146 pub async fn publish_namespace(
2147 &mut self,
2148 track_namespace: TrackNamespace,
2149 parameters: Vec<KeyValuePair>,
2150 ) -> Result<VarInt, ConnectionError> {
2151 let (req_id, msg) = self.endpoint.publish_namespace(track_namespace, parameters)?;
2152 self.send_control(&msg).await?;
2153 Ok(req_id)
2154 }
2155
2156 /// Accept a request the peer opened, sending REQUEST_OK.
2157 ///
2158 /// The endpoint refuses a second answer to one request, so nothing is
2159 /// written on the wire when it does. On this draft an announcement and a
2160 /// track status are the requests REQUEST_OK accepts; a subscription, a
2161 /// publication and a fetch each have an acceptance of their own that
2162 /// carries more than this one can.
2163 pub async fn request_ok(
2164 &mut self,
2165 request_id: VarInt,
2166 parameters: Vec<KeyValuePair>,
2167 ) -> Result<(), ConnectionError> {
2168 let msg = self.endpoint.send_request_ok(request_id, parameters)?;
2169 self.send_control(&msg).await
2170 }
2171
2172 /// Revoke an acceptance, sending PUBLISH_NAMESPACE_CANCEL.
2173 ///
2174 /// The endpoint refuses one for an announcement it never accepted, so
2175 /// nothing is written on the wire when it does.
2176 pub async fn publish_namespace_cancel(
2177 &mut self,
2178 request_id: VarInt,
2179 error_code: VarInt,
2180 reason_phrase: Vec<u8>,
2181 ) -> Result<(), ConnectionError> {
2182 let msg = self.endpoint.publish_namespace_cancel(request_id, error_code, reason_phrase)?;
2183 self.send_control(&msg).await
2184 }
2185
2186 /// Withdraw an announcement this endpoint made, sending
2187 /// PUBLISH_NAMESPACE_DONE.
2188 ///
2189 /// The mirror of [`Self::publish_namespace`], and the counterpart of
2190 /// [`Self::publish_namespace_cancel`]: this one ends an announcement of
2191 /// this endpoint's, that one revokes the acceptance of one the peer made.
2192 pub async fn publish_namespace_done(
2193 &mut self,
2194 request_id: VarInt,
2195 ) -> Result<(), ConnectionError> {
2196 let msg = self.endpoint.publish_namespace_done(request_id)?;
2197 self.send_control(&msg).await
2198 }
2199 // -- Track Status flow ------------------------------------------
2200
2201 /// Send a TRACK_STATUS and return the allocated request ID.
2202 pub async fn track_status(
2203 &mut self,
2204 track_namespace: TrackNamespace,
2205 track_name: Vec<u8>,
2206 parameters: Vec<KeyValuePair>,
2207 ) -> Result<VarInt, ConnectionError> {
2208 let (req_id, msg) = self.endpoint.track_status(track_namespace, track_name, parameters)?;
2209 self.send_control(&msg).await?;
2210 Ok(req_id)
2211 }
2212
2213 // -- Publish flow (publisher side) ------------------------------
2214
2215 /// Send a PUBLISH and return the allocated request ID.
2216 pub async fn publish(
2217 &mut self,
2218 track_namespace: TrackNamespace,
2219 track_name: Vec<u8>,
2220 track_alias: VarInt,
2221 track_extensions: Vec<KeyValuePair>,
2222 parameters: Vec<KeyValuePair>,
2223 ) -> Result<VarInt, ConnectionError> {
2224 let (req_id, msg) = self.endpoint.publish(
2225 track_namespace,
2226 track_name,
2227 track_alias,
2228 track_extensions,
2229 parameters,
2230 )?;
2231 self.send_control(&msg).await?;
2232 Ok(req_id)
2233 }
2234
2235 /// Send a PUBLISH_DONE for the given request ID.
2236 pub async fn publish_done(
2237 &mut self,
2238 request_id: VarInt,
2239 status_code: VarInt,
2240 stream_count: VarInt,
2241 reason_phrase: Vec<u8>,
2242 ) -> Result<(), ConnectionError> {
2243 let msg = self.endpoint.send_publish_done(
2244 request_id,
2245 status_code,
2246 stream_count,
2247 reason_phrase,
2248 )?;
2249 self.send_control(&msg).await
2250 }
2251
2252 // -- Data streams -----------------------------------------------
2253
2254 /// Open a new unidirectional stream for sending subgroup data.
2255 pub async fn open_subgroup_stream(
2256 &self,
2257 header: &AnySubgroupHeader,
2258 ) -> Result<FramedSendStream, ConnectionError> {
2259 let send = self.transport.open_uni().await?;
2260 let mut framed = FramedSendStream::new(send, self.draft);
2261 let sid = framed.stream_id();
2262 framed.write_subgroup_header(header).await?;
2263 self.emit(ClientEvent::StreamOpened {
2264 direction: Direction::Send,
2265 stream_kind: StreamKind::Subgroup,
2266 stream_id: sid,
2267 });
2268 self.emit(ClientEvent::DataStreamHeader {
2269 stream_id: sid,
2270 direction: Direction::Send,
2271 header: header.clone(),
2272 });
2273 Ok(framed)
2274 }
2275
2276 /// Open a new unidirectional stream for sending a FETCH's objects.
2277 ///
2278 /// The objects answering a FETCH do not go on the request's own stream:
2279 /// they go on a unidirectional stream of their own, which opens with a
2280 /// FETCH_HEADER naming the request they belong to. This writes that header
2281 /// and hands back the stream, the same way
2282 /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
2283 /// subgroup.
2284 ///
2285 /// The caller owns the stream that comes back. Nothing here remembers
2286 /// which request it belongs to, so an endpoint serving several fetches at
2287 /// once keeps its own map from Request ID to stream.
2288 pub async fn open_fetch_stream(
2289 &self,
2290 header: &AnyFetchHeader,
2291 ) -> Result<FramedSendStream, ConnectionError> {
2292 let send = self.transport.open_uni().await?;
2293 let mut framed = FramedSendStream::new(send, self.draft);
2294 let sid = framed.stream_id();
2295 framed.write_fetch_header(header).await?;
2296 self.emit(ClientEvent::StreamOpened {
2297 direction: Direction::Send,
2298 stream_kind: StreamKind::Fetch,
2299 stream_id: sid,
2300 });
2301 Ok(framed)
2302 }
2303
2304 /// Accept the next unidirectional stream and read its fetch header.
2305 ///
2306 /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
2307 /// are separate because the header decides how every object after it is
2308 /// framed, so a caller has to know which it is expecting before the first
2309 /// byte is read.
2310 ///
2311 /// Objects come off the returned stream with
2312 /// [`FramedRecvStream::read_fetch_object`].
2313 pub async fn accept_fetch_stream(
2314 &self,
2315 ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
2316 let recv = self.transport.accept_uni().await?;
2317 let mut framed = FramedRecvStream::new(recv, self.draft);
2318 let sid = framed.stream_id();
2319 let header = framed.read_fetch_header().await?;
2320 self.emit(ClientEvent::StreamOpened {
2321 direction: Direction::Receive,
2322 stream_kind: StreamKind::Fetch,
2323 stream_id: sid,
2324 });
2325 self.emit(ClientEvent::FetchStreamHeader {
2326 stream_id: sid,
2327 direction: Direction::Receive,
2328 header: header.clone(),
2329 });
2330 // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
2331 // carries an `AnySubgroupHeader` and cannot express one. What
2332 // `accept_subgroup_stream` does beyond this - the forwarding-preference
2333 // note, the object measurement - is about a subgroup and has no
2334 // counterpart on a fetch stream.
2335 Ok((header, framed))
2336 }
2337
2338 /// Accept an incoming unidirectional data stream and read its subgroup
2339 /// header.
2340 pub async fn accept_subgroup_stream(
2341 &self,
2342 ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
2343 let recv = self.transport.accept_uni().await?;
2344 let mut framed = FramedRecvStream::new(recv, self.draft);
2345 let sid = framed.stream_id();
2346 let header = framed.read_subgroup_header().await?;
2347 self.emit(ClientEvent::StreamOpened {
2348 direction: Direction::Receive,
2349 stream_kind: StreamKind::Subgroup,
2350 stream_id: sid,
2351 });
2352 self.emit(ClientEvent::DataStreamHeader {
2353 stream_id: sid,
2354 direction: Direction::Receive,
2355 header: header.clone(),
2356 });
2357 // The track is resolved here and not inside the stream: it takes the
2358 // endpoint's alias table, which a stream handle has no way back to.
2359 // Handed over rather than offered, so measuring is not something a
2360 // caller has to remember to ask for.
2361 if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
2362 framed.measure_objects_against(objects, header.group_id());
2363 }
2364 Ok((header, framed))
2365 }
2366
2367 /// Send an object via datagram.
2368 ///
2369 /// The header goes through `AnyDatagramHeader::encode`, which refuses a
2370 /// header whose Object Status the framing it names cannot carry. Such a
2371 /// header errors here and nothing is sent, rather than going out as an
2372 /// ordinary payload datagram with the status quietly dropped.
2373 pub fn send_datagram(
2374 &self,
2375 header: &AnyDatagramHeader,
2376 payload: &[u8],
2377 ) -> Result<(), ConnectionError> {
2378 let mut buf = Vec::new();
2379 header.encode(&mut buf)?;
2380 buf.extend_from_slice(payload);
2381 self.emit(ClientEvent::DatagramReceived {
2382 direction: Direction::Send,
2383 header: header.clone(),
2384 payload_len: payload.len(),
2385 });
2386 self.transport.send_datagram(bytes::Bytes::from(buf))?;
2387 Ok(())
2388 }
2389
2390 /// Receive a datagram and decode it whole.
2391 ///
2392 /// An extension block announced with a length of 0 is returned as
2393 /// [`ConnectionError::EmptyDatagramExtensions`], which
2394 /// [`Connection::close_for_data_stream`] closes the session over. Extension
2395 /// headers on a status other than Normal are returned as
2396 /// [`ConnectionError::ExtensionsOnNonNormalStatus`], which it also closes over.
2397 ///
2398 /// The codec's `DatagramHeader::decode_object` then reads the whole datagram,
2399 /// and its refusals are returned as [`ConnectionError::Codec`]: an extension
2400 /// block that does not parse, a payload after a status, and a Prior Group ID
2401 /// Gap or Prior Object ID Gap fault. A gap fault,
2402 /// [`CodecError::MalformedTrack`], and an extension block that does not
2403 /// parse, [`CodecError::MalformedObjectProperties`] (draft-16 Section 11.2),
2404 /// make the track malformed, and this withdraws from it before returning,
2405 /// as draft-16 Section 2.4.2 requires: "When a subscriber detects a
2406 /// Malformed Track, it MUST UNSUBSCRIBE any subscription and FETCH_CANCEL
2407 /// any fetch for that Track from that publisher, and SHOULD deliver an
2408 /// error to the application." The session stays open.
2409 pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
2410 let data = self.transport.recv_datagram().await?;
2411 let mut cursor = &data[..];
2412 let header = match AnyDatagramHeader::decode(self.draft, &mut cursor) {
2413 Ok(header) => header,
2414 Err(CodecError::InvalidField) => {
2415 return Err(empty_datagram_extensions(&data)
2416 .unwrap_or(ConnectionError::Codec(CodecError::InvalidField)));
2417 }
2418 Err(err) => return Err(err.into()),
2419 };
2420 let consumed = data.len() - cursor.len();
2421 let payload = data.slice(consumed..);
2422 self.emit(ClientEvent::DatagramReceived {
2423 direction: Direction::Receive,
2424 header: header.clone(),
2425 payload_len: payload.len(),
2426 });
2427 // Refutable only in a build with more than one draft enabled.
2428 #[allow(irrefutable_let_patterns)]
2429 if let AnyDatagramHeader::Draft16(h) = &header {
2430 // draft-16 Section 10.2.1.2: "If an endpoint receives extension
2431 // headers on Objects with status that is not Normal, it MUST close
2432 // the session with a PROTOCOL_VIOLATION." The header decoder
2433 // reports it through `extensions_permitted` and does not refuse it.
2434 if !h.extensions_permitted() {
2435 return Err(ConnectionError::ExtensionsOnNonNormalStatus {
2436 object_id: h.object_id.into_inner(),
2437 extensions_len: h.extension_headers.len(),
2438 // Not permitted only beside a status that is present.
2439 status: h
2440 .object_status
2441 .unwrap_or(moqtap_codec::draft16::types::ObjectStatus::Normal),
2442 });
2443 }
2444 let whole =
2445 moqtap_codec::draft16::data_stream::DatagramHeader::decode_object(&mut &data[..]);
2446 if let Err(err) = whole {
2447 if let Some(condition) =
2448 malformed_track_condition(&ConnectionError::Codec(err.clone()))
2449 {
2450 self.withdraw_malformed_track(header.meta().track_alias, condition).await;
2451 }
2452 return Err(err.into());
2453 }
2454 }
2455 // A datagram is a whole object, so the connection can measure it
2456 // without help from the caller - and answer the condition itself,
2457 // because an UNSUBSCRIBE takes the connection an object on a stream
2458 // cannot reach.
2459 let meta = header.meta();
2460 if let Err(err) = self.endpoint.note_received_object(
2461 meta.track_alias,
2462 ObjectLocation { group: meta.group_id, object: meta.object_id },
2463 object_role(meta.status),
2464 ) {
2465 self.withdraw_malformed_track(
2466 meta.track_alias,
2467 MalformedTrackCondition::ObjectPastFinalObject,
2468 )
2469 .await;
2470 return Err(err.into());
2471 }
2472 Ok((header, payload))
2473 }
2474
2475 /// Close the session on the wire when the endpoint says a violation is
2476 /// fatal to it, and hand the error back unchanged.
2477 ///
2478 /// [`EndpointError::session_error_code`] answers `Some` for exactly the
2479 /// errors this draft ends the session over, and the endpoint has already
2480 /// moved its own state machine to Closed by the time this runs. Without
2481 /// this step that move is purely internal: the local endpoint refuses to
2482 /// start anything new while the peer, which is the one that broke the
2483 /// rule, sees a session that is still open and goes on sending. A rule
2484 /// that names a session termination code is a statement about the wire,
2485 /// so it takes a CONNECTION_CLOSE to satisfy it.
2486 ///
2487 /// The reason phrase is the error's own `Display` text, which names the
2488 /// rule rather than repeating the numeric code the close already carries.
2489 ///
2490 /// Errors that answer `None` are recoverable and nothing is sent.
2491 fn close_for(&self, err: &EndpointError) {
2492 if let Some(code) = err.session_error_code() {
2493 // QUIC application error codes are 62-bit; every code in this
2494 // registry is far below `u32::MAX`, and saturating rather than
2495 // truncating means a future code that is not could never be
2496 // reported as a different, assigned one.
2497 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2498 self.close(wire_code, err.to_string().as_bytes());
2499 }
2500 }
2501
2502 /// [`close_for`](Self::close_for), then the error unchanged, for the
2503 /// common case where the endpoint's error is also what the caller returns.
2504 fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
2505 self.close_for(&err);
2506 ConnectionError::Endpoint(err)
2507 }
2508
2509 /// Send the messages Section 2.4.2 asks for when a track is found
2510 /// Send the messages Section 2.4.2 asks for when a track is found
2511 /// malformed, and stop at the first one the control stream refuses.
2512 ///
2513 /// "When a subscriber detects a Malformed Track, it MUST UNSUBSCRIBE any
2514 /// subscription and FETCH_CANCEL any fetch for that Track from that
2515 /// publisher" - one message per request, in Request ID order, and the
2516 /// endpoint decides which message each request takes.
2517 ///
2518 /// A write that fails is not reported. The caller is on its way to
2519 /// returning an error that says what went wrong with the track, and a
2520 /// control stream that will not take an UNSUBSCRIBE is a session on its way
2521 /// out for a reason of its own; replacing the condition's report with a
2522 /// transport error would lose the only account of why the track was
2523 /// withdrawn. The rest of the withdrawal is abandoned, because a stream
2524 /// that refused one message will refuse the next.
2525 async fn withdraw_malformed_track(&self, alias: u64, condition: MalformedTrackCondition) {
2526 for msg in self.endpoint.withdraw_malformed_track(alias, condition) {
2527 if self.send_control(&msg).await.is_err() {
2528 break;
2529 }
2530 }
2531 }
2532
2533 /// [`Self::withdraw_malformed_track`] for the track the FETCH `request_id`
2534 /// asked for.
2535 async fn withdraw_malformed_fetch_track(
2536 &self,
2537 request_id: u64,
2538 condition: MalformedTrackCondition,
2539 ) {
2540 for msg in self.endpoint.withdraw_malformed_fetch_track(request_id, condition) {
2541 if self.send_control(&msg).await.is_err() {
2542 break;
2543 }
2544 }
2545 }
2546
2547 /// Withdraw from a track a data stream found malformed, reporting whether
2548 /// it did.
2549 ///
2550 /// The Malformed Track twin of [`Connection::close_for_data_stream`], and
2551 /// separate from it for the same reason and one more. The same one: a
2552 /// [`FramedRecvStream`] holds no connection, so the reader that finds the
2553 /// fault is not the object that can send an UNSUBSCRIBE. The one more: the
2554 /// two answers are opposites - that call ends the session, this one gives
2555 /// up a track and leaves it running - and a single entry point would have
2556 /// to decide between them from the error alone, which is exactly the
2557 /// decision a caller reproducing a capture wants to make itself.
2558 ///
2559 /// The datagram path needs none of this. It is read through the connection,
2560 /// so [`Connection::recv_datagram`] answers the condition where it finds
2561 /// it, and this is only for the objects that arrive on a stream the caller
2562 /// holds.
2563 pub async fn withdraw_for_data_stream(&self, err: &ConnectionError) -> bool {
2564 let ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) = err
2565 else {
2566 return false;
2567 };
2568 self.withdraw_malformed_track(*alias, MalformedTrackCondition::ObjectPastFinalObject).await;
2569 true
2570 }
2571
2572 /// [`Connection::withdraw_for_data_stream`] for an error read from the
2573 /// subgroup stream `header` opened, which also withdraws from the track
2574 /// over the [`CodecError::MalformedTrack`] that
2575 /// [`FramedRecvStream::read_subgroup_object`] returns for a Prior Group ID
2576 /// Gap or Prior Object ID Gap fault, and over the
2577 /// [`CodecError::MalformedObjectProperties`] it returns for an extension
2578 /// block that does not parse (draft-16 Section 11.2). Those errors name no
2579 /// track, so the header supplies its alias.
2580 ///
2581 /// draft-16 Section 2.4.2: "When a subscriber detects a Malformed Track, it
2582 /// MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for that
2583 /// Track from that publisher, and SHOULD deliver an error to the
2584 /// application."
2585 pub async fn withdraw_for_subgroup_stream(
2586 &self,
2587 header: &AnySubgroupHeader,
2588 err: &ConnectionError,
2589 ) -> bool {
2590 let Some(condition) = malformed_track_condition(err) else {
2591 return self.withdraw_for_data_stream(err).await;
2592 };
2593 self.withdraw_malformed_track(header.track_alias(), condition).await;
2594 true
2595 }
2596
2597 /// [`Connection::withdraw_for_data_stream`] for an error read from the fetch
2598 /// stream `header` opened, which also withdraws from the track the FETCH
2599 /// asked for over the [`CodecError::MalformedTrack`] that the codec's
2600 /// `FetchObjectReader::resolve` returns for a Prior Group ID Gap or Prior
2601 /// Object ID Gap fault, and over the
2602 /// [`CodecError::MalformedObjectProperties`] it returns for an extension
2603 /// block that does not parse (draft-16 Section 11.2). Those errors name no
2604 /// track, so the header's Request ID supplies it.
2605 ///
2606 /// This draft's [`FramedRecvStream::read_fetch_object`] does not resolve an
2607 /// Object's Location, so the caller passes the error `resolve` returned as
2608 /// [`ConnectionError::Codec`].
2609 ///
2610 /// draft-16 Section 2.4.2: "When a subscriber detects a Malformed Track, it
2611 /// MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for that
2612 /// Track from that publisher, and SHOULD deliver an error to the
2613 /// application."
2614 pub async fn withdraw_for_fetch_stream(
2615 &self,
2616 header: &AnyFetchHeader,
2617 err: &ConnectionError,
2618 ) -> bool {
2619 let Some(condition) = malformed_track_condition(err) else {
2620 return self.withdraw_for_data_stream(err).await;
2621 };
2622 self.withdraw_malformed_fetch_track(header.request_id(), condition).await;
2623 true
2624 }
2625
2626 /// Close the session when a failure raised while reading a *data* stream is
2627 /// one draft-16 answers with a close. Reports whether it closed.
2628 ///
2629 /// [`accept_subgroup_stream`](Self::accept_subgroup_stream) hands the caller
2630 /// a [`FramedRecvStream`], which holds no connection and so cannot close
2631 /// one, and the read that raises this failure happens there. The caller is
2632 /// the only party holding both halves, which is what this is for.
2633 ///
2634 /// Splitting it this way rather than closing inside the reader keeps a
2635 /// caller that is deliberately permissive — a tool reproducing a capture,
2636 /// say — able to read a violating stream and report it without tearing the
2637 /// session down. The rule is stated at endpoints, and this is where an
2638 /// endpoint decides it is one.
2639 ///
2640 /// Answers the extension-header rule of Section 10.2.1.2, and any decode
2641 /// failure `codec_session_error_code` recognises, so a rule is answered
2642 /// with one code whichever stream carried it.
2643 pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
2644 use crate::above_codec_rules::DraftSpecificCause;
2645
2646 match err {
2647 ConnectionError::Codec(inner) => {
2648 let Some(code) = Self::codec_session_error_code(inner) else { return false };
2649 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2650 self.close(wire_code, inner.to_string().as_bytes());
2651 true
2652 }
2653 // Not a `Codec` failure: the codec refuses such an Object as a bare
2654 // `InvalidField`, which `read_subgroup_object` narrows to this
2655 // crate's own variant, so the mapping table above never sees it.
2656 //
2657 // The code comes from `draft_specific_cause` rather than from a
2658 // constant here, so this draft's reading of its own sentence is
2659 // written down once and a caller who reads the error as a value
2660 // sees the same code the peer was sent.
2661 ConnectionError::ExtensionsOnNonNormalStatus { .. } => {
2662 let Some(DraftSpecificCause::PeerViolation { close: Some(code), .. }) =
2663 Self::draft_specific_cause(err)
2664 else {
2665 return false;
2666 };
2667 // Saturate rather than truncate, so a future code above
2668 // `u32::MAX` is never reported as a different assigned one.
2669 self.close(u32::try_from(code).unwrap_or(u32::MAX), err.to_string().as_bytes());
2670 true
2671 }
2672 // Not a `Codec` failure: the codec refuses the datagram as a bare
2673 // `InvalidField`, which `recv_datagram` names, so the code comes
2674 // from `draft_specific_cause`.
2675 ConnectionError::EmptyDatagramExtensions { .. } => {
2676 let Some(crate::above_codec_rules::DraftSpecificCause::Codec {
2677 close: Some(code),
2678 ..
2679 }) = Self::draft_specific_cause(err)
2680 else {
2681 return false;
2682 };
2683 self.close(u32::try_from(code).unwrap_or(u32::MAX), err.to_string().as_bytes());
2684 true
2685 }
2686 _ => false,
2687 }
2688 }
2689
2690 // -- Accessors --------------------------------------------------
2691
2692 /// Access the underlying endpoint state machine.
2693 pub fn endpoint(&self) -> &Endpoint {
2694 &self.endpoint
2695 }
2696
2697 /// Mutable access to the endpoint state machine.
2698 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
2699 &mut self.endpoint
2700 }
2701
2702 /// The SETUP message the server answered the handshake with.
2703 ///
2704 /// `SERVER_SETUP` through draft-16, the server's half of the unified
2705 /// `SETUP` from draft-17. [`AnyControlMessage::fields`] renders it under
2706 /// this draft's own parameter names, in the order they arrived.
2707 pub fn server_setup(&self) -> &AnyControlMessage {
2708 &self.server_setup
2709 }
2710
2711 /// The framed wire bytes of [`Self::server_setup`], as they arrived.
2712 ///
2713 /// Kept beside the decoded form because the encoding is evidence the
2714 /// decoding discards: two relays sending the same parameter can still
2715 /// disagree on how wide a varint they wrote it in.
2716 pub fn server_setup_raw(&self) -> Option<&[u8]> {
2717 self.server_setup_raw.as_deref()
2718 }
2719
2720 /// Returns the draft version this connection is using.
2721 pub fn draft(&self) -> DraftVersion {
2722 self.draft
2723 }
2724
2725 /// Which of this draft's *own* `ConnectionError` variants this error is,
2726 /// and which kind of thing it says.
2727 ///
2728 /// The ten every draft carries answer `None` here: [`AnyConnectionError`]
2729 /// classifies those itself, once, and never asks a draft about them. What
2730 /// is left splits two ways, and the split is the reason this function
2731 /// exists — before it, both halves reached a caller as a sentence and read
2732 /// exactly alike. A [`LocalRefusal`] is this endpoint declining to write
2733 /// something, so nothing reached the wire and no relay is implicated; a
2734 /// [`PeerViolation`] is a peer having done something draft-16 forbids, and
2735 /// carries the session error code draft-16's own text answers it with.
2736 ///
2737 /// Matched exhaustively, with no wildcard arm and deliberately so: a
2738 /// variant added to this draft's error type has to arrive here as a compile
2739 /// error, beside the doc comment quoting the sentence it enforces, rather
2740 /// than as a silent [`ErrorCause::Unclassified`] in the facade.
2741 ///
2742 /// [`AnyConnectionError`]: crate::dispatch::AnyConnectionError
2743 /// [`ErrorCause::Unclassified`]: crate::dispatch::ErrorCause::Unclassified
2744 /// [`LocalRefusal`]: crate::above_codec_rules::DraftSpecificCause::LocalRefusal
2745 /// [`PeerViolation`]: crate::above_codec_rules::DraftSpecificCause::PeerViolation
2746 pub fn draft_specific_cause(
2747 err: &ConnectionError,
2748 ) -> Option<crate::above_codec_rules::DraftSpecificCause> {
2749 use crate::above_codec_rules::{AboveCodecRule, DraftSpecificCause};
2750 use moqtap_codec::draft16::error_codes::SessionErrorCode;
2751
2752 match err {
2753 ConnectionError::Endpoint(_)
2754 | ConnectionError::Codec(_)
2755 | ConnectionError::Transport(_)
2756 | ConnectionError::VarInt(_)
2757 | ConnectionError::NoControlStream
2758 | ConnectionError::UnexpectedEnd
2759 | ConnectionError::StreamFinished
2760 | ConnectionError::InvalidAddress(_)
2761 | ConnectionError::TlsConfig(_)
2762 | ConnectionError::DataStreamState(_) => None,
2763 // This build decoding a message and then failing to narrow it to
2764 // its own draft. Nothing reached the wire and no peer is
2765 // implicated, which is the whole reason it is not
2766 // `ConnectionError::Codec`: under that name it would carry
2767 // `Some(PROTOCOL_VIOLATION)` out of `codec_session_error_code` and
2768 // publish a relay for this build's defect. See the variant's own
2769 // doc.
2770 ConnectionError::ControlMessageNarrowing => {
2771 Some(crate::above_codec_rules::DraftSpecificCause::LocalRefusal)
2772 }
2773 // Section 3.3: a bidirectional stream may begin with CLIENT_SETUP
2774 // or SUBSCRIBE_NAMESPACE and nothing else, "unless negotiated. If
2775 // they do, the peer MUST close the Session with a Protocol
2776 // Violation." The session has already been closed on the wire by
2777 // the time this is returned, so the code is carried here for a
2778 // caller to read which rule was answered, not for it to answer one
2779 // again.
2780 ConnectionError::NonSubscribeNamespaceOnBidiStream(_) => {
2781 Some(DraftSpecificCause::PeerViolation {
2782 rule: AboveCodecRule::BidiStreamOpener,
2783 close: Some(SessionErrorCode::ProtocolViolation.as_u64()),
2784 })
2785 }
2786 // A `respond_*` helper pointed at a namespace subscription this
2787 // endpoint opened. Nothing was written and no state moved: the
2788 // answer to a SUBSCRIBE_NAMESPACE is owed by whoever received it.
2789 ConnectionError::NotOursToAnswer(_) => Some(DraftSpecificCause::LocalRefusal),
2790 // Section 10.2.1.2 states the rule and names the code in the same
2791 // sentence. The codec refuses such an Object only as a bare
2792 // `InvalidField`, so this layer is the one that names the rule,
2793 // and `close_for_data_stream` performs the close by
2794 // reading this same answer.
2795 ConnectionError::ExtensionsOnNonNormalStatus { .. } => {
2796 Some(DraftSpecificCause::PeerViolation {
2797 rule: AboveCodecRule::PropertiesOnNonNormalStatus,
2798 close: Some(SessionErrorCode::ProtocolViolation.as_u64()),
2799 })
2800 }
2801 // The codec refuses the datagram as a bare `InvalidField`, and
2802 // `recv_datagram` names the rule, so `close_for_data_stream` reads
2803 // the close here. See the variant's own doc.
2804 ConnectionError::EmptyDatagramExtensions { .. } => {
2805 Some(crate::above_codec_rules::DraftSpecificCause::Codec {
2806 error: CodecError::InvalidField,
2807 close: Some(
2808 moqtap_codec::draft16::error_codes::SessionErrorCode::ProtocolViolation
2809 .as_u64(),
2810 ),
2811 })
2812 }
2813 }
2814 }
2815
2816 /// The code to close the session with when a control message could not be
2817 /// decoded because the peer broke a rule draft-16 answers with a close.
2818 ///
2819 /// Every variant listed here comes from a sentence in this draft that names
2820 /// the consequence, and the list is deliberately shorter than draft-17's:
2821 /// the bounds are per draft, and answering one this draft does not state
2822 /// would close a session over traffic a conforming peer may send.
2823 ///
2824 /// - Reason Phrase, maximum 1024 bytes: "If an endpoint receives a length
2825 /// exceeding the maximum, it MUST close the session with a
2826 /// PROTOCOL_VIOLATION."
2827 /// - KVP value, maximum 2^16-1 bytes, with the same sentence.
2828 /// - Track Namespace field count: "If an endpoint receives a Track
2829 /// Namespace consisting of 0 or greater than 32 Track Namespace Fields,
2830 /// it MUST close the session with a PROTOCOL_VIOLATION." Note the lower
2831 /// bound — an empty tuple is refused here, where drafts 17 and later
2832 /// permit it.
2833 /// - Full Track Name, maximum 4,096 bytes. This draft widened the rule from
2834 /// draft-15's: "If an endpoint receives a Track Namespace or a Full
2835 /// Track Name exceeding 4,096 bytes".
2836 /// - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
2837 /// check that there are no unexpected duplicate parameters and close the
2838 /// session as a PROTOCOL_VIOLATION"
2839 /// - A Track Namespace Field of length zero, which draft-15 does not
2840 /// state: "Each Track Namespace Field Value MUST contain at least one
2841 /// byte."
2842 /// - The delta-encoded parameter type overflow, which arrives with this
2843 /// draft along with delta encoding itself.
2844 ///
2845 /// - GOAWAY New Session URI, maximum 8,192 bytes: "If an endpoint
2846 /// receives a length exceeding the maximum, it MUST close the session
2847 /// with a PROTOCOL_VIOLATION." Every draft from 11 to 19 states it; 07
2848 /// through 10 state no maximum for the field at all.
2849 /// - Unknown control message type: "An endpoint that receives an unknown
2850 /// message type MUST close the session." All the drafts state it,
2851 /// in the same words, and the sentence names no code, so Protocol
2852 /// Violation is what carries it.
2853 ///
2854 /// `None` for everything else, including [`CodecError::InvalidField`]. That
2855 /// variant is shared by a dozen unrelated malformations, only some of which
2856 /// the draft answers with a close, so treating it as fatal would close
2857 /// sessions the draft does not ask to be closed. Splitting it is the way to
2858 /// bring the rest of those rules under this function; widening the match is
2859 /// not.
2860 pub fn codec_session_error_code(
2861 err: &CodecError,
2862 ) -> Option<moqtap_codec::draft16::error_codes::SessionErrorCode> {
2863 use moqtap_codec::draft16::error_codes::SessionErrorCode;
2864 use moqtap_codec::kvp::KvpError;
2865 match err {
2866 // The declared Length disagreeing with the fields, which every
2867 // draft answers with a close. Drafts 07 through 10 name no code for
2868 // it, so it takes the one their other unnamed rules take.
2869 // A Filter Type outside the four this draft assigns, Section 5.1.2:
2870 // "An endpoint that receives a filter type other than the above MUST
2871 // close the session with PROTOCOL_VIOLATION."
2872 //
2873 // Drafts 07 through 14 carried the Filter Type as a field of
2874 // SUBSCRIBE. From draft-15 it is the first field inside the
2875 // length-prefixed filter parameter, where a codec that carries the
2876 // value as opaque bytes never reads it — the rule did not change and
2877 // the place it has to be enforced did.
2878 CodecError::InvalidFilterType(_) => Some(SessionErrorCode::ProtocolViolation),
2879 // A filter parameter whose value is not a filter, Section 9.2.2.5:
2880 // "It is a length-prefixed Subscription Filter... If the length of
2881 // the Subscription Filter does not match the parameter length, the
2882 // publisher MUST close the session with PROTOCOL_VIOLATION."
2883 //
2884 // The one key-value malformation this draft answers with something
2885 // other than KEY_VALUE_FORMATTING_ERROR. The general rule covers the
2886 // same bytes and names that code; the sentence above is the specific
2887 // one, so it governs. Drafts 17 and later drop it and leave only the
2888 // general rule, which is why the same malformation ends a session
2889 // there under a different code.
2890 CodecError::SubscriptionFilterMalformed { .. } => {
2891 Some(SessionErrorCode::ProtocolViolation)
2892 }
2893 // A Fetch Type outside the three this draft assigns: "An endpoint
2894 // that receives a Fetch Type other than 0x1, 0x2 or 0x3 MUST close
2895 // the session with a PROTOCOL_VIOLATION." The value decides which
2896 // fields follow it — a Standalone fetch carries a track name and a
2897 // range where a joining fetch carries a Request ID and an offset —
2898 // so a reader that cannot name the type cannot find the end of the
2899 // message.
2900 CodecError::InvalidFetchType(_) => Some(SessionErrorCode::ProtocolViolation),
2901 CodecError::ControlMessageLengthMismatch { .. } => {
2902 Some(SessionErrorCode::ProtocolViolation)
2903 }
2904 CodecError::KeyDeltaOverflow(..)
2905 | CodecError::DuplicateParameter(_)
2906 | CodecError::TrackNameTooLong
2907 | CodecError::InvalidNamespaceTupleSize(_)
2908 | CodecError::ReasonPhraseTooLong
2909 | CodecError::GoAwayUriTooLong
2910 | CodecError::UnknownMessageType(_)
2911 | CodecError::Kvp(KvpError::ValueTooLong(_))
2912 | CodecError::EmptyNamespaceField => Some(SessionErrorCode::ProtocolViolation),
2913 // An unknown data-plane type, Section 10: "An endpoint that
2914 // receives an unknown stream or datagram type MUST close the
2915 // session." One sentence covering two tables, which is why both
2916 // variants sit here.
2917 // A Message Parameter whose value is outside the range its type
2918 // allows: DELIVERY_TIMEOUT in Section 9.2.2.2, FORWARD in Section
2919 // 9.2.2.8, GROUP_ORDER in Section 9.2.2.4 and SUBSCRIBER_PRIORITY in
2920 // Section 9.2.2.3.
2921 // Each states that a receiver "MUST close the session with
2922 // PROTOCOL_VIOLATION".
2923 CodecError::ParameterValueOutOfRange { .. } => {
2924 Some(SessionErrorCode::ProtocolViolation)
2925 }
2926 // A Track Extension or Track Property whose value is outside the
2927 // range its type allows: DELIVERY_TIMEOUT in Section 11.1,
2928 // DEFAULT_PUBLISHER_GROUP_ORDER in Section 11.1.1.2 and DYNAMIC_GROUPS in
2929 // Section 11.1.1.3.
2930 // Each states that a receiver "MUST close the session with
2931 // PROTOCOL_VIOLATION".
2932 //
2933 // A separate arm from the parameter rule above because the two
2934 // registries are separate: 0x22 is GROUP_ORDER as a parameter and
2935 // DEFAULT_PUBLISHER_GROUP_ORDER as a Track Extension, and a log that
2936 // named only the number would not say which.
2937 CodecError::TrackPropertyValueOutOfRange { .. } => {
2938 Some(SessionErrorCode::ProtocolViolation)
2939 }
2940 CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
2941 Some(SessionErrorCode::ProtocolViolation)
2942 }
2943 // A Type inside a form this draft defines but on a list it names as
2944 // invalid: Section 10.4.2 for a subgroup header whose SUBGROUP_ID_MODE
2945 // is the reserved 0b11, Section 10.3.1 for a datagram asking to be both
2946 // an object status and an end-of-group marker. Unlike the rule above,
2947 // these two name their code outright.
2948 CodecError::InvalidStreamTypeValue { .. }
2949 | CodecError::InvalidDatagramTypeValue { .. } => {
2950 Some(SessionErrorCode::ProtocolViolation)
2951 }
2952 // A key-value pair whose value is not the serialization its own
2953 // Type defines, Section 1.4.2: "If a receiver understands a Type,
2954 // and the following Value or Length/Value does not match the
2955 // serialization defined by that Type, the receiver MUST close the
2956 // session with error code KEY_VALUE_FORMATTING_ERROR."
2957 //
2958 // Section 9.2.2.1 states the same answer for the one structure this
2959 // draft spells out: "If the Token structure cannot be decoded, the
2960 // receiver MUST close the Session with KEY_VALUE_FORMATTING_ERROR."
2961 //
2962 // The one rule in this table that names a code other than Protocol
2963 // Violation.
2964 CodecError::KeyValueFormatting { .. } => {
2965 Some(SessionErrorCode::KeyValueFormattingError)
2966 }
2967 // A Message Parameter whose type this draft does not define, Section
2968 // 9.2: "All Message Parameters MUST be defined in the negotiated
2969 // version of MOQT or negotiated via Setup Parameters. An endpoint that
2970 // receives an unknown Message Parameter MUST close the session with
2971 // PROTOCOL_VIOLATION."
2972 //
2973 // One namespace only. This draft also says a receiver ignores an
2974 // unrecognised Setup Parameter, so an unknown type in a SETUP is carried and
2975 // the codec never raises this for one.
2976 CodecError::UnknownMessageParameter(_) => Some(SessionErrorCode::ProtocolViolation),
2977 // Everything this draft does not answer, named rather than swept up
2978 // by a wildcard. The arm is exhaustive deliberately: a new
2979 // `CodecError` variant will not compile until it has been placed on
2980 // one side or the other, on this draft, which is the decision a `_`
2981 // arm makes silently and invisibly in every draft module at once.
2982 //
2983 // Adding one variant to `CodecError` produces an `E0004` in every
2984 // draft module that matches it exhaustively, each naming the
2985 // variant that has nowhere to go. That is the whole mechanism.
2986 //
2987 // The nesting stops at `VarInt`, whose variants report how the bytes
2988 // ran out rather than a rule an endpoint states, so there is nothing
2989 // in it for a draft to answer. `Kvp` is spelled out because it does
2990 // carry one.
2991 // Neither field exists from draft-15 on. Forwarding became the
2992 // FORWARD parameter, which carries the same rule in a different
2993 // shape and is answered above under its own variant; Content Exists
2994 // became the presence or absence of a LARGEST_OBJECT parameter.
2995 CodecError::InvalidForward(_)
2996 | CodecError::InvalidContentExists(_)
2997 | CodecError::UnexpectedEnd
2998 | CodecError::MessageTooLong(_)
2999 | CodecError::VarInt(_)
3000 | CodecError::InvalidField
3001 | CodecError::InvalidRange(..)
3002 | CodecError::ParameterLengthMismatch(_)
3003 | CodecError::EndOfTrackObjectId(_)
3004 | CodecError::ParametersOutOfOrder(..)
3005 | CodecError::ObjectIdOverflow(..)
3006 | CodecError::ExtensionsOnNonExistentObject(_)
3007 | CodecError::InvalidRequiredRequestIdDelta(..)
3008 // Not `ParameterOutOfScope`, even though this draft is the one that
3009 // starts closing over an unknown Message Parameter above. The scope
3010 // rule is a separate sentence and it keeps the older answer. Section
3011 // 9.2.2: "Each message parameter definition indicates the message types
3012 // in which it can appear. If it appears in some other type of message,
3013 // it MUST be ignored." The two halves part company at draft-17, which
3014 // is where the second sentence becomes a close, so this draft carries an
3015 // out-of-scope parameter and the codec never raises the variant here.
3016 | CodecError::ParameterOutOfScope { .. }
3017 // The End Group is written out in full on this draft, so there is
3018 // nothing to add and nothing to overflow. Drafts 17 and later
3019 // replaced it with a delta measured from the Start Location's Group,
3020 // and 18 and 19 close the session when the sum leaves the range.
3021 | CodecError::FilterEndGroupOverflow { .. }
3022 // The object payload rule, Section 10.2.1.1: "Any object with a status
3023 // code other than zero MUST have an empty payload." A MUST on the
3024 // sender with no receiver action named anywhere — the "SHOULD be
3025 // treated as a protocol error" in the same paragraph belongs to the
3026 // sentence before it, which is about a status value this draft does
3027 // not assign — so an object carrying a payload it may not is refused
3028 // and the session stays open.
3029 | CodecError::PayloadNotPermitted { .. }
3030 // A property block that does not parse makes the Track malformed,
3031 // Section 11.2.
3032 | CodecError::MalformedObjectProperties { .. }
3033 // A Malformed Track (Section 2.4.2) costs the track and not the
3034 // session.
3035 | CodecError::MalformedTrack { .. }
3036 // Raised only by the draft-23 codec.
3037 | CodecError::ControlMessageExceedsLimit { .. }
3038 | CodecError::MissingErrorDetail { .. }
3039 | CodecError::MalformedErrorDetail { .. }
3040 | CodecError::TokenAliasInSetup { .. }
3041 | CodecError::EmptyTrackName
3042 | CodecError::MalformedAuthToken { .. }
3043 | CodecError::TrackPropertiesNotPermitted
3044 | CodecError::DeltaDecodingOverflow { .. }
3045 | CodecError::MalformedObject { .. }
3046 | CodecError::UnknownObjectStatus(_)
3047 | CodecError::UnsupportedDraft(_)
3048 | CodecError::Kvp(
3049 KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
3050 ) => None,
3051 }
3052 }
3053
3054 /// Close the session on the wire when a decode failure is one draft-16
3055 /// answers with a close, and hand the error back unchanged.
3056 /// Without it every bound the decoder enforces would stop at *this endpoint
3057 /// refused the frame* while the peer, which is the one that broke the rule,
3058 /// saw a session that was still open and went on sending. "MUST close the
3059 /// session with a PROTOCOL_VIOLATION" is a statement about the wire.
3060 fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
3061 if let ConnectionError::Codec(inner) = &err {
3062 if let Some(code) = Self::codec_session_error_code(inner) {
3063 // QUIC application error codes are 62-bit; every code in this
3064 // registry is far below `u32::MAX`, and saturating rather than
3065 // truncating means a future code that is not could never be
3066 // reported as a different, assigned one.
3067 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
3068 self.close(wire_code, inner.to_string().as_bytes());
3069 }
3070 }
3071 err
3072 }
3073
3074 /// Close the connection.
3075 pub fn close(&self, code: u32, reason: &[u8]) {
3076 self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
3077 self.transport.close(code, reason);
3078 }
3079}
3080
3081/// The Malformed Track condition a codec refusal on a data path is, on this
3082/// draft, or `None` for a refusal that is not one.
3083fn malformed_track_condition(err: &ConnectionError) -> Option<MalformedTrackCondition> {
3084 match err {
3085 ConnectionError::Codec(CodecError::MalformedTrack { .. }) => {
3086 Some(MalformedTrackCondition::PriorGap)
3087 }
3088 // draft-16 Section 11.2: "A Key-Value-Pair cannot be parsed."
3089 ConnectionError::Codec(CodecError::MalformedObjectProperties { .. }) => {
3090 Some(MalformedTrackCondition::MalformedPropertyBlock)
3091 }
3092 _ => None,
3093 }
3094}
3095
3096/// Determine the encoded length of a varint from its first byte.
3097fn varint_len(first_byte: u8) -> usize {
3098 1 << (first_byte >> 6)
3099}
3100
3101#[cfg(test)]
3102mod tests {
3103 use super::*;
3104
3105 /// This build failing to narrow a message it decoded is never a finding
3106 /// about the peer.
3107 ///
3108 /// The arm that raises `ControlMessageNarrowing` is unreachable — this
3109 /// draft's decoder can only hand back this draft's variant — and nothing
3110 /// pins that. What is pinned here is the half that matters.
3111 /// `CodecError::UnknownMessageType(0)` is what the arm must not raise:
3112 /// `codec_session_error_code` answers it `Some(PROTOCOL_VIOLATION)` on
3113 /// every draft in range, so the day the narrowing failed a conformance
3114 /// probe would publish a relay for sending a control message type this
3115 /// draft does not assign — with `0x00` attached as the codepoint that
3116 /// proved it, which is an accusation better evidenced than any real one
3117 /// this build makes. The section stating that rule is numbered differently
3118 /// on every draft, and the point does not turn on the number.
3119 #[test]
3120 fn a_message_this_build_could_not_narrow_names_nobody() {
3121 use crate::dispatch::{AnyConnectionError, ErrorCause};
3122
3123 let err: AnyConnectionError = ConnectionError::ControlMessageNarrowing.into();
3124 assert!(err.is_local(), "a narrowing this build could not do is this build's");
3125 assert_eq!(
3126 err.cause(),
3127 &ErrorCause::Facade,
3128 "nothing reached the wire, so there is no rule and no close code to read"
3129 );
3130 }
3131
3132 #[test]
3133 fn varint_len_single_byte() {
3134 assert_eq!(varint_len(0x00), 1);
3135 assert_eq!(varint_len(0x3F), 1);
3136 }
3137
3138 #[test]
3139 fn varint_len_two_bytes() {
3140 assert_eq!(varint_len(0x40), 2);
3141 assert_eq!(varint_len(0x7F), 2);
3142 }
3143
3144 #[test]
3145 fn varint_len_four_bytes() {
3146 assert_eq!(varint_len(0x80), 4);
3147 assert_eq!(varint_len(0xBF), 4);
3148 }
3149
3150 #[test]
3151 fn varint_len_eight_bytes() {
3152 assert_eq!(varint_len(0xC0), 8);
3153 assert_eq!(varint_len(0xFF), 8);
3154 }
3155
3156 #[test]
3157 fn client_config_alpn_quic_draft16() {
3158 let config = ClientConfig {
3159 draft: DraftVersion::Draft16,
3160 transport: TransportType::Quic,
3161 skip_cert_verification: false,
3162 ca_certs: Vec::new(),
3163 setup_parameters: Vec::new(),
3164 };
3165 assert_eq!(config.alpn(), vec![b"moqt-16".to_vec()]);
3166 }
3167
3168 #[test]
3169 fn client_config_alpn_webtransport() {
3170 let config = ClientConfig {
3171 draft: DraftVersion::Draft16,
3172 transport: TransportType::WebTransport { url: "https://example.com".to_string() },
3173 skip_cert_verification: false,
3174 ca_certs: Vec::new(),
3175 setup_parameters: Vec::new(),
3176 };
3177 assert_eq!(config.alpn(), vec![b"h3".to_vec()]);
3178 }
3179
3180 /// `MOQT_ALPN` is the ALPN a client configured for this draft offers.
3181 #[test]
3182 fn moqt_alpn_is_the_one_a_client_offers() {
3183 // Checked against what a client configured for this draft puts on the
3184 // wire, and only then against the literal.
3185 let config = ClientConfig {
3186 draft: DraftVersion::Draft16,
3187 transport: TransportType::Quic,
3188 skip_cert_verification: false,
3189 ca_certs: Vec::new(),
3190 setup_parameters: Vec::new(),
3191 };
3192 assert_eq!(
3193 config.alpn(),
3194 vec![MOQT_ALPN.to_vec()],
3195 "MOQT_ALPN is {:?}; a draft-{} client offers {:?}",
3196 String::from_utf8_lossy(MOQT_ALPN),
3197 16,
3198 config
3199 .alpn()
3200 .iter()
3201 .map(|a| String::from_utf8_lossy(a).into_owned())
3202 .collect::<Vec<_>>(),
3203 );
3204 assert_eq!(MOQT_ALPN, b"moqt-16");
3205 }
3206
3207 #[test]
3208 fn transport_type_debug() {
3209 let quic = TransportType::Quic;
3210 assert!(format!("{quic:?}").contains("Quic"));
3211
3212 let wt = TransportType::WebTransport { url: "https://example.com".to_string() };
3213 assert!(format!("{wt:?}").contains("WebTransport"));
3214 }
3215}