moqtap_client/draft14/connection.rs
1use bytes::{Buf, Bytes, BytesMut};
2
3use crate::draft14::endpoint::{Endpoint, EndpointError};
4use crate::draft14::event::{ClientEvent, Direction, StreamKind};
5use crate::draft14::observer::ConnectionObserver;
6use crate::draft14::session::request_id::Role;
7use crate::draft14::session::setup;
8use crate::forwarding_preference::ObjectForwardingPreference;
9use crate::malformed_tracks::MalformedTrackCondition;
10use crate::track_locations::{ObjectLocation, ObjectRole, TrackObjects};
11use crate::transport::{RecvStream, SendStream, Transport, TransportError};
12use moqtap_codec::dispatch::{
13 AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
14};
15use moqtap_codec::draft14::data_stream::{FetchObject, SubgroupObject, SubgroupObjectReader};
16use moqtap_codec::draft14::message::ControlMessage;
17use moqtap_codec::error::CodecError;
18use moqtap_codec::kvp::KeyValuePair;
19use moqtap_codec::types::*;
20use moqtap_codec::varint::VarInt;
21use moqtap_codec::version::DraftVersion;
22
23/// MoQT ALPN identifier (used by raw QUIC transport).
24pub const MOQT_ALPN: &[u8] = b"moq-00";
25
26/// Errors from the connection layer.
27#[derive(Debug, thiserror::Error)]
28pub enum ConnectionError {
29 /// Endpoint state machine error.
30 #[error("endpoint error: {0}")]
31 Endpoint(#[from] EndpointError),
32 /// Wire codec error.
33 #[error("codec error: {0}")]
34 Codec(#[from] CodecError),
35 /// Transport-level error.
36 #[error("transport error: {0}")]
37 Transport(#[from] TransportError),
38 /// Variable-length integer decoding error.
39 #[error("varint error: {0}")]
40 VarInt(#[from] moqtap_codec::varint::VarIntError),
41 /// Control stream was not opened.
42 #[error("control stream not open")]
43 NoControlStream,
44 /// Stream ended before a complete message was read.
45 #[error("unexpected end of stream")]
46 UnexpectedEnd,
47 /// Stream was finished by the peer.
48 #[error("stream finished")]
49 StreamFinished,
50 /// Invalid server address string.
51 #[error("invalid server address: {0}")]
52 InvalidAddress(String),
53 /// TLS configuration error.
54 #[error("TLS config error: {0}")]
55 TlsConfig(String),
56 /// Data stream used out of order (e.g. object before header).
57 #[error("data stream state error: {0}")]
58 DataStreamState(&'static str),
59}
60
61impl From<crate::transport::DialError> for ConnectionError {
62 /// Preserves the variants this error had when the dial was inlined here,
63 /// so a caller matching on `InvalidAddress` or `TlsConfig` sees no change.
64 fn from(e: crate::transport::DialError) -> Self {
65 match e {
66 crate::transport::DialError::InvalidAddress(s) => ConnectionError::InvalidAddress(s),
67 crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
68 crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
69 }
70 }
71}
72
73/// Transport type for the connection.
74#[derive(Debug, Clone)]
75pub enum TransportType {
76 /// Raw QUIC via quinn. The `addr` field should be `host:port`.
77 Quic,
78 /// WebTransport via wtransport. The `url` field is the WebTransport URL.
79 WebTransport {
80 /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
81 url: String,
82 },
83}
84
85/// Configuration for a MoQT client connection.
86///
87/// Both `draft` and `transport` are required — there is no `Default` impl.
88pub struct ClientConfig {
89 /// The MoQT draft version to use (primary, determines codec/framing).
90 pub draft: DraftVersion,
91 /// Additional draft versions to offer in CLIENT_SETUP.
92 /// The primary `draft` is always included first.
93 pub additional_versions: Vec<DraftVersion>,
94 /// The transport type (QUIC or WebTransport).
95 pub transport: TransportType,
96 /// Whether to skip TLS certificate verification (for testing).
97 pub skip_cert_verification: bool,
98 /// Custom CA certificates to trust (DER-encoded).
99 pub ca_certs: Vec<Vec<u8>>,
100 /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
101 pub setup_parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
102}
103
104impl ClientConfig {
105 /// Returns the MoQT version varints for the CLIENT_SETUP message.
106 /// Primary draft first, then any additional versions.
107 pub fn supported_versions(&self) -> Vec<VarInt> {
108 let mut versions = vec![self.draft.version_varint()];
109 for v in &self.additional_versions {
110 let varint = v.version_varint();
111 if !versions.contains(&varint) {
112 versions.push(varint);
113 }
114 }
115 versions
116 }
117
118 /// Returns the ALPN protocol identifiers for the transport.
119 pub fn alpn(&self) -> Vec<Vec<u8>> {
120 match &self.transport {
121 TransportType::Quic => vec![self.draft.quic_alpn().to_vec()],
122 TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
123 }
124 }
125}
126
127/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
128pub struct FramedSendStream {
129 inner: SendStream,
130 draft: DraftVersion,
131 /// Stateful subgroup object encoder. Initialized by
132 /// [`FramedSendStream::write_subgroup_header`] and used by
133 /// [`FramedSendStream::write_subgroup_object`] to track the delta-encoded
134 /// object ID state.
135 subgroup_io: Option<SubgroupObjectReader>,
136}
137
138impl FramedSendStream {
139 /// Create a new framed send stream for the given draft version.
140 pub fn new(inner: SendStream, draft: DraftVersion) -> Self {
141 Self { inner, draft, subgroup_io: None }
142 }
143
144 /// Get the transport-level stream ID.
145 pub fn stream_id(&self) -> u64 {
146 self.inner.stream_id()
147 }
148
149 /// Write a control message to the stream with type+length framing.
150 /// Returns the raw bytes that were written (for event capture).
151 pub async fn write_control(
152 &mut self,
153 msg: &AnyControlMessage,
154 ) -> Result<Vec<u8>, ConnectionError> {
155 let mut buf = Vec::new();
156 msg.encode(&mut buf)?;
157 self.inner.write_all(&buf).await?;
158 Ok(buf)
159 }
160
161 /// Write a subgroup stream header. Also initializes the internal
162 /// delta-encoding state used by [`FramedSendStream::write_subgroup_object`].
163 ///
164 /// The header is refused, and nothing is written, if its fields disagree
165 /// with its own stream type. That check has to happen here rather than at
166 /// the first object: the type is what every object after it is framed
167 /// against, so a header that went out saying the wrong thing cannot be
168 /// taken back.
169 pub async fn write_subgroup_header(
170 &mut self,
171 header: &AnySubgroupHeader,
172 ) -> Result<(), ConnectionError> {
173 let mut buf = Vec::new();
174 header.encode_stream_checked(&mut buf)?;
175 self.inner.write_all(&buf).await?;
176 // Clippy would rather see these two arms as an `if let`, and rustc rejects
177 // that in a single-draft build, where the pattern is irrefutable. Only a
178 // `match` satisfies both.
179 #[allow(clippy::single_match)]
180 match header {
181 AnySubgroupHeader::Draft14(ref d14) => {
182 self.subgroup_io = Some(SubgroupObjectReader::new(d14));
183 }
184 // Only this draft's header seeds the object reader. With draft 14 the only enabled
185 // draft `AnySubgroupHeader` has a single variant, the arm above is exhaustive and this
186 // one unreachable. Compiled in every configuration with the lint allowed, rather than
187 // gated on a `cfg` naming the other thirteen drafts: that list had to be edited in
188 // every draft module whenever a draft was added, and a copy that omitted one left this
189 // match non-exhaustive.
190 #[allow(unreachable_patterns)]
191 _ => {}
192 }
193 Ok(())
194 }
195
196 /// Write a fetch response header.
197 pub async fn write_fetch_header(
198 &mut self,
199 header: &AnyFetchHeader,
200 ) -> Result<(), ConnectionError> {
201 let mut buf = Vec::new();
202 header.encode_stream(&mut buf);
203 self.inner.write_all(&buf).await?;
204 Ok(())
205 }
206
207 /// Append a draft-14 subgroup object to the stream. Uses the stateful
208 /// reader installed by [`FramedSendStream::write_subgroup_header`] to
209 /// produce the correct delta-encoded object ID. Returns an error if
210 /// called before a subgroup header was written.
211 pub async fn write_subgroup_object(
212 &mut self,
213 object: &SubgroupObject,
214 ) -> Result<(), ConnectionError> {
215 let writer = self
216 .subgroup_io
217 .as_mut()
218 .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
219 let mut buf = Vec::new();
220 writer.write_object(object, &mut buf)?;
221 self.inner.write_all(&buf).await?;
222 Ok(())
223 }
224
225 /// Append a draft-14 fetch object to the stream. Fetch objects are
226 /// self-contained (each carries its own group/subgroup/object IDs and
227 /// priority), so no prior `write_fetch_header` bookkeeping is required.
228 /// Append a fetch object to the stream.
229 ///
230 /// Draft-14 derives the declared length from the payload itself rather than
231 /// holding it as a field, so there is nothing here for the two to disagree
232 /// about - unlike the subgroup object, whose header carries the length and
233 /// whose writer has to overwrite it.
234 ///
235 /// # Errors
236 ///
237 /// Whatever the checked encoder refuses: a payload beside a status that
238 /// forbids one, or a status the draft leaves unassigned. This went through
239 /// the unchecked encoder, so both were written.
240 pub async fn write_fetch_object(
241 &mut self,
242 object: &FetchObject,
243 ) -> Result<(), ConnectionError> {
244 let mut buf = Vec::new();
245 object.encode_checked(&mut buf)?;
246 self.inner.write_all(&buf).await?;
247 Ok(())
248 }
249
250 /// Finish the stream (send FIN).
251 pub async fn finish(&mut self) -> Result<(), ConnectionError> {
252 self.inner.finish()?;
253 Ok(())
254 }
255
256 /// Returns the draft version this stream is framed for.
257 pub fn draft(&self) -> DraftVersion {
258 self.draft
259 }
260}
261
262/// What an Object Status makes of an object here.
263///
264/// Two answers where drafts 08 through 13 have three, and the missing one is
265/// the point: the end-of-track status settles where the track ended and is
266/// judged against nothing, because the rule about where one may be placed is
267/// not in this draft. `a_track_may_end_where_it_has_already_been.rs` asserts
268/// that acceptance.
269///
270/// Every other status is a statement about objects rather than one of them.
271fn object_role(status: Option<u64>) -> ObjectRole {
272 match status {
273 None | Some(0x0) => ObjectRole::Produced,
274 Some(0x4) => ObjectRole::EndsTrack(None),
275 _ => ObjectRole::Neither,
276 }
277}
278
279/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
280pub struct FramedRecvStream {
281 inner: RecvStream,
282 buf: BytesMut,
283 draft: DraftVersion,
284 /// Stateful subgroup object decoder. Initialized by
285 /// [`FramedRecvStream::read_subgroup_header`] and used by
286 /// [`FramedRecvStream::read_subgroup_object`] to track delta-encoded
287 /// object IDs and whether extension headers are present.
288 subgroup_io: Option<SubgroupObjectReader>,
289 /// The record this stream's objects are measured against, and the Group ID
290 /// its header named.
291 ///
292 /// One group for the whole stream: a subgroup header names it once and no
293 /// object header repeats it. `None` on a stream that was never given one -
294 /// a stream for an alias no live binding names, and every stream built
295 /// outside [`Connection::accept_subgroup_stream`] - and such a stream reads
296 /// exactly as it did before this existed.
297 tracking: Option<(TrackObjects, u64)>,
298}
299
300impl FramedRecvStream {
301 /// Create a new framed receive stream for the given draft version.
302 pub fn new(inner: RecvStream, draft: DraftVersion) -> Self {
303 Self { inner, buf: BytesMut::with_capacity(4096), draft, subgroup_io: None, tracking: None }
304 }
305
306 /// Get the transport-level stream ID.
307 pub fn stream_id(&self) -> u64 {
308 self.inner.stream_id()
309 }
310
311 /// Measure this stream's objects against `objects`, all of them in `group`.
312 ///
313 /// Called by [`Connection::accept_subgroup_stream`] once the header has
314 /// been read, which is the only point at which both the track and the group
315 /// are known.
316 fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
317 self.tracking = Some((objects, group));
318 }
319
320 /// Judge one object this stream carried against where its track ended.
321 ///
322 /// The object's Group ID is the stream's and its Object ID is its own,
323 /// already resolved from the delta the wire carries; what they are measured
324 /// against is the end an end-of-track object settled on any stream.
325 fn note_subgroup_object(
326 &self,
327 object: u64,
328 status: Option<u64>,
329 ) -> Result<(), ConnectionError> {
330 let Some((objects, group)) = &self.tracking else { return Ok(()) };
331 let at = ObjectLocation { group: *group, object };
332 objects.note_past_final(at, object_role(status)).map_err(|end| {
333 ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject {
334 alias: objects.alias(),
335 group: at.group,
336 object: at.object,
337 final_group: end.group,
338 final_object: end.object,
339 })
340 })
341 }
342
343 /// Read more data from the stream into the internal buffer.
344 async fn fill(&mut self) -> Result<bool, ConnectionError> {
345 let mut tmp = [0u8; 4096];
346 match self.inner.read(&mut tmp).await {
347 Ok(Some(n)) => {
348 self.buf.extend_from_slice(&tmp[..n]);
349 Ok(true)
350 }
351 Ok(None) => Ok(false),
352 Err(e) => Err(ConnectionError::Transport(e)),
353 }
354 }
355
356 /// Ensure at least `n` bytes are available in the buffer.
357 async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
358 while self.buf.len() < n {
359 if !self.fill().await? {
360 return Err(ConnectionError::UnexpectedEnd);
361 }
362 }
363 Ok(())
364 }
365
366 /// Read a control message from the stream.
367 ///
368 /// When `capture_raw` is true, the returned tuple includes a clone of the
369 /// framed wire bytes (for observer emission). When false, the second
370 /// element is `None` and the payload clone is skipped.
371 pub async fn read_control(
372 &mut self,
373 capture_raw: bool,
374 ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
375 // Read type ID varint
376 self.ensure(1).await?;
377 let type_len = varint_len(self.buf[0]);
378 self.ensure(type_len).await?;
379
380 let mut cursor = &self.buf[..type_len];
381 let _type_id = VarInt::decode(&mut cursor)?;
382
383 // Read payload length (16-bit BE for draft-11+, varint for earlier drafts)
384 let (payload_len, len_field_size) = if self.draft.uses_fixed_length_framing() {
385 self.ensure(type_len + 2).await?;
386 let hi = self.buf[type_len] as usize;
387 let lo = self.buf[type_len + 1] as usize;
388 ((hi << 8) | lo, 2)
389 } else {
390 self.ensure(type_len + 1).await?;
391 let payload_len_start = type_len;
392 let payload_len_varint_len = varint_len(self.buf[payload_len_start]);
393 self.ensure(type_len + payload_len_varint_len).await?;
394 let mut cursor = &self.buf[payload_len_start..type_len + payload_len_varint_len];
395 let payload_len = VarInt::decode(&mut cursor)?.into_inner() as usize;
396 (payload_len, payload_len_varint_len)
397 };
398
399 // Read full payload
400 let total = type_len + len_field_size + payload_len;
401 self.ensure(total).await?;
402
403 // Capture raw bytes only if requested (observer attached).
404 let raw = capture_raw.then(|| self.buf[..total].to_vec());
405
406 // Now decode the whole message
407 let mut frame = &self.buf[..total];
408 let msg = AnyControlMessage::decode(self.draft, &mut frame)?;
409 self.buf.advance(total);
410 Ok((msg, raw))
411 }
412
413 /// Read a subgroup stream header. Also initializes the internal
414 /// delta-decoding state used by
415 /// [`FramedRecvStream::read_subgroup_object`].
416 pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
417 self.ensure(1).await?;
418 loop {
419 let mut cursor = &self.buf[..];
420 match AnySubgroupHeader::decode(self.draft, &mut cursor) {
421 Ok(header) => {
422 let consumed = self.buf.len() - cursor.remaining();
423 self.buf.advance(consumed);
424 // Clippy would rather see these two arms as an `if let`, and rustc rejects
425 // that in a single-draft build, where the pattern is irrefutable. Only a
426 // `match` satisfies both.
427 #[allow(clippy::single_match)]
428 match header {
429 AnySubgroupHeader::Draft14(ref d14) => {
430 self.subgroup_io = Some(SubgroupObjectReader::new(d14));
431 }
432 // Only this draft's header seeds the object reader. With draft 14 the only
433 // enabled draft `AnySubgroupHeader` has a single variant, the arm above is
434 // exhaustive and this one unreachable. Compiled in every configuration with
435 // the lint allowed, rather than gated on a `cfg` naming the other thirteen
436 // drafts: that list had to be edited in every draft module whenever a draft
437 // was added, and a copy that omitted one left this match non-exhaustive.
438 #[allow(unreachable_patterns)]
439 _ => {}
440 }
441 return Ok(header);
442 }
443 Err(CodecError::UnexpectedEnd) => {
444 if !self.fill().await? {
445 return Err(ConnectionError::UnexpectedEnd);
446 }
447 }
448 Err(e) => return Err(ConnectionError::Codec(e)),
449 }
450 }
451 }
452
453 /// Read a fetch response header.
454 pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
455 self.ensure(1).await?;
456 loop {
457 let mut cursor = &self.buf[..];
458 match AnyFetchHeader::decode(self.draft, &mut cursor) {
459 Ok(header) => {
460 let consumed = self.buf.len() - cursor.remaining();
461 self.buf.advance(consumed);
462 return Ok(header);
463 }
464 Err(CodecError::UnexpectedEnd) => {
465 if !self.fill().await? {
466 return Err(ConnectionError::UnexpectedEnd);
467 }
468 }
469 Err(e) => return Err(ConnectionError::Codec(e)),
470 }
471 }
472 }
473
474 /// Read the next draft-14 subgroup object from this stream. Uses the
475 /// stateful reader installed by
476 /// [`FramedRecvStream::read_subgroup_header`] to decode the delta-
477 /// encoded object ID and handle extension headers per the stream type.
478 /// Returns an error if called before a subgroup header was read.
479 pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
480 if self.subgroup_io.is_none() {
481 return Err(ConnectionError::DataStreamState("subgroup header not read yet"));
482 }
483 loop {
484 let reader = self.subgroup_io.as_mut().unwrap();
485 let mut probe = reader.clone();
486 let mut cursor = &self.buf[..];
487 match probe.read_object(&mut cursor) {
488 Ok(obj) => {
489 let consumed = self.buf.len() - cursor.remaining();
490 self.buf.advance(consumed);
491 // Commit state from the successful probe decode.
492 *reader = probe;
493 self.note_subgroup_object(
494 obj.object_id.into_inner(),
495 obj.status.map(|s| s as u64),
496 )?;
497 return Ok(obj);
498 }
499 Err(CodecError::UnexpectedEnd) => {
500 if !self.fill().await? {
501 return Err(ConnectionError::UnexpectedEnd);
502 }
503 }
504 Err(e) => return Err(ConnectionError::Codec(e)),
505 }
506 }
507 }
508
509 /// Read the next draft-14 fetch object from this stream. Fetch objects
510 /// are self-describing, so no prior `read_fetch_header` state is needed
511 /// to decode each object.
512 pub async fn read_fetch_object(&mut self) -> Result<FetchObject, ConnectionError> {
513 loop {
514 let mut cursor = &self.buf[..];
515 match FetchObject::decode(&mut cursor) {
516 Ok(obj) => {
517 let consumed = self.buf.len() - cursor.remaining();
518 self.buf.advance(consumed);
519 return Ok(obj);
520 }
521 Err(CodecError::UnexpectedEnd) => {
522 if !self.fill().await? {
523 return Err(ConnectionError::UnexpectedEnd);
524 }
525 }
526 Err(e) => return Err(ConnectionError::Codec(e)),
527 }
528 }
529 }
530
531 /// Returns the draft version this stream is framed for.
532 pub fn draft(&self) -> DraftVersion {
533 self.draft
534 }
535}
536
537/// A live MoQT connection over QUIC or WebTransport, combining the endpoint
538/// state machine with actual network I/O.
539pub struct Connection {
540 transport: Transport,
541 endpoint: Endpoint,
542 draft: DraftVersion,
543 /// Behind a lock because a control message is written from two kinds of
544 /// place. Most of them are the caller's own request, made through `&mut
545 /// self`. The messages that answer a Malformed Track are not: the
546 /// conditions that make a track malformed are detected on the data plane,
547 /// where this connection is reached through a shared reference. The lock
548 /// also makes one message the unit of writing, so two of them cannot
549 /// interleave on the stream.
550 control_send: Option<tokio::sync::Mutex<FramedSendStream>>,
551 control_recv: Option<FramedRecvStream>,
552 observer: Option<Box<dyn ConnectionObserver>>,
553 /// Setup events buffered during `connect()` and replayed when an
554 /// observer attaches via `set_observer` — without this, an observer
555 /// attached after `connect` returns would never see the handshake.
556 pending_events: Vec<ClientEvent>,
557}
558
559impl Connection {
560 /// Connect to a MoQT server as a client.
561 ///
562 /// Establishes a QUIC or WebTransport connection (based on `config.transport`),
563 /// opens a bidirectional control stream, performs the CLIENT_SETUP /
564 /// SERVER_SETUP handshake, and returns a ready-to-use connection.
565 pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
566 // PATH is for native QUIC only, and the transport is known here and
567 // nowhere further in. Refusing before dialling means a session that
568 // the server would close on sight is never opened.
569 setup::validate_client_path_transport(
570 &config.setup_parameters,
571 matches!(config.transport, TransportType::WebTransport { .. }),
572 )
573 .map_err(EndpointError::from)?;
574
575 let transport = match &config.transport {
576 TransportType::Quic => Self::connect_quic(addr, &config).await?,
577 TransportType::WebTransport { url } => {
578 let url = url.clone();
579 Self::connect_webtransport(&url, &config).await?
580 }
581 };
582
583 Self::adopt(transport, config).await
584 }
585
586 /// Run the MoQT setup handshake over a transport somebody else established.
587 ///
588 /// For choosing the draft from what the server selected: dial once through
589 /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
590 /// connection to the module its answer names. [`Self::connect`] cannot do
591 /// this — it derives its single ALPN from the draft it was given.
592 ///
593 /// `config.draft` must match this module. The transport is adopted as
594 /// given; nothing here re-checks the ALPN it was negotiated with.
595 pub async fn adopt(
596 transport: Transport,
597 config: ClientConfig,
598 ) -> Result<Self, ConnectionError> {
599 let draft = config.draft;
600 // PATH is for native QUIC only, and the transport is known here and
601 // nowhere further in. Refusing before dialling means a session that
602 // the server would close on sight is never opened.
603 setup::validate_client_path_transport(
604 &config.setup_parameters,
605 matches!(config.transport, TransportType::WebTransport { .. }),
606 )
607 .map_err(EndpointError::from)?;
608
609 // Open bidirectional control stream
610 let (send, recv) = transport.open_bi().await?;
611 let mut control_send = FramedSendStream::new(send, draft);
612 let mut control_recv = FramedRecvStream::new(recv, draft);
613
614 // Perform setup handshake
615 let mut endpoint = Endpoint::new(Role::Client);
616 endpoint.connect()?;
617 let setup_msg = endpoint
618 .send_client_setup(config.supported_versions(), config.setup_parameters.clone())?;
619 let any_setup = AnyControlMessage::Draft14(setup_msg);
620 let raw_setup = control_send.write_control(&any_setup).await?;
621
622 let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
623 // Unwrap to draft-14 for the endpoint (which is draft-14 only)
624 match &server_setup {
625 AnyControlMessage::Draft14(ControlMessage::ServerSetup(ref ss)) => {
626 endpoint.receive_server_setup(ss)?;
627 }
628 _ => {
629 return Err(ConnectionError::Endpoint(EndpointError::NotActive));
630 }
631 }
632
633 let mut pending_events = Vec::with_capacity(3);
634 pending_events.push(ClientEvent::ControlMessage {
635 direction: Direction::Send,
636 message: any_setup,
637 raw: Some(raw_setup),
638 });
639 pending_events.push(ClientEvent::ControlMessage {
640 direction: Direction::Receive,
641 message: server_setup,
642 raw: raw_server_setup,
643 });
644 if let Some(v) = endpoint.negotiated_version() {
645 pending_events.push(ClientEvent::SetupComplete { negotiated_version: v.into_inner() });
646 }
647
648 Ok(Self {
649 transport,
650 endpoint,
651 draft,
652 control_send: Some(tokio::sync::Mutex::new(control_send)),
653 control_recv: Some(control_recv),
654 observer: None,
655 pending_events,
656 })
657 }
658
659 /// Establish a raw QUIC connection.
660 ///
661 /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
662 /// TLS and endpoint setup.
663 async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
664 let (transport, _negotiated) = crate::transport::dial_quic(
665 addr,
666 &crate::transport::QuicDialOptions {
667 skip_cert_verification: config.skip_cert_verification,
668 ca_certs: config.ca_certs.clone(),
669 alpn: config.alpn(),
670 },
671 )
672 .await?;
673 Ok(transport)
674 }
675
676 /// Establish a WebTransport connection.
677 #[cfg(feature = "webtransport")]
678 async fn connect_webtransport(
679 url: &str,
680 config: &ClientConfig,
681 ) -> Result<Transport, ConnectionError> {
682 use crate::transport::webtransport::WebTransportTransport;
683
684 let wt_config = if config.skip_cert_verification {
685 wtransport::ClientConfig::builder()
686 .with_bind_default()
687 .with_no_cert_validation()
688 .build()
689 } else {
690 wtransport::ClientConfig::builder().with_bind_default().with_native_certs().build()
691 };
692
693 let endpoint = wtransport::Endpoint::client(wt_config)
694 .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
695
696 let connection = endpoint
697 .connect(url)
698 .await
699 .map_err(|e| ConnectionError::Transport(TransportError::Connect(e.to_string())))?;
700
701 Ok(Transport::WebTransport(WebTransportTransport::new(connection)))
702 }
703
704 /// Stub for when the webtransport feature is not enabled.
705 #[cfg(not(feature = "webtransport"))]
706 async fn connect_webtransport(
707 _url: &str,
708 _config: &ClientConfig,
709 ) -> Result<Transport, ConnectionError> {
710 Err(ConnectionError::Transport(TransportError::Connect(
711 "webtransport feature not enabled".into(),
712 )))
713 }
714
715 // ── Observer ───────────────────────────────────────────────
716
717 /// Attach an observer. Buffered handshake events from `connect()` are
718 /// flushed in arrival order before this returns.
719 pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
720 self.observer = Some(observer);
721 for event in self.pending_events.drain(..) {
722 if let Some(ref obs) = self.observer {
723 obs.on_event_owned(event);
724 }
725 }
726 }
727
728 /// Remove the observer.
729 pub fn clear_observer(&mut self) {
730 self.observer = None;
731 }
732
733 /// Emit an event to the observer, if one is attached.
734 fn emit(&self, event: ClientEvent) {
735 if let Some(ref obs) = self.observer {
736 obs.on_event_owned(event);
737 }
738 }
739
740 // ── Control message I/O ─────────────────────────────────
741
742 /// Send a control message on the control stream.
743 ///
744 /// Wraps the draft-14 message in `AnyControlMessage::Draft14` for framing.
745 pub async fn send_control(&self, msg: &ControlMessage) -> Result<(), ConnectionError> {
746 let any = AnyControlMessage::Draft14(msg.clone());
747 let mut send =
748 self.control_send.as_ref().ok_or(ConnectionError::NoControlStream)?.lock().await;
749 let raw = send.write_control(&any).await?;
750 drop(send);
751 self.emit(ClientEvent::ControlMessage {
752 direction: Direction::Send,
753 message: any,
754 raw: Some(raw),
755 });
756 Ok(())
757 }
758
759 /// Read the next control message from the control stream.
760 ///
761 /// Returns the `AnyControlMessage` and also extracts the draft-14
762 /// `ControlMessage` for internal endpoint dispatch.
763 pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
764 let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
765 let capture_raw = self.observer.is_some();
766 let (any, raw) = match recv.read_control(capture_raw).await {
767 Ok(v) => v,
768 Err(e) => return Err(self.close_for_codec(e)),
769 };
770 if capture_raw {
771 self.emit(ClientEvent::ControlMessage {
772 direction: Direction::Receive,
773 message: any.clone(),
774 raw,
775 });
776 }
777 // Unwrap to draft-14 for the endpoint
778 match any {
779 AnyControlMessage::Draft14(msg) => Ok(msg),
780 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 14 the
781 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
782 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
783 // naming the other thirteen drafts: that list had to be edited in every draft module
784 // whenever a draft was added, and a copy that omitted one left this match
785 // non-exhaustive.
786 #[allow(unreachable_patterns)]
787 _ => Err(ConnectionError::Codec(CodecError::UnknownMessageType(0))),
788 }
789 }
790
791 /// Read and dispatch the next incoming control message through the endpoint
792 /// state machine. Returns the decoded message for inspection.
793 pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
794 let msg = self.recv_control().await?;
795 self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
796
797 // Emit draining event if this was a GoAway
798 if let ControlMessage::GoAway(ref ga) = msg {
799 self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
800 }
801
802 Ok(msg)
803 }
804
805 // ── Subscribe flow ──────────────────────────────────────
806
807 /// Send a SUBSCRIBE and return the allocated request ID.
808 pub async fn subscribe(
809 &mut self,
810 track_namespace: TrackNamespace,
811 track_name: Vec<u8>,
812 subscriber_priority: u8,
813 group_order: GroupOrder,
814 filter_type: FilterType,
815 parameters: Vec<KeyValuePair>,
816 ) -> Result<VarInt, ConnectionError> {
817 let (req_id, msg) = self.endpoint.subscribe(
818 track_namespace,
819 track_name,
820 subscriber_priority,
821 group_order,
822 filter_type,
823 parameters,
824 )?;
825 self.send_control(&msg).await?;
826 Ok(req_id)
827 }
828
829 /// Send a SUBSCRIBE for a range of the track, starting at a given
830 /// location.
831 ///
832 /// The Filter Type is derived from the range, so the message cannot name a
833 /// filter whose fields it does not carry.
834 #[allow(clippy::too_many_arguments)]
835 pub async fn subscribe_range(
836 &mut self,
837 track_namespace: TrackNamespace,
838 track_name: Vec<u8>,
839 subscriber_priority: u8,
840 group_order: GroupOrder,
841 start_location: Location,
842 end_group: Option<VarInt>,
843 parameters: Vec<KeyValuePair>,
844 ) -> Result<VarInt, ConnectionError> {
845 let (req_id, msg) = self.endpoint.subscribe_range(
846 track_namespace,
847 track_name,
848 subscriber_priority,
849 group_order,
850 start_location,
851 end_group,
852 parameters,
853 )?;
854 self.send_control(&msg).await?;
855 Ok(req_id)
856 }
857
858 /// Send an UNSUBSCRIBE for the given request ID.
859 pub async fn unsubscribe(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
860 let msg = self.endpoint.unsubscribe(request_id)?;
861 self.send_control(&msg).await
862 }
863
864 /// Accept a subscription the peer opened, sending SUBSCRIBE_OK and giving
865 /// its track a Track Alias.
866 ///
867 /// The endpoint refuses an alias a live track of its own already holds and
868 /// refuses a second answer to one SUBSCRIBE, so nothing is written on the
869 /// wire when it does either.
870 pub async fn subscribe_ok(
871 &mut self,
872 request_id: VarInt,
873 track_alias: VarInt,
874 expires: VarInt,
875 group_order: GroupOrder,
876 parameters: Vec<KeyValuePair>,
877 ) -> Result<(), ConnectionError> {
878 let msg = self.endpoint.send_subscribe_ok(
879 request_id,
880 track_alias,
881 expires,
882 group_order,
883 parameters,
884 )?;
885 self.send_control(&msg).await
886 }
887
888 /// Reject a subscription the peer opened, sending SUBSCRIBE_ERROR.
889 ///
890 /// The endpoint refuses a second answer to one SUBSCRIBE, so nothing is
891 /// written on the wire when it does.
892 pub async fn subscribe_error(
893 &mut self,
894 request_id: VarInt,
895 error_code: VarInt,
896 reason_phrase: Vec<u8>,
897 ) -> Result<(), ConnectionError> {
898 let msg = self.endpoint.send_subscribe_error(request_id, error_code, reason_phrase)?;
899 self.send_control(&msg).await
900 }
901
902 /// Accept a PUBLISH the peer sent, which establishes the subscription it
903 /// opened.
904 ///
905 /// The endpoint refuses a second answer to one PUBLISH, so nothing is
906 /// written on the wire when it does.
907 #[allow(clippy::too_many_arguments)]
908 pub async fn publish_ok(
909 &mut self,
910 request_id: VarInt,
911 forward: Forward,
912 subscriber_priority: u8,
913 group_order: GroupOrder,
914 filter_type: FilterType,
915 start_location: Option<Location>,
916 end_group: Option<VarInt>,
917 ) -> Result<(), ConnectionError> {
918 let msg = self.endpoint.send_publish_ok(
919 request_id,
920 forward,
921 subscriber_priority,
922 group_order,
923 filter_type,
924 start_location,
925 end_group,
926 )?;
927 self.send_control(&msg).await
928 }
929
930 /// Reject a PUBLISH the peer sent, which ends the subscription it opened
931 /// before it was established.
932 ///
933 /// The endpoint refuses a second answer to one PUBLISH, so nothing is
934 /// written on the wire when it does.
935 pub async fn publish_error(
936 &mut self,
937 request_id: VarInt,
938 error_code: VarInt,
939 reason_phrase: Vec<u8>,
940 ) -> Result<(), ConnectionError> {
941 let msg = self.endpoint.send_publish_error(request_id, error_code, reason_phrase)?;
942 self.send_control(&msg).await
943 }
944
945 /// Send a SUBSCRIBE_UPDATE for an active subscription. Returns the
946 /// allocated request ID of the update message.
947 pub async fn subscribe_update(
948 &mut self,
949 subscription_request_id: VarInt,
950 start_location: Location,
951 end_group: VarInt,
952 subscriber_priority: u8,
953 forward: Forward,
954 parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
955 ) -> Result<VarInt, ConnectionError> {
956 let (req_id, msg) = self.endpoint.subscribe_update(
957 subscription_request_id,
958 start_location,
959 end_group,
960 subscriber_priority,
961 forward,
962 parameters,
963 )?;
964 self.send_control(&msg).await?;
965 Ok(req_id)
966 }
967
968 // ── Fetch flow ──────────────────────────────────────────
969
970 /// Send a FETCH and return the allocated request ID.
971 #[allow(clippy::too_many_arguments)]
972 pub async fn fetch(
973 &mut self,
974 track_namespace: TrackNamespace,
975 track_name: Vec<u8>,
976 subscriber_priority: u8,
977 group_order: GroupOrder,
978 start_group: VarInt,
979 start_object: VarInt,
980 end_group: VarInt,
981 end_object: VarInt,
982 parameters: Vec<KeyValuePair>,
983 ) -> Result<VarInt, ConnectionError> {
984 let (req_id, msg) = self.endpoint.fetch(
985 track_namespace,
986 track_name,
987 subscriber_priority,
988 group_order,
989 start_group,
990 start_object,
991 end_group,
992 end_object,
993 parameters,
994 )?;
995 self.send_control(&msg).await?;
996 Ok(req_id)
997 }
998
999 /// Send a Relative Joining Fetch and return the allocated request ID.
1000 ///
1001 /// `joining_start` is a count of groups back from the subscription's live
1002 /// edge. For the form that names the group outright, see
1003 /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
1004 pub async fn joining_fetch(
1005 &mut self,
1006 subscriber_priority: u8,
1007 group_order: GroupOrder,
1008 joining_request_id: VarInt,
1009 joining_start: VarInt,
1010 parameters: Vec<KeyValuePair>,
1011 ) -> Result<VarInt, ConnectionError> {
1012 let (req_id, msg) = self.endpoint.joining_fetch(
1013 subscriber_priority,
1014 group_order,
1015 joining_request_id,
1016 joining_start,
1017 parameters,
1018 )?;
1019 self.send_control(&msg).await?;
1020 Ok(req_id)
1021 }
1022
1023 /// Send an Absolute Joining Fetch and return the allocated request ID.
1024 ///
1025 /// `joining_start` is the group to begin at.
1026 pub async fn absolute_joining_fetch(
1027 &mut self,
1028 subscriber_priority: u8,
1029 group_order: GroupOrder,
1030 joining_request_id: VarInt,
1031 joining_start: VarInt,
1032 parameters: Vec<KeyValuePair>,
1033 ) -> Result<VarInt, ConnectionError> {
1034 let (req_id, msg) = self.endpoint.absolute_joining_fetch(
1035 subscriber_priority,
1036 group_order,
1037 joining_request_id,
1038 joining_start,
1039 parameters,
1040 )?;
1041 self.send_control(&msg).await?;
1042 Ok(req_id)
1043 }
1044
1045 /// Send a FETCH_CANCEL for the given request ID.
1046 pub async fn fetch_cancel(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1047 let msg = self.endpoint.fetch_cancel(request_id)?;
1048 self.send_control(&msg).await
1049 }
1050
1051 /// Accept a fetch the peer opened, sending FETCH_OK.
1052 ///
1053 /// The endpoint refuses a Joining Fetch naming a subscription this session
1054 /// cannot join and refuses a second answer to one FETCH, so nothing is
1055 /// written on the wire when it does either.
1056 pub async fn fetch_ok(
1057 &mut self,
1058 request_id: VarInt,
1059 group_order: GroupOrder,
1060 end_of_track: u8,
1061 end_location: Location,
1062 parameters: Vec<KeyValuePair>,
1063 ) -> Result<(), ConnectionError> {
1064 let msg = self.endpoint.send_fetch_ok(
1065 request_id,
1066 group_order,
1067 end_of_track,
1068 end_location,
1069 parameters,
1070 )?;
1071 self.send_control(&msg).await
1072 }
1073
1074 /// Refuse a fetch the peer opened, sending FETCH_ERROR.
1075 ///
1076 /// The endpoint refuses a second answer to one FETCH, and refuses a
1077 /// Joining Fetch's refusal under any code but the one the draft names for
1078 /// it, so nothing is written on the wire when it does either.
1079 pub async fn fetch_error(
1080 &mut self,
1081 request_id: VarInt,
1082 error_code: VarInt,
1083 reason_phrase: Vec<u8>,
1084 ) -> Result<(), ConnectionError> {
1085 let msg = self.endpoint.send_fetch_error(request_id, error_code, reason_phrase)?;
1086 self.send_control(&msg).await
1087 }
1088
1089 // ── Namespace flows ─────────────────────────────────────
1090
1091 /// Send a SUBSCRIBE_NAMESPACE and return the request ID.
1092 pub async fn subscribe_namespace(
1093 &mut self,
1094 track_namespace: TrackNamespace,
1095 parameters: Vec<KeyValuePair>,
1096 ) -> Result<VarInt, ConnectionError> {
1097 let (req_id, msg) = self.endpoint.subscribe_namespace(track_namespace, parameters)?;
1098 self.send_control(&msg).await?;
1099 Ok(req_id)
1100 }
1101
1102 /// Accept a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_OK.
1103 ///
1104 /// The endpoint refuses a second answer to one SUBSCRIBE_NAMESPACE, so nothing is
1105 /// written on the wire when it does.
1106 pub async fn subscribe_namespace_ok(
1107 &mut self,
1108 request_id: VarInt,
1109 ) -> Result<(), ConnectionError> {
1110 let msg = self.endpoint.send_subscribe_namespace_ok(request_id)?;
1111 self.send_control(&msg).await
1112 }
1113
1114 /// Refuse a namespace subscription the peer made, sending SUBSCRIBE_NAMESPACE_ERROR.
1115 ///
1116 /// The other half of the same sentence: one answer, and this is the other
1117 /// one it can be.
1118 pub async fn subscribe_namespace_error(
1119 &mut self,
1120 request_id: VarInt,
1121 error_code: VarInt,
1122 reason_phrase: Vec<u8>,
1123 ) -> Result<(), ConnectionError> {
1124 let msg =
1125 self.endpoint.send_subscribe_namespace_error(request_id, error_code, reason_phrase)?;
1126 self.send_control(&msg).await
1127 }
1128
1129 /// Send a PUBLISH_NAMESPACE and return the request ID.
1130 pub async fn publish_namespace(
1131 &mut self,
1132 track_namespace: TrackNamespace,
1133 parameters: Vec<KeyValuePair>,
1134 ) -> Result<VarInt, ConnectionError> {
1135 let (req_id, msg) = self.endpoint.publish_namespace(track_namespace, parameters)?;
1136 self.send_control(&msg).await?;
1137 Ok(req_id)
1138 }
1139
1140 /// Accept an announcement the peer made, sending PUBLISH_NAMESPACE_OK.
1141 ///
1142 /// The endpoint refuses a second answer to one PUBLISH_NAMESPACE, so
1143 /// nothing is written on the wire when it does.
1144 pub async fn publish_namespace_ok(
1145 &mut self,
1146 request_id: VarInt,
1147 ) -> Result<(), ConnectionError> {
1148 let msg = self.endpoint.send_publish_namespace_ok(request_id)?;
1149 self.send_control(&msg).await
1150 }
1151
1152 /// Refuse an announcement the peer made, sending PUBLISH_NAMESPACE_ERROR.
1153 ///
1154 /// The other half of the same sentence: one answer, and this is the other
1155 /// one it can be.
1156 pub async fn publish_namespace_error(
1157 &mut self,
1158 request_id: VarInt,
1159 error_code: VarInt,
1160 reason_phrase: Vec<u8>,
1161 ) -> Result<(), ConnectionError> {
1162 let msg =
1163 self.endpoint.send_publish_namespace_error(request_id, error_code, reason_phrase)?;
1164 self.send_control(&msg).await
1165 }
1166
1167 /// Revoke an acceptance, sending PUBLISH_NAMESPACE_CANCEL.
1168 ///
1169 /// The endpoint refuses one for an announcement it never accepted, so
1170 /// nothing is written on the wire when it does.
1171 pub async fn publish_namespace_cancel(
1172 &mut self,
1173 track_namespace: TrackNamespace,
1174 error_code: VarInt,
1175 reason_phrase: Vec<u8>,
1176 ) -> Result<(), ConnectionError> {
1177 let msg =
1178 self.endpoint.publish_namespace_cancel(track_namespace, error_code, reason_phrase)?;
1179 self.send_control(&msg).await
1180 }
1181
1182 /// Withdraw an announcement this endpoint made, sending
1183 /// PUBLISH_NAMESPACE_DONE.
1184 ///
1185 /// The mirror of [`Self::publish_namespace`], and the counterpart of
1186 /// [`Self::publish_namespace_cancel`]: this one ends an announcement of
1187 /// this endpoint's, that one revokes the acceptance of one the peer made.
1188 pub async fn publish_namespace_done(
1189 &mut self,
1190 track_namespace: TrackNamespace,
1191 ) -> Result<(), ConnectionError> {
1192 let msg = self.endpoint.publish_namespace_done(track_namespace)?;
1193 self.send_control(&msg).await
1194 }
1195 // ── Track Status flow ────────────────────────────────────
1196
1197 /// Send a TRACK_STATUS and return the allocated request ID.
1198 #[allow(clippy::too_many_arguments)]
1199 pub async fn track_status(
1200 &mut self,
1201 track_namespace: TrackNamespace,
1202 track_name: Vec<u8>,
1203 subscriber_priority: u8,
1204 group_order: GroupOrder,
1205 forward: Forward,
1206 filter_type: FilterType,
1207 parameters: Vec<KeyValuePair>,
1208 ) -> Result<VarInt, ConnectionError> {
1209 let (req_id, msg) = self.endpoint.track_status(
1210 track_namespace,
1211 track_name,
1212 subscriber_priority,
1213 group_order,
1214 forward,
1215 filter_type,
1216 parameters,
1217 )?;
1218 self.send_control(&msg).await?;
1219 Ok(req_id)
1220 }
1221
1222 /// Accept a track status the peer asked for, sending TRACK_STATUS_OK.
1223 ///
1224 /// The endpoint refuses a second answer to one request, so nothing is
1225 /// written on the wire when it does. It also chooses the Track Alias the
1226 /// message carries, because the draft leaves only one value open.
1227 pub async fn track_status_ok(
1228 &mut self,
1229 request_id: VarInt,
1230 expires: VarInt,
1231 group_order: GroupOrder,
1232 parameters: Vec<KeyValuePair>,
1233 ) -> Result<(), ConnectionError> {
1234 let msg =
1235 self.endpoint.send_track_status_ok(request_id, expires, group_order, parameters)?;
1236 self.send_control(&msg).await
1237 }
1238
1239 /// Refuse a track status the peer asked for, sending TRACK_STATUS_ERROR.
1240 ///
1241 /// The other answer the request can have, and the endpoint holds it to the
1242 /// same count of one.
1243 pub async fn track_status_error(
1244 &mut self,
1245 request_id: VarInt,
1246 error_code: VarInt,
1247 reason_phrase: Vec<u8>,
1248 ) -> Result<(), ConnectionError> {
1249 let msg = self.endpoint.send_track_status_error(request_id, error_code, reason_phrase)?;
1250 self.send_control(&msg).await
1251 }
1252
1253 // ── Publish flow (publisher side) ───────────────────────
1254
1255 /// Offer the peer a subscription to a track this endpoint publishes, and
1256 /// return the Request ID the offer was allocated.
1257 ///
1258 /// Nothing is written on the wire when the endpoint refuses to build the
1259 /// offer, which it does when the Track Alias is one another live track of
1260 /// this session already holds.
1261 #[allow(clippy::too_many_arguments)]
1262 pub async fn publish(
1263 &mut self,
1264 track_namespace: TrackNamespace,
1265 track_name: Vec<u8>,
1266 track_alias: VarInt,
1267 group_order: GroupOrder,
1268 largest_location: Option<Location>,
1269 forward: Forward,
1270 parameters: Vec<KeyValuePair>,
1271 ) -> Result<VarInt, ConnectionError> {
1272 let (req_id, msg) = self.endpoint.publish(
1273 track_namespace,
1274 track_name,
1275 track_alias,
1276 group_order,
1277 largest_location,
1278 forward,
1279 parameters,
1280 )?;
1281 self.send_control(&msg).await?;
1282 Ok(req_id)
1283 }
1284
1285 /// Send a PUBLISH_DONE for the given request ID.
1286 pub async fn publish_done(
1287 &mut self,
1288 request_id: VarInt,
1289 status_code: VarInt,
1290 reason_phrase: Vec<u8>,
1291 ) -> Result<(), ConnectionError> {
1292 let msg = self.endpoint.send_publish_done(request_id, status_code, reason_phrase)?;
1293 self.send_control(&msg).await
1294 }
1295
1296 // ── Malformed Tracks ────────────────────────────────────
1297
1298 /// Send what Section 2.5 asks for when this endpoint finds a track
1299 /// malformed.
1300 ///
1301 /// "it MUST UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for
1302 /// that Track from that publisher" — one message per request, in Request
1303 /// ID order, and the endpoint decides which message each request takes.
1304 ///
1305 /// A write that fails is not reported. The caller is on its way to
1306 /// returning an error that says what went wrong with the track, and a
1307 /// control stream that will not take an UNSUBSCRIBE is a session on its
1308 /// way out for a reason of its own; replacing the condition's report with
1309 /// a transport error would lose the only account of why the track was
1310 /// withdrawn. The rest of the withdrawal is abandoned, because a stream
1311 /// that refused one message will refuse the next.
1312 async fn withdraw_malformed_track(&self, alias: u64, condition: MalformedTrackCondition) {
1313 for msg in self.endpoint.withdraw_malformed_track(alias, condition) {
1314 if self.send_control(&msg).await.is_err() {
1315 break;
1316 }
1317 }
1318 }
1319
1320 /// Record the framing an arriving object was sent with, and withdraw from
1321 /// the track when it is the second framing that track has been sent.
1322 ///
1323 /// The receiving half of a pair. The two writing paths call the endpoint
1324 /// directly and answer a mixed track by refusing to write it, because
1325 /// Section 2.5's sentence is a subscriber's: an endpoint about to send an
1326 /// object is that object's Original Publisher, and a publisher has no
1327 /// subscription of its own to withdraw and no fetch of its own to cancel.
1328 async fn note_received_framing(
1329 &self,
1330 alias: u64,
1331 seen: ObjectForwardingPreference,
1332 ) -> Result<(), ConnectionError> {
1333 let Err(err) = self.endpoint.note_object_forwarding_preference(alias, seen) else {
1334 return Ok(());
1335 };
1336 self.withdraw_malformed_track(alias, MalformedTrackCondition::MixedForwardingPreference)
1337 .await;
1338 Err(err.into())
1339 }
1340
1341 // ── Data streams ────────────────────────────────────────
1342
1343 /// Open a new unidirectional stream for sending subgroup data.
1344 pub async fn open_subgroup_stream(
1345 &self,
1346 header: &AnySubgroupHeader,
1347 ) -> Result<FramedSendStream, ConnectionError> {
1348 // Before the stream is opened: the Original Publisher is who the rule
1349 // binds, so a header that would mix this track's framing is refused
1350 // here rather than written and answered by the peer.
1351 self.endpoint.note_object_forwarding_preference(
1352 header.track_alias(),
1353 ObjectForwardingPreference::Subgroup,
1354 )?;
1355 let send = self.transport.open_uni().await?;
1356 let mut framed = FramedSendStream::new(send, self.draft);
1357 let sid = framed.stream_id();
1358 framed.write_subgroup_header(header).await?;
1359 self.emit(ClientEvent::StreamOpened {
1360 direction: Direction::Send,
1361 stream_kind: StreamKind::Subgroup,
1362 stream_id: sid,
1363 });
1364 self.emit(ClientEvent::DataStreamHeader {
1365 stream_id: sid,
1366 direction: Direction::Send,
1367 header: header.clone(),
1368 });
1369 Ok(framed)
1370 }
1371
1372 /// Open a new unidirectional stream for sending a FETCH's objects.
1373 ///
1374 /// The objects answering a FETCH do not go on the request's own stream:
1375 /// they go on a unidirectional stream of their own, which opens with a
1376 /// FETCH_HEADER naming the request they belong to. This writes that header
1377 /// and hands back the stream, the same way
1378 /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1379 /// subgroup.
1380 ///
1381 /// The caller owns the stream that comes back. Nothing here remembers
1382 /// which request it belongs to, so an endpoint serving several fetches at
1383 /// once keeps its own map from Request ID to stream.
1384 pub async fn open_fetch_stream(
1385 &self,
1386 header: &AnyFetchHeader,
1387 ) -> Result<FramedSendStream, ConnectionError> {
1388 let send = self.transport.open_uni().await?;
1389 let mut framed = FramedSendStream::new(send, self.draft);
1390 let sid = framed.stream_id();
1391 framed.write_fetch_header(header).await?;
1392 self.emit(ClientEvent::StreamOpened {
1393 direction: Direction::Send,
1394 stream_kind: StreamKind::Fetch,
1395 stream_id: sid,
1396 });
1397 Ok(framed)
1398 }
1399
1400 /// Accept the next unidirectional stream and read its fetch header.
1401 ///
1402 /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1403 /// are separate because the header decides how every object after it is
1404 /// framed, so a caller has to know which it is expecting before the first
1405 /// byte is read.
1406 ///
1407 /// Objects come off the returned stream with
1408 /// [`FramedRecvStream::read_fetch_object`].
1409 pub async fn accept_fetch_stream(
1410 &self,
1411 ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1412 let recv = self.transport.accept_uni().await?;
1413 let mut framed = FramedRecvStream::new(recv, self.draft);
1414 let sid = framed.stream_id();
1415 let header = framed.read_fetch_header().await?;
1416 self.emit(ClientEvent::StreamOpened {
1417 direction: Direction::Receive,
1418 stream_kind: StreamKind::Fetch,
1419 stream_id: sid,
1420 });
1421 self.emit(ClientEvent::FetchStreamHeader {
1422 stream_id: sid,
1423 direction: Direction::Receive,
1424 header: header.clone(),
1425 });
1426 // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1427 // carries an `AnySubgroupHeader` and cannot express one. What
1428 // `accept_subgroup_stream` does beyond this - the forwarding-preference
1429 // note, the object measurement - is about a subgroup and has no
1430 // counterpart on a fetch stream.
1431 Ok((header, framed))
1432 }
1433
1434 /// Accept an incoming unidirectional data stream and read its subgroup
1435 /// header.
1436 pub async fn accept_subgroup_stream(
1437 &self,
1438 ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1439 let recv = self.transport.accept_uni().await?;
1440 let mut framed = FramedRecvStream::new(recv, self.draft);
1441 let sid = framed.stream_id();
1442 let header = framed.read_subgroup_header().await?;
1443 self.emit(ClientEvent::StreamOpened {
1444 direction: Direction::Receive,
1445 stream_kind: StreamKind::Subgroup,
1446 stream_id: sid,
1447 });
1448 self.emit(ClientEvent::DataStreamHeader {
1449 stream_id: sid,
1450 direction: Direction::Receive,
1451 header: header.clone(),
1452 });
1453 // Every object on a subgroup stream has the Subgroup preference, so
1454 // the header settles the track's framing before a single object is
1455 // read.
1456 self.note_received_framing(header.track_alias(), ObjectForwardingPreference::Subgroup)
1457 .await?;
1458 // The track is resolved here and not inside the stream: it takes the
1459 // endpoint's alias table, which a stream handle has no way back to.
1460 if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
1461 framed.measure_objects_against(objects, header.group_id());
1462 }
1463 Ok((header, framed))
1464 }
1465
1466 /// Send an object via datagram.
1467 ///
1468 /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1469 /// header whose Object Status the framing it names cannot carry. Such a
1470 /// header errors here and nothing is sent, rather than going out as an
1471 /// ordinary payload datagram with the status quietly dropped.
1472 pub fn send_datagram(
1473 &self,
1474 header: &AnyDatagramHeader,
1475 payload: &[u8],
1476 ) -> Result<(), ConnectionError> {
1477 // Before anything is encoded, for the reason `open_subgroup_stream`
1478 // gives.
1479 self.endpoint.note_object_forwarding_preference(
1480 header.meta().track_alias,
1481 ObjectForwardingPreference::Datagram,
1482 )?;
1483 let mut buf = Vec::new();
1484 header.encode(&mut buf)?;
1485 buf.extend_from_slice(payload);
1486 self.emit(ClientEvent::DatagramReceived {
1487 direction: Direction::Send,
1488 header: header.clone(),
1489 payload_len: payload.len(),
1490 });
1491 self.transport.send_datagram(bytes::Bytes::from(buf))?;
1492 Ok(())
1493 }
1494
1495 /// Receive a datagram and decode its header.
1496 pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1497 let data = self.transport.recv_datagram().await?;
1498 let mut cursor = &data[..];
1499 let header = AnyDatagramHeader::decode(self.draft, &mut cursor)?;
1500 let consumed = data.len() - cursor.len();
1501 let payload = data.slice(consumed..);
1502 self.emit(ClientEvent::DatagramReceived {
1503 direction: Direction::Receive,
1504 header: header.clone(),
1505 payload_len: payload.len(),
1506 });
1507 // A datagram is the other framing, and it settles the track's just as a
1508 // subgroup header does.
1509 self.note_received_framing(header.meta().track_alias, ObjectForwardingPreference::Datagram)
1510 .await?;
1511 // A datagram is a whole object, so the connection can measure it
1512 // without help from the caller - and answer the condition itself,
1513 // because an UNSUBSCRIBE takes the connection an object on a stream
1514 // cannot reach.
1515 let meta = header.meta();
1516 if let Err(err) = self.endpoint.note_received_object(
1517 meta.track_alias,
1518 ObjectLocation { group: meta.group_id, object: meta.object_id },
1519 object_role(meta.status),
1520 ) {
1521 self.withdraw_malformed_track(
1522 meta.track_alias,
1523 MalformedTrackCondition::ObjectPastFinalObject,
1524 )
1525 .await;
1526 return Err(err.into());
1527 }
1528 Ok((header, payload))
1529 }
1530
1531 // ── Accessors ───────────────────────────────────────────
1532
1533 /// Access the underlying endpoint state machine.
1534 pub fn endpoint(&self) -> &Endpoint {
1535 &self.endpoint
1536 }
1537
1538 /// Mutable access to the endpoint state machine.
1539 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
1540 &mut self.endpoint
1541 }
1542
1543 /// Get the negotiated MoQT version.
1544 pub fn negotiated_version(&self) -> Option<VarInt> {
1545 self.endpoint.negotiated_version()
1546 }
1547
1548 /// Returns the draft version this connection is using.
1549 pub fn draft(&self) -> DraftVersion {
1550 self.draft
1551 }
1552
1553 /// The code to close the session with when a message could not be decoded
1554 /// because the peer broke a rule draft-14 answers with a close.
1555 ///
1556 /// Every variant listed here comes from a sentence in this draft that names
1557 /// the consequence, and the list is per draft: answering a bound this draft
1558 /// does not state would close a session over traffic a conforming peer may
1559 /// send.
1560 ///
1561 /// - Reason Phrase, maximum 1024 bytes: "If an endpoint receives a length
1562 /// exceeding the maximum, it MUST close the session with a
1563 /// PROTOCOL_VIOLATION."
1564 /// - GOAWAY New Session URI, maximum 8,192 bytes, with the same sentence.
1565 /// Drafts 11 through 19 state it; 07 through 10 state no maximum for
1566 /// the field at all.
1567 /// - Key-Value-Pair value, maximum 2^16-1 bytes, with the same sentence.
1568 /// - Track Namespace tuple size: "If an endpoint receives a Track
1569 /// Namespace tuple with an N of 0 or more than 32, it MUST close the
1570 /// session with a Protocol Violation." Note the lower bound - an empty
1571 /// tuple is refused here, where drafts 17 and later permit one.
1572 /// - Full Track Name, maximum 4,096 bytes, "computed as the sum of the
1573 /// lengths of each Track Namespace tuple field and the Track Name
1574 /// length field". This draft bounds the pair and not the namespace
1575 /// alone; draft-16 widened it.
1576 /// - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
1577 /// check that there are no unauthorized duplicate parameters and close
1578 /// the session as a PROTOCOL_VIOLATION if found." The rule is
1579 /// asymmetric here - "Receivers MUST allow duplicates of unknown
1580 /// parameters", and one known type is granted repeats - and the codec
1581 /// reports only the repeats this draft actually forbids.
1582 /// - Unknown control message type: "An endpoint that receives an unknown
1583 /// message type MUST close the session."
1584 /// - Extension headers on an Object whose status is Object Does Not
1585 /// Exist, Section 10.2.1.2. That one arrives on a data stream or a
1586 /// datagram, so [`Connection::close_for_data_stream`] is what carries
1587 /// it.
1588 ///
1589 /// **Not** the zero-length Track Namespace Field, the delta-encoded
1590 /// parameter type overflow, or the Object ID delta wrap. Those enter the
1591 /// specification at drafts 16 and 18, and this draft states none of them.
1592 ///
1593 /// **Not** the 2^16-1 control message length either. This draft states the
1594 /// limit - "the total length of a control message is limited to 2^16-1
1595 /// bytes" - and states no consequence for exceeding it, so an oversized
1596 /// message is refused by the decoder and stops there.
1597 ///
1598 /// **Not** [`CodecError::UnexpectedEnd`], which reports no rule at all: the
1599 /// reader raises it whenever a message is still arriving, and
1600 /// `read_control` loops on it. Closing over it would end a session on an
1601 /// ordinary short read.
1602 ///
1603 /// **Not** the unknown Message Parameter rule. Drafts 16 through 19 require
1604 /// a close for a Message Parameter whose type the negotiated version does
1605 /// not define. This draft states the opposite and states it about the same
1606 /// parameters: "Receivers MUST allow duplicates of unknown parameters",
1607 /// which presumes an unknown parameter arrives and is carried. Refusing one
1608 /// here would close a session over an extension this draft leaves room for.
1609 ///
1610 /// `None` for everything else, including [`CodecError::InvalidField`]. That
1611 /// variant is shared by a dozen unrelated malformations, only some of which
1612 /// the draft answers with a close, so a session cannot be ended on it
1613 /// without ending sessions the draft does not ask to be ended. Splitting it
1614 /// is the way to bring the rest of those rules under this function;
1615 /// widening the match is not - the extension-header rule above reached this
1616 /// table by being split out of it.
1617 fn codec_session_error_code(
1618 err: &CodecError,
1619 ) -> Option<moqtap_codec::draft14::error_codes::SessionErrorCode> {
1620 use moqtap_codec::draft14::error_codes::SessionErrorCode;
1621 use moqtap_codec::kvp::KvpError;
1622 match err {
1623 // The declared Length disagreeing with the fields, which every
1624 // draft answers with a close. Drafts 07 through 10 name no code for
1625 // it, so it takes the one their other unnamed rules take.
1626 // A Filter Type outside the four this draft assigns, Section 9.7:
1627 // "An endpoint that receives a filter type other than the above MUST
1628 // be close the session with PROTOCOL_VIOLATION" — the missing word
1629 // is the draft's. This is the draft the rule gained a consequence
1630 // on. Drafts 07 through 13 write the same sentence as "MUST be
1631 // treated as error", which names none - draft-13 Section 8.7 among
1632 // them - and their tables leave it unanswered.
1633 CodecError::InvalidFilterType(_) => Some(SessionErrorCode::ProtocolViolation),
1634 // A Fetch Type outside the three this draft assigns, Section
1635 // 9.16: "An endpoint that receives a Fetch Type other than 0x1,
1636 // 0x2 or 0x3 MUST be close the session with a PROTOCOL_VIOLATION."
1637 // The missing word is the draft's, as it is for the filter type
1638 // above. The value decides which
1639 // fields follow it — a Standalone fetch carries a track name and a
1640 // range where a joining fetch carries a Request ID and an offset —
1641 // so a reader that cannot name the type cannot find the end of the
1642 // message.
1643 CodecError::InvalidFetchType(_) => Some(SessionErrorCode::ProtocolViolation),
1644 CodecError::ControlMessageLengthMismatch { .. } => {
1645 Some(SessionErrorCode::ProtocolViolation)
1646 }
1647 CodecError::ReasonPhraseTooLong
1648 | CodecError::GoAwayUriTooLong
1649 | CodecError::InvalidNamespaceTupleSize(_)
1650 | CodecError::TrackNameTooLong
1651 | CodecError::DuplicateParameter(_)
1652 | CodecError::UnknownMessageType(_)
1653 | CodecError::ExtensionsOnNonExistentObject(_)
1654 | CodecError::Kvp(KvpError::ValueTooLong(_)) => {
1655 Some(SessionErrorCode::ProtocolViolation)
1656 }
1657 // An unknown data-plane type, Section 10: "An endpoint that
1658 // receives an unknown stream or datagram type MUST close the
1659 // session." One sentence covering two tables, which is why both
1660 // variants sit here.
1661 // A Content Exists field that is neither zero nor one, Sections 9.8
1662 // and 9.13: "Any other value is a protocol error and MUST terminate
1663 // the session with a PROTOCOL_VIOLATION".
1664 CodecError::InvalidContentExists(_) => Some(SessionErrorCode::ProtocolViolation),
1665 // A Forward field that is neither zero nor one. Sections 9.7 and
1666 // 9.10 use the sentence above; Section 9.13 says "Any value other
1667 // than 0 or 1 is a PROTOCOL_VIOLATION". Section 9.14 names the two
1668 // legal values without a consequence and is answered the same way,
1669 // for the reason given on the variant. Draft-15 replaces the field
1670 // with the FORWARD parameter, which carries the same rule in a
1671 // different shape and reports it as `ParameterValueOutOfRange`.
1672 CodecError::InvalidForward(_) => Some(SessionErrorCode::ProtocolViolation),
1673 CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
1674 Some(SessionErrorCode::ProtocolViolation)
1675 }
1676 // A key-value pair whose value is not the serialization its own
1677 // Type defines, Section 1.4.2: "If a receiver understands a Type,
1678 // and the following Value or Length/Value does not match the
1679 // serialization defined by that Type, the receiver MUST terminate the
1680 // session with error code KEY_VALUE_FORMATTING_ERROR."
1681 //
1682 // Section 9.2.1.1 states the same answer for the one structure this
1683 // draft spells out: "If the Token structure cannot be decoded, the
1684 // receiver MUST close the Session with Key-Value Formatting error."
1685 //
1686 // The one rule in this table that names a code other than Protocol
1687 // Violation.
1688 CodecError::KeyValueFormatting { .. } => {
1689 Some(SessionErrorCode::KeyValueFormattingError)
1690 }
1691 // Everything this draft does not answer, named rather than swept up
1692 // by a wildcard. The arm is exhaustive deliberately: a new
1693 // `CodecError` variant will not compile until it has been placed on
1694 // one side or the other, on this draft, which is the decision a `_`
1695 // arm makes silently and invisibly on all thirteen at once.
1696 //
1697 // Adding one variant to `CodecError` was tried, and produces
1698 // thirteen `E0004`s, one per draft, each naming the variant that has
1699 // nowhere to go. That is the whole mechanism.
1700 //
1701 // The nesting stops at `VarInt`, whose variants report how the bytes
1702 // ran out rather than a rule an endpoint states, so there is nothing
1703 // in it for a draft to answer. `Kvp` is spelled out because it does
1704 // carry one.
1705 // Not `ParameterValueOutOfRange`: no parameter this draft defines
1706 // restricts its value's range. Forwarding is a message field here
1707 // and is answered above.
1708 CodecError::ParameterValueOutOfRange { .. }
1709 | CodecError::UnexpectedEnd
1710 | CodecError::MessageTooLong(_)
1711 | CodecError::VarInt(_)
1712 | CodecError::InvalidField
1713 | CodecError::EmptyNamespaceField
1714 | CodecError::InvalidRange(..)
1715 | CodecError::ParameterLengthMismatch(_)
1716 | CodecError::EndOfTrackObjectId(_)
1717 | CodecError::KeyDeltaOverflow(..)
1718 // Not `TrackPropertyValueOutOfRange`: this draft has neither
1719 // namespace the variant is about. Draft-16 opens an extension header
1720 // registry with value rules of its own, and draft-17 renames it to
1721 // the Track Property registry. Before that, everything with a
1722 // restricted range is either a message field or a Message Parameter.
1723 | CodecError::TrackPropertyValueOutOfRange { .. }
1724 | CodecError::ParametersOutOfOrder(..)
1725 | CodecError::ObjectIdOverflow(..)
1726 | CodecError::InvalidRequiredRequestIdDelta(..)
1727 | CodecError::InvalidTypeValue { .. }
1728 | CodecError::UnknownMessageParameter(_)
1729 // Not `ParameterOutOfScope`: this draft states the scope rule and
1730 // answers it the other way. Section 9.2.1 Version Specific Parameters: "Each
1731 // version-specific parameter definition indicates the message types in which it can
1732 // appear. If it appears in some other type of message, it MUST be
1733 // ignored." The codec carries such a parameter on this draft and never
1734 // raises the variant, so this arm records a rule this draft has and
1735 // does not close over, not one it is missing. Draft-17 is where the
1736 // second sentence becomes a close.
1737 | CodecError::ParameterOutOfScope { .. }
1738 // Both belong to the parameter form of the filter. Draft-15 moved
1739 // the Filter Type, Start Location and End Group out of SUBSCRIBE and
1740 // into one length-prefixed parameter; this draft still carries them
1741 // as fields, where a value that runs short is a truncated frame and
1742 // an End Group is written out rather than added to anything.
1743 | CodecError::SubscriptionFilterMalformed { .. }
1744 | CodecError::FilterEndGroupOverflow { .. }
1745 // The object payload rule, Section 10.2.1.1: "Any object with a status
1746 // code other than zero MUST have an empty payload." A MUST on the
1747 // sender with no receiver action named anywhere — the "SHOULD be
1748 // treated as a protocol error" in the same paragraph belongs to the
1749 // sentence before it, which is about a status value this draft does
1750 // not assign — so an object carrying a payload it may not is refused
1751 // and the session stays open.
1752 //
1753 // That was already the answer. The bytes used to arrive as
1754 // `InvalidField`, which is on this side too; naming the rule changes
1755 // nothing a peer can observe and makes the decision legible.
1756 | CodecError::PayloadNotPermitted { .. }
1757 | CodecError::UnsupportedDraft(_)
1758 | CodecError::Kvp(
1759 KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
1760 ) => None,
1761 }
1762 }
1763
1764 /// Close the session on the wire when a decode failure is one draft-14
1765 /// answers with a close, and hand the error back unchanged.
1766 /// Without it every bound the decoder enforces would stop at *this endpoint
1767 /// refused the frame* while the peer, which is the one that broke the rule,
1768 /// saw a session that was still open and went on sending. "MUST close the
1769 /// session" is a statement about the wire.
1770 fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
1771 if let ConnectionError::Codec(inner) = &err {
1772 if let Some(code) = Self::codec_session_error_code(inner) {
1773 // QUIC application error codes are 62-bit; every code in this
1774 // registry is far below `u32::MAX`, and saturating rather than
1775 // truncating means a future code that is not could never be
1776 // reported as a different, assigned one.
1777 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1778 self.close(wire_code, inner.to_string().as_bytes());
1779 }
1780 }
1781 err
1782 }
1783
1784 /// Close the session on the wire when the endpoint says a violation is
1785 /// fatal to it, and hand the error back unchanged.
1786 ///
1787 /// [`EndpointError::session_error_code`] answers `Some` for exactly the
1788 /// errors this draft ends the session over, and the endpoint has already
1789 /// moved its own state machine to Closed by the time this runs. Without
1790 /// this step that move is purely internal: the local endpoint refuses to
1791 /// start anything new while the peer, which is the one that broke the
1792 /// rule, sees a session that is still open and goes on sending. A rule
1793 /// that names a session termination code is a statement about the wire,
1794 /// so it takes a CONNECTION_CLOSE to satisfy it.
1795 ///
1796 /// The reason phrase is the error's own `Display` text, which names the
1797 /// rule rather than repeating the numeric code the close already carries.
1798 ///
1799 /// Errors that answer `None` are recoverable and nothing is sent.
1800 fn close_for(&self, err: &EndpointError) {
1801 if let Some(code) = err.session_error_code() {
1802 // QUIC application error codes are 62-bit; every code in this
1803 // registry is far below `u32::MAX`, and saturating rather than
1804 // truncating means a future code that is not could never be
1805 // reported as a different, assigned one.
1806 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1807 self.close(wire_code, err.to_string().as_bytes());
1808 }
1809 }
1810
1811 /// [`close_for`](Self::close_for), then the error unchanged, for the
1812 /// common case where the endpoint's error is also what the caller returns.
1813 fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
1814 self.close_for(&err);
1815 ConnectionError::Endpoint(err)
1816 }
1817
1818 /// Withdraw from a track a data stream found malformed, reporting whether
1819 /// it did.
1820 ///
1821 /// The Malformed Track twin of [`Connection::close_for_data_stream`], and
1822 /// separate from it for the same reason and one more. The same one: a
1823 /// [`FramedRecvStream`] holds no connection, so the reader that finds the
1824 /// fault is not the object that can send an UNSUBSCRIBE. The one more: the
1825 /// two answers are opposites - that call ends the session, this one gives
1826 /// up a track and leaves it running - and a single entry point would have
1827 /// to decide between them from the error alone, which is exactly the
1828 /// decision a caller reproducing a capture wants to make itself.
1829 ///
1830 /// The datagram path needs none of this. It is read through the connection,
1831 /// so [`Connection::recv_datagram`] answers the condition where it finds
1832 /// it, and this is only for the objects that arrive on a stream the caller
1833 /// holds.
1834 pub async fn withdraw_for_data_stream(&self, err: &ConnectionError) -> bool {
1835 let ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) = err
1836 else {
1837 return false;
1838 };
1839 self.withdraw_malformed_track(*alias, MalformedTrackCondition::ObjectPastFinalObject).await;
1840 true
1841 }
1842
1843 /// Close the session over a rule broken on a data stream, reporting whether
1844 /// it did.
1845 ///
1846 /// A data stream cannot close for itself the way `recv_control` does:
1847 /// [`Connection::accept_subgroup_stream`] hands the caller a
1848 /// [`FramedRecvStream`] holding no connection, so the reader that finds the
1849 /// violation is not the object that can act on it. Keeping it a separate
1850 /// call is deliberate as well - a permissive caller, one reproducing a
1851 /// capture, can read a violating stream and report it without tearing the
1852 /// session down.
1853 ///
1854 /// The rule this draft answers here is extension headers on an Object whose
1855 /// status is Object Does Not Exist, which reaches subgroup streams, fetch
1856 /// streams and status datagrams alike. It shares `codec_session_error_code`
1857 /// with the control path, so a rule is answered with one code whichever
1858 /// stream carried it.
1859 ///
1860 /// Not every rule that reaches here is the decoder's. A track whose objects
1861 /// mix forwarding preferences is the endpoint's to notice — it takes the
1862 /// alias table to know which track an object belongs to — and it arrives on
1863 /// exactly these streams. Both kinds are asked for a code the same way, and
1864 /// a rule with no code is declined rather than guessed at.
1865 pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
1866 match err {
1867 ConnectionError::Codec(inner) => {
1868 let Some(code) = Self::codec_session_error_code(inner) else { return false };
1869 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1870 self.close(wire_code, inner.to_string().as_bytes());
1871 true
1872 }
1873 // A rule the endpoint raises rather than the decoder. The two
1874 // reach their codes through different tables and mean the same
1875 // thing here: `Some` is a rule this draft ends the session over.
1876 ConnectionError::Endpoint(inner) => {
1877 let Some(code) = inner.session_error_code() else { return false };
1878 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
1879 self.close(wire_code, inner.to_string().as_bytes());
1880 true
1881 }
1882 _ => false,
1883 }
1884 }
1885
1886 /// Close the connection.
1887 pub fn close(&self, code: u32, reason: &[u8]) {
1888 self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
1889 self.transport.close(code, reason);
1890 }
1891}
1892
1893/// Determine the encoded length of a varint from its first byte.
1894fn varint_len(first_byte: u8) -> usize {
1895 1 << (first_byte >> 6)
1896}
1897
1898#[cfg(test)]
1899mod tests {
1900 use super::*;
1901
1902 #[test]
1903 fn varint_len_single_byte() {
1904 // 0b00xxxxxx -> 1 byte
1905 assert_eq!(varint_len(0x00), 1);
1906 assert_eq!(varint_len(0x3F), 1);
1907 }
1908
1909 #[test]
1910 fn varint_len_two_bytes() {
1911 // 0b01xxxxxx -> 2 bytes
1912 assert_eq!(varint_len(0x40), 2);
1913 assert_eq!(varint_len(0x7F), 2);
1914 }
1915
1916 #[test]
1917 fn varint_len_four_bytes() {
1918 // 0b10xxxxxx -> 4 bytes
1919 assert_eq!(varint_len(0x80), 4);
1920 assert_eq!(varint_len(0xBF), 4);
1921 }
1922
1923 #[test]
1924 fn varint_len_eight_bytes() {
1925 // 0b11xxxxxx -> 8 bytes
1926 assert_eq!(varint_len(0xC0), 8);
1927 assert_eq!(varint_len(0xFF), 8);
1928 }
1929
1930 #[test]
1931 fn client_config_supported_versions_draft14() {
1932 let config = ClientConfig {
1933 draft: DraftVersion::Draft14,
1934 additional_versions: Vec::new(),
1935 transport: TransportType::Quic,
1936 skip_cert_verification: false,
1937 ca_certs: Vec::new(),
1938 setup_parameters: Vec::new(),
1939 };
1940 let versions = config.supported_versions();
1941 assert_eq!(versions.len(), 1);
1942 assert_eq!(versions[0].into_inner(), 0xff000000 + 14);
1943 }
1944
1945 #[test]
1946 fn client_config_supported_versions_draft07() {
1947 let config = ClientConfig {
1948 draft: DraftVersion::Draft07,
1949 additional_versions: Vec::new(),
1950 transport: TransportType::Quic,
1951 skip_cert_verification: false,
1952 ca_certs: Vec::new(),
1953 setup_parameters: Vec::new(),
1954 };
1955 let versions = config.supported_versions();
1956 assert_eq!(versions.len(), 1);
1957 assert_eq!(versions[0].into_inner(), 0xff000000 + 7);
1958 }
1959
1960 #[test]
1961 fn client_config_alpn_quic() {
1962 let config = ClientConfig {
1963 draft: DraftVersion::Draft14,
1964 additional_versions: Vec::new(),
1965 transport: TransportType::Quic,
1966 skip_cert_verification: false,
1967 ca_certs: Vec::new(),
1968 setup_parameters: Vec::new(),
1969 };
1970 assert_eq!(config.alpn(), vec![b"moq-00".to_vec()]);
1971 }
1972
1973 #[test]
1974 fn client_config_alpn_webtransport() {
1975 let config = ClientConfig {
1976 draft: DraftVersion::Draft14,
1977 additional_versions: Vec::new(),
1978 transport: TransportType::WebTransport { url: "https://example.com".to_string() },
1979 skip_cert_verification: false,
1980 ca_certs: Vec::new(),
1981 setup_parameters: Vec::new(),
1982 };
1983 assert_eq!(config.alpn(), vec![b"h3".to_vec()]);
1984 }
1985
1986 #[test]
1987 fn moqt_alpn_value() {
1988 assert_eq!(MOQT_ALPN, b"moq-00");
1989 }
1990
1991 #[test]
1992 fn transport_type_debug() {
1993 let quic = TransportType::Quic;
1994 assert!(format!("{quic:?}").contains("Quic"));
1995
1996 let wt = TransportType::WebTransport { url: "https://example.com".to_string() };
1997 assert!(format!("{wt:?}").contains("WebTransport"));
1998 }
1999}