moqtap_client/draft14/connection.rs
1use bytes::{Buf, Bytes, BytesMut};
2
3use crate::draft14::endpoint::{Endpoint, EndpointError};
4use crate::draft14::event::{ClientEvent, Direction, StreamKind};
5use crate::draft14::observer::ConnectionObserver;
6use crate::draft14::session::request_id::Role;
7use crate::draft14::session::setup;
8use crate::forwarding_preference::ObjectForwardingPreference;
9use crate::malformed_tracks::MalformedTrackCondition;
10use crate::track_locations::{ObjectLocation, ObjectRole, TrackObjects};
11use crate::transport::{RecvStream, SendStream, Transport, TransportError};
12use moqtap_codec::dispatch::{
13 AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
14};
15use moqtap_codec::draft14::data_stream::{FetchObject, SubgroupObject, SubgroupObjectReader};
16use moqtap_codec::draft14::message::ControlMessage;
17use moqtap_codec::error::CodecError;
18use moqtap_codec::kvp::KeyValuePair;
19use moqtap_codec::types::*;
20use moqtap_codec::varint::VarInt;
21use moqtap_codec::version::DraftVersion;
22
23/// MoQT ALPN identifier (used by raw QUIC transport).
24pub const MOQT_ALPN: &[u8] = b"moq-00";
25
26/// Errors from the connection layer.
27#[derive(Debug, thiserror::Error)]
28pub enum ConnectionError {
29 /// Endpoint state machine error.
30 #[error("endpoint error: {0}")]
31 Endpoint(#[from] EndpointError),
32 /// Wire codec error.
33 #[error("codec error: {0}")]
34 Codec(#[from] CodecError),
35 /// Transport-level error.
36 #[error("transport error: {0}")]
37 Transport(#[from] TransportError),
38 /// Variable-length integer decoding error.
39 #[error("varint error: {0}")]
40 VarInt(#[from] moqtap_codec::varint::VarIntError),
41 /// Control stream was not opened.
42 #[error("control stream not open")]
43 NoControlStream,
44 /// Stream ended before a complete message was read.
45 #[error("unexpected end of stream")]
46 UnexpectedEnd,
47 /// Stream was finished by the peer.
48 #[error("stream finished")]
49 StreamFinished,
50 /// Invalid server address string.
51 #[error("invalid server address: {0}")]
52 InvalidAddress(String),
53 /// TLS configuration error.
54 #[error("TLS config error: {0}")]
55 TlsConfig(String),
56 /// Data stream used out of order (e.g. object before header).
57 #[error("data stream state error: {0}")]
58 DataStreamState(&'static str),
59 /// A control message this build decoded for draft-14 and then could not
60 /// narrow to draft-14's own message type.
61 ///
62 /// Unreachable, and that is not the same as harmless. `read_control`
63 /// decodes with this connection's own draft, so the `AnyControlMessage` it
64 /// hands back can only carry this draft's variant — but the narrowing arm
65 /// is compiled in every configuration anyway, under
66 /// `#[allow(unreachable_patterns)]` rather than a `cfg` naming the other
67 /// drafts, because such a list has to be edited in every per-draft
68 /// module whenever a draft is added and a copy that omits one leaves the
69 /// match non-exhaustive.
70 ///
71 /// Spelled as `CodecError::UnknownMessageType(0)` it would not stay inert:
72 /// every draft's
73 /// [`codec_session_error_code`](Connection::codec_session_error_code)
74 /// answers that variant `Some(PROTOCOL_VIOLATION)`. So the day the
75 /// narrowing did fail, this build's own defect would reach a caller as *the
76 /// peer sent a control message type this draft does not assign, and the
77 /// session must be closed with a Protocol Violation* — carrying `0x00` as
78 /// the codepoint that proved it. A conformance report reading that
79 /// publishes a named, well-evidenced accusation against a relay for
80 /// something no relay did.
81 ///
82 /// A variant of its own is what stops that.
83 /// [`draft_specific_cause`](Connection::draft_specific_cause) answers it
84 /// [`LocalRefusal`], the facade turns that into [`ErrorCause::Facade`], and
85 /// nothing downstream can read a rule out of a cause that says nothing
86 /// reached the wire. What is pinned is the consequence rather than the
87 /// unreachability: nothing pins the arm's reachability, which is exactly
88 /// why the consequence must not be an accusation.
89 ///
90 /// [`LocalRefusal`]: crate::above_codec_rules::DraftSpecificCause::LocalRefusal
91 /// [`ErrorCause::Facade`]: crate::dispatch::ErrorCause::Facade
92 #[error(
93 "a control message decoded for draft-14 did not narrow to draft-14: a defect in this build, and evidence about nothing the peer did"
94 )]
95 ControlMessageNarrowing,
96 /// A datagram whose Type announces extension headers arrived with an
97 /// Extension Headers Length of 0.
98 ///
99 /// draft-14 Section 10.3.1: "If an endpoint receives a datagram with
100 /// Extensions Present as "Yes" and a Extension Headers Length of 0, it MUST
101 /// close the session with PROTOCOL_VIOLATION."
102 ///
103 /// The codec's header decoder refuses such a datagram as a bare
104 /// `InvalidField`. [`Connection::recv_datagram`] names it here, and
105 /// [`Connection::close_for_data_stream`] closes the session over it.
106 #[error(
107 "the datagram for object {object_id} announces extension headers and has an Extension Headers Length of 0"
108 )]
109 EmptyDatagramExtensions {
110 /// The Object ID the datagram named.
111 object_id: u64,
112 },
113}
114
115/// [`ConnectionError::EmptyDatagramExtensions`] for a datagram the header
116/// decoder refused as `InvalidField`, when its empty extension block was the
117/// fault.
118///
119/// Every Type the draft-14 Section 10.3.1 table marks Extensions Present, with
120/// the Object ID where the table marks it present.
121fn empty_datagram_extensions(data: &[u8]) -> Option<ConnectionError> {
122 use crate::datagram_prefix::{empty_extension_block, Prefix};
123 empty_extension_block(data, |datagram_type, _| {
124 let ty = moqtap_codec::draft14::data_stream::DatagramType::from_u8(
125 u8::try_from(datagram_type).ok()?,
126 )?;
127 ty.extensions_present()
128 .then_some(Prefix { object_id: ty.object_id_present(), priority: true })
129 })
130 .map(|object_id| ConnectionError::EmptyDatagramExtensions { object_id })
131}
132
133impl From<crate::transport::DialError> for ConnectionError {
134 /// Maps a dial failure onto the variants this error already has, so a
135 /// caller matches `InvalidAddress` or `TlsConfig`.
136 ///
137 /// A `LocalSocket` failure maps to `InvalidAddress`, which the facade reads
138 /// as this side's failure
139 /// ([`ErrorCause::Facade`](crate::dispatch::ErrorCause::Facade)). The dial
140 /// phase stays on [`DialError::phase`](crate::transport::DialError::phase).
141 fn from(e: crate::transport::DialError) -> Self {
142 match e {
143 // Two variants, one arm, deliberately — see above.
144 crate::transport::DialError::InvalidAddress(s)
145 | crate::transport::DialError::LocalSocket(s) => ConnectionError::InvalidAddress(s),
146 crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
147 crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
148 }
149 }
150}
151
152/// Transport type for the connection.
153#[derive(Debug, Clone)]
154pub enum TransportType {
155 /// Raw QUIC via quinn. The `addr` field should be `host:port`.
156 Quic,
157 /// WebTransport via wtransport. The `url` field is the WebTransport URL.
158 WebTransport {
159 /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
160 url: String,
161 },
162}
163
164/// Configuration for a MoQT client connection.
165///
166/// Both `draft` and `transport` are required — there is no `Default` impl.
167pub struct ClientConfig {
168 /// The MoQT draft version to use (primary, determines codec/framing).
169 pub draft: DraftVersion,
170 /// Additional draft versions to offer in CLIENT_SETUP.
171 /// The primary `draft` is always included first.
172 pub additional_versions: Vec<DraftVersion>,
173 /// The transport type (QUIC or WebTransport).
174 pub transport: TransportType,
175 /// Whether to skip TLS certificate verification (for testing).
176 pub skip_cert_verification: bool,
177 /// Custom CA certificates to trust (DER-encoded).
178 pub ca_certs: Vec<Vec<u8>>,
179 /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
180 pub setup_parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
181}
182
183impl ClientConfig {
184 /// Returns the MoQT version varints for the CLIENT_SETUP message.
185 /// Primary draft first, then any additional versions.
186 pub fn supported_versions(&self) -> Vec<VarInt> {
187 let mut versions = vec![self.draft.version_varint()];
188 for v in &self.additional_versions {
189 let varint = v.version_varint();
190 if !versions.contains(&varint) {
191 versions.push(varint);
192 }
193 }
194 versions
195 }
196
197 /// Returns the ALPN protocol identifiers for the transport.
198 pub fn alpn(&self) -> Vec<Vec<u8>> {
199 match &self.transport {
200 TransportType::Quic => vec![self.draft.quic_alpn().to_vec()],
201 TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
202 }
203 }
204}
205
206/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
207pub struct FramedSendStream {
208 inner: SendStream,
209 draft: DraftVersion,
210 /// Stateful subgroup object encoder. Initialized by
211 /// [`FramedSendStream::write_subgroup_header`] and used by
212 /// [`FramedSendStream::write_subgroup_object`] to track the delta-encoded
213 /// object ID state.
214 subgroup_io: Option<SubgroupObjectReader>,
215}
216
217impl FramedSendStream {
218 /// Create a new framed send stream for the given draft version.
219 pub fn new(inner: SendStream, draft: DraftVersion) -> Self {
220 Self { inner, draft, subgroup_io: None }
221 }
222
223 /// Get the transport-level stream ID.
224 pub fn stream_id(&self) -> u64 {
225 self.inner.stream_id()
226 }
227
228 /// Write a control message to the stream with type+length framing.
229 /// Returns the raw bytes that were written (for event capture).
230 pub async fn write_control(
231 &mut self,
232 msg: &AnyControlMessage,
233 ) -> Result<Vec<u8>, ConnectionError> {
234 let mut buf = Vec::new();
235 msg.encode(&mut buf)?;
236 self.inner.write_all(&buf).await?;
237 Ok(buf)
238 }
239
240 /// Write a subgroup stream header. Also initializes the internal
241 /// delta-encoding state used by [`FramedSendStream::write_subgroup_object`].
242 ///
243 /// The header is refused, and nothing is written, if its fields disagree
244 /// with its own stream type. That check has to happen here rather than at
245 /// the first object: the type is what every object after it is framed
246 /// against, so a header that went out saying the wrong thing cannot be
247 /// taken back.
248 pub async fn write_subgroup_header(
249 &mut self,
250 header: &AnySubgroupHeader,
251 ) -> Result<(), ConnectionError> {
252 let mut buf = Vec::new();
253 header.encode_stream_checked(&mut buf)?;
254 self.inner.write_all(&buf).await?;
255 // Clippy would rather see these two arms as an `if let`, and rustc rejects
256 // that in a single-draft build, where the pattern is irrefutable. Only a
257 // `match` satisfies both.
258 #[allow(clippy::single_match)]
259 match header {
260 AnySubgroupHeader::Draft14(ref d14) => {
261 self.subgroup_io = Some(SubgroupObjectReader::new(d14));
262 }
263 // Only this draft's header seeds the object reader. With draft 14 the only enabled
264 // draft `AnySubgroupHeader` has a single variant, the arm above is exhaustive and this
265 // one unreachable. Compiled in every configuration with the lint allowed, rather than
266 // gated on a `cfg` naming the other drafts: such a list has to be edited in
267 // every draft module whenever a draft is added, and a copy that omits one leaves this
268 // match non-exhaustive.
269 #[allow(unreachable_patterns)]
270 _ => {}
271 }
272 Ok(())
273 }
274
275 /// Write a fetch response header.
276 pub async fn write_fetch_header(
277 &mut self,
278 header: &AnyFetchHeader,
279 ) -> Result<(), ConnectionError> {
280 let mut buf = Vec::new();
281 header.encode_stream(&mut buf);
282 self.inner.write_all(&buf).await?;
283 Ok(())
284 }
285
286 /// Append a draft-14 subgroup object to the stream. Uses the stateful
287 /// reader installed by [`FramedSendStream::write_subgroup_header`] to
288 /// produce the correct delta-encoded object ID. Returns an error if
289 /// called before a subgroup header was written.
290 pub async fn write_subgroup_object(
291 &mut self,
292 object: &SubgroupObject,
293 ) -> Result<(), ConnectionError> {
294 let writer = self
295 .subgroup_io
296 .as_mut()
297 .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
298 let mut buf = Vec::new();
299 writer.write_object(object, &mut buf)?;
300 self.inner.write_all(&buf).await?;
301 Ok(())
302 }
303
304 /// Append a draft-14 fetch object to the stream. Fetch objects are
305 /// self-contained (each carries its own group/subgroup/object IDs and
306 /// priority), so no prior `write_fetch_header` bookkeeping is required.
307 /// Append a fetch object to the stream.
308 ///
309 /// Draft-14 derives the declared length from the payload itself rather than
310 /// holding it as a field, so there is nothing here for the two to disagree
311 /// about - unlike the subgroup object, whose header carries the length and
312 /// whose writer has to overwrite it.
313 ///
314 /// # Errors
315 ///
316 /// Whatever the checked encoder refuses: a payload beside a status that
317 /// forbids one, or a status the draft leaves unassigned.
318 pub async fn write_fetch_object(
319 &mut self,
320 object: &FetchObject,
321 ) -> Result<(), ConnectionError> {
322 let mut buf = Vec::new();
323 object.encode_checked(&mut buf)?;
324 self.inner.write_all(&buf).await?;
325 Ok(())
326 }
327
328 /// Finish the stream (send FIN).
329 pub async fn finish(&mut self) -> Result<(), ConnectionError> {
330 self.inner.finish()?;
331 Ok(())
332 }
333
334 /// Returns the draft version this stream is framed for.
335 pub fn draft(&self) -> DraftVersion {
336 self.draft
337 }
338}
339
340/// What an Object Status makes of an object here.
341///
342/// Two answers where drafts 08 through 13 have three, and the missing one is
343/// the point: the end-of-track status settles where the track ended and is
344/// judged against nothing, because the rule about where one may be placed is
345/// not in this draft. `a_track_may_end_where_it_has_already_been.rs` asserts
346/// that acceptance.
347///
348/// Every other status is a statement about objects rather than one of them.
349fn object_role(status: Option<u64>) -> ObjectRole {
350 match status {
351 None | Some(0x0) => ObjectRole::Produced,
352 Some(0x4) => ObjectRole::EndsTrack(None),
353 _ => ObjectRole::Neither,
354 }
355}
356
357/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
358pub struct FramedRecvStream {
359 inner: RecvStream,
360 buf: BytesMut,
361 draft: DraftVersion,
362 /// Stateful subgroup object decoder. Initialized by
363 /// [`FramedRecvStream::read_subgroup_header`] and used by
364 /// [`FramedRecvStream::read_subgroup_object`] to track delta-encoded
365 /// object IDs and whether extension headers are present.
366 subgroup_io: Option<SubgroupObjectReader>,
367 /// The record this stream's objects are measured against, and the Group ID
368 /// its header named.
369 ///
370 /// One group for the whole stream: a subgroup header names it once and no
371 /// object header repeats it. `None` on a stream that was never given one -
372 /// a stream for an alias no live binding names, and every stream built
373 /// outside [`Connection::accept_subgroup_stream`] - and such a stream reads
374 /// without being measured, because `note_subgroup_object` has nothing to
375 /// measure it against.
376 tracking: Option<(TrackObjects, u64)>,
377 /// The Prior gap conditions this fetch stream's Objects show. Reset by
378 /// [`FramedRecvStream::read_fetch_header`].
379 fetch_gaps: moqtap_codec::draft14::data_stream::FetchGaps,
380 /// The bytes of the last control frame a capturing read refused.
381 refused: Option<Vec<u8>>,
382}
383
384impl FramedRecvStream {
385 /// Create a new framed receive stream for the given draft version.
386 pub fn new(inner: RecvStream, draft: DraftVersion) -> Self {
387 Self {
388 inner,
389 buf: BytesMut::with_capacity(4096),
390 draft,
391 subgroup_io: None,
392 tracking: None,
393 fetch_gaps: moqtap_codec::draft14::data_stream::FetchGaps::new(),
394 refused: None,
395 }
396 }
397
398 /// Get the transport-level stream ID.
399 pub fn stream_id(&self) -> u64 {
400 self.inner.stream_id()
401 }
402
403 /// The bytes of the control frame the last capturing read refused.
404 pub(crate) fn take_refused(&mut self) -> Option<Vec<u8>> {
405 self.refused.take()
406 }
407
408 /// Keep the first `len` buffered bytes as a refused frame when the read is
409 /// capturing, and hand `err` back.
410 fn refuse(&mut self, capture_raw: bool, len: usize, err: ConnectionError) -> ConnectionError {
411 if capture_raw {
412 self.refused = Some(self.buf[..len].to_vec());
413 }
414 err
415 }
416
417 /// Measure this stream's objects against `objects`, all of them in `group`.
418 ///
419 /// Called by [`Connection::accept_subgroup_stream`] once the header has
420 /// been read, which is the only point at which both the track and the group
421 /// are known.
422 fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
423 self.tracking = Some((objects, group));
424 }
425
426 /// Judge one object this stream carried against where its track ended.
427 ///
428 /// The object's Group ID is the stream's and its Object ID is its own,
429 /// already resolved from the delta the wire carries; what they are measured
430 /// against is the end an end-of-track object settled on any stream.
431 fn note_subgroup_object(
432 &self,
433 object: u64,
434 status: Option<u64>,
435 ) -> Result<(), ConnectionError> {
436 let Some((objects, group)) = &self.tracking else { return Ok(()) };
437 let at = ObjectLocation { group: *group, object };
438 objects.note_past_final(at, object_role(status)).map_err(|end| {
439 ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject {
440 alias: objects.alias(),
441 group: at.group,
442 object: at.object,
443 final_group: end.group,
444 final_object: end.object,
445 })
446 })
447 }
448
449 /// Read more data from the stream into the internal buffer.
450 async fn fill(&mut self) -> Result<bool, ConnectionError> {
451 let mut tmp = [0u8; 4096];
452 match self.inner.read(&mut tmp).await {
453 Ok(Some(n)) => {
454 self.buf.extend_from_slice(&tmp[..n]);
455 Ok(true)
456 }
457 Ok(None) => Ok(false),
458 Err(e) => Err(ConnectionError::Transport(e)),
459 }
460 }
461
462 /// Ensure at least `n` bytes are available in the buffer.
463 async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
464 while self.buf.len() < n {
465 if !self.fill().await? {
466 return Err(ConnectionError::UnexpectedEnd);
467 }
468 }
469 Ok(())
470 }
471
472 /// Read a control message from the stream.
473 ///
474 /// When `capture_raw` is true, the returned tuple includes a clone of the
475 /// framed wire bytes (for observer emission). When false, the second
476 /// element is `None` and the payload clone is skipped.
477 pub async fn read_control(
478 &mut self,
479 capture_raw: bool,
480 ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
481 self.refused = None;
482 // Read type ID varint
483 self.ensure(1).await?;
484 let type_len = varint_len(self.buf[0]);
485 self.ensure(type_len).await?;
486
487 let mut cursor = &self.buf[..type_len];
488 if let Err(e) = VarInt::decode(&mut cursor) {
489 return Err(self.refuse(capture_raw, type_len, e.into()));
490 }
491
492 // Read payload length (16-bit BE for draft-11+, varint for earlier drafts)
493 let (payload_len, len_field_size) = if self.draft.uses_fixed_length_framing() {
494 self.ensure(type_len + 2).await?;
495 let hi = self.buf[type_len] as usize;
496 let lo = self.buf[type_len + 1] as usize;
497 ((hi << 8) | lo, 2)
498 } else {
499 self.ensure(type_len + 1).await?;
500 let payload_len_start = type_len;
501 let payload_len_varint_len = varint_len(self.buf[payload_len_start]);
502 self.ensure(type_len + payload_len_varint_len).await?;
503 let header_len = type_len + payload_len_varint_len;
504 let mut cursor = &self.buf[payload_len_start..header_len];
505 let payload_len = match VarInt::decode(&mut cursor) {
506 Ok(length) => length.into_inner() as usize,
507 Err(e) => return Err(self.refuse(capture_raw, header_len, e.into())),
508 };
509 (payload_len, payload_len_varint_len)
510 };
511
512 // Read full payload
513 let total = type_len + len_field_size + payload_len;
514 self.ensure(total).await?;
515
516 // Capture raw bytes only if requested (observer attached).
517 let raw = capture_raw.then(|| self.buf[..total].to_vec());
518
519 // Now decode the whole message
520 let mut frame = &self.buf[..total];
521 let msg = match AnyControlMessage::decode(self.draft, &mut frame) {
522 Ok(msg) => msg,
523 Err(e) => return Err(self.refuse(capture_raw, total, e.into())),
524 };
525 self.buf.advance(total);
526 Ok((msg, raw))
527 }
528
529 /// Read a subgroup stream header. Also initializes the internal
530 /// delta-decoding state used by
531 /// [`FramedRecvStream::read_subgroup_object`].
532 pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
533 self.ensure(1).await?;
534 loop {
535 let mut cursor = &self.buf[..];
536 match AnySubgroupHeader::decode(self.draft, &mut cursor) {
537 Ok(header) => {
538 let consumed = self.buf.len() - cursor.remaining();
539 self.buf.advance(consumed);
540 // Clippy would rather see these two arms as an `if let`, and rustc rejects
541 // that in a single-draft build, where the pattern is irrefutable. Only a
542 // `match` satisfies both.
543 #[allow(clippy::single_match)]
544 match header {
545 AnySubgroupHeader::Draft14(ref d14) => {
546 self.subgroup_io = Some(SubgroupObjectReader::new(d14));
547 }
548 // Only this draft's header seeds the object reader. With draft 14 the only
549 // enabled draft `AnySubgroupHeader` has a single variant, the arm above is
550 // exhaustive and this one unreachable. Compiled in every configuration with
551 // the lint allowed, rather than gated on a `cfg` naming every other
552 // draft: such a list has to be edited in every draft module whenever a
553 // draft is added, and a copy that omits one leaves this match
554 // non-exhaustive.
555 #[allow(unreachable_patterns)]
556 _ => {}
557 }
558 return Ok(header);
559 }
560 Err(e) if e.is_incomplete() => {
561 if !self.fill().await? {
562 return Err(ConnectionError::UnexpectedEnd);
563 }
564 }
565 Err(e) => return Err(ConnectionError::Codec(e)),
566 }
567 }
568 }
569
570 /// Read a fetch response header.
571 pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
572 self.ensure(1).await?;
573 loop {
574 let mut cursor = &self.buf[..];
575 match AnyFetchHeader::decode(self.draft, &mut cursor) {
576 Ok(header) => {
577 let consumed = self.buf.len() - cursor.remaining();
578 self.buf.advance(consumed);
579 self.fetch_gaps = moqtap_codec::draft14::data_stream::FetchGaps::new();
580 return Ok(header);
581 }
582 Err(e) if e.is_incomplete() => {
583 if !self.fill().await? {
584 return Err(ConnectionError::UnexpectedEnd);
585 }
586 }
587 Err(e) => return Err(ConnectionError::Codec(e)),
588 }
589 }
590 }
591
592 /// Read the next draft-14 subgroup object from this stream. Uses the
593 /// stateful reader installed by
594 /// [`FramedRecvStream::read_subgroup_header`] to decode the delta-
595 /// encoded object ID and handle extension headers per the stream type.
596 /// Returns an error if called before a subgroup header was read.
597 pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
598 if self.subgroup_io.is_none() {
599 return Err(ConnectionError::DataStreamState("subgroup header not read yet"));
600 }
601 loop {
602 let reader = self.subgroup_io.as_mut().unwrap();
603 let mut probe = reader.clone();
604 let mut cursor = &self.buf[..];
605 match probe.read_object(&mut cursor) {
606 Ok(obj) => {
607 let consumed = self.buf.len() - cursor.remaining();
608 self.buf.advance(consumed);
609 // Commit state from the successful probe decode.
610 *reader = probe;
611 self.note_subgroup_object(
612 obj.object_id.into_inner(),
613 obj.status.map(|s| s as u64),
614 )?;
615 return Ok(obj);
616 }
617 Err(e) if e.is_incomplete() => {
618 if !self.fill().await? {
619 return Err(ConnectionError::UnexpectedEnd);
620 }
621 }
622 Err(e) => return Err(ConnectionError::Codec(e)),
623 }
624 }
625 }
626
627 /// Read the next draft-14 fetch object from this stream. Fetch objects
628 /// are self-describing, so no prior `read_fetch_header` state is needed
629 /// to decode each object.
630 ///
631 /// A Prior Group ID Gap or Prior Object ID Gap fault the stream shows is
632 /// returned as [`CodecError::MalformedTrack`], and an extension block that
633 /// does not parse as [`CodecError::MalformedObjectProperties`];
634 /// [`Connection::withdraw_for_fetch_stream`] withdraws from the track.
635 pub async fn read_fetch_object(&mut self) -> Result<FetchObject, ConnectionError> {
636 loop {
637 let mut cursor = &self.buf[..];
638 match FetchObject::decode(&mut cursor) {
639 Ok(obj) => {
640 let consumed = self.buf.len() - cursor.remaining();
641 self.buf.advance(consumed);
642 self.fetch_gaps
643 .check(
644 obj.group_id.into_inner(),
645 obj.object_id.into_inner(),
646 &obj.extension_headers,
647 )
648 .map_err(ConnectionError::Codec)?;
649 return Ok(obj);
650 }
651 Err(e) if e.is_incomplete() => {
652 if !self.fill().await? {
653 return Err(ConnectionError::UnexpectedEnd);
654 }
655 }
656 Err(e) => return Err(ConnectionError::Codec(e)),
657 }
658 }
659 }
660
661 /// Returns the draft version this stream is framed for.
662 pub fn draft(&self) -> DraftVersion {
663 self.draft
664 }
665}
666
667/// A live MoQT connection over QUIC or WebTransport, combining the endpoint
668/// state machine with actual network I/O.
669pub struct Connection {
670 transport: Transport,
671 endpoint: Endpoint,
672 draft: DraftVersion,
673 /// Behind a lock because a control message is written from two kinds of
674 /// place. Most of them are the caller's own request, made through `&mut
675 /// self`. The messages that answer a Malformed Track are not: the
676 /// conditions that make a track malformed are detected on the data plane,
677 /// where this connection is reached through a shared reference. The lock
678 /// also makes one message the unit of writing, so two of them cannot
679 /// interleave on the stream.
680 control_send: Option<tokio::sync::Mutex<FramedSendStream>>,
681 control_recv: Option<FramedRecvStream>,
682 observer: Option<Box<dyn ConnectionObserver>>,
683 /// Setup events buffered during `connect()` and replayed when an
684 /// observer attaches via `set_observer` — without this, an observer
685 /// attached after `connect` returns would never see the handshake.
686 pending_events: Vec<ClientEvent>,
687 /// The server's half of the setup handshake, kept whole.
688 ///
689 /// The endpoint acts on the parameters it recognises and retains none of
690 /// them, and which parameters a server sends — in what order, with what
691 /// values — is the sharpest thing a session says about the implementation
692 /// behind it.
693 server_setup: AnyControlMessage,
694 /// The framed wire bytes of [`Self::server_setup`].
695 server_setup_raw: Option<Vec<u8>>,
696}
697
698impl Connection {
699 /// Connect to a MoQT server as a client.
700 ///
701 /// Establishes a QUIC or WebTransport connection (based on `config.transport`),
702 /// opens a bidirectional control stream, performs the CLIENT_SETUP /
703 /// SERVER_SETUP handshake, and returns a ready-to-use connection.
704 pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
705 // PATH is for native QUIC only, and the transport is known here and
706 // nowhere further in. Refusing before dialling means a session that
707 // the server would close on sight is never opened.
708 setup::validate_client_path_transport(
709 &config.setup_parameters,
710 matches!(config.transport, TransportType::WebTransport { .. }),
711 )
712 .map_err(EndpointError::from)?;
713
714 let transport = match &config.transport {
715 TransportType::Quic => Self::connect_quic(addr, &config).await?,
716 TransportType::WebTransport { url } => {
717 let url = url.clone();
718 Self::connect_webtransport(&url, &config).await?
719 }
720 };
721
722 Self::adopt(transport, config).await
723 }
724
725 /// Run the MoQT setup handshake over a transport somebody else established.
726 ///
727 /// For choosing the draft from what the server selected: dial once through
728 /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
729 /// connection to the module its answer names. [`Self::connect`] cannot do
730 /// this — it derives its single ALPN from the draft it was given.
731 ///
732 /// `config.draft` must match this module. The transport is adopted as
733 /// given; nothing here re-checks the ALPN it was negotiated with.
734 pub async fn adopt(
735 transport: Transport,
736 config: ClientConfig,
737 ) -> Result<Self, ConnectionError> {
738 Self::adopt_offering(transport, config, None).await
739 }
740
741 /// [`Self::adopt`], offering exactly `versions` in CLIENT_SETUP.
742 ///
743 /// `None` offers what `config` implies, which is what [`Self::adopt`]
744 /// passes. `Some` replaces the list outright, and takes raw varints rather
745 /// than [`DraftVersion`]s because the reason to reach for this is to offer
746 /// a version no draft assigns — which an enum of drafts cannot name.
747 ///
748 /// A server MUST answer with a version the client offered and MUST
749 /// otherwise close the session; from draft-11 the code for that is
750 /// `VERSION_NEGOTIATION_FAILED` (0x15). How a relay spells the refusal is
751 /// a conformance measurement, and offering a version deliberately outside
752 /// the negotiable set is the only way to ask for it.
753 pub async fn adopt_offering(
754 transport: Transport,
755 config: ClientConfig,
756 versions: Option<Vec<VarInt>>,
757 ) -> Result<Self, ConnectionError> {
758 Self::adopt_with(transport, config, versions, None).await
759 }
760
761 /// [`Self::adopt`] with `observer` attached before the handshake.
762 ///
763 /// The SETUP this side sends is reported as it is written, and a SETUP from
764 /// the peer that this side refuses as [`ClientEvent::ControlMessageRefused`].
765 /// [`Self::set_observer`] after `adopt` sees the same handshake events, but
766 /// only once the handshake has succeeded.
767 pub async fn adopt_observed(
768 transport: Transport,
769 config: ClientConfig,
770 observer: Box<dyn ConnectionObserver>,
771 ) -> Result<Self, ConnectionError> {
772 Self::adopt_with(transport, config, None, Some(observer)).await
773 }
774
775 /// [`Self::adopt_offering`] with `observer` attached before the handshake,
776 /// as [`Self::adopt_observed`].
777 pub async fn adopt_offering_observed(
778 transport: Transport,
779 config: ClientConfig,
780 versions: Option<Vec<VarInt>>,
781 observer: Box<dyn ConnectionObserver>,
782 ) -> Result<Self, ConnectionError> {
783 Self::adopt_with(transport, config, versions, Some(observer)).await
784 }
785
786 /// [`Self::adopt_offering`], reporting to `observer` from the first byte
787 /// written.
788 pub(crate) async fn adopt_with(
789 transport: Transport,
790 config: ClientConfig,
791 versions: Option<Vec<VarInt>>,
792 observer: Option<Box<dyn ConnectionObserver>>,
793 ) -> Result<Self, ConnectionError> {
794 let draft = config.draft;
795 // PATH is for native QUIC only, and the transport is known here and
796 // nowhere further in. Refusing before dialling means a session that
797 // the server would close on sight is never opened.
798 setup::validate_client_path_transport(
799 &config.setup_parameters,
800 matches!(config.transport, TransportType::WebTransport { .. }),
801 )
802 .map_err(EndpointError::from)?;
803
804 // Open bidirectional control stream
805 let (send, recv) = transport.open_bi().await?;
806 let mut control_send = FramedSendStream::new(send, draft);
807 let mut control_recv = FramedRecvStream::new(recv, draft);
808
809 // Perform setup handshake
810 let mut endpoint = Endpoint::new(Role::Client);
811 endpoint.connect()?;
812 let setup_msg = endpoint.send_client_setup(
813 versions.unwrap_or_else(|| config.supported_versions()),
814 config.setup_parameters.clone(),
815 )?;
816 let any_setup = AnyControlMessage::Draft14(setup_msg);
817 let raw_setup = control_send.write_control(&any_setup).await?;
818 // Reported as written, so a peer that never answers still leaves it
819 // observed.
820 if let Some(obs) = &observer {
821 obs.on_event_owned(ClientEvent::ControlMessage {
822 direction: Direction::Send,
823 message: any_setup.clone(),
824 raw: Some(raw_setup.clone()),
825 });
826 }
827
828 let (server_setup, raw_server_setup) = match control_recv.read_control(true).await {
829 Ok(read) => read,
830 Err(e) => {
831 if let Some(obs) = &observer {
832 if let Some(event) = Self::refused_event(&e, control_recv.take_refused()) {
833 obs.on_event_owned(event);
834 }
835 }
836 return Err(e);
837 }
838 };
839 // Unwrap to draft-14 for the endpoint (which is draft-14 only)
840 match &server_setup {
841 AnyControlMessage::Draft14(ControlMessage::ServerSetup(ref ss)) => {
842 if let Err(e) = endpoint.receive_server_setup(ss) {
843 let err = ConnectionError::from(e);
844 Self::refuse_setup(observer.as_deref(), &err, &server_setup, &raw_server_setup);
845 return Err(err);
846 }
847 }
848 _ => {
849 return Err(ConnectionError::Endpoint(EndpointError::NotActive));
850 }
851 }
852
853 let mut pending_events = Vec::with_capacity(3);
854 pending_events.push(ClientEvent::ControlMessage {
855 direction: Direction::Send,
856 message: any_setup,
857 raw: Some(raw_setup),
858 });
859 pending_events.push(ClientEvent::ControlMessage {
860 direction: Direction::Receive,
861 message: server_setup.clone(),
862 raw: raw_server_setup.clone(),
863 });
864 if let Some(v) = endpoint.negotiated_version() {
865 pending_events.push(ClientEvent::SetupComplete { negotiated_version: v.into_inner() });
866 }
867
868 if let Some(obs) = &observer {
869 // The SETUP this side sent was reported as it was written.
870 for event in pending_events.drain(..).skip(1) {
871 obs.on_event_owned(event);
872 }
873 }
874
875 Ok(Self {
876 transport,
877 endpoint,
878 draft,
879 control_send: Some(tokio::sync::Mutex::new(control_send)),
880 control_recv: Some(control_recv),
881 observer,
882 pending_events,
883 server_setup,
884 server_setup_raw: raw_server_setup,
885 })
886 }
887
888 /// Establish a raw QUIC connection.
889 ///
890 /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
891 /// TLS and endpoint setup.
892 async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
893 let (transport, _negotiated) = crate::transport::dial_quic(
894 addr,
895 &crate::transport::QuicDialOptions {
896 skip_cert_verification: config.skip_cert_verification,
897 ca_certs: config.ca_certs.clone(),
898 ..crate::transport::QuicDialOptions::new(config.alpn())
899 },
900 )
901 .await?;
902 Ok(transport)
903 }
904
905 /// Establish a WebTransport connection.
906 ///
907 /// [`crate::transport::dial_webtransport`] holds the TLS and endpoint
908 /// setup, exactly as `connect_quic` above defers its own. That is not
909 /// only deduplication: both dials must trust the same roots. Settling trust
910 /// at this call site instead — from `wtransport`'s own builder settings, or
911 /// from a second config of this draft's own — puts the decision in two
912 /// places, where it can stop matching what the QUIC dial trusts, so one
913 /// relay would pass on one transport and fail on the other and a caller's
914 /// private CA would reach only the dials whose call site installed it.
915 /// Both ask the same function what to trust.
916 #[cfg(feature = "webtransport")]
917 async fn connect_webtransport(
918 url: &str,
919 config: &ClientConfig,
920 ) -> Result<Transport, ConnectionError> {
921 Ok(crate::transport::dial_webtransport(
922 url,
923 &crate::transport::QuicDialOptions {
924 skip_cert_verification: config.skip_cert_verification,
925 ca_certs: config.ca_certs.clone(),
926 ..crate::transport::QuicDialOptions::new(config.alpn())
927 },
928 )
929 .await?)
930 }
931
932 /// Stub for when the webtransport feature is not enabled.
933 #[cfg(not(feature = "webtransport"))]
934 async fn connect_webtransport(
935 _url: &str,
936 _config: &ClientConfig,
937 ) -> Result<Transport, ConnectionError> {
938 Err(ConnectionError::Transport(TransportError::Connect(
939 "webtransport feature not enabled".into(),
940 )))
941 }
942
943 // ── Observer ───────────────────────────────────────────────
944
945 /// Attach an observer. Buffered handshake events from `connect()` are
946 /// flushed in arrival order before this returns.
947 pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
948 self.observer = Some(observer);
949 for event in self.pending_events.drain(..) {
950 if let Some(ref obs) = self.observer {
951 obs.on_event_owned(event);
952 }
953 }
954 }
955
956 /// Remove the observer.
957 pub fn clear_observer(&mut self) {
958 self.observer = None;
959 }
960
961 /// How the QUIC connection was closed, once it has been. See
962 /// [`TransportClose`](crate::transport::TransportClose).
963 pub fn transport_close(&self) -> Option<crate::transport::TransportClose> {
964 self.transport.transport_close()
965 }
966
967 /// Emit an event to the observer, if one is attached.
968 fn emit(&self, event: ClientEvent) {
969 if let Some(ref obs) = self.observer {
970 obs.on_event_owned(event);
971 }
972 }
973
974 /// Report a received frame this endpoint refused, when an observer is
975 /// attached and the refusal is the peer's: see [`Self::refused_event`].
976 fn emit_refused(&self, err: &ConnectionError, raw: Option<Vec<u8>>) {
977 if self.observer.is_some() {
978 if let Some(event) = Self::refused_event(err, raw) {
979 self.emit(event);
980 }
981 }
982 }
983
984 /// The [`ClientEvent::ControlMessageRefused`] for `raw`, when `err` refuses
985 /// what the peer sent: the decoder refused it, or it broke a rule
986 /// [`EndpointError::fault`] or [`Self::draft_specific_cause`] lays on the
987 /// peer.
988 fn refused_event(err: &ConnectionError, raw: Option<Vec<u8>>) -> Option<ClientEvent> {
989 use crate::above_codec_rules::{DraftSpecificCause, EndpointFault};
990 let peers = match err {
991 ConnectionError::Codec(_) | ConnectionError::VarInt(_) => true,
992 ConnectionError::Endpoint(e) => matches!(e.fault(), EndpointFault::Peer(_)),
993 other => matches!(
994 Self::draft_specific_cause(other),
995 Some(DraftSpecificCause::PeerViolation { .. } | DraftSpecificCause::Codec { .. })
996 ),
997 };
998 let raw = raw.filter(|_| peers)?;
999 Some(ClientEvent::ControlMessageRefused { raw })
1000 }
1001
1002 /// Report a peer's SETUP the handshake refused to an observer attached
1003 /// before it: the message as received, then the refusal.
1004 fn refuse_setup(
1005 observer: Option<&dyn ConnectionObserver>,
1006 err: &ConnectionError,
1007 setup: &AnyControlMessage,
1008 raw: &Option<Vec<u8>>,
1009 ) {
1010 let Some(obs) = observer else { return };
1011 obs.on_event_owned(ClientEvent::ControlMessage {
1012 direction: Direction::Receive,
1013 message: setup.clone(),
1014 raw: raw.clone(),
1015 });
1016 if let Some(event) = Self::refused_event(err, raw.clone()) {
1017 obs.on_event_owned(event);
1018 }
1019 }
1020
1021 // ── Control message I/O ─────────────────────────────────
1022
1023 /// Send a control message on the control stream.
1024 ///
1025 /// Wraps the draft-14 message in `AnyControlMessage::Draft14` for framing.
1026 pub async fn send_control(&self, msg: &ControlMessage) -> Result<(), ConnectionError> {
1027 let any = AnyControlMessage::Draft14(msg.clone());
1028 let mut send =
1029 self.control_send.as_ref().ok_or(ConnectionError::NoControlStream)?.lock().await;
1030 let raw = send.write_control(&any).await?;
1031 drop(send);
1032 self.emit(ClientEvent::ControlMessage {
1033 direction: Direction::Send,
1034 message: any,
1035 raw: Some(raw),
1036 });
1037 Ok(())
1038 }
1039
1040 /// Read the next control message from the control stream.
1041 ///
1042 /// Returns the `AnyControlMessage` and also extracts the draft-14
1043 /// `ControlMessage` for internal endpoint dispatch.
1044 pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
1045 self.recv_control_raw().await.map(|(msg, _)| msg)
1046 }
1047
1048 /// [`Self::recv_control`], keeping the frame's wire bytes when an observer
1049 /// is attached.
1050 async fn recv_control_raw(
1051 &mut self,
1052 ) -> Result<(ControlMessage, Option<Vec<u8>>), ConnectionError> {
1053 let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
1054 let capture_raw = self.observer.is_some();
1055 let (any, raw) = match recv.read_control(capture_raw).await {
1056 Ok(v) => v,
1057 Err(e) => {
1058 let raw = recv.take_refused();
1059 let err = self.close_for_codec(e);
1060 self.emit_refused(&err, raw);
1061 return Err(err);
1062 }
1063 };
1064 if capture_raw {
1065 self.emit(ClientEvent::ControlMessage {
1066 direction: Direction::Receive,
1067 message: any.clone(),
1068 raw: raw.clone(),
1069 });
1070 }
1071 // Unwrap to draft-14 for the endpoint
1072 match any {
1073 AnyControlMessage::Draft14(msg) => Ok((msg, raw)),
1074 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 14 the
1075 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
1076 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
1077 // naming the other drafts: such a list has to be edited in every draft module
1078 // whenever a draft is added, and a copy that omits one leaves this match
1079 // non-exhaustive.
1080 #[allow(unreachable_patterns)]
1081 _ => Err(ConnectionError::ControlMessageNarrowing),
1082 }
1083 }
1084
1085 /// Read and dispatch the next incoming control message through the endpoint
1086 /// state machine. Returns the decoded message for inspection.
1087 pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
1088 let (msg, raw) = self.recv_control_raw().await?;
1089 if let Err(e) = self.endpoint.receive_message(msg.clone()) {
1090 let err = self.close_if_session_fatal(e);
1091 self.emit_refused(&err, raw);
1092 return Err(err);
1093 }
1094
1095 // Emit draining event if this was a GoAway
1096 if let ControlMessage::GoAway(ref ga) = msg {
1097 self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
1098 }
1099
1100 Ok(msg)
1101 }
1102
1103 // ── Subscribe flow ──────────────────────────────────────
1104
1105 /// Send a SUBSCRIBE and return the allocated request ID.
1106 pub async fn subscribe(
1107 &mut self,
1108 track_namespace: TrackNamespace,
1109 track_name: Vec<u8>,
1110 subscriber_priority: u8,
1111 group_order: GroupOrder,
1112 filter_type: FilterType,
1113 parameters: Vec<KeyValuePair>,
1114 ) -> Result<VarInt, ConnectionError> {
1115 let (req_id, msg) = self.endpoint.subscribe(
1116 track_namespace,
1117 track_name,
1118 subscriber_priority,
1119 group_order,
1120 filter_type,
1121 parameters,
1122 )?;
1123 self.send_control(&msg).await?;
1124 Ok(req_id)
1125 }
1126
1127 /// Send a SUBSCRIBE for a range of the track, starting at a given
1128 /// location.
1129 ///
1130 /// The Filter Type is derived from the range, so the message cannot name a
1131 /// filter whose fields it does not carry.
1132 #[allow(clippy::too_many_arguments)]
1133 pub async fn subscribe_range(
1134 &mut self,
1135 track_namespace: TrackNamespace,
1136 track_name: Vec<u8>,
1137 subscriber_priority: u8,
1138 group_order: GroupOrder,
1139 start_location: Location,
1140 end_group: Option<VarInt>,
1141 parameters: Vec<KeyValuePair>,
1142 ) -> Result<VarInt, ConnectionError> {
1143 let (req_id, msg) = self.endpoint.subscribe_range(
1144 track_namespace,
1145 track_name,
1146 subscriber_priority,
1147 group_order,
1148 start_location,
1149 end_group,
1150 parameters,
1151 )?;
1152 self.send_control(&msg).await?;
1153 Ok(req_id)
1154 }
1155
1156 /// Send an UNSUBSCRIBE for the given request ID.
1157 pub async fn unsubscribe(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1158 let msg = self.endpoint.unsubscribe(request_id)?;
1159 self.send_control(&msg).await
1160 }
1161
1162 /// Accept a subscription the peer opened, sending SUBSCRIBE_OK and giving
1163 /// its track a Track Alias.
1164 ///
1165 /// The endpoint refuses an alias a live track of its own already holds and
1166 /// refuses a second answer to one SUBSCRIBE, so nothing is written on the
1167 /// wire when it does either.
1168 pub async fn subscribe_ok(
1169 &mut self,
1170 request_id: VarInt,
1171 track_alias: VarInt,
1172 expires: VarInt,
1173 group_order: GroupOrder,
1174 parameters: Vec<KeyValuePair>,
1175 ) -> Result<(), ConnectionError> {
1176 let msg = self.endpoint.send_subscribe_ok(
1177 request_id,
1178 track_alias,
1179 expires,
1180 group_order,
1181 parameters,
1182 )?;
1183 self.send_control(&msg).await
1184 }
1185
1186 /// Reject a subscription the peer opened, sending SUBSCRIBE_ERROR.
1187 ///
1188 /// The endpoint refuses a second answer to one SUBSCRIBE, so nothing is
1189 /// written on the wire when it does.
1190 pub async fn subscribe_error(
1191 &mut self,
1192 request_id: VarInt,
1193 error_code: VarInt,
1194 reason_phrase: Vec<u8>,
1195 ) -> Result<(), ConnectionError> {
1196 let msg = self.endpoint.send_subscribe_error(request_id, error_code, reason_phrase)?;
1197 self.send_control(&msg).await
1198 }
1199
1200 /// Accept a PUBLISH the peer sent, which establishes the subscription it
1201 /// opened.
1202 ///
1203 /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1204 /// written on the wire when it does.
1205 #[allow(clippy::too_many_arguments)]
1206 pub async fn publish_ok(
1207 &mut self,
1208 request_id: VarInt,
1209 forward: Forward,
1210 subscriber_priority: u8,
1211 group_order: GroupOrder,
1212 filter_type: FilterType,
1213 start_location: Option<Location>,
1214 end_group: Option<VarInt>,
1215 ) -> Result<(), ConnectionError> {
1216 let msg = self.endpoint.send_publish_ok(
1217 request_id,
1218 forward,
1219 subscriber_priority,
1220 group_order,
1221 filter_type,
1222 start_location,
1223 end_group,
1224 )?;
1225 self.send_control(&msg).await
1226 }
1227
1228 /// Reject a PUBLISH the peer sent, which ends the subscription it opened
1229 /// before it was established.
1230 ///
1231 /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1232 /// written on the wire when it does.
1233 pub async fn publish_error(
1234 &mut self,
1235 request_id: VarInt,
1236 error_code: VarInt,
1237 reason_phrase: Vec<u8>,
1238 ) -> Result<(), ConnectionError> {
1239 let msg = self.endpoint.send_publish_error(request_id, error_code, reason_phrase)?;
1240 self.send_control(&msg).await
1241 }
1242
1243 /// Send a SUBSCRIBE_UPDATE for an active subscription. Returns the
1244 /// allocated request ID of the update message.
1245 pub async fn subscribe_update(
1246 &mut self,
1247 subscription_request_id: VarInt,
1248 start_location: Location,
1249 end_group: VarInt,
1250 subscriber_priority: u8,
1251 forward: Forward,
1252 parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
1253 ) -> Result<VarInt, ConnectionError> {
1254 let (req_id, msg) = self.endpoint.subscribe_update(
1255 subscription_request_id,
1256 start_location,
1257 end_group,
1258 subscriber_priority,
1259 forward,
1260 parameters,
1261 )?;
1262 self.send_control(&msg).await?;
1263 Ok(req_id)
1264 }
1265
1266 // ── Fetch flow ──────────────────────────────────────────
1267
1268 /// Send a FETCH and return the allocated request ID.
1269 #[allow(clippy::too_many_arguments)]
1270 pub async fn fetch(
1271 &mut self,
1272 track_namespace: TrackNamespace,
1273 track_name: Vec<u8>,
1274 subscriber_priority: u8,
1275 group_order: GroupOrder,
1276 start_group: VarInt,
1277 start_object: VarInt,
1278 end_group: VarInt,
1279 end_object: VarInt,
1280 parameters: Vec<KeyValuePair>,
1281 ) -> Result<VarInt, ConnectionError> {
1282 let (req_id, msg) = self.endpoint.fetch(
1283 track_namespace,
1284 track_name,
1285 subscriber_priority,
1286 group_order,
1287 start_group,
1288 start_object,
1289 end_group,
1290 end_object,
1291 parameters,
1292 )?;
1293 self.send_control(&msg).await?;
1294 Ok(req_id)
1295 }
1296
1297 /// Send a Relative Joining Fetch and return the allocated request ID.
1298 ///
1299 /// `joining_start` is a count of groups back from the subscription's live
1300 /// edge. For the form that names the group outright, see
1301 /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
1302 pub async fn joining_fetch(
1303 &mut self,
1304 subscriber_priority: u8,
1305 group_order: GroupOrder,
1306 joining_request_id: VarInt,
1307 joining_start: VarInt,
1308 parameters: Vec<KeyValuePair>,
1309 ) -> Result<VarInt, ConnectionError> {
1310 let (req_id, msg) = self.endpoint.joining_fetch(
1311 subscriber_priority,
1312 group_order,
1313 joining_request_id,
1314 joining_start,
1315 parameters,
1316 )?;
1317 self.send_control(&msg).await?;
1318 Ok(req_id)
1319 }
1320
1321 /// Send an Absolute Joining Fetch and return the allocated request ID.
1322 ///
1323 /// `joining_start` is the group to begin at.
1324 pub async fn absolute_joining_fetch(
1325 &mut self,
1326 subscriber_priority: u8,
1327 group_order: GroupOrder,
1328 joining_request_id: VarInt,
1329 joining_start: VarInt,
1330 parameters: Vec<KeyValuePair>,
1331 ) -> Result<VarInt, ConnectionError> {
1332 let (req_id, msg) = self.endpoint.absolute_joining_fetch(
1333 subscriber_priority,
1334 group_order,
1335 joining_request_id,
1336 joining_start,
1337 parameters,
1338 )?;
1339 self.send_control(&msg).await?;
1340 Ok(req_id)
1341 }
1342
1343 /// Send a FETCH_CANCEL for the given request ID.
1344 pub async fn fetch_cancel(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1345 let msg = self.endpoint.fetch_cancel(request_id)?;
1346 self.send_control(&msg).await
1347 }
1348
1349 /// Accept a fetch the peer opened, sending FETCH_OK.
1350 ///
1351 /// The endpoint refuses a Joining Fetch naming a subscription this session
1352 /// cannot join and refuses a second answer to one FETCH, so nothing is
1353 /// written on the wire when it does either.
1354 pub async fn fetch_ok(
1355 &mut self,
1356 request_id: VarInt,
1357 group_order: GroupOrder,
1358 end_of_track: u8,
1359 end_location: Location,
1360 parameters: Vec<KeyValuePair>,
1361 ) -> Result<(), ConnectionError> {
1362 let msg = self.endpoint.send_fetch_ok(
1363 request_id,
1364 group_order,
1365 end_of_track,
1366 end_location,
1367 parameters,
1368 )?;
1369 self.send_control(&msg).await
1370 }
1371
1372 /// Refuse a fetch the peer opened, sending FETCH_ERROR.
1373 ///
1374 /// The endpoint refuses a second answer to one FETCH, and refuses a
1375 /// Joining Fetch's refusal under any code but the one the draft names for
1376 /// it, so nothing is written on the wire when it does either.
1377 pub async fn fetch_error(
1378 &mut self,
1379 request_id: VarInt,
1380 error_code: VarInt,
1381 reason_phrase: Vec<u8>,
1382 ) -> Result<(), ConnectionError> {
1383 let msg = self.endpoint.send_fetch_error(request_id, error_code, reason_phrase)?;
1384 self.send_control(&msg).await
1385 }
1386
1387 // ── Namespace flows ─────────────────────────────────────
1388
1389 /// Send a SUBSCRIBE_NAMESPACE and return the request ID.
1390 pub async fn subscribe_namespace(
1391 &mut self,
1392 track_namespace: TrackNamespace,
1393 parameters: Vec<KeyValuePair>,
1394 ) -> Result<VarInt, ConnectionError> {
1395 let (req_id, msg) = self.endpoint.subscribe_namespace(track_namespace, parameters)?;
1396 self.send_control(&msg).await?;
1397 Ok(req_id)
1398 }
1399
1400 /// Accept a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_OK.
1401 ///
1402 /// The endpoint refuses a second answer to one SUBSCRIBE_NAMESPACE, so nothing is
1403 /// written on the wire when it does.
1404 pub async fn subscribe_namespace_ok(
1405 &mut self,
1406 request_id: VarInt,
1407 ) -> Result<(), ConnectionError> {
1408 let msg = self.endpoint.send_subscribe_namespace_ok(request_id)?;
1409 self.send_control(&msg).await
1410 }
1411
1412 /// Refuse a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_ERROR.
1413 ///
1414 /// The other half of the same sentence: one answer, and this is the other
1415 /// one it can be.
1416 pub async fn subscribe_namespace_error(
1417 &mut self,
1418 request_id: VarInt,
1419 error_code: VarInt,
1420 reason_phrase: Vec<u8>,
1421 ) -> Result<(), ConnectionError> {
1422 let msg =
1423 self.endpoint.send_subscribe_namespace_error(request_id, error_code, reason_phrase)?;
1424 self.send_control(&msg).await
1425 }
1426
1427 /// Send a PUBLISH_NAMESPACE and return the request ID.
1428 pub async fn publish_namespace(
1429 &mut self,
1430 track_namespace: TrackNamespace,
1431 parameters: Vec<KeyValuePair>,
1432 ) -> Result<VarInt, ConnectionError> {
1433 let (req_id, msg) = self.endpoint.publish_namespace(track_namespace, parameters)?;
1434 self.send_control(&msg).await?;
1435 Ok(req_id)
1436 }
1437
1438 /// Accept an announcement the peer made, sending PUBLISH_NAMESPACE_OK.
1439 ///
1440 /// The endpoint refuses a second answer to one PUBLISH_NAMESPACE, so
1441 /// nothing is written on the wire when it does.
1442 pub async fn publish_namespace_ok(
1443 &mut self,
1444 request_id: VarInt,
1445 ) -> Result<(), ConnectionError> {
1446 let msg = self.endpoint.send_publish_namespace_ok(request_id)?;
1447 self.send_control(&msg).await
1448 }
1449
1450 /// Refuse an announcement the peer made, sending PUBLISH_NAMESPACE_ERROR.
1451 ///
1452 /// The other half of the same sentence: one answer, and this is the other
1453 /// one it can be.
1454 pub async fn publish_namespace_error(
1455 &mut self,
1456 request_id: VarInt,
1457 error_code: VarInt,
1458 reason_phrase: Vec<u8>,
1459 ) -> Result<(), ConnectionError> {
1460 let msg =
1461 self.endpoint.send_publish_namespace_error(request_id, error_code, reason_phrase)?;
1462 self.send_control(&msg).await
1463 }
1464
1465 /// Revoke an acceptance, sending PUBLISH_NAMESPACE_CANCEL.
1466 ///
1467 /// The endpoint refuses one for an announcement it never accepted, so
1468 /// nothing is written on the wire when it does.
1469 pub async fn publish_namespace_cancel(
1470 &mut self,
1471 track_namespace: TrackNamespace,
1472 error_code: VarInt,
1473 reason_phrase: Vec<u8>,
1474 ) -> Result<(), ConnectionError> {
1475 let msg =
1476 self.endpoint.publish_namespace_cancel(track_namespace, error_code, reason_phrase)?;
1477 self.send_control(&msg).await
1478 }
1479
1480 /// Withdraw an announcement this endpoint made, sending
1481 /// PUBLISH_NAMESPACE_DONE.
1482 ///
1483 /// The mirror of [`Self::publish_namespace`], and the counterpart of
1484 /// [`Self::publish_namespace_cancel`]: this one ends an announcement of
1485 /// this endpoint's, that one revokes the acceptance of one the peer made.
1486 pub async fn publish_namespace_done(
1487 &mut self,
1488 track_namespace: TrackNamespace,
1489 ) -> Result<(), ConnectionError> {
1490 let msg = self.endpoint.publish_namespace_done(track_namespace)?;
1491 self.send_control(&msg).await
1492 }
1493 // ── Track Status flow ────────────────────────────────────
1494
1495 /// Send a TRACK_STATUS and return the allocated request ID.
1496 #[allow(clippy::too_many_arguments)]
1497 pub async fn track_status(
1498 &mut self,
1499 track_namespace: TrackNamespace,
1500 track_name: Vec<u8>,
1501 subscriber_priority: u8,
1502 group_order: GroupOrder,
1503 forward: Forward,
1504 filter_type: FilterType,
1505 parameters: Vec<KeyValuePair>,
1506 ) -> Result<VarInt, ConnectionError> {
1507 let (req_id, msg) = self.endpoint.track_status(
1508 track_namespace,
1509 track_name,
1510 subscriber_priority,
1511 group_order,
1512 forward,
1513 filter_type,
1514 parameters,
1515 )?;
1516 self.send_control(&msg).await?;
1517 Ok(req_id)
1518 }
1519
1520 /// Accept a track status the peer asked for, sending TRACK_STATUS_OK.
1521 ///
1522 /// The endpoint refuses a second answer to one request, so nothing is
1523 /// written on the wire when it does. It also chooses the Track Alias the
1524 /// message carries, because the draft leaves only one value open.
1525 pub async fn track_status_ok(
1526 &mut self,
1527 request_id: VarInt,
1528 expires: VarInt,
1529 group_order: GroupOrder,
1530 parameters: Vec<KeyValuePair>,
1531 ) -> Result<(), ConnectionError> {
1532 let msg =
1533 self.endpoint.send_track_status_ok(request_id, expires, group_order, parameters)?;
1534 self.send_control(&msg).await
1535 }
1536
1537 /// Refuse a track status the peer asked for, sending TRACK_STATUS_ERROR.
1538 ///
1539 /// The other answer the request can have, and the endpoint holds it to the
1540 /// same count of one.
1541 pub async fn track_status_error(
1542 &mut self,
1543 request_id: VarInt,
1544 error_code: VarInt,
1545 reason_phrase: Vec<u8>,
1546 ) -> Result<(), ConnectionError> {
1547 let msg = self.endpoint.send_track_status_error(request_id, error_code, reason_phrase)?;
1548 self.send_control(&msg).await
1549 }
1550
1551 // ── Publish flow (publisher side) ───────────────────────
1552
1553 /// Offer the peer a subscription to a track this endpoint publishes, and
1554 /// return the Request ID the offer was allocated.
1555 ///
1556 /// Nothing is written on the wire when the endpoint refuses to build the
1557 /// offer, which it does when the Track Alias is one another live track of
1558 /// this session already holds.
1559 #[allow(clippy::too_many_arguments)]
1560 pub async fn publish(
1561 &mut self,
1562 track_namespace: TrackNamespace,
1563 track_name: Vec<u8>,
1564 track_alias: VarInt,
1565 group_order: GroupOrder,
1566 largest_location: Option<Location>,
1567 forward: Forward,
1568 parameters: Vec<KeyValuePair>,
1569 ) -> Result<VarInt, ConnectionError> {
1570 let (req_id, msg) = self.endpoint.publish(
1571 track_namespace,
1572 track_name,
1573 track_alias,
1574 group_order,
1575 largest_location,
1576 forward,
1577 parameters,
1578 )?;
1579 self.send_control(&msg).await?;
1580 Ok(req_id)
1581 }
1582
1583 /// Send a PUBLISH_DONE for the given request ID.
1584 pub async fn publish_done(
1585 &mut self,
1586 request_id: VarInt,
1587 status_code: VarInt,
1588 reason_phrase: Vec<u8>,
1589 ) -> Result<(), ConnectionError> {
1590 let msg = self.endpoint.send_publish_done(request_id, status_code, reason_phrase)?;
1591 self.send_control(&msg).await
1592 }
1593
1594 // ── Malformed Tracks ────────────────────────────────────
1595
1596 /// Send what Section 2.5 asks for when this endpoint finds a track
1597 /// malformed.
1598 ///
1599 /// "it MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for
1600 /// that Track from that publisher" — one message per request, in Request
1601 /// ID order, and the endpoint decides which message each request takes.
1602 ///
1603 /// A write that fails is not reported. The caller is on its way to
1604 /// returning an error that says what went wrong with the track, and a
1605 /// control stream that will not take an UNSUBSCRIBE is a session on its
1606 /// way out for a reason of its own; replacing the condition's report with
1607 /// a transport error would lose the only account of why the track was
1608 /// withdrawn. The rest of the withdrawal is abandoned, because a stream
1609 /// that refused one message will refuse the next.
1610 async fn withdraw_malformed_track(&self, alias: u64, condition: MalformedTrackCondition) {
1611 for msg in self.endpoint.withdraw_malformed_track(alias, condition) {
1612 if self.send_control(&msg).await.is_err() {
1613 break;
1614 }
1615 }
1616 }
1617
1618 /// [`Self::withdraw_malformed_track`] for the track the FETCH `request_id`
1619 /// asked for.
1620 async fn withdraw_malformed_fetch_track(
1621 &self,
1622 request_id: u64,
1623 condition: MalformedTrackCondition,
1624 ) {
1625 for msg in self.endpoint.withdraw_malformed_fetch_track(request_id, condition) {
1626 if self.send_control(&msg).await.is_err() {
1627 break;
1628 }
1629 }
1630 }
1631
1632 /// Record the framing an arriving object was sent with, and withdraw from
1633 /// the track when it is the second framing that track has been sent.
1634 ///
1635 /// The receiving half of a pair. The two writing paths call the endpoint
1636 /// directly and answer a mixed track by refusing to write it, because
1637 /// Section 2.5's sentence is a subscriber's: an endpoint about to send an
1638 /// object is that object's Original Publisher, and a publisher has no
1639 /// subscription of its own to withdraw and no fetch of its own to cancel.
1640 async fn note_received_framing(
1641 &self,
1642 alias: u64,
1643 seen: ObjectForwardingPreference,
1644 ) -> Result<(), ConnectionError> {
1645 let Err(err) = self.endpoint.note_object_forwarding_preference(alias, seen) else {
1646 return Ok(());
1647 };
1648 self.withdraw_malformed_track(alias, MalformedTrackCondition::MixedForwardingPreference)
1649 .await;
1650 Err(err.into())
1651 }
1652
1653 // ── Data streams ────────────────────────────────────────
1654
1655 /// Open a new unidirectional stream for sending subgroup data.
1656 pub async fn open_subgroup_stream(
1657 &self,
1658 header: &AnySubgroupHeader,
1659 ) -> Result<FramedSendStream, ConnectionError> {
1660 // Before the stream is opened: the Original Publisher is who the rule
1661 // binds, so a header that would mix this track's framing is refused
1662 // here rather than written and answered by the peer.
1663 self.endpoint.note_object_forwarding_preference(
1664 header.track_alias(),
1665 ObjectForwardingPreference::Subgroup,
1666 )?;
1667 let send = self.transport.open_uni().await?;
1668 let mut framed = FramedSendStream::new(send, self.draft);
1669 let sid = framed.stream_id();
1670 framed.write_subgroup_header(header).await?;
1671 self.emit(ClientEvent::StreamOpened {
1672 direction: Direction::Send,
1673 stream_kind: StreamKind::Subgroup,
1674 stream_id: sid,
1675 });
1676 self.emit(ClientEvent::DataStreamHeader {
1677 stream_id: sid,
1678 direction: Direction::Send,
1679 header: header.clone(),
1680 });
1681 Ok(framed)
1682 }
1683
1684 /// Open a new unidirectional stream for sending a FETCH's objects.
1685 ///
1686 /// The objects answering a FETCH do not go on the request's own stream:
1687 /// they go on a unidirectional stream of their own, which opens with a
1688 /// FETCH_HEADER naming the request they belong to. This writes that header
1689 /// and hands back the stream, the same way
1690 /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1691 /// subgroup.
1692 ///
1693 /// The caller owns the stream that comes back. Nothing here remembers
1694 /// which request it belongs to, so an endpoint serving several fetches at
1695 /// once keeps its own map from Request ID to stream.
1696 pub async fn open_fetch_stream(
1697 &self,
1698 header: &AnyFetchHeader,
1699 ) -> Result<FramedSendStream, ConnectionError> {
1700 let send = self.transport.open_uni().await?;
1701 let mut framed = FramedSendStream::new(send, self.draft);
1702 let sid = framed.stream_id();
1703 framed.write_fetch_header(header).await?;
1704 self.emit(ClientEvent::StreamOpened {
1705 direction: Direction::Send,
1706 stream_kind: StreamKind::Fetch,
1707 stream_id: sid,
1708 });
1709 Ok(framed)
1710 }
1711
1712 /// Accept the next unidirectional stream and read its fetch header.
1713 ///
1714 /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1715 /// are separate because the header decides how every object after it is
1716 /// framed, so a caller has to know which it is expecting before the first
1717 /// byte is read.
1718 ///
1719 /// Objects come off the returned stream with
1720 /// [`FramedRecvStream::read_fetch_object`].
1721 pub async fn accept_fetch_stream(
1722 &self,
1723 ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1724 let recv = self.transport.accept_uni().await?;
1725 let mut framed = FramedRecvStream::new(recv, self.draft);
1726 let sid = framed.stream_id();
1727 let header = framed.read_fetch_header().await?;
1728 self.emit(ClientEvent::StreamOpened {
1729 direction: Direction::Receive,
1730 stream_kind: StreamKind::Fetch,
1731 stream_id: sid,
1732 });
1733 self.emit(ClientEvent::FetchStreamHeader {
1734 stream_id: sid,
1735 direction: Direction::Receive,
1736 header: header.clone(),
1737 });
1738 // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1739 // carries an `AnySubgroupHeader` and cannot express one. What
1740 // `accept_subgroup_stream` does beyond this - the forwarding-preference
1741 // note, the object measurement - is about a subgroup and has no
1742 // counterpart on a fetch stream.
1743 Ok((header, framed))
1744 }
1745
1746 /// Accept an incoming unidirectional data stream and read its subgroup
1747 /// header.
1748 pub async fn accept_subgroup_stream(
1749 &self,
1750 ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1751 let recv = self.transport.accept_uni().await?;
1752 let mut framed = FramedRecvStream::new(recv, self.draft);
1753 let sid = framed.stream_id();
1754 let header = framed.read_subgroup_header().await?;
1755 self.emit(ClientEvent::StreamOpened {
1756 direction: Direction::Receive,
1757 stream_kind: StreamKind::Subgroup,
1758 stream_id: sid,
1759 });
1760 self.emit(ClientEvent::DataStreamHeader {
1761 stream_id: sid,
1762 direction: Direction::Receive,
1763 header: header.clone(),
1764 });
1765 // Every object on a subgroup stream has the Subgroup preference, so
1766 // the header settles the track's framing before a single object is
1767 // read.
1768 self.note_received_framing(header.track_alias(), ObjectForwardingPreference::Subgroup)
1769 .await?;
1770 // The track is resolved here and not inside the stream: it takes the
1771 // endpoint's alias table, which a stream handle has no way back to.
1772 if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
1773 framed.measure_objects_against(objects, header.group_id());
1774 }
1775 Ok((header, framed))
1776 }
1777
1778 /// Send an object via datagram.
1779 ///
1780 /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1781 /// header whose Object Status the framing it names cannot carry. Such a
1782 /// header errors here and nothing is sent, rather than going out as an
1783 /// ordinary payload datagram with the status quietly dropped.
1784 pub fn send_datagram(
1785 &self,
1786 header: &AnyDatagramHeader,
1787 payload: &[u8],
1788 ) -> Result<(), ConnectionError> {
1789 // Before anything is encoded, for the reason `open_subgroup_stream`
1790 // gives.
1791 self.endpoint.note_object_forwarding_preference(
1792 header.meta().track_alias,
1793 ObjectForwardingPreference::Datagram,
1794 )?;
1795 let mut buf = Vec::new();
1796 header.encode(&mut buf)?;
1797 buf.extend_from_slice(payload);
1798 self.emit(ClientEvent::DatagramReceived {
1799 direction: Direction::Send,
1800 header: header.clone(),
1801 payload_len: payload.len(),
1802 });
1803 self.transport.send_datagram(bytes::Bytes::from(buf))?;
1804 Ok(())
1805 }
1806
1807 /// Receive a datagram and decode it whole.
1808 ///
1809 /// An extension block announced with a length of 0 is returned as
1810 /// [`ConnectionError::EmptyDatagramExtensions`], which
1811 /// [`Connection::close_for_data_stream`] closes the session over.
1812 ///
1813 /// The codec's `DatagramObject::decode_object` then reads the whole datagram,
1814 /// and its refusals are returned as [`ConnectionError::Codec`]: an extension
1815 /// block that does not parse, a payload after a status, and a Prior Group ID
1816 /// Gap or Prior Object ID Gap fault. A gap fault,
1817 /// [`CodecError::MalformedTrack`], and an extension block that does not
1818 /// parse, [`CodecError::MalformedObjectProperties`] (draft-14 Section 11.2),
1819 /// make the track malformed, and this withdraws from it before returning,
1820 /// as draft-14 Section 2.5 requires: "When a subscriber detects a Malformed
1821 /// Track, it MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch
1822 /// for that Track from that publisher, and SHOULD deliver an error to the
1823 /// application." The session stays open.
1824 pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1825 let data = self.transport.recv_datagram().await?;
1826 let mut cursor = &data[..];
1827 let header = match AnyDatagramHeader::decode(self.draft, &mut cursor) {
1828 Ok(header) => header,
1829 Err(CodecError::InvalidField) => {
1830 return Err(empty_datagram_extensions(&data)
1831 .unwrap_or(ConnectionError::Codec(CodecError::InvalidField)));
1832 }
1833 Err(err) => return Err(err.into()),
1834 };
1835 let consumed = data.len() - cursor.len();
1836 let payload = data.slice(consumed..);
1837 self.emit(ClientEvent::DatagramReceived {
1838 direction: Direction::Receive,
1839 header: header.clone(),
1840 payload_len: payload.len(),
1841 });
1842 // Refutable only in a build with more than one draft enabled.
1843 #[allow(irrefutable_let_patterns)]
1844 if let AnyDatagramHeader::Draft14(_) = &header {
1845 let whole =
1846 moqtap_codec::draft14::data_stream::DatagramObject::decode_object(&mut &data[..]);
1847 if let Err(err) = whole {
1848 if let Some(condition) =
1849 malformed_track_condition(&ConnectionError::Codec(err.clone()))
1850 {
1851 self.withdraw_malformed_track(header.meta().track_alias, condition).await;
1852 }
1853 return Err(err.into());
1854 }
1855 }
1856 // A datagram is the other framing, and it settles the track's just as a
1857 // subgroup header does.
1858 self.note_received_framing(header.meta().track_alias, ObjectForwardingPreference::Datagram)
1859 .await?;
1860 // A datagram is a whole object, so the connection can measure it
1861 // without help from the caller - and answer the condition itself,
1862 // because an UNSUBSCRIBE takes the connection an object on a stream
1863 // cannot reach.
1864 let meta = header.meta();
1865 if let Err(err) = self.endpoint.note_received_object(
1866 meta.track_alias,
1867 ObjectLocation { group: meta.group_id, object: meta.object_id },
1868 object_role(meta.status),
1869 ) {
1870 self.withdraw_malformed_track(
1871 meta.track_alias,
1872 MalformedTrackCondition::ObjectPastFinalObject,
1873 )
1874 .await;
1875 return Err(err.into());
1876 }
1877 Ok((header, payload))
1878 }
1879
1880 // ── Accessors ───────────────────────────────────────────
1881
1882 /// Access the underlying endpoint state machine.
1883 pub fn endpoint(&self) -> &Endpoint {
1884 &self.endpoint
1885 }
1886
1887 /// Mutable access to the endpoint state machine.
1888 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
1889 &mut self.endpoint
1890 }
1891
1892 /// The SETUP message the server answered the handshake with.
1893 ///
1894 /// `SERVER_SETUP` through draft-16, the server's half of the unified
1895 /// `SETUP` from draft-17. [`AnyControlMessage::fields`] renders it under
1896 /// this draft's own parameter names, in the order they arrived.
1897 pub fn server_setup(&self) -> &AnyControlMessage {
1898 &self.server_setup
1899 }
1900
1901 /// The framed wire bytes of [`Self::server_setup`], as they arrived.
1902 ///
1903 /// Kept beside the decoded form because the encoding is evidence the
1904 /// decoding discards: two relays sending the same parameter can still
1905 /// disagree on how wide a varint they wrote it in.
1906 pub fn server_setup_raw(&self) -> Option<&[u8]> {
1907 self.server_setup_raw.as_deref()
1908 }
1909
1910 /// Get the negotiated MoQT version.
1911 pub fn negotiated_version(&self) -> Option<VarInt> {
1912 self.endpoint.negotiated_version()
1913 }
1914
1915 /// Returns the draft version this connection is using.
1916 pub fn draft(&self) -> DraftVersion {
1917 self.draft
1918 }
1919
1920 /// Which of this draft's *own* `ConnectionError` variants this error is,
1921 /// and which kind of thing it says.
1922 ///
1923 /// The ten every draft carries answer `None` here: [`AnyConnectionError`]
1924 /// classifies those itself. Draft-14 adds two:
1925 /// [`ConnectionError::ControlMessageNarrowing`], a defect in this build, and
1926 /// [`ConnectionError::EmptyDatagramExtensions`], a decoder refusal the draft
1927 /// answers with a close.
1928 ///
1929 /// Matched exhaustively, with no wildcard arm and deliberately so: a
1930 /// variant added to this draft's error type has to arrive here as a compile
1931 /// error, beside the doc comment that quotes the sentence it enforces,
1932 /// rather than as a silent [`ErrorCause::Unclassified`] in the facade.
1933 ///
1934 /// [`AnyConnectionError`]: crate::dispatch::AnyConnectionError
1935 /// [`ErrorCause::Unclassified`]: crate::dispatch::ErrorCause::Unclassified
1936 pub fn draft_specific_cause(
1937 err: &ConnectionError,
1938 ) -> Option<crate::above_codec_rules::DraftSpecificCause> {
1939 match err {
1940 ConnectionError::Endpoint(_)
1941 | ConnectionError::Codec(_)
1942 | ConnectionError::Transport(_)
1943 | ConnectionError::VarInt(_)
1944 | ConnectionError::NoControlStream
1945 | ConnectionError::UnexpectedEnd
1946 | ConnectionError::StreamFinished
1947 | ConnectionError::InvalidAddress(_)
1948 | ConnectionError::TlsConfig(_)
1949 | ConnectionError::DataStreamState(_) => None,
1950 // This build decoding a message and then failing to narrow it to
1951 // its own draft. Nothing reached the wire and no peer is
1952 // implicated, which is the whole reason it is not
1953 // `ConnectionError::Codec`: under that name it would carry
1954 // `Some(PROTOCOL_VIOLATION)` out of `codec_session_error_code` and
1955 // publish a relay for this build's defect. See the variant's own
1956 // doc.
1957 ConnectionError::ControlMessageNarrowing => {
1958 Some(crate::above_codec_rules::DraftSpecificCause::LocalRefusal)
1959 }
1960 // The codec refuses the datagram as a bare `InvalidField`, and
1961 // `recv_datagram` names the rule, so `close_for_data_stream` reads
1962 // the close here. See the variant's own doc.
1963 ConnectionError::EmptyDatagramExtensions { .. } => {
1964 Some(crate::above_codec_rules::DraftSpecificCause::Codec {
1965 error: CodecError::InvalidField,
1966 close: Some(
1967 moqtap_codec::draft14::error_codes::SessionErrorCode::ProtocolViolation
1968 .as_u64(),
1969 ),
1970 })
1971 }
1972 }
1973 }
1974
1975 /// The code to close the session with when a message could not be decoded
1976 /// because the peer broke a rule draft-14 answers with a close.
1977 ///
1978 /// Every variant listed here comes from a sentence in this draft that names
1979 /// the consequence, and the list is per draft: answering a bound this draft
1980 /// does not state would close a session over traffic a conforming peer may
1981 /// send.
1982 ///
1983 /// - Reason Phrase, maximum 1024 bytes: "If an endpoint receives a length
1984 /// exceeding the maximum, it MUST close the session with a
1985 /// PROTOCOL_VIOLATION."
1986 /// - GOAWAY New Session URI, maximum 8,192 bytes, with the same sentence.
1987 /// Drafts 11 through 19 state it; 07 through 10 state no maximum for
1988 /// the field at all.
1989 /// - Key-Value-Pair value, maximum 2^16-1 bytes, with the same sentence.
1990 /// - Track Namespace tuple size: "If an endpoint receives a Track
1991 /// Namespace tuple with an N of 0 or more than 32, it MUST close the
1992 /// session with a Protocol Violation." Note the lower bound - an empty
1993 /// tuple is refused here, where drafts 17 and later permit one.
1994 /// - Full Track Name, maximum 4,096 bytes, "computed as the sum of the
1995 /// lengths of each Track Namespace tuple field and the Track Name
1996 /// length field". This draft bounds the pair and not the namespace
1997 /// alone; draft-16 widened it.
1998 /// - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
1999 /// check that there are no unauthorized duplicate parameters and close
2000 /// the session as a PROTOCOL_VIOLATION if found." The rule is
2001 /// asymmetric here - "Receivers MUST allow duplicates of unknown
2002 /// parameters", and one known type is granted repeats - and the codec
2003 /// reports only the repeats this draft actually forbids.
2004 /// - Unknown control message type: "An endpoint that receives an unknown
2005 /// message type MUST close the session."
2006 /// - Extension headers on an Object whose status is Object Does Not
2007 /// Exist, Section 10.2.1.2. That one arrives on a data stream or a
2008 /// datagram, so [`Connection::close_for_data_stream`] is what carries
2009 /// it.
2010 ///
2011 /// **Not** the zero-length Track Namespace Field, the delta-encoded
2012 /// parameter type overflow, or the Object ID delta wrap. Those enter the
2013 /// specification at drafts 16 and 18, and this draft states none of them.
2014 ///
2015 /// **Not** the 2^16-1 control message length either. This draft states the
2016 /// limit - "the total length of a control message is limited to 2^16-1
2017 /// bytes" - and states no consequence for exceeding it, so an oversized
2018 /// message is refused by the decoder and stops there.
2019 ///
2020 /// **Not** [`CodecError::UnexpectedEnd`], which reports no rule at all: the
2021 /// reader raises it whenever a message is still arriving, and
2022 /// `read_control` loops on it. Closing over it would end a session on an
2023 /// ordinary short read.
2024 ///
2025 /// **Not** the unknown Message Parameter rule. Drafts 16 through 19 require
2026 /// a close for a Message Parameter whose type the negotiated version does
2027 /// not define. This draft states the opposite and states it about the same
2028 /// parameters: "Receivers MUST allow duplicates of unknown parameters",
2029 /// which presumes an unknown parameter arrives and is carried. Refusing one
2030 /// here would close a session over an extension this draft leaves room for.
2031 ///
2032 /// `None` for everything else, including [`CodecError::InvalidField`]. That
2033 /// variant is shared by a dozen unrelated malformations, only some of which
2034 /// the draft answers with a close, so a session cannot be ended on it
2035 /// without ending sessions the draft does not ask to be ended. Splitting it
2036 /// is the way to bring the rest of those rules under this function;
2037 /// widening the match is not - the extension-header rule above is in this
2038 /// table because it has a variant of its own.
2039 pub fn codec_session_error_code(
2040 err: &CodecError,
2041 ) -> Option<moqtap_codec::draft14::error_codes::SessionErrorCode> {
2042 use moqtap_codec::draft14::error_codes::SessionErrorCode;
2043 use moqtap_codec::kvp::KvpError;
2044 match err {
2045 // The declared Length disagreeing with the fields, which every
2046 // draft answers with a close. Drafts 07 through 10 name no code for
2047 // it, so it takes the one their other unnamed rules take.
2048 // A Filter Type outside the four this draft assigns, Section 9.7:
2049 // "An endpoint that receives a filter type other than the above MUST
2050 // be close the session with PROTOCOL_VIOLATION" — the missing word
2051 // is the draft's. This is the draft the rule gained a consequence
2052 // on. Drafts 07 through 13 write the same sentence as "MUST be
2053 // treated as error", which names none - draft-13 Section 8.7 among
2054 // them - and their tables leave it unanswered.
2055 CodecError::InvalidFilterType(_) => Some(SessionErrorCode::ProtocolViolation),
2056 // A Fetch Type outside the three this draft assigns, Section
2057 // 9.16: "An endpoint that receives a Fetch Type other than 0x1,
2058 // 0x2 or 0x3 MUST be close the session with a PROTOCOL_VIOLATION."
2059 // The missing word is the draft's, as it is for the filter type
2060 // above. The value decides which
2061 // fields follow it — a Standalone fetch carries a track name and a
2062 // range where a joining fetch carries a Request ID and an offset —
2063 // so a reader that cannot name the type cannot find the end of the
2064 // message.
2065 CodecError::InvalidFetchType(_) => Some(SessionErrorCode::ProtocolViolation),
2066 CodecError::ControlMessageLengthMismatch { .. } => {
2067 Some(SessionErrorCode::ProtocolViolation)
2068 }
2069 CodecError::ReasonPhraseTooLong
2070 | CodecError::GoAwayUriTooLong
2071 | CodecError::InvalidNamespaceTupleSize(_)
2072 | CodecError::TrackNameTooLong
2073 | CodecError::DuplicateParameter(_)
2074 | CodecError::UnknownMessageType(_)
2075 | CodecError::ExtensionsOnNonExistentObject(_)
2076 | CodecError::Kvp(KvpError::ValueTooLong(_)) => {
2077 Some(SessionErrorCode::ProtocolViolation)
2078 }
2079 // An unknown data-plane type, Section 10: "An endpoint that
2080 // receives an unknown stream or datagram type MUST close the
2081 // session." One sentence covering two tables, which is why both
2082 // variants sit here.
2083 // A Content Exists field that is neither zero nor one, Section 9.8
2084 // and Section 9.13: "Any other value is a protocol error and MUST
2085 // terminate the session with a PROTOCOL_VIOLATION".
2086 CodecError::InvalidContentExists(_) => Some(SessionErrorCode::ProtocolViolation),
2087 // A Forward field that is neither zero nor one. Section 9.7 and
2088 // Section 9.10 use the sentence above; Section 9.13 says "Any value
2089 // other than 0 or 1 is a PROTOCOL_VIOLATION". Section 9.14 names
2090 // the two legal values without a consequence and is answered the
2091 // same way, for the reason given on the variant. Draft-15 replaces
2092 // the field with the FORWARD parameter, which carries the same rule
2093 // in a different shape and reports it as
2094 // `ParameterValueOutOfRange`.
2095 CodecError::InvalidForward(_) => Some(SessionErrorCode::ProtocolViolation),
2096 CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
2097 Some(SessionErrorCode::ProtocolViolation)
2098 }
2099 // A key-value pair whose value is not the serialization its own
2100 // Type defines, Section 1.4.2: "If a receiver understands a Type,
2101 // and the following Value or Length/Value does not match the
2102 // serialization defined by that Type, the receiver MUST terminate the
2103 // session with error code KEY_VALUE_FORMATTING_ERROR."
2104 //
2105 // Section 9.2.1.1 states the same answer for the one structure this
2106 // draft spells out: "If the Token structure cannot be decoded, the
2107 // receiver MUST close the Session with Key-Value Formatting error."
2108 //
2109 // The one rule in this table that names a code other than Protocol
2110 // Violation.
2111 CodecError::KeyValueFormatting { .. } => {
2112 Some(SessionErrorCode::KeyValueFormattingError)
2113 }
2114 // Everything this draft does not answer, named rather than swept up
2115 // by a wildcard. The arm is exhaustive deliberately: a new
2116 // `CodecError` variant will not compile until it has been placed on
2117 // one side or the other, on this draft, which is the decision a `_`
2118 // arm makes silently and invisibly in every draft module at once.
2119 //
2120 // Adding one variant to `CodecError` produces an `E0004` in every
2121 // draft module that matches it exhaustively, each naming the
2122 // variant that has nowhere to go. That is the whole mechanism.
2123 //
2124 // The nesting stops at `VarInt`, whose variants report how the bytes
2125 // ran out rather than a rule an endpoint states, so there is nothing
2126 // in it for a draft to answer. `Kvp` is spelled out because it does
2127 // carry one.
2128 // Not `ParameterValueOutOfRange`: no parameter this draft defines
2129 // restricts its value's range. Forwarding is a message field here
2130 // and is answered above.
2131 CodecError::ParameterValueOutOfRange { .. }
2132 | CodecError::UnexpectedEnd
2133 | CodecError::MessageTooLong(_)
2134 | CodecError::VarInt(_)
2135 | CodecError::InvalidField
2136 | CodecError::EmptyNamespaceField
2137 | CodecError::InvalidRange(..)
2138 | CodecError::ParameterLengthMismatch(_)
2139 | CodecError::EndOfTrackObjectId(_)
2140 | CodecError::KeyDeltaOverflow(..)
2141 // Not `TrackPropertyValueOutOfRange`: this draft has neither
2142 // namespace the variant is about. Draft-16 opens an extension header
2143 // registry with value rules of its own, and draft-17 renames it to
2144 // the Track Property registry. Before that, everything with a
2145 // restricted range is either a message field or a Message Parameter.
2146 | CodecError::TrackPropertyValueOutOfRange { .. }
2147 | CodecError::ParametersOutOfOrder(..)
2148 | CodecError::ObjectIdOverflow(..)
2149 | CodecError::InvalidRequiredRequestIdDelta(..)
2150 | CodecError::InvalidStreamTypeValue { .. }
2151 | CodecError::InvalidDatagramTypeValue { .. }
2152 | CodecError::UnknownMessageParameter(_)
2153 // Not `ParameterOutOfScope`: this draft states the scope rule and
2154 // answers it the other way. Section 9.2.1 Version Specific Parameters: "Each
2155 // version-specific parameter definition indicates the message types in which it can
2156 // appear. If it appears in some other type of message, it MUST be
2157 // ignored." The codec carries such a parameter on this draft and never
2158 // raises the variant, so this arm records a rule this draft has and
2159 // does not close over, not one it is missing. Draft-17 is where the
2160 // second sentence becomes a close.
2161 | CodecError::ParameterOutOfScope { .. }
2162 // Both belong to the parameter form of the filter. Draft-15 moved
2163 // the Filter Type, Start Location and End Group out of SUBSCRIBE and
2164 // into one length-prefixed parameter; this draft still carries them
2165 // as fields, where a value that runs short is a truncated frame and
2166 // an End Group is written out rather than added to anything.
2167 | CodecError::SubscriptionFilterMalformed { .. }
2168 | CodecError::FilterEndGroupOverflow { .. }
2169 // The object payload rule, Section 10.2.1.1: "Any object with a status
2170 // code other than zero MUST have an empty payload." A MUST on the
2171 // sender with no receiver action named anywhere — the "SHOULD be
2172 // treated as a protocol error" in the same paragraph belongs to the
2173 // sentence before it, which is about a status value this draft does
2174 // not assign — so an object carrying a payload it may not is refused
2175 // and the session stays open.
2176 | CodecError::PayloadNotPermitted { .. }
2177 // A property block that does not parse makes the Track malformed,
2178 // Section 11.2.
2179 | CodecError::MalformedObjectProperties { .. }
2180 // A Malformed Track (Section 2.5) costs the track and not the
2181 // session.
2182 | CodecError::MalformedTrack { .. }
2183 // Raised only by the draft-23 codec.
2184 | CodecError::ControlMessageExceedsLimit { .. }
2185 | CodecError::MissingErrorDetail { .. }
2186 | CodecError::MalformedErrorDetail { .. }
2187 | CodecError::TokenAliasInSetup { .. }
2188 | CodecError::EmptyTrackName
2189 | CodecError::MalformedAuthToken { .. }
2190 | CodecError::TrackPropertiesNotPermitted
2191 | CodecError::DeltaDecodingOverflow { .. }
2192 | CodecError::MalformedObject { .. }
2193 | CodecError::UnknownObjectStatus(_)
2194 | CodecError::UnsupportedDraft(_)
2195 | CodecError::Kvp(
2196 KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
2197 ) => None,
2198 }
2199 }
2200
2201 /// Close the session on the wire when a decode failure is one draft-14
2202 /// answers with a close, and hand the error back unchanged.
2203 /// Without it every bound the decoder enforces would stop at *this endpoint
2204 /// refused the frame* while the peer, which is the one that broke the rule,
2205 /// saw a session that was still open and went on sending. "MUST close the
2206 /// session" is a statement about the wire.
2207 fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
2208 if let ConnectionError::Codec(inner) = &err {
2209 if let Some(code) = Self::codec_session_error_code(inner) {
2210 // QUIC application error codes are 62-bit; every code in this
2211 // registry is far below `u32::MAX`, and saturating rather than
2212 // truncating means a future code that is not could never be
2213 // reported as a different, assigned one.
2214 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2215 self.close(wire_code, inner.to_string().as_bytes());
2216 }
2217 }
2218 err
2219 }
2220
2221 /// Close the session on the wire when the endpoint says a violation is
2222 /// fatal to it, and hand the error back unchanged.
2223 ///
2224 /// [`EndpointError::session_error_code`] answers `Some` for exactly the
2225 /// errors this draft ends the session over, and the endpoint has already
2226 /// moved its own state machine to Closed by the time this runs. Without
2227 /// this step that move is purely internal: the local endpoint refuses to
2228 /// start anything new while the peer, which is the one that broke the
2229 /// rule, sees a session that is still open and goes on sending. A rule
2230 /// that names a session termination code is a statement about the wire,
2231 /// so it takes a CONNECTION_CLOSE to satisfy it.
2232 ///
2233 /// The reason phrase is the error's own `Display` text, which names the
2234 /// rule rather than repeating the numeric code the close already carries.
2235 ///
2236 /// Errors that answer `None` are recoverable and nothing is sent.
2237 fn close_for(&self, err: &EndpointError) {
2238 if let Some(code) = err.session_error_code() {
2239 // QUIC application error codes are 62-bit; every code in this
2240 // registry is far below `u32::MAX`, and saturating rather than
2241 // truncating means a future code that is not could never be
2242 // reported as a different, assigned one.
2243 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2244 self.close(wire_code, err.to_string().as_bytes());
2245 }
2246 }
2247
2248 /// [`close_for`](Self::close_for), then the error unchanged, for the
2249 /// common case where the endpoint's error is also what the caller returns.
2250 fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
2251 self.close_for(&err);
2252 ConnectionError::Endpoint(err)
2253 }
2254
2255 /// Withdraw from a track a data stream found malformed, reporting whether
2256 /// it did.
2257 ///
2258 /// The Malformed Track twin of [`Connection::close_for_data_stream`], and
2259 /// separate from it for the same reason and one more. The same one: a
2260 /// [`FramedRecvStream`] holds no connection, so the reader that finds the
2261 /// fault is not the object that can send an UNSUBSCRIBE. The one more: the
2262 /// two answers are opposites - that call ends the session, this one gives
2263 /// up a track and leaves it running - and a single entry point would have
2264 /// to decide between them from the error alone, which is exactly the
2265 /// decision a caller reproducing a capture wants to make itself.
2266 ///
2267 /// The datagram path needs none of this. It is read through the connection,
2268 /// so [`Connection::recv_datagram`] answers the condition where it finds
2269 /// it, and this is only for the objects that arrive on a stream the caller
2270 /// holds.
2271 pub async fn withdraw_for_data_stream(&self, err: &ConnectionError) -> bool {
2272 let ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) = err
2273 else {
2274 return false;
2275 };
2276 self.withdraw_malformed_track(*alias, MalformedTrackCondition::ObjectPastFinalObject).await;
2277 true
2278 }
2279
2280 /// [`Connection::withdraw_for_data_stream`] for an error read from the
2281 /// subgroup stream `header` opened, which also withdraws from the track
2282 /// over the [`CodecError::MalformedTrack`] that
2283 /// [`FramedRecvStream::read_subgroup_object`] returns for a Prior Group ID
2284 /// Gap or Prior Object ID Gap fault, and over the
2285 /// [`CodecError::MalformedObjectProperties`] it returns for an extension
2286 /// block that does not parse (draft-14 Section 11.2). Those errors name no
2287 /// track, so the header supplies its alias.
2288 ///
2289 /// draft-14 Section 2.5: "When a subscriber detects a Malformed Track, it
2290 /// MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for that
2291 /// Track from that publisher, and SHOULD deliver an error to the
2292 /// application."
2293 pub async fn withdraw_for_subgroup_stream(
2294 &self,
2295 header: &AnySubgroupHeader,
2296 err: &ConnectionError,
2297 ) -> bool {
2298 let Some(condition) = malformed_track_condition(err) else {
2299 return self.withdraw_for_data_stream(err).await;
2300 };
2301 self.withdraw_malformed_track(header.track_alias(), condition).await;
2302 true
2303 }
2304
2305 /// [`Connection::withdraw_for_data_stream`] for an error read from the fetch
2306 /// stream `header` opened, which also withdraws from the track the FETCH
2307 /// asked for over the [`CodecError::MalformedTrack`] that
2308 /// [`FramedRecvStream::read_fetch_object`] returns for a Prior Group ID Gap
2309 /// or Prior Object ID Gap fault, and over the
2310 /// [`CodecError::MalformedObjectProperties`] it returns for an extension
2311 /// block that does not parse (draft-14 Section 11.2). Those errors name no
2312 /// track, so the header's Request ID supplies it.
2313 ///
2314 /// draft-14 Section 2.5: "When a subscriber detects a Malformed Track, it
2315 /// MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for that
2316 /// Track from that publisher, and SHOULD deliver an error to the
2317 /// application."
2318 pub async fn withdraw_for_fetch_stream(
2319 &self,
2320 header: &AnyFetchHeader,
2321 err: &ConnectionError,
2322 ) -> bool {
2323 let Some(condition) = malformed_track_condition(err) else {
2324 return self.withdraw_for_data_stream(err).await;
2325 };
2326 self.withdraw_malformed_fetch_track(header.request_id(), condition).await;
2327 true
2328 }
2329
2330 /// Close the session over a rule broken on a data stream, reporting whether
2331 /// it did.
2332 ///
2333 /// A data stream cannot close for itself the way `recv_control` does:
2334 /// [`Connection::accept_subgroup_stream`] hands the caller a
2335 /// [`FramedRecvStream`] holding no connection, so the reader that finds the
2336 /// violation is not the object that can act on it. Keeping it a separate
2337 /// call is deliberate as well - a permissive caller, one reproducing a
2338 /// capture, can read a violating stream and report it without tearing the
2339 /// session down.
2340 ///
2341 /// The rule this draft answers here is extension headers on an Object whose
2342 /// status is Object Does Not Exist, which reaches subgroup streams, fetch
2343 /// streams and status datagrams alike. It shares `codec_session_error_code`
2344 /// with the control path, so a rule is answered with one code whichever
2345 /// stream carried it.
2346 ///
2347 /// Not every rule that reaches here is the decoder's. A track whose objects
2348 /// mix forwarding preferences is the endpoint's to notice — it takes the
2349 /// alias table to know which track an object belongs to — and it arrives on
2350 /// exactly these streams. Both kinds are asked for a code the same way, and
2351 /// a rule with no code is declined rather than guessed at.
2352 pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
2353 match err {
2354 ConnectionError::Codec(inner) => {
2355 let Some(code) = Self::codec_session_error_code(inner) else { return false };
2356 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2357 self.close(wire_code, inner.to_string().as_bytes());
2358 true
2359 }
2360 // A rule the endpoint raises rather than the decoder. The two
2361 // reach their codes through different tables and mean the same
2362 // thing here: `Some` is a rule this draft ends the session over.
2363 ConnectionError::Endpoint(inner) => {
2364 let Some(code) = inner.session_error_code() else { return false };
2365 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2366 self.close(wire_code, inner.to_string().as_bytes());
2367 true
2368 }
2369 // Not a `Codec` failure: the codec refuses the datagram as a bare
2370 // `InvalidField`, which `recv_datagram` names, so the code comes
2371 // from `draft_specific_cause`.
2372 ConnectionError::EmptyDatagramExtensions { .. } => {
2373 let Some(crate::above_codec_rules::DraftSpecificCause::Codec {
2374 close: Some(code),
2375 ..
2376 }) = Self::draft_specific_cause(err)
2377 else {
2378 return false;
2379 };
2380 self.close(u32::try_from(code).unwrap_or(u32::MAX), err.to_string().as_bytes());
2381 true
2382 }
2383 _ => false,
2384 }
2385 }
2386
2387 /// Close the connection.
2388 pub fn close(&self, code: u32, reason: &[u8]) {
2389 self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
2390 self.transport.close(code, reason);
2391 }
2392}
2393
2394/// The Malformed Track condition a codec refusal on a data path is, on this
2395/// draft, or `None` for a refusal that is not one.
2396fn malformed_track_condition(err: &ConnectionError) -> Option<MalformedTrackCondition> {
2397 match err {
2398 ConnectionError::Codec(CodecError::MalformedTrack { .. }) => {
2399 Some(MalformedTrackCondition::PriorGap)
2400 }
2401 // draft-14 Section 11.2: "A Key-Value-Pair cannot be parsed"
2402 ConnectionError::Codec(CodecError::MalformedObjectProperties { .. }) => {
2403 Some(MalformedTrackCondition::MalformedPropertyBlock)
2404 }
2405 _ => None,
2406 }
2407}
2408
2409/// Determine the encoded length of a varint from its first byte.
2410fn varint_len(first_byte: u8) -> usize {
2411 1 << (first_byte >> 6)
2412}
2413
2414#[cfg(test)]
2415mod tests {
2416 use super::*;
2417
2418 /// Only a refusal of what the peer sent is reported as a refused frame,
2419 /// and only with the bytes it was about.
2420 #[test]
2421 fn only_a_refusal_of_the_peers_frame_is_reported() {
2422 let raw = Some(vec![0x01]);
2423 let decoder = ConnectionError::Codec(CodecError::UnexpectedEnd);
2424 assert!(Connection::refused_event(&decoder, raw.clone()).is_some());
2425 assert!(Connection::refused_event(&decoder, None).is_none());
2426 let local = ConnectionError::Endpoint(EndpointError::NotActive);
2427 assert!(Connection::refused_event(&local, raw).is_none());
2428 }
2429
2430 /// This build failing to narrow a message it decoded is never a finding
2431 /// about the peer.
2432 ///
2433 /// The arm that raises `ControlMessageNarrowing` is unreachable — this
2434 /// draft's decoder can only hand back this draft's variant — and nothing
2435 /// pins that. What is pinned here is the half that matters.
2436 /// `CodecError::UnknownMessageType(0)` is what the arm must not raise:
2437 /// `codec_session_error_code` answers it `Some(PROTOCOL_VIOLATION)` on
2438 /// every draft in range, so the day the narrowing failed a conformance
2439 /// probe would publish a relay for sending a control message type this
2440 /// draft does not assign — with `0x00` attached as the codepoint that
2441 /// proved it, which is an accusation better evidenced than any real one
2442 /// this build makes. The section stating that rule is renumbered several
2443 /// times across the series, and the point does not turn on the
2444 /// number.
2445 #[test]
2446 fn a_message_this_build_could_not_narrow_names_nobody() {
2447 use crate::dispatch::{AnyConnectionError, ErrorCause};
2448
2449 let err: AnyConnectionError = ConnectionError::ControlMessageNarrowing.into();
2450 assert!(err.is_local(), "a narrowing this build could not do is this build's");
2451 assert_eq!(
2452 err.cause(),
2453 &ErrorCause::Facade,
2454 "nothing reached the wire, so there is no rule and no close code to read"
2455 );
2456 }
2457
2458 #[test]
2459 fn varint_len_single_byte() {
2460 // 0b00xxxxxx -> 1 byte
2461 assert_eq!(varint_len(0x00), 1);
2462 assert_eq!(varint_len(0x3F), 1);
2463 }
2464
2465 #[test]
2466 fn varint_len_two_bytes() {
2467 // 0b01xxxxxx -> 2 bytes
2468 assert_eq!(varint_len(0x40), 2);
2469 assert_eq!(varint_len(0x7F), 2);
2470 }
2471
2472 #[test]
2473 fn varint_len_four_bytes() {
2474 // 0b10xxxxxx -> 4 bytes
2475 assert_eq!(varint_len(0x80), 4);
2476 assert_eq!(varint_len(0xBF), 4);
2477 }
2478
2479 #[test]
2480 fn varint_len_eight_bytes() {
2481 // 0b11xxxxxx -> 8 bytes
2482 assert_eq!(varint_len(0xC0), 8);
2483 assert_eq!(varint_len(0xFF), 8);
2484 }
2485
2486 #[test]
2487 fn client_config_supported_versions_draft14() {
2488 let config = ClientConfig {
2489 draft: DraftVersion::Draft14,
2490 additional_versions: Vec::new(),
2491 transport: TransportType::Quic,
2492 skip_cert_verification: false,
2493 ca_certs: Vec::new(),
2494 setup_parameters: Vec::new(),
2495 };
2496 let versions = config.supported_versions();
2497 assert_eq!(versions.len(), 1);
2498 assert_eq!(versions[0].into_inner(), 0xff000000 + 14);
2499 }
2500
2501 #[test]
2502 fn client_config_supported_versions_draft07() {
2503 let config = ClientConfig {
2504 draft: DraftVersion::Draft07,
2505 additional_versions: Vec::new(),
2506 transport: TransportType::Quic,
2507 skip_cert_verification: false,
2508 ca_certs: Vec::new(),
2509 setup_parameters: Vec::new(),
2510 };
2511 let versions = config.supported_versions();
2512 assert_eq!(versions.len(), 1);
2513 assert_eq!(versions[0].into_inner(), 0xff000000 + 7);
2514 }
2515
2516 #[test]
2517 fn client_config_alpn_quic() {
2518 let config = ClientConfig {
2519 draft: DraftVersion::Draft14,
2520 additional_versions: Vec::new(),
2521 transport: TransportType::Quic,
2522 skip_cert_verification: false,
2523 ca_certs: Vec::new(),
2524 setup_parameters: Vec::new(),
2525 };
2526 assert_eq!(config.alpn(), vec![b"moq-00".to_vec()]);
2527 }
2528
2529 #[test]
2530 fn client_config_alpn_webtransport() {
2531 let config = ClientConfig {
2532 draft: DraftVersion::Draft14,
2533 additional_versions: Vec::new(),
2534 transport: TransportType::WebTransport { url: "https://example.com".to_string() },
2535 skip_cert_verification: false,
2536 ca_certs: Vec::new(),
2537 setup_parameters: Vec::new(),
2538 };
2539 assert_eq!(config.alpn(), vec![b"h3".to_vec()]);
2540 }
2541
2542 #[test]
2543 fn moqt_alpn_value() {
2544 assert_eq!(MOQT_ALPN, b"moq-00");
2545 }
2546
2547 #[test]
2548 fn transport_type_debug() {
2549 let quic = TransportType::Quic;
2550 assert!(format!("{quic:?}").contains("Quic"));
2551
2552 let wt = TransportType::WebTransport { url: "https://example.com".to_string() };
2553 assert!(format!("{wt:?}").contains("WebTransport"));
2554 }
2555}