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