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