moqtap_client/draft18/connection.rs
1use std::collections::VecDeque;
2use std::sync::Mutex;
3
4use bytes::{Buf, Bytes, BytesMut};
5
6use crate::draft18::endpoint::{Endpoint, EndpointError};
7use crate::draft18::event::{ClientEvent, Direction, StreamKind};
8use crate::draft18::observer::ConnectionObserver;
9use crate::draft18::session::request_id::Role;
10use crate::draft18::session::setup;
11use crate::malformed_tracks::MalformedTrackCondition;
12use crate::track_locations::{ObjectLocation, ObjectRole, TrackObjects};
13use crate::transport::{RecvStream, SendStream, Transport, TransportError};
14use moqtap_codec::dispatch::{
15 AnyControlMessage, AnyDatagramHeader, AnyFetchHeader, AnySubgroupHeader,
16};
17use moqtap_codec::draft18::data_stream::{
18 FetchHeader, FetchObject, FetchObjectHeader, FetchObjectReader, GroupOrder, SubgroupObject,
19 SubgroupObjectReader,
20};
21use moqtap_codec::draft18::error_codes::StreamResetErrorCode;
22use moqtap_codec::draft18::message::{
23 ControlMessage, FetchOk, MessageType, Namespace, NamespaceDone, PublishBlocked, RequestError,
24 RequestOk, SubscribeOk,
25};
26use moqtap_codec::error::CodecError;
27use moqtap_codec::kvp::KeyValuePair;
28use moqtap_codec::types::*;
29use moqtap_codec::varint::VarInt;
30use moqtap_codec::version::DraftVersion;
31
32/// The ALPN identifier draft-18 uses on raw QUIC, `moqt-18`.
33///
34/// Drafts 07 to 14 share one ALPN, `moq-00`, and a peer that offers it has
35/// said nothing about which of the eight it speaks. Draft-15 ended that:
36/// from there each draft has an ALPN of its own, so the version is settled
37/// by the TLS handshake before a byte of MoQT is written.
38///
39/// This is [`DraftVersion::Draft18`]'s own
40/// [`quic_alpn`](DraftVersion::quic_alpn), which is what
41/// [`ClientConfig::alpn`] offers; the test below holds the two together.
42pub const MOQT_ALPN: &[u8] = b"moqt-18";
43
44/// The unidirectional stream type that marks one direction of the control
45/// plane, and the SETUP message type. On draft-18 they are one number,
46/// 0x2F00: Section 3.4 (Unidirectional Stream Types) lists it as the type of
47/// a SETUP stream, and Section 10.3 gives it as the SETUP message's own type
48/// field.
49///
50/// Because they are the same number a control stream carries no separate
51/// stream header. The varint a reader uses to recognise the stream is the
52/// first field of the SETUP message it then decodes, and a writer that
53/// encodes a SETUP onto a fresh unidirectional stream has already written the
54/// stream type by writing the message.
55///
56/// Encoded with MoQT's variable-length integer, whose width is the number of
57/// leading 1 bits in the first byte, 0x2F00 is the two bytes `AF 00` — four
58/// under RFC 9000's encoding, which this draft does not use. Read it through
59/// [`DraftVersion::decode_varint`] rather than assuming a width.
60pub const CONTROL_STREAM_TYPE: u64 = 0x2F00;
61
62/// The application error code a request stream is reset with when the
63/// requester abandons it: `CANCELLED`, 0x1.
64///
65/// Draft-17 removed UNSUBSCRIBE and FETCH_CANCEL and draft-18 keeps them
66/// gone. Cancelling a request is resetting the bidirectional stream it was
67/// made on, and `CANCELLED` is the code draft-18 assigns for "the stream was
68/// cancelled by either endpoint" — see [`StreamResetErrorCode::Cancelled`].
69/// Taken from the codec's own registry rather than written as a literal so a
70/// renumbering in a later draft cannot be missed here.
71///
72/// Draft-18 renamed the registry: draft-17 Section 14.5.4 called it "Data
73/// Stream Reset Error Codes" and draft-18 calls it "Stream Reset Error Codes"
74/// (Section 15.10), widening it to cover request streams explicitly. The value
75/// is unchanged.
76///
77/// This is what [`RequestStream::cancel`] uses when no code is chosen for it,
78/// and what `RequestStream`'s [`Drop`] sends.
79pub const REQUEST_CANCELLED: u64 = StreamResetErrorCode::Cancelled as u64;
80
81/// The application error code a request stream **the peer opened** is reset
82/// with when this endpoint abandons it: `INTERNAL_ERROR`, 0x0.
83///
84/// Dropping an inbound request is not the act [`REQUEST_CANCELLED`] describes.
85/// Draft-18 Section 3.3.2 grants a responder a cancel — "Receivers cancel
86/// requests if they are unable to or choose not to respond" — but a handle
87/// that fell out of scope chose nothing; it failed to serve, which is what
88/// [`StreamResetErrorCode::InternalError`], "an implementation specific error"
89/// in Section 3.3.3, names. The two codes are on the wire, so a peer counting
90/// refusals can tell a deliberate rejection from a dropped request only if they
91/// differ.
92///
93/// It is also what a responder that already answered is reset with when it is
94/// dropped without finishing. A FIN there would claim the request completed,
95/// and for a subscription it has not: PUBLISH_DONE is still owed.
96pub const REQUEST_UNANSWERED: u64 = StreamResetErrorCode::InternalError as u64;
97
98/// Errors from the connection layer.
99#[derive(Debug, thiserror::Error)]
100pub enum ConnectionError {
101 /// Endpoint state machine error.
102 #[error("endpoint error: {0}")]
103 Endpoint(#[from] EndpointError),
104 /// Wire codec error.
105 #[error("codec error: {0}")]
106 Codec(#[from] CodecError),
107 /// Transport-level error.
108 #[error("transport error: {0}")]
109 Transport(#[from] TransportError),
110 /// Variable-length integer decoding error.
111 #[error("varint error: {0}")]
112 VarInt(#[from] moqtap_codec::varint::VarIntError),
113 /// Control stream was not opened.
114 #[error("control stream not open")]
115 NoControlStream,
116 /// Stream ended before a complete message was read.
117 #[error("unexpected end of stream")]
118 UnexpectedEnd,
119 /// Stream was finished by the peer.
120 #[error("stream finished")]
121 StreamFinished,
122 /// Invalid server address string.
123 #[error("invalid server address: {0}")]
124 InvalidAddress(String),
125 /// TLS configuration error.
126 #[error("TLS config error: {0}")]
127 TlsConfig(String),
128 /// Data stream used out of order (e.g. object before header).
129 #[error("data stream state error: {0}")]
130 DataStreamState(&'static str),
131 /// A control message this build decoded for draft-18 and then could not
132 /// narrow to draft-18's own message type.
133 ///
134 /// Unreachable, and that is not the same as harmless. `read_control`
135 /// decodes with this connection's own draft, so the `AnyControlMessage` it
136 /// hands back can only carry this draft's variant — but the narrowing arm
137 /// is compiled in every configuration anyway, under
138 /// `#[allow(unreachable_patterns)]` rather than a `cfg` naming the other
139 /// drafts, because such a list has to be edited in every per-draft
140 /// module whenever a draft is added and a copy that omits one leaves the
141 /// match non-exhaustive.
142 ///
143 /// Spelled as `CodecError::UnknownMessageType(0)` it would not stay inert:
144 /// every draft's
145 /// [`codec_session_error_code`](Connection::codec_session_error_code)
146 /// answers that variant `Some(PROTOCOL_VIOLATION)`. So the day the
147 /// narrowing did fail, this build's own defect would reach a caller as *the
148 /// peer sent a control message type this draft does not assign, and the
149 /// session must be closed with a Protocol Violation* — carrying `0x00` as
150 /// the codepoint that proved it. A conformance report reading that
151 /// publishes a named, well-evidenced accusation against a relay for
152 /// something no relay did.
153 ///
154 /// A variant of its own is what stops that.
155 /// [`draft_specific_cause`](Connection::draft_specific_cause) answers it
156 /// [`LocalRefusal`], the facade turns that into [`ErrorCause::Facade`], and
157 /// nothing downstream can read a rule out of a cause that says nothing
158 /// reached the wire. What is pinned is the consequence rather than the
159 /// unreachability: nothing pins the arm's reachability, which is exactly
160 /// why the consequence must not be an accusation.
161 ///
162 /// [`LocalRefusal`]: crate::above_codec_rules::DraftSpecificCause::LocalRefusal
163 /// [`ErrorCause::Facade`]: crate::dispatch::ErrorCause::Facade
164 #[error(
165 "a control message decoded for draft-18 did not narrow to draft-18: a defect in this build, and evidence about nothing the peer did"
166 )]
167 ControlMessageNarrowing,
168 /// An Object arrived carrying properties on a status that is not Normal.
169 ///
170 /// Draft-18 Section 11.2.1.2: "Any Object with status Normal can have
171 /// properties (Section 2.5). If an endpoint receives properties on an
172 /// Object with status that is not Normal, it MUST close the session with a
173 /// PROTOCOL_VIOLATION."
174 ///
175 /// The codec's `read_object` refuses such an Object as a bare
176 /// `InvalidField`. Its `read_object_meta` frames it, which is how the
177 /// receive path names the rule here and stays in step with the wire.
178 #[error(
179 "object {object_id} carries {properties_len} bytes of properties on status {status:?}, which is not Normal"
180 )]
181 PropertiesOnNonNormalStatus {
182 /// The Object ID the properties arrived on.
183 object_id: u64,
184 /// Length in bytes of the properties block.
185 properties_len: usize,
186 /// The Object's status, resolved through the encoding's elision rule.
187 ///
188 /// Spelled out in full because the glob import of `moqtap_codec::types`
189 /// brings a different `ObjectStatus` into this module.
190 status: moqtap_codec::draft18::types::ObjectStatus,
191 },
192 /// A message that begins a request stream was handed to
193 /// [`Connection::send_control`]. Nothing was written.
194 #[error(
195 "{0:?} begins a request stream of its own and must not be written on the control stream"
196 )]
197 RequestOnControlStream(MessageType),
198 /// A message draft-18 places on a request stream was handed to
199 /// [`Connection::send_control`]. Nothing was written.
200 ///
201 /// Distinct from [`ConnectionError::RequestOnControlStream`], which is about
202 /// a message that would *begin* a request stream of its own. This one is
203 /// about a message that belongs on a request stream already open, and so has
204 /// no meaning without one around it.
205 #[error("{0:?} belongs on a request stream, not on the control stream")]
206 RequestStreamMessageOnControlStream(MessageType),
207 /// The codec refused a datagram with [`CodecError::MalformedTrack`], for a
208 /// Prior Group ID Gap or Prior Object ID Gap fault (draft-18 Section 12.8,
209 /// draft-18 Section 12.9), or with [`CodecError::MalformedObjectProperties`],
210 /// for a property block that does not parse (draft-18 Section 12.7). Either
211 /// makes the datagram's track malformed.
212 ///
213 /// Carries the datagram's Track Alias, which the codec's error does not
214 /// name, so [`Connection::requests_to_cancel`] can name the requests to
215 /// cancel. The session stays open.
216 #[error("datagram for track alias {track_alias}: {error}")]
217 MalformedTrackDatagram {
218 /// The Track Alias the datagram named.
219 track_alias: u64,
220 /// The codec's refusal.
221 error: CodecError,
222 },
223 /// A datagram with the PROPERTIES bit set arrived with a Properties Length
224 /// of 0.
225 ///
226 /// draft-18 Section 11.3.1: "If an endpoint receives a datagram with the
227 /// PROPERTIES bit set and an Properties Length of 0, it MUST close the
228 /// session with a PROTOCOL_VIOLATION."
229 ///
230 /// [`Connection::recv_datagram`] checks the decoded header for it, and
231 /// [`Connection::close_for_data_stream`] closes the session over it.
232 #[error(
233 "the datagram for object {object_id} sets the PROPERTIES bit and has a Properties Length of 0"
234 )]
235 EmptyDatagramProperties {
236 /// The Object ID the datagram named.
237 object_id: u64,
238 },
239 /// A datagram carrying an Object Status arrived with bytes after its
240 /// header.
241 ///
242 /// Draft-18 Section 11.2.1.1: "Any object with a status code other than
243 /// zero MUST have an empty payload." Section 11.3.1 says the same thing
244 /// about the framing: a datagram with the STATUS bit set "is present and
245 /// there is no Object Payload."
246 ///
247 /// The codec cannot see this. `DatagramHeader::decode` stops at the end of
248 /// the header and never owns the datagram's tail, so the only layer that
249 /// holds both the status and the bytes after it is this one. Without the
250 /// check the application is handed, say, an End-of-Group object carrying
251 /// four bytes of payload — a combination the draft forbids outright.
252 ///
253 /// Recoverable: the drafts state the rule as a property of a conforming
254 /// object, not as one of the "MUST close the session" cases, so the datagram
255 /// is refused and the session left running.
256 #[error(
257 "datagram for object {object_id} carries {payload_len} bytes after a header \
258 whose status is {status:?}, which permits no payload"
259 )]
260 PayloadOnStatusDatagram {
261 /// Object ID from the datagram header.
262 object_id: u64,
263 /// How many bytes followed the header.
264 payload_len: usize,
265 /// The status the header declared.
266 ///
267 /// Spelled out in full because the glob import of `moqtap_codec::types`
268 /// brings a different `ObjectStatus` into this module.
269 status: Option<moqtap_codec::draft18::types::ObjectStatus>,
270 },
271 /// A bidirectional stream the peer opened began with a message type that
272 /// does not open a request stream.
273 ///
274 /// Draft-18 Section 3.3: "Bidirectional streams MUST NOT begin with any
275 /// other message type unless negotiated. If they do, the peer MUST close
276 /// the Session with a PROTOCOL_VIOLATION." The session has already been
277 /// closed on the wire by the time this is returned, and the offending
278 /// stream reset.
279 #[error(
280 "a bidirectional stream the peer opened began with {0:?}, which does not begin a request stream; the session was closed"
281 )]
282 NonRequestOnRequestStream(MessageType),
283 /// A `respond_*` helper was handed a request stream this endpoint opened.
284 /// Nothing was written and no state moved.
285 #[error(
286 "this endpoint opened request {0}; only the endpoint a request stream was opened toward may answer it"
287 )]
288 RespondedToOwnRequest(u64),
289}
290
291impl From<crate::transport::DialError> for ConnectionError {
292 /// Maps a dial failure onto the variants this error already has, so a
293 /// caller matches `InvalidAddress` or `TlsConfig`.
294 ///
295 /// A `LocalSocket` failure maps to `InvalidAddress`, which the facade reads
296 /// as this side's failure
297 /// ([`ErrorCause::Facade`](crate::dispatch::ErrorCause::Facade)). The dial
298 /// phase stays on [`DialError::phase`](crate::transport::DialError::phase).
299 fn from(e: crate::transport::DialError) -> Self {
300 match e {
301 // Two variants, one arm, deliberately — see above.
302 crate::transport::DialError::InvalidAddress(s)
303 | crate::transport::DialError::LocalSocket(s) => ConnectionError::InvalidAddress(s),
304 crate::transport::DialError::TlsConfig(s) => ConnectionError::TlsConfig(s),
305 crate::transport::DialError::Transport(e) => ConnectionError::Transport(e),
306 }
307 }
308}
309
310/// Transport type for the connection.
311#[derive(Debug, Clone)]
312pub enum TransportType {
313 /// Raw QUIC via quinn. The `addr` field should be `host:port`.
314 Quic,
315 /// WebTransport via wtransport. The `url` field is the WebTransport URL.
316 WebTransport {
317 /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
318 url: String,
319 },
320}
321
322/// Configuration for a MoQT client connection.
323///
324/// Both `draft` and `transport` are required -- there is no `Default` impl.
325pub struct ClientConfig {
326 /// The MoQT draft version to use (primary, determines codec/framing).
327 pub draft: DraftVersion,
328 /// The transport type (QUIC or WebTransport).
329 pub transport: TransportType,
330 /// Whether to skip TLS certificate verification (for testing).
331 pub skip_cert_verification: bool,
332 /// Custom CA certificates to trust (DER-encoded).
333 pub ca_certs: Vec<Vec<u8>>,
334 /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
335 pub setup_parameters: Vec<KeyValuePair>,
336}
337
338impl ClientConfig {
339 /// Returns the ALPN protocol identifiers for the transport.
340 pub fn alpn(&self) -> Vec<Vec<u8>> {
341 match &self.transport {
342 TransportType::Quic => vec![self.draft.quic_alpn().to_vec()],
343 TransportType::WebTransport { .. } => vec![b"h3".to_vec()],
344 }
345 }
346}
347
348/// A framed writer for a send stream. Handles MoQT length-prefixed framing.
349pub struct FramedSendStream {
350 inner: SendStream,
351 draft: DraftVersion,
352 /// Stateful subgroup object writer.
353 subgroup_io: Option<SubgroupObjectReader>,
354}
355
356impl FramedSendStream {
357 /// Create a new framed send stream for the given draft version.
358 pub fn new(inner: SendStream, draft: DraftVersion) -> Self {
359 Self { inner, draft, subgroup_io: None }
360 }
361
362 /// Get the transport-level stream ID.
363 pub fn stream_id(&self) -> u64 {
364 self.inner.stream_id()
365 }
366
367 /// Write a control message to the stream with type+length framing.
368 /// Returns the raw bytes that were written (for event capture).
369 pub async fn write_control(
370 &mut self,
371 msg: &AnyControlMessage,
372 ) -> Result<Vec<u8>, ConnectionError> {
373 let mut buf = Vec::new();
374 msg.encode(&mut buf)?;
375 self.inner.write_all(&buf).await?;
376 Ok(buf)
377 }
378
379 /// Write a subgroup stream header. Also initializes the internal
380 /// delta-encoding state used by
381 /// [`FramedSendStream::write_subgroup_object`].
382 ///
383 /// The header is refused, and nothing is written, if its fields disagree
384 /// with its own stream type. That check has to happen here rather than at
385 /// the first object: the type is what every object after it is framed
386 /// against, so a header that went out saying the wrong thing cannot be
387 /// taken back.
388 pub async fn write_subgroup_header(
389 &mut self,
390 header: &AnySubgroupHeader,
391 ) -> Result<(), ConnectionError> {
392 let mut buf = Vec::new();
393 header.encode_stream_checked(&mut buf)?;
394 self.inner.write_all(&buf).await?;
395 // Clippy would rather see these two arms as an `if let`, and rustc rejects
396 // that in a single-draft build, where the pattern is irrefutable. Only a
397 // `match` satisfies both.
398 #[allow(clippy::single_match)]
399 match header {
400 AnySubgroupHeader::Draft18(ref header) => {
401 self.subgroup_io = Some(SubgroupObjectReader::new(header));
402 }
403 // Only this draft's header seeds the object reader. With draft 18 the only enabled
404 // draft `AnySubgroupHeader` has a single variant, the arm above is exhaustive and this
405 // one unreachable. Compiled in every configuration with the lint allowed, rather than
406 // gated on a `cfg` naming the other drafts: such a list has to be edited in
407 // every draft module whenever a draft is added, and a copy that omits one leaves this
408 // match non-exhaustive.
409 #[allow(unreachable_patterns)]
410 _ => {}
411 }
412 Ok(())
413 }
414
415 /// Write a fetch response header.
416 pub async fn write_fetch_header(
417 &mut self,
418 header: &AnyFetchHeader,
419 ) -> Result<(), ConnectionError> {
420 let mut buf = Vec::new();
421 header.encode_stream(&mut buf);
422 self.inner.write_all(&buf).await?;
423 Ok(())
424 }
425
426 /// Append a draft-18 subgroup object to the stream using the
427 /// stateful writer seeded from
428 /// [`FramedSendStream::write_subgroup_header`].
429 pub async fn write_subgroup_object(
430 &mut self,
431 object: &SubgroupObject,
432 ) -> Result<(), ConnectionError> {
433 let writer = self
434 .subgroup_io
435 .as_mut()
436 .ok_or(ConnectionError::DataStreamState("subgroup header not written yet"))?;
437 let mut buf = Vec::new();
438 writer.write_object(object, &mut buf)?;
439 self.inner.write_all(&buf).await?;
440 Ok(())
441 }
442
443 /// Append a fetch object to the stream.
444 ///
445 /// The fetch stream had a header writer and no object writer, so a caller
446 /// could open one and put nothing on it through this type. The subgroup
447 /// stream has had both since the writer was introduced.
448 ///
449 /// The declared length comes from the payload rather than from the caller's
450 /// field: a header that disagrees with the bytes beside it desynchronises
451 /// every object after it on the stream, and nothing downstream can recover.
452 ///
453 /// # Errors
454 ///
455 /// [`ConnectionError::Codec`] if the header's fields disagree with the
456 /// Serialization Flags that announce them, which the encoder refuses rather
457 /// than writing a frame its own reader cannot take apart.
458 pub async fn write_fetch_object(
459 &mut self,
460 header: &FetchObjectHeader,
461 payload: &[u8],
462 ) -> Result<(), ConnectionError> {
463 let mut header = header.clone();
464 header.payload_length = VarInt::from_usize(payload.len());
465 let mut buf = Vec::new();
466 header.encode(&mut buf)?;
467 buf.extend_from_slice(payload);
468 self.inner.write_all(&buf).await?;
469 Ok(())
470 }
471
472 /// Finish the stream (send FIN).
473 pub async fn finish(&mut self) -> Result<(), ConnectionError> {
474 self.inner.finish()?;
475 Ok(())
476 }
477
478 /// Reset the stream with `code`, telling the peer transmission was
479 /// abandoned rather than completed.
480 ///
481 /// Dropping a send stream sends a FIN, which claims the stream ended
482 /// cleanly; this is the only way to say the opposite. See
483 /// [`SendStream::reset`].
484 pub fn reset(&mut self, code: u64) -> Result<(), ConnectionError> {
485 self.inner.reset(code)?;
486 Ok(())
487 }
488
489 /// Returns the draft version this stream is framed for.
490 pub fn draft(&self) -> DraftVersion {
491 self.draft
492 }
493}
494
495/// What an Object Status makes of an object here.
496///
497/// Two answers where drafts 08 through 13 have three, and the missing one is
498/// the point: the end-of-track status settles where the track ended and is
499/// judged against nothing, because the rule about where one may be placed is
500/// not in this draft. `a_track_may_end_where_it_has_already_been.rs` asserts
501/// that acceptance.
502///
503/// Every other status is a statement about objects rather than one of them.
504fn object_role(status: Option<u64>) -> ObjectRole {
505 match status {
506 None | Some(0x0) => ObjectRole::Produced,
507 Some(0x4) => ObjectRole::EndsTrack(None),
508 _ => ObjectRole::Neither,
509 }
510}
511
512/// A framed reader for a recv stream. Handles MoQT varint-length decoding.
513pub struct FramedRecvStream {
514 inner: RecvStream,
515 buf: BytesMut,
516 draft: DraftVersion,
517 /// Stateful subgroup object reader.
518 subgroup_io: Option<SubgroupObjectReader>,
519 /// Stateful fetch object reader, holding the prior Object's Location and the
520 /// Group Order its deltas count in. Started by
521 /// [`FramedRecvStream::begin_fetch_objects`] rather than by the fetch
522 /// header, which does not carry the order.
523 fetch_io: Option<FetchObjectReader>,
524 /// The record this stream's objects are measured against, and the Group ID
525 /// its header named.
526 ///
527 /// One group for the whole stream: a subgroup header names it once and no
528 /// object header repeats it. `None` on a stream that was never given one -
529 /// a stream for an alias no live binding names, and every stream built
530 /// outside [`Connection::accept_subgroup_stream`].
531 tracking: Option<(TrackObjects, u64)>,
532}
533
534impl FramedRecvStream {
535 /// Create a new framed receive stream for the given draft version.
536 pub fn new(inner: RecvStream, draft: DraftVersion) -> Self {
537 Self {
538 inner,
539 buf: BytesMut::with_capacity(4096),
540 draft,
541 subgroup_io: None,
542 fetch_io: None,
543 tracking: None,
544 }
545 }
546
547 /// Get the transport-level stream ID.
548 pub fn stream_id(&self) -> u64 {
549 self.inner.stream_id()
550 }
551
552 /// Measure this stream's objects against `objects`, all of them in `group`.
553 fn measure_objects_against(&mut self, objects: TrackObjects, group: u64) {
554 self.tracking = Some((objects, group));
555 }
556
557 /// Judge one object this stream carried against where its track ended.
558 fn note_subgroup_object(
559 &self,
560 object: u64,
561 status: Option<u64>,
562 ) -> Result<(), ConnectionError> {
563 let Some((objects, group)) = &self.tracking else { return Ok(()) };
564 let at = ObjectLocation { group: *group, object };
565 objects.note_past_final(at, object_role(status)).map_err(|end| {
566 ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject {
567 alias: objects.alias(),
568 group: at.group,
569 object: at.object,
570 final_group: end.group,
571 final_object: end.object,
572 })
573 })
574 }
575
576 /// Read more data from the stream into the internal buffer.
577 async fn fill(&mut self) -> Result<bool, ConnectionError> {
578 let mut tmp = [0u8; 4096];
579 match self.inner.read(&mut tmp).await {
580 Ok(Some(n)) => {
581 self.buf.extend_from_slice(&tmp[..n]);
582 Ok(true)
583 }
584 Ok(None) => Ok(false),
585 Err(e) => Err(ConnectionError::Transport(e)),
586 }
587 }
588
589 /// Ensure at least `n` bytes are available in the buffer.
590 async fn ensure(&mut self, n: usize) -> Result<(), ConnectionError> {
591 while self.buf.len() < n {
592 if !self.fill().await? {
593 return Err(ConnectionError::UnexpectedEnd);
594 }
595 }
596 Ok(())
597 }
598
599 /// Read this stream's leading variable-length integer **without
600 /// consuming it**, and return its value.
601 ///
602 /// Every unidirectional MoQT stream on draft-18 opens with a varint
603 /// naming what it is (Section 3.4): 0x05 for FETCH_HEADER, 0x10-0x1D for
604 /// SUBGROUP_HEADER, and [`CONTROL_STREAM_TYPE`] for SETUP. Telling the
605 /// peer's control stream apart from a data stream means reading that
606 /// varint, and taking it off the transport would destroy it: the control
607 /// stream's type varint *is* the SETUP message's type field, so a stream
608 /// whose type had been stripped would no longer decode as a SETUP.
609 ///
610 /// Nothing is stripped. The bytes land in this reader's own buffer, and
611 /// every other method here — [`read_control`](Self::read_control),
612 /// [`read_subgroup_header`](Self::read_subgroup_header),
613 /// [`read_fetch_header`](Self::read_fetch_header) — decodes out of that
614 /// buffer and advances it only on a successful decode. A stream this was
615 /// called on is indistinguishable from one it was not, which is what
616 /// makes it safe to peek a stream and then hand it to whichever reader
617 /// the type turned out to call for.
618 ///
619 /// It reads, so it can block: a peer that opens a stream and then writes
620 /// nothing leaves this pending until a byte arrives or the stream ends.
621 ///
622 /// # Errors
623 ///
624 /// - [`ConnectionError::UnexpectedEnd`] if the stream ends before a whole
625 /// varint has arrived.
626 /// - [`ConnectionError::Transport`] if the peer reset the stream.
627 /// - [`ConnectionError::VarInt`] if the bytes are not a valid varint.
628 ///
629 /// Whatever did arrive stays in the buffer in every case.
630 pub async fn peek_stream_type(&mut self) -> Result<u64, ConnectionError> {
631 self.ensure(1).await?;
632 let type_len = self.draft.varint_len(self.buf[0]);
633 self.ensure(type_len).await?;
634 let mut cursor = &self.buf[..type_len];
635 Ok(self.draft.decode_varint(&mut cursor)?.into_inner())
636 }
637
638 /// Read a control message from the stream.
639 ///
640 /// When `capture_raw` is true, the returned tuple includes a clone of the
641 /// framed wire bytes (for observer emission). When false, the second
642 /// element is `None` and the payload clone is skipped.
643 pub async fn read_control(
644 &mut self,
645 capture_raw: bool,
646 ) -> Result<(AnyControlMessage, Option<Vec<u8>>), ConnectionError> {
647 // Read type ID varint
648 self.ensure(1).await?;
649 let type_len = self.draft.varint_len(self.buf[0]);
650 self.ensure(type_len).await?;
651
652 let mut cursor = &self.buf[..type_len];
653 let _type_id = self.draft.decode_varint(&mut cursor)?;
654
655 // Draft-18: 16-bit BE payload length
656 let (payload_len, len_field_size) = if self.draft.uses_fixed_length_framing() {
657 self.ensure(type_len + 2).await?;
658 let hi = self.buf[type_len] as usize;
659 let lo = self.buf[type_len + 1] as usize;
660 ((hi << 8) | lo, 2)
661 } else {
662 self.ensure(type_len + 1).await?;
663 let payload_len_start = type_len;
664 let payload_len_varint_len = self.draft.varint_len(self.buf[payload_len_start]);
665 self.ensure(type_len + payload_len_varint_len).await?;
666 let mut cursor = &self.buf[payload_len_start..type_len + payload_len_varint_len];
667 let payload_len = self.draft.decode_varint(&mut cursor)?.into_inner() as usize;
668 (payload_len, payload_len_varint_len)
669 };
670
671 // Read full payload
672 let total = type_len + len_field_size + payload_len;
673 self.ensure(total).await?;
674
675 // Capture raw bytes only if requested (observer attached).
676 let raw = capture_raw.then(|| self.buf[..total].to_vec());
677
678 // Now decode the whole message
679 let mut frame = &self.buf[..total];
680 let msg = AnyControlMessage::decode(self.draft, &mut frame)?;
681 self.buf.advance(total);
682 Ok((msg, raw))
683 }
684
685 /// Read a subgroup stream header. Also initializes the internal
686 /// delta-decoding state.
687 pub async fn read_subgroup_header(&mut self) -> Result<AnySubgroupHeader, ConnectionError> {
688 self.ensure(1).await?;
689 loop {
690 let mut cursor = &self.buf[..];
691 match AnySubgroupHeader::decode(self.draft, &mut cursor) {
692 Ok(header) => {
693 let consumed = self.buf.len() - cursor.remaining();
694 self.buf.advance(consumed);
695 // Clippy would rather see these two arms as an `if let`, and rustc rejects
696 // that in a single-draft build, where the pattern is irrefutable. Only a
697 // `match` satisfies both.
698 #[allow(clippy::single_match)]
699 match header {
700 AnySubgroupHeader::Draft18(ref header) => {
701 self.subgroup_io = Some(SubgroupObjectReader::new(header));
702 }
703 // Only this draft's header seeds the object reader. With draft 18 the only
704 // enabled draft `AnySubgroupHeader` has a single variant, the arm above is
705 // exhaustive and this one unreachable. Compiled in every configuration with
706 // the lint allowed, rather than gated on a `cfg` naming every other
707 // draft: such a list has to be edited in every draft module whenever a
708 // draft is added, and a copy that omits one leaves this match
709 // non-exhaustive.
710 #[allow(unreachable_patterns)]
711 _ => {}
712 }
713 return Ok(header);
714 }
715 Err(e) if e.is_incomplete() => {
716 if !self.fill().await? {
717 return Err(ConnectionError::UnexpectedEnd);
718 }
719 }
720 Err(e) => return Err(ConnectionError::Codec(e)),
721 }
722 }
723 }
724
725 /// Read a fetch response header.
726 pub async fn read_fetch_header(&mut self) -> Result<AnyFetchHeader, ConnectionError> {
727 self.ensure(1).await?;
728 loop {
729 let mut cursor = &self.buf[..];
730 match AnyFetchHeader::decode(self.draft, &mut cursor) {
731 Ok(header) => {
732 let consumed = self.buf.len() - cursor.remaining();
733 self.buf.advance(consumed);
734 return Ok(header);
735 }
736 Err(e) if e.is_incomplete() => {
737 if !self.fill().await? {
738 return Err(ConnectionError::UnexpectedEnd);
739 }
740 }
741 Err(e) => return Err(ConnectionError::Codec(e)),
742 }
743 }
744 }
745
746 /// Read the next draft-18 subgroup object from this stream using
747 /// the stateful reader seeded by
748 /// [`FramedRecvStream::read_subgroup_header`].
749 ///
750 /// Errors with [`ConnectionError::PropertiesOnNonNormalStatus`] on an
751 /// Object that carries properties on a status other than Normal, which
752 /// draft-18 Section 11.2.1.2 answers with a session close. The Object is
753 /// consumed from the stream before the check, so the reader stays in step
754 /// with the wire and a caller that reports the violation and reads on sees
755 /// the following Object rather than a re-parse of this one.
756 pub async fn read_subgroup_object(&mut self) -> Result<SubgroupObject, ConnectionError> {
757 if self.subgroup_io.is_none() {
758 return Err(ConnectionError::DataStreamState("subgroup header not read yet"));
759 }
760 loop {
761 let reader = self.subgroup_io.as_mut().unwrap();
762 let mut probe = reader.clone();
763 let mut cursor = &self.buf[..];
764 match probe.read_object(&mut cursor) {
765 Ok(obj) => {
766 let consumed = self.buf.len() - cursor.remaining();
767 self.buf.advance(consumed);
768 *reader = probe;
769 if !obj.properties_permitted() {
770 return Err(ConnectionError::PropertiesOnNonNormalStatus {
771 object_id: obj.object_id.into_inner(),
772 properties_len: obj.extension_headers.len(),
773 status: obj.status(),
774 });
775 }
776 self.note_subgroup_object(
777 obj.object_id.into_inner(),
778 obj.object_status.map(|s| s as u64),
779 )?;
780 return Ok(obj);
781 }
782 Err(e) if e.is_incomplete() => {
783 if !self.fill().await? {
784 return Err(ConnectionError::UnexpectedEnd);
785 }
786 }
787 // The codec refuses properties on a non-Normal status as
788 // `InvalidField`, one of many readings of that variant. Its
789 // framing-only reader accepts them, which tells this case apart
790 // from the others and keeps the stream in step.
791 Err(CodecError::InvalidField) => {
792 let mut framing = reader.clone();
793 let mut cursor = &self.buf[..];
794 if let Ok(meta) = framing.read_object_meta(&mut cursor) {
795 let status = meta
796 .status
797 .and_then(moqtap_codec::draft18::types::ObjectStatus::from_u64);
798 if let Some(status) = status.filter(|s| {
799 meta.extension_headers_len > 0
800 && *s != moqtap_codec::draft18::types::ObjectStatus::Normal
801 }) {
802 let consumed = self.buf.len() - cursor.remaining();
803 self.buf.advance(consumed);
804 *reader = framing;
805 return Err(ConnectionError::PropertiesOnNonNormalStatus {
806 object_id: meta.object_id,
807 properties_len: meta.extension_headers_len as usize,
808 status,
809 });
810 }
811 }
812 return Err(ConnectionError::Codec(CodecError::InvalidField));
813 }
814 Err(e) => return Err(ConnectionError::Codec(e)),
815 }
816 }
817 }
818
819 /// Read the next draft-18 fetch header from this stream.
820 pub async fn read_fetch_stream_header(&mut self) -> Result<FetchHeader, ConnectionError> {
821 loop {
822 let mut cursor = &self.buf[..];
823 match FetchHeader::decode(&mut cursor) {
824 Ok(hdr) => {
825 let consumed = self.buf.len() - cursor.remaining();
826 self.buf.advance(consumed);
827 return Ok(hdr);
828 }
829 Err(e) if e.is_incomplete() => {
830 if !self.fill().await? {
831 return Err(ConnectionError::UnexpectedEnd);
832 }
833 }
834 Err(e) => return Err(ConnectionError::Codec(e)),
835 }
836 }
837 }
838
839 /// Stop accepting data on this stream with `code` as the `STOP_SENDING`
840 /// application error code, discarding anything unread.
841 ///
842 /// Dropping a receive stream also stops it, but with a hard-coded 0. See
843 /// [`RecvStream::stop`].
844 pub fn stop(&mut self, code: u64) -> Result<(), ConnectionError> {
845 self.inner.stop(code)?;
846 Ok(())
847 }
848
849 /// Wait for the peer to reset this stream, consuming nothing.
850 ///
851 /// See [`RecvStream::received_reset`] for what `Ok(None)` means and why a
852 /// caller must not re-poll after it.
853 pub async fn received_reset(&mut self) -> Result<Option<u64>, ConnectionError> {
854 Ok(self.inner.received_reset().await?)
855 }
856
857 /// Start reading the objects of a fetch stream whose Groups arrive in
858 /// `group_order`.
859 ///
860 /// Section 11.4.4.1 makes a Group ID Delta count downward under Descending
861 /// and upward under Ascending, so the same bytes are two different
862 /// Locations and nothing on the data stream says which. The order is the
863 /// one the **request** asked for — Section 10.12.3: "The publisher
864 /// responding to a FETCH is responsible for delivering all available
865 /// Objects in the requested range in the requested order" — carried by the
866 /// GROUP_ORDER parameter on the FETCH, or by its absence, which Section
867 /// 10.2.8 reads as Ascending. Either way it is a control message this
868 /// stream never sees. Draft-19 encodes both IDs the same way and needs the
869 /// same call; drafts 15, 16 and 17 resolve their fetch objects without an
870 /// order, because none of those drafts encodes an ID as a difference.
871 ///
872 /// Call it after [`FramedRecvStream::read_fetch_header`] and before the
873 /// first [`FramedRecvStream::read_fetch_object`]; calling it again restarts
874 /// the running state, which is what a second fetch stream on the same
875 /// connection would want and what the middle of one would not.
876 pub fn begin_fetch_objects(&mut self, group_order: GroupOrder) {
877 self.fetch_io = Some(FetchObjectReader::new(group_order));
878 }
879
880 /// Read the next draft-18 fetch object and its payload.
881 ///
882 /// The mirror of [`FramedSendStream::write_fetch_object`]. What comes back
883 /// is a [`FetchObject`] rather than a header: draft-18's reader resolves the
884 /// Location as it reads, and the resolved Group ID, Subgroup ID, Object ID
885 /// and Priority are the fields a subscriber acts on. The header it decoded
886 /// from is inside it.
887 ///
888 /// The payload comes back with it because `payload_length` says how many
889 /// bytes follow, and a reader that takes the wrong number of them
890 /// desynchronises every later object on the stream.
891 ///
892 /// # Errors
893 ///
894 /// [`ConnectionError::DataStreamState`] when
895 /// [`FramedRecvStream::begin_fetch_objects`] has not been called,
896 /// [`ConnectionError::UnexpectedEnd`] when the stream ends inside the header
897 /// or inside the payload it declared, and [`ConnectionError::Codec`] on
898 /// every rule Section 11.4.4.1 states — a first Object that inherits, a
899 /// delta that runs off either end of the space, or an Object ID above
900 /// 2^64-1.
901 ///
902 /// A Prior Group ID Gap or Prior Object ID Gap fault the stream shows is
903 /// returned as [`CodecError::MalformedTrack`], and a property block that
904 /// does not parse as [`CodecError::MalformedObjectProperties`];
905 /// [`Connection::requests_to_cancel_for_fetch_stream`] names the requests to
906 /// cancel.
907 pub async fn read_fetch_object(&mut self) -> Result<(FetchObject, Vec<u8>), ConnectionError> {
908 if self.fetch_io.is_none() {
909 return Err(ConnectionError::DataStreamState("fetch object reader not started"));
910 }
911 let object = loop {
912 let reader = self.fetch_io.as_mut().unwrap();
913 // Advanced on a probe and committed only once the whole header was
914 // there to read: a reader advanced by a short read would resolve
915 // the next Object against a half-read one.
916 let mut probe = reader.clone();
917 let mut cursor = &self.buf[..];
918 match probe.read_object_header(&mut cursor) {
919 Ok(object) => {
920 let consumed = self.buf.len() - cursor.remaining();
921 self.buf.advance(consumed);
922 *reader = probe;
923 break object;
924 }
925 Err(e) if e.is_incomplete() => {
926 if !self.fill().await? {
927 return Err(ConnectionError::UnexpectedEnd);
928 }
929 }
930 Err(e) => return Err(ConnectionError::Codec(e)),
931 }
932 };
933 let payload = self.read_object_payload(&object.header.payload_length).await?;
934 Ok((object, payload))
935 }
936
937 /// Take the `length` payload bytes that follow a fetch object's header.
938 ///
939 /// Separate from the header read because the header is decoded from a probe
940 /// cursor that may have to be retried after a fill, and the payload is a
941 /// flat byte count that never is.
942 async fn read_object_payload(&mut self, length: &VarInt) -> Result<Vec<u8>, ConnectionError> {
943 let length = length.into_inner() as usize;
944 self.ensure(length).await?;
945 let payload = self.buf[..length].to_vec();
946 self.buf.advance(length);
947 Ok(payload)
948 }
949
950 /// Returns the draft version this stream is framed for.
951 pub fn draft(&self) -> DraftVersion {
952 self.draft
953 }
954}
955
956/// Which of the seven message types draft-18 Section 3.3 lets a bidirectional
957/// stream begin with opened a request stream.
958///
959/// Draft-18 Section 3.3: "A request stream begins with one of these seven
960/// message types: TRACK_STATUS, SUBSCRIBE, PUBLISH, FETCH, PUBLISH_NAMESPACE,
961/// SUBSCRIBE_NAMESPACE, and SUBSCRIBE_TRACKS. Bidirectional streams MUST NOT
962/// begin with any other message type unless negotiated."
963///
964/// The set is per draft and is not stable across drafts. Draft-17 named six:
965/// draft-18 added SUBSCRIBE_TRACKS and renumbered SUBSCRIBE_NAMESPACE from
966/// 0x11 to 0x50. A per-draft enum is what makes it impossible to name
967/// draft-18's seventh kind in draft-17 code, or to forget it here.
968#[derive(Debug, Clone, Copy, PartialEq, Eq)]
969pub enum RequestKind {
970 /// TRACK_STATUS, 0x0D.
971 TrackStatus,
972 /// SUBSCRIBE, 0x03.
973 Subscribe,
974 /// PUBLISH, 0x1D.
975 Publish,
976 /// FETCH, 0x16 — standalone or joining.
977 Fetch,
978 /// PUBLISH_NAMESPACE, 0x06.
979 PublishNamespace,
980 /// SUBSCRIBE_NAMESPACE, 0x50 on this draft — 0x11 on draft-17.
981 SubscribeNamespace,
982 /// SUBSCRIBE_TRACKS, 0x51, new in draft-18.
983 SubscribeTracks,
984}
985
986impl RequestKind {
987 /// The message type a request stream of this kind begins with.
988 pub const fn message_type(self) -> MessageType {
989 match self {
990 RequestKind::TrackStatus => MessageType::TrackStatus,
991 RequestKind::Subscribe => MessageType::Subscribe,
992 RequestKind::Publish => MessageType::Publish,
993 RequestKind::Fetch => MessageType::Fetch,
994 RequestKind::PublishNamespace => MessageType::PublishNamespace,
995 RequestKind::SubscribeNamespace => MessageType::SubscribeNamespace,
996 RequestKind::SubscribeTracks => MessageType::SubscribeTracks,
997 }
998 }
999
1000 /// The kind of request stream `ty` opens, or `None` when it opens none.
1001 ///
1002 /// The inverse of [`message_type`](Self::message_type) and the classifier
1003 /// the accept path runs on the first message of a bidirectional stream the
1004 /// peer opened. `None` is the PROTOCOL_VIOLATION case of draft-18
1005 /// Section 3.3.
1006 ///
1007 /// Like [`starts_a_request_stream`] the match is exhaustive over
1008 /// [`MessageType`] with **no wildcard arm**, so a message type added in a
1009 /// later draft stops this compiling until someone classifies it; the unit
1010 /// test below holds the two functions to the same answer for every
1011 /// assigned type, so neither can drift from the other.
1012 pub const fn from_message_type(ty: MessageType) -> Option<RequestKind> {
1013 match ty {
1014 MessageType::TrackStatus => Some(RequestKind::TrackStatus),
1015 MessageType::Subscribe => Some(RequestKind::Subscribe),
1016 MessageType::Publish => Some(RequestKind::Publish),
1017 MessageType::Fetch => Some(RequestKind::Fetch),
1018 MessageType::PublishNamespace => Some(RequestKind::PublishNamespace),
1019 MessageType::SubscribeNamespace => Some(RequestKind::SubscribeNamespace),
1020 MessageType::SubscribeTracks => Some(RequestKind::SubscribeTracks),
1021 MessageType::Setup
1022 | MessageType::GoAway
1023 | MessageType::Namespace
1024 | MessageType::NamespaceDone
1025 | MessageType::PublishBlocked
1026 | MessageType::RequestUpdate
1027 | MessageType::SubscribeOk
1028 | MessageType::RequestOk
1029 | MessageType::RequestError
1030 | MessageType::FetchOk
1031 | MessageType::PublishDone => None,
1032 }
1033 }
1034}
1035
1036/// Which side opened the bidirectional stream a request travels on.
1037///
1038/// Draft-18 Section 3.3 gives every request a bidirectional stream, and either
1039/// endpoint may open one. The two directions are not symmetric — one side owes
1040/// a response and the other is waiting for it — so a [`RequestStream`] carries
1041/// this to say which side of that it is on.
1042#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1043pub enum RequestOrigin {
1044 /// This endpoint opened the stream and wrote the request on it. What comes
1045 /// back is a response, and dropping the handle cancels the request.
1046 Local,
1047 /// The peer opened the stream; this endpoint owes it a response. What
1048 /// comes back is a follow-up to the peer's request, never a response, and
1049 /// dropping the handle abandons a request that was asked of us.
1050 Peer,
1051}
1052/// Whether draft-18 Table 5 places this message on a request stream that is
1053/// already open.
1054///
1055/// All four of the messages Table 5 marks "Request" without their beginning one:
1056/// REQUEST_UPDATE modifies the request its stream carries (Section 10.9),
1057/// NAMESPACE and NAMESPACE_DONE report namespaces on the SUBSCRIBE_NAMESPACE
1058/// stream that asked for them (Section 10.16 and Section 10.17), and
1059/// PUBLISH_BLOCKED names a track on the SUBSCRIBE_TRACKS stream that asked for
1060/// it (Section 10.20).
1061///
1062/// `Endpoint::receive_message` already closes the session over all four when
1063/// they arrive on the control stream. Without this the client would write on the
1064/// control stream exactly what its own peer half refuses to read there.
1065fn belongs_on_a_request_stream(ty: MessageType) -> bool {
1066 matches!(
1067 ty,
1068 MessageType::RequestUpdate
1069 | MessageType::Namespace
1070 | MessageType::NamespaceDone
1071 | MessageType::PublishBlocked
1072 )
1073}
1074
1075/// Whether `ty` is one of the seven message types draft-18 Section 3.3 lets a
1076/// bidirectional stream begin with.
1077///
1078/// The match is exhaustive over [`MessageType`] and deliberately has **no
1079/// wildcard arm**. That is the drift guard: `MessageType` is not
1080/// `#[non_exhaustive]`, so the day a draft gains a message type this stops
1081/// compiling until someone says here whether the new type opens a request
1082/// stream. A wildcard would silently answer "no" for it — which is exactly
1083/// what would have happened to SUBSCRIBE_TRACKS, the type draft-18 added.
1084///
1085/// Classification is over the typed `MessageType`, never over a raw `u64`,
1086/// because the number alone does not say which registry it came from: on
1087/// draft-18, 0x50 is SUBSCRIBE_NAMESPACE as a control message type and also
1088/// the start of a SUBGROUP_HEADER range as a unidirectional stream type.
1089pub const fn starts_a_request_stream(ty: MessageType) -> bool {
1090 match ty {
1091 // The seven that begin a request stream.
1092 MessageType::TrackStatus
1093 | MessageType::Subscribe
1094 | MessageType::Publish
1095 | MessageType::Fetch
1096 | MessageType::PublishNamespace
1097 | MessageType::SubscribeNamespace
1098 | MessageType::SubscribeTracks => true,
1099 // These travel on a request stream too — draft-18's Table 5 marks
1100 // NAMESPACE, NAMESPACE_DONE, PUBLISH_BLOCKED and REQUEST_UPDATE
1101 // "Request", and GOAWAY "Control, Request" — but none of them may
1102 // *begin* one, which is the only question asked here. Table 5 marks
1103 // just the seven above "Request, First". SETUP is the control stream's
1104 // own type varint and belongs to no bidirectional stream at all.
1105 MessageType::Setup
1106 | MessageType::GoAway
1107 | MessageType::Namespace
1108 | MessageType::NamespaceDone
1109 | MessageType::PublishBlocked
1110 | MessageType::RequestUpdate => false,
1111 // Responses. They cannot begin a stream: they arrive on the request
1112 // stream their request opened, which is why they carry no request id
1113 // of their own on this draft. Draft-18 has one fewer than draft-17
1114 // because PUBLISH_OK became an alias of REQUEST_OK, 0x07.
1115 MessageType::SubscribeOk
1116 | MessageType::RequestOk
1117 | MessageType::RequestError
1118 | MessageType::FetchOk
1119 | MessageType::PublishDone => false,
1120 }
1121}
1122
1123/// One request and its answer, on a bidirectional stream of their own.
1124///
1125/// Draft-18 Section 3.3 carries forward draft-17's move of requests off the
1126/// control plane: each request is the first message on a bidirectional stream
1127/// it opens, and the response comes back on that same stream. Responses carry
1128/// no request id on this draft — **the stream is the correlation**, which is
1129/// why this handle exists and why a bare request id is no longer enough to
1130/// find an answer.
1131///
1132/// # Reading and writing go through the connection
1133///
1134/// This handle owns both halves of the stream but not the session, so the
1135/// endpoint state machine and the observer stay where they were. Read a
1136/// response with [`Connection::recv_on_request_stream`], write a follow-up with
1137/// [`Connection::send_on_request_stream`], and cancel with
1138/// [`Connection::cancel_request_stream`].
1139///
1140/// [`cancel`](Self::cancel) and [`peer_cancelled`](Self::peer_cancelled) are on
1141/// the handle because they touch the stream and nothing else, and [`Drop`]
1142/// needs the first of them. Neither moves the endpoint's record of the request,
1143/// which is why the connection carries a pair of its own.
1144///
1145/// # Dropping this cancels the request
1146///
1147/// A dropped handle resets the send half and sends `STOP_SENDING` on the
1148/// receive half, unless the stream was already cancelled or finished. Letting
1149/// the default drop stand would send a FIN instead, telling the peer the
1150/// request ended *cleanly* when it was abandoned. Which code goes on the wire
1151/// depends on who opened the stream — see [`Drop`].
1152///
1153/// The consequence is sharp and worth stating: a live subscription's request
1154/// stream must be **held for the subscription's life**, because PUBLISH_DONE
1155/// arrives on it. Keeping only [`request_id`](Self::request_id) and letting
1156/// the handle fall out of scope cancels the subscription.
1157///
1158/// What a drop cannot do is say so at the endpoint. [`Drop`] holds the stream
1159/// and not the session, so the request stays where it was in the endpoint's
1160/// record while the stream it travelled on is gone. Call
1161/// [`Connection::cancel_request_stream`] wherever that record matters.
1162///
1163/// # Which side opened it changes what this handle does
1164///
1165/// [`origin`](Self::origin) says whether this endpoint opened the stream or
1166/// accepted it, and three behaviours turn on it: reads dispatch as responses
1167/// or as follow-ups to the peer's request, the `respond_*` helpers refuse a
1168/// stream this endpoint opened, and [`Drop`] resets with
1169/// [`REQUEST_UNANSWERED`] rather than [`REQUEST_CANCELLED`]. Everything else —
1170/// [`cancel`](Self::cancel), [`peer_cancelled`](Self::peer_cancelled),
1171/// [`finish`](Self::finish),
1172/// [`Connection::send_on_request_stream`] — is the same in both directions.
1173/// Draft-18 Section 3.3.2 is explicit that a cancel is available to both:
1174/// "Senders cancel requests if the response is no longer of interest;
1175/// Receivers cancel requests if they are unable to or choose not to respond."
1176///
1177/// All fields are private so the shape can grow without breaking callers.
1178#[must_use = "dropping a request stream cancels the request; hold it until the response arrives"]
1179pub struct RequestStream {
1180 send: FramedSendStream,
1181 recv: FramedRecvStream,
1182 request_id: VarInt,
1183 kind: RequestKind,
1184 /// The unidirectional stream this request's objects are being served on.
1185 ///
1186 /// Only a FETCH has one, and only once the caller has opened it through
1187 /// [`Connection::open_fetch_stream_on`]. Held here rather than in a table
1188 /// on the connection because the request stream is already the thing that
1189 /// knows what this request still owes, and because a handle kept beside
1190 /// the request cannot outlive it.
1191 fetch_data: Option<FramedSendStream>,
1192 draft: DraftVersion,
1193 stream_id: u64,
1194 cancelled: bool,
1195 finished: bool,
1196 origin: RequestOrigin,
1197 /// Whether a `respond_*` helper has written a response on this stream.
1198 /// True on a [`RequestOrigin::Peer`] stream from the response written on
1199 /// it, and on a [`RequestOrigin::Local`] one from the answer to an update
1200 /// the peer sent, which is the only response this endpoint writes on a
1201 /// stream of its own.
1202 responded: bool,
1203}
1204
1205impl RequestStream {
1206 /// The request id the endpoint allocated for this request.
1207 ///
1208 /// Useful for logging and for endpoint calls that still take one. It is
1209 /// not enough to find the response: draft-18 responses carry no request
1210 /// id, so only this stream identifies them.
1211 pub fn request_id(&self) -> VarInt {
1212 self.request_id
1213 }
1214
1215 /// Which of the seven request types opened this stream.
1216 pub fn kind(&self) -> RequestKind {
1217 self.kind
1218 }
1219
1220 /// The transport-level stream identifier, the same one
1221 /// [`ClientEvent::StreamOpened`] reports for data streams.
1222 pub fn stream_id(&self) -> u64 {
1223 self.stream_id
1224 }
1225
1226 /// The draft version this stream is framed for.
1227 pub fn draft(&self) -> DraftVersion {
1228 self.draft
1229 }
1230
1231 /// Which side opened this stream.
1232 ///
1233 /// [`RequestOrigin::Peer`] means this endpoint owes a response and the
1234 /// `respond_*` helpers apply; [`RequestOrigin::Local`] means it is waiting
1235 /// for one.
1236 pub fn origin(&self) -> RequestOrigin {
1237 self.origin
1238 }
1239
1240 /// Whether a response has been written on this stream by one of the
1241 /// `respond_*` helpers.
1242 ///
1243 /// On a [`RequestOrigin::Local`] stream this says an update the peer sent
1244 /// was answered here, not that the request itself was: that one is
1245 /// answered by the peer.
1246 pub fn responded(&self) -> bool {
1247 self.responded
1248 }
1249
1250 /// The stream this request's objects are being served on, if one is open.
1251 ///
1252 /// Only a FETCH answered through
1253 /// [`Connection::open_fetch_stream_on`](Connection::open_fetch_stream_on)
1254 /// has one. Writing objects goes through this rather than through a handle
1255 /// the caller keeps, so that the connection can still reach the stream
1256 /// when a rule says to reset it.
1257 pub fn fetch_data(&mut self) -> Option<&mut FramedSendStream> {
1258 self.fetch_data.as_mut()
1259 }
1260
1261 /// Reset the fetch data stream, if one was opened, and forget it.
1262 ///
1263 /// The sentence that requires this names no error code, so the code comes
1264 /// from the registry rather than from here: CANCELLED is "the stream was
1265 /// cancelled by either endpoint", which is what a publisher abandoning the
1266 /// objects it was serving has done.
1267 fn reset_fetch_data(&mut self) {
1268 if let Some(mut framed) = self.fetch_data.take() {
1269 let _ = framed.reset(StreamResetErrorCode::Cancelled as u64);
1270 }
1271 }
1272
1273 /// Whether [`cancel`](Self::cancel) has already run on this handle.
1274 ///
1275 /// Says nothing about the peer: a peer reset is learned from
1276 /// [`peer_cancelled`](Self::peer_cancelled) or from the next read.
1277 pub fn is_cancelled(&self) -> bool {
1278 self.cancelled
1279 }
1280
1281 /// Cancel the request by resetting the stream, handing the peer `code`.
1282 ///
1283 /// Draft-18 has no UNSUBSCRIBE and no FETCH_CANCEL: resetting the request
1284 /// stream is how a request is withdrawn. Both halves are shut — a QUIC
1285 /// bidirectional stream has two independent halves, so resetting only the
1286 /// send half would leave the peer free to keep writing a response nobody
1287 /// will read. The send half is reset with `code` and the receive half is
1288 /// stopped with the same value.
1289 /// [`REQUEST_CANCELLED`] is the ordinary choice. The parameter is a plain
1290 /// `u64` rather than a draft enum because the registry is named differently
1291 /// across these drafts — draft-17 Section 14.5.4's "Data Stream Reset Error
1292 /// Codes" against draft-18's "Stream Reset Error Codes" — and a caller who
1293 /// wants a typed value has [`StreamResetErrorCode::as_u64`].
1294 ///
1295 /// **This is the stream and nothing else.** The endpoint's record of the
1296 /// request does not move, so a response already in flight is still accepted
1297 /// after this returns. [`Connection::cancel_request_stream`] does both and
1298 /// is what a caller holding a connection should reach for; this stays
1299 /// because [`Drop`] has no connection to reach.
1300 ///
1301 /// Idempotent, and it retires the [`Drop`] behaviour: a cancelled handle
1302 /// does nothing further when it goes out of scope. Errors from a stream
1303 /// that was already reset or stopped are swallowed for the same reason —
1304 /// the request is cancelled either way.
1305 ///
1306 /// # Errors
1307 ///
1308 /// [`ConnectionError::Transport`] carrying [`TransportError::Write`] if
1309 /// `code` is outside the QUIC varint range (`0..2^62`). Nothing is sent
1310 /// in that case, and the handle is *not* marked cancelled, so a caller
1311 /// can retry with a representable code.
1312 pub fn cancel(&mut self, code: u64) -> Result<(), ConnectionError> {
1313 if self.cancelled {
1314 return Ok(());
1315 }
1316 // Reject an unrepresentable code before either half is touched, so a
1317 // failed call leaves the stream exactly as it was.
1318 if code > MAX_QUIC_VARINT {
1319 return Err(ConnectionError::Transport(TransportError::Write(format!(
1320 "error code {code} exceeds the varint range"
1321 ))));
1322 }
1323 self.cancelled = true;
1324 // Already-finished or already-reset halves report StreamClosed; the
1325 // request ends up cancelled regardless, so neither is worth raising.
1326 let _ = self.send.reset(code);
1327 let _ = self.recv.stop(code);
1328 Ok(())
1329 }
1330
1331 /// Wait for the peer to cancel this request, consuming nothing.
1332 ///
1333 /// A caller applying backpressure is deliberately not calling
1334 /// [`Connection::recv_on_request_stream`], which is the only other place a
1335 /// peer reset surfaces — so without this the abandonment goes unobserved
1336 /// for as long as the backpressure lasts. This grants no flow-control
1337 /// credit and is cancel-safe.
1338 ///
1339 /// Returns `Ok(Some(code))` with the peer's application error code, or
1340 /// `Ok(None)` meaning **no reset is observable, now or ever — stop
1341 /// asking**. A caller that re-polls after `Ok(None)` spins.
1342 ///
1343 /// Like [`cancel`](Self::cancel), this records nothing at the endpoint.
1344 /// [`Connection::peer_cancelled_on_request_stream`] is the same wait with
1345 /// the record attached.
1346 ///
1347 /// On WebTransport this always answers `Ok(None)`: `wtransport` exposes no
1348 /// reset-only observable, so a WebTransport caller learns of a peer cancel
1349 /// on its next read and not before.
1350 pub async fn peer_cancelled(&mut self) -> Result<Option<u64>, ConnectionError> {
1351 self.recv.received_reset().await
1352 }
1353
1354 /// Finish the send half cleanly, leaving the receive half open.
1355 ///
1356 /// Whether a requester may FIN before its response arrives is not settled
1357 /// by anything this implementation can check, so no request helper calls
1358 /// this and the default is to leave the send half open for the request's
1359 /// life. It is offered for a caller that knows its peer.
1360 ///
1361 /// A finished handle, like a cancelled one, does nothing further on
1362 /// [`Drop`].
1363 pub async fn finish(&mut self) -> Result<(), ConnectionError> {
1364 if self.finished || self.cancelled {
1365 return Ok(());
1366 }
1367 self.finished = true;
1368 self.send.finish().await
1369 }
1370}
1371
1372impl Drop for RequestStream {
1373 /// Reset the request unless it was already cancelled or finished.
1374 ///
1375 /// See the type-level note: the default drop would FIN the send half,
1376 /// which claims a clean end for a request the caller walked away from.
1377 ///
1378 /// The code says which walking away it was. A stream this endpoint opened
1379 /// is cancelled — [`REQUEST_CANCELLED`] — which is the requester act
1380 /// draft-18 Section 3.3.2 describes. A stream the peer opened is reset
1381 /// with [`REQUEST_UNANSWERED`] whether or not a response was already
1382 /// written: before one, the request was never served; after one, the
1383 /// obligations that follow it are still outstanding.
1384 fn drop(&mut self) {
1385 if self.cancelled || self.finished {
1386 return;
1387 }
1388 let code = match self.origin {
1389 RequestOrigin::Local => REQUEST_CANCELLED,
1390 RequestOrigin::Peer => REQUEST_UNANSWERED,
1391 };
1392 let _ = self.send.reset(code);
1393 let _ = self.recv.stop(code);
1394 }
1395}
1396
1397/// Holds a peer-opened stream pair while its first message is being read, and
1398/// puts it back on the connection's queue if that read is abandoned.
1399///
1400/// [`Connection::accept_request_stream`] awaits a whole control message, and a
1401/// caller may drop that future — a `select!` against a shutdown signal is the
1402/// ordinary reason. Without this the stream, and every byte already read off
1403/// it into the reader's buffer, would go with the future: the peer would see a
1404/// request stream reset for no reason it could act on.
1405///
1406/// [`Drop`] is the only place this can run, because a cancelled future is
1407/// never polled again. Every path that finishes — success or error — takes the
1408/// pair out first, so a pair still present when this drops was cancelled.
1409struct PendingInbound<'a> {
1410 pair: Option<(FramedSendStream, FramedRecvStream)>,
1411 queue: &'a Mutex<VecDeque<(FramedSendStream, FramedRecvStream)>>,
1412}
1413
1414impl Drop for PendingInbound<'_> {
1415 fn drop(&mut self) {
1416 if let Some(pair) = self.pair.take() {
1417 // Front, not back: this stream arrived before anything still
1418 // queued behind it, and a partially read message must not be
1419 // handed out after a stream that arrived later.
1420 self.queue.lock().unwrap_or_else(|p| p.into_inner()).push_front(pair);
1421 }
1422 }
1423}
1424
1425/// The largest value a QUIC application error code can carry, `2^62 - 1`.
1426///
1427/// Checked by [`RequestStream::cancel`] before either half of the stream is
1428/// touched, so an unrepresentable code cannot half-cancel a request.
1429const MAX_QUIC_VARINT: u64 = (1u64 << 62) - 1;
1430
1431/// A live MoQT connection over QUIC or WebTransport, combining the endpoint
1432/// state machine with actual network I/O.
1433pub struct Connection {
1434 transport: Transport,
1435 endpoint: Endpoint,
1436 draft: DraftVersion,
1437 control_send: Option<FramedSendStream>,
1438 control_recv: Option<FramedRecvStream>,
1439 observer: Option<Box<dyn ConnectionObserver>>,
1440 /// Setup events buffered during `connect()` and replayed when an
1441 /// observer attaches via `set_observer` — without this, an observer
1442 /// attached after `connect` returns would never see the handshake.
1443 pending_events: Vec<ClientEvent>,
1444 /// The server's half of the setup handshake, kept whole.
1445 ///
1446 /// The endpoint acts on the parameters it recognises and retains none of
1447 /// them, and which parameters a server sends — in what order, with what
1448 /// values — is the sharpest thing a session says about the implementation
1449 /// behind it.
1450 server_setup: AnyControlMessage,
1451 /// The framed wire bytes of [`Self::server_setup`].
1452 server_setup_raw: Option<Vec<u8>>,
1453 /// Unidirectional streams accepted while `connect` was looking for the
1454 /// peer's control stream, in arrival order.
1455 ///
1456 /// Data streams are allowed to arrive before the control streams on this
1457 /// draft, so the search cannot assume the first unidirectional stream is
1458 /// the control one — and dropping the ones that are not would silently
1459 /// lose objects the peer already sent.
1460 /// [`accept_subgroup_stream`](Connection::accept_subgroup_stream) empties
1461 /// this before it accepts anything new.
1462 ///
1463 /// Behind a mutex because that method takes `&self`. The lock is only
1464 /// ever held for a `pop_front`, never across an await.
1465 deferred_uni: Mutex<VecDeque<FramedRecvStream>>,
1466 /// Bidirectional streams the peer opened that
1467 /// [`accept_request_stream`](Connection::accept_request_stream) took off
1468 /// the transport but did not finish reading a first message from, because
1469 /// its future was dropped. In arrival order.
1470 ///
1471 /// Without this a caller could not put `accept_request_stream` in a
1472 /// `select!` at all: losing the race would lose a stream the peer had
1473 /// already opened and, with it, whatever of the request had arrived.
1474 /// [`accept_request_stream`](Connection::accept_request_stream) empties
1475 /// this before it accepts anything new.
1476 ///
1477 /// Behind a mutex for the same reason `deferred_uni` is: the lock is only
1478 /// ever held for a push or a pop, never across an await.
1479 pending_inbound: Mutex<VecDeque<(FramedSendStream, FramedRecvStream)>>,
1480}
1481
1482impl Connection {
1483 /// Connect to a MoQT server as a client.
1484 ///
1485 /// Establishes a QUIC or WebTransport connection (based on
1486 /// `config.transport`), brings up the control plane, performs the SETUP
1487 /// handshake, and returns a ready-to-use connection.
1488 ///
1489 /// # The control plane is a pair of unidirectional streams
1490 ///
1491 /// Draft-18 Section 3.3: "MOQT uses a pair of unidirectional streams for
1492 /// creating the session and exchanging control messages. Each peer opens
1493 /// one control stream beginning with a SETUP message." So each direction
1494 /// is a separate stream opened by the peer that writes on it. This opens
1495 /// one with `open_uni` and writes SETUP on it, then finds the peer's by
1496 /// accepting unidirectional streams until one leads with
1497 /// [`CONTROL_STREAM_TYPE`].
1498 ///
1499 /// Nothing is written ahead of the SETUP: 0x2F00 is both the SETUP
1500 /// message type and the unidirectional stream type for a control stream,
1501 /// so the message's own first field is the stream header. See
1502 /// [`CONTROL_STREAM_TYPE`].
1503 ///
1504 /// A bidirectional stream is *not* the control stream here — the same
1505 /// section makes it a request stream, one that begins with TRACK_STATUS,
1506 /// SUBSCRIBE, PUBLISH, FETCH, PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE or
1507 /// SUBSCRIBE_TRACKS: "Bidirectional streams MUST NOT begin with any other
1508 /// message type unless negotiated. If they do, the peer MUST close the
1509 /// Session with a PROTOCOL_VIOLATION." A SETUP written on a bidirectional
1510 /// stream is exactly that case, so a peer that enforces the topology
1511 /// answers it by closing the session.
1512 ///
1513 /// # Unidirectional streams that arrive before the peer's control stream
1514 ///
1515 /// They are kept, not dropped. Section 3.3 expects them: "Unidirectional
1516 /// streams containing Objects or bidirectional stream(s) beginning with a
1517 /// request message could arrive prior to the control streams, in which
1518 /// case the data SHOULD be buffered until both control streams arrive and
1519 /// setup is complete." Each such stream is set aside and handed to
1520 /// [`accept_subgroup_stream`](Self::accept_subgroup_stream) in arrival
1521 /// order, ahead of any newly accepted stream. Only the leading type
1522 /// varint is read from them here; the rest stays on the transport, unread
1523 /// and still flow-controlled, so nothing is buffered in this process
1524 /// beyond those few bytes.
1525 ///
1526 /// One limit worth knowing: the search waits for each stream's type
1527 /// varint in turn, so a peer that opens a unidirectional stream and then
1528 /// writes nothing on it stalls the handshake behind that stream.
1529 pub async fn connect(addr: &str, config: ClientConfig) -> Result<Self, ConnectionError> {
1530 // PATH is for native QUIC only, and the transport is known here and
1531 // nowhere further in. Refusing before dialling means a session that
1532 // the server would close on sight is never opened.
1533 setup::validate_client_path_transport(
1534 &config.setup_parameters,
1535 matches!(config.transport, TransportType::WebTransport { .. }),
1536 )
1537 .map_err(EndpointError::from)?;
1538
1539 let transport = match &config.transport {
1540 TransportType::Quic => Self::connect_quic(addr, &config).await?,
1541 TransportType::WebTransport { url } => {
1542 let url = url.clone();
1543 Self::connect_webtransport(&url, &config).await?
1544 }
1545 };
1546
1547 Self::adopt(transport, config).await
1548 }
1549
1550 /// Run the MoQT setup handshake over a transport somebody else established.
1551 ///
1552 /// For choosing the draft from what the server selected: dial once through
1553 /// [`crate::transport::dial_quic`] offering every ALPN, then bring the
1554 /// connection to the module its answer names. [`Self::connect`] cannot do
1555 /// this — it derives its single ALPN from the draft it was given.
1556 ///
1557 /// `config.draft` must match this module. The transport is adopted as
1558 /// given; nothing here re-checks the ALPN it was negotiated with.
1559 pub async fn adopt(
1560 transport: Transport,
1561 config: ClientConfig,
1562 ) -> Result<Self, ConnectionError> {
1563 let draft = config.draft;
1564 // PATH is for native QUIC only, and the transport is known here and
1565 // nowhere further in. Refusing before dialling means a session that
1566 // the server would close on sight is never opened.
1567 setup::validate_client_path_transport(
1568 &config.setup_parameters,
1569 matches!(config.transport, TransportType::WebTransport { .. }),
1570 )
1571 .map_err(EndpointError::from)?;
1572
1573 // Send half of the control plane: one unidirectional stream whose
1574 // first message is SETUP, which is also its stream header.
1575 let send = transport.open_uni().await?;
1576 let mut control_send = FramedSendStream::new(send, draft);
1577
1578 // Perform setup handshake (draft-18: no versions)
1579 let mut endpoint = Endpoint::new(Role::Client);
1580 endpoint.connect()?;
1581 let setup_msg = endpoint.send_setup(config.setup_parameters.clone())?;
1582 let any_setup = AnyControlMessage::Draft18(setup_msg);
1583 let raw_setup = control_send.write_control(&any_setup).await?;
1584
1585 // Receive half: the peer's control stream is whichever unidirectional
1586 // stream leads with CONTROL_STREAM_TYPE.
1587 let mut deferred_uni: VecDeque<FramedRecvStream> = VecDeque::new();
1588 let mut control_recv = loop {
1589 let recv = transport.accept_uni().await?;
1590 let mut framed = FramedRecvStream::new(recv, draft);
1591 match framed.peek_stream_type().await {
1592 Ok(CONTROL_STREAM_TYPE) => break framed,
1593 // Every other type is a data stream — and so is a stream that
1594 // ended or failed before its type arrived, not because it is
1595 // one but because there is nothing left to decide with. The
1596 // data path sees the same end one read later and reports it
1597 // the way it reports every other. Treating it as the control
1598 // stream would hand the session's control plane to a stream
1599 // that carried nothing.
1600 _ => deferred_uni.push_back(framed),
1601 }
1602 };
1603
1604 let (server_setup, raw_server_setup) = control_recv.read_control(true).await?;
1605 // Unified SETUP in draft-18: server responds with the same message type.
1606 match &server_setup {
1607 AnyControlMessage::Draft18(ControlMessage::Setup(ref s)) => {
1608 endpoint.receive_setup(s)?;
1609 }
1610 _ => {
1611 return Err(ConnectionError::Endpoint(EndpointError::NotActive));
1612 }
1613 }
1614
1615 let pending_events = vec![
1616 ClientEvent::ControlMessage {
1617 direction: Direction::Send,
1618 message: any_setup,
1619 stream_id: None,
1620 raw: Some(raw_setup),
1621 },
1622 ClientEvent::ControlMessage {
1623 direction: Direction::Receive,
1624 message: server_setup.clone(),
1625 stream_id: None,
1626 raw: raw_server_setup.clone(),
1627 },
1628 ClientEvent::SetupComplete { negotiated_version: 0xff000000 + 18 },
1629 ];
1630
1631 Ok(Self {
1632 transport,
1633 endpoint,
1634 draft,
1635 control_send: Some(control_send),
1636 control_recv: Some(control_recv),
1637 observer: None,
1638 pending_events,
1639 server_setup,
1640 server_setup_raw: raw_server_setup,
1641 deferred_uni: Mutex::new(deferred_uni),
1642 pending_inbound: Mutex::new(VecDeque::new()),
1643 })
1644 }
1645
1646 /// Establish a raw QUIC connection.
1647 ///
1648 /// Offers this draft's ALPN alone; [`crate::transport::dial_quic`] holds the
1649 /// TLS and endpoint setup.
1650 async fn connect_quic(addr: &str, config: &ClientConfig) -> Result<Transport, ConnectionError> {
1651 let (transport, _negotiated) = crate::transport::dial_quic(
1652 addr,
1653 &crate::transport::QuicDialOptions {
1654 skip_cert_verification: config.skip_cert_verification,
1655 ca_certs: config.ca_certs.clone(),
1656 ..crate::transport::QuicDialOptions::new(config.alpn())
1657 },
1658 )
1659 .await?;
1660 Ok(transport)
1661 }
1662
1663 /// Establish a WebTransport connection.
1664 ///
1665 /// [`crate::transport::dial_webtransport`] holds the TLS and endpoint
1666 /// setup, exactly as `connect_quic` above defers its own. That is not
1667 /// only deduplication: both dials must trust the same roots. Settling trust
1668 /// at this call site instead — from `wtransport`'s own builder settings, or
1669 /// from a second config of this draft's own — puts the decision in two
1670 /// places, where it can stop matching what the QUIC dial trusts, so one
1671 /// relay would pass on one transport and fail on the other and a caller's
1672 /// private CA would reach only the dials whose call site installed it.
1673 /// Both ask the same function what to trust.
1674 #[cfg(feature = "webtransport")]
1675 async fn connect_webtransport(
1676 url: &str,
1677 config: &ClientConfig,
1678 ) -> Result<Transport, ConnectionError> {
1679 Ok(crate::transport::dial_webtransport(
1680 url,
1681 &crate::transport::QuicDialOptions {
1682 skip_cert_verification: config.skip_cert_verification,
1683 ca_certs: config.ca_certs.clone(),
1684 // The draft's own protocol identifier, which this draft
1685 // negotiates in `WT-Available-Protocols` rather than in
1686 // CLIENT_SETUP: "The client includes MOQT protocol identifiers
1687 // in the WT-Available-Protocols header". `config.alpn()` above
1688 // is `h3`, which is the HTTP/3 name and settles no version.
1689 wt_protocols: vec![config.draft.quic_alpn().to_vec()],
1690 ..crate::transport::QuicDialOptions::new(config.alpn())
1691 },
1692 )
1693 .await?)
1694 }
1695
1696 /// Stub for when the webtransport feature is not enabled.
1697 #[cfg(not(feature = "webtransport"))]
1698 async fn connect_webtransport(
1699 _url: &str,
1700 _config: &ClientConfig,
1701 ) -> Result<Transport, ConnectionError> {
1702 Err(ConnectionError::Transport(TransportError::Connect(
1703 "webtransport feature not enabled".into(),
1704 )))
1705 }
1706
1707 // -- Observer ---------------------------------------------------
1708
1709 /// Attach an observer. Buffered handshake events from `connect()` are
1710 /// flushed in arrival order before this returns.
1711 pub fn set_observer(&mut self, observer: Box<dyn ConnectionObserver>) {
1712 self.observer = Some(observer);
1713 for event in self.pending_events.drain(..) {
1714 if let Some(ref obs) = self.observer {
1715 obs.on_event_owned(event);
1716 }
1717 }
1718 }
1719
1720 /// Remove the observer.
1721 pub fn clear_observer(&mut self) {
1722 self.observer = None;
1723 }
1724
1725 /// Emit an event to the observer, if one is attached.
1726 fn emit(&self, event: ClientEvent) {
1727 if let Some(ref obs) = self.observer {
1728 obs.on_event_owned(event);
1729 }
1730 }
1731
1732 // -- Control message I/O ----------------------------------------
1733
1734 /// Send a control message on the control stream.
1735 ///
1736 /// Wraps the draft-18 message in `AnyControlMessage::Draft18` for
1737 /// framing. This is the route for the message that belongs to the session
1738 /// rather than to one request: GOAWAY. NAMESPACE, NAMESPACE_DONE,
1739 /// PUBLISH_BLOCKED and REQUEST_UPDATE belong on a request stream, and this
1740 /// returns [`ConnectionError::RequestStreamMessageOnControlStream`] for them.
1741 /// SETUP is written by [`connect`](Self::connect) and is the control
1742 /// stream's own type varint.
1743 ///
1744 /// # Requests are refused here
1745 ///
1746 /// Draft-18 Section 3.3 keeps requests off the control plane: "In addition
1747 /// to the control streams, this specification uses bidirectional streams
1748 /// to carry requests. A request stream begins with one of these seven
1749 /// message types: TRACK_STATUS, SUBSCRIBE, PUBLISH, FETCH,
1750 /// PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE, and SUBSCRIBE_TRACKS." The
1751 /// response comes back on that same bidirectional stream, and resetting it
1752 /// cancels the request (Section 3.3.1).
1753 ///
1754 /// Handing one of those seven to this method returns
1755 /// [`ConnectionError::RequestOnControlStream`] and writes **nothing** —
1756 /// an enforcing peer sees no bytes at all, not a misplaced request. Use
1757 /// the typed helpers, which open a bidirectional stream each:
1758 /// [`subscribe`](Self::subscribe), [`fetch`](Self::fetch),
1759 /// [`joining_fetch`](Self::joining_fetch), [`publish`](Self::publish),
1760 /// [`track_status`](Self::track_status),
1761 /// [`publish_namespace`](Self::publish_namespace),
1762 /// [`subscribe_namespace`](Self::subscribe_namespace) and
1763 /// [`subscribe_tracks`](Self::subscribe_tracks).
1764 ///
1765 /// Response types are still permitted, and should not be used: a response
1766 /// written here will be refused by a conforming peer, whose endpoint
1767 /// answers a response on the control stream with an error. Answer a peer's
1768 /// request on the stream it opened, with the helpers
1769 /// [`accept_request_stream`](Self::accept_request_stream) hands a handle
1770 /// for — [`respond_ok`](Self::respond_ok),
1771 /// [`respond_subscribe_ok`](Self::respond_subscribe_ok),
1772 /// [`respond_fetch_ok`](Self::respond_fetch_ok) and
1773 /// [`respond_error`](Self::respond_error).
1774 /// [`publish_done`](Self::publish_done) does not come through here either:
1775 /// it takes the request stream its PUBLISH opened.
1776 ///
1777 /// NAMESPACE, NAMESPACE_DONE and PUBLISH_BLOCKED are refused here too:
1778 /// draft-18's Table 5 marks all three "Request",
1779 /// and Section 10.16, Section 10.17 and Section 10.20 put each on the
1780 /// stream of the request that asked for it.
1781 /// [`namespace_on`](Self::namespace_on),
1782 /// [`namespace_done_on`](Self::namespace_done_on) and
1783 /// [`publish_blocked_on`](Self::publish_blocked_on) are the routes that
1784 /// place them where the table says.
1785 pub async fn send_control(&mut self, msg: &ControlMessage) -> Result<(), ConnectionError> {
1786 let ty = msg.message_type();
1787 if starts_a_request_stream(ty) {
1788 return Err(ConnectionError::RequestOnControlStream(ty));
1789 }
1790 // What this endpoint refuses to receive on the control stream it must
1791 // not write there either, or the client emits frames its own peer half
1792 // would close the session over.
1793 if belongs_on_a_request_stream(ty) {
1794 return Err(ConnectionError::RequestStreamMessageOnControlStream(ty));
1795 }
1796 let any = AnyControlMessage::Draft18(msg.clone());
1797 let send = self.control_send.as_mut().ok_or(ConnectionError::NoControlStream)?;
1798 let raw = send.write_control(&any).await?;
1799 self.emit(ClientEvent::ControlMessage {
1800 direction: Direction::Send,
1801 message: any,
1802 stream_id: None,
1803 raw: Some(raw),
1804 });
1805 Ok(())
1806 }
1807
1808 /// Read the next control message from the control stream.
1809 ///
1810 /// Returns the `AnyControlMessage` and also extracts the draft-18
1811 /// `ControlMessage` for internal endpoint dispatch.
1812 pub async fn recv_control(&mut self) -> Result<ControlMessage, ConnectionError> {
1813 let recv = self.control_recv.as_mut().ok_or(ConnectionError::NoControlStream)?;
1814 let capture_raw = self.observer.is_some();
1815 let read = recv.read_control(capture_raw).await;
1816 let (any, raw) = match read {
1817 Ok(v) => v,
1818 Err(e) => return Err(self.close_for_codec(e)),
1819 };
1820 if capture_raw {
1821 self.emit(ClientEvent::ControlMessage {
1822 direction: Direction::Receive,
1823 message: any.clone(),
1824 stream_id: None,
1825 raw,
1826 });
1827 }
1828 // Unwrap to draft-18 for the endpoint
1829 match any {
1830 AnyControlMessage::Draft18(msg) => Ok(msg),
1831 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 18 the
1832 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
1833 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
1834 // naming the other drafts: such a list has to be edited in every draft module
1835 // whenever a draft is added, and a copy that omits one leaves this match
1836 // non-exhaustive.
1837 #[allow(unreachable_patterns)]
1838 _ => Err(ConnectionError::ControlMessageNarrowing),
1839 }
1840 }
1841
1842 /// Read and dispatch the next incoming control message through the
1843 /// endpoint state machine. Returns the decoded message for inspection.
1844 ///
1845 /// Responses never arrive here. Draft-18 responses carry no request id
1846 /// and belong on the request stream that asked for them, so the endpoint
1847 /// refuses a response that turns up on the control stream. Read them with
1848 /// [`recv_on_request_stream`](Self::recv_on_request_stream).
1849 pub async fn recv_and_dispatch(&mut self) -> Result<ControlMessage, ConnectionError> {
1850 let msg = self.recv_control().await?;
1851 self.endpoint.receive_message(msg.clone()).map_err(|e| self.close_if_session_fatal(e))?;
1852
1853 // Emit draining event if this was a GoAway
1854 if let ControlMessage::GoAway(ref ga) = msg {
1855 self.emit(ClientEvent::Draining { new_session_uri: ga.new_session_uri.clone() });
1856 }
1857
1858 Ok(msg)
1859 }
1860
1861 // -- Request streams --------------------------------------------
1862
1863 /// Open the bidirectional stream a request will be carried on.
1864 ///
1865 /// Opened *before* the endpoint allocates a request id, so a transport
1866 /// that refuses a new stream — the peer's `initial_max_streams_bidi` is
1867 /// exhausted, the connection is gone — costs nothing. The endpoint has no
1868 /// way to abandon a request it has already allocated, so every failure
1869 /// that can be moved ahead of the allocation is.
1870 ///
1871 /// Nothing is written here. A request stream carries no stream-type
1872 /// header: its first field is the leading message's own type field, which
1873 /// is what [`begin_request`](Self::begin_request) writes.
1874 async fn open_request_bi(
1875 &self,
1876 ) -> Result<(FramedSendStream, FramedRecvStream), ConnectionError> {
1877 let (send, recv) = self.transport.open_bi().await?;
1878 Ok((FramedSendStream::new(send, self.draft), FramedRecvStream::new(recv, self.draft)))
1879 }
1880
1881 /// Reset a request stream that was opened but whose request could not be
1882 /// built, and pass the endpoint's error through.
1883 ///
1884 /// Without this, an endpoint refusal — the session is draining, the
1885 /// request-id range is exhausted — would leave a bidirectional stream
1886 /// open that never carries a first message, and dropping it would FIN it,
1887 /// telling the peer an empty stream ended cleanly.
1888 fn or_abandon<T>(
1889 halves: &mut (FramedSendStream, FramedRecvStream),
1890 built: Result<T, EndpointError>,
1891 ) -> Result<T, ConnectionError> {
1892 match built {
1893 Ok(value) => Ok(value),
1894 Err(e) => {
1895 let _ = halves.0.reset(REQUEST_CANCELLED);
1896 let _ = halves.1.stop(REQUEST_CANCELLED);
1897 Err(ConnectionError::Endpoint(e))
1898 }
1899 }
1900 }
1901
1902 /// Write `msg` as the first message on an opened bidirectional stream and
1903 /// hand back the [`RequestStream`] that owns both halves.
1904 ///
1905 /// This is the one place a request reaches the wire. Every request helper
1906 /// funnels through it, so the ordering — open, allocate, write, emit — is
1907 /// stated once.
1908 ///
1909 /// A failed write resets both halves rather than leaving a half-written
1910 /// request stream behind. What it cannot undo is the endpoint's
1911 /// allocation: the request id and its state machine already exist, and
1912 /// there is no way to retract them, so a write that fails here leaves one
1913 /// pending request the endpoint will never see answered.
1914 async fn begin_request(
1915 &mut self,
1916 halves: (FramedSendStream, FramedRecvStream),
1917 kind: RequestKind,
1918 request_id: VarInt,
1919 msg: &ControlMessage,
1920 ) -> Result<RequestStream, ConnectionError> {
1921 debug_assert_eq!(
1922 msg.message_type(),
1923 kind.message_type(),
1924 "a request stream's first message must be the one its kind names"
1925 );
1926 let (mut send, mut recv) = halves;
1927 let stream_id = send.stream_id();
1928 self.emit(ClientEvent::StreamOpened {
1929 direction: Direction::Send,
1930 stream_kind: StreamKind::Request,
1931 stream_id,
1932 });
1933 let any = AnyControlMessage::Draft18(msg.clone());
1934 let raw = match send.write_control(&any).await {
1935 Ok(raw) => raw,
1936 Err(e) => {
1937 let _ = send.reset(REQUEST_CANCELLED);
1938 let _ = recv.stop(REQUEST_CANCELLED);
1939 return Err(e);
1940 }
1941 };
1942 self.emit(ClientEvent::ControlMessage {
1943 direction: Direction::Send,
1944 message: any,
1945 stream_id: Some(stream_id),
1946 raw: Some(raw),
1947 });
1948 Ok(RequestStream {
1949 send,
1950 recv,
1951 request_id,
1952 kind,
1953 draft: self.draft,
1954 stream_id,
1955 cancelled: false,
1956 finished: false,
1957 origin: RequestOrigin::Local,
1958 responded: false,
1959 fetch_data: None,
1960 })
1961 }
1962
1963 /// Read the next message off a request stream and dispatch it through the
1964 /// endpoint with that stream's own request id.
1965 ///
1966 /// On draft-18 a response carries no request id; the stream is the
1967 /// correlation, so the id comes from the handle and not from the wire.
1968 ///
1969 /// This blocks until a whole message has arrived. Backpressure is per
1970 /// request: a stream nobody reads stays unread, and the peer stays flow
1971 /// controlled on it alone. A peer that reset the stream surfaces as
1972 /// [`ConnectionError::Transport`] carrying
1973 /// [`TransportError::StreamReset`] with the peer's code; a caller that is
1974 /// deliberately not reading should watch
1975 /// [`RequestStream::peer_cancelled`] instead.
1976 ///
1977 /// # Errors
1978 ///
1979 /// [`ConnectionError::Endpoint`] if the message is not one of this
1980 /// draft's response types, or if it does not fit the request's state.
1981 /// The message has already been emitted to the observer by then — what
1982 /// arrived is reported whether or not the endpoint accepts it.
1983 pub async fn recv_on_request_stream(
1984 &mut self,
1985 stream: &mut RequestStream,
1986 ) -> Result<ControlMessage, ConnectionError> {
1987 let capture_raw = self.observer.is_some();
1988 let (any, raw) = match stream.recv.read_control(capture_raw).await {
1989 Ok(read) => read,
1990 Err(e) => {
1991 // A peer that reset this stream cancelled the request on it,
1992 // and this is where a caller reading normally learns of it. The
1993 // record is made and its verdict dropped: the read's own error
1994 // is what the caller has to act on, and returning a state error
1995 // in its place would hide a reset behind it.
1996 if matches!(e, ConnectionError::Transport(TransportError::StreamReset(_))) {
1997 let _ = self.endpoint.cancel_request(stream.request_id);
1998 }
1999 return Err(e);
2000 }
2001 };
2002 if capture_raw {
2003 self.emit(ClientEvent::ControlMessage {
2004 direction: Direction::Receive,
2005 message: any.clone(),
2006 stream_id: Some(stream.stream_id()),
2007 raw,
2008 });
2009 }
2010 let msg = match any {
2011 AnyControlMessage::Draft18(msg) => Ok::<_, ConnectionError>(msg),
2012 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 18 the
2013 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
2014 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
2015 // naming the other drafts: such a list has to be edited in every draft module
2016 // whenever a draft is added, and a copy that omits one leaves this match
2017 // non-exhaustive.
2018 #[allow(unreachable_patterns)]
2019 _ => Err(ConnectionError::ControlMessageNarrowing),
2020 }?;
2021 // Which dispatcher this belongs to is decided by who opened the
2022 // stream, not by the message. On a stream this endpoint opened the
2023 // next message is the answer to our request; on one the peer opened it
2024 // cannot be, because we are the one who owes an answer. Feeding a
2025 // peer's REQUEST_UPDATE to the response dispatcher would look up a
2026 // request we never made.
2027 let dispatched = match stream.origin {
2028 RequestOrigin::Local => {
2029 self.endpoint.receive_response_on_stream(stream.request_id, msg.clone())
2030 }
2031 RequestOrigin::Peer => {
2032 self.endpoint.receive_on_peer_request_stream(stream.request_id, msg.clone())
2033 }
2034 };
2035 dispatched.map_err(|e| self.close_if_session_fatal(e))?;
2036 Ok(msg)
2037 }
2038
2039 /// Write a follow-up message on an already-open request stream.
2040 ///
2041 /// The request itself was written when the stream was opened; this is for
2042 /// what comes after it on the same stream, PUBLISH_DONE among them — see
2043 /// [`publish_done`](Self::publish_done), which uses this.
2044 ///
2045 /// It does not refuse any message type. Which messages may follow a
2046 /// request on its own stream is not something this implementation can
2047 /// settle, so the choice is left to the caller rather than guessed at.
2048 pub async fn send_on_request_stream(
2049 &mut self,
2050 stream: &mut RequestStream,
2051 msg: &ControlMessage,
2052 ) -> Result<(), ConnectionError> {
2053 let any = AnyControlMessage::Draft18(msg.clone());
2054 let raw = stream.send.write_control(&any).await?;
2055 self.emit(ClientEvent::ControlMessage {
2056 direction: Direction::Send,
2057 message: any,
2058 stream_id: Some(stream.stream_id()),
2059 raw: Some(raw),
2060 });
2061 Ok(())
2062 }
2063
2064 /// Cancel a request: record it at the endpoint, then terminate its stream.
2065 ///
2066 /// Section 3.3.2 puts the cancel at the stream — "Implementations SHOULD
2067 /// cancel requests by abruptly terminating any directions of a stream that
2068 /// are still open" — while the request's own state lives in the endpoint,
2069 /// so the two have to move together. This is the only place that moves
2070 /// both.
2071 ///
2072 /// The endpoint goes first and the stream is terminated only if it agrees,
2073 /// which is the order every request path here uses: a caller acts on a
2074 /// stream after the endpoint has accepted the step, never before. A refused
2075 /// cancel therefore leaves the stream exactly as it was, and
2076 /// [`RequestStream::cancel`] is still there for a caller that wants the
2077 /// stream reset regardless.
2078 ///
2079 /// Idempotent from both ends: a request that has already ended accepts the
2080 /// cancel and stays where it is, and a handle that has already been
2081 /// cancelled resets nothing a second time.
2082 ///
2083 /// # Errors
2084 ///
2085 /// [`ConnectionError::Endpoint`] if no request carries this stream's id or
2086 /// the request has not been written, and [`ConnectionError::Transport`] if
2087 /// `code` is outside the QUIC varint range — see
2088 /// [`RequestStream::cancel`], which is what sends it.
2089 pub fn cancel_request_stream(
2090 &mut self,
2091 stream: &mut RequestStream,
2092 code: u64,
2093 ) -> Result<(), ConnectionError> {
2094 let recorded = self.endpoint.cancel_request(stream.request_id);
2095 recorded.map_err(|e| self.close_if_session_fatal(e))?;
2096 stream.cancel(code)
2097 }
2098
2099 /// Wait for the peer to cancel this request, and record it if it does.
2100 ///
2101 /// [`RequestStream::peer_cancelled`] with the endpoint's record attached. A
2102 /// caller applying backpressure is deliberately not calling
2103 /// [`recv_on_request_stream`](Self::recv_on_request_stream), which is the
2104 /// other place a peer reset surfaces, so without this the request would end
2105 /// on the wire and stay open in the endpoint's record for as long as the
2106 /// backpressure lasts.
2107 ///
2108 /// Returns what the handle's own method returns; see it for the `Ok(None)`
2109 /// case and for what WebTransport can and cannot observe. Cancel-safe, and
2110 /// it grants no flow-control credit.
2111 pub async fn peer_cancelled_on_request_stream(
2112 &mut self,
2113 stream: &mut RequestStream,
2114 ) -> Result<Option<u64>, ConnectionError> {
2115 let code = stream.peer_cancelled().await?;
2116 if code.is_some() {
2117 // Discarded for the reason the read path discards it: the peer has
2118 // ended the request whatever the record said, and a state error
2119 // here would replace the answer the caller asked for.
2120 let _ = self.endpoint.cancel_request(stream.request_id);
2121 }
2122 Ok(code)
2123 }
2124
2125 // -- Accepting the peer's request streams -----------------------
2126
2127 /// Accept the next bidirectional stream the peer opened, read the request
2128 /// it begins with, and hand back that request and a handle to answer it
2129 /// on.
2130 ///
2131 /// This is the mirror of the request helpers. Where
2132 /// [`subscribe`](Self::subscribe) and its siblings open a stream and write
2133 /// a request, this takes one the peer opened and reads one. Draft-18
2134 /// Section 3.3 puts requests in both directions on bidirectional streams,
2135 /// so a client that only ever calls the helpers can never be published to
2136 /// or subscribed from.
2137 ///
2138 /// The returned [`RequestStream`] carries [`RequestOrigin::Peer`]. Answer
2139 /// it with [`respond_subscribe_ok`](Self::respond_subscribe_ok),
2140 /// [`respond_fetch_ok`](Self::respond_fetch_ok),
2141 /// [`respond_ok`](Self::respond_ok) or
2142 /// [`respond_error`](Self::respond_error), and **hold it for as long as
2143 /// the request lasts** — a subscription's PUBLISH_DONE is written on it,
2144 /// and dropping it resets the stream.
2145 ///
2146 /// # Two refusals, two codes
2147 ///
2148 /// Draft-18 Section 3.3, on a stream that begins with the wrong type:
2149 /// "Bidirectional streams MUST NOT begin with any other message type
2150 /// unless negotiated. If they do, the peer MUST close the Session with a
2151 /// PROTOCOL_VIOLATION." Section 10.1, on the Request ID: "If an endpoint
2152 /// receives a Request ID where the least significant bit is incorrect for
2153 /// the sender, or a duplicate Request ID, it MUST close the session with
2154 /// INVALID_REQUEST_ID." Both are closes of the session on the wire, with
2155 /// different codes, and both happen before this returns — the error handed
2156 /// back reports a session that is already gone, not one the caller must
2157 /// remember to close.
2158 ///
2159 /// # Cancelling this future loses nothing
2160 ///
2161 /// A stream taken off the transport but not yet read is put back on an
2162 /// internal queue, and the next call takes it before accepting anything
2163 /// new — including whatever bytes of the request had already arrived,
2164 /// which live in the stream's own reader. So this is safe to `select!`
2165 /// against a shutdown signal or a timer. See
2166 /// [`pending_inbound_count`](Self::pending_inbound_count).
2167 ///
2168 /// What it is **not** safe to do is run concurrently with another method
2169 /// on the same connection: this takes `&mut self` because registering the
2170 /// peer's request moves endpoint state, and no signature avoids that while
2171 /// the connection owns the endpoint. A caller blocked in
2172 /// [`recv_on_request_stream`](Self::recv_on_request_stream) waiting for
2173 /// its own response is not accepting, and the peer's request streams queue
2174 /// up in the transport behind it. One loop that never blocks indefinitely
2175 /// on a single read is the shape this supports.
2176 ///
2177 /// # Ordering
2178 ///
2179 /// The endpoint is told about the request last, after every step that can
2180 /// fail or be cancelled, and building the handle afterwards cannot fail.
2181 /// This is the inverse of the outbound path's reasoning — it opens the
2182 /// stream before allocating a Request ID for the same reason — and rests
2183 /// on the same fact: the endpoint has no way to abandon a request it has
2184 /// already registered. Registering earlier would let a cancelled accept
2185 /// leave a state machine keyed to a stream nobody holds, and the peer's
2186 /// next use of that Request ID would then be reported as a duplicate — a
2187 /// session close, over an id the peer used exactly once.
2188 ///
2189 /// # Errors
2190 ///
2191 /// - [`ConnectionError::NonRequestOnRequestStream`] — the session has been
2192 /// closed with PROTOCOL_VIOLATION and the stream reset.
2193 /// - [`ConnectionError::Endpoint`] carrying `RequestId` or
2194 /// `DuplicateRequestId` — the session has been closed with
2195 /// INVALID_REQUEST_ID and the stream reset.
2196 /// - [`ConnectionError::Endpoint`] carrying `NotActive` or `Draining` —
2197 /// the stream is reset, the session is left alone.
2198 /// - [`ConnectionError::Transport`] or [`ConnectionError::Codec`] — the
2199 /// stream is reset, the session is left alone.
2200 pub async fn accept_request_stream(
2201 &mut self,
2202 ) -> Result<(ControlMessage, RequestStream), ConnectionError> {
2203 let pair = match self.take_pending_inbound() {
2204 Some(pair) => pair,
2205 None => {
2206 let (send, recv) = self.transport.accept_bi().await?;
2207 (FramedSendStream::new(send, self.draft), FramedRecvStream::new(recv, self.draft))
2208 }
2209 };
2210 let capture_raw = self.observer.is_some();
2211
2212 let (any, raw, mut send, mut recv) = {
2213 let mut pending = PendingInbound { pair: Some(pair), queue: &self.pending_inbound };
2214 let read = {
2215 let (_, recv) = pending.pair.as_mut().expect("set on construction");
2216 recv.read_control(capture_raw).await
2217 };
2218 // Taken out before anything can return, so the guard's Drop puts
2219 // the pair back for exactly one reason: this future was cancelled.
2220 let (mut send, mut recv) = pending.pair.take().expect("set on construction");
2221 match read {
2222 Ok((any, raw)) => (any, raw, send, recv),
2223 Err(e) => {
2224 // A stream whose first message could not be read is not
2225 // worth queueing: the next accept would fail on it the
2226 // same way. Reset rather than FIN — nothing was served.
2227 let _ = send.reset(REQUEST_UNANSWERED);
2228 let _ = recv.stop(REQUEST_UNANSWERED);
2229 return Err(e);
2230 }
2231 }
2232 };
2233
2234 // Reported once the request has actually arrived rather than when the
2235 // stream came off the transport, so a cancelled accept that is retried
2236 // does not report the same stream twice.
2237 let stream_id = send.stream_id();
2238 self.emit(ClientEvent::StreamOpened {
2239 direction: Direction::Receive,
2240 stream_kind: StreamKind::Request,
2241 stream_id,
2242 });
2243 if capture_raw {
2244 self.emit(ClientEvent::ControlMessage {
2245 direction: Direction::Receive,
2246 message: any.clone(),
2247 stream_id: Some(stream_id),
2248 raw,
2249 });
2250 }
2251
2252 let msg = match any {
2253 AnyControlMessage::Draft18(msg) => msg,
2254 // `AnyControlMessage` carries one variant per enabled draft feature. With draft 18 the
2255 // only one enabled the arm above is exhaustive and this rejection arm unreachable.
2256 // Compiled in every configuration with the lint allowed, rather than gated on a `cfg`
2257 // naming the other drafts: such a list has to be edited in every draft module
2258 // whenever a draft is added, and a copy that omits one leaves this match
2259 // non-exhaustive.
2260 #[allow(unreachable_patterns)]
2261 _ => {
2262 let _ = send.reset(REQUEST_UNANSWERED);
2263 let _ = recv.stop(REQUEST_UNANSWERED);
2264 return Err(ConnectionError::ControlMessageNarrowing);
2265 }
2266 };
2267
2268 let ty = msg.message_type();
2269 let Some(kind) = RequestKind::from_message_type(ty) else {
2270 let err = self.endpoint.refuse_non_request(ty);
2271 self.close_for(&err);
2272 let _ = send.reset(REQUEST_UNANSWERED);
2273 let _ = recv.stop(REQUEST_UNANSWERED);
2274 return Err(ConnectionError::NonRequestOnRequestStream(ty));
2275 };
2276
2277 let request_id = match self.endpoint.receive_request_on_stream(&msg) {
2278 Ok(request_id) => request_id,
2279 Err(e) => {
2280 let _ = send.reset(REQUEST_UNANSWERED);
2281 let _ = recv.stop(REQUEST_UNANSWERED);
2282 return Err(self.close_if_session_fatal(e));
2283 }
2284 };
2285
2286 Ok((
2287 msg,
2288 RequestStream {
2289 send,
2290 recv,
2291 request_id,
2292 kind,
2293 draft: self.draft,
2294 stream_id,
2295 cancelled: false,
2296 finished: false,
2297 origin: RequestOrigin::Peer,
2298 responded: false,
2299 fetch_data: None,
2300 },
2301 ))
2302 }
2303
2304 /// Take the oldest stream pair a cancelled
2305 /// [`accept_request_stream`](Self::accept_request_stream) put back, if any.
2306 ///
2307 /// Synchronous on purpose, like
2308 /// [`take_deferred_uni`](Self::take_deferred_uni): the guard is dropped
2309 /// before the caller awaits, so the lock is never held across a suspension
2310 /// point.
2311 fn take_pending_inbound(&self) -> Option<(FramedSendStream, FramedRecvStream)> {
2312 self.pending_inbound.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).pop_front()
2313 }
2314
2315 /// How many peer-opened request streams a cancelled
2316 /// [`accept_request_stream`](Self::accept_request_stream) put back and a
2317 /// later call has not yet taken.
2318 ///
2319 /// Zero unless an accept future was dropped mid-read.
2320 pub fn pending_inbound_count(&self) -> usize {
2321 self.pending_inbound.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).len()
2322 }
2323
2324 // -- Answering the peer's requests ------------------------------
2325
2326 /// Write `msg` on the request stream `stream` carries, driving the endpoint
2327 /// first and the wire second.
2328 ///
2329 /// Everything a responder writes goes on the request's own bidirectional
2330 /// stream and never on the control stream: draft-18 responses carry no
2331 /// Request ID, so the stream is the only thing that says what is being
2332 /// answered. Taking the id off the handle rather than from the caller makes
2333 /// that correlation unforgeable.
2334 async fn drive_and_send(
2335 &mut self,
2336 stream: &mut RequestStream,
2337 msg: &ControlMessage,
2338 ) -> Result<(), ConnectionError> {
2339 // A request this endpoint made is answered by the peer, with one
2340 // exception the draft states outright: "A subscriber can also send
2341 // REQUEST_UPDATE to modify parameters of a subscription established
2342 // with PUBLISH", and the receiver of one "MUST respond with exactly one
2343 // REQUEST_OK or REQUEST_ERROR message indicating if the update was
2344 // successful". On a PUBLISH this endpoint sent, that receiver is this
2345 // endpoint, so the one response it may write on a stream of its own is
2346 // the answer to an update waiting there.
2347 let answers_an_update =
2348 matches!(msg, ControlMessage::RequestOk(_) | ControlMessage::RequestError(_))
2349 && self.endpoint.has_unanswered_update(stream.request_id);
2350 if stream.origin != RequestOrigin::Peer && !answers_an_update {
2351 return Err(ConnectionError::RespondedToOwnRequest(stream.request_id.into_inner()));
2352 }
2353 // The endpoint first, so a message that does not fit the request's
2354 // state is refused before any of it reaches the wire. What this cannot
2355 // undo is a write that fails afterwards, which leaves the state
2356 // machine one step ahead of the peer — the same asymmetry
2357 // `begin_request` carries on the outbound side.
2358 self.endpoint.send_response_on_stream(stream.request_id, msg)?;
2359 self.send_on_request_stream(stream, msg).await
2360 }
2361
2362 /// [`drive_and_send`](Self::drive_and_send) for a message that *answers*
2363 /// the request, marking the handle as responded and optionally finishing
2364 /// the send half.
2365 ///
2366 /// `fin` is true only for REQUEST_ERROR. See
2367 /// [`respond_error`](Self::respond_error).
2368 async fn respond(
2369 &mut self,
2370 stream: &mut RequestStream,
2371 msg: ControlMessage,
2372 fin: bool,
2373 ) -> Result<(), ConnectionError> {
2374 self.drive_and_send(stream, &msg).await?;
2375 stream.responded = true;
2376 // `fin` says the message ends the exchange; owing a termination says
2377 // it does not, whatever the message looks like. A REQUEST_ERROR
2378 // answering an update is the case where the two disagree, and the
2379 // draft asks for a PUBLISH_DONE after it that a finished send half
2380 // could not carry.
2381 // Section 10.9.1: "When a REQUEST_UPDATE fails for a FETCH, the
2382 // publisher MUST reset the FETCH data stream." A REQUEST_ERROR on a
2383 // fetch that has already been answered can only be answering an
2384 // update, because the request's own answer was the FETCH_OK; one
2385 // before that refuses the fetch itself, and there is no data stream
2386 // open to reset.
2387 if fin && stream.kind == RequestKind::Fetch && stream.responded {
2388 stream.reset_fetch_data();
2389 }
2390 if fin && !self.endpoint.owes_update_failure(stream.request_id) {
2391 stream.finish().await?;
2392 }
2393 Ok(())
2394 }
2395
2396 /// Answer a peer's PUBLISH, PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE,
2397 /// SUBSCRIBE_TRACKS or TRACK_STATUS with REQUEST_OK.
2398 ///
2399 /// Draft-18 Section 10.5 folded PUBLISH_OK into REQUEST_OK, so this is the
2400 /// route for a PUBLISH the peer offered as well — draft-17 had a message of
2401 /// its own for that case and a helper to match.
2402 ///
2403 /// The send half is left open. A SUBSCRIBE_NAMESPACE responder still owes
2404 /// the peer the namespaces it accepted, and a PUBLISH responder is now the
2405 /// subscriber of a live subscription, so finishing here would end the
2406 /// request before it had been served; a TRACK_STATUS responder owes nothing
2407 /// further and may call [`RequestStream::finish`] straight after.
2408 ///
2409 /// # Errors
2410 ///
2411 /// [`ConnectionError::RespondedToOwnRequest`] if `stream` is one this
2412 /// endpoint opened, and [`ConnectionError::Endpoint`] if no request of a
2413 /// kind REQUEST_OK answers is pending on it. Nothing is written either
2414 /// way.
2415 pub async fn respond_ok(
2416 &mut self,
2417 stream: &mut RequestStream,
2418 response: RequestOk,
2419 ) -> Result<(), ConnectionError> {
2420 self.respond(stream, ControlMessage::RequestOk(response), false).await
2421 }
2422
2423 /// Answer a peer's SUBSCRIBE with SUBSCRIBE_OK.
2424 ///
2425 /// The send half is left open, and it must be: this endpoint is now the
2426 /// publisher of an established subscription and owes it a PUBLISH_DONE,
2427 /// which travels on this same stream —
2428 /// [`publish_done_on`](Self::publish_done_on).
2429 pub async fn respond_subscribe_ok(
2430 &mut self,
2431 stream: &mut RequestStream,
2432 response: SubscribeOk,
2433 ) -> Result<(), ConnectionError> {
2434 self.respond(stream, ControlMessage::SubscribeOk(response), false).await
2435 }
2436
2437 /// Answer a peer's FETCH with FETCH_OK.
2438 ///
2439 /// The send half is left open. The fetched objects travel on separate
2440 /// unidirectional streams, so a fetch responder may call
2441 /// [`RequestStream::finish`] as soon as this returns; it is not done here
2442 /// because nothing about FETCH_OK says the responder has no more to write.
2443 pub async fn respond_fetch_ok(
2444 &mut self,
2445 stream: &mut RequestStream,
2446 response: FetchOk,
2447 ) -> Result<(), ConnectionError> {
2448 self.respond(stream, ControlMessage::FetchOk(response), false).await
2449 }
2450
2451 /// Reject a peer's request with REQUEST_ERROR, and finish the send half.
2452 ///
2453 /// The FIN is part of the act, not a convenience: draft-18 Section 3.3.2
2454 /// says "When an endpoint rejects a request without performing any
2455 /// application processing, it SHOULD send a REQUEST_ERROR and FIN the
2456 /// stream." It is also the one response that can be finished immediately,
2457 /// because a rejected request leaves nothing further to send — every
2458 /// success path owes the peer something more.
2459 ///
2460 /// A finished handle does nothing further on [`Drop`], so the rejected
2461 /// stream is not then reset.
2462 pub async fn respond_error(
2463 &mut self,
2464 stream: &mut RequestStream,
2465 response: RequestError,
2466 ) -> Result<(), ConnectionError> {
2467 self.respond(stream, ControlMessage::RequestError(response), true).await
2468 }
2469
2470 /// End a subscription this endpoint accepted, on the stream the peer's
2471 /// SUBSCRIBE opened.
2472 ///
2473 /// The mirror of [`publish_done`](Self::publish_done), which ends a
2474 /// publication this endpoint offered with PUBLISH. Both write PUBLISH_DONE
2475 /// on a request stream and take the Request ID off the handle; they differ
2476 /// in which state machine moves, and therefore in which one refuses.
2477 pub async fn publish_done_on(
2478 &mut self,
2479 stream: &mut RequestStream,
2480 status_code: VarInt,
2481 stream_count: VarInt,
2482 reason_phrase: Vec<u8>,
2483 ) -> Result<(), ConnectionError> {
2484 let msg = ControlMessage::PublishDone(moqtap_codec::draft18::message::PublishDone {
2485 status_code,
2486 stream_count,
2487 reason_phrase,
2488 });
2489 self.respond(stream, msg, false).await
2490 }
2491
2492 /// Report a namespace on the stream a peer's SUBSCRIBE_NAMESPACE opened.
2493 ///
2494 /// Draft-18 Table 5 marks NAMESPACE (0x8) "Request", and Section 10.16 says
2495 /// why: it "is sent on the response stream of a SUBSCRIBE_NAMESPACE
2496 /// request", carrying only the suffix left after the prefix that request
2497 /// named. So it goes here and not through
2498 /// [`send_control`](Self::send_control).
2499 ///
2500 /// This is not the response — [`respond_ok`](Self::respond_ok) is, and it
2501 /// must come first, since a namespace subscription that has not been
2502 /// accepted has nothing to report on. The handle is not marked as
2503 /// responded and the send half stays open: more namespaces may follow.
2504 pub async fn namespace_on(
2505 &mut self,
2506 stream: &mut RequestStream,
2507 message: Namespace,
2508 ) -> Result<(), ConnectionError> {
2509 self.drive_and_send(stream, &ControlMessage::Namespace(message)).await
2510 }
2511
2512 /// Report that a namespace is finished, on the stream a peer's
2513 /// SUBSCRIBE_NAMESPACE opened.
2514 ///
2515 /// Table 5 marks NAMESPACE_DONE (0xE) "Request" for the same reason
2516 /// [`namespace_on`](Self::namespace_on) gives. Section 10.17: it says the
2517 /// publisher will stop serving new subscriptions for that one namespace,
2518 /// which leaves the namespace subscription itself running, so the send half
2519 /// stays open here too.
2520 pub async fn namespace_done_on(
2521 &mut self,
2522 stream: &mut RequestStream,
2523 message: NamespaceDone,
2524 ) -> Result<(), ConnectionError> {
2525 self.drive_and_send(stream, &ControlMessage::NamespaceDone(message)).await
2526 }
2527
2528 /// Report a track that cannot be published, on the stream a peer's
2529 /// SUBSCRIBE_TRACKS opened.
2530 ///
2531 /// Table 5 marks PUBLISH_BLOCKED (0xF) "Request", and Section 10.20 says
2532 /// "All PUBLISH_BLOCKED messages are in response to a SUBSCRIBE_TRACKS" —
2533 /// so this needs the SUBSCRIBE_TRACKS stream, which is what distinguishes
2534 /// it from [`namespace_on`](Self::namespace_on) and its sibling. The rest
2535 /// of the subscription is unaffected, so the send half stays open.
2536 pub async fn publish_blocked_on(
2537 &mut self,
2538 stream: &mut RequestStream,
2539 message: PublishBlocked,
2540 ) -> Result<(), ConnectionError> {
2541 self.drive_and_send(stream, &ControlMessage::PublishBlocked(message)).await
2542 }
2543
2544 // -- Subscribe flow ---------------------------------------------
2545
2546 /// Send a SUBSCRIBE on a bidirectional stream of its own.
2547 ///
2548 /// The returned [`RequestStream`] is where SUBSCRIBE_OK, REQUEST_ERROR
2549 /// and later PUBLISH_DONE arrive — read them with
2550 /// [`recv_on_request_stream`](Self::recv_on_request_stream). **Hold it for
2551 /// the subscription's life**: dropping it resets the stream, which
2552 /// cancels the subscription.
2553 pub async fn subscribe(
2554 &mut self,
2555 track_namespace: TrackNamespace,
2556 track_name: Vec<u8>,
2557 parameters: Vec<KeyValuePair>,
2558 ) -> Result<RequestStream, ConnectionError> {
2559 let mut halves = self.open_request_bi().await?;
2560 let (req_id, msg) = Self::or_abandon(
2561 &mut halves,
2562 self.endpoint.subscribe(track_namespace, track_name, parameters),
2563 )?;
2564 self.begin_request(halves, RequestKind::Subscribe, req_id, &msg).await
2565 }
2566
2567 // Draft-18 keeps draft-17's removal of UNSUBSCRIBE. Subscribers end a
2568 // subscription by resetting its request stream — `RequestStream::cancel`
2569 // — or wait for PublishDone.
2570
2571 // -- Fetch flow -------------------------------------------------
2572
2573 /// Send a standalone FETCH on a bidirectional stream of its own.
2574 ///
2575 /// FETCH_OK or REQUEST_ERROR comes back on the returned
2576 /// [`RequestStream`]; the fetched objects arrive on separate
2577 /// unidirectional data streams. Dropping the handle cancels the fetch.
2578 #[allow(clippy::too_many_arguments)]
2579 pub async fn fetch(
2580 &mut self,
2581 track_namespace: TrackNamespace,
2582 track_name: Vec<u8>,
2583 start_group: VarInt,
2584 start_object: VarInt,
2585 end_group: VarInt,
2586 end_object: VarInt,
2587 parameters: Vec<KeyValuePair>,
2588 ) -> Result<RequestStream, ConnectionError> {
2589 let mut halves = self.open_request_bi().await?;
2590 let (req_id, msg) = Self::or_abandon(
2591 &mut halves,
2592 self.endpoint.fetch(
2593 track_namespace,
2594 track_name,
2595 start_group,
2596 start_object,
2597 end_group,
2598 end_object,
2599 parameters,
2600 ),
2601 )?;
2602 self.begin_request(halves, RequestKind::Fetch, req_id, &msg).await
2603 }
2604
2605 /// Send a Relative Joining Fetch (Fetch Type 0x2) on a bidirectional
2606 /// stream of its own.
2607 ///
2608 /// A joining FETCH names an existing subscription's request id but is
2609 /// still a FETCH, so it opens its own request stream rather than sharing
2610 /// the subscription's.
2611 ///
2612 /// `joining_start` counts groups back from the subscription's largest
2613 /// group. To name the starting group outright, use
2614 /// [`absolute_joining_fetch`](Self::absolute_joining_fetch).
2615 pub async fn joining_fetch(
2616 &mut self,
2617 joining_request_id: VarInt,
2618 joining_start: VarInt,
2619 parameters: Vec<KeyValuePair>,
2620 ) -> Result<RequestStream, ConnectionError> {
2621 let mut halves = self.open_request_bi().await?;
2622 let (req_id, msg) = Self::or_abandon(
2623 &mut halves,
2624 self.endpoint.joining_fetch(joining_request_id, joining_start, parameters),
2625 )?;
2626 self.begin_request(halves, RequestKind::Fetch, req_id, &msg).await
2627 }
2628
2629 /// Send an Absolute Joining Fetch (Fetch Type 0x3) on a bidirectional
2630 /// stream of its own.
2631 ///
2632 /// Here `joining_start` is the group to begin at rather than an offset:
2633 /// draft-18 Section 10.12.2.1 has the publisher set the Start Location to
2634 /// {Joining Start, 0}.
2635 pub async fn absolute_joining_fetch(
2636 &mut self,
2637 joining_request_id: VarInt,
2638 joining_start: VarInt,
2639 parameters: Vec<KeyValuePair>,
2640 ) -> Result<RequestStream, ConnectionError> {
2641 let mut halves = self.open_request_bi().await?;
2642 let (req_id, msg) = Self::or_abandon(
2643 &mut halves,
2644 self.endpoint.absolute_joining_fetch(joining_request_id, joining_start, parameters),
2645 )?;
2646 self.begin_request(halves, RequestKind::Fetch, req_id, &msg).await
2647 }
2648
2649 // Draft-18 keeps draft-17's removal of FETCH_CANCEL. Fetchers abort with
2650 // `RequestStream::cancel`, which resets the request stream.
2651
2652 // -- Namespace flows --------------------------------------------
2653
2654 /// Send a SUBSCRIBE_NAMESPACE on a bidirectional stream of its own.
2655 ///
2656 /// Draft-18 split the draft-17 SUBSCRIBE_NAMESPACE into two messages.
2657 /// This call sends the renumbered SUBSCRIBE_NAMESPACE (type 0x50), which
2658 /// subscribes to NAMESPACE / NAMESPACE_DONE announcements only. To
2659 /// receive PUBLISH messages for matching tracks, use
2660 /// [`Self::subscribe_tracks`] instead.
2661 ///
2662 /// Draft-17's `subscribe_options` field went with the split and this
2663 /// signature does not carry it.
2664 pub async fn subscribe_namespace(
2665 &mut self,
2666 namespace_prefix: TrackNamespace,
2667 parameters: Vec<KeyValuePair>,
2668 ) -> Result<RequestStream, ConnectionError> {
2669 let mut halves = self.open_request_bi().await?;
2670 let (req_id, msg) = Self::or_abandon(
2671 &mut halves,
2672 self.endpoint.subscribe_namespace(namespace_prefix, parameters),
2673 )?;
2674 self.begin_request(halves, RequestKind::SubscribeNamespace, req_id, &msg).await
2675 }
2676
2677 /// Send a SUBSCRIBE_TRACKS (type 0x51, new in draft-18) on a bidirectional
2678 /// stream of its own. Causes the relay to PUBLISH matching tracks back to
2679 /// us.
2680 ///
2681 /// Draft-18 Section 3.3 lists SUBSCRIBE_TRACKS among the message types a
2682 /// bidirectional stream may begin with, so it is a request like the other
2683 /// six and not a control-stream message. It has no draft-17 counterpart.
2684 pub async fn subscribe_tracks(
2685 &mut self,
2686 namespace_prefix: TrackNamespace,
2687 parameters: Vec<KeyValuePair>,
2688 ) -> Result<RequestStream, ConnectionError> {
2689 let mut halves = self.open_request_bi().await?;
2690 let (req_id, msg) = Self::or_abandon(
2691 &mut halves,
2692 self.endpoint.subscribe_tracks(namespace_prefix, parameters),
2693 )?;
2694 self.begin_request(halves, RequestKind::SubscribeTracks, req_id, &msg).await
2695 }
2696
2697 /// Send a PUBLISH_NAMESPACE on a bidirectional stream of its own.
2698 pub async fn publish_namespace(
2699 &mut self,
2700 track_namespace: TrackNamespace,
2701 parameters: Vec<KeyValuePair>,
2702 ) -> Result<RequestStream, ConnectionError> {
2703 let mut halves = self.open_request_bi().await?;
2704 let (req_id, msg) = Self::or_abandon(
2705 &mut halves,
2706 self.endpoint.publish_namespace(track_namespace, parameters),
2707 )?;
2708 self.begin_request(halves, RequestKind::PublishNamespace, req_id, &msg).await
2709 }
2710
2711 // -- Track Status flow ------------------------------------------
2712
2713 /// Send a TRACK_STATUS on a bidirectional stream of its own.
2714 pub async fn track_status(
2715 &mut self,
2716 track_namespace: TrackNamespace,
2717 track_name: Vec<u8>,
2718 parameters: Vec<KeyValuePair>,
2719 ) -> Result<RequestStream, ConnectionError> {
2720 let mut halves = self.open_request_bi().await?;
2721 let (req_id, msg) = Self::or_abandon(
2722 &mut halves,
2723 self.endpoint.track_status(track_namespace, track_name, parameters),
2724 )?;
2725 self.begin_request(halves, RequestKind::TrackStatus, req_id, &msg).await
2726 }
2727
2728 // -- Publish flow (publisher side) ------------------------------
2729
2730 /// Send a PUBLISH on a bidirectional stream of its own.
2731 ///
2732 /// REQUEST_OK or REQUEST_ERROR comes back on the returned
2733 /// [`RequestStream`] — draft-18 folded PUBLISH_OK into REQUEST_OK — and
2734 /// [`publish_done`](Self::publish_done) is written back on it when the
2735 /// publication ends, so the handle must be held for as long as the
2736 /// publication lasts.
2737 pub async fn publish(
2738 &mut self,
2739 track_namespace: TrackNamespace,
2740 track_name: Vec<u8>,
2741 track_alias: VarInt,
2742 parameters: Vec<KeyValuePair>,
2743 track_properties: Vec<KeyValuePair>,
2744 ) -> Result<RequestStream, ConnectionError> {
2745 let mut halves = self.open_request_bi().await?;
2746 let (req_id, msg) = Self::or_abandon(
2747 &mut halves,
2748 self.endpoint.publish(
2749 track_namespace,
2750 track_name,
2751 track_alias,
2752 parameters,
2753 track_properties,
2754 ),
2755 )?;
2756 self.begin_request(halves, RequestKind::Publish, req_id, &msg).await
2757 }
2758
2759 /// Send a PUBLISH_DONE on the request stream the PUBLISH opened.
2760 ///
2761 /// PUBLISH_DONE is a response and carries no request id on the wire, so
2762 /// the stream is the only thing that says which publication ended. The id
2763 /// the endpoint needs is taken off `stream`, which makes the correlation
2764 /// unforgeable — there is no way to name one request and write on
2765 /// another's stream.
2766 pub async fn publish_done(
2767 &mut self,
2768 stream: &mut RequestStream,
2769 status_code: VarInt,
2770 stream_count: VarInt,
2771 reason_phrase: Vec<u8>,
2772 ) -> Result<(), ConnectionError> {
2773 let request_id = stream.request_id();
2774 let msg = self.endpoint.send_publish_done(
2775 request_id,
2776 status_code,
2777 stream_count,
2778 reason_phrase,
2779 )?;
2780 self.send_on_request_stream(stream, &msg).await
2781 }
2782
2783 // -- Data streams -----------------------------------------------
2784
2785 /// Open a new unidirectional stream for sending subgroup data.
2786 pub async fn open_subgroup_stream(
2787 &self,
2788 header: &AnySubgroupHeader,
2789 ) -> Result<FramedSendStream, ConnectionError> {
2790 let send = self.transport.open_uni().await?;
2791 let mut framed = FramedSendStream::new(send, self.draft);
2792 let sid = framed.stream_id();
2793 framed.write_subgroup_header(header).await?;
2794 self.emit(ClientEvent::StreamOpened {
2795 direction: Direction::Send,
2796 stream_kind: StreamKind::Subgroup,
2797 stream_id: sid,
2798 });
2799 self.emit(ClientEvent::DataStreamHeader {
2800 stream_id: sid,
2801 direction: Direction::Send,
2802 header: header.clone(),
2803 });
2804 Ok(framed)
2805 }
2806
2807 /// Open a new unidirectional stream for sending a FETCH's objects.
2808 ///
2809 /// The objects answering a FETCH do not go on the request's own stream:
2810 /// they go on a unidirectional stream of their own, which opens with a
2811 /// FETCH_HEADER naming the request they belong to. This writes that header
2812 /// and hands back the stream, the same way
2813 /// [`open_subgroup_stream`](Self::open_subgroup_stream) does for a
2814 /// subgroup.
2815 ///
2816 /// The caller owns the stream that comes back. Nothing here remembers
2817 /// which request it belongs to, so an endpoint serving several fetches at
2818 /// once keeps its own map from Request ID to stream.
2819 pub async fn open_fetch_stream(
2820 &self,
2821 header: &AnyFetchHeader,
2822 ) -> Result<FramedSendStream, ConnectionError> {
2823 let send = self.transport.open_uni().await?;
2824 let mut framed = FramedSendStream::new(send, self.draft);
2825 let sid = framed.stream_id();
2826 framed.write_fetch_header(header).await?;
2827 self.emit(ClientEvent::StreamOpened {
2828 direction: Direction::Send,
2829 stream_kind: StreamKind::Fetch,
2830 stream_id: sid,
2831 });
2832 Ok(framed)
2833 }
2834
2835 /// Open the data stream for a FETCH this endpoint is answering, and keep
2836 /// the handle on the request.
2837 ///
2838 /// The same stream [`open_fetch_stream`](Self::open_fetch_stream) returns,
2839 /// parked on the request stream it belongs to. That is what lets a rule
2840 /// about the fetch reach the objects it is serving: a refused
2841 /// REQUEST_UPDATE has to reset this stream, and the connection cannot
2842 /// reset a handle the caller walked away with.
2843 ///
2844 /// Write objects through
2845 /// [`RequestStream::fetch_data`](RequestStream::fetch_data), or through
2846 /// the borrow this returns.
2847 pub async fn open_fetch_stream_on<'s>(
2848 &self,
2849 stream: &'s mut RequestStream,
2850 header: &AnyFetchHeader,
2851 ) -> Result<&'s mut FramedSendStream, ConnectionError> {
2852 let framed = self.open_fetch_stream(header).await?;
2853 stream.fetch_data = Some(framed);
2854 Ok(stream.fetch_data.as_mut().expect("just stored"))
2855 }
2856
2857 /// Accept an incoming unidirectional data stream and read its subgroup
2858 /// header.
2859 ///
2860 /// Streams the peer opened before its control stream are returned first,
2861 /// in arrival order, before any new one is accepted from the transport:
2862 /// [`connect`](Self::connect) had to look at them to find the control
2863 /// stream and set the rest aside rather than drop them. They are
2864 /// otherwise ordinary — the type varint `connect` read is still on the
2865 /// front of each one.
2866 pub async fn accept_subgroup_stream(
2867 &self,
2868 ) -> Result<(AnySubgroupHeader, FramedRecvStream), ConnectionError> {
2869 let mut framed = match self.take_deferred_uni() {
2870 Some(framed) => framed,
2871 None => FramedRecvStream::new(self.transport.accept_uni().await?, self.draft),
2872 };
2873 let sid = framed.stream_id();
2874 let header = framed.read_subgroup_header().await?;
2875 self.emit(ClientEvent::StreamOpened {
2876 direction: Direction::Receive,
2877 stream_kind: StreamKind::Subgroup,
2878 stream_id: sid,
2879 });
2880 self.emit(ClientEvent::DataStreamHeader {
2881 stream_id: sid,
2882 direction: Direction::Receive,
2883 header: header.clone(),
2884 });
2885 // The track is resolved here and not inside the stream: it takes the
2886 // endpoint's alias table, which a stream handle has no way back to.
2887 // Handed over rather than offered, so measuring is not something a
2888 // caller has to remember to ask for.
2889 if let Some(objects) = self.endpoint.track_objects(header.track_alias()) {
2890 framed.measure_objects_against(objects, header.group_id());
2891 }
2892 Ok((header, framed))
2893 }
2894
2895 /// Accept the next unidirectional stream and read its fetch header.
2896 ///
2897 /// [`accept_subgroup_stream`](Self::accept_subgroup_stream)'s twin. The two
2898 /// are separate because the header decides how every object after it is
2899 /// framed, so a caller has to know which it is expecting before the first
2900 /// byte is read.
2901 ///
2902 /// Objects come off the returned stream with
2903 /// [`FramedRecvStream::read_fetch_object`].
2904 pub async fn accept_fetch_stream(
2905 &self,
2906 ) -> Result<(AnyFetchHeader, FramedRecvStream), ConnectionError> {
2907 let mut framed = match self.take_deferred_uni() {
2908 Some(framed) => framed,
2909 None => FramedRecvStream::new(self.transport.accept_uni().await?, self.draft),
2910 };
2911 let sid = framed.stream_id();
2912 let header = framed.read_fetch_header().await?;
2913 self.emit(ClientEvent::StreamOpened {
2914 direction: Direction::Receive,
2915 stream_kind: StreamKind::Fetch,
2916 stream_id: sid,
2917 });
2918 self.emit(ClientEvent::FetchStreamHeader {
2919 stream_id: sid,
2920 direction: Direction::Receive,
2921 header: header.clone(),
2922 });
2923 // A fetch header goes out as `FetchStreamHeader`; `DataStreamHeader`
2924 // carries an `AnySubgroupHeader` and cannot express one. What
2925 // `accept_subgroup_stream` does beyond this - the forwarding-preference
2926 // note, the object measurement - is about a subgroup and has no
2927 // counterpart on a fetch stream.
2928 Ok((header, framed))
2929 }
2930
2931 /// Take the oldest stream [`connect`](Self::connect) set aside, if any.
2932 ///
2933 /// Synchronous on purpose: the guard is dropped before the caller awaits,
2934 /// so the lock is never held across a suspension point. A poisoned lock
2935 /// is recovered rather than propagated — nothing here can leave the queue
2936 /// in a state a later reader could be misled by, since the only mutation
2937 /// is a `pop_front`.
2938 fn take_deferred_uni(&self) -> Option<FramedRecvStream> {
2939 self.deferred_uni.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).pop_front()
2940 }
2941
2942 /// How many unidirectional streams [`connect`](Self::connect) set aside
2943 /// and [`accept_subgroup_stream`](Self::accept_subgroup_stream) has not
2944 /// yet handed back.
2945 ///
2946 /// Zero for a peer that opened its control stream first, which is the
2947 /// ordinary case.
2948 pub fn deferred_stream_count(&self) -> usize {
2949 self.deferred_uni.lock().unwrap_or_else(|poisoned| poisoned.into_inner()).len()
2950 }
2951
2952 /// Send an object via datagram.
2953 ///
2954 /// The header goes through `AnyDatagramHeader::encode`, which refuses a
2955 /// header whose Object Status the framing it names cannot carry. Such a
2956 /// header errors here and nothing is sent, rather than going out as an
2957 /// ordinary payload datagram with the status quietly dropped.
2958 pub fn send_datagram(
2959 &self,
2960 header: &AnyDatagramHeader,
2961 payload: &[u8],
2962 ) -> Result<(), ConnectionError> {
2963 let mut buf = Vec::new();
2964 header.encode(&mut buf)?;
2965 buf.extend_from_slice(payload);
2966 self.emit(ClientEvent::DatagramReceived {
2967 direction: Direction::Send,
2968 header: header.clone(),
2969 payload_len: payload.len(),
2970 });
2971 self.transport.send_datagram(bytes::Bytes::from(buf))?;
2972 Ok(())
2973 }
2974
2975 /// Receive a datagram and decode it whole.
2976 ///
2977 /// Errors with [`ConnectionError::EmptyDatagramProperties`] on a datagram
2978 /// with the PROPERTIES bit set and a Properties Length of 0, which
2979 /// [`Connection::close_for_data_stream`] closes the session over.
2980 ///
2981 /// Errors with [`ConnectionError::PropertiesOnNonNormalStatus`] on a
2982 /// datagram carrying properties on a status other than Normal, which
2983 /// [`Connection::close_for_data_stream`] closes the session over.
2984 ///
2985 /// A payload after a status is returned as
2986 /// [`ConnectionError::PayloadOnStatusDatagram`], and the session stays open.
2987 /// The codec's `DatagramHeader::decode_object` then reads the whole
2988 /// datagram. A Prior Group ID Gap or Prior Object ID Gap fault, and a
2989 /// property block that does not parse, are returned as
2990 /// [`ConnectionError::MalformedTrackDatagram`], for
2991 /// [`Connection::requests_to_cancel`]; its other refusals as
2992 /// [`ConnectionError::Codec`].
2993 pub async fn recv_datagram(&self) -> Result<(AnyDatagramHeader, Bytes), ConnectionError> {
2994 let data = self.transport.recv_datagram().await?;
2995 let mut cursor = &data[..];
2996 let header = AnyDatagramHeader::decode(self.draft, &mut cursor)?;
2997 let consumed = data.len() - cursor.len();
2998 let payload = data.slice(consumed..);
2999 self.emit(ClientEvent::DatagramReceived {
3000 direction: Direction::Receive,
3001 header: header.clone(),
3002 payload_len: payload.len(),
3003 });
3004 // Refutable only in a build with more than one draft enabled;
3005 // in a single-draft build `AnyDatagramHeader` has one variant.
3006 #[allow(irrefutable_let_patterns)]
3007 if let AnyDatagramHeader::Draft18(h) = &header {
3008 // draft-18 Section 11.3.1: "If an endpoint receives a datagram with the
3009 // PROPERTIES bit set and an Properties Length of 0, it MUST close
3010 // the session with a PROTOCOL_VIOLATION."
3011 if !h.properties_block_well_formed() {
3012 return Err(ConnectionError::EmptyDatagramProperties {
3013 object_id: h.object_id.into_inner(),
3014 });
3015 }
3016 // draft-18 Section 11.3.1: "If an Object Datagram includes both the
3017 // STATUS bit and PROPERTIES bit, and the Object Status is not Normal
3018 // (0x0), the endpoint MUST close the session with a PROTOCOL_VIOLATION, because
3019 // only Normal Objects can have Properties."
3020 if !h.properties_permitted() {
3021 return Err(ConnectionError::PropertiesOnNonNormalStatus {
3022 object_id: h.object_id.into_inner(),
3023 properties_len: h.properties.len(),
3024 status: h.status(),
3025 });
3026 }
3027 if !h.permits_payload() && !payload.is_empty() {
3028 return Err(ConnectionError::PayloadOnStatusDatagram {
3029 object_id: h.object_id.into_inner(),
3030 payload_len: payload.len(),
3031 status: h.object_status,
3032 });
3033 }
3034 // The codec's whole-datagram read, for the rules a header alone
3035 // does not show. A gap fault is returned with the datagram's alias,
3036 // for `requests_to_cancel`.
3037 let whole =
3038 moqtap_codec::draft18::data_stream::DatagramHeader::decode_object(&mut &data[..]);
3039 match whole {
3040 Ok(_) => {}
3041 Err(
3042 error @ (CodecError::MalformedTrack { .. }
3043 | CodecError::MalformedObjectProperties { .. }),
3044 ) => {
3045 return Err(ConnectionError::MalformedTrackDatagram {
3046 track_alias: h.track_alias.into_inner(),
3047 error,
3048 });
3049 }
3050 Err(error) => return Err(ConnectionError::Codec(error)),
3051 }
3052 }
3053 // A datagram is a whole object, so the connection can measure it
3054 // without help from the caller. It cannot *answer* the condition,
3055 // though: the answer is a reset of a request stream the caller holds,
3056 // so both data paths report and neither withdraws - see
3057 // `Connection::requests_to_cancel`.
3058 let meta = header.meta();
3059 self.endpoint.note_received_object(
3060 meta.track_alias,
3061 ObjectLocation { group: meta.group_id, object: meta.object_id },
3062 object_role(meta.status),
3063 )?;
3064 Ok((header, payload))
3065 }
3066
3067 // -- Accessors --------------------------------------------------
3068
3069 /// Access the underlying endpoint state machine.
3070 pub fn endpoint(&self) -> &Endpoint {
3071 &self.endpoint
3072 }
3073
3074 /// Mutable access to the endpoint state machine.
3075 pub fn endpoint_mut(&mut self) -> &mut Endpoint {
3076 &mut self.endpoint
3077 }
3078
3079 /// The SETUP message the server answered the handshake with.
3080 ///
3081 /// `SERVER_SETUP` through draft-16, the server's half of the unified
3082 /// `SETUP` from draft-17. [`AnyControlMessage::fields`] renders it under
3083 /// this draft's own parameter names, in the order they arrived.
3084 pub fn server_setup(&self) -> &AnyControlMessage {
3085 &self.server_setup
3086 }
3087
3088 /// The framed wire bytes of [`Self::server_setup`], as they arrived.
3089 ///
3090 /// Kept beside the decoded form because the encoding is evidence the
3091 /// decoding discards: two relays sending the same parameter can still
3092 /// disagree on how wide a varint they wrote it in.
3093 pub fn server_setup_raw(&self) -> Option<&[u8]> {
3094 self.server_setup_raw.as_deref()
3095 }
3096
3097 /// Returns the draft version this connection is using.
3098 pub fn draft(&self) -> DraftVersion {
3099 self.draft
3100 }
3101
3102 /// Close the session on the wire when the endpoint says a violation is
3103 /// fatal to it.
3104 ///
3105 /// [`EndpointError::session_error_code`] answers `Some` for exactly the
3106 /// errors draft-18 tells the receiver to close the session over, and the
3107 /// endpoint has already moved its own state machine to Closed by the time
3108 /// this runs. Without this step that move is purely internal: the local
3109 /// endpoint refuses to start anything new while the peer, which is the one
3110 /// that broke the rule, sees a session that is still open and goes on
3111 /// sending. "MUST close the session with a PROTOCOL_VIOLATION" is a
3112 /// statement about the wire, so it takes a CONNECTION_CLOSE to satisfy it.
3113 ///
3114 /// The reason phrase is the error's own `Display` text, which names the
3115 /// message and the rule rather than repeating the numeric code the close
3116 /// already carries.
3117 ///
3118 /// Errors that answer `None` are recoverable and nothing is sent.
3119 fn close_for(&self, err: &EndpointError) {
3120 if let Some(code) = err.session_error_code() {
3121 // QUIC application error codes are 62-bit; every code in this
3122 // registry is far below `u32::MAX`, and saturating rather than
3123 // truncating means a future code that is not could never be
3124 // reported as a different, assigned one.
3125 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
3126 self.close(wire_code, err.to_string().as_bytes());
3127 }
3128 }
3129
3130 /// [`close_for`](Self::close_for), then the error unchanged, for the
3131 /// common case where the endpoint's error is also what the caller returns.
3132 fn close_if_session_fatal(&self, err: EndpointError) -> ConnectionError {
3133 self.close_for(&err);
3134 ConnectionError::Endpoint(err)
3135 }
3136
3137 /// Which of this draft's *own* `ConnectionError` variants this error is,
3138 /// and which kind of thing it says.
3139 ///
3140 /// The ten every draft carries answer `None` here: [`AnyConnectionError`]
3141 /// classifies those itself, once, and never asks a draft about them. What
3142 /// is left splits two ways, and the split is the reason this function
3143 /// exists — before it, both halves reached a caller as a sentence and read
3144 /// exactly alike. A [`LocalRefusal`] is this endpoint declining to write
3145 /// something, so nothing reached the wire and no relay is implicated; a
3146 /// [`PeerViolation`] is a peer having done something draft-18 forbids, and
3147 /// carries the session error code draft-18's own text answers it with.
3148 ///
3149 /// Matched exhaustively, with no wildcard arm and deliberately so: a
3150 /// variant added to this draft's error type has to arrive here as a compile
3151 /// error, beside the doc comment quoting the sentence it enforces, rather
3152 /// than as a silent [`ErrorCause::Unclassified`] in the facade.
3153 ///
3154 /// [`AnyConnectionError`]: crate::dispatch::AnyConnectionError
3155 /// [`ErrorCause::Unclassified`]: crate::dispatch::ErrorCause::Unclassified
3156 /// [`LocalRefusal`]: crate::above_codec_rules::DraftSpecificCause::LocalRefusal
3157 /// [`PeerViolation`]: crate::above_codec_rules::DraftSpecificCause::PeerViolation
3158 pub fn draft_specific_cause(
3159 err: &ConnectionError,
3160 ) -> Option<crate::above_codec_rules::DraftSpecificCause> {
3161 use crate::above_codec_rules::{AboveCodecRule, DraftSpecificCause};
3162 use moqtap_codec::draft18::error_codes::SessionErrorCode;
3163
3164 match err {
3165 ConnectionError::Endpoint(_)
3166 | ConnectionError::Codec(_)
3167 | ConnectionError::Transport(_)
3168 | ConnectionError::VarInt(_)
3169 | ConnectionError::NoControlStream
3170 | ConnectionError::UnexpectedEnd
3171 | ConnectionError::StreamFinished
3172 | ConnectionError::InvalidAddress(_)
3173 | ConnectionError::TlsConfig(_)
3174 | ConnectionError::DataStreamState(_) => None,
3175 // This build decoding a message and then failing to narrow it to
3176 // its own draft. Nothing reached the wire and no peer is
3177 // implicated, which is the whole reason it is not
3178 // `ConnectionError::Codec`: under that name it would carry
3179 // `Some(PROTOCOL_VIOLATION)` out of `codec_session_error_code` and
3180 // publish a relay for this build's defect. See the variant's own
3181 // doc.
3182 ConnectionError::ControlMessageNarrowing => {
3183 Some(crate::above_codec_rules::DraftSpecificCause::LocalRefusal)
3184 }
3185 // Section 11.2.1.2 states the rule and names the code in the same
3186 // sentence. The codec refuses such an Object only as a bare
3187 // `InvalidField`, so this layer is the one that names the rule,
3188 // and `close_for_data_stream` performs the close by
3189 // reading this same answer.
3190 ConnectionError::PropertiesOnNonNormalStatus { .. } => {
3191 Some(DraftSpecificCause::PeerViolation {
3192 rule: AboveCodecRule::PropertiesOnNonNormalStatus,
3193 close: Some(SessionErrorCode::ProtocolViolation.as_u64()),
3194 })
3195 }
3196 // draft-18 Section 11.3.1 answers it with a close, which this variant
3197 // names. See the variant's own doc.
3198 ConnectionError::EmptyDatagramProperties { .. } => Some(DraftSpecificCause::Codec {
3199 error: CodecError::InvalidField,
3200 close: Some(SessionErrorCode::ProtocolViolation.as_u64()),
3201 }),
3202 // The codec's refusal, carried with the alias its error does
3203 // not name. draft-18 Section 2.4.2 answers a Malformed Track by
3204 // cancelling requests, and `codec_session_error_code` names no
3205 // close for it.
3206 ConnectionError::MalformedTrackDatagram { error, .. } => {
3207 Some(DraftSpecificCause::Codec {
3208 close: Self::codec_session_error_code(error).map(|code| code.as_u64()),
3209 error: error.clone(),
3210 })
3211 }
3212 // Section 11.2.1.1 states this one as a property of a conforming
3213 // Object rather than as one of the cases a draft answers with a
3214 // close. That phrase carries no quotation marks and must not: it
3215 // is this crate naming a shape of drafting, and the marks would
3216 // file the words on the section named right beside them — which is
3217 // the one section here that pointedly does not carry them. The
3218 // datagram is refused and the session is left running. `None` is
3219 // that reading, and it is what keeps a relay from being published
3220 // for a rule its draft attaches no consequence to.
3221 ConnectionError::PayloadOnStatusDatagram { .. } => {
3222 Some(DraftSpecificCause::PeerViolation {
3223 rule: AboveCodecRule::PayloadOnStatusDatagram,
3224 close: None,
3225 })
3226 }
3227 // Section 3.3: "Bidirectional streams MUST NOT begin with any
3228 // other message type unless negotiated. If they do, the peer MUST
3229 // close the Session with a PROTOCOL_VIOLATION." The session has
3230 // already been closed on the wire by the time this is returned, so
3231 // the code is carried here for a caller to read which rule was
3232 // answered, not for it to answer one again.
3233 ConnectionError::NonRequestOnRequestStream(_) => {
3234 Some(DraftSpecificCause::PeerViolation {
3235 rule: AboveCodecRule::BidiStreamOpener,
3236 close: Some(SessionErrorCode::ProtocolViolation.as_u64()),
3237 })
3238 }
3239 // Three ways of handing this endpoint a message it will not write,
3240 // and one answer: nothing reached the wire, so nothing here is
3241 // evidence about a peer. Two are messages put on the control stream
3242 // that belong on a request stream of their own; the third is a
3243 // `respond_*` helper pointed at a request this endpoint opened,
3244 // which only the endpoint a request was opened *toward* may answer.
3245 ConnectionError::RequestOnControlStream(_)
3246 | ConnectionError::RequestStreamMessageOnControlStream(_)
3247 | ConnectionError::RespondedToOwnRequest(_) => Some(DraftSpecificCause::LocalRefusal),
3248 }
3249 }
3250
3251 /// The code to close the session with when a control message could not be
3252 /// decoded because the peer broke a rule draft-18 answers with a close.
3253 ///
3254 /// Every variant listed here comes from a sentence in the draft that names
3255 /// the consequence: the reason phrase and GOAWAY URI maxima (Section 1.4.4
3256 /// and Section 10.4), the KVP value maximum and the delta-encoded
3257 /// type overflow (Section 1.4.3), the duplicate-parameter rule (Section
3258 /// 10.2), the Track Namespace field, count and length rules
3259 /// (Section 2.4.1), and the Object ID delta wrap (Section 11.4.2). Each of
3260 /// those reads "MUST close the session with a PROTOCOL_VIOLATION".
3261 ///
3262 /// The Object ID wrap is the one that arrives here from a data stream
3263 /// rather than a control message, and it is draft-18 and draft-19 only:
3264 /// "The Object ID Delta + 1 is added to the previous Object ID in the
3265 /// Subgroup stream if there was one... If the resulting Object ID would be
3266 /// greater than 2^64 - 1, the endpoint MUST close the session with a
3267 /// PROTOCOL_VIOLATION." Draft-17 describes the same arithmetic and states
3268 /// no consequence for overflowing it, so its connection deliberately leaves
3269 /// the wrap off this list and treats it as a decode failure alone.
3270 ///
3271 /// One more rule reaches this table without naming a code: "An endpoint
3272 /// that receives an unknown message type MUST close the session", stated in
3273 /// those words by all the drafts. Protocol Violation is what carries
3274 /// it, as it does on every draft below this one.
3275 ///
3276 /// `None` for everything else, including [`CodecError::InvalidField`]. That
3277 /// variant is shared by a dozen unrelated malformations, only some of which
3278 /// the draft answers with a close, so treating it as fatal would close
3279 /// sessions the draft does not ask to be closed. Splitting it is the way to
3280 /// bring the rest of those rules under this function; widening the match is
3281 /// not — the Object ID wrap is answerable here because it has a variant of
3282 /// its own rather than being one more reading of `InvalidField`.
3283 pub fn codec_session_error_code(
3284 err: &CodecError,
3285 ) -> Option<moqtap_codec::draft18::error_codes::SessionErrorCode> {
3286 use moqtap_codec::draft18::error_codes::SessionErrorCode;
3287 use moqtap_codec::kvp::KvpError;
3288 match err {
3289 // The declared Length disagreeing with the fields, which every
3290 // draft answers with a close. Drafts 07 through 10 name no code for
3291 // it, so it takes the one their other unnamed rules take.
3292 // A Filter Type outside the four this draft assigns, Section 5.1.2:
3293 // "An endpoint that receives a filter type other than the above MUST
3294 // close the session with PROTOCOL_VIOLATION."
3295 //
3296 // Drafts 07 through 14 carried the Filter Type as a field of
3297 // SUBSCRIBE. From draft-15 it is the first field inside the
3298 // length-prefixed filter parameter, where a codec that carries the
3299 // value as opaque bytes never reads it — the rule did not change and
3300 // the place it has to be enforced did.
3301 CodecError::InvalidFilterType(_) => Some(SessionErrorCode::ProtocolViolation),
3302 // An AbsoluteRange filter whose End Group Delta carries the range
3303 // past the end of the number space, Section 5.1.2: "the last Group
3304 // ID to be delivered will be the Group ID in Start Location plus the
3305 // End Group Delta. If the resulting Group ID would be greater than
3306 // 2^64 - 1, the endpoint MUST close the session with a
3307 // PROTOCOL_VIOLATION." New in draft-18; draft-17, which introduced
3308 // the delta, states no such sentence.
3309 CodecError::FilterEndGroupOverflow { .. } => {
3310 Some(SessionErrorCode::ProtocolViolation)
3311 }
3312 // A Fetch Type outside the three this draft assigns: "An endpoint
3313 // that receives a Fetch Type other than 0x1, 0x2 or 0x3 MUST close
3314 // the session with a PROTOCOL_VIOLATION." The value decides which
3315 // fields follow it — a Standalone fetch carries a track name and a
3316 // range where a joining fetch carries a Request ID and an offset —
3317 // so a reader that cannot name the type cannot find the end of the
3318 // message.
3319 CodecError::InvalidFetchType(_) => Some(SessionErrorCode::ProtocolViolation),
3320 CodecError::ControlMessageLengthMismatch { .. } => {
3321 Some(SessionErrorCode::ProtocolViolation)
3322 }
3323 CodecError::KeyDeltaOverflow(..)
3324 | CodecError::DuplicateParameter(_)
3325 | CodecError::TrackNameTooLong
3326 | CodecError::InvalidNamespaceTupleSize(_)
3327 | CodecError::ReasonPhraseTooLong
3328 | CodecError::GoAwayUriTooLong
3329 | CodecError::UnknownMessageType(_)
3330 | CodecError::Kvp(KvpError::ValueTooLong(_))
3331 | CodecError::EmptyNamespaceField
3332 | CodecError::ObjectIdOverflow(..) => Some(SessionErrorCode::ProtocolViolation),
3333 // An unknown data-plane type. Drafts 17 and later split the sentence
3334 // in two: Section 3.4 for streams, Section 11 for datagrams, both
3335 // ending "MUST close the session" and neither naming a code, so both
3336 // take the one this draft's other unnamed rules take.
3337 // A Message Parameter whose value is outside the range its type
3338 // allows: FORWARD in Section 10.2.12 and GROUP_ORDER in Section 10.2.8.
3339 // Each states that a receiver "MUST close the session with
3340 // PROTOCOL_VIOLATION".
3341 CodecError::ParameterValueOutOfRange { .. } => {
3342 Some(SessionErrorCode::ProtocolViolation)
3343 }
3344 // A Track Extension or Track Property whose value is outside the
3345 // range its type allows: DEFAULT_PUBLISHER_GROUP_ORDER in Section 12.5
3346 // and DYNAMIC_GROUPS in Section 12.6.
3347 // Each states that a receiver "MUST close the session with
3348 // PROTOCOL_VIOLATION".
3349 //
3350 // A separate arm from the parameter rule above because the two
3351 // registries are separate: 0x22 is GROUP_ORDER as a parameter and
3352 // DEFAULT_PUBLISHER_GROUP_ORDER as a Track Property, and a log that
3353 // named only the number would not say which.
3354 CodecError::TrackPropertyValueOutOfRange { .. } => {
3355 Some(SessionErrorCode::ProtocolViolation)
3356 }
3357 CodecError::UnknownStreamType(_) | CodecError::UnknownDatagramType(_) => {
3358 Some(SessionErrorCode::ProtocolViolation)
3359 }
3360 // A Type inside a form this draft defines but on a list it names as
3361 // invalid: Section 11.4.2 for a subgroup header whose SUBGROUP_ID_MODE
3362 // is the reserved 0b11, Section 11.3.1 for a datagram asking to be both
3363 // an object status and an end-of-group marker. Unlike the rule above,
3364 // these two name their code outright.
3365 CodecError::InvalidStreamTypeValue { .. }
3366 | CodecError::InvalidDatagramTypeValue { .. } => {
3367 Some(SessionErrorCode::ProtocolViolation)
3368 }
3369 // A key-value pair whose value is not the serialization its own
3370 // Type defines, Section 1.4.3: "If a receiver understands a Type,
3371 // and the following Value or Length/Value does not match the
3372 // serialization defined by that Type, the receiver MUST close the
3373 // session with error code KEY_VALUE_FORMATTING_ERROR."
3374 //
3375 // Section 10.2.2 states the same answer for the one structure this
3376 // draft spells out: "If the Token structure cannot be decoded, the
3377 // receiver MUST close the Session with KEY_VALUE_FORMATTING_ERROR."
3378 //
3379 // The one rule in this table that names a code other than Protocol
3380 // Violation.
3381 CodecError::KeyValueFormatting { .. }
3382 // A filter parameter whose value is not a filter reaches the same
3383 // sentence. Drafts 15 and 16 answered it with PROTOCOL_VIOLATION
3384 // instead, on the strength of a sentence of the parameter's own that
3385 // this draft dropped; what remains is the general rule above, so the
3386 // code changed with it.
3387 | CodecError::SubscriptionFilterMalformed { .. } => {
3388 Some(SessionErrorCode::KeyValueFormattingError)
3389 }
3390 // A Message Parameter whose type this draft does not define, Section
3391 // 10.2: "All Message Parameters MUST be defined in the negotiated
3392 // version of MOQT or negotiated via Setup Options. An endpoint that
3393 // receives an unknown Message Parameter MUST close the session with
3394 // PROTOCOL_VIOLATION."
3395 //
3396 // One namespace only. This draft also says a receiver ignores an
3397 // unrecognised Setup Option, so an unknown type in a SETUP is carried and
3398 // the codec never raises this for one.
3399 CodecError::UnknownMessageParameter(_) => Some(SessionErrorCode::ProtocolViolation),
3400 // A Message Parameter in a message type its own definition does not
3401 // name, Section 10.2.1: "Each Message Parameter definition indicates
3402 // the message types in which it can appear. If it appears in some
3403 // other type of message, the receiving endpoint MUST close the
3404 // connection with a PROTOCOL_VIOLATION."
3405 //
3406 // Draft-16 and every draft before it end that same sentence "it MUST
3407 // be ignored", so this is a rule whose answer reverses rather than
3408 // one that arrives.
3409 CodecError::ParameterOutOfScope { .. } => Some(SessionErrorCode::ProtocolViolation),
3410 // Everything this draft does not answer, named rather than swept up
3411 // by a wildcard. The arm is exhaustive deliberately: a new
3412 // `CodecError` variant will not compile until it has been placed on
3413 // one side or the other, on this draft, which is the decision a `_`
3414 // arm makes silently and invisibly in every draft module at once.
3415 //
3416 // Adding one variant to `CodecError` produces an `E0004` in every
3417 // draft module that matches it exhaustively, each naming the
3418 // variant that has nowhere to go. That is the whole mechanism.
3419 //
3420 // The nesting stops at `VarInt`, whose variants report how the bytes
3421 // ran out rather than a rule an endpoint states, so there is nothing
3422 // in it for a draft to answer. `Kvp` is spelled out because it does
3423 // carry one.
3424 // Neither field exists from draft-15 on. Forwarding became the
3425 // FORWARD parameter, which carries the same rule in a different
3426 // shape and is answered above under its own variant; Content Exists
3427 // became the presence or absence of a LARGEST_OBJECT parameter.
3428 CodecError::InvalidForward(_)
3429 | CodecError::InvalidContentExists(_)
3430 | CodecError::UnexpectedEnd
3431 | CodecError::MessageTooLong(_)
3432 | CodecError::VarInt(_)
3433 | CodecError::InvalidField
3434 | CodecError::InvalidRange(..)
3435 | CodecError::ParameterLengthMismatch(_)
3436 | CodecError::EndOfTrackObjectId(_)
3437 | CodecError::ParametersOutOfOrder(..)
3438 | CodecError::ExtensionsOnNonExistentObject(_)
3439 | CodecError::InvalidRequiredRequestIdDelta(..)
3440 // The object payload rule, Section 11.2.1.1: "Any object with a status
3441 // code other than zero MUST have an empty payload." A MUST on the
3442 // sender with no receiver action named anywhere — the "SHOULD be
3443 // treated as a protocol error" in the same paragraph belongs to the
3444 // sentence before it, which is about a status value this draft does
3445 // not assign — so an object carrying a payload it may not is refused
3446 // and the session stays open.
3447 | CodecError::PayloadNotPermitted { .. }
3448 // A property block that does not parse makes the Track malformed,
3449 // Section 12.7.
3450 | CodecError::MalformedObjectProperties { .. }
3451 // A Malformed Track (Section 2.4.2) costs the track and not the
3452 // session.
3453 | CodecError::MalformedTrack { .. }
3454 // Raised only by the draft-23 codec.
3455 | CodecError::ControlMessageExceedsLimit { .. }
3456 | CodecError::MissingErrorDetail { .. }
3457 | CodecError::MalformedErrorDetail { .. }
3458 | CodecError::TokenAliasInSetup { .. }
3459 | CodecError::EmptyTrackName
3460 | CodecError::MalformedAuthToken { .. }
3461 | CodecError::TrackPropertiesNotPermitted
3462 | CodecError::DeltaDecodingOverflow { .. }
3463 | CodecError::MalformedObject { .. }
3464 | CodecError::UnknownObjectStatus(_)
3465 | CodecError::UnsupportedDraft(_)
3466 | CodecError::Kvp(
3467 KvpError::MissingLength | KvpError::UnexpectedEnd | KvpError::VarInt(_),
3468 ) => None,
3469 }
3470 }
3471
3472 /// Close the session on the wire when a decode failure is one draft-18
3473 /// answers with a close, and hand the error back unchanged.
3474 ///
3475 /// The codec's counterpart to
3476 /// [`close_if_session_fatal`](Self::close_if_session_fatal). Without it
3477 /// every bound the decoder enforces would stop at *this endpoint refused the
3478 /// frame* while the peer, which is the one that broke the rule, saw a
3479 /// session that was still open and went on sending. "MUST close the session
3480 /// with a PROTOCOL_VIOLATION" is a statement about the wire.
3481 fn close_for_codec(&self, err: ConnectionError) -> ConnectionError {
3482 if let ConnectionError::Codec(inner) = &err {
3483 if let Some(code) = Self::codec_session_error_code(inner) {
3484 // QUIC application error codes are 62-bit; every code in this
3485 // registry is far below `u32::MAX`, and saturating rather than
3486 // truncating means a future code that is not could never be
3487 // reported as a different, assigned one.
3488 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
3489 self.close(wire_code, inner.to_string().as_bytes());
3490 }
3491 }
3492 err
3493 }
3494
3495 /// Name every request whose stream the caller must reset, for a track a
3496 /// data path has just found malformed.
3497 ///
3498 /// Section 2.4.2 answers its whole list of conditions at once: "it MUST
3499 /// cancel any corresponding subscription or fetches for that Track from
3500 /// that publisher". On this draft cancelling a request is a transport
3501 /// operation rather than a message — Section 3.3.2: "Implementations SHOULD cancel requests
3502 /// by abruptly terminating any directions of a stream that are still open
3503 /// by resetting or sending STOP_SENDING."
3504 ///
3505 /// Draft-18 drops the QUIC frame names draft-17 spelled out, and adds a
3506 /// sentence asking the application to pick a relevant error code.
3507 ///
3508 /// # Why this returns ids instead of doing it
3509 ///
3510 /// Because the streams are the caller's. Every request on this draft lives
3511 /// at the front of a bidirectional stream of its own, and
3512 /// [`Connection::recv_on_request_stream`] hands that stream back as a
3513 /// [`RequestStream`]. There is no handle here to reset. So the connection
3514 /// does the half it can — note the track, and work out which requests
3515 /// receive it — and the caller passes each id to
3516 /// [`Connection::cancel_request_stream`], which resets the stream *and*
3517 /// moves the endpoint's record.
3518 ///
3519 /// **This is the one place in this crate where the two halves of an answer
3520 /// are split across the API boundary**, and it is the draft that splits
3521 /// them: drafts 12 through 16 answer with a control message, which the
3522 /// connection owns, so `withdraw_for_data_stream` there does the whole
3523 /// thing.
3524 ///
3525 /// # Both data paths come here
3526 ///
3527 /// Unlike the drafts that answer with a message, where a datagram is read
3528 /// through the connection and answers itself. Here neither path can, for
3529 /// the same reason, so there is one entry point rather than two. Pass it
3530 /// whatever error a read returned; anything that is not this condition
3531 /// gives back an empty list.
3532 ///
3533 /// Empty is not "the track was fine" — it is also what an alias no live
3534 /// binding names gives, and what a track this endpoint only publishes
3535 /// gives.
3536 pub fn requests_to_cancel(&self, err: &ConnectionError) -> Vec<VarInt> {
3537 match err {
3538 ConnectionError::Endpoint(EndpointError::ObjectPastFinalObject { alias, .. }) => {
3539 self.endpoint.requests_for_malformed_track(
3540 *alias,
3541 MalformedTrackCondition::ObjectPastFinalObject,
3542 )
3543 }
3544 ConnectionError::MalformedTrackDatagram { track_alias, error } => {
3545 malformed_track_condition(error).map_or_else(Vec::new, |condition| {
3546 self.endpoint.requests_for_malformed_track(*track_alias, condition)
3547 })
3548 }
3549 _ => Vec::new(),
3550 }
3551 }
3552
3553 /// [`Connection::requests_to_cancel`] for an error read from the subgroup
3554 /// stream `header` opened, which also names the requests to cancel over
3555 /// the [`CodecError::MalformedTrack`] that
3556 /// [`FramedRecvStream::read_subgroup_object`] returns for a Prior Group ID
3557 /// Gap or Prior Object ID Gap fault (draft-18 Section 12.8,
3558 /// draft-18 Section 12.9), and over the
3559 /// [`CodecError::MalformedObjectProperties`] it returns for a property block
3560 /// that does not parse (draft-18 Section 12.7). Those errors name no track, so
3561 /// the header supplies its alias.
3562 ///
3563 /// draft-18 Section 2.4.2: "When a subscriber detects a Malformed Track, it
3564 /// MUST cancel any corresponding subscription or fetches for that Track
3565 /// from that publisher (see Section 3.3.2)". The session stays open.
3566 pub fn requests_to_cancel_for_subgroup_stream(
3567 &self,
3568 header: &AnySubgroupHeader,
3569 err: &ConnectionError,
3570 ) -> Vec<VarInt> {
3571 if let ConnectionError::Codec(codec) = err {
3572 if let Some(condition) = malformed_track_condition(codec) {
3573 return self.endpoint.requests_for_malformed_track(header.track_alias(), condition);
3574 }
3575 }
3576 self.requests_to_cancel(err)
3577 }
3578
3579 /// [`Connection::requests_to_cancel`] for an error read from the fetch stream
3580 /// `header` opened, which also names the requests to cancel over the
3581 /// [`CodecError::MalformedTrack`] that
3582 /// [`FramedRecvStream::read_fetch_object`] returns for a Prior Group ID
3583 /// Gap or Prior Object ID Gap fault (draft-18 Section 12.8,
3584 /// draft-18 Section 12.9), and over the
3585 /// [`CodecError::MalformedObjectProperties`] it returns for a property block
3586 /// that does not parse (draft-18 Section 12.7). Those errors name no track, so
3587 /// the track the FETCH under the header's Request ID asked for is the one.
3588 ///
3589 /// draft-18 Section 2.4.2: "When a subscriber detects a Malformed Track, it
3590 /// MUST cancel any corresponding subscription or fetches for that Track
3591 /// from that publisher (see Section 3.3.2)". The session stays open.
3592 pub fn requests_to_cancel_for_fetch_stream(
3593 &self,
3594 header: &AnyFetchHeader,
3595 err: &ConnectionError,
3596 ) -> Vec<VarInt> {
3597 if let ConnectionError::Codec(codec) = err {
3598 if let Some(condition) = malformed_track_condition(codec) {
3599 return self
3600 .endpoint
3601 .requests_for_malformed_fetch_track(header.request_id(), condition);
3602 }
3603 }
3604 self.requests_to_cancel(err)
3605 }
3606
3607 /// Close the session when a failure raised while reading a *data* stream is
3608 /// one draft-18 answers with a close. Reports whether it closed.
3609 ///
3610 /// [`recv_control`](Self::recv_control) does this for itself, because it
3611 /// owns both the stream and the connection. A data stream does not:
3612 /// [`accept_subgroup_stream`](Self::accept_subgroup_stream) hands the caller
3613 /// a [`FramedRecvStream`], which holds no connection and so cannot close
3614 /// one, and the reads that raise these failures happen there. The caller is
3615 /// the only party holding both halves, which is what this is for.
3616 ///
3617 /// Splitting it this way rather than closing inside the reader keeps a
3618 /// caller that is deliberately permissive — a tool reproducing a capture,
3619 /// say — able to read a violating stream and report it without tearing the
3620 /// session down. The rule is stated at endpoints, and this is where an
3621 /// endpoint decides it is one.
3622 ///
3623 /// Only [`ConnectionError::Codec`] failures are matched, against the same
3624 /// `codec_session_error_code` table the
3625 /// control path uses, so a rule is answered with one code whichever stream
3626 /// carried it. The Object ID delta wrap of Section 11.4.2 is the entry that
3627 /// can only arrive this way.
3628 pub fn close_for_data_stream(&self, err: &ConnectionError) -> bool {
3629 use crate::above_codec_rules::DraftSpecificCause;
3630
3631 match err {
3632 ConnectionError::Codec(inner) => {
3633 let Some(code) = Self::codec_session_error_code(inner) else { return false };
3634 let wire_code = u32::try_from(code.as_u64()).unwrap_or(u32::MAX);
3635 self.close(wire_code, inner.to_string().as_bytes());
3636 true
3637 }
3638 // Not a `Codec` failure: the codec refuses such an Object as a bare
3639 // `InvalidField`, which `read_subgroup_object` narrows to this
3640 // crate's own variant, so the mapping table above never sees it.
3641 //
3642 // The code comes from `draft_specific_cause` rather than from a
3643 // constant here, so this draft's reading of its own sentence is
3644 // written down once and a caller who reads the error as a value
3645 // sees the same code the peer was sent.
3646 ConnectionError::PropertiesOnNonNormalStatus { .. } => {
3647 let Some(DraftSpecificCause::PeerViolation { close: Some(code), .. }) =
3648 Self::draft_specific_cause(err)
3649 else {
3650 return false;
3651 };
3652 // Saturate rather than truncate, so a future code above
3653 // `u32::MAX` is never reported as a different assigned one.
3654 self.close(u32::try_from(code).unwrap_or(u32::MAX), err.to_string().as_bytes());
3655 true
3656 }
3657 // Not a `Codec` failure: the codec refuses the datagram as a bare
3658 // `InvalidField`, which `recv_datagram` names, so the code comes
3659 // from `draft_specific_cause`.
3660 ConnectionError::EmptyDatagramProperties { .. } => {
3661 let Some(DraftSpecificCause::Codec { close: Some(code), .. }) =
3662 Self::draft_specific_cause(err)
3663 else {
3664 return false;
3665 };
3666 self.close(u32::try_from(code).unwrap_or(u32::MAX), err.to_string().as_bytes());
3667 true
3668 }
3669 _ => false,
3670 }
3671 }
3672
3673 /// Close the connection.
3674 pub fn close(&self, code: u32, reason: &[u8]) {
3675 self.emit(ClientEvent::Closed { code, reason: reason.to_vec() });
3676 self.transport.close(code, reason);
3677 }
3678}
3679
3680/// The Malformed Track condition a codec refusal on a data path is, on this
3681/// draft, or `None` for a refusal that is not one.
3682fn malformed_track_condition(err: &CodecError) -> Option<MalformedTrackCondition> {
3683 match err {
3684 CodecError::MalformedTrack { .. } => Some(MalformedTrackCondition::PriorGap),
3685 // draft-18 Section 12.7: "A Key-Value-Pair cannot be parsed."
3686 CodecError::MalformedObjectProperties { .. } => {
3687 Some(MalformedTrackCondition::MalformedPropertyBlock)
3688 }
3689 _ => None,
3690 }
3691}
3692
3693#[cfg(test)]
3694mod tests {
3695 use super::*;
3696
3697 /// This build failing to narrow a message it decoded is never a finding
3698 /// about the peer.
3699 ///
3700 /// The arm that raises `ControlMessageNarrowing` is unreachable — this
3701 /// draft's decoder can only hand back this draft's variant — and nothing
3702 /// pins that. What is pinned here is the half that matters.
3703 /// `CodecError::UnknownMessageType(0)` is what the arm must not raise:
3704 /// `codec_session_error_code` answers it `Some(PROTOCOL_VIOLATION)` on
3705 /// every draft in range, so the day the narrowing failed a conformance
3706 /// probe would publish a relay for sending a control message type this
3707 /// draft does not assign — with `0x00` attached as the codepoint that
3708 /// proved it, which is an accusation better evidenced than any real one
3709 /// this build makes. The section stating that rule is numbered differently
3710 /// on every draft, and the point does not turn on the number.
3711 #[test]
3712 fn a_message_this_build_could_not_narrow_names_nobody() {
3713 use crate::dispatch::{AnyConnectionError, ErrorCause};
3714
3715 let err: AnyConnectionError = ConnectionError::ControlMessageNarrowing.into();
3716 assert!(err.is_local(), "a narrowing this build could not do is this build's");
3717 assert_eq!(
3718 err.cause(),
3719 &ErrorCause::Facade,
3720 "nothing reached the wire, so there is no rule and no close code to read"
3721 );
3722 }
3723
3724 /// Draft-18 uses MoQT's variable-length integer, whose length is the
3725 /// number of leading 1 bits in the first byte, not RFC 9000's two-bit
3726 /// prefix. Control framing measures the type field with it before any
3727 /// bytes past the first have arrived.
3728 #[test]
3729 fn varint_len_follows_the_moqt_encoding() {
3730 let draft = DraftVersion::Draft18;
3731 assert_eq!(draft.varint_len(0x00), 1);
3732 assert_eq!(draft.varint_len(0x7F), 1);
3733 assert_eq!(draft.varint_len(0x80), 2);
3734 assert_eq!(draft.varint_len(0xBF), 2);
3735 assert_eq!(draft.varint_len(0xC0), 3);
3736 assert_eq!(draft.varint_len(0xFF), 9);
3737 // SETUP's type id, 0x2F00, is two bytes here and four under RFC 9000.
3738 assert_eq!(draft.varint_len(0xAF), 2);
3739 }
3740
3741 #[test]
3742 fn client_config_alpn_quic_draft18() {
3743 let config = ClientConfig {
3744 draft: DraftVersion::Draft18,
3745 transport: TransportType::Quic,
3746 skip_cert_verification: false,
3747 ca_certs: Vec::new(),
3748 setup_parameters: Vec::new(),
3749 };
3750 assert_eq!(config.alpn(), vec![b"moqt-18".to_vec()]);
3751 }
3752
3753 #[test]
3754 fn client_config_alpn_webtransport() {
3755 let config = ClientConfig {
3756 draft: DraftVersion::Draft18,
3757 transport: TransportType::WebTransport { url: "https://example.com".to_string() },
3758 skip_cert_verification: false,
3759 ca_certs: Vec::new(),
3760 setup_parameters: Vec::new(),
3761 };
3762 assert_eq!(config.alpn(), vec![b"h3".to_vec()]);
3763 }
3764
3765 /// `MOQT_ALPN` is the ALPN a client configured for this draft offers.
3766 #[test]
3767 fn moqt_alpn_is_the_one_a_client_offers() {
3768 // A literal on its own is what let this constant keep `moq-00` for
3769 // five drafts after draft-15 stopped using it, so the value is
3770 // checked against what a client configured for this draft actually
3771 // puts on the wire, and only then against the literal.
3772 let config = ClientConfig {
3773 draft: DraftVersion::Draft18,
3774 transport: TransportType::Quic,
3775 skip_cert_verification: false,
3776 ca_certs: Vec::new(),
3777 setup_parameters: Vec::new(),
3778 };
3779 assert_eq!(
3780 config.alpn(),
3781 vec![MOQT_ALPN.to_vec()],
3782 "MOQT_ALPN is {:?}; a draft-{} client offers {:?}",
3783 String::from_utf8_lossy(MOQT_ALPN),
3784 18,
3785 config
3786 .alpn()
3787 .iter()
3788 .map(|a| String::from_utf8_lossy(a).into_owned())
3789 .collect::<Vec<_>>(),
3790 );
3791 assert_eq!(MOQT_ALPN, b"moqt-18");
3792 }
3793
3794 /// Draft-18 Section 3.3 names seven message types a bidirectional stream
3795 /// may begin with, and no others. The set is checked against the raw
3796 /// numbers this draft's registry assigns rather than against the names,
3797 /// so a variant that is renumbered — SUBSCRIBE_NAMESPACE moved from 0x11
3798 /// to 0x50 between draft-17 and draft-18 — is caught even though the
3799 /// spelling did not change, and a variant that is missing — draft-18's
3800 /// own SUBSCRIBE_TRACKS, 0x51 — is caught even though nothing else in the
3801 /// file would notice.
3802 ///
3803 /// Every type this draft assigns is classified: the loop walks the whole
3804 /// assigned range and asks the classifier about each one it finds.
3805 #[test]
3806 fn only_seven_message_types_open_a_request_stream() {
3807 // TRACK_STATUS, SUBSCRIBE, PUBLISH, FETCH, PUBLISH_NAMESPACE,
3808 // SUBSCRIBE_NAMESPACE and SUBSCRIBE_TRACKS, written as the numbers
3809 // draft-18 assigns them.
3810 let mut expected = vec![0x0D, 0x03, 0x1D, 0x16, 0x06, 0x50, 0x51];
3811 expected.sort_unstable();
3812
3813 let mut opens = Vec::new();
3814 for id in 0..=CONTROL_STREAM_TYPE {
3815 if let Some(ty) = MessageType::from_id(id) {
3816 if starts_a_request_stream(ty) {
3817 opens.push(id);
3818 }
3819 }
3820 }
3821 opens.sort_unstable();
3822
3823 assert_eq!(
3824 opens, expected,
3825 "the types that open a request stream are {opens:?}; \
3826 draft-18 Section 3.3 names {expected:?}"
3827 );
3828 }
3829
3830 /// The kind a request helper labels its stream with must name the message
3831 /// that helper actually writes, and the check is made against the type
3832 /// varint the encoded message leads with — the byte a peer reads to
3833 /// decide whether the bidirectional stream is legal.
3834 ///
3835 /// This is the mislabelling a port from another draft is most likely to
3836 /// introduce, because the numbers move between drafts while the names do
3837 /// not: SUBSCRIBE_NAMESPACE is 0x11 on draft-17 and 0x50 here.
3838 #[test]
3839 fn each_request_kind_labels_the_message_its_helper_writes() {
3840 use crate::draft18::endpoint::Endpoint;
3841 use moqtap_codec::draft18::message::Setup;
3842
3843 let v = |n: u64| VarInt::from_u64(n).unwrap();
3844 let ns = TrackNamespace(vec![b"ns".to_vec()]);
3845
3846 let mut ep = Endpoint::new(Role::Client);
3847 ep.connect().unwrap();
3848 let _ = ep.send_setup(vec![]).unwrap();
3849 ep.receive_setup(&Setup { options: vec![] }).unwrap();
3850
3851 let (sub_id, subscribe) = ep.subscribe(ns.clone(), b"t".to_vec(), vec![]).unwrap();
3852 let built = vec![
3853 (RequestKind::Subscribe, subscribe),
3854 (
3855 RequestKind::Fetch,
3856 ep.fetch(ns.clone(), b"t".to_vec(), v(0), v(0), v(1), v(1), vec![]).unwrap().1,
3857 ),
3858 (RequestKind::Fetch, ep.joining_fetch(sub_id, v(2), Vec::new()).unwrap().1),
3859 (
3860 RequestKind::SubscribeNamespace,
3861 ep.subscribe_namespace(ns.clone(), vec![]).unwrap().1,
3862 ),
3863 (RequestKind::SubscribeTracks, ep.subscribe_tracks(ns.clone(), vec![]).unwrap().1),
3864 (RequestKind::PublishNamespace, ep.publish_namespace(ns.clone(), vec![]).unwrap().1),
3865 (
3866 RequestKind::TrackStatus,
3867 ep.track_status(ns.clone(), b"t".to_vec(), vec![]).unwrap().1,
3868 ),
3869 (
3870 RequestKind::Publish,
3871 ep.publish(ns.clone(), b"t".to_vec(), v(7), vec![], vec![]).unwrap().1,
3872 ),
3873 ];
3874
3875 for (kind, msg) in built {
3876 let mut wire = Vec::new();
3877 msg.encode(&mut wire).unwrap();
3878 let mut cursor = &wire[..];
3879 let on_the_wire =
3880 DraftVersion::Draft18.decode_varint(&mut cursor).unwrap().into_inner();
3881 assert_eq!(
3882 kind.message_type().id(),
3883 on_the_wire,
3884 "{kind:?} is labelled {} but its message leads with {on_the_wire}",
3885 kind.message_type().id(),
3886 );
3887 assert!(
3888 starts_a_request_stream(kind.message_type()),
3889 "{kind:?} labels a message type that may not begin a bidirectional stream",
3890 );
3891 }
3892 }
3893
3894 /// The classifier the accept path runs and the one
3895 /// [`Connection::send_control`] runs must answer alike for every message
3896 /// type this draft assigns, or a message could be refused on the control
3897 /// stream and refused again as the opening of a request stream — leaving
3898 /// no legal place for it.
3899 #[test]
3900 fn the_two_request_stream_classifiers_agree() {
3901 let mut classified = 0;
3902 for id in 0..=CONTROL_STREAM_TYPE {
3903 let Some(ty) = MessageType::from_id(id) else { continue };
3904 classified += 1;
3905 let kind = RequestKind::from_message_type(ty);
3906 assert_eq!(
3907 kind.is_some(),
3908 starts_a_request_stream(ty),
3909 "type {id} {} a request stream but from_message_type calls it {kind:?}",
3910 if starts_a_request_stream(ty) { "opens" } else { "does not open" },
3911 );
3912 if let Some(kind) = kind {
3913 assert_eq!(
3914 kind.message_type(),
3915 ty,
3916 "from_message_type sent type {id} to {kind:?}, which names a different message",
3917 );
3918 }
3919 }
3920 assert!(classified > 7, "the loop found only {classified} assigned message types");
3921 }
3922
3923 /// A stream this endpoint opened is cancelled when its handle is dropped;
3924 /// one the peer opened is reset as unserved. Both codes are on the wire,
3925 /// so they may not be the same number.
3926 #[test]
3927 fn the_two_abandonment_codes_are_distinct() {
3928 assert_eq!(REQUEST_CANCELLED, 0x1);
3929 assert_eq!(REQUEST_UNANSWERED, 0x0);
3930 assert_ne!(
3931 REQUEST_CANCELLED, REQUEST_UNANSWERED,
3932 "a peer cannot tell a rejected request from a dropped one if both reset with the same code",
3933 );
3934 }
3935
3936 #[test]
3937 fn transport_type_debug() {
3938 let quic = TransportType::Quic;
3939 assert!(format!("{quic:?}").contains("Quic"));
3940
3941 let wt = TransportType::WebTransport { url: "https://example.com".to_string() };
3942 assert!(format!("{wt:?}").contains("WebTransport"));
3943 }
3944}
3945#[cfg(test)]
3946mod accept_on_the_wire {
3947 //! The accept path against a real QUIC peer.
3948 //!
3949 //! Draft-18's connection module has had one of these since it was written,
3950 //! and every transport rule this draft shares with it was gated only there
3951 //! — which meant the rules were carried here by the shape of the edit
3952 //! rather than by anything that observes them. What a peer can see, only a
3953 //! peer can check.
3954
3955 use super::*;
3956 use std::sync::Arc;
3957
3958 use std::net::SocketAddr;
3959 use std::time::Duration;
3960
3961 use moqtap_codec::draft18::message::{Setup, SubscribeNamespace};
3962
3963 /// Long enough that a loaded machine cannot fail a test that would
3964 /// otherwise pass, short enough that a hang is reported rather than run to
3965 /// the harness timeout.
3966 const PATIENCE: Duration = Duration::from_secs(10);
3967
3968 fn v(n: u64) -> VarInt {
3969 VarInt::from_u64(n).unwrap()
3970 }
3971
3972 fn ns() -> TrackNamespace {
3973 TrackNamespace(vec![b"live".to_vec()])
3974 }
3975
3976 fn encode(msg: ControlMessage) -> Vec<u8> {
3977 let mut buf = Vec::new();
3978 AnyControlMessage::Draft18(msg).encode(&mut buf).expect("encode");
3979 buf
3980 }
3981
3982 fn request_update(id: u64) -> ControlMessage {
3983 ControlMessage::RequestUpdate(moqtap_codec::draft18::message::RequestUpdate {
3984 request_id: v(id),
3985 parameters: vec![],
3986 })
3987 }
3988
3989 fn request_error() -> RequestError {
3990 RequestError {
3991 error_code: v(0x1),
3992 retry_interval: v(0),
3993 reason_phrase: b"no".to_vec(),
3994 redirect: None,
3995 }
3996 }
3997
3998 fn peer_fetch(id: u64) -> ControlMessage {
3999 ControlMessage::Fetch(moqtap_codec::draft18::message::Fetch {
4000 request_id: v(id),
4001 fetch_type: moqtap_codec::draft18::message::FetchType::Standalone,
4002 fetch_payload: moqtap_codec::draft18::message::FetchPayload::Standalone {
4003 track_namespace: ns(),
4004 track_name: b"video".to_vec(),
4005 start_group: v(0),
4006 start_object: v(0),
4007 end_group: v(1),
4008 end_object: v(0),
4009 },
4010 parameters: vec![],
4011 })
4012 }
4013
4014 fn fetch_ok() -> FetchOk {
4015 FetchOk {
4016 end_of_track: 0,
4017 end_group: v(1),
4018 end_object: v(0),
4019 parameters: vec![],
4020 track_properties: vec![],
4021 }
4022 }
4023
4024 fn init_crypto() {
4025 let _ = rustls::crypto::ring::default_provider().install_default();
4026 }
4027
4028 /// A quinn server on a loopback port, offering this draft's ALPN.
4029 fn server_endpoint() -> (quinn::Endpoint, SocketAddr) {
4030 use rcgen::{CertificateParams, KeyPair, PKCS_ECDSA_P256_SHA256};
4031 use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
4032
4033 let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("keypair");
4034 let params = CertificateParams::new(vec!["localhost".into()]).expect("params");
4035 let cert = params.self_signed(&key_pair).expect("self-sign");
4036 let cert_der = CertificateDer::from(cert.der().to_vec());
4037 let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
4038
4039 let mut server_crypto = rustls::ServerConfig::builder()
4040 .with_no_client_auth()
4041 .with_single_cert(vec![cert_der], key_der)
4042 .expect("server cert");
4043 server_crypto.alpn_protocols = vec![DraftVersion::Draft18.quic_alpn().to_vec()];
4044 let server_crypto =
4045 quinn::crypto::rustls::QuicServerConfig::try_from(server_crypto).expect("quic crypto");
4046 let server_config = quinn::ServerConfig::with_crypto(Arc::new(server_crypto));
4047 let endpoint = quinn::Endpoint::server(server_config, "127.0.0.1:0".parse().unwrap())
4048 .expect("bind server");
4049 let addr = endpoint.local_addr().expect("local_addr");
4050 (endpoint, addr)
4051 }
4052
4053 async fn connect_client(addr: SocketAddr) -> Result<Connection, ConnectionError> {
4054 Connection::connect(
4055 &addr.to_string(),
4056 ClientConfig {
4057 draft: DraftVersion::Draft18,
4058 transport: TransportType::Quic,
4059 skip_cert_verification: true,
4060 ca_certs: Vec::new(),
4061 setup_parameters: Vec::new(),
4062 },
4063 )
4064 .await
4065 }
4066
4067 /// The peer's half of the setup exchange: read the client's SETUP off its
4068 /// unidirectional control stream, answer with one of our own.
4069 ///
4070 /// Both control streams are handed back so they stay open for the
4071 /// connection's life. Dropping a quinn receive stream sends STOP_SENDING
4072 /// and dropping a send stream resets it, either of which would look to the
4073 /// client like the control plane failing.
4074 async fn peer_handshake(
4075 endpoint: &quinn::Endpoint,
4076 ) -> (quinn::Connection, quinn::SendStream, quinn::RecvStream) {
4077 let conn = endpoint.accept().await.expect("accept").await.expect("tls handshake");
4078 let mut client_control = conn.accept_uni().await.expect("accept_uni");
4079 let mut seen = Vec::new();
4080 let mut chunk = [0u8; 1024];
4081 while seen.len() < 3 {
4082 match client_control.read(&mut chunk).await.expect("read SETUP") {
4083 Some(n) => seen.extend_from_slice(&chunk[..n]),
4084 None => break,
4085 }
4086 }
4087 assert!(!seen.is_empty(), "the client sent no SETUP");
4088 let mut ours = conn.open_uni().await.expect("open_uni");
4089 ours.write_all(&encode(ControlMessage::Setup(Setup { options: Vec::new() })))
4090 .await
4091 .expect("write SETUP");
4092 (conn, ours, client_control)
4093 }
4094
4095 /// A connected client and the peer holding the other end.
4096 struct Loopback {
4097 conn: Connection,
4098 peer: quinn::Connection,
4099 _endpoint: quinn::Endpoint,
4100 _control_send: quinn::SendStream,
4101 _control_recv: quinn::RecvStream,
4102 }
4103
4104 async fn loopback() -> Loopback {
4105 init_crypto();
4106 let (endpoint, addr) = server_endpoint();
4107 let (client, peer) = tokio::join!(connect_client(addr), peer_handshake(&endpoint));
4108 let (peer, control_send, control_recv) = peer;
4109 Loopback {
4110 conn: client.expect("client connect"),
4111 peer,
4112 _endpoint: endpoint,
4113 _control_send: control_send,
4114 _control_recv: control_recv,
4115 }
4116 }
4117
4118 fn framed(recv: quinn::RecvStream) -> FramedRecvStream {
4119 FramedRecvStream::new(RecvStream::Quic(recv), DraftVersion::Draft18)
4120 }
4121
4122 /// Read one control message the client wrote, failing rather than hanging.
4123 async fn next_control(recv: &mut FramedRecvStream) -> ControlMessage {
4124 let (any, _) = tokio::time::timeout(PATIENCE, recv.read_control(false))
4125 .await
4126 .expect("the client wrote nothing")
4127 .expect("read control");
4128 match any {
4129 AnyControlMessage::Draft18(msg) => msg,
4130 #[allow(unreachable_patterns)]
4131 other => panic!("expected a draft-18 message, got {other:?}"),
4132 }
4133 }
4134
4135 /// An update on a PUBLISH this endpoint sent is answered here.
4136 ///
4137 /// Section 10.9 names the one case where a requester answers rather than
4138 /// asks: "A subscriber can also send REQUEST_UPDATE to modify parameters
4139 /// of a subscription established with PUBLISH." The receiver of that
4140 /// update "MUST respond with exactly one REQUEST_OK or REQUEST_ERROR
4141 /// message indicating if the update was successful", and on a PUBLISH this
4142 /// endpoint sent, the receiver is this endpoint.
4143 #[tokio::test]
4144 async fn an_update_on_a_publish_we_sent_is_answered_here() {
4145 let mut lb = loopback().await;
4146
4147 let mut outbound =
4148 lb.conn.publish(ns(), b"video".to_vec(), v(7), vec![], vec![]).await.expect("publish");
4149 let (mut their_send, their_recv) = tokio::time::timeout(PATIENCE, lb.peer.accept_bi())
4150 .await
4151 .expect("the client opened no request stream")
4152 .expect("accept_bi");
4153 let mut their_recv = framed(their_recv);
4154 assert!(matches!(next_control(&mut their_recv).await, ControlMessage::Publish(_)));
4155
4156 // The subscriber accepts the publication, then updates it.
4157 their_send
4158 .write_all(&encode(ControlMessage::RequestOk(RequestOk {
4159 parameters: vec![],
4160 track_properties: vec![],
4161 })))
4162 .await
4163 .expect("write REQUEST_OK");
4164 let msg = tokio::time::timeout(PATIENCE, lb.conn.recv_on_request_stream(&mut outbound))
4165 .await
4166 .expect("no REQUEST_OK arrived")
4167 .expect("read REQUEST_OK");
4168 assert!(matches!(msg, ControlMessage::RequestOk(_)), "{msg:?}");
4169
4170 their_send.write_all(&encode(request_update(0))).await.expect("write REQUEST_UPDATE");
4171 let msg = tokio::time::timeout(PATIENCE, lb.conn.recv_on_request_stream(&mut outbound))
4172 .await
4173 .expect("no REQUEST_UPDATE arrived")
4174 .expect("read REQUEST_UPDATE");
4175 assert!(matches!(msg, ControlMessage::RequestUpdate(_)), "{msg:?}");
4176
4177 lb.conn
4178 .respond_ok(&mut outbound, RequestOk { parameters: vec![], track_properties: vec![] })
4179 .await
4180 .expect("the subscriber's update is this endpoint's to answer");
4181 assert!(matches!(next_control(&mut their_recv).await, ControlMessage::RequestOk(_)));
4182
4183 // The publication is untouched by the update, and the ending it still
4184 // owes goes out without complaint. Draft-19 keeps that obligation on
4185 // the request stream and can be asked; this draft does not, so the
4186 // ending being accepted is the observation.
4187 lb.conn
4188 .publish_done(&mut outbound, v(0), v(0), Vec::new())
4189 .await
4190 .expect("an accepted update leaves the ending free");
4191 }
4192
4193 /// Refusing that update owes the same ending as refusing any other.
4194 ///
4195 /// Section 10.9.1: "When a REQUEST_UPDATE is unsuccessful, the publisher
4196 /// MUST also terminate the subscription by sending a PUBLISH_DONE with
4197 /// error code UPDATE_FAILED." The publisher of a subscription established
4198 /// with PUBLISH is the endpoint that sent it, and the ending goes on the
4199 /// stream that endpoint opened rather than on one the peer opened.
4200 #[tokio::test]
4201 async fn a_refused_update_on_a_publish_we_sent_owes_its_ending() {
4202 let mut lb = loopback().await;
4203
4204 let mut outbound =
4205 lb.conn.publish(ns(), b"video".to_vec(), v(7), vec![], vec![]).await.expect("publish");
4206 let (mut their_send, their_recv) = tokio::time::timeout(PATIENCE, lb.peer.accept_bi())
4207 .await
4208 .expect("the client opened no request stream")
4209 .expect("accept_bi");
4210 let mut their_recv = framed(their_recv);
4211 assert!(matches!(next_control(&mut their_recv).await, ControlMessage::Publish(_)));
4212
4213 their_send
4214 .write_all(&encode(ControlMessage::RequestOk(RequestOk {
4215 parameters: vec![],
4216 track_properties: vec![],
4217 })))
4218 .await
4219 .expect("write REQUEST_OK");
4220 tokio::time::timeout(PATIENCE, lb.conn.recv_on_request_stream(&mut outbound))
4221 .await
4222 .expect("no REQUEST_OK arrived")
4223 .expect("read REQUEST_OK");
4224
4225 their_send.write_all(&encode(request_update(0))).await.expect("write REQUEST_UPDATE");
4226 tokio::time::timeout(PATIENCE, lb.conn.recv_on_request_stream(&mut outbound))
4227 .await
4228 .expect("no REQUEST_UPDATE arrived")
4229 .expect("read REQUEST_UPDATE");
4230
4231 lb.conn
4232 .respond_error(&mut outbound, request_error())
4233 .await
4234 .expect("refuse the subscriber's update");
4235 assert!(matches!(next_control(&mut their_recv).await, ControlMessage::RequestError(_)));
4236
4237 // The ending is owed under one status, and asking for another leaves
4238 // the publication exactly where it was rather than half ended.
4239 let err = lb
4240 .conn
4241 .publish_done(&mut outbound, v(0), v(0), Vec::new())
4242 .await
4243 .expect_err("a refused update fixes the status of the ending");
4244 assert!(
4245 matches!(
4246 err,
4247 ConnectionError::Endpoint(EndpointError::WrongUpdateFailureStatus {
4248 request: 0,
4249 required: 0x8,
4250 })
4251 ),
4252 "{err}",
4253 );
4254 lb.conn
4255 .publish_done(&mut outbound, v(0x8), v(0), Vec::new())
4256 .await
4257 .expect("the termination the refusal owes");
4258 assert!(matches!(next_control(&mut their_recv).await, ControlMessage::PublishDone(_)));
4259 }
4260
4261 /// A refused namespace update closes the stream rather than owing a message.
4262 ///
4263 /// Section 10.9.1 sorts a refused REQUEST_UPDATE by what was being
4264 /// updated, and a namespace subscription falls in the clause that ends
4265 /// with the transport rather than with a message: "When a REQUEST_UPDATE
4266 /// fails for a SUBSCRIBE_NAMESPACE or PUBLISH_NAMESPACE, the responder
4267 /// MUST close the bidi stream." Draft-19 adds SUBSCRIBE_TRACKS to that
4268 /// list and a pointer to its own Section 3.3.2; this draft has neither. There is
4269 /// no subscription to terminate here and no PUBLISH_DONE that could go
4270 /// out, so a send half held open for one would stay open for good.
4271 #[tokio::test]
4272 async fn a_refused_namespace_update_closes_the_stream() {
4273 let mut lb = loopback().await;
4274
4275 let (mut ps, pr) = lb.peer.open_bi().await.expect("open_bi");
4276 ps.write_all(&encode(ControlMessage::SubscribeNamespace(SubscribeNamespace {
4277 request_id: v(1),
4278 namespace_prefix: ns(),
4279 parameters: vec![],
4280 })))
4281 .await
4282 .expect("write SUBSCRIBE_NAMESPACE");
4283 let mut pr = framed(pr);
4284
4285 let (_, mut stream) = tokio::time::timeout(PATIENCE, lb.conn.accept_request_stream())
4286 .await
4287 .expect("accept hung")
4288 .expect("accept");
4289 lb.conn
4290 .respond_ok(&mut stream, RequestOk { parameters: vec![], track_properties: vec![] })
4291 .await
4292 .expect("respond_ok");
4293 assert!(matches!(next_control(&mut pr).await, ControlMessage::RequestOk(_)));
4294
4295 // The peer asks to move the prefix and this endpoint refuses.
4296 ps.write_all(&encode(request_update(1))).await.expect("write REQUEST_UPDATE");
4297 let update = tokio::time::timeout(PATIENCE, lb.conn.recv_on_request_stream(&mut stream))
4298 .await
4299 .expect("read hung")
4300 .expect("read REQUEST_UPDATE");
4301 assert!(matches!(update, ControlMessage::RequestUpdate(_)));
4302 lb.conn.respond_error(&mut stream, request_error()).await.expect("refuse the update");
4303 assert!(matches!(next_control(&mut pr).await, ControlMessage::RequestError(_)));
4304
4305 // The refusal ends this request stream, so the peer's next read finds
4306 // the send half closed rather than waiting on a message a namespace
4307 // subscription has no way to send.
4308 let ended = tokio::time::timeout(PATIENCE, pr.read_control(false))
4309 .await
4310 .expect("the peer is still reading: the refusal never closed the send half")
4311 .expect_err("a refused namespace update left the stream open");
4312 assert!(
4313 matches!(ended, ConnectionError::UnexpectedEnd),
4314 "the peer should have seen the FIN, got {ended:?}",
4315 );
4316 }
4317
4318 /// A refused fetch update resets the stream the objects were going out on.
4319 ///
4320 /// Section 10.9.1: "When a REQUEST_UPDATE fails for a FETCH, the publisher
4321 /// MUST reset the FETCH data stream." The objects are not on the request
4322 /// stream, so refusing the update on that stream is only half of it; the
4323 /// data stream has to go too, and the connection can only reset a handle
4324 /// it still holds.
4325 #[tokio::test]
4326 async fn a_refused_fetch_update_resets_the_fetch_data_stream() {
4327 let mut lb = loopback().await;
4328
4329 let (mut ps, pr) = lb.peer.open_bi().await.expect("open_bi");
4330 ps.write_all(&encode(peer_fetch(1))).await.expect("write FETCH");
4331 let mut pr = framed(pr);
4332
4333 let (_, mut stream) = tokio::time::timeout(PATIENCE, lb.conn.accept_request_stream())
4334 .await
4335 .expect("accept hung")
4336 .expect("accept");
4337 assert_eq!(stream.kind(), RequestKind::Fetch);
4338 lb.conn.respond_fetch_ok(&mut stream, fetch_ok()).await.expect("respond_fetch_ok");
4339 assert!(matches!(next_control(&mut pr).await, ControlMessage::FetchOk(_)));
4340
4341 // The objects go out on a stream of their own, which the request now
4342 // holds.
4343 lb.conn
4344 .open_fetch_stream_on(
4345 &mut stream,
4346 &AnyFetchHeader::Draft18(FetchHeader { request_id: v(1) }),
4347 )
4348 .await
4349 .expect("open the fetch data stream");
4350 let data = tokio::time::timeout(PATIENCE, lb.peer.accept_uni())
4351 .await
4352 .expect("no data stream arrived")
4353 .expect("accept_uni");
4354 let mut data = framed(data);
4355 tokio::time::timeout(PATIENCE, data.read_fetch_header())
4356 .await
4357 .expect("the header never arrived")
4358 .expect("the data stream opens with a FETCH_HEADER");
4359
4360 // The peer updates the fetch and this endpoint refuses it.
4361 ps.write_all(&encode(request_update(1))).await.expect("write REQUEST_UPDATE");
4362 tokio::time::timeout(PATIENCE, lb.conn.recv_on_request_stream(&mut stream))
4363 .await
4364 .expect("read hung")
4365 .expect("read REQUEST_UPDATE");
4366 lb.conn.respond_error(&mut stream, request_error()).await.expect("refuse the update");
4367 assert!(matches!(next_control(&mut pr).await, ControlMessage::RequestError(_)));
4368
4369 // The data stream went with it: the peer's next read on it fails
4370 // rather than waiting for objects that are not coming.
4371 let err = tokio::time::timeout(PATIENCE, data.read_control(false))
4372 .await
4373 .expect("the peer is still waiting on a stream that should have been reset")
4374 .expect_err("the data stream should have been reset");
4375 assert!(
4376 format!("{err}").to_lowercase().contains("reset"),
4377 "the peer should see a reset, got {err}",
4378 );
4379 }
4380}