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