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