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