Skip to main content

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};
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}
60
61impl From<crate::transport::DialError> for ConnectionError {
62    /// Preserves the variants this error had when the dial was inlined here,
63    /// so a caller matching on `InvalidAddress` or `TlsConfig` sees no change.
64    fn from(e: crate::transport::DialError) -> Self {
65        match e {
66            crate::transport::DialError::InvalidAddress(s) => ConnectionError::InvalidAddress(s),
67            crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
68            crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
69        }
70    }
71}
72
73/// Transport type for the connection.
74#[derive(Debug, Clone)]
75pub enum TransportType {
76    /// Raw QUIC via quinn. The `addr` field should be `host:port`.
77    Quic,
78    /// WebTransport via wtransport. The `url` field is the WebTransport URL.
79    WebTransport {
80        /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
81        url: String,
82    },
83}
84
85/// Configuration for a draft-11 MoQT client connection.
86pub struct ClientConfig {
87    /// Additional draft versions to offer in CLIENT_SETUP (draft-11 is always
88    /// offered first).
89    pub additional_versions: Vec<DraftVersion>,
90    /// The transport type (QUIC or WebTransport).
91    pub transport: TransportType,
92    /// Whether to skip TLS certificate verification (for testing).
93    pub skip_cert_verification: bool,
94    /// Custom CA certificates to trust (DER-encoded).
95    pub ca_certs: Vec<Vec<u8>>,
96    /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
97    pub setup_parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
98}
99
100impl ClientConfig {
101    /// Returns the MoQT version varints for the CLIENT_SETUP message.
102    /// Draft-11 first, then any additional versions.
103    pub fn supported_versions(&self) -> Vec<VarInt> {
104        let mut versions = vec![DraftVersion::Draft11.version_varint()];
105        for v in &self.additional_versions {
106            let varint = v.version_varint();
107            if !versions.contains(&varint) {
108                versions.push(varint);
109            }
110        }
111        versions
112    }
113
114    /// Returns the ALPN protocol identifiers for the transport.
115    pub fn alpn(&self) -> Vec<Vec<u8>> {
116        match &self.transport {
117            TransportType::Quic => vec![DraftVersion::Draft11.quic_alpn().to_vec()],
118            TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
119        }
120    }
121}
122
123/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
124pub struct FramedSendStream {
125    inner: SendStream,
126    /// What the subgroup header opened on this stream settled, once one has
127    /// been written.
128    ///
129    /// `None` until a subgroup header has been written, which is what makes an
130    /// object sent before its header answerable rather than unframed bytes.
131    subgroup_objects: Option<SubgroupStreamState>,
132}
133
134/// What a subgroup header fixes for every object written after it.
135///
136/// Two facts, and neither is recoverable from an object on its own: the
137/// Object ID it has to advance past, and whether the stream's type puts an
138/// extension block on each object. The second is the one that used to be
139/// guessed - always "absent" - which wrote a stream a reader could not follow
140/// whenever the header said otherwise.
141#[derive(Debug, Clone, Copy)]
142struct SubgroupStreamState {
143    /// The last Object ID written, or `None` before the first object.
144    previous_object_id: Option<u64>,
145    /// Whether each object writes an Extension Headers Length field.
146    carries_extension_block: bool,
147}
148
149impl FramedSendStream {
150    /// Create a new framed send stream.
151    pub fn new(inner: SendStream) -> Self {
152        Self { inner, subgroup_objects: None }
153    }
154
155    /// Get the transport-level stream ID.
156    pub fn stream_id(&self) -> u64 {
157        self.inner.stream_id()
158    }
159
160    /// Write a control message to the stream with type+length framing.
161    /// Returns the raw bytes that were written (for event capture).
162    pub async fn write_control(
163        &mut self,
164        msg: &AnyControlMessage,
165    ) -> Result<Vec<u8>, ConnectionError> {
166        let mut buf = Vec::new();
167        msg.encode(&mut buf)?;
168        self.inner.write_all(&buf).await?;
169        Ok(buf)
170    }
171
172    /// Write a subgroup stream header. Also opens the Object ID bookkeeping
173    /// [`FramedSendStream::write_subgroup_object`] holds the stream to.
174    ///
175    /// The header is refused, and nothing is written, if its fields disagree
176    /// with its own stream type. That check has to happen here rather than at
177    /// the first object: the type is what every object after it is framed
178    /// against, so a header that went out saying the wrong thing cannot be
179    /// taken back.
180    pub async fn write_subgroup_header(
181        &mut self,
182        header: &AnySubgroupHeader,
183    ) -> Result<(), ConnectionError> {
184        let mut buf = Vec::new();
185        header.encode_stream_checked(&mut buf)?;
186        self.inner.write_all(&buf).await?;
187        // The framing is read off the header now, while the header is in hand.
188        // An object arriving later cannot say whether the stream carries an
189        // extension block, and writing one either way is not a recoverable
190        // mistake: the reader takes the next field along as the missing length
191        // and misframes every object after it.
192        self.subgroup_objects = Some(SubgroupStreamState {
193            previous_object_id: None,
194            carries_extension_block: header.carries_extension_block(),
195        });
196        Ok(())
197    }
198
199    /// Write a fetch response header.
200    pub async fn write_fetch_header(
201        &mut self,
202        header: &AnyFetchHeader,
203    ) -> Result<(), ConnectionError> {
204        let mut buf = Vec::new();
205        header.encode_stream(&mut buf);
206        self.inner.write_all(&buf).await?;
207        Ok(())
208    }
209
210    /// Append a draft-11 subgroup object (header + payload) to the stream.
211    ///
212    /// Section 9.4.2: "A publisher MUST NOT send an Object on a stream if its
213    /// Object ID is less than a previously sent Object ID within a given group
214    /// in that stream." A subgroup stream carries one group, so the Object IDs
215    /// written here are exactly the ones that sentence compares, and the
216    /// comparison needs the object before - which no per-header check can see.
217    /// The state advances only once the object has been written, so declining to
218    /// write an object leaves the next one measured against the last one kept.
219    ///
220    /// An equal Object ID is refused as well as a smaller one. The draft's own
221    /// sentence forbids only "less than", but an Object ID names an Object
222    /// within a Group: writing one twice on a stream describes the same Object
223    /// with two different payloads, and a reader has no way to choose. The
224    /// dispatch-level writer in the codec draws the line in the same place, and
225    /// two writers that disagreed about it would be worse than either answer.
226    ///
227    /// # Errors
228    ///
229    /// [`ConnectionError::DataStreamState`] if no subgroup header has been
230    /// written yet, or if `object` does not advance past the last one written.
231    pub async fn write_subgroup_object(
232        &mut self,
233        object: &SubgroupObject,
234    ) -> Result<(), ConnectionError> {
235        let state = self
236            .subgroup_objects
237            .as_mut()
238            .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
239        let object_id = object.header.object_id.into_inner();
240        if matches!(state.previous_object_id, Some(prev) if object_id <= prev) {
241            return Err(ConnectionError::DataStreamState(
242                "object id does not advance on the last one written to this stream",
243            ));
244        }
245        // The declared length comes from the payload rather than from the
246        // caller's field: a header that disagrees with the bytes beside it
247        // desynchronises every object after it on the stream, and nothing
248        // downstream can recover.
249        let mut header = object.header.clone();
250        header.payload_length = VarInt::from_usize(object.payload.len());
251        let mut buf = Vec::new();
252        header.encode_checked_with_extensions(state.carries_extension_block, &mut buf)?;
253        buf.extend_from_slice(&object.payload);
254        self.inner.write_all(&buf).await?;
255        state.previous_object_id = Some(object_id);
256        Ok(())
257    }
258
259    /// Append a draft-11 fetch object (header + payload) to the stream.
260    pub async fn write_fetch_object(
261        &mut self,
262        object: &FetchObject,
263    ) -> Result<(), ConnectionError> {
264        // The declared length comes from the payload rather than from the
265        // caller's field: a header that disagrees with the bytes beside it
266        // desynchronises every object after it on the stream, and nothing
267        // downstream can recover.
268        let mut header = object.header.clone();
269        header.payload_length = VarInt::from_usize(object.payload.len());
270        let mut buf = Vec::new();
271        header.encode_checked(&mut buf)?;
272        buf.extend_from_slice(&object.payload);
273        self.inner.write_all(&buf).await?;
274        Ok(())
275    }
276
277    /// Finish the stream (send FIN).
278    pub async fn finish(&mut self) -> Result<(), ConnectionError> {
279        self.inner.finish()?;
280        Ok(())
281    }
282}
283
284/// What an Object Status makes of an object, for Section 9.1.1.1's table.
285///
286/// `None` is Normal and not "no status": a datagram carrying a payload has no
287/// status field at all, and an object with a payload is an ordinary object.
288fn object_role(status: Option<u64>) -> ObjectRole {
289    match status {
290        None | Some(0x0) => ObjectRole::Produced,
291        // 0x4, end of Track. This draft folded drafts 08 through 10's second
292        // end-of-track status into this one and kept the looser of the two
293        // conditions, so the Group ID may equal the largest group seen.
294        Some(0x4) => ObjectRole::EndsTrack(Some(EndOfTrackForm::LastGroup)),
295        // Every other status is a statement about objects rather than one of
296        // them. Two of them name an Object ID one past the largest on purpose,
297        // so counting one as produced would refuse the end-of-track object the
298        // draft goes on to define.
299        _ => ObjectRole::Neither,
300    }
301}
302
303/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
304pub struct FramedRecvStream {
305    inner: RecvStream,
306    buf: BytesMut,
307    /// The record this stream's objects are measured against, and the Group ID
308    /// its header named.
309    ///
310    /// One group for the whole stream: a subgroup header names it once and no
311    /// object header repeats it. `None` on a stream that was never given one —
312    /// a stream for an alias no live binding names, and every stream built
313    /// outside [`Connection::accept_subgroup_stream`] — and such a stream reads
314    /// exactly as it did before this existed.
315    tracking: Option<(TrackObjects, u64)>,
316    /// Whether the subgroup stream this reader is on carries an extension block
317    /// on every object.
318    ///
319    /// The stream's Type says so and nothing in an object header repeats it, so
320    /// a reader that does not remember the Type cannot parse the objects at all:
321    /// on an extensions-bearing stream it reads the Extension Headers Length as
322    /// the Object Payload Length and every field after it is nonsense.
323    ///
324    /// Set by [`FramedRecvStream::read_subgroup_header`] and read by
325    /// [`FramedRecvStream::read_subgroup_object`]. `false` until a subgroup
326    /// header has been read, which is the only order those two may be called
327    /// in.
328    subgroup_has_extensions: bool,
329}
330
331impl FramedRecvStream {
332    /// Create a new framed receive stream.
333    pub fn new(inner: RecvStream) -> Self {
334        Self {
335            inner,
336            buf: BytesMut::with_capacity(4096),
337            subgroup_has_extensions: false,
338            tracking: None,
339        }
340    }
341
342    /// Measure this stream's objects against `objects`, all of them in `group`.
343    ///
344    /// Called by [`Connection::accept_subgroup_stream`] once the header has been
345    /// read, which is the only point at which both the track and the group are
346    /// known.
347    fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
348        self.tracking = Some((objects, group));
349    }
350
351    /// Record or judge one object this stream carried.
352    ///
353    /// The whole of Section 9.1.1.1's rule that a single header cannot settle: the
354    /// object's Group ID is the stream's, its Object ID is its own, and what
355    /// they are measured against is everything the track has carried on any
356    /// stream.
357    fn note_subgroup_object(&self, header: &ObjectHeader) -> Result<(), ConnectionError> {
358        let Some((objects, group)) = &self.tracking else { return Ok(()) };
359        let at = ObjectLocation { group: *group, object: header.object_id.into_inner() };
360        objects.note(at, object_role(Some(header.object_status as u64))).map_err(|placement| {
361            ConnectionError::Endpoint(EndpointError::EndOfTrackOutOfPlace {
362                alias: objects.alias(),
363                group: at.group,
364                object: at.object,
365                placement,
366            })
367        })
368    }
369
370    /// Get the transport-level stream ID.
371    pub fn stream_id(&self) -> u64 {
372        self.inner.stream_id()
373    }
374
375    /// Read more data from the stream into the internal buffer.
376    async fn fill(&mut self) -> Result<bool, ConnectionError> {
377        let mut tmp = [0u8; 4096];
378        match self.inner.read(&mut tmp).await {
379            Ok(Some(n)) => {
380                self.buf.extend_from_slice(&tmp[..n]);
381                Ok(true)
382            }
383            Ok(None) => Ok(false),
384            Err(e) => Err(ConnectionError::Transport(e)),
385        }
386    }
387
388    /// Ensure at least `n` bytes are available in the buffer.
389    async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
390        while self.buf.len() < n {
391            if !self.fill().await? {
392                return Err(ConnectionError::UnexpectedEnd);
393            }
394        }
395        Ok(())
396    }
397
398    /// Read a control message from the stream.
399    ///
400    /// When `capture_raw` is true, the returned tuple includes a clone of the
401    /// framed wire bytes (for observer emission). When false, the second
402    /// element is `None` and the payload clone is skipped.
403    pub async fn read_control(
404        &mut self,
405        capture_raw: bool,
406    ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
407        // Read type ID varint
408        self.ensure(1).await?;
409        let type_len = varint_len(self.buf[0]);
410        self.ensure(type_len).await?;
411
412        let mut cursor = &self.buf[..type_len];
413        let _type_id = VarInt::decode(&mut cursor)?;
414
415        // Draft-11 uses a fixed 16-bit length after the type id.
416        let len_field_size = 2;
417        self.ensure(type_len + len_field_size).await?;
418        let payload_len = u16::from_be_bytes([self.buf[type_len], self.buf[type_len + 1]]) as usize;
419
420        // Read full payload
421        let total = type_len + len_field_size + payload_len;
422        self.ensure(total).await?;
423
424        // Capture raw bytes only if requested (observer attached).
425        let raw = capture_raw.then(|| self.buf[..total].to_vec());
426
427        // Now decode the whole message using the draft-11 dispatcher
428        let mut frame = &self.buf[..total];
429        let msg = AnyControlMessage::decode(DraftVersion::Draft11, &mut frame)?;
430        self.buf.advance(total);
431        Ok((msg, raw))
432    }
433
434    /// Read a subgroup stream header.
435    pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
436        self.ensure(1).await?;
437        loop {
438            let mut cursor = &self.buf[..];
439            match AnySubgroupHeader::decode_stream(DraftVersion::Draft11, &mut cursor) {
440                Ok(header) => {
441                    let consumed = self.buf.len() - cursor.remaining();
442                    self.buf.advance(consumed);
443                    // Clippy would rather see these two arms as an `if let`, and rustc rejects
444                    // that in a single-draft build, where the pattern is irrefutable. Only a
445                    // `match` satisfies both.
446                    #[allow(clippy::single_match)]
447                    match header {
448                        AnySubgroupHeader::Draft11(ref d) => {
449                            self.subgroup_has_extensions = d.stream_type.has_extensions();
450                        }
451                        // Only this draft's header sets the flag. With draft 11 the only enabled
452                        // draft `AnySubgroupHeader` has a single variant, the arm above is
453                        // exhaustive and this one unreachable. Compiled in every configuration with
454                        // the lint allowed, rather than gated on a `cfg` naming the other thirteen
455                        // drafts: that list had to be edited in every draft module whenever a draft
456                        // was added, and a copy that omitted one left this match non-exhaustive.
457                        #[allow(unreachable_patterns)]
458                        _ => {}
459                    }
460                    return Ok(header);
461                }
462                Err(CodecError::UnexpectedEnd) => {
463                    if !self.fill().await? {
464                        return Err(ConnectionError::UnexpectedEnd);
465                    }
466                }
467                Err(e) => return Err(ConnectionError::Codec(e)),
468            }
469        }
470    }
471
472    /// Read a fetch response header.
473    pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
474        self.ensure(1).await?;
475        loop {
476            let mut cursor = &self.buf[..];
477            match AnyFetchHeader::decode_stream(DraftVersion::Draft11, &mut cursor) {
478                Ok(header) => {
479                    let consumed = self.buf.len() - cursor.remaining();
480                    self.buf.advance(consumed);
481                    return Ok(header);
482                }
483                Err(CodecError::UnexpectedEnd) => {
484                    if !self.fill().await? {
485                        return Err(ConnectionError::UnexpectedEnd);
486                    }
487                }
488                Err(e) => return Err(ConnectionError::Codec(e)),
489            }
490        }
491    }
492
493    /// Read the next draft-11 subgroup object (header + payload).
494    ///
495    /// Whether an object carries an extension block is a property of the
496    /// stream's Type, not of the object, so this reads it from the header
497    /// [`FramedRecvStream::read_subgroup_header`] recorded rather than assuming
498    /// either answer. Assuming "no extensions" is not a conservative default: on
499    /// a stream whose Type announces them the Extension Headers Length is read
500    /// as the Object Payload Length, and every object on the stream comes back
501    /// wrong instead of being refused.
502    ///
503    /// It is also what puts the rule binding extension headers to Object Status
504    /// within reach on a subgroup stream. A reader that never reads the block
505    /// cannot notice that a non-existent Object carried one.
506    pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
507        loop {
508            let mut cursor = &self.buf[..];
509            match ObjectHeader::decode_with_extensions(self.subgroup_has_extensions, &mut cursor) {
510                Ok(header) => {
511                    let header_consumed = self.buf.len() - cursor.remaining();
512                    let payload_len = header.payload_length.into_inner() as usize;
513                    let total = header_consumed + payload_len;
514                    if self.buf.len() < total {
515                        if !self.fill().await? {
516                            return Err(ConnectionError::UnexpectedEnd);
517                        }
518                        continue;
519                    }
520                    let payload = self.buf[header_consumed..total].to_vec();
521                    self.buf.advance(total);
522                    self.note_subgroup_object(&header)?;
523                    return Ok(SubgroupObject { header, payload });
524                }
525                Err(CodecError::UnexpectedEnd) => {
526                    if !self.fill().await? {
527                        return Err(ConnectionError::UnexpectedEnd);
528                    }
529                }
530                Err(e) => return Err(ConnectionError::Codec(e)),
531            }
532        }
533    }
534
535    /// Read the next draft-11 fetch object (header + payload).
536    pub async fn read_fetch_object(&mut self) -> Result<FetchObject, ConnectionError> {
537        loop {
538            let mut cursor = &self.buf[..];
539            match FetchObjectHeader::decode(&mut cursor) {
540                Ok(header) => {
541                    let header_consumed = self.buf.len() - cursor.remaining();
542                    let payload_len = header.payload_length.into_inner() as usize;
543                    let total = header_consumed + payload_len;
544                    if self.buf.len() < total {
545                        if !self.fill().await? {
546                            return Err(ConnectionError::UnexpectedEnd);
547                        }
548                        continue;
549                    }
550                    let payload = self.buf[header_consumed..total].to_vec();
551                    self.buf.advance(total);
552                    return Ok(FetchObject { header, payload });
553                }
554                Err(CodecError::UnexpectedEnd) => {
555                    if !self.fill().await? {
556                        return Err(ConnectionError::UnexpectedEnd);
557                    }
558                }
559                Err(e) => return Err(ConnectionError::Codec(e)),
560            }
561        }
562    }
563}
564
565/// A live draft-11 MoQT connection over QUIC or WebTransport.
566pub struct Connection {
567    transport: Transport,
568    endpoint: Endpoint,
569    control_send: Option<FramedSendStream>,
570    control_recv: Option<FramedRecvStream>,
571    observer: Option<Box<dyn ConnectionObserver>>,
572    /// Setup events buffered during `connect()` and replayed when an
573    /// observer attaches via `set_observer` — without this, an observer
574    /// attached after `connect` returns would never see the handshake.
575    pending_events: Vec<ClientEvent>,
576}
577
578impl Connection {
579    /// Connect to a draft-11 MoQT server as a client.
580    pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
581        // PATH is for native QUIC only, and the transport is known here and
582        // nowhere further in. Refusing before dialling means a session that
583        // the server would close on sight is never opened.
584        setup::validate_client_path_transport(
585            &config.setup_parameters,
586            matches!(config.transport, TransportType::WebTransport { .. }),
587        )
588        .map_err(EndpointError::from)?;
589
590        let transport = match &config.transport {
591            TransportType::Quic => Self::connect_quic(addr, &config).await?,
592            TransportType::WebTransport { url } => {
593                let url = url.clone();
594                Self::connect_webtransport(&url, &config).await?
595            }
596        };
597
598        Self::adopt(transport, config).await
599    }
600
601    /// Run the MoQT setup handshake over a transport somebody else established.
602    ///
603    /// For choosing the draft from what the server selected: dial once through
604    /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
605    /// connection to the module its answer names. [`Self::connect`] cannot do
606    /// this — it derives its single ALPN from the draft it was given.
607    ///
608    /// `config.draft` must match this module. The transport is adopted as
609    /// given; nothing here re-checks the ALPN it was negotiated with.
610    pub async fn adopt(
611        transport: Transport,
612        config: ClientConfig,
613    ) -> Result<Self, ConnectionError> {
614        // PATH is for native QUIC only, and the transport is known here and
615        // nowhere further in. Refusing before dialling means a session that
616        // the server would close on sight is never opened.
617        setup::validate_client_path_transport(
618            &config.setup_parameters,
619            matches!(config.transport, TransportType::WebTransport { .. }),
620        )
621        .map_err(EndpointError::from)?;
622
623        // Open bidirectional control stream
624        let (send, recv) = transport.open_bi().await?;
625        let mut control_send = FramedSendStream::new(send);
626        let mut control_recv = FramedRecvStream::new(recv);
627
628        // Perform setup handshake
629        let mut endpoint = Endpoint::new(Role::Client);
630        endpoint.connect()?;
631        let setup_msg = endpoint
632            .send_client_setup(config.supported_versions(), config.setup_parameters.clone())?;
633        let any_setup = AnyControlMessage::Draft11(setup_msg);
634        let raw_setup = control_send.write_control(&any_setup).await?;
635
636        let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
637        match &server_setup {
638            AnyControlMessage::Draft11(ControlMessage::ServerSetup(ref ss)) => {
639                endpoint.receive_server_setup(ss)?;
640            }
641            _ => {
642                return Err(ConnectionError::Endpoint(EndpointError::NotActive));
643            }
644        }
645
646        let mut pending_events = Vec::with_capacity(3);
647        pending_events.push(ClientEvent::ControlMessage {
648            direction: Direction::Send,
649            message: any_setup,
650            raw: Some(raw_setup),
651        });
652        pending_events.push(ClientEvent::ControlMessage {
653            direction: Direction::Receive,
654            message: server_setup,
655            raw: raw_server_setup,
656        });
657        if let Some(v) = endpoint.negotiated_version() {
658            pending_events.push(ClientEvent::SetupComplete { negotiated_version: v.into_inner() });
659        }
660
661        Ok(Self {
662            transport,
663            endpoint,
664            control_send: Some(control_send),
665            control_recv: Some(control_recv),
666            observer: None,
667            pending_events,
668        })
669    }
670
671    /// Establish a raw QUIC connection.
672    ///
673    /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
674    /// TLS and endpoint setup.
675    async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
676        let (transport, _negotiated) = crate::transport::dial_quic(
677            addr,
678            &crate::transport::QuicDialOptions {
679                skip_cert_verification: config.skip_cert_verification,
680                ca_certs: config.ca_certs.clone(),
681                alpn: config.alpn(),
682            },
683        )
684        .await?;
685        Ok(transport)
686    }
687
688    /// Establish a WebTransport connection.
689    #[cfg(feature = "webtransport")]
690    async fn connect_webtransport(
691        url: &str,
692        config: &ClientConfig,
693    ) -> Result<Transport, ConnectionError> {
694        use crate::transport::webtransport::WebTransportTransport;
695
696        let wt_config = if config.skip_cert_verification {
697            wtransport::ClientConfig::builder()
698                .with_bind_default()
699                .with_no_cert_validation()
700                .build()
701        } else {
702            wtransport::ClientConfig::builder().with_bind_default().with_native_certs().build()
703        };
704
705        let endpoint = wtransport::Endpoint::client(wt_config)
706            .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
707
708        let connection = endpoint
709            .connect(url)
710            .await
711            .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
712
713        Ok(Transport::WebTransport(WebTransportTransport::new(connection)))
714    }
715
716    /// Stub for when the webtransport feature is not enabled.
717    #[cfg(not(feature = "webtransport"))]
718    async fn connect_webtransport(
719        _url: &str,
720        _config: &ClientConfig,
721    ) -> Result<Transport, ConnectionError> {
722        Err(ConnectionError::Transport(TransportError::Connect(
723            "webtransport feature not enabled".into(),
724        )))
725    }
726
727    // ── Observer ───────────────────────────────────────────────
728
729    /// Attach an observer. Buffered handshake events from `connect()` are
730    /// flushed in arrival order before this returns.
731    pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
732        self.observer = Some(observer);
733        for event in self.pending_events.drain(..) {
734            if let Some(ref obs) = self.observer {
735                obs.on_event_owned(event);
736            }
737        }
738    }
739
740    /// Remove the observer.
741    pub fn clear_observer(&mut self) {
742        self.observer = None;
743    }
744
745    /// Emit an event to the observer, if one is attached.
746    fn emit(&self, event: ClientEvent) {
747        if let Some(ref obs) = self.observer {
748            obs.on_event_owned(event);
749        }
750    }
751
752    // ── Control message I/O ─────────────────────────────────
753
754    /// Send a control message on the control stream.
755    pub async fn send_control(&mut self, msg: &ControlMessage) -> Result<(), ConnectionError> {
756        let any = AnyControlMessage::Draft11(msg.clone());
757        let send = self.control_send.as_mut().ok_or(ConnectionError::NoControlStream)?;
758        let raw = send.write_control(&any).await?;
759        self.emit(ClientEvent::ControlMessage {
760            direction: Direction::Send,
761            message: any,
762            raw: Some(raw),
763        });
764        Ok(())
765    }
766
767    /// Read the next control message from the control stream.
768    pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
769        let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
770        let capture_raw = self.observer.is_some();
771        let (any, raw) = match recv.read_control(capture_raw).await {
772            Ok(v) => v,
773            Err(e) => return Err(self.close_for_codec(e)),
774        };
775        if capture_raw {
776            self.emit(ClientEvent::ControlMessage {
777                direction: Direction::Receive,
778                message: any.clone(),
779                raw,
780            });
781        }
782        match any {
783            AnyControlMessage::Draft11(msg) => Ok(msg),
784            // `AnyControlMessage` carries one variant per enabled draft feature. With draft 11 the
785            // only one enabled the arm above is exhaustive and this rejection arm unreachable.
786            // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
787            // naming the other thirteen drafts: that list had to be edited in every draft module
788            // whenever a draft was added, and a copy that omitted one left this match
789            // non-exhaustive.
790            #[allow(unreachable_patterns)]
791            _ => Err(ConnectionError::Codec(CodecError::UnknownMessageType(0))),
792        }
793    }
794
795    /// Read and dispatch the next incoming control message through the endpoint
796    /// state machine. Returns the decoded message for inspection.
797    pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
798        let msg = self.recv_control().await?;
799        self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
800
801        if let ControlMessage::GoAway(ref ga) = msg {
802            self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
803        }
804
805        Ok(msg)
806    }
807
808    // ── Subscribe flow ──────────────────────────────────────
809
810    /// Send a SUBSCRIBE and return the allocated request ID.
811    #[allow(clippy::too_many_arguments)]
812    pub async fn subscribe(
813        &mut self,
814        track_alias: VarInt,
815        track_namespace: TrackNamespace,
816        track_name: Vec<u8>,
817        subscriber_priority: u8,
818        group_order: GroupOrder,
819        filter_type: VarInt,
820    ) -> Result<VarInt, ConnectionError> {
821        let (req_id, msg) = self.endpoint.subscribe(
822            track_alias,
823            track_namespace,
824            track_name,
825            subscriber_priority,
826            group_order,
827            filter_type,
828        )?;
829        self.send_control(&msg).await?;
830        Ok(req_id)
831    }
832
833    /// Send a SUBSCRIBE for a range of the track and return the allocated ID.
834    ///
835    /// The Filter Type comes from the arguments, so the message cannot name a
836    /// filter whose fields it does not carry.
837    #[allow(clippy::too_many_arguments)]
838    pub async fn subscribe_range(
839        &mut self,
840        track_alias: VarInt,
841        track_namespace: TrackNamespace,
842        track_name: Vec<u8>,
843        subscriber_priority: u8,
844        group_order: GroupOrder,
845        start_location: Location,
846        end_group: Option<VarInt>,
847    ) -> Result<VarInt, ConnectionError> {
848        let (req_id, msg) = self.endpoint.subscribe_range(
849            track_alias,
850            track_namespace,
851            track_name,
852            subscriber_priority,
853            group_order,
854            start_location,
855            end_group,
856        )?;
857        self.send_control(&msg).await?;
858        Ok(req_id)
859    }
860
861    /// Send an UNSUBSCRIBE for the given request ID.
862    pub async fn unsubscribe(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
863        let msg = self.endpoint.unsubscribe(request_id)?;
864        self.send_control(&msg).await
865    }
866
867    /// Accept a subscription the peer opened, sending SUBSCRIBE_OK.
868    pub async fn subscribe_ok(
869        &mut self,
870        request_id: VarInt,
871        expires: VarInt,
872        group_order: GroupOrder,
873        parameters: Vec<KeyValuePair>,
874    ) -> Result<(), ConnectionError> {
875        let msg = self.endpoint.send_subscribe_ok(request_id, expires, group_order, parameters)?;
876        self.send_control(&msg).await
877    }
878
879    /// Reject a subscription the peer opened, sending SUBSCRIBE_ERROR.
880    ///
881    /// The Track Alias travels back with the refusal: under the 'Retry Track
882    /// Alias' code it is the alias the peer should try again with, and under
883    /// any other code it is ignored.
884    pub async fn subscribe_error(
885        &mut self,
886        request_id: VarInt,
887        error_code: VarInt,
888        reason_phrase: Vec<u8>,
889        track_alias: VarInt,
890    ) -> Result<(), ConnectionError> {
891        let msg = self.endpoint.send_subscribe_error(
892            request_id,
893            error_code,
894            reason_phrase,
895            track_alias,
896        )?;
897        self.send_control(&msg).await
898    }
899
900    /// End a subscription this endpoint accepted, sending SUBSCRIBE_DONE.
901    pub async fn subscribe_done(
902        &mut self,
903        request_id: VarInt,
904        status_code: VarInt,
905        reason_phrase: Vec<u8>,
906    ) -> Result<(), ConnectionError> {
907        let msg = self.endpoint.send_subscribe_done(request_id, status_code, reason_phrase)?;
908        self.send_control(&msg).await
909    }
910
911    // ── Fetch flow ──────────────────────────────────────────
912
913    /// Send a FETCH and return the allocated request ID.
914    #[allow(clippy::too_many_arguments)]
915    pub async fn fetch(
916        &mut self,
917        track_namespace: TrackNamespace,
918        track_name: Vec<u8>,
919        subscriber_priority: u8,
920        group_order: GroupOrder,
921        start_group: VarInt,
922        start_object: VarInt,
923        end_group: VarInt,
924        end_object: VarInt,
925    ) -> Result<VarInt, ConnectionError> {
926        let (req_id, msg) = self.endpoint.fetch(
927            track_namespace,
928            track_name,
929            subscriber_priority,
930            group_order,
931            start_group,
932            start_object,
933            end_group,
934            end_object,
935        )?;
936        self.send_control(&msg).await?;
937        Ok(req_id)
938    }
939
940    /// Send a Relative Joining Fetch and return the allocated request ID.
941    ///
942    /// `joining_start` counts groups back from the live edge of the
943    /// subscription it names. Section 8.13.1 computes the range from "the
944    /// Preceding Group Offset", which is this field under the name it carried
945    /// one draft ago; the field list calls it Joining Start and says that for
946    /// a Relative Joining Fetch "a value of 0 indicates the Fetch starts at
947    /// the beginning of the Current Group". For the form that names the group
948    /// outright, see
949    /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
950    ///
951    /// The subscription is named by `joining_subscribe_id`, which is the
952    /// field's name in this draft even though the draft describes it as "The
953    /// Request ID of the existing subscription to be joined".
954    pub async fn joining_fetch(
955        &mut self,
956        subscriber_priority: u8,
957        group_order: GroupOrder,
958        joining_subscribe_id: VarInt,
959        joining_start: VarInt,
960    ) -> Result<VarInt, ConnectionError> {
961        let (req_id, msg) = self.endpoint.joining_fetch(
962            subscriber_priority,
963            group_order,
964            joining_subscribe_id,
965            joining_start,
966        )?;
967        self.send_control(&msg).await?;
968        Ok(req_id)
969    }
970
971    /// Send an Absolute Joining Fetch and return the allocated request ID.
972    ///
973    /// `joining_start` is the group to begin at. Section 8.13 has an Absolute
974    /// Joining Fetch "Identical to a Relative Joining Fetch except that the
975    /// Start Group is determined by an absolute Group value rather than a
976    /// relative offset to the subscription", so a subscriber that knows the
977    /// group it wants can ask for it without first being told a Largest Group
978    /// to count back from.
979    ///
980    /// Two calls rather than one taking a Fetch Type, because these are the
981    /// only two the joining form admits. The endpoint is named the same way,
982    /// so no call between an application and the wire has a Fetch Type in it
983    /// to be given a wrong one.
984    pub async fn absolute_joining_fetch(
985        &mut self,
986        subscriber_priority: u8,
987        group_order: GroupOrder,
988        joining_subscribe_id: VarInt,
989        joining_start: VarInt,
990    ) -> Result<VarInt, ConnectionError> {
991        let (req_id, msg) = self.endpoint.absolute_joining_fetch(
992            subscriber_priority,
993            group_order,
994            joining_subscribe_id,
995            joining_start,
996        )?;
997        self.send_control(&msg).await?;
998        Ok(req_id)
999    }
1000
1001    /// Send a FETCH_CANCEL for the given request ID.
1002    pub async fn fetch_cancel(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1003        let msg = self.endpoint.fetch_cancel(request_id)?;
1004        self.send_control(&msg).await
1005    }
1006
1007    /// Accept a fetch the peer opened, sending FETCH_OK.
1008    ///
1009    /// The endpoint refuses a Joining Fetch naming a subscription this session
1010    /// cannot join and refuses a second answer to one FETCH, so nothing is
1011    /// written on the wire when it does either.
1012    pub async fn fetch_ok(
1013        &mut self,
1014        request_id: VarInt,
1015        group_order: GroupOrder,
1016        end_of_track: u8,
1017        end_location: Location,
1018        parameters: Vec<KeyValuePair>,
1019    ) -> Result<(), ConnectionError> {
1020        let msg = self.endpoint.send_fetch_ok(
1021            request_id,
1022            group_order,
1023            end_of_track,
1024            end_location,
1025            parameters,
1026        )?;
1027        self.send_control(&msg).await
1028    }
1029
1030    /// Refuse a fetch the peer opened, sending FETCH_ERROR.
1031    ///
1032    /// The endpoint refuses a second answer to one FETCH, and refuses a
1033    /// Joining Fetch's refusal under any code but the one the draft names for
1034    /// it, so nothing is written on the wire when it does either.
1035    pub async fn fetch_error(
1036        &mut self,
1037        request_id: VarInt,
1038        error_code: VarInt,
1039        reason_phrase: Vec<u8>,
1040    ) -> Result<(), ConnectionError> {
1041        let msg = self.endpoint.send_fetch_error(request_id, error_code, reason_phrase)?;
1042        self.send_control(&msg).await
1043    }
1044
1045    // ── Namespace flows ─────────────────────────────────────
1046
1047    /// Send a SUBSCRIBE_ANNOUNCES. Returns the allocated request ID.
1048    pub async fn subscribe_announces(
1049        &mut self,
1050        track_namespace_prefix: TrackNamespace,
1051    ) -> Result<VarInt, ConnectionError> {
1052        let (req_id, msg) = self.endpoint.subscribe_announces(track_namespace_prefix)?;
1053        self.send_control(&msg).await?;
1054        Ok(req_id)
1055    }
1056
1057    /// Accept a namespace subscription the peer made, sending SUBSCRIBE_ANNOUNCES_OK.
1058    ///
1059    /// The endpoint refuses a second answer to one SUBSCRIBE_ANNOUNCES, so nothing is
1060    /// written on the wire when it does.
1061    pub async fn subscribe_announces_ok(
1062        &mut self,
1063        request_id: VarInt,
1064    ) -> Result<(), ConnectionError> {
1065        let msg = self.endpoint.send_subscribe_announces_ok(request_id)?;
1066        self.send_control(&msg).await
1067    }
1068
1069    /// Refuse a namespace subscription the peer made, sending SUBSCRIBE_ANNOUNCES_ERROR.
1070    ///
1071    /// The other half of the same sentence: one answer, and this is the other
1072    /// one it can be.
1073    pub async fn subscribe_announces_error(
1074        &mut self,
1075        request_id: VarInt,
1076        error_code: VarInt,
1077        reason_phrase: Vec<u8>,
1078    ) -> Result<(), ConnectionError> {
1079        let msg =
1080            self.endpoint.send_subscribe_announces_error(request_id, error_code, reason_phrase)?;
1081        self.send_control(&msg).await
1082    }
1083
1084    /// Send an ANNOUNCE. Returns the allocated request ID.
1085    pub async fn announce(
1086        &mut self,
1087        track_namespace: TrackNamespace,
1088    ) -> Result<VarInt, ConnectionError> {
1089        let (req_id, msg) = self.endpoint.announce(track_namespace)?;
1090        self.send_control(&msg).await?;
1091        Ok(req_id)
1092    }
1093
1094    /// Send an UNANNOUNCE.
1095    pub async fn unannounce(
1096        &mut self,
1097        track_namespace: TrackNamespace,
1098    ) -> Result<(), ConnectionError> {
1099        let msg = self.endpoint.unannounce(track_namespace)?;
1100        self.send_control(&msg).await
1101    }
1102
1103    /// Accept an announcement the peer made, sending ANNOUNCE_OK.
1104    ///
1105    /// The endpoint refuses a second answer to one ANNOUNCE, so nothing is
1106    /// written on the wire when it does.
1107    pub async fn announce_ok(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1108        let msg = self.endpoint.send_announce_ok(request_id)?;
1109        self.send_control(&msg).await
1110    }
1111
1112    /// Refuse an announcement the peer made, sending ANNOUNCE_ERROR.
1113    ///
1114    /// The other half of the same sentence: one answer, and this is the other
1115    /// one it can be.
1116    pub async fn announce_error(
1117        &mut self,
1118        request_id: VarInt,
1119        error_code: VarInt,
1120        reason_phrase: Vec<u8>,
1121    ) -> Result<(), ConnectionError> {
1122        let msg = self.endpoint.send_announce_error(request_id, error_code, reason_phrase)?;
1123        self.send_control(&msg).await
1124    }
1125
1126    /// Revoke an acceptance, sending ANNOUNCE_CANCEL.
1127    ///
1128    /// The endpoint refuses one for an announcement it never accepted, so
1129    /// nothing is written on the wire when it does.
1130    pub async fn announce_cancel(
1131        &mut self,
1132        track_namespace: TrackNamespace,
1133        error_code: VarInt,
1134        reason_phrase: Vec<u8>,
1135    ) -> Result<(), ConnectionError> {
1136        let msg = self.endpoint.announce_cancel(track_namespace, error_code, reason_phrase)?;
1137        self.send_control(&msg).await
1138    }
1139    // ── Track Status flow ────────────────────────────────────
1140
1141    /// Send a TRACK_STATUS_REQUEST. Returns the allocated request ID.
1142    pub async fn track_status_request(
1143        &mut self,
1144        track_namespace: TrackNamespace,
1145        track_name: Vec<u8>,
1146    ) -> Result<VarInt, ConnectionError> {
1147        let (req_id, msg) = self.endpoint.track_status_request(track_namespace, track_name)?;
1148        self.send_control(&msg).await?;
1149        Ok(req_id)
1150    }
1151
1152    /// Answer a TRACK_STATUS_REQUEST the peer sent, sending TRACK_STATUS.
1153    ///
1154    /// The endpoint refuses a second answer to one request, so nothing is
1155    /// written on the wire when it does.
1156    pub async fn track_status(
1157        &mut self,
1158        request_id: VarInt,
1159        status_code: VarInt,
1160        largest_location: Location,
1161        parameters: Vec<KeyValuePair>,
1162    ) -> Result<(), ConnectionError> {
1163        let msg = self.endpoint.send_track_status(
1164            request_id,
1165            status_code,
1166            largest_location,
1167            parameters,
1168        )?;
1169        self.send_control(&msg).await
1170    }
1171
1172    // ── Data streams ────────────────────────────────────────
1173
1174    /// Open a new unidirectional stream for sending subgroup data.
1175    pub async fn open_subgroup_stream(
1176        &self,
1177        header: &AnySubgroupHeader,
1178    ) -> Result<FramedSendStream, ConnectionError> {
1179        // Before the stream is opened: the Original Publisher is who the rule
1180        // binds, so a header that would mix this track's framing is refused
1181        // here rather than written and answered by the peer.
1182        self.endpoint.note_object_forwarding_preference(
1183            header.track_alias(),
1184            ObjectForwardingPreference::Subgroup,
1185        )?;
1186        let send = self.transport.open_uni().await?;
1187        let mut framed = FramedSendStream::new(send);
1188        let sid = framed.stream_id();
1189        framed.write_subgroup_header(header).await?;
1190        self.emit(ClientEvent::StreamOpened {
1191            direction: Direction::Send,
1192            stream_kind: StreamKind::Subgroup,
1193            stream_id: sid,
1194        });
1195        self.emit(ClientEvent::DataStreamHeader {
1196            stream_id: sid,
1197            direction: Direction::Send,
1198            header: header.clone(),
1199        });
1200        Ok(framed)
1201    }
1202
1203    /// Open a new unidirectional stream for sending a FETCH's objects.
1204    ///
1205    /// The objects answering a FETCH do not go on the request's own stream:
1206    /// they go on a unidirectional stream of their own, which opens with a
1207    /// FETCH_HEADER naming the request they belong to. This writes that header
1208    /// and hands back the stream, the same way
1209    /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1210    /// subgroup.
1211    ///
1212    /// The caller owns the stream that comes back. Nothing here remembers
1213    /// which request it belongs to, so an endpoint serving several fetches at
1214    /// once keeps its own map from Request ID to stream.
1215    pub async fn open_fetch_stream(
1216        &self,
1217        header: &AnyFetchHeader,
1218    ) -> Result<FramedSendStream, ConnectionError> {
1219        let send = self.transport.open_uni().await?;
1220        let mut framed = FramedSendStream::new(send);
1221        let sid = framed.stream_id();
1222        framed.write_fetch_header(header).await?;
1223        self.emit(ClientEvent::StreamOpened {
1224            direction: Direction::Send,
1225            stream_kind: StreamKind::Fetch,
1226            stream_id: sid,
1227        });
1228        Ok(framed)
1229    }
1230
1231    /// Accept the next unidirectional stream and read its fetch header.
1232    ///
1233    /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1234    /// are separate because the header decides how every object after it is
1235    /// framed, so a caller has to know which it is expecting before the first
1236    /// byte is read.
1237    ///
1238    /// Objects come off the returned stream with
1239    /// [`FramedRecvStream::read_fetch_object`].
1240    pub async fn accept_fetch_stream(
1241        &self,
1242    ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1243        let recv = self.transport.accept_uni().await?;
1244        let mut framed = FramedRecvStream::new(recv);
1245        let sid = framed.stream_id();
1246        let header = framed.read_fetch_header().await?;
1247        self.emit(ClientEvent::StreamOpened {
1248            direction: Direction::Receive,
1249            stream_kind: StreamKind::Fetch,
1250            stream_id: sid,
1251        });
1252        self.emit(ClientEvent::FetchStreamHeader {
1253            stream_id: sid,
1254            direction: Direction::Receive,
1255            header: header.clone(),
1256        });
1257        // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1258        // carries an `AnySubgroupHeader` and cannot express one. What
1259        // `accept_subgroup_stream` does beyond this - the forwarding-preference
1260        // note, the object measurement - is about a subgroup and has no
1261        // counterpart on a fetch stream.
1262        Ok((header, framed))
1263    }
1264
1265    /// Accept an incoming unidirectional data stream and read its subgroup
1266    /// header.
1267    pub async fn accept_subgroup_stream(
1268        &self,
1269    ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1270        let recv = self.transport.accept_uni().await?;
1271        let mut framed = FramedRecvStream::new(recv);
1272        let sid = framed.stream_id();
1273        let header = framed.read_subgroup_header().await?;
1274        self.emit(ClientEvent::StreamOpened {
1275            direction: Direction::Receive,
1276            stream_kind: StreamKind::Subgroup,
1277            stream_id: sid,
1278        });
1279        self.emit(ClientEvent::DataStreamHeader {
1280            stream_id: sid,
1281            direction: Direction::Receive,
1282            header: header.clone(),
1283        });
1284        // Every object on a subgroup stream has the Subgroup preference, so
1285        // the header settles the track's framing before a single object is
1286        // read.
1287        self.endpoint.note_object_forwarding_preference(
1288            header.track_alias(),
1289            ObjectForwardingPreference::Subgroup,
1290        )?;
1291        // The track is resolved here and not inside the stream: it takes the
1292        // endpoint's alias table, which a stream handle has no way back to.
1293        if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
1294            framed.measure_objects_against(objects, header.group_id());
1295        }
1296        Ok((header, framed))
1297    }
1298
1299    /// Send an object via datagram.
1300    ///
1301    /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1302    /// header whose Object Status the framing it names cannot carry. Such a
1303    /// header errors here and nothing is sent, rather than going out as an
1304    /// ordinary payload datagram with the status quietly dropped.
1305    pub fn send_datagram(
1306        &self,
1307        header: &AnyDatagramHeader,
1308        payload: &[u8],
1309    ) -> Result<(), ConnectionError> {
1310        // Before anything is encoded, for the reason `open_subgroup_stream`
1311        // gives.
1312        self.endpoint.note_object_forwarding_preference(
1313            header.meta().track_alias,
1314            ObjectForwardingPreference::Datagram,
1315        )?;
1316        let mut buf = Vec::new();
1317        header.encode(&mut buf)?;
1318        buf.extend_from_slice(payload);
1319        self.emit(ClientEvent::DatagramReceived {
1320            direction: Direction::Send,
1321            header: header.clone(),
1322            payload_len: payload.len(),
1323        });
1324        self.transport.send_datagram(bytes::Bytes::from(buf))?;
1325        Ok(())
1326    }
1327
1328    /// Receive a datagram and decode its header.
1329    pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1330        let data = self.transport.recv_datagram().await?;
1331        let mut cursor = &data[..];
1332        let header = AnyDatagramHeader::decode(DraftVersion::Draft11, &mut cursor)?;
1333        let consumed = data.len() - cursor.len();
1334        let payload = data.slice(consumed..);
1335        self.emit(ClientEvent::DatagramReceived {
1336            direction: Direction::Receive,
1337            header: header.clone(),
1338            payload_len: payload.len(),
1339        });
1340        // A datagram is the other framing, and it settles the track's just as a
1341        // subgroup header does.
1342        self.endpoint.note_object_forwarding_preference(
1343            header.meta().track_alias,
1344            ObjectForwardingPreference::Datagram,
1345        )?;
1346        // A datagram is a whole object, so the connection can measure it
1347        // without help from the caller.
1348        let meta = header.meta();
1349        self.endpoint.note_received_object(
1350            meta.track_alias,
1351            ObjectLocation { group: meta.group_id, object: meta.object_id },
1352            object_role(meta.status),
1353        )?;
1354        Ok((header, payload))
1355    }
1356
1357    // ── Accessors ───────────────────────────────────────────
1358
1359    /// Access the underlying endpoint state machine.
1360    pub fn endpoint(&self) -> &Endpoint {
1361        &self.endpoint
1362    }
1363
1364    /// Mutable access to the endpoint state machine.
1365    pub fn endpoint_mut(&mut self) -> &mut Endpoint {
1366        &mut self.endpoint
1367    }
1368
1369    /// Get the negotiated MoQT version.
1370    pub fn negotiated_version(&self) -> Option<VarInt> {
1371        self.endpoint.negotiated_version()
1372    }
1373
1374    /// The code to close the session with when a message could not be decoded
1375    /// because the peer broke a rule draft-11 answers with a close.
1376    ///
1377    /// Every variant listed here comes from a sentence in this draft that names
1378    /// the consequence, and the list is per draft: answering a bound this draft
1379    /// does not state would close a session over traffic a conforming peer may
1380    /// send.
1381    ///
1382    ///   - Reason Phrase, maximum 1024 bytes: "If an endpoint receives a length
1383    ///     exceeding the maximum, it MUST close the session with a Protocol
1384    ///     Violation."
1385    ///   - GOAWAY New Session URI, maximum 8,192 bytes, with the same sentence.
1386    ///     Drafts 11 through 19 state it; 07 through 10 state no maximum for
1387    ///     the field at all.
1388    ///   - Key-Value-Pair value, maximum 2^16-1 bytes, with the same sentence.
1389    ///   - Track Namespace tuple size: "If an endpoint receives a Track
1390    ///     Namespace tuple with an N of 0 or more than 32, it MUST close the
1391    ///     session with a Protocol Violation." Note the lower bound - an empty
1392    ///     tuple is refused here, where drafts 17 and later permit one.
1393    ///   - Full Track Name, maximum 4,096 bytes, "computed as the sum of the
1394    ///     lengths of each Track Namespace tuple field and the Track Name
1395    ///     length field". This draft bounds the pair and not the namespace
1396    ///     alone; draft-16 widened it.
1397    ///   - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
1398    ///     check that there are no unauthorized duplicate parameters and close
1399    ///     the session as a 'Protocol Violation' if found." The rule is
1400    ///     asymmetric here - "Receivers MUST allow duplicates of unknown
1401    ///     parameters", and one known type is granted repeats - and the codec
1402    ///     reports only the repeats this draft actually forbids.
1403    ///   - Unknown control message type: "An endpoint that receives an unknown
1404    ///     message type MUST close the session."
1405    ///   - Extension headers on an Object whose status is Object Does Not
1406    ///     Exist, Section 9.1.1.2. That one arrives on a data stream or a
1407    ///     datagram, so [`Connection::close_for_data_stream`] is what carries
1408    ///     it.
1409    ///
1410    /// **Not** the zero-length Track Namespace Field, the delta-encoded
1411    /// parameter type overflow, or the Object ID delta wrap. Those enter the
1412    /// specification at drafts 16 and 18, and this draft states none of them.
1413    /// **Not** the 2^16-1 control message length either. This draft states the
1414    /// limit - "the total length of a control message is limited to 2^16-1" -
1415    /// and states no consequence for exceeding it, so an oversized message is
1416    /// refused by the decoder and stops there.
1417    ///
1418    /// **Not** [`CodecError::UnexpectedEnd`], which reports no rule at all: the
1419    /// reader raises it whenever a message is still arriving, and
1420    /// `read_control` loops on it. Closing over it would end a session on an
1421    /// ordinary short read.
1422    ///
1423    /// **Not** the unknown Message Parameter rule. Drafts 16 through 19 require
1424    /// a close for a Message Parameter whose type the negotiated version does
1425    /// not define. This draft states the opposite and states it about the same
1426    /// parameters: "Receivers MUST allow duplicates of unknown parameters",
1427    /// which presumes an unknown parameter arrives and is carried. Refusing one
1428    /// here would close a session over an extension this draft leaves room for.
1429    ///
1430    /// `None` for everything else, including [`CodecError::InvalidField`]. That
1431    /// variant is shared by a dozen unrelated malformations, only some of which
1432    /// the draft answers with a close, so a session cannot be ended on it
1433    /// without ending sessions the draft does not ask to be ended. Splitting it
1434    /// is the way to bring the rest of those rules under this function;
1435    /// widening the match is not - the extension-header rule above reached this
1436    /// table by being split out of it.
1437    fn codec_session_error_code(
1438        err: &CodecError,
1439    ) -> Option<moqtap_codec::draft11::error_codes::SessionErrorCode> {
1440        use moqtap_codec::draft11::error_codes::SessionErrorCode;
1441        use moqtap_codec::kvp::KvpError;
1442        match err {
1443            // The declared Length disagreeing with the fields, which every
1444            // draft answers with a close. Drafts 07 through 10 name no code for
1445            // it, so it takes the one their other unnamed rules take.
1446            CodecError::ControlMessageLengthMismatch { .. } => {
1447                Some(SessionErrorCode::ProtocolViolation)
1448            }
1449            CodecError::ReasonPhraseTooLong
1450            | CodecError::GoAwayUriTooLong
1451            | CodecError::InvalidNamespaceTupleSize(_)
1452            | CodecError::TrackNameTooLong
1453            | CodecError::DuplicateParameter(_)
1454            | CodecError::UnknownMessageType(_)
1455            | CodecError::ExtensionsOnNonExistentObject(_)
1456            | CodecError::Kvp(KvpError::ValueTooLong(_)) => {
1457                Some(SessionErrorCode::ProtocolViolation)
1458            }
1459            // An unknown data-plane type, Section 9: "An endpoint that
1460            // receives an unknown stream or datagram type MUST close the
1461            // session." One sentence covering two tables, which is why both
1462            // variants sit here.
1463            // A Content Exists field that is neither zero nor one,
1464            // Section 8.8: "Any other value is a protocol error and
1465            // MUST terminate the session with a Protocol Violation".
1466            CodecError::InvalidContentExists(_) => Some(SessionErrorCode::ProtocolViolation),
1467            // A Forward field that is neither zero nor one, Sections 8.7 and
1468            // 8.10: "Any other value is a protocol error and MUST terminate the
1469            // session with a Protocol Violation". Draft-11 is where the field
1470            // arrives, and it carries only these two sites; PUBLISH and
1471            // PUBLISH_OK add two more from draft-12.
1472            CodecError::InvalidForward(_) => Some(SessionErrorCode::ProtocolViolation),
1473            CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
1474                Some(SessionErrorCode::ProtocolViolation)
1475            }
1476            // A key-value pair whose value is not the serialization its own
1477            // Type defines, Section 1.3.2: "If a receiver understands a Type,
1478            // and the following Value or Length/Value does not match the
1479            // serialization defined by that Type, the receiver MUST terminate the
1480            // session with error code 'Key-Value Formatting Error'."
1481            //
1482            // This draft states the general rule alone. The Authorization Token
1483            // structure it defines gets its own sentence only from draft-12 on,
1484            // and a token that cannot be decoded is answered here either way.
1485            //
1486            // The one rule in this table that names a code other than Protocol
1487            // Violation.
1488            CodecError::KeyValueFormatting { .. } => {
1489                Some(SessionErrorCode::KeyValueFormattingError)
1490            }
1491            // Everything this draft does not answer, named rather than swept up
1492            // by a wildcard. The arm is exhaustive deliberately: a new
1493            // `CodecError` variant will not compile until it has been placed on
1494            // one side or the other, on this draft, which is the decision a `_`
1495            // arm makes silently and invisibly on all thirteen at once.
1496            //
1497            // Adding one variant to `CodecError` was tried, and produces
1498            // thirteen `E0004`s, one per draft, each naming the variant that has
1499            // nowhere to go. That is the whole mechanism.
1500            //
1501            // The nesting stops at `VarInt`, whose variants report how the bytes
1502            // ran out rather than a rule an endpoint states, so there is nothing
1503            // in it for a draft to answer. `Kvp` is spelled out because it does
1504            // carry one.
1505            // Not `ParameterValueOutOfRange`: no parameter this draft defines
1506            // restricts its value's range. Forwarding is a message field here
1507            // and is answered above.
1508            CodecError::ParameterValueOutOfRange { .. }
1509            | CodecError::UnexpectedEnd
1510            | CodecError::MessageTooLong(_)
1511            | CodecError::VarInt(_)
1512            | CodecError::InvalidField
1513            | CodecError::EmptyNamespaceField
1514            | CodecError::InvalidRange(..)
1515            | CodecError::ParameterLengthMismatch(_)
1516            | CodecError::EndOfTrackObjectId(_)
1517            | CodecError::KeyDeltaOverflow(..)
1518            // Not `TrackPropertyValueOutOfRange`: this draft has neither
1519            // namespace the variant is about. Draft-16 opens an extension header
1520            // registry with value rules of its own, and draft-17 renames it to
1521            // the Track Property registry. Before that, everything with a
1522            // restricted range is either a message field or a Message Parameter.
1523            | CodecError::TrackPropertyValueOutOfRange { .. }
1524            | CodecError::ParametersOutOfOrder(..)
1525            | CodecError::ObjectIdOverflow(..)
1526            | CodecError::InvalidRequiredRequestIdDelta(..)
1527            | CodecError::InvalidTypeValue { .. }
1528            | CodecError::UnknownMessageParameter(_)
1529            // Not `ParameterOutOfScope`: this draft states the scope rule and
1530            // answers it the other way. Section 8.2.1 Version Specific Parameters: "Each
1531            // version-specific parameter definition indicates the message types in which it can
1532            // appear. If it appears in some other type of message, it MUST be
1533            // ignored." The codec carries such a parameter on this draft and never
1534            // raises the variant, so this arm records a rule this draft has and
1535            // does not close over, not one it is missing. Draft-17 is where the
1536            // second sentence becomes a close.
1537            | CodecError::ParameterOutOfScope { .. }
1538            // A Filter Type outside the set this draft assigns. Section 8.7
1539            // states the rule and stops there: "A filter type other than the
1540            // above MUST be treated as error." No code, no close, and no
1541            // sentence elsewhere in the draft that turns an error into one — so
1542            // the message is refused and the session stays open.
1543            // Draft-14 Section 9.7 is where the same sentence gained "MUST be
1544            // close the session with PROTOCOL_VIOLATION", and it is answered
1545            // there.
1546            //
1547            // The assigned set is not the same on every draft either: 07 and 08
1548            // assign 0x1 as Latest Group, 09 and 10 withdraw it, and 11 and
1549            // later reinstate it as Next Group Start. The decoder holds each
1550            // draft to its own list; this arm only decides what a refusal does
1551            // to the session.
1552            //
1553            // The two rules below belong to the parameter form of the filter,
1554            // which arrives at draft-15. This draft carries the Filter Type as a
1555            // field of SUBSCRIBE, so there is no parameter for either to be
1556            // about.
1557            | CodecError::InvalidFilterType(_)
1558            | CodecError::SubscriptionFilterMalformed { .. }
1559            | CodecError::FilterEndGroupOverflow { .. }
1560            // A Fetch Type outside the set this draft assigns. Section 8.13
1561            // states the rule and stops there: "A Fetch Type other than 0x1,
1562            // 0x2 or 0x3 MUST be treated as an error", naming no code and no
1563            // close, exactly as this draft's Filter Type sentence does.
1564            // Draft-14 Section 9.16 is where both gained "MUST be close the
1565            // session with a PROTOCOL_VIOLATION", and it answers them there.
1566            | CodecError::InvalidFetchType(_)
1567            // The object payload rule, Section 9.1.1.1: "Any object with a status
1568            // code other than zero MUST have an empty payload." A MUST on the
1569            // sender with no receiver action named anywhere — the "SHOULD be
1570            // treated as a protocol error" in the same paragraph belongs to the
1571            // sentence before it, which is about a status value this draft does
1572            // not assign — so an object carrying a payload it may not is refused
1573            // and the session stays open.
1574            //
1575            // That was already the answer. The bytes used to arrive as
1576            // `InvalidField`, which is on this side too; naming the rule changes
1577            // nothing a peer can observe and makes the decision legible.
1578            | CodecError::PayloadNotPermitted { .. }
1579            | CodecError::UnsupportedDraft(_)
1580            | CodecError::Kvp(
1581                KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
1582            ) => None,
1583        }
1584    }
1585
1586    /// Close the session on the wire when a decode failure is one draft-11
1587    /// answers with a close, and hand the error back unchanged.
1588    /// Without it every bound the decoder enforces would stop at *this endpoint
1589    /// refused the frame* while the peer, which is the one that broke the rule,
1590    /// saw a session that was still open and went on sending. "MUST close the
1591    /// session" is a statement about the wire.
1592    fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
1593        if let ConnectionError::Codec(inner) = &err {
1594            if let Some(code) = Self::codec_session_error_code(inner) {
1595                // QUIC application error codes are 62-bit; every code in this
1596                // registry is far below `u32::MAX`, and saturating rather than
1597                // truncating means a future code that is not could never be
1598                // reported as a different, assigned one.
1599                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1600                self.close(wire_code, inner.to_string().as_bytes());
1601            }
1602        }
1603        err
1604    }
1605
1606    /// Close the session on the wire when the endpoint says a violation is
1607    /// fatal to it, and hand the error back unchanged.
1608    ///
1609    /// [`EndpointError::session_error_code`] answers `Some` for exactly the
1610    /// errors this draft ends the session over, and the endpoint has already
1611    /// moved its own state machine to Closed by the time this runs. Without
1612    /// this step that move is purely internal: the local endpoint refuses to
1613    /// start anything new while the peer, which is the one that broke the
1614    /// rule, sees a session that is still open and goes on sending. A rule
1615    /// that names a session termination code is a statement about the wire,
1616    /// so it takes a CONNECTION_CLOSE to satisfy it.
1617    ///
1618    /// The reason phrase is the error's own `Display` text, which names the
1619    /// rule rather than repeating the numeric code the close already carries.
1620    ///
1621    /// Errors that answer `None` are recoverable and nothing is sent.
1622    fn close_for(&self, err: &EndpointError) {
1623        if let Some(code) = err.session_error_code() {
1624            // QUIC application error codes are 62-bit; every code in this
1625            // registry is far below `u32::MAX`, and saturating rather than
1626            // truncating means a future code that is not could never be
1627            // reported as a different, assigned one.
1628            let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1629            self.close(wire_code, err.to_string().as_bytes());
1630        }
1631    }
1632
1633    /// [`close_for`](Self::close_for), then the error unchanged, for the
1634    /// common case where the endpoint's error is also what the caller returns.
1635    fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
1636        self.close_for(&err);
1637        ConnectionError::Endpoint(err)
1638    }
1639
1640    /// Close the session over a rule broken on a data stream, reporting whether
1641    /// it did.
1642    ///
1643    /// A data stream cannot close for itself the way `recv_control` does:
1644    /// [`Connection::accept_subgroup_stream`] hands the caller a
1645    /// [`FramedRecvStream`] holding no connection, so the reader that finds the
1646    /// violation is not the object that can act on it. Keeping it a separate
1647    /// call is deliberate as well - a permissive caller, one reproducing a
1648    /// capture, can read a violating stream and report it without tearing the
1649    /// session down.
1650    ///
1651    /// The rule this draft answers here is extension headers on an Object whose
1652    /// status is Object Does Not Exist, which reaches subgroup streams, fetch
1653    /// streams and status datagrams alike. It shares `codec_session_error_code`
1654    /// with the control path, so a rule is answered with one code whichever
1655    /// stream carried it.
1656    ///
1657    /// Not every rule that reaches here is the decoder's. A track whose objects
1658    /// mix forwarding preferences is the endpoint's to notice — it takes the
1659    /// alias table to know which track an object belongs to — and it arrives on
1660    /// exactly these streams. Both kinds are asked for a code the same way, and
1661    /// a rule with no code is declined rather than guessed at.
1662    pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
1663        match err {
1664            ConnectionError::Codec(inner) => {
1665                let Some(code) = Self::codec_session_error_code(inner) else { return false };
1666                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1667                self.close(wire_code, inner.to_string().as_bytes());
1668                true
1669            }
1670            // A rule the endpoint raises rather than the decoder. The two
1671            // reach their codes through different tables and mean the same
1672            // thing here: `Some` is a rule this draft ends the session over.
1673            ConnectionError::Endpoint(inner) => {
1674                let Some(code) = inner.session_error_code() else { return false };
1675                let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1676                self.close(wire_code, inner.to_string().as_bytes());
1677                true
1678            }
1679            _ => false,
1680        }
1681    }
1682
1683    /// Close the connection.
1684    pub fn close(&self, code: u32, reason: &[u8]) {
1685        self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
1686        self.transport.close(code, reason);
1687    }
1688}
1689
1690/// Determine the encoded length of a varint from its first byte.
1691fn varint_len(first_byte: u8) -> usize {
1692    1 << (first_byte >> 6)
1693}
1694
1695#[cfg(test)]
1696mod tests {
1697    use super::*;
1698
1699    #[test]
1700    fn client_config_supported_versions_default() {
1701        let config = ClientConfig {
1702            additional_versions: Vec::new(),
1703            transport: TransportType::Quic,
1704            skip_cert_verification: false,
1705            ca_certs: Vec::new(),
1706            setup_parameters: Vec::new(),
1707        };
1708        let versions = config.supported_versions();
1709        assert_eq!(versions.len(), 1);
1710        assert_eq!(versions[0].into_inner(), 0xff000000 + 11);
1711    }
1712
1713    #[test]
1714    fn client_config_alpn_quic() {
1715        let config = ClientConfig {
1716            additional_versions: Vec::new(),
1717            transport: TransportType::Quic,
1718            skip_cert_verification: false,
1719            ca_certs: Vec::new(),
1720            setup_parameters: Vec::new(),
1721        };
1722        assert_eq!(config.alpn(), vec![DraftVersion::Draft11.quic_alpn().to_vec()]);
1723    }
1724
1725    #[test]
1726    fn moqt_alpn_value() {
1727        assert_eq!(MOQT_ALPN, b"moq-00");
1728    }
1729}