Skip to main content

moqtap_client/draft10/
connection.rs

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