moqtap_client/draft12/connection.rs
1use bytes::{Buf, Bytes, BytesMut};
2
3use crate::draft12::endpoint::{Endpoint, EndpointError};
4use crate::draft12::event::{ClientEvent, Direction, FetchObject, StreamKind, SubgroupObject};
5use crate::draft12::observer::ConnectionObserver;
6use crate::draft12::session::request_id::Role;
7use crate::draft12::session::setup;
8use crate::forwarding_preference::ObjectForwardingPreference;
9use crate::malformed_tracks::MalformedTrackCondition;
10use crate::track_locations::{EndOfTrackForm, ObjectLocation, ObjectRole, TrackObjects};
11use crate::transport::{RecvStream, SendStream, Transport, TransportError};
12use moqtap_codec::dispatch::{
13 AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
14};
15use moqtap_codec::draft12::data_stream::{FetchObjectHeader, ObjectHeader, SubgroupGaps};
16use moqtap_codec::draft12::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 draft-12 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: an object before its header, or an
57 /// Object ID that does not advance on the last one written.
58 #[error("data stream state error: {0}")]
59 DataStreamState(&'static str),
60 /// A control message this build decoded for draft-12 and then could not
61 /// narrow to draft-12's own message type.
62 ///
63 /// Unreachable, and that is not the same as harmless. `read_control`
64 /// decodes with this connection's own draft, so the `AnyControlMessage` it
65 /// hands back can only carry this draft's variant — but the narrowing arm
66 /// is compiled in every configuration anyway, under
67 /// `#[allow(unreachable_patterns)]` rather than a `cfg` naming the other
68 /// drafts, because such a list has to be edited in every draft
69 /// module whenever a draft is added, and a copy that omits one leaves the
70 /// match non-exhaustive.
71 ///
72 /// Spelled as `CodecError::UnknownMessageType(0)` it would not stay inert:
73 /// every draft's
74 /// [`codec_session_error_code`](Connection::codec_session_error_code)
75 /// answers that variant `Some(PROTOCOL_VIOLATION)`. So the day the
76 /// narrowing did fail, this build's own defect would reach a caller as *the
77 /// peer sent a control message type this draft does not assign, and the
78 /// session must be closed with a Protocol Violation* — carrying `0x00` as
79 /// the codepoint that proved it. A conformance report reading that
80 /// publishes a named, well-evidenced accusation against a relay for
81 /// something no relay did.
82 ///
83 /// A variant of its own is what stops that.
84 /// [`draft_specific_cause`](Connection::draft_specific_cause) answers it
85 /// [`LocalRefusal`], the facade turns that into [`ErrorCause::Facade`], and
86 /// nothing downstream can read a rule out of a cause that says nothing
87 /// reached the wire. What is pinned is the consequence rather than the
88 /// unreachability: nothing pins the arm's reachability, which is exactly
89 /// why the consequence must not be an accusation.
90 ///
91 /// [`LocalRefusal`]: crate::above_codec_rules::DraftSpecificCause::LocalRefusal
92 /// [`ErrorCause::Facade`]: crate::dispatch::ErrorCause::Facade
93 #[error(
94 "a control message decoded for draft-12 did not narrow to draft-12: a defect in this build, and evidence about nothing the peer did"
95 )]
96 ControlMessageNarrowing,
97 /// A datagram whose Type announces extension headers arrived with an
98 /// Extension Headers Length of 0.
99 ///
100 /// Types 0x01, 0x03 and 0x05 close the session. The two close rules still
101 /// carry draft-11's code points, and are read through the datagram table in
102 /// draft-12 Section 9, which assigns 0x00-0x03 to OBJECT_DATAGRAM and
103 /// 0x04-0x05 to OBJECT_DATAGRAM_STATUS:
104 ///
105 /// - draft-12 Section 9.3.2: "The Type field takes the form 0b0000001X (or
106 /// the set of values from 0x02 to 0x03)" and "If an endpoint receives a
107 /// datagram with Type 0x03 and Extension Headers Length is 0, it MUST
108 /// close the session with Protocol Violation." draft-11 Section 9 assigns
109 /// 0x02-0x03 to OBJECT_DATAGRAM_STATUS. draft-14 Appendix A.1 lists
110 /// "Corrected missed code point updates to Object Datagram Status
111 /// (#1082)", and that change rewrites the sentence to "Type 0x05". It
112 /// names 0x05.
113 /// - draft-12 Section 9.3.1: "If an endpoint receives a datagram with Type
114 /// 0x01 and Extension Headers Length is 0, it MUST close the session with
115 /// Protocol Violation." 0x01 is the only OBJECT_DATAGRAM Type with
116 /// extensions in draft-11 Section 9.2. draft-12 Section 9.3.1 Table 12
117 /// adds 0x03, End Of Group with Extensions Present "Yes", and the
118 /// sentence was not updated. draft-14 Section 10.3.1 states the rule for
119 /// every such Type: "If an endpoint receives a datagram with Extensions
120 /// Present as "Yes" and a Extension Headers Length of 0, it MUST close
121 /// the session with PROTOCOL_VIOLATION." It names 0x01 and 0x03.
122 ///
123 /// The codec's `DatagramType` carries the same ruling.
124 ///
125 /// The codec's header decoder refuses such a datagram as a bare
126 /// `InvalidField`. [`Connection::recv_datagram`] names it here, and
127 /// [`Connection::close_for_data_stream`] closes the session over it.
128 #[error(
129 "the datagram for object {object_id} announces extension headers and has an Extension Headers Length of 0"
130 )]
131 EmptyDatagramExtensions {
132 /// The Object ID the datagram named.
133 object_id: u64,
134 },
135}
136
137/// [`ConnectionError::EmptyDatagramExtensions`] for a datagram the header
138/// decoder refused as `InvalidField`, when its empty extension block was the
139/// fault.
140///
141/// Every Type the codec's `DatagramType` says carries extensions: 0x01, 0x03
142/// and 0x05. Each carries an Object ID and a Publisher Priority.
143fn empty_datagram_extensions(data: &[u8]) -> Option<ConnectionError> {
144 use crate::datagram_prefix::{empty_extension_block, Prefix};
145 use moqtap_codec::draft12::data_stream::DatagramType;
146 empty_extension_block(data, |datagram_type, _| {
147 DatagramType::from_id(datagram_type)?
148 .has_extensions()
149 .then_some(Prefix { object_id: true, priority: true })
150 })
151 .map(|object_id| ConnectionError::EmptyDatagramExtensions { object_id })
152}
153
154impl From<crate::transport::DialError> for ConnectionError {
155 /// Maps a dial failure onto the variants this error already has, so a
156 /// caller matches `InvalidAddress` or `TlsConfig`.
157 ///
158 /// A `LocalSocket` failure maps to `InvalidAddress`, which the facade reads
159 /// as this side's failure
160 /// ([`ErrorCause::Facade`](crate::dispatch::ErrorCause::Facade)). The dial
161 /// phase stays on [`DialError::phase`](crate::transport::DialError::phase).
162 fn from(e: crate::transport::DialError) -> Self {
163 match e {
164 // Two variants, one arm, deliberately — see above.
165 crate::transport::DialError::InvalidAddress(s)
166 | crate::transport::DialError::LocalSocket(s) => ConnectionError::InvalidAddress(s),
167 crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
168 crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
169 }
170 }
171}
172
173/// Transport type for the connection.
174#[derive(Debug, Clone)]
175pub enum TransportType {
176 /// Raw QUIC via quinn. The `addr` field should be `host:port`.
177 Quic,
178 /// WebTransport via wtransport. The `url` field is the WebTransport URL.
179 WebTransport {
180 /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
181 url: String,
182 },
183}
184
185/// Configuration for a draft-12 MoQT client connection.
186pub struct ClientConfig {
187 /// Additional draft versions to offer in CLIENT_SETUP (draft-12 is always
188 /// offered first).
189 pub additional_versions: Vec<DraftVersion>,
190 /// The transport type (QUIC or WebTransport).
191 pub transport: TransportType,
192 /// Whether to skip TLS certificate verification (for testing).
193 pub skip_cert_verification: bool,
194 /// Custom CA certificates to trust (DER-encoded).
195 pub ca_certs: Vec<Vec<u8>>,
196 /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
197 pub setup_parameters: Vec<moqtap_codec::kvp::KeyValuePair>,
198}
199
200impl ClientConfig {
201 /// Returns the MoQT version varints for the CLIENT_SETUP message.
202 /// Draft-12 first, then any additional versions.
203 pub fn supported_versions(&self) -> Vec<VarInt> {
204 let mut versions = vec![DraftVersion::Draft12.version_varint()];
205 for v in &self.additional_versions {
206 let varint = v.version_varint();
207 if !versions.contains(&varint) {
208 versions.push(varint);
209 }
210 }
211 versions
212 }
213
214 /// Returns the ALPN protocol identifiers for the transport.
215 pub fn alpn(&self) -> Vec<Vec<u8>> {
216 match &self.transport {
217 TransportType::Quic => vec![DraftVersion::Draft12.quic_alpn().to_vec()],
218 TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
219 }
220 }
221}
222
223/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
224pub struct FramedSendStream {
225 inner: SendStream,
226 /// What the subgroup header opened on this stream settled, once one has
227 /// been written.
228 ///
229 /// `None` until a subgroup header has been written, which is what makes an
230 /// object sent before its header answerable rather than unframed bytes.
231 subgroup_objects: Option<SubgroupStreamState>,
232}
233
234/// What a subgroup header fixes for every object written after it.
235///
236/// Two facts, and neither is recoverable from an object on its own: the
237/// Object ID it has to advance past, and whether the stream's type puts an
238/// extension block on each object. The second has to be remembered rather than
239/// guessed: a header type whose Extensions Present column reads Yes puts an
240/// Extension Headers Length on every object of the subgroup, so assuming
241/// "absent" would write a stream a reader could not follow.
242#[derive(Debug, Clone, Copy)]
243struct SubgroupStreamState {
244 /// The last Object ID written, or `None` before the first object.
245 previous_object_id: Option<u64>,
246 /// Whether each object writes an Extension Headers Length field.
247 carries_extension_block: bool,
248}
249
250impl FramedSendStream {
251 /// Create a new framed send stream.
252 pub fn new(inner: SendStream) -> Self {
253 Self { inner, subgroup_objects: None }
254 }
255
256 /// Get the transport-level stream ID.
257 pub fn stream_id(&self) -> u64 {
258 self.inner.stream_id()
259 }
260
261 /// Write a control message to the stream with type+length framing.
262 /// Returns the raw bytes that were written (for event capture).
263 pub async fn write_control(
264 &mut self,
265 msg: &AnyControlMessage,
266 ) -> Result<Vec<u8>, ConnectionError> {
267 let mut buf = Vec::new();
268 msg.encode(&mut buf)?;
269 self.inner.write_all(&buf).await?;
270 Ok(buf)
271 }
272
273 /// Write a subgroup stream header. Also opens the Object ID bookkeeping
274 /// [`FramedSendStream::write_subgroup_object`] holds the stream to.
275 ///
276 /// The header is refused, and nothing is written, if its fields disagree
277 /// with its own stream type. That check has to happen here rather than at
278 /// the first object: the type is what every object after it is framed
279 /// against, so a header that went out saying the wrong thing cannot be
280 /// taken back.
281 pub async fn write_subgroup_header(
282 &mut self,
283 header: &AnySubgroupHeader,
284 ) -> Result<(), ConnectionError> {
285 let mut buf = Vec::new();
286 header.encode_stream_checked(&mut buf)?;
287 self.inner.write_all(&buf).await?;
288 // The framing is read off the header now, while the header is in hand.
289 // An object arriving later cannot say whether the stream carries an
290 // extension block, and writing one either way is not a recoverable
291 // mistake: the reader takes the next field along as the missing length
292 // and misframes every object after it.
293 self.subgroup_objects = Some(SubgroupStreamState {
294 previous_object_id: None,
295 carries_extension_block: header.carries_extension_block(),
296 });
297 Ok(())
298 }
299
300 /// Write a fetch response header.
301 pub async fn write_fetch_header(
302 &mut self,
303 header: &AnyFetchHeader,
304 ) -> Result<(), ConnectionError> {
305 let mut buf = Vec::new();
306 header.encode_stream(&mut buf);
307 self.inner.write_all(&buf).await?;
308 Ok(())
309 }
310
311 /// Append a draft-12 subgroup object (header + payload) to the stream.
312 ///
313 /// Section 9.4.2: "A publisher MUST NOT send an Object on a stream if its
314 /// Object ID is less than a previously sent Object ID within a given group
315 /// in that stream." A subgroup stream carries one group, so the Object IDs
316 /// written here are exactly the ones that sentence compares, and the
317 /// comparison needs the object before - which no per-header check can see.
318 /// The state advances only once the object has been written, so declining to
319 /// write an object leaves the next one measured against the last one kept.
320 ///
321 /// An equal Object ID is refused as well as a smaller one. The draft's own
322 /// sentence forbids only "less than", but an Object ID names an Object
323 /// within a Group: writing one twice on a stream describes the same Object
324 /// with two different payloads, and a reader has no way to choose. The
325 /// dispatch-level writer in the codec draws the line in the same place, and
326 /// two writers that disagreed about it would be worse than either answer.
327 ///
328 /// # Errors
329 ///
330 /// [`ConnectionError::DataStreamState`] if no subgroup header has been
331 /// written yet, or if `object` does not advance past the last one written.
332 pub async fn write_subgroup_object(
333 &mut self,
334 object: &SubgroupObject,
335 ) -> Result<(), ConnectionError> {
336 let state = self
337 .subgroup_objects
338 .as_mut()
339 .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
340 let object_id = object.header.object_id.into_inner();
341 if matches!(state.previous_object_id, Some(prev) if object_id <= prev) {
342 return Err(ConnectionError::DataStreamState(
343 "object id does not advance on the last one written to this stream",
344 ));
345 }
346 // The declared length comes from the payload rather than from the
347 // caller's field: a header that disagrees with the bytes beside it
348 // desynchronises every object after it on the stream, and nothing
349 // downstream can recover.
350 let mut header = object.header.clone();
351 header.payload_length = VarInt::from_usize(object.payload.len());
352 let mut buf = Vec::new();
353 header.encode_checked_with_extensions(state.carries_extension_block, &mut buf)?;
354 buf.extend_from_slice(&object.payload);
355 self.inner.write_all(&buf).await?;
356 state.previous_object_id = Some(object_id);
357 Ok(())
358 }
359
360 /// Append a draft-12 fetch object (header + payload) to the stream.
361 pub async fn write_fetch_object(
362 &mut self,
363 object: &FetchObject,
364 ) -> Result<(), ConnectionError> {
365 // The declared length comes from the payload rather than from the
366 // caller's field: a header that disagrees with the bytes beside it
367 // desynchronises every object after it on the stream, and nothing
368 // downstream can recover.
369 let mut header = object.header.clone();
370 header.payload_length = VarInt::from_usize(object.payload.len());
371 let mut buf = Vec::new();
372 header.encode_checked(&mut buf)?;
373 buf.extend_from_slice(&object.payload);
374 self.inner.write_all(&buf).await?;
375 Ok(())
376 }
377
378 /// Finish the stream (send FIN).
379 pub async fn finish(&mut self) -> Result<(), ConnectionError> {
380 self.inner.finish()?;
381 Ok(())
382 }
383}
384
385/// What an Object Status makes of an object, for Section 9.2.1.1's table.
386///
387/// `None` is Normal and not "no status": a datagram carrying a payload has no
388/// status field at all, and an object with a payload is an ordinary object.
389fn object_role(status: Option<u64>) -> ObjectRole {
390 match status {
391 None | Some(0x0) => ObjectRole::Produced,
392 // 0x4, end of Track. This draft folded drafts 08 through 10's second
393 // end-of-track status into this one and kept the looser of the two
394 // conditions, so the Group ID may equal the largest group seen.
395 Some(0x4) => ObjectRole::EndsTrack(Some(EndOfTrackForm::LastGroup)),
396 // Every other status is a statement about objects rather than one of
397 // them. Two of them name an Object ID one past the largest on purpose,
398 // so counting one as produced would refuse the end-of-track object the
399 // draft goes on to define.
400 _ => ObjectRole::Neither,
401 }
402}
403
404/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
405pub struct FramedRecvStream {
406 inner: RecvStream,
407 buf: BytesMut,
408 /// The record this stream's objects are measured against, and the Group ID
409 /// its header named.
410 ///
411 /// One group for the whole stream: a subgroup header names it once and no
412 /// object header repeats it. `None` on a stream that was never given one —
413 /// a stream for an alias no live binding names, and every stream built
414 /// outside [`Connection::accept_subgroup_stream`] — and on such a stream
415 /// the objects are read without being measured against the track at all.
416 tracking: Option<(TrackObjects, u64)>,
417 /// Whether the subgroup stream this reader is on carries an extension block
418 /// on every object.
419 ///
420 /// The stream's Type says so and nothing in an object header repeats it, so
421 /// a reader that does not remember the Type cannot parse the objects at all:
422 /// on an extensions-bearing stream it reads the Extension Headers Length as
423 /// the Object Payload Length and every field after it is nonsense.
424 ///
425 /// Set by [`FramedRecvStream::read_subgroup_header`] and read by
426 /// [`FramedRecvStream::read_subgroup_object`]. `false` until a subgroup
427 /// header has been read, which is the only order those two may be called
428 /// in.
429 subgroup_has_extensions: bool,
430 /// The Prior Group ID Gap values this subgroup stream's Objects carried.
431 /// Set by [`FramedRecvStream::read_subgroup_header`].
432 subgroup_gaps: Option<SubgroupGaps>,
433 /// The Prior gap conditions this fetch stream's Objects show. Reset by
434 /// [`FramedRecvStream::read_fetch_header`].
435 fetch_gaps: moqtap_codec::draft12::data_stream::FetchGaps,
436 /// The bytes of the last control frame a capturing read refused.
437 refused: Option<Vec<u8>>,
438}
439
440impl FramedRecvStream {
441 /// Create a new framed receive stream.
442 pub fn new(inner: RecvStream) -> Self {
443 Self {
444 inner,
445 buf: BytesMut::with_capacity(4096),
446 subgroup_has_extensions: false,
447 subgroup_gaps: None,
448 tracking: None,
449 fetch_gaps: moqtap_codec::draft12::data_stream::FetchGaps::new(),
450 refused: None,
451 }
452 }
453
454 /// Measure this stream's objects against `objects`, all of them in `group`.
455 ///
456 /// Called by [`Connection::accept_subgroup_stream`] once the header has been
457 /// read, which is the only point at which both the track and the group are
458 /// known.
459 fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
460 self.tracking = Some((objects, group));
461 }
462
463 /// The bytes of the control frame the last capturing read refused.
464 pub(crate) fn take_refused(&mut self) -> Option<Vec<u8>> {
465 self.refused.take()
466 }
467
468 /// Keep the first `len` buffered bytes as a refused frame when the read is
469 /// capturing, and hand `err` back.
470 fn refuse(&mut self, capture_raw: bool, len: usize, err: ConnectionError) -> ConnectionError {
471 if capture_raw {
472 self.refused = Some(self.buf[..len].to_vec());
473 }
474 err
475 }
476
477 /// Record or judge one object this stream carried.
478 ///
479 /// The whole of Section 9.2.1.1's rule that a single header cannot settle: the
480 /// object's Group ID is the stream's, its Object ID is its own, and what
481 /// they are measured against is everything the track has carried on any
482 /// stream.
483 fn note_subgroup_object(&self, header: &ObjectHeader) -> Result<(), ConnectionError> {
484 let Some((objects, group)) = &self.tracking else { return Ok(()) };
485 let at = ObjectLocation { group: *group, object: header.object_id.into_inner() };
486 objects.note_with_final_object(at, object_role(Some(header.object_status as u64))).map_err(
487 |fault| {
488 ConnectionError::Endpoint(EndpointError::for_track_fault(
489 objects.alias(),
490 at,
491 fault,
492 ))
493 },
494 )
495 }
496
497 /// Get the transport-level stream ID.
498 pub fn stream_id(&self) -> u64 {
499 self.inner.stream_id()
500 }
501
502 /// Read more data from the stream into the internal buffer.
503 async fn fill(&mut self) -> Result<bool, ConnectionError> {
504 let mut tmp = [0u8; 4096];
505 match self.inner.read(&mut tmp).await {
506 Ok(Some(n)) => {
507 self.buf.extend_from_slice(&tmp[..n]);
508 Ok(true)
509 }
510 Ok(None) => Ok(false),
511 Err(e) => Err(ConnectionError::Transport(e)),
512 }
513 }
514
515 /// Ensure at least `n` bytes are available in the buffer.
516 async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
517 while self.buf.len() < n {
518 if !self.fill().await? {
519 return Err(ConnectionError::UnexpectedEnd);
520 }
521 }
522 Ok(())
523 }
524
525 /// Read a control message from the stream.
526 ///
527 /// When `capture_raw` is true, the returned tuple includes a clone of the
528 /// framed wire bytes (for observer emission). When false, the second
529 /// element is `None` and the payload clone is skipped.
530 pub async fn read_control(
531 &mut self,
532 capture_raw: bool,
533 ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
534 self.refused = None;
535 // Read type ID varint
536 self.ensure(1).await?;
537 let type_len = varint_len(self.buf[0]);
538 self.ensure(type_len).await?;
539
540 let mut cursor = &self.buf[..type_len];
541 if let Err(e) = VarInt::decode(&mut cursor) {
542 return Err(self.refuse(capture_raw, type_len, e.into()));
543 }
544
545 // Draft-12 uses a fixed 16-bit length after the type id.
546 let len_field_size = 2;
547 self.ensure(type_len + len_field_size).await?;
548 let payload_len = u16::from_be_bytes([self.buf[type_len], self.buf[type_len + 1]]) as usize;
549
550 // Read full payload
551 let total = type_len + len_field_size + payload_len;
552 self.ensure(total).await?;
553
554 // Capture raw bytes only if requested (observer attached).
555 let raw = capture_raw.then(|| self.buf[..total].to_vec());
556
557 // Now decode the whole message using the draft-12 dispatcher
558 let mut frame = &self.buf[..total];
559 let msg = match AnyControlMessage::decode(DraftVersion::Draft12, &mut frame) {
560 Ok(msg) => msg,
561 Err(e) => return Err(self.refuse(capture_raw, total, e.into())),
562 };
563 self.buf.advance(total);
564 Ok((msg, raw))
565 }
566
567 /// Read a subgroup stream header.
568 pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
569 self.ensure(1).await?;
570 loop {
571 let mut cursor = &self.buf[..];
572 match AnySubgroupHeader::decode_stream(DraftVersion::Draft12, &mut cursor) {
573 Ok(header) => {
574 let consumed = self.buf.len() - cursor.remaining();
575 self.buf.advance(consumed);
576 // Clippy would rather see these two arms as an `if let`, and rustc rejects
577 // that in a single-draft build, where the pattern is irrefutable. Only a
578 // `match` satisfies both.
579 #[allow(clippy::single_match)]
580 match header {
581 AnySubgroupHeader::Draft12(ref d) => {
582 self.subgroup_has_extensions = d.stream_type.has_extensions();
583 self.subgroup_gaps = Some(SubgroupGaps::new(d));
584 }
585 // Only this draft's header sets the flag. With draft 12 the only enabled
586 // draft `AnySubgroupHeader` has a single variant, the arm above is
587 // exhaustive and this one unreachable. Compiled in every configuration with
588 // the lint allowed, rather than gated on a `cfg` naming every other
589 // draft: such a list has to be edited in every draft module whenever a
590 // draft is added, and a copy that omits one leaves this match
591 // non-exhaustive.
592 #[allow(unreachable_patterns)]
593 _ => {}
594 }
595 return Ok(header);
596 }
597 Err(e) if e.is_incomplete() => {
598 if !self.fill().await? {
599 return Err(ConnectionError::UnexpectedEnd);
600 }
601 }
602 Err(e) => return Err(ConnectionError::Codec(e)),
603 }
604 }
605 }
606
607 /// Read a fetch response header.
608 pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
609 self.ensure(1).await?;
610 loop {
611 let mut cursor = &self.buf[..];
612 match AnyFetchHeader::decode_stream(DraftVersion::Draft12, &mut cursor) {
613 Ok(header) => {
614 let consumed = self.buf.len() - cursor.remaining();
615 self.buf.advance(consumed);
616 self.fetch_gaps = moqtap_codec::draft12::data_stream::FetchGaps::new();
617 return Ok(header);
618 }
619 Err(e) if e.is_incomplete() => {
620 if !self.fill().await? {
621 return Err(ConnectionError::UnexpectedEnd);
622 }
623 }
624 Err(e) => return Err(ConnectionError::Codec(e)),
625 }
626 }
627 }
628
629 /// Read the next draft-12 subgroup object (header + payload).
630 ///
631 /// Whether an object carries an extension block is a property of the
632 /// stream's Type, not of the object, so this reads it from the header
633 /// [`FramedRecvStream::read_subgroup_header`] recorded rather than assuming
634 /// either answer. Assuming "no extensions" is not a conservative default: on
635 /// a stream whose Type announces them the Extension Headers Length is read
636 /// as the Object Payload Length, and every object on the stream comes back
637 /// wrong instead of being refused.
638 ///
639 /// It is also what puts the rule binding extension headers to Object Status
640 /// within reach on a subgroup stream. A reader that never reads the block
641 /// cannot notice that a non-existent Object carried one.
642 ///
643 /// A Prior Group ID Gap larger than the stream's Group ID, or gaps that
644 /// leave no value common to every Object on the stream that carried one,
645 /// are returned as [`CodecError::MalformedTrack`] (draft-12 Section 10.1);
646 /// [`Connection::withdraw_for_subgroup_stream`] withdraws from the track.
647 pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
648 loop {
649 let mut cursor = &self.buf[..];
650 match ObjectHeader::decode_with_extensions(self.subgroup_has_extensions, &mut cursor) {
651 Ok(header) => {
652 let header_consumed = self.buf.len() - cursor.remaining();
653 let payload_len = header.payload_length.into_inner() as usize;
654 let total = header_consumed + payload_len;
655 if self.buf.len() < total {
656 if !self.fill().await? {
657 return Err(ConnectionError::UnexpectedEnd);
658 }
659 continue;
660 }
661 if let Some(gaps) = &mut self.subgroup_gaps {
662 gaps.check(&header).map_err(ConnectionError::Codec)?;
663 }
664 let payload = self.buf[header_consumed..total].to_vec();
665 self.buf.advance(total);
666 self.note_subgroup_object(&header)?;
667 return Ok(SubgroupObject { header, payload });
668 }
669 Err(e) if e.is_incomplete() => {
670 if !self.fill().await? {
671 return Err(ConnectionError::UnexpectedEnd);
672 }
673 }
674 Err(e) => return Err(ConnectionError::Codec(e)),
675 }
676 }
677 }
678
679 /// Read the next draft-12 fetch object (header + payload).
680 ///
681 /// A Prior Group ID Gap fault the stream shows (draft-12 Section 10.1) is
682 /// returned as [`CodecError::MalformedTrack`], and an extension block that
683 /// does not parse as [`CodecError::MalformedObjectProperties`].
684 /// [`Connection::withdraw_for_fetch_stream`] withdraws from the track over the
685 /// first; this draft states no Malformed Track condition for the second.
686 pub async fn read_fetch_object(&mut self) -> Result<FetchObject, ConnectionError> {
687 loop {
688 let mut cursor = &self.buf[..];
689 match FetchObjectHeader::decode(&mut cursor) {
690 Ok(header) => {
691 let header_consumed = self.buf.len() - cursor.remaining();
692 let payload_len = header.payload_length.into_inner() as usize;
693 let total = header_consumed + payload_len;
694 if self.buf.len() < total {
695 if !self.fill().await? {
696 return Err(ConnectionError::UnexpectedEnd);
697 }
698 continue;
699 }
700 let payload = self.buf[header_consumed..total].to_vec();
701 self.buf.advance(total);
702 self.fetch_gaps
703 .check(
704 header.group_id.into_inner(),
705 header.object_id.into_inner(),
706 &header.extensions,
707 )
708 .map_err(ConnectionError::Codec)?;
709 return Ok(FetchObject { header, payload });
710 }
711 Err(e) if e.is_incomplete() => {
712 if !self.fill().await? {
713 return Err(ConnectionError::UnexpectedEnd);
714 }
715 }
716 Err(e) => return Err(ConnectionError::Codec(e)),
717 }
718 }
719 }
720}
721
722/// A live draft-12 MoQT connection over QUIC or WebTransport.
723pub struct Connection {
724 transport: Transport,
725 endpoint: Endpoint,
726 /// Behind a lock because a control message is written from two kinds of
727 /// place. Most of them are the caller's own request, made through `&mut
728 /// self`. The UNSUBSCRIBE that answers a Malformed Track is not: the
729 /// conditions that make a track malformed are detected on the data plane,
730 /// where this connection is reached through a shared reference. The lock
731 /// also makes one message the unit of writing, so two of them cannot
732 /// interleave on the stream.
733 control_send: Option<tokio::sync::Mutex<FramedSendStream>>,
734 control_recv: Option<FramedRecvStream>,
735 observer: Option<Box<dyn ConnectionObserver>>,
736 /// Setup events buffered during `connect()` and replayed when an
737 /// observer attaches via `set_observer` — without this, an observer
738 /// attached after `connect` returns would never see the handshake.
739 pending_events: Vec<ClientEvent>,
740 /// The server's half of the setup handshake, kept whole.
741 ///
742 /// The endpoint acts on the parameters it recognises and retains none of
743 /// them, and which parameters a server sends — in what order, with what
744 /// values — is the sharpest thing a session says about the implementation
745 /// behind it.
746 server_setup: AnyControlMessage,
747 /// The framed wire bytes of [`Self::server_setup`].
748 server_setup_raw: Option<Vec<u8>>,
749}
750
751impl Connection {
752 /// Connect to a draft-12 MoQT server as a client.
753 pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
754 // PATH is for native QUIC only, and the transport is known here and
755 // nowhere further in. Refusing before dialling means a session that
756 // the server would close on sight is never opened.
757 setup::validate_client_path_transport(
758 &config.setup_parameters,
759 matches!(config.transport, TransportType::WebTransport { .. }),
760 )
761 .map_err(EndpointError::from)?;
762
763 let transport = match &config.transport {
764 TransportType::Quic => Self::connect_quic(addr, &config).await?,
765 TransportType::WebTransport { url } => {
766 let url = url.clone();
767 Self::connect_webtransport(&url, &config).await?
768 }
769 };
770
771 Self::adopt(transport, config).await
772 }
773
774 /// Run the MoQT setup handshake over a transport somebody else established.
775 ///
776 /// For choosing the draft from what the server selected: dial once through
777 /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
778 /// connection to the module its answer names. [`Self::connect`] cannot do
779 /// this — it derives its single ALPN from the draft it was given.
780 ///
781 /// `config.draft` must match this module. The transport is adopted as
782 /// given; nothing here re-checks the ALPN it was negotiated with.
783 pub async fn adopt(
784 transport: Transport,
785 config: ClientConfig,
786 ) -> Result<Self, ConnectionError> {
787 Self::adopt_offering(transport, config, None).await
788 }
789
790 /// [`Self::adopt`], offering exactly `versions` in CLIENT_SETUP.
791 ///
792 /// `None` offers what `config` implies, which is what [`Self::adopt`]
793 /// passes. `Some` replaces the list outright, and takes raw varints rather
794 /// than [`DraftVersion`]s because the reason to reach for this is to offer
795 /// a version no draft assigns — which an enum of drafts cannot name.
796 ///
797 /// A server MUST answer with a version the client offered and MUST
798 /// otherwise close the session; from draft-11 the code for that is
799 /// `VERSION_NEGOTIATION_FAILED` (0x15). How a relay spells the refusal is
800 /// a conformance measurement, and offering a version deliberately outside
801 /// the negotiable set is the only way to ask for it.
802 pub async fn adopt_offering(
803 transport: Transport,
804 config: ClientConfig,
805 versions: Option<Vec<VarInt>>,
806 ) -> Result<Self, ConnectionError> {
807 Self::adopt_with(transport, config, versions, None).await
808 }
809
810 /// [`Self::adopt`] with `observer` attached before the handshake.
811 ///
812 /// The SETUP this side sends is reported as it is written, and a SETUP from
813 /// the peer that this side refuses as [`ClientEvent::ControlMessageRefused`].
814 /// [`Self::set_observer`] after `adopt` sees the same handshake events, but
815 /// only once the handshake has succeeded.
816 pub async fn adopt_observed(
817 transport: Transport,
818 config: ClientConfig,
819 observer: Box<dyn ConnectionObserver>,
820 ) -> Result<Self, ConnectionError> {
821 Self::adopt_with(transport, config, None, Some(observer)).await
822 }
823
824 /// [`Self::adopt_offering`] with `observer` attached before the handshake,
825 /// as [`Self::adopt_observed`].
826 pub async fn adopt_offering_observed(
827 transport: Transport,
828 config: ClientConfig,
829 versions: Option<Vec<VarInt>>,
830 observer: Box<dyn ConnectionObserver>,
831 ) -> Result<Self, ConnectionError> {
832 Self::adopt_with(transport, config, versions, Some(observer)).await
833 }
834
835 /// [`Self::adopt_offering`], reporting to `observer` from the first byte
836 /// written.
837 pub(crate) async fn adopt_with(
838 transport: Transport,
839 config: ClientConfig,
840 versions: Option<Vec<VarInt>>,
841 observer: Option<Box<dyn ConnectionObserver>>,
842 ) -> Result<Self, ConnectionError> {
843 // PATH is for native QUIC only, and the transport is known here and
844 // nowhere further in. Refusing before dialling means a session that
845 // the server would close on sight is never opened.
846 setup::validate_client_path_transport(
847 &config.setup_parameters,
848 matches!(config.transport, TransportType::WebTransport { .. }),
849 )
850 .map_err(EndpointError::from)?;
851
852 // Open bidirectional control stream
853 let (send, recv) = transport.open_bi().await?;
854 let mut control_send = FramedSendStream::new(send);
855 let mut control_recv = FramedRecvStream::new(recv);
856
857 // Perform setup handshake
858 let mut endpoint = Endpoint::new(Role::Client);
859 endpoint.connect()?;
860 let setup_msg = endpoint.send_client_setup(
861 versions.unwrap_or_else(|| config.supported_versions()),
862 config.setup_parameters.clone(),
863 )?;
864 let any_setup = AnyControlMessage::Draft12(setup_msg);
865 let raw_setup = control_send.write_control(&any_setup).await?;
866 // Reported as written, so a peer that never answers still leaves it
867 // observed.
868 if let Some(obs) = &observer {
869 obs.on_event_owned(ClientEvent::ControlMessage {
870 direction: Direction::Send,
871 message: any_setup.clone(),
872 raw: Some(raw_setup.clone()),
873 });
874 }
875
876 let (server_setup, raw_server_setup) = match control_recv.read_control(true).await {
877 Ok(read) => read,
878 Err(e) => {
879 if let Some(obs) = &observer {
880 if let Some(event) = Self::refused_event(&e, control_recv.take_refused()) {
881 obs.on_event_owned(event);
882 }
883 }
884 return Err(e);
885 }
886 };
887 match &server_setup {
888 AnyControlMessage::Draft12(ControlMessage::ServerSetup(ref ss)) => {
889 if let Err(e) = endpoint.receive_server_setup(ss) {
890 let err = ConnectionError::from(e);
891 Self::refuse_setup(observer.as_deref(), &err, &server_setup, &raw_server_setup);
892 return Err(err);
893 }
894 }
895 _ => {
896 return Err(ConnectionError::Endpoint(EndpointError::NotActive));
897 }
898 }
899
900 let mut pending_events = Vec::with_capacity(3);
901 pending_events.push(ClientEvent::ControlMessage {
902 direction: Direction::Send,
903 message: any_setup,
904 raw: Some(raw_setup),
905 });
906 pending_events.push(ClientEvent::ControlMessage {
907 direction: Direction::Receive,
908 message: server_setup.clone(),
909 raw: raw_server_setup.clone(),
910 });
911 if let Some(v) = endpoint.negotiated_version() {
912 pending_events.push(ClientEvent::SetupComplete { negotiated_version: v.into_inner() });
913 }
914
915 if let Some(obs) = &observer {
916 // The SETUP this side sent was reported as it was written.
917 for event in pending_events.drain(..).skip(1) {
918 obs.on_event_owned(event);
919 }
920 }
921
922 Ok(Self {
923 transport,
924 endpoint,
925 control_send: Some(tokio::sync::Mutex::new(control_send)),
926 control_recv: Some(control_recv),
927 observer,
928 pending_events,
929 server_setup,
930 server_setup_raw: raw_server_setup,
931 })
932 }
933
934 /// Establish a raw QUIC connection.
935 ///
936 /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
937 /// TLS and endpoint setup.
938 async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
939 let (transport, _negotiated) = crate::transport::dial_quic(
940 addr,
941 &crate::transport::QuicDialOptions {
942 skip_cert_verification: config.skip_cert_verification,
943 ca_certs: config.ca_certs.clone(),
944 ..crate::transport::QuicDialOptions::new(config.alpn())
945 },
946 )
947 .await?;
948 Ok(transport)
949 }
950
951 /// Establish a WebTransport connection.
952 ///
953 /// [`crate::transport::dial_webtransport`] holds the TLS and endpoint
954 /// setup, exactly as `connect_quic` above defers its own. That is not
955 /// only deduplication: both dials hand the same `QuicDialOptions` to the
956 /// same config constructor in `transport::quic`, so the bundled roots and
957 /// `config.ca_certs` are what each of them trusts and one relay gets one
958 /// verdict whichever transport carries it. Settling trust at this call site
959 /// instead — from `wtransport`'s own builder settings, or from a second
960 /// config of this draft's own — puts the decision in two places, and a
961 /// caller's private CA then reaches only the dials whose call site
962 /// installed it.
963 #[cfg(feature = "webtransport")]
964 async fn connect_webtransport(
965 url: &str,
966 config: &ClientConfig,
967 ) -> Result<Transport, ConnectionError> {
968 Ok(crate::transport::dial_webtransport(
969 url,
970 &crate::transport::QuicDialOptions {
971 skip_cert_verification: config.skip_cert_verification,
972 ca_certs: config.ca_certs.clone(),
973 ..crate::transport::QuicDialOptions::new(config.alpn())
974 },
975 )
976 .await?)
977 }
978
979 /// Stub for when the webtransport feature is not enabled.
980 #[cfg(not(feature = "webtransport"))]
981 async fn connect_webtransport(
982 _url: &str,
983 _config: &ClientConfig,
984 ) -> Result<Transport, ConnectionError> {
985 Err(ConnectionError::Transport(TransportError::Connect(
986 "webtransport feature not enabled".into(),
987 )))
988 }
989
990 // ── Observer ───────────────────────────────────────────────
991
992 /// Attach an observer. Buffered handshake events from `connect()` are
993 /// flushed in arrival order before this returns.
994 pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
995 self.observer = Some(observer);
996 for event in self.pending_events.drain(..) {
997 if let Some(ref obs) = self.observer {
998 obs.on_event_owned(event);
999 }
1000 }
1001 }
1002
1003 /// Remove the observer.
1004 pub fn clear_observer(&mut self) {
1005 self.observer = None;
1006 }
1007
1008 /// How the QUIC connection was closed, once it has been. See
1009 /// [`TransportClose`](crate::transport::TransportClose).
1010 pub fn transport_close(&self) -> Option<crate::transport::TransportClose> {
1011 self.transport.transport_close()
1012 }
1013
1014 /// Emit an event to the observer, if one is attached.
1015 fn emit(&self, event: ClientEvent) {
1016 if let Some(ref obs) = self.observer {
1017 obs.on_event_owned(event);
1018 }
1019 }
1020
1021 /// Report a received frame this endpoint refused, when an observer is
1022 /// attached and the refusal is the peer's: see [`Self::refused_event`].
1023 fn emit_refused(&self, err: &ConnectionError, raw: Option<Vec<u8>>) {
1024 if self.observer.is_some() {
1025 if let Some(event) = Self::refused_event(err, raw) {
1026 self.emit(event);
1027 }
1028 }
1029 }
1030
1031 /// The [`ClientEvent::ControlMessageRefused`] for `raw`, when `err` refuses
1032 /// what the peer sent: the decoder refused it, or it broke a rule
1033 /// [`EndpointError::fault`] or [`Self::draft_specific_cause`] lays on the
1034 /// peer.
1035 fn refused_event(err: &ConnectionError, raw: Option<Vec<u8>>) -> Option<ClientEvent> {
1036 use crate::above_codec_rules::{DraftSpecificCause, EndpointFault};
1037 let peers = match err {
1038 ConnectionError::Codec(_) | ConnectionError::VarInt(_) => true,
1039 ConnectionError::Endpoint(e) => matches!(e.fault(), EndpointFault::Peer(_)),
1040 other => matches!(
1041 Self::draft_specific_cause(other),
1042 Some(DraftSpecificCause::PeerViolation { .. } | DraftSpecificCause::Codec { .. })
1043 ),
1044 };
1045 let raw = raw.filter(|_| peers)?;
1046 Some(ClientEvent::ControlMessageRefused { raw })
1047 }
1048
1049 /// Report a peer's SETUP the handshake refused to an observer attached
1050 /// before it: the message as received, then the refusal.
1051 fn refuse_setup(
1052 observer: Option<&dyn ConnectionObserver>,
1053 err: &ConnectionError,
1054 setup: &AnyControlMessage,
1055 raw: &Option<Vec<u8>>,
1056 ) {
1057 let Some(obs) = observer else { return };
1058 obs.on_event_owned(ClientEvent::ControlMessage {
1059 direction: Direction::Receive,
1060 message: setup.clone(),
1061 raw: raw.clone(),
1062 });
1063 if let Some(event) = Self::refused_event(err, raw.clone()) {
1064 obs.on_event_owned(event);
1065 }
1066 }
1067
1068 // ── Control message I/O ─────────────────────────────────
1069
1070 /// Send a control message on the control stream.
1071 pub async fn send_control(&self, msg: &ControlMessage) -> Result<(), ConnectionError> {
1072 let any = AnyControlMessage::Draft12(msg.clone());
1073 let mut send =
1074 self.control_send.as_ref().ok_or(ConnectionError::NoControlStream)?.lock().await;
1075 let raw = send.write_control(&any).await?;
1076 drop(send);
1077 self.emit(ClientEvent::ControlMessage {
1078 direction: Direction::Send,
1079 message: any,
1080 raw: Some(raw),
1081 });
1082 Ok(())
1083 }
1084
1085 /// Read the next control message from the control stream.
1086 pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
1087 self.recv_control_raw().await.map(|(msg, _)| msg)
1088 }
1089
1090 /// [`Self::recv_control`], keeping the frame's wire bytes when an observer
1091 /// is attached.
1092 async fn recv_control_raw(
1093 &mut self,
1094 ) -> Result<(ControlMessage, Option<Vec<u8>>), ConnectionError> {
1095 let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
1096 let capture_raw = self.observer.is_some();
1097 let (any, raw) = match recv.read_control(capture_raw).await {
1098 Ok(v) => v,
1099 Err(e) => {
1100 let raw = recv.take_refused();
1101 let err = self.close_for_codec(e);
1102 self.emit_refused(&err, raw);
1103 return Err(err);
1104 }
1105 };
1106 if capture_raw {
1107 self.emit(ClientEvent::ControlMessage {
1108 direction: Direction::Receive,
1109 message: any.clone(),
1110 raw: raw.clone(),
1111 });
1112 }
1113 match any {
1114 AnyControlMessage::Draft12(msg) => Ok((msg, raw)),
1115 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 12 the
1116 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
1117 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
1118 // naming the other drafts: such a list has to be edited in every draft
1119 // module whenever a draft is added, and a copy that omits one leaves this match
1120 // non-exhaustive.
1121 #[allow(unreachable_patterns)]
1122 _ => Err(ConnectionError::ControlMessageNarrowing),
1123 }
1124 }
1125
1126 /// Read and dispatch the next incoming control message through the endpoint
1127 /// state machine. Returns the decoded message for inspection.
1128 pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
1129 let (msg, raw) = self.recv_control_raw().await?;
1130 if let Err(e) = self.endpoint.receive_message(msg.clone()) {
1131 let err = self.close_if_session_fatal(e);
1132 self.emit_refused(&err, raw);
1133 return Err(err);
1134 }
1135
1136 if let ControlMessage::GoAway(ref ga) = msg {
1137 self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
1138 }
1139
1140 Ok(msg)
1141 }
1142
1143 // ── Subscribe flow ──────────────────────────────────────
1144
1145 /// Send a SUBSCRIBE and return the allocated request ID.
1146 ///
1147 /// Draft-12: the subscriber no longer chooses a track alias. The alias is
1148 /// returned by the publisher in SUBSCRIBE_OK and can be retrieved later
1149 /// from the endpoint via `endpoint().track_alias_for(request_id)`.
1150 pub async fn subscribe(
1151 &mut self,
1152 track_namespace: TrackNamespace,
1153 track_name: Vec<u8>,
1154 subscriber_priority: u8,
1155 group_order: GroupOrder,
1156 filter_type: VarInt,
1157 parameters: Vec<KeyValuePair>,
1158 ) -> Result<VarInt, ConnectionError> {
1159 let (req_id, msg) = self.endpoint.subscribe(
1160 track_namespace,
1161 track_name,
1162 subscriber_priority,
1163 group_order,
1164 filter_type,
1165 parameters,
1166 )?;
1167 self.send_control(&msg).await?;
1168 Ok(req_id)
1169 }
1170
1171 /// Send a SUBSCRIBE for a range of the track and return the allocated ID.
1172 ///
1173 /// The Filter Type comes from the arguments, so the message cannot name a
1174 /// filter whose fields it does not carry.
1175 #[allow(clippy::too_many_arguments)]
1176 pub async fn subscribe_range(
1177 &mut self,
1178 track_namespace: TrackNamespace,
1179 track_name: Vec<u8>,
1180 subscriber_priority: u8,
1181 group_order: GroupOrder,
1182 start_location: Location,
1183 end_group: Option<VarInt>,
1184 parameters: Vec<KeyValuePair>,
1185 ) -> Result<VarInt, ConnectionError> {
1186 let (req_id, msg) = self.endpoint.subscribe_range(
1187 track_namespace,
1188 track_name,
1189 subscriber_priority,
1190 group_order,
1191 start_location,
1192 end_group,
1193 parameters,
1194 )?;
1195 self.send_control(&msg).await?;
1196 Ok(req_id)
1197 }
1198
1199 /// Send an UNSUBSCRIBE for the given request ID.
1200 pub async fn unsubscribe(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1201 let msg = self.endpoint.unsubscribe(request_id)?;
1202 self.send_control(&msg).await
1203 }
1204
1205 /// Accept a subscription the peer opened, sending SUBSCRIBE_OK and giving
1206 /// its track a Track Alias.
1207 ///
1208 /// The endpoint refuses an alias a live track of its own already holds and
1209 /// refuses a second answer to one SUBSCRIBE, so nothing is written on the
1210 /// wire when it does either.
1211 pub async fn subscribe_ok(
1212 &mut self,
1213 request_id: VarInt,
1214 track_alias: VarInt,
1215 expires: VarInt,
1216 group_order: GroupOrder,
1217 parameters: Vec<KeyValuePair>,
1218 ) -> Result<(), ConnectionError> {
1219 let msg = self.endpoint.send_subscribe_ok(
1220 request_id,
1221 track_alias,
1222 expires,
1223 group_order,
1224 parameters,
1225 )?;
1226 self.send_control(&msg).await
1227 }
1228
1229 /// Reject a subscription the peer opened, sending SUBSCRIBE_ERROR.
1230 ///
1231 /// The endpoint refuses a second answer to one SUBSCRIBE, so nothing is
1232 /// written on the wire when it does.
1233 pub async fn subscribe_error(
1234 &mut self,
1235 request_id: VarInt,
1236 error_code: VarInt,
1237 reason_phrase: Vec<u8>,
1238 ) -> Result<(), ConnectionError> {
1239 let msg = self.endpoint.send_subscribe_error(request_id, error_code, reason_phrase)?;
1240 self.send_control(&msg).await
1241 }
1242
1243 /// End a subscription this endpoint accepted, sending SUBSCRIBE_DONE.
1244 pub async fn subscribe_done(
1245 &mut self,
1246 request_id: VarInt,
1247 status_code: VarInt,
1248 stream_count: VarInt,
1249 reason_phrase: Vec<u8>,
1250 ) -> Result<(), ConnectionError> {
1251 let msg = self.endpoint.send_subscribe_done(
1252 request_id,
1253 status_code,
1254 stream_count,
1255 reason_phrase,
1256 )?;
1257 self.send_control(&msg).await
1258 }
1259
1260 /// Narrow a subscription this endpoint opened, sending SUBSCRIBE_UPDATE.
1261 ///
1262 /// No Request ID is spent: on this draft the message's one identifier
1263 /// field names the subscription it modifies.
1264 #[allow(clippy::too_many_arguments)]
1265 pub async fn subscribe_update(
1266 &mut self,
1267 request_id: VarInt,
1268 start_group: VarInt,
1269 start_object: VarInt,
1270 end_group: VarInt,
1271 subscriber_priority: u8,
1272 forward: Forward,
1273 parameters: Vec<KeyValuePair>,
1274 ) -> Result<(), ConnectionError> {
1275 let msg = self.endpoint.subscribe_update(
1276 request_id,
1277 start_group,
1278 start_object,
1279 end_group,
1280 subscriber_priority,
1281 forward,
1282 parameters,
1283 )?;
1284 self.send_control(&msg).await
1285 }
1286
1287 /// Offer the peer a subscription to a track this endpoint publishes, and
1288 /// return the Request ID the offer was allocated.
1289 ///
1290 /// Nothing is written on the wire when the endpoint refuses to build the
1291 /// offer, which it does when the Track Alias is one another live track of
1292 /// this session already holds.
1293 #[allow(clippy::too_many_arguments)]
1294 pub async fn publish(
1295 &mut self,
1296 track_namespace: TrackNamespace,
1297 track_name: Vec<u8>,
1298 track_alias: VarInt,
1299 group_order: GroupOrder,
1300 largest_location: Option<Location>,
1301 forward: Forward,
1302 parameters: Vec<KeyValuePair>,
1303 ) -> Result<VarInt, ConnectionError> {
1304 let (req_id, msg) = self.endpoint.publish(
1305 track_namespace,
1306 track_name,
1307 track_alias,
1308 group_order,
1309 largest_location,
1310 forward,
1311 parameters,
1312 )?;
1313 self.send_control(&msg).await?;
1314 Ok(req_id)
1315 }
1316
1317 /// Accept a PUBLISH the peer sent, which establishes the subscription it
1318 /// opened.
1319 ///
1320 /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1321 /// written on the wire when it does.
1322 #[allow(clippy::too_many_arguments)]
1323 pub async fn publish_ok(
1324 &mut self,
1325 request_id: VarInt,
1326 forward: Forward,
1327 subscriber_priority: u8,
1328 group_order: GroupOrder,
1329 filter_type: VarInt,
1330 start_group: Option<VarInt>,
1331 start_object: Option<VarInt>,
1332 end_group: Option<VarInt>,
1333 ) -> Result<(), ConnectionError> {
1334 let msg = self.endpoint.send_publish_ok(
1335 request_id,
1336 forward,
1337 subscriber_priority,
1338 group_order,
1339 filter_type,
1340 start_group,
1341 start_object,
1342 end_group,
1343 )?;
1344 self.send_control(&msg).await
1345 }
1346
1347 /// Reject a PUBLISH the peer sent, which ends the subscription it opened
1348 /// before it was established.
1349 ///
1350 /// The endpoint refuses a second answer to one PUBLISH, so nothing is
1351 /// written on the wire when it does.
1352 pub async fn publish_error(
1353 &mut self,
1354 request_id: VarInt,
1355 error_code: VarInt,
1356 reason_phrase: Vec<u8>,
1357 ) -> Result<(), ConnectionError> {
1358 let msg = self.endpoint.send_publish_error(request_id, error_code, reason_phrase)?;
1359 self.send_control(&msg).await
1360 }
1361
1362 // ── Fetch flow ──────────────────────────────────────────
1363
1364 /// Send a FETCH and return the allocated request ID.
1365 #[allow(clippy::too_many_arguments)]
1366 pub async fn fetch(
1367 &mut self,
1368 track_namespace: TrackNamespace,
1369 track_name: Vec<u8>,
1370 subscriber_priority: u8,
1371 group_order: GroupOrder,
1372 start_group: VarInt,
1373 start_object: VarInt,
1374 end_group: VarInt,
1375 end_object: VarInt,
1376 parameters: Vec<KeyValuePair>,
1377 ) -> Result<VarInt, ConnectionError> {
1378 let (req_id, msg) = self.endpoint.fetch(
1379 track_namespace,
1380 track_name,
1381 subscriber_priority,
1382 group_order,
1383 start_group,
1384 start_object,
1385 end_group,
1386 end_object,
1387 parameters,
1388 )?;
1389 self.send_control(&msg).await?;
1390 Ok(req_id)
1391 }
1392
1393 /// Send a Relative Joining Fetch and return the allocated request ID.
1394 ///
1395 /// `joining_start` counts groups back from the live edge of the
1396 /// subscription it names. Section 8.16.1 computes the range from "the
1397 /// Preceding Group Offset", which is this field under the name it carried
1398 /// two drafts ago; the field list calls it Joining Start and says that for
1399 /// a Relative Joining Fetch "a value of 0 indicates the Fetch starts at
1400 /// the beginning of the Current Group". For the form that names the group
1401 /// outright, see
1402 /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
1403 pub async fn joining_fetch(
1404 &mut self,
1405 subscriber_priority: u8,
1406 group_order: GroupOrder,
1407 joining_request_id: VarInt,
1408 joining_start: VarInt,
1409 parameters: Vec<KeyValuePair>,
1410 ) -> Result<VarInt, ConnectionError> {
1411 let (req_id, msg) = self.endpoint.joining_fetch(
1412 subscriber_priority,
1413 group_order,
1414 joining_request_id,
1415 joining_start,
1416 parameters,
1417 )?;
1418 self.send_control(&msg).await?;
1419 Ok(req_id)
1420 }
1421
1422 /// Send an Absolute Joining Fetch and return the allocated request ID.
1423 ///
1424 /// `joining_start` is the group to begin at. Section 8.16 has an Absolute
1425 /// Joining Fetch "Identical to a Relative Joining Fetch except that the
1426 /// Start Group is determined by an absolute Group value rather than a
1427 /// relative offset to the subscription", so a subscriber that knows the
1428 /// group it wants can ask for it without first being told a Largest
1429 /// Location to count back from.
1430 ///
1431 /// Two calls rather than one taking a Fetch Type, because these are the
1432 /// only two the joining form admits. The endpoint is named the same way,
1433 /// so no call between an application and the wire has a Fetch Type in it
1434 /// to be given a wrong one.
1435 pub async fn absolute_joining_fetch(
1436 &mut self,
1437 subscriber_priority: u8,
1438 group_order: GroupOrder,
1439 joining_request_id: VarInt,
1440 joining_start: VarInt,
1441 parameters: Vec<KeyValuePair>,
1442 ) -> Result<VarInt, ConnectionError> {
1443 let (req_id, msg) = self.endpoint.absolute_joining_fetch(
1444 subscriber_priority,
1445 group_order,
1446 joining_request_id,
1447 joining_start,
1448 parameters,
1449 )?;
1450 self.send_control(&msg).await?;
1451 Ok(req_id)
1452 }
1453
1454 /// Send a FETCH_CANCEL for the given request ID.
1455 pub async fn fetch_cancel(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1456 let msg = self.endpoint.fetch_cancel(request_id)?;
1457 self.send_control(&msg).await
1458 }
1459
1460 /// Accept a fetch the peer opened, sending FETCH_OK.
1461 ///
1462 /// The endpoint refuses a Joining Fetch naming a subscription this session
1463 /// cannot join and refuses a second answer to one FETCH, so nothing is
1464 /// written on the wire when it does either.
1465 pub async fn fetch_ok(
1466 &mut self,
1467 request_id: VarInt,
1468 group_order: GroupOrder,
1469 end_of_track: u8,
1470 end_location: Location,
1471 parameters: Vec<KeyValuePair>,
1472 ) -> Result<(), ConnectionError> {
1473 let msg = self.endpoint.send_fetch_ok(
1474 request_id,
1475 group_order,
1476 end_of_track,
1477 end_location,
1478 parameters,
1479 )?;
1480 self.send_control(&msg).await
1481 }
1482
1483 /// Refuse a fetch the peer opened, sending FETCH_ERROR.
1484 ///
1485 /// The endpoint refuses a second answer to one FETCH, and refuses a
1486 /// Joining Fetch's refusal under any code but the one the draft names for
1487 /// it, so nothing is written on the wire when it does either.
1488 pub async fn fetch_error(
1489 &mut self,
1490 request_id: VarInt,
1491 error_code: VarInt,
1492 reason_phrase: Vec<u8>,
1493 ) -> Result<(), ConnectionError> {
1494 let msg = self.endpoint.send_fetch_error(request_id, error_code, reason_phrase)?;
1495 self.send_control(&msg).await
1496 }
1497
1498 // ── Namespace flows ─────────────────────────────────────
1499
1500 /// Send a SUBSCRIBE_ANNOUNCES. Returns the allocated request ID.
1501 pub async fn subscribe_announces(
1502 &mut self,
1503 track_namespace_prefix: TrackNamespace,
1504 parameters: Vec<KeyValuePair>,
1505 ) -> Result<VarInt, ConnectionError> {
1506 let (req_id, msg) =
1507 self.endpoint.subscribe_announces(track_namespace_prefix, parameters)?;
1508 self.send_control(&msg).await?;
1509 Ok(req_id)
1510 }
1511
1512 /// Accept a namespace subscription the peer made, sending SUBSCRIBE_ANNOUNCES_OK.
1513 ///
1514 /// The endpoint refuses a second answer to one SUBSCRIBE_ANNOUNCES, so nothing is
1515 /// written on the wire when it does.
1516 pub async fn subscribe_announces_ok(
1517 &mut self,
1518 request_id: VarInt,
1519 ) -> Result<(), ConnectionError> {
1520 let msg = self.endpoint.send_subscribe_announces_ok(request_id)?;
1521 self.send_control(&msg).await
1522 }
1523
1524 /// Refuse a namespace subscription the peer made, sending SUBSCRIBE_ANNOUNCES_ERROR.
1525 ///
1526 /// The other half of the same sentence: one answer, and this is the other
1527 /// one it can be.
1528 pub async fn subscribe_announces_error(
1529 &mut self,
1530 request_id: VarInt,
1531 error_code: VarInt,
1532 reason_phrase: Vec<u8>,
1533 ) -> Result<(), ConnectionError> {
1534 let msg =
1535 self.endpoint.send_subscribe_announces_error(request_id, error_code, reason_phrase)?;
1536 self.send_control(&msg).await
1537 }
1538
1539 /// Send an ANNOUNCE. Returns the allocated request ID.
1540 pub async fn announce(
1541 &mut self,
1542 track_namespace: TrackNamespace,
1543 parameters: Vec<KeyValuePair>,
1544 ) -> Result<VarInt, ConnectionError> {
1545 let (req_id, msg) = self.endpoint.announce(track_namespace, parameters)?;
1546 self.send_control(&msg).await?;
1547 Ok(req_id)
1548 }
1549
1550 /// Send an UNANNOUNCE.
1551 pub async fn unannounce(
1552 &mut self,
1553 track_namespace: TrackNamespace,
1554 ) -> Result<(), ConnectionError> {
1555 let msg = self.endpoint.unannounce(track_namespace)?;
1556 self.send_control(&msg).await
1557 }
1558
1559 /// Accept an announcement the peer made, sending ANNOUNCE_OK.
1560 ///
1561 /// The endpoint refuses a second answer to one ANNOUNCE, so nothing is
1562 /// written on the wire when it does.
1563 pub async fn announce_ok(&mut self, request_id: VarInt) -> Result<(), ConnectionError> {
1564 let msg = self.endpoint.send_announce_ok(request_id)?;
1565 self.send_control(&msg).await
1566 }
1567
1568 /// Refuse an announcement the peer made, sending ANNOUNCE_ERROR.
1569 ///
1570 /// The other half of the same sentence: one answer, and this is the other
1571 /// one it can be.
1572 pub async fn announce_error(
1573 &mut self,
1574 request_id: VarInt,
1575 error_code: VarInt,
1576 reason_phrase: Vec<u8>,
1577 ) -> Result<(), ConnectionError> {
1578 let msg = self.endpoint.send_announce_error(request_id, error_code, reason_phrase)?;
1579 self.send_control(&msg).await
1580 }
1581
1582 /// Revoke an acceptance, sending ANNOUNCE_CANCEL.
1583 ///
1584 /// The endpoint refuses one for an announcement it never accepted, so
1585 /// nothing is written on the wire when it does.
1586 pub async fn announce_cancel(
1587 &mut self,
1588 track_namespace: TrackNamespace,
1589 error_code: VarInt,
1590 reason_phrase: Vec<u8>,
1591 ) -> Result<(), ConnectionError> {
1592 let msg = self.endpoint.announce_cancel(track_namespace, error_code, reason_phrase)?;
1593 self.send_control(&msg).await
1594 }
1595 // ── Track Status flow ────────────────────────────────────
1596
1597 /// Send a TRACK_STATUS_REQUEST. Returns the allocated request ID.
1598 pub async fn track_status_request(
1599 &mut self,
1600 track_namespace: TrackNamespace,
1601 track_name: Vec<u8>,
1602 parameters: Vec<KeyValuePair>,
1603 ) -> Result<VarInt, ConnectionError> {
1604 let (req_id, msg) =
1605 self.endpoint.track_status_request(track_namespace, track_name, parameters)?;
1606 self.send_control(&msg).await?;
1607 Ok(req_id)
1608 }
1609
1610 /// Answer a TRACK_STATUS_REQUEST the peer sent, sending TRACK_STATUS.
1611 ///
1612 /// The endpoint refuses a second answer to one request, so nothing is
1613 /// written on the wire when it does.
1614 pub async fn track_status(
1615 &mut self,
1616 request_id: VarInt,
1617 status_code: VarInt,
1618 largest_location: Location,
1619 parameters: Vec<KeyValuePair>,
1620 ) -> Result<(), ConnectionError> {
1621 let msg = self.endpoint.send_track_status(
1622 request_id,
1623 status_code,
1624 largest_location,
1625 parameters,
1626 )?;
1627 self.send_control(&msg).await
1628 }
1629
1630 // ── Malformed Tracks ────────────────────────────────────
1631
1632 /// Answer Section 2.5 for the track `alias` names: withdraw from it, and
1633 /// send the UNSUBSCRIBE that withdrawal is.
1634 ///
1635 /// "When a subscriber detects a Malformed Track, it MUST UNSUBSCRIBE from
1636 /// the Track and SHOULD deliver an error to the application." The caller
1637 /// delivers the error by returning it; this is the other half, and the
1638 /// caller does not have to remember to ask for it.
1639 ///
1640 /// Nothing here ends the session, and that is the point of it. The
1641 /// conditions in Section 2.5 are answered by giving up one track.
1642 ///
1643 /// # Why a write that fails is not reported
1644 ///
1645 /// The error the caller is about to receive is the one that says what
1646 /// went wrong with the track. A control stream that will not take an
1647 /// UNSUBSCRIBE is a session on its way out for a reason of its own, and
1648 /// replacing the condition's report with a transport error would lose the
1649 /// only account of why the track was withdrawn. The rest of the
1650 /// withdrawal is abandoned, because a stream that refused one message
1651 /// will refuse the next.
1652 async fn withdraw_malformed_track(&self, alias: u64, condition: MalformedTrackCondition) {
1653 for msg in self.endpoint.withdraw_malformed_track(alias, condition) {
1654 if self.send_control(&msg).await.is_err() {
1655 break;
1656 }
1657 }
1658 }
1659
1660 /// [`Self::withdraw_malformed_track`] for the track the FETCH `request_id`
1661 /// asked for.
1662 async fn withdraw_malformed_fetch_track(
1663 &self,
1664 request_id: u64,
1665 condition: MalformedTrackCondition,
1666 ) {
1667 for msg in self.endpoint.withdraw_malformed_fetch_track(request_id, condition) {
1668 if self.send_control(&msg).await.is_err() {
1669 break;
1670 }
1671 }
1672 }
1673
1674 /// Record the framing an arriving object was sent with, and withdraw from
1675 /// the track when it is the second framing that track has been sent.
1676 ///
1677 /// The receiving half of a pair. The two writing paths call the endpoint
1678 /// directly and answer a mixed track by refusing to write it, because
1679 /// Section 2.5's sentence is a subscriber's: an endpoint about to send an
1680 /// object is that object's Original Publisher, and a publisher has no
1681 /// subscription of its own to withdraw.
1682 async fn note_received_framing(
1683 &self,
1684 alias: u64,
1685 seen: ObjectForwardingPreference,
1686 ) -> Result<(), ConnectionError> {
1687 let Err(err) = self.endpoint.note_object_forwarding_preference(alias, seen) else {
1688 return Ok(());
1689 };
1690 self.withdraw_malformed_track(alias, MalformedTrackCondition::MixedForwardingPreference)
1691 .await;
1692 Err(err.into())
1693 }
1694
1695 // ── Data streams ────────────────────────────────────────
1696
1697 /// Open a new unidirectional stream for sending subgroup data.
1698 pub async fn open_subgroup_stream(
1699 &self,
1700 header: &AnySubgroupHeader,
1701 ) -> Result<FramedSendStream, ConnectionError> {
1702 // Before the stream is opened: the Original Publisher is who the rule
1703 // binds, so a header that would mix this track's framing is refused
1704 // here rather than written and answered by the peer.
1705 self.endpoint.note_object_forwarding_preference(
1706 header.track_alias(),
1707 ObjectForwardingPreference::Subgroup,
1708 )?;
1709 let send = self.transport.open_uni().await?;
1710 let mut framed = FramedSendStream::new(send);
1711 let sid = framed.stream_id();
1712 framed.write_subgroup_header(header).await?;
1713 self.emit(ClientEvent::StreamOpened {
1714 direction: Direction::Send,
1715 stream_kind: StreamKind::Subgroup,
1716 stream_id: sid,
1717 });
1718 self.emit(ClientEvent::DataStreamHeader {
1719 stream_id: sid,
1720 direction: Direction::Send,
1721 header: header.clone(),
1722 });
1723 Ok(framed)
1724 }
1725
1726 /// Open a new unidirectional stream for sending a FETCH's objects.
1727 ///
1728 /// The objects answering a FETCH do not go on the request's own stream:
1729 /// they go on a unidirectional stream of their own, which opens with a
1730 /// FETCH_HEADER naming the request they belong to. This writes that header
1731 /// and hands back the stream, the same way
1732 /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
1733 /// subgroup.
1734 ///
1735 /// The caller owns the stream that comes back. Nothing here remembers
1736 /// which request it belongs to, so an endpoint serving several fetches at
1737 /// once keeps its own map from Request ID to stream.
1738 pub async fn open_fetch_stream(
1739 &self,
1740 header: &AnyFetchHeader,
1741 ) -> Result<FramedSendStream, ConnectionError> {
1742 let send = self.transport.open_uni().await?;
1743 let mut framed = FramedSendStream::new(send);
1744 let sid = framed.stream_id();
1745 framed.write_fetch_header(header).await?;
1746 self.emit(ClientEvent::StreamOpened {
1747 direction: Direction::Send,
1748 stream_kind: StreamKind::Fetch,
1749 stream_id: sid,
1750 });
1751 Ok(framed)
1752 }
1753
1754 /// Accept the next unidirectional stream and read its fetch header.
1755 ///
1756 /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
1757 /// are separate because the header decides how every object after it is
1758 /// framed, so a caller has to know which it is expecting before the first
1759 /// byte is read.
1760 ///
1761 /// Objects come off the returned stream with
1762 /// [`FramedRecvStream::read_fetch_object`].
1763 pub async fn accept_fetch_stream(
1764 &self,
1765 ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
1766 let recv = self.transport.accept_uni().await?;
1767 let mut framed = FramedRecvStream::new(recv);
1768 let sid = framed.stream_id();
1769 let header = framed.read_fetch_header().await?;
1770 self.emit(ClientEvent::StreamOpened {
1771 direction: Direction::Receive,
1772 stream_kind: StreamKind::Fetch,
1773 stream_id: sid,
1774 });
1775 self.emit(ClientEvent::FetchStreamHeader {
1776 stream_id: sid,
1777 direction: Direction::Receive,
1778 header: header.clone(),
1779 });
1780 // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
1781 // carries an `AnySubgroupHeader` and cannot express one. What
1782 // `accept_subgroup_stream` does beyond this - the forwarding-preference
1783 // note, the object measurement - is about a subgroup and has no
1784 // counterpart on a fetch stream.
1785 Ok((header, framed))
1786 }
1787
1788 /// Accept an incoming unidirectional data stream and read its subgroup
1789 /// header.
1790 pub async fn accept_subgroup_stream(
1791 &self,
1792 ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
1793 let recv = self.transport.accept_uni().await?;
1794 let mut framed = FramedRecvStream::new(recv);
1795 let sid = framed.stream_id();
1796 let header = framed.read_subgroup_header().await?;
1797 self.emit(ClientEvent::StreamOpened {
1798 direction: Direction::Receive,
1799 stream_kind: StreamKind::Subgroup,
1800 stream_id: sid,
1801 });
1802 self.emit(ClientEvent::DataStreamHeader {
1803 stream_id: sid,
1804 direction: Direction::Receive,
1805 header: header.clone(),
1806 });
1807 // Every object on a subgroup stream has the Subgroup preference, so
1808 // the header settles the track's framing before a single object is
1809 // read.
1810 self.note_received_framing(header.track_alias(), ObjectForwardingPreference::Subgroup)
1811 .await?;
1812 // The track is resolved here and not inside the stream: it takes the
1813 // endpoint's alias table, which a stream handle has no way back to.
1814 if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
1815 framed.measure_objects_against(objects, header.group_id());
1816 }
1817 Ok((header, framed))
1818 }
1819
1820 /// Send an object via datagram.
1821 ///
1822 /// The header goes through `AnyDatagramHeader::encode`, which refuses a
1823 /// header whose Object Status the framing it names cannot carry. Such a
1824 /// header errors here and nothing is sent, rather than going out as an
1825 /// ordinary payload datagram with the status quietly dropped.
1826 pub fn send_datagram(
1827 &self,
1828 header: &AnyDatagramHeader,
1829 payload: &[u8],
1830 ) -> Result<(), ConnectionError> {
1831 // Before anything is encoded, for the reason `open_subgroup_stream`
1832 // gives.
1833 self.endpoint.note_object_forwarding_preference(
1834 header.meta().track_alias,
1835 ObjectForwardingPreference::Datagram,
1836 )?;
1837 let mut buf = Vec::new();
1838 header.encode(&mut buf)?;
1839 buf.extend_from_slice(payload);
1840 self.emit(ClientEvent::DatagramReceived {
1841 direction: Direction::Send,
1842 header: header.clone(),
1843 payload_len: payload.len(),
1844 });
1845 self.transport.send_datagram(bytes::Bytes::from(buf))?;
1846 Ok(())
1847 }
1848
1849 /// Receive a datagram and decode it whole.
1850 ///
1851 /// An extension block announced with a length of 0 is returned as
1852 /// [`ConnectionError::EmptyDatagramExtensions`], which
1853 /// [`Connection::close_for_data_stream`] closes the session over.
1854 ///
1855 /// The codec's `Datagram::decode_object` then reads the whole datagram, and its
1856 /// refusals are returned as [`ConnectionError::Codec`]: an extension block that
1857 /// does not parse, a payload after a status, and a Prior Group ID Gap fault
1858 /// (draft-12 Section 10.1). A gap fault, [`CodecError::MalformedTrack`], makes
1859 /// the track malformed, and this withdraws from it before returning, as
1860 /// draft-12 Section 2.5 requires: "When a subscriber detects a Malformed Track,
1861 /// it MUST UNSUBSCRIBE from the Track and SHOULD deliver an error to the
1862 /// application." The session stays open.
1863 pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
1864 let data = self.transport.recv_datagram().await?;
1865 let mut cursor = &data[..];
1866 let header = match AnyDatagramHeader::decode(DraftVersion::Draft12, &mut cursor) {
1867 Ok(header) => header,
1868 Err(CodecError::InvalidField) => {
1869 return Err(empty_datagram_extensions(&data)
1870 .unwrap_or(ConnectionError::Codec(CodecError::InvalidField)));
1871 }
1872 Err(err) => return Err(err.into()),
1873 };
1874 let consumed = data.len() - cursor.len();
1875 let payload = data.slice(consumed..);
1876 self.emit(ClientEvent::DatagramReceived {
1877 direction: Direction::Receive,
1878 header: header.clone(),
1879 payload_len: payload.len(),
1880 });
1881 if let Err(err) =
1882 moqtap_codec::draft12::data_stream::Datagram::decode_object(&mut &data[..])
1883 {
1884 if let CodecError::MalformedTrack { .. } = err {
1885 self.withdraw_malformed_track(
1886 header.meta().track_alias,
1887 MalformedTrackCondition::PriorGroupIdGap,
1888 )
1889 .await;
1890 }
1891 return Err(err.into());
1892 }
1893 // A datagram is the other framing, and it settles the track's just as a
1894 // subgroup header does.
1895 self.note_received_framing(header.meta().track_alias, ObjectForwardingPreference::Datagram)
1896 .await?;
1897 // A datagram is a whole object, so the connection can measure it
1898 // without help from the caller.
1899 let meta = header.meta();
1900 if let Err(err) = self.endpoint.note_received_object(
1901 meta.track_alias,
1902 ObjectLocation { group: meta.group_id, object: meta.object_id },
1903 object_role(meta.status),
1904 ) {
1905 // One of the two rules that record answers is a Malformed Track,
1906 // and this path can give the answer itself: a datagram is read
1907 // through the connection, which is what an UNSUBSCRIBE takes. The
1908 // other rule ends the session and is the close table's.
1909 if matches!(err, EndpointError::ObjectPastFinalObject { .. }) {
1910 self.withdraw_malformed_track(
1911 meta.track_alias,
1912 MalformedTrackCondition::ObjectPastFinalObject,
1913 )
1914 .await;
1915 }
1916 return Err(err.into());
1917 }
1918 Ok((header, payload))
1919 }
1920
1921 // ── Accessors ───────────────────────────────────────────
1922
1923 /// Access the underlying endpoint state machine.
1924 pub fn endpoint(&self) -> &Endpoint {
1925 &self.endpoint
1926 }
1927
1928 /// Mutable access to the endpoint state machine.
1929 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
1930 &mut self.endpoint
1931 }
1932
1933 /// The SETUP message the server answered the handshake with.
1934 ///
1935 /// `SERVER_SETUP` through draft-16, the server's half of the unified
1936 /// `SETUP` from draft-17. [`AnyControlMessage::fields`] renders it under
1937 /// this draft's own parameter names, in the order they arrived.
1938 pub fn server_setup(&self) -> &AnyControlMessage {
1939 &self.server_setup
1940 }
1941
1942 /// The framed wire bytes of [`Self::server_setup`], as they arrived.
1943 ///
1944 /// Kept beside the decoded form because the encoding is evidence the
1945 /// decoding discards: two relays sending the same parameter can still
1946 /// disagree on how wide a varint they wrote it in.
1947 pub fn server_setup_raw(&self) -> Option<&[u8]> {
1948 self.server_setup_raw.as_deref()
1949 }
1950
1951 /// Get the negotiated MoQT version.
1952 pub fn negotiated_version(&self) -> Option<VarInt> {
1953 self.endpoint.negotiated_version()
1954 }
1955
1956 /// Which of this draft's *own* `ConnectionError` variants this error is,
1957 /// and which kind of thing it says.
1958 ///
1959 /// The ten every draft carries answer `None` here: [`AnyConnectionError`]
1960 /// classifies those itself. Draft-12 adds two:
1961 /// [`ConnectionError::ControlMessageNarrowing`], a defect in this build, and
1962 /// [`ConnectionError::EmptyDatagramExtensions`], a decoder refusal the draft
1963 /// answers with a close.
1964 ///
1965 /// Matched exhaustively, with no wildcard arm and deliberately so: a
1966 /// variant added to this draft's error type has to arrive here as a compile
1967 /// error, beside the doc comment that quotes the sentence it enforces,
1968 /// rather than as a silent [`ErrorCause::Unclassified`] in the facade.
1969 ///
1970 /// [`AnyConnectionError`]: crate::dispatch::AnyConnectionError
1971 /// [`ErrorCause::Unclassified`]: crate::dispatch::ErrorCause::Unclassified
1972 pub fn draft_specific_cause(
1973 err: &ConnectionError,
1974 ) -> Option<crate::above_codec_rules::DraftSpecificCause> {
1975 match err {
1976 ConnectionError::Endpoint(_)
1977 | ConnectionError::Codec(_)
1978 | ConnectionError::Transport(_)
1979 | ConnectionError::VarInt(_)
1980 | ConnectionError::NoControlStream
1981 | ConnectionError::UnexpectedEnd
1982 | ConnectionError::StreamFinished
1983 | ConnectionError::InvalidAddress(_)
1984 | ConnectionError::TlsConfig(_)
1985 | ConnectionError::DataStreamState(_) => None,
1986 // This build decoding a message and then failing to narrow it to
1987 // its own draft. Nothing reached the wire and no peer is
1988 // implicated, which is the whole reason it is not
1989 // `ConnectionError::Codec`: under that name it would carry
1990 // `Some(PROTOCOL_VIOLATION)` out of `codec_session_error_code` and
1991 // publish a relay for this build's defect. See the variant's own
1992 // doc.
1993 ConnectionError::ControlMessageNarrowing => {
1994 Some(crate::above_codec_rules::DraftSpecificCause::LocalRefusal)
1995 }
1996 // The codec refuses the datagram as a bare `InvalidField`, and
1997 // `recv_datagram` names the rule, so `close_for_data_stream` reads
1998 // the close here. See the variant's own doc.
1999 ConnectionError::EmptyDatagramExtensions { .. } => {
2000 Some(crate::above_codec_rules::DraftSpecificCause::Codec {
2001 error: CodecError::InvalidField,
2002 close: Some(
2003 moqtap_codec::draft12::error_codes::SessionErrorCode::ProtocolViolation
2004 .as_u64(),
2005 ),
2006 })
2007 }
2008 }
2009 }
2010
2011 /// The code to close the session with when a message could not be decoded
2012 /// because the peer broke a rule draft-12 answers with a close.
2013 ///
2014 /// Every variant listed here comes from a sentence in this draft that names
2015 /// the consequence, and the list is per draft: answering a bound this draft
2016 /// does not state would close a session over traffic a conforming peer may
2017 /// send.
2018 ///
2019 /// - Reason Phrase, maximum 1024 bytes: "If an endpoint receives a length
2020 /// exceeding the maximum, it MUST close the session with a Protocol
2021 /// Violation."
2022 /// - GOAWAY New Session URI, maximum 8,192 bytes, with the same sentence.
2023 /// Drafts 11 through 19 state it; 07 through 10 state no maximum for
2024 /// the field at all.
2025 /// - Key-Value-Pair value, maximum 2^16-1 bytes, with the same sentence.
2026 /// - Track Namespace tuple size: "If an endpoint receives a Track
2027 /// Namespace tuple with an N of 0 or more than 32, it MUST close the
2028 /// session with a Protocol Violation." Note the lower bound - an empty
2029 /// tuple is refused here, where drafts 17 and later permit one.
2030 /// - Full Track Name, maximum 4,096 bytes, "computed as the sum of the
2031 /// lengths of each Track Namespace tuple field and the Track Name
2032 /// length field". This draft bounds the pair and not the namespace
2033 /// alone; draft-16 widened it.
2034 /// - Duplicate parameters, a SHOULD rather than a MUST: "Receivers SHOULD
2035 /// check that there are no unauthorized duplicate parameters and close
2036 /// the session as a 'Protocol Violation' if found." The rule is
2037 /// asymmetric here - "Receivers MUST allow duplicates of unknown
2038 /// parameters", and one known type is granted repeats - and the codec
2039 /// reports only the repeats this draft actually forbids.
2040 /// - Unknown control message type: "An endpoint that receives an unknown
2041 /// message type MUST close the session."
2042 /// - Extension headers on an Object whose status is Object Does Not
2043 /// Exist, Section 9.2.1.2. That one arrives on a data stream or a
2044 /// datagram, so [`Connection::close_for_data_stream`] is what carries
2045 /// it.
2046 ///
2047 /// **Not** the zero-length Track Namespace Field, the delta-encoded
2048 /// parameter type overflow, or the Object ID delta wrap. Those enter the
2049 /// specification at drafts 16 and 18, and this draft states none of them.
2050 ///
2051 /// **Not** the 2^16-1 control message length either. This draft states the
2052 /// limit - "the total length of a control message is limited to 2^16-1
2053 /// bytes" - and states no consequence for exceeding it, so an oversized
2054 /// message is refused by the decoder and stops there.
2055 ///
2056 /// **Not** [`CodecError::UnexpectedEnd`], which reports no rule at all: the
2057 /// reader raises it whenever a message is still arriving, and
2058 /// `read_control` loops on it. Closing over it would end a session on an
2059 /// ordinary short read.
2060 ///
2061 /// **Not** the unknown Message Parameter rule. Drafts 16 through 19 require
2062 /// a close for a Message Parameter whose type the negotiated version does
2063 /// not define. This draft states the opposite and states it about the same
2064 /// parameters: "Receivers MUST allow duplicates of unknown parameters",
2065 /// which presumes an unknown parameter arrives and is carried. Refusing one
2066 /// here would close a session over an extension this draft leaves room for.
2067 ///
2068 /// `None` for everything else, including [`CodecError::InvalidField`]. That
2069 /// variant is shared by a dozen unrelated malformations, only some of which
2070 /// the draft answers with a close, so a session cannot be ended on it
2071 /// without ending sessions the draft does not ask to be ended. Splitting it
2072 /// is the way to bring the rest of those rules under this function;
2073 /// widening the match is not - the extension-header rule above is
2074 /// answerable here because it has a variant of its own rather than being
2075 /// one more reading of `InvalidField`.
2076 pub fn codec_session_error_code(
2077 err: &CodecError,
2078 ) -> Option<moqtap_codec::draft12::error_codes::SessionErrorCode> {
2079 use moqtap_codec::draft12::error_codes::SessionErrorCode;
2080 use moqtap_codec::kvp::KvpError;
2081 match err {
2082 // The declared Length disagreeing with the fields, which every
2083 // draft answers with a close. Drafts 07 through 10 name no code for
2084 // it, so it takes the one their other unnamed rules take.
2085 CodecError::ControlMessageLengthMismatch { .. } => {
2086 Some(SessionErrorCode::ProtocolViolation)
2087 }
2088 CodecError::ReasonPhraseTooLong
2089 | CodecError::GoAwayUriTooLong
2090 | CodecError::InvalidNamespaceTupleSize(_)
2091 | CodecError::TrackNameTooLong
2092 | CodecError::DuplicateParameter(_)
2093 | CodecError::UnknownMessageType(_)
2094 | CodecError::ExtensionsOnNonExistentObject(_)
2095 | CodecError::Kvp(KvpError::ValueTooLong(_)) => {
2096 Some(SessionErrorCode::ProtocolViolation)
2097 }
2098 // An unknown data-plane type, Section 9: "An endpoint that
2099 // receives an unknown stream or datagram type MUST close the
2100 // session." One sentence covering two tables, which is why both
2101 // variants sit here.
2102 // A Content Exists field that is neither zero nor one,
2103 // Section 8.8 and Section 8.13: "Any other value is a protocol
2104 // error and MUST terminate the session with a Protocol Violation".
2105 CodecError::InvalidContentExists(_) => Some(SessionErrorCode::ProtocolViolation),
2106 // A Forward field that is neither zero nor one. Section 8.7 and
2107 // Section 8.10: "Any other value is a protocol error and MUST
2108 // terminate the session with a Protocol Violation". Section 8.13
2109 // says "Any value other than 0 or 1 is a Protocol Violation".
2110 // Section 8.14 names the two legal values without a consequence and
2111 // is answered the same way, for the reason given on the variant.
2112 CodecError::InvalidForward(_) => Some(SessionErrorCode::ProtocolViolation),
2113 CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
2114 Some(SessionErrorCode::ProtocolViolation)
2115 }
2116 // A key-value pair whose value is not the serialization its own
2117 // Type defines, Section 1.3.2: "If a receiver understands a Type,
2118 // and the following Value or Length/Value does not match the
2119 // serialization defined by that Type, the receiver MUST terminate the
2120 // session with error code 'Key-Value Formatting Error'."
2121 // Section 8.2.1.1 states the same answer for the one structure this
2122 // draft spells out: "If the Token structure cannot be decoded, the
2123 // receiver MUST close the Session with Key-Value Formatting error."
2124 //
2125 // The one rule in this table that names a code other than Protocol
2126 // Violation.
2127 CodecError::KeyValueFormatting { .. } => {
2128 Some(SessionErrorCode::KeyValueFormattingError)
2129 }
2130 // Everything this draft does not answer, named rather than swept up
2131 // by a wildcard. The arm is exhaustive deliberately: a new
2132 // `CodecError` variant will not compile until it has been placed on
2133 // one side or the other, on this draft, which is the decision a `_`
2134 // arm makes silently and invisibly in every draft module at once.
2135 //
2136 // Adding one variant to `CodecError` produces an `E0004` in every
2137 // draft module that matches it exhaustively, each naming the
2138 // variant that has nowhere to go. That is the whole mechanism.
2139 //
2140 // The nesting stops at `VarInt`, whose variants report how the bytes
2141 // ran out rather than a rule an endpoint states, so there is nothing
2142 // in it for a draft to answer. `Kvp` is spelled out because it does
2143 // carry one.
2144 // Not `ParameterValueOutOfRange`: no parameter this draft defines
2145 // restricts its value's range. Forwarding is a message field here
2146 // and is answered above.
2147 CodecError::ParameterValueOutOfRange { .. }
2148 | CodecError::UnexpectedEnd
2149 | CodecError::MessageTooLong(_)
2150 | CodecError::VarInt(_)
2151 | CodecError::InvalidField
2152 | CodecError::EmptyNamespaceField
2153 | CodecError::InvalidRange(..)
2154 | CodecError::ParameterLengthMismatch(_)
2155 | CodecError::EndOfTrackObjectId(_)
2156 | CodecError::KeyDeltaOverflow(..)
2157 // Not `TrackPropertyValueOutOfRange`: this draft has neither
2158 // namespace the variant is about. Draft-16 opens an extension header
2159 // registry with value rules of its own, and draft-17 renames it to
2160 // the Track Property registry. Before that, everything with a
2161 // restricted range is either a message field or a Message Parameter.
2162 | CodecError::TrackPropertyValueOutOfRange { .. }
2163 | CodecError::ParametersOutOfOrder(..)
2164 | CodecError::ObjectIdOverflow(..)
2165 | CodecError::InvalidRequiredRequestIdDelta(..)
2166 | CodecError::InvalidStreamTypeValue { .. }
2167 | CodecError::InvalidDatagramTypeValue { .. }
2168 | CodecError::UnknownMessageParameter(_)
2169 // Not `ParameterOutOfScope`: this draft states the scope rule and
2170 // answers it the other way. Section 8.2.1 Version Specific Parameters: "Each
2171 // version-specific parameter definition indicates the message types in which it can
2172 // appear. If it appears in some other type of message, it MUST be
2173 // ignored." The codec carries such a parameter on this draft and never
2174 // raises the variant, so this arm records a rule this draft has and
2175 // does not close over, not one it is missing. Draft-17 is where the
2176 // second sentence becomes a close.
2177 | CodecError::ParameterOutOfScope { .. }
2178 // A Filter Type outside the set this draft assigns. Section 8.7
2179 // states the rule and stops there: "A filter type other than the
2180 // above MUST be treated as error." No code, no close, and no
2181 // sentence elsewhere in the draft that turns an error into one — so
2182 // the message is refused and the session stays open.
2183 // Draft-14 Section 9.7 is where the same sentence gained "MUST be
2184 // close the session with PROTOCOL_VIOLATION", and it is answered
2185 // there.
2186 //
2187 // The assigned set is not the same on every draft either: 07 and 08
2188 // assign 0x1 as Latest Group, 09 and 10 withdraw it, and 11 and
2189 // later reinstate it as Next Group Start. The decoder holds each
2190 // draft to its own list; this arm only decides what a refusal does
2191 // to the session.
2192 //
2193 // The two rules below belong to the parameter form of the filter,
2194 // which arrives at draft-15. This draft carries the Filter Type as a
2195 // field of SUBSCRIBE, so there is no parameter for either to be
2196 // about.
2197 | CodecError::InvalidFilterType(_)
2198 | CodecError::SubscriptionFilterMalformed { .. }
2199 | CodecError::FilterEndGroupOverflow { .. }
2200 // A Fetch Type outside the set this draft assigns. Section 8.16
2201 // states the rule and stops there: "A Fetch Type other than 0x1,
2202 // 0x2 or 0x3 MUST be treated as an error", naming no code and no
2203 // close, exactly as this draft's Filter Type sentence does.
2204 // Draft-14 Section 9.16 is where both gained "MUST be close the
2205 // session with a PROTOCOL_VIOLATION", and it answers them there.
2206 | CodecError::InvalidFetchType(_)
2207 // The object payload rule, Section 9.2.1.1: "Any object with a status
2208 // code other than zero MUST have an empty payload." A MUST on the
2209 // sender with no receiver action named anywhere — the "SHOULD be
2210 // treated as a protocol error" in the same paragraph belongs to the
2211 // sentence before it, which is about a status value this draft does
2212 // not assign — so an object carrying a payload it may not is refused
2213 // and the session stays open.
2214 | CodecError::PayloadNotPermitted { .. }
2215 | CodecError::MalformedObjectProperties { .. }
2216 // A Prior Group ID Gap fault makes the track malformed
2217 // (draft-12 Section 10.1), which draft-12 Section 2.5 answers with
2218 // an UNSUBSCRIBE, through `withdraw_for_subgroup_stream`, and not a
2219 // close.
2220 | CodecError::MalformedTrack { .. }
2221 // Raised only by the draft-23 codec.
2222 | CodecError::ControlMessageExceedsLimit { .. }
2223 | CodecError::MissingErrorDetail { .. }
2224 | CodecError::MalformedErrorDetail { .. }
2225 | CodecError::TokenAliasInSetup { .. }
2226 | CodecError::EmptyTrackName
2227 | CodecError::MalformedAuthToken { .. }
2228 | CodecError::TrackPropertiesNotPermitted
2229 | CodecError::DeltaDecodingOverflow { .. }
2230 | CodecError::MalformedObject { .. }
2231 | CodecError::UnknownObjectStatus(_)
2232 | CodecError::UnsupportedDraft(_)
2233 | CodecError::Kvp(
2234 KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
2235 ) => None,
2236 }
2237 }
2238
2239 /// Close the session on the wire when a decode failure is one draft-12
2240 /// answers with a close, and hand the error back unchanged.
2241 /// Without it every bound the decoder enforces would stop at *this endpoint
2242 /// refused the frame* while the peer, which is the one that broke the rule,
2243 /// saw a session that was still open and went on sending. "MUST close the
2244 /// session" is a statement about the wire.
2245 fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
2246 if let ConnectionError::Codec(inner) = &err {
2247 if let Some(code) = Self::codec_session_error_code(inner) {
2248 // QUIC application error codes are 62-bit; every code in this
2249 // registry is far below `u32::MAX`, and saturating rather than
2250 // truncating means a future code that is not could never be
2251 // reported as a different, assigned one.
2252 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2253 self.close(wire_code, inner.to_string().as_bytes());
2254 }
2255 }
2256 err
2257 }
2258
2259 /// Close the session on the wire when the endpoint says a violation is
2260 /// fatal to it, and hand the error back unchanged.
2261 ///
2262 /// [`EndpointError::session_error_code`] answers `Some` for exactly the
2263 /// errors this draft ends the session over, and the endpoint has already
2264 /// moved its own state machine to Closed by the time this runs. Without
2265 /// this step that move is purely internal: the local endpoint refuses to
2266 /// start anything new while the peer, which is the one that broke the
2267 /// rule, sees a session that is still open and goes on sending. A rule
2268 /// that names a session termination code is a statement about the wire,
2269 /// so it takes a CONNECTION_CLOSE to satisfy it.
2270 ///
2271 /// The reason phrase is the error's own `Display` text, which names the
2272 /// rule rather than repeating the numeric code the close already carries.
2273 ///
2274 /// Errors that answer `None` are recoverable and nothing is sent.
2275 fn close_for(&self, err: &EndpointError) {
2276 if let Some(code) = err.session_error_code() {
2277 // QUIC application error codes are 62-bit; every code in this
2278 // registry is far below `u32::MAX`, and saturating rather than
2279 // truncating means a future code that is not could never be
2280 // reported as a different, assigned one.
2281 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2282 self.close(wire_code, err.to_string().as_bytes());
2283 }
2284 }
2285
2286 /// [`close_for`](Self::close_for), then the error unchanged, for the
2287 /// common case where the endpoint's error is also what the caller returns.
2288 fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
2289 self.close_for(&err);
2290 ConnectionError::Endpoint(err)
2291 }
2292
2293 /// Withdraw from a track a data stream found malformed, reporting whether
2294 /// it did.
2295 ///
2296 /// The Malformed Track twin of [`Connection::close_for_data_stream`], and
2297 /// separate from it for the same reason and one more. The same one: a
2298 /// [`FramedRecvStream`] holds no connection, so the reader that finds the
2299 /// fault is not the object that can send an UNSUBSCRIBE. The one more: the
2300 /// two answers are opposites — that call ends the session, this one gives
2301 /// up a track and leaves it running — and a single entry point would have
2302 /// to decide between them from the error alone, which is exactly the
2303 /// decision a caller reproducing a capture wants to make itself.
2304 ///
2305 /// The datagram path needs none of this. It is read through the connection,
2306 /// so [`Connection::recv_datagram`] answers the condition where it finds
2307 /// it, and this is only for the objects that arrive on a stream the caller
2308 /// holds.
2309 pub async fn withdraw_for_data_stream(&self, err: &ConnectionError) -> bool {
2310 let ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) = err
2311 else {
2312 return false;
2313 };
2314 self.withdraw_malformed_track(*alias, MalformedTrackCondition::ObjectPastFinalObject).await;
2315 true
2316 }
2317
2318 /// [`Connection::withdraw_for_data_stream`] for an error read from the
2319 /// subgroup stream `header` opened, which also withdraws from the track
2320 /// over the [`CodecError::MalformedTrack`] that
2321 /// [`FramedRecvStream::read_subgroup_object`] returns for a Prior Group ID
2322 /// Gap fault (draft-12 Section 10.1). That error names no track, so the
2323 /// header supplies its alias.
2324 ///
2325 /// draft-12 Section 2.5: "When a subscriber detects a Malformed Track, it
2326 /// MUST UNSUBSCRIBE from the Track and SHOULD deliver an error to the
2327 /// application."
2328 pub async fn withdraw_for_subgroup_stream(
2329 &self,
2330 header: &AnySubgroupHeader,
2331 err: &ConnectionError,
2332 ) -> bool {
2333 if let ConnectionError::Codec(CodecError::MalformedTrack { .. }) = err {
2334 self.withdraw_malformed_track(
2335 header.track_alias(),
2336 MalformedTrackCondition::PriorGroupIdGap,
2337 )
2338 .await;
2339 return true;
2340 }
2341 self.withdraw_for_data_stream(err).await
2342 }
2343
2344 /// [`Connection::withdraw_for_data_stream`] for an error read from the fetch
2345 /// stream `header` opened, which also withdraws from the track the FETCH
2346 /// asked for over the [`CodecError::MalformedTrack`] that
2347 /// [`FramedRecvStream::read_fetch_object`] returns for a Prior Group ID Gap
2348 /// fault (draft-12 Section 10.1). That error names no track, so the header's
2349 /// Request ID supplies it.
2350 ///
2351 /// draft-12 Section 2.5: "When a subscriber detects a Malformed Track, it
2352 /// MUST UNSUBSCRIBE from the Track and SHOULD deliver an error to the
2353 /// application."
2354 pub async fn withdraw_for_fetch_stream(
2355 &self,
2356 header: &AnyFetchHeader,
2357 err: &ConnectionError,
2358 ) -> bool {
2359 let ConnectionError::Codec(CodecError::MalformedTrack { .. }) = err else {
2360 return self.withdraw_for_data_stream(err).await;
2361 };
2362 let condition = MalformedTrackCondition::PriorGroupIdGap;
2363 self.withdraw_malformed_fetch_track(header.request_id(), condition).await;
2364 true
2365 }
2366
2367 /// Close the session over a rule broken on a data stream, reporting whether
2368 /// it did.
2369 ///
2370 /// A data stream cannot close for itself the way `recv_control` does:
2371 /// [`Connection::accept_subgroup_stream`] hands the caller a
2372 /// [`FramedRecvStream`] holding no connection, so the reader that finds the
2373 /// violation is not the object that can act on it. Keeping it a separate
2374 /// call is deliberate as well - a permissive caller, one reproducing a
2375 /// capture, can read a violating stream and report it without tearing the
2376 /// session down.
2377 ///
2378 /// The rule this draft answers here is extension headers on an Object whose
2379 /// status is Object Does Not Exist, which reaches subgroup streams, fetch
2380 /// streams and status datagrams alike. It shares `codec_session_error_code`
2381 /// with the control path, so a rule is answered with one code whichever
2382 /// stream carried it.
2383 ///
2384 /// Not every rule that reaches here is the decoder's. A track whose objects
2385 /// mix forwarding preferences is the endpoint's to notice — it takes the
2386 /// alias table to know which track an object belongs to — and it arrives on
2387 /// exactly these streams. Both kinds are asked for a code the same way, and
2388 /// a rule with no code is declined rather than guessed at.
2389 pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
2390 match err {
2391 ConnectionError::Codec(inner) => {
2392 let Some(code) = Self::codec_session_error_code(inner) else { return false };
2393 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2394 self.close(wire_code, inner.to_string().as_bytes());
2395 true
2396 }
2397 // A rule the endpoint raises rather than the decoder. The two
2398 // reach their codes through different tables and mean the same
2399 // thing here: `Some` is a rule this draft ends the session over.
2400 ConnectionError::Endpoint(inner) => {
2401 let Some(code) = inner.session_error_code() else { return false };
2402 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
2403 self.close(wire_code, inner.to_string().as_bytes());
2404 true
2405 }
2406 // Not a `Codec` failure: the codec refuses the datagram as a bare
2407 // `InvalidField`, which `recv_datagram` names, so the code comes
2408 // from `draft_specific_cause`.
2409 ConnectionError::EmptyDatagramExtensions { .. } => {
2410 let Some(crate::above_codec_rules::DraftSpecificCause::Codec {
2411 close: Some(code),
2412 ..
2413 }) = Self::draft_specific_cause(err)
2414 else {
2415 return false;
2416 };
2417 self.close(u32::try_from(code).unwrap_or(u32::MAX), err.to_string().as_bytes());
2418 true
2419 }
2420 _ => false,
2421 }
2422 }
2423
2424 /// Close the connection.
2425 pub fn close(&self, code: u32, reason: &[u8]) {
2426 self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
2427 self.transport.close(code, reason);
2428 }
2429}
2430
2431/// Determine the encoded length of a varint from its first byte.
2432fn varint_len(first_byte: u8) -> usize {
2433 1 << (first_byte >> 6)
2434}
2435
2436#[cfg(test)]
2437mod tests {
2438 use super::*;
2439
2440 /// Only a refusal of what the peer sent is reported as a refused frame,
2441 /// and only with the bytes it was about.
2442 #[test]
2443 fn only_a_refusal_of_the_peers_frame_is_reported() {
2444 let raw = Some(vec![0x01]);
2445 let decoder = ConnectionError::Codec(CodecError::UnexpectedEnd);
2446 assert!(Connection::refused_event(&decoder, raw.clone()).is_some());
2447 assert!(Connection::refused_event(&decoder, None).is_none());
2448 let local = ConnectionError::Endpoint(EndpointError::NotActive);
2449 assert!(Connection::refused_event(&local, raw).is_none());
2450 }
2451
2452 /// This build failing to narrow a message it decoded is never a finding
2453 /// about the peer.
2454 ///
2455 /// The arm that raises `ControlMessageNarrowing` is unreachable — this
2456 /// draft's decoder can only hand back this draft's variant — and nothing
2457 /// pins that. What is pinned here is the half that matters.
2458 /// `CodecError::UnknownMessageType(0)` is what the arm must not raise:
2459 /// `codec_session_error_code` answers it `Some(PROTOCOL_VIOLATION)` on
2460 /// every draft in range, so the day the narrowing failed a conformance
2461 /// probe would publish a relay for sending a control message type this
2462 /// draft does not assign — with `0x00` attached as the codepoint that
2463 /// proved it, which is an accusation better evidenced than any real one
2464 /// this build makes. The section stating that rule is renumbered several
2465 /// times across the series, and the point does not turn on the
2466 /// number.
2467 ///
2468 /// Fails if the arm raises
2469 /// `ConnectionError::Codec(CodecError::UnknownMessageType(0))`, as does the
2470 /// probe's own `violation::a_message_this_build_could_not_narrow_names_nobody`.
2471 #[test]
2472 fn a_message_this_build_could_not_narrow_names_nobody() {
2473 use crate::dispatch::{AnyConnectionError, ErrorCause};
2474
2475 let err: AnyConnectionError = ConnectionError::ControlMessageNarrowing.into();
2476 assert!(err.is_local(), "a narrowing this build could not do is this build's");
2477 assert_eq!(
2478 err.cause(),
2479 &ErrorCause::Facade,
2480 "nothing reached the wire, so there is no rule and no close code to read"
2481 );
2482 }
2483
2484 /// Types 0x01, 0x03 and 0x05 carry extensions, so a zero byte where their
2485 /// Extension Headers Length sits is the close. Types 0x00, 0x02 and 0x04
2486 /// have no such field. The ruling is on `EmptyDatagramExtensions`.
2487 #[test]
2488 fn an_empty_extension_block_is_named_on_exactly_the_types_with_extensions() {
2489 for datagram_type in [0x00u8, 0x01, 0x02, 0x03, 0x04, 0x05] {
2490 let datagram = [datagram_type, 0x07, 0x01, 0x09, 0x80, 0x00, 0x00];
2491 let got = empty_datagram_extensions(&datagram);
2492 if matches!(datagram_type, 0x01 | 0x03 | 0x05) {
2493 assert!(
2494 matches!(got, Some(ConnectionError::EmptyDatagramExtensions { object_id: 9 })),
2495 "Type {datagram_type:#04x}: expected the close, got {got:?}"
2496 );
2497 } else {
2498 assert!(got.is_none(), "Type {datagram_type:#04x}: carries no extensions");
2499 }
2500 }
2501 }
2502
2503 #[test]
2504 fn client_config_supported_versions_default() {
2505 let config = ClientConfig {
2506 additional_versions: Vec::new(),
2507 transport: TransportType::Quic,
2508 skip_cert_verification: false,
2509 ca_certs: Vec::new(),
2510 setup_parameters: Vec::new(),
2511 };
2512 let versions = config.supported_versions();
2513 assert_eq!(versions.len(), 1);
2514 assert_eq!(versions[0].into_inner(), 0xff000000 + 12);
2515 }
2516
2517 #[test]
2518 fn client_config_alpn_quic() {
2519 let config = ClientConfig {
2520 additional_versions: Vec::new(),
2521 transport: TransportType::Quic,
2522 skip_cert_verification: false,
2523 ca_certs: Vec::new(),
2524 setup_parameters: Vec::new(),
2525 };
2526 assert_eq!(config.alpn(), vec![DraftVersion::Draft12.quic_alpn().to_vec()]);
2527 }
2528
2529 #[test]
2530 fn moqt_alpn_value() {
2531 assert_eq!(MOQT_ALPN, b"moq-00");
2532 }
2533}