Skip to main content

moqtap_client/draft07/
connection.rs

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