h3i/
frame.rs

1// Copyright (C) 2024, Cloudflare, Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright notice,
9//       this list of conditions and the following disclaimer.
10//
11//     * Redistributions in binary form must reproduce the above copyright
12//       notice, this list of conditions and the following disclaimer in the
13//       documentation and/or other materials provided with the distribution.
14//
15// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
16// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
17// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
19// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
27//! Helpers for dealing with quiche stream events and HTTP/3 frames.
28
29use std::cmp;
30use std::convert::TryFrom;
31use std::error::Error;
32use std::fmt::Debug;
33use std::sync::Arc;
34
35use multimap::MultiMap;
36use quiche;
37
38use quiche::h3::frame::Frame as QFrame;
39use quiche::h3::Header;
40use quiche::h3::NameValue;
41use serde::ser::SerializeStruct;
42use serde::ser::Serializer;
43use serde::Serialize;
44
45use crate::client::connection_summary::MAX_SERIALIZED_BUFFER_LEN;
46use crate::encode_header_block;
47
48pub type BoxError = Box<dyn Error + Send + Sync + 'static>;
49
50/// An internal representation of a QUIC or HTTP/3 frame. This type exists so
51/// that we can extend types defined in Quiche.
52#[derive(Debug, Eq, PartialEq, Clone)]
53pub enum H3iFrame {
54    /// A wrapper around a quiche HTTP/3 frame.
55    QuicheH3(QFrame),
56    /// A wrapper around an [EnrichedHeaders] struct.
57    Headers(EnrichedHeaders),
58    /// A wrapper around a [ResetStream] struct
59    ResetStream(ResetStream),
60}
61
62impl H3iFrame {
63    /// Try to convert this `H3iFrame` to an [EnrichedHeaders].
64    ///
65    /// Returns `Some` if the operation succeeded.
66    pub fn to_enriched_headers(&self) -> Option<EnrichedHeaders> {
67        if let H3iFrame::Headers(header) = self {
68            Some(header.clone())
69        } else {
70            None
71        }
72    }
73}
74
75impl Serialize for H3iFrame {
76    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
77    where
78        S: Serializer,
79    {
80        match self {
81            H3iFrame::QuicheH3(frame) => {
82                let mut state = s.serialize_struct("frame", 1)?;
83                let name = frame_name(frame);
84                state.serialize_field(name, &SerializableQFrame(frame))?;
85                state.end()
86            },
87            H3iFrame::Headers(headers) => {
88                let mut state = s.serialize_struct("enriched_headers", 1)?;
89                state.serialize_field("enriched_headers", headers)?;
90                state.end()
91            },
92            H3iFrame::ResetStream(reset) => {
93                let mut state = s.serialize_struct("reset_stream", 1)?;
94                state.serialize_field("reset_stream", reset)?;
95                state.end()
96            },
97        }
98    }
99}
100
101impl From<QFrame> for H3iFrame {
102    fn from(value: QFrame) -> Self {
103        Self::QuicheH3(value)
104    }
105}
106
107impl From<Vec<Header>> for H3iFrame {
108    fn from(value: Vec<Header>) -> Self {
109        Self::Headers(EnrichedHeaders::from(value))
110    }
111}
112
113pub type HeaderMap = MultiMap<Vec<u8>, Vec<u8>>;
114
115/// An HTTP/3 HEADERS frame with decoded headers and a [HeaderMap].
116#[derive(Clone, PartialEq, Eq)]
117pub struct EnrichedHeaders {
118    header_block: Vec<u8>,
119    headers: Vec<Header>,
120    /// A multi-map of raw header names to values, similar to http's HeaderMap.
121    header_map: HeaderMap,
122}
123
124/// A wrapper to help serialize an quiche HTTP header.
125pub struct SerializableHeader<'a>(&'a Header);
126
127impl Serialize for SerializableHeader<'_> {
128    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
129    where
130        S: Serializer,
131    {
132        let mut state = s.serialize_struct("header", 2)?;
133        state.serialize_field("name", &String::from_utf8_lossy(self.0.name()))?;
134        state
135            .serialize_field("value", &String::from_utf8_lossy(self.0.value()))?;
136        state.end()
137    }
138}
139
140impl EnrichedHeaders {
141    /// Return the array of headers in this frame.
142    ///
143    /// # Examples
144    /// ```
145    /// use h3i::frame::EnrichedHeaders;
146    /// use quiche::h3::Header;
147    ///
148    /// let raw = vec![
149    ///     Header::new(b"new jersey", b"devils"),
150    ///     Header::new(b"new york", b"jets"),
151    /// ];
152    /// let headers = EnrichedHeaders::from(raw.clone());
153    /// assert_eq!(headers.headers(), raw);
154    /// ```
155    pub fn headers(&self) -> &[Header] {
156        &self.headers
157    }
158
159    /// Returns a multi-map of header keys to values.
160    ///
161    /// If a single key contains multiple values, the values in the entry will
162    /// be returned in the same order as they appear in the array of headers
163    /// which backs the [`EnrichedHeaders`].
164    ///
165    /// # Examples
166    /// ```
167    /// use h3i::frame::EnrichedHeaders;
168    /// use h3i::frame::H3iFrame;
169    /// use multimap::MultiMap;
170    /// use quiche::h3::Header;
171    /// use std::iter::FromIterator;
172    ///
173    /// let header_frame = vec![
174    ///     Header::new(b":status", b"200"),
175    ///     Header::new(b"hello", b"world"),
176    ///     Header::new(b"hello", b"super-earth"),
177    /// ];
178    ///
179    /// let enriched = H3iFrame::Headers(header_frame.into())
180    ///     .to_enriched_headers()
181    ///     .unwrap();
182    ///
183    /// let expected = MultiMap::from_iter([
184    ///     (b":status".to_vec(), vec![b"200".to_vec()]),
185    ///     (b"hello".to_vec(), vec![
186    ///         b"world".to_vec(),
187    ///         b"super-earth".to_vec(),
188    ///     ]),
189    /// ]);
190    ///
191    /// assert_eq!(*enriched.header_map(), expected);
192    /// ```
193    pub fn header_map(&self) -> &HeaderMap {
194        &self.header_map
195    }
196
197    /// Fetches the value of the `:status` pseudo-header.
198    ///
199    /// # Examples
200    /// ```
201    /// use h3i::frame::EnrichedHeaders;
202    /// use quiche::h3::Header;
203    ///
204    /// let headers = EnrichedHeaders::from(vec![Header::new(b"hello", b"world")]);
205    /// assert!(headers.status_code().is_none());
206    ///
207    /// let headers = EnrichedHeaders::from(vec![Header::new(b":status", b"200")]);
208    /// assert_eq!(headers.status_code().expect("status code is Some"), b"200");
209    /// ```
210    pub fn status_code(&self) -> Option<&Vec<u8>> {
211        self.header_map.get(b":status".as_slice())
212    }
213}
214
215impl Serialize for EnrichedHeaders {
216    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
217    where
218        S: Serializer,
219    {
220        let mut state = s.serialize_struct("enriched_headers", 2)?;
221        state.serialize_field("header_block_len", &self.header_block.len())?;
222        let x: Vec<SerializableHeader> =
223            self.headers.iter().map(SerializableHeader).collect();
224        state.serialize_field("headers", &x)?;
225        state.end()
226    }
227}
228
229impl From<Vec<Header>> for EnrichedHeaders {
230    fn from(headers: Vec<Header>) -> Self {
231        let header_block = encode_header_block(&headers).unwrap();
232
233        let mut header_map: HeaderMap = MultiMap::with_capacity(headers.len());
234        for header in headers.iter() {
235            header_map.insert(header.name().to_vec(), header.value().to_vec());
236        }
237
238        Self {
239            header_block,
240            headers,
241            header_map,
242        }
243    }
244}
245
246impl TryFrom<QFrame> for EnrichedHeaders {
247    type Error = BoxError;
248
249    fn try_from(value: QFrame) -> Result<Self, Self::Error> {
250        match value {
251            QFrame::Headers { header_block } => {
252                let mut qpack_decoder = quiche::h3::qpack::Decoder::new();
253                let headers =
254                    qpack_decoder.decode(&header_block, u64::MAX).unwrap();
255
256                Ok(EnrichedHeaders::from(headers))
257            },
258            _ => Err("Cannot convert non-Headers frame into HeadersFrame".into()),
259        }
260    }
261}
262
263impl Debug for EnrichedHeaders {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        write!(f, "{:?}", self.headers)
266    }
267}
268
269/// A `RESET_STREAM` frame.
270///
271/// See [RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000#name-reset_stream-frames) for
272/// more.
273#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
274pub struct ResetStream {
275    /// The stream ID over which the RESET_STREAM frame was sent.
276    pub stream_id: u64,
277    /// The error code sent from the peer.
278    pub error_code: u64,
279}
280
281fn frame_name(frame: &QFrame) -> &'static str {
282    match frame {
283        QFrame::Data { .. } => "DATA",
284        QFrame::Headers { .. } => "HEADERS",
285        QFrame::CancelPush { .. } => "CANCEL_PUSH",
286        QFrame::Settings { .. } => "SETTINGS",
287        QFrame::PushPromise { .. } => "PUSH_PROMISE",
288        QFrame::GoAway { .. } => "GO_AWAY",
289        QFrame::MaxPushId { .. } => "MAX_PUSH_ID",
290        QFrame::PriorityUpdateRequest { .. } => "PRIORITY_UPDATE(REQUEST)",
291        QFrame::PriorityUpdatePush { .. } => "PRIORITY_UPDATE(PUSH)",
292        QFrame::Unknown { .. } => "UNKNOWN",
293    }
294}
295
296/// A wrapper to help serialize a quiche HTTP/3 frame.
297pub struct SerializableQFrame<'a>(&'a QFrame);
298
299impl Serialize for SerializableQFrame<'_> {
300    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
301    where
302        S: Serializer,
303    {
304        let name = frame_name(self.0);
305        match self.0 {
306            QFrame::Data { payload } => {
307                let mut state = s.serialize_struct(name, 2)?;
308                let max = cmp::min(payload.len(), MAX_SERIALIZED_BUFFER_LEN);
309                state.serialize_field("payload_len", &payload.len())?;
310                state.serialize_field(
311                    "payload",
312                    &qlog::HexSlice::maybe_string(Some(&payload[..max])),
313                )?;
314                state.end()
315            },
316
317            QFrame::Headers { header_block } => {
318                let mut state = s.serialize_struct(name, 1)?;
319                state.serialize_field("header_block_len", &header_block.len())?;
320                state.end()
321            },
322
323            QFrame::CancelPush { push_id } => {
324                let mut state = s.serialize_struct(name, 1)?;
325                state.serialize_field("push_id", &push_id)?;
326                state.end()
327            },
328
329            QFrame::Settings {
330                max_field_section_size,
331                qpack_max_table_capacity,
332                qpack_blocked_streams,
333                connect_protocol_enabled,
334                h3_datagram,
335                grease: _,
336                additional_settings,
337                raw: _,
338            } => {
339                let mut state = s.serialize_struct(name, 6)?;
340                state.serialize_field(
341                    "max_field_section_size",
342                    &max_field_section_size,
343                )?;
344                state.serialize_field(
345                    "qpack_max_table_capacity",
346                    &qpack_max_table_capacity,
347                )?;
348                state.serialize_field(
349                    "qpack_blocked_streams",
350                    &qpack_blocked_streams,
351                )?;
352                state.serialize_field(
353                    "connect_protocol_enabled",
354                    &connect_protocol_enabled,
355                )?;
356                state.serialize_field("h3_datagram", &h3_datagram)?;
357                state.serialize_field(
358                    "additional_settings",
359                    &additional_settings,
360                )?;
361                state.end()
362            },
363
364            QFrame::PushPromise {
365                push_id,
366                header_block,
367            } => {
368                let mut state = s.serialize_struct(name, 2)?;
369                state.serialize_field("push_id", &push_id)?;
370                state.serialize_field("header_block_len", &header_block.len())?;
371                state.end()
372            },
373
374            QFrame::GoAway { id } => {
375                let mut state = s.serialize_struct(name, 1)?;
376                state.serialize_field("id", &id)?;
377                state.end()
378            },
379
380            QFrame::MaxPushId { push_id } => {
381                let mut state = s.serialize_struct(name, 1)?;
382                state.serialize_field("push_id", &push_id)?;
383                state.end()
384            },
385
386            QFrame::PriorityUpdateRequest {
387                prioritized_element_id,
388                priority_field_value,
389            } => {
390                let mut state = s.serialize_struct(name, 3)?;
391                state.serialize_field(
392                    "prioritized_element_id",
393                    &prioritized_element_id,
394                )?;
395
396                let max = cmp::min(
397                    priority_field_value.len(),
398                    MAX_SERIALIZED_BUFFER_LEN,
399                );
400                state.serialize_field(
401                    "priority_field_value_len",
402                    &priority_field_value.len(),
403                )?;
404                state.serialize_field(
405                    "priority_field_value",
406                    &String::from_utf8_lossy(&priority_field_value[..max]),
407                )?;
408                state.end()
409            },
410
411            QFrame::PriorityUpdatePush {
412                prioritized_element_id,
413                priority_field_value,
414            } => {
415                let mut state = s.serialize_struct(name, 3)?;
416                state.serialize_field(
417                    "prioritized_element_id",
418                    &prioritized_element_id,
419                )?;
420                let max = cmp::min(
421                    priority_field_value.len(),
422                    MAX_SERIALIZED_BUFFER_LEN,
423                );
424                state.serialize_field(
425                    "priority_field_value_len",
426                    &priority_field_value.len(),
427                )?;
428                state.serialize_field(
429                    "priority_field_value",
430                    &String::from_utf8_lossy(&priority_field_value[..max]),
431                )?;
432                state.end()
433            },
434
435            QFrame::Unknown { raw_type, payload } => {
436                let mut state = s.serialize_struct(name, 3)?;
437                state.serialize_field("raw_type", &raw_type)?;
438                let max = cmp::min(payload.len(), MAX_SERIALIZED_BUFFER_LEN);
439                state.serialize_field("payload_len", &payload.len())?;
440                state.serialize_field(
441                    "payload",
442                    &qlog::HexSlice::maybe_string(Some(&payload[..max])),
443                )?;
444                state.end()
445            },
446        }
447    }
448}
449
450type CustomEquivalenceHandler =
451    Box<dyn for<'f> Fn(&'f H3iFrame) -> bool + Send + Sync + 'static>;
452
453#[derive(Clone)]
454enum Comparator {
455    Frame(H3iFrame),
456    /// Specifies how to compare an incoming [`H3iFrame`] with this
457    /// [`CloseTriggerFrame`]. Typically, the validation attempts to fuzzy-match
458    /// the [`CloseTriggerFrame`] against the incoming [`H3iFrame`], but there
459    /// are times where other behavior is desired (for example, checking
460    /// deserialized JSON payloads in a headers frame, or ensuring a random
461    /// value matches a regex).
462    ///
463    /// See [`CloseTriggerFrame::is_equivalent`] for more on how frames are
464    /// compared.
465    Fn(Arc<CustomEquivalenceHandler>),
466}
467
468impl Serialize for Comparator {
469    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
470    where
471        S: Serializer,
472    {
473        match self {
474            Self::Fn(_) => serializer.serialize_str("<comparator_fn>"),
475            Self::Frame(f) => {
476                let mut frame_ser = serializer.serialize_struct("frame", 1)?;
477                frame_ser.serialize_field("frame", f)?;
478                frame_ser.end()
479            },
480        }
481    }
482}
483
484/// Instructs h3i to watch for certain incoming [`H3iFrame`]s. The incoming
485/// frames can either be supplied directly via [`CloseTriggerFrame::new`], or
486/// via a verification callback  passed to
487/// [`CloseTriggerFrame::new_with_comparator`].
488#[derive(Serialize, Clone)]
489pub struct CloseTriggerFrame {
490    stream_id: u64,
491    comparator: Comparator,
492}
493
494impl CloseTriggerFrame {
495    /// Create a new [`CloseTriggerFrame`] which should watch for the provided
496    /// [`H3iFrame`].
497    ///
498    /// # Note
499    ///
500    /// For [QuicheH3] and [ResetStream] variants, equivalence is the same as
501    /// equality.
502    ///
503    /// For Headers variants, this [`CloseTriggerFrame`] is equivalent to the
504    /// incoming [`H3iFrame`] if the [`H3iFrame`] contains all [`Header`]s
505    /// in _this_ frame. In other words, `this` can be considered equivalent
506    /// to `other` if `other` contains a superset of `this`'s [`Header`]s.
507    ///
508    /// This allows users for fuzzy-matching on header frames without needing to
509    /// supply every individual header on the frame.
510    ///
511    /// [ResetStream]: H3iFrame::ResetStream
512    /// [QuicheH3]: H3iFrame::QuicheH3
513    pub fn new(stream_id: u64, frame: impl Into<H3iFrame>) -> Self {
514        Self {
515            stream_id,
516            comparator: Comparator::Frame(frame.into()),
517        }
518    }
519
520    /// Create a new [`CloseTriggerFrame`] which will match incoming
521    /// [`H3iFrame`]s according to the passed `comparator_fn`.
522    ///
523    /// The `comparator_fn` will be called with every incoming [`H3iFrame`]. It
524    /// should return `true` if the incoming frame is expected, and `false`
525    /// if it is not.
526    pub fn new_with_comparator<F>(stream_id: u64, comparator_fn: F) -> Self
527    where
528        F: Fn(&H3iFrame) -> bool + Send + Sync + 'static,
529    {
530        Self {
531            stream_id,
532            comparator: Comparator::Fn(Arc::new(Box::new(comparator_fn))),
533        }
534    }
535
536    pub(crate) fn stream_id(&self) -> u64 {
537        self.stream_id
538    }
539
540    pub(crate) fn is_equivalent(&self, other: &H3iFrame) -> bool {
541        let frame = match &self.comparator {
542            Comparator::Fn(compare) => return compare(other),
543            Comparator::Frame(frame) => frame,
544        };
545
546        match frame {
547            H3iFrame::Headers(me) => {
548                let H3iFrame::Headers(other) = other else {
549                    return false;
550                };
551
552                // TODO(evanrittenhouse): we could theoretically hand-roll a
553                // MultiMap which uses a HashSet as the
554                // multi-value collection, but in practice we don't expect very
555                // many headers on an CloseTriggerFrame
556                //
557                // ref: https://docs.rs/multimap/latest/src/multimap/lib.rs.html#89
558                me.headers().iter().all(|m| other.headers().contains(m))
559            },
560            H3iFrame::QuicheH3(me) => match other {
561                H3iFrame::QuicheH3(other) => me == other,
562                _ => false,
563            },
564            H3iFrame::ResetStream(me) => match other {
565                H3iFrame::ResetStream(rs) => me == rs,
566                _ => false,
567            },
568        }
569    }
570}
571
572impl Debug for CloseTriggerFrame {
573    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574        let repr = match &self.comparator {
575            Comparator::Frame(frame) => format!("{frame:?}"),
576            Comparator::Fn(_) => "closure".to_string(),
577        };
578
579        write!(
580            f,
581            "CloseTriggerFrame {{ stream_id: {}, comparator: {repr} }}",
582            self.stream_id
583        )
584    }
585}
586
587impl PartialEq for CloseTriggerFrame {
588    fn eq(&self, other: &Self) -> bool {
589        match (&self.comparator, &other.comparator) {
590            (Comparator::Frame(this_frame), Comparator::Frame(other_frame)) =>
591                self.stream_id == other.stream_id && this_frame == other_frame,
592            _ => false,
593        }
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use quiche::h3::frame::Frame;
601
602    #[test]
603    fn test_header_equivalence() {
604        let this = CloseTriggerFrame::new(0, vec![
605            Header::new(b"hello", b"world"),
606            Header::new(b"go", b"jets"),
607        ]);
608        let other: H3iFrame = vec![
609            Header::new(b"hello", b"world"),
610            Header::new(b"go", b"jets"),
611            Header::new(b"go", b"devils"),
612        ]
613        .into();
614
615        assert!(this.is_equivalent(&other));
616    }
617
618    #[test]
619    fn test_header_non_equivalence() {
620        let this = CloseTriggerFrame::new(0, vec![
621            Header::new(b"hello", b"world"),
622            Header::new(b"go", b"jets"),
623            Header::new(b"go", b"devils"),
624        ]);
625        let other: H3iFrame =
626            vec![Header::new(b"hello", b"world"), Header::new(b"go", b"jets")]
627                .into();
628
629        // `other` does not contain the `go: devils` header, so it's not
630        // equivalent to `this.
631        assert!(!this.is_equivalent(&other));
632    }
633
634    #[test]
635    fn test_rst_stream_equivalence() {
636        let mut rs = ResetStream {
637            stream_id: 0,
638            error_code: 57,
639        };
640
641        let this = CloseTriggerFrame::new(0, H3iFrame::ResetStream(rs.clone()));
642        let incoming = H3iFrame::ResetStream(rs.clone());
643        assert!(this.is_equivalent(&incoming));
644
645        rs.stream_id = 57;
646        let incoming = H3iFrame::ResetStream(rs);
647        assert!(!this.is_equivalent(&incoming));
648    }
649
650    #[test]
651    fn test_frame_equivalence() {
652        let mut d = Frame::Data {
653            payload: b"57".to_vec(),
654        };
655
656        let this = CloseTriggerFrame::new(0, H3iFrame::QuicheH3(d.clone()));
657        let incoming = H3iFrame::QuicheH3(d.clone());
658        assert!(this.is_equivalent(&incoming));
659
660        d = Frame::Data {
661            payload: b"go jets".to_vec(),
662        };
663        let incoming = H3iFrame::QuicheH3(d.clone());
664        assert!(!this.is_equivalent(&incoming));
665    }
666
667    #[test]
668    fn test_comparator() {
669        let this = CloseTriggerFrame::new_with_comparator(0, |frame| {
670            if let H3iFrame::Headers(..) = frame {
671                frame
672                    .to_enriched_headers()
673                    .unwrap()
674                    .header_map()
675                    .get(&b"cookie".to_vec())
676                    .is_some_and(|v| {
677                        std::str::from_utf8(v)
678                            .map(|s| s.to_lowercase())
679                            .unwrap()
680                            .contains("cookie")
681                    })
682            } else {
683                false
684            }
685        });
686
687        let incoming: H3iFrame =
688            vec![Header::new(b"cookie", b"SomeRandomCookie1234")].into();
689
690        assert!(this.is_equivalent(&incoming));
691    }
692}