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