Skip to main content

http_net/
forward.rs

1//! Hop-by-hop fields: the fields a proxy must not forward (RFC 9110
2//! section 7.6.1). [`Request::forward_from`](crate::Request::forward_from)
3//! uses the same rule.
4//!
5//! A head's `Connection` options are read once into a sorted list, so
6//! filtering a whole head is linear in its size (plus a sort of the
7//! options): checking each field against a fresh `connection_options()`
8//! would cost fields times options, which a client controls.
9
10use alloc::vec::Vec;
11use core::cmp::Ordering;
12
13use crate::head::{Headers, ListElements, RequestHead, ResponseHead};
14
15/// The fields that are hop-by-hop whatever `Connection` lists: they describe
16/// this one connection (RFC 9110 section 7.6.1 names `Connection`,
17/// `Proxy-Connection`, `Keep-Alive`, `TE`, `Transfer-Encoding`, `Upgrade`;
18/// `Trailer` announces trailers of a body that is re-framed per hop).
19const HOP_BY_HOP: &[&str] = &[
20    "connection",
21    "keep-alive",
22    "proxy-connection",
23    "te",
24    "transfer-encoding",
25    "upgrade",
26    "trailer",
27];
28
29/// Whether a field named `name` is hop-by-hop: one of `connection`,
30/// `keep-alive`, `proxy-connection`, `te`, `transfer-encoding`, `upgrade`,
31/// `trailer`, or any name the message's `Connection` field lists (RFC 9110
32/// section 7.6.1). A proxy removes these before forwarding a message.
33/// Names compare ASCII case-insensitively. For a request, pass
34/// [`RequestHead::connection_options`](crate::RequestHead::connection_options)
35/// as `connection_options`; for a response, pass
36/// [`ResponseHead::header_list("connection")`](crate::ResponseHead::header_list)
37/// (a `ResponseHead` has no `connection_options`).
38///
39/// This answers for one name, reading every option. To filter a whole
40/// head, use [`RequestHead::end_to_end_headers`] or
41/// [`ResponseHead::end_to_end_headers`], which read the options once:
42/// calling this for every field costs fields times options.
43///
44/// # Examples
45///
46/// ```
47/// use http_net::{ServerConn, ServerEvent, is_hop_by_hop};
48///
49/// let mut conn = ServerConn::new();
50/// let _ = conn.feed(b"GET / HTTP/1.1\r\nHost: a\r\nConnection: X-Trace\r\nX-Trace: 1\r\nAccept: */*\r\n\r\n");
51/// let ServerEvent::Request(req) = conn.poll().unwrap() else { panic!() };
52/// assert!(is_hop_by_hop("Keep-Alive", req.connection_options()));
53/// assert!(is_hop_by_hop("x-trace", req.connection_options()));
54/// assert!(!is_hop_by_hop("accept", req.connection_options()));
55/// ```
56#[must_use]
57pub fn is_hop_by_hop(name: &str, mut connection_options: ListElements<'_, '_>) -> bool {
58    HOP_BY_HOP.iter().any(|h| h.eq_ignore_ascii_case(name))
59        || connection_options.any(|o| o.eq_ignore_ascii_case(name.as_bytes()))
60}
61
62/// ASCII case-insensitive order, for the sorted option list.
63fn cmp_ignore_case(a: &[u8], b: &[u8]) -> Ordering {
64    a.iter()
65        .map(u8::to_ascii_lowercase)
66        .cmp(b.iter().map(u8::to_ascii_lowercase))
67}
68
69/// The hop-by-hop names of one message: the fixed set and every name its
70/// `Connection` fields list, read once, sorted, and deduplicated, so each
71/// lookup is a binary search. A head without `Connection` allocates
72/// nothing.
73pub(crate) struct HopByHop<'a> {
74    listed: Vec<&'a [u8]>,
75}
76
77impl<'a> HopByHop<'a> {
78    pub(crate) fn new(options: ListElements<'a, '_>) -> Self {
79        let mut listed: Vec<&'a [u8]> = options.collect();
80        listed.sort_unstable_by(|a, b| cmp_ignore_case(a, b));
81        listed.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
82        Self { listed }
83    }
84
85    pub(crate) fn contains(&self, name: &str) -> bool {
86        HOP_BY_HOP.iter().any(|h| h.eq_ignore_ascii_case(name))
87            || self
88                .listed
89                .binary_search_by(|o| cmp_ignore_case(o, name.as_bytes()))
90                .is_ok()
91    }
92}
93
94/// The fields of `headers` that `hop` does not name.
95fn end_to_end<'a>(
96    headers: Headers<'a>,
97    hop: HopByHop<'a>,
98) -> impl Iterator<Item = (&'a str, &'a [u8])> + use<'a> {
99    headers.filter(move |(name, _)| !hop.contains(name))
100}
101
102impl<'a> RequestHead<'a> {
103    /// The fields a proxy forwards: every field except the hop-by-hop ones
104    /// (RFC 9110 section 7.6.1, the rule of [`is_hop_by_hop`]), in order.
105    /// The `Connection` options are read once, so this is linear in the
106    /// head however many options and fields a client sends.
107    ///
108    /// Framing (`content-length`), `host`, and `proxy-authorization` are
109    /// end-to-end by this rule and are kept; a proxy rebuilds or drops
110    /// them, as [`Request::forward_from`](crate::Request::forward_from)
111    /// does.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use http_net::{ServerConn, ServerEvent};
117    ///
118    /// let mut conn = ServerConn::new();
119    /// let _ = conn.feed(b"GET / HTTP/1.1\r\nHost: a\r\nConnection: X-Trace\r\nX-Trace: 1\r\nKeep-Alive: 5\r\nAccept: */*\r\n\r\n");
120    /// let ServerEvent::Request(req) = conn.poll()? else { panic!() };
121    /// let names: Vec<&str> = req.end_to_end_headers().map(|(name, _)| name).collect();
122    /// assert_eq!(names, ["Host", "Accept"]);
123    /// # Ok::<(), http_net::HttpError>(())
124    /// ```
125    pub fn end_to_end_headers(&self) -> impl Iterator<Item = (&'a str, &'a [u8])> + use<'a> {
126        end_to_end(self.headers(), HopByHop::new(self.connection_options()))
127    }
128}
129
130impl<'a> ResponseHead<'a> {
131    /// The fields a proxy forwards: every field except the hop-by-hop ones
132    /// (as [`RequestHead::end_to_end_headers`]), in order, linear in the
133    /// head. `content-length` is kept; the next hop's framing is the
134    /// proxy's to write.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use http_net::{ClientConn, ClientEvent, Request};
140    ///
141    /// let mut conn = ClientConn::new();
142    /// conn.send_request(&Request::get("/").header("host", "a"), &mut Vec::new())?;
143    /// let _ = conn.feed(b"HTTP/1.1 200 OK\r\nConnection: x-hop\r\nX-Hop: 1\r\nETag: \"7\"\r\nContent-Length: 0\r\n\r\n");
144    /// let ClientEvent::Response(resp) = conn.poll()? else { panic!() };
145    /// let names: Vec<&str> = resp.end_to_end_headers().map(|(name, _)| name).collect();
146    /// assert_eq!(names, ["ETag", "Content-Length"]);
147    /// # Ok::<(), http_net::HttpError>(())
148    /// ```
149    pub fn end_to_end_headers(&self) -> impl Iterator<Item = (&'a str, &'a [u8])> + use<'a> {
150        end_to_end(
151            self.headers(),
152            HopByHop::new(self.header_list("connection")),
153        )
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use crate::{ServerConn, ServerEvent};
160
161    use super::*;
162
163    #[test]
164    fn test_is_hop_by_hop_fixed_set_and_connection_options() {
165        let mut conn = ServerConn::new();
166        let _ = conn.feed(
167            b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close, X-A\r\nConnection: x-b\r\n\r\n",
168        );
169        let Ok(ServerEvent::Request(req)) = conn.poll() else {
170            panic!("request");
171        };
172        for name in HOP_BY_HOP {
173            assert!(is_hop_by_hop(name, req.connection_options()), "{name}");
174            let upper = name.to_ascii_uppercase();
175            assert!(is_hop_by_hop(&upper, req.connection_options()), "{upper}");
176        }
177        assert!(is_hop_by_hop("X-A", req.connection_options()));
178        assert!(is_hop_by_hop("x-b", req.connection_options()));
179        // `close` is an option but also names no field anyone sends; it is
180        // still treated as listed.
181        assert!(is_hop_by_hop("close", req.connection_options()));
182        for name in ["host", "accept", "x-c", "content-length", "trailers"] {
183            assert!(!is_hop_by_hop(name, req.connection_options()), "{name}");
184        }
185    }
186
187    /// The sorted set answers exactly as the per-name scan does.
188    #[test]
189    fn test_hop_by_hop_set_agrees_with_is_hop_by_hop() {
190        let mut conn = ServerConn::new();
191        let _ = conn.feed(
192            b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close, X-A, x-a, Zed\r\nConnection: x-b, , \"q\"\r\n\r\n",
193        );
194        let Ok(ServerEvent::Request(req)) = conn.poll() else {
195            panic!("request");
196        };
197        let set = HopByHop::new(req.connection_options());
198        for name in [
199            "connection",
200            "TE",
201            "x-a",
202            "X-A",
203            "zed",
204            "ZED",
205            "x-b",
206            "close",
207            "host",
208            "accept",
209            "x",
210            "x-a2",
211            "",
212            "\"q\"",
213            "trailer",
214            "zee",
215        ] {
216            assert_eq!(
217                set.contains(name),
218                is_hop_by_hop(name, req.connection_options()),
219                "{name}"
220            );
221        }
222    }
223}