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}