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