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