Skip to main content

hyper/
headers.rs

1#[cfg(all(feature = "client", feature = "http1"))]
2use bytes::BytesMut;
3use http::header::HeaderValue;
4#[cfg(all(feature = "http2", feature = "client"))]
5use http::Method;
6#[cfg(any(feature = "client", all(feature = "server", feature = "http2")))]
7use http::{
8    header::{ValueIter, CONTENT_LENGTH},
9    HeaderMap,
10};
11
12#[cfg(feature = "http1")]
13pub(super) fn connection_keep_alive(value: &HeaderValue) -> bool {
14    connection_has(value, "keep-alive")
15}
16
17#[cfg(feature = "http1")]
18pub(super) fn connection_close(value: &HeaderValue) -> bool {
19    connection_has(value, "close")
20}
21
22// Returns true if any `Connection` header field carries a `close` token.
23// A message may have more than one `Connection` header line, so all of them
24// must be inspected (`get`/`connection_close` alone only sees the first).
25#[cfg(feature = "http1")]
26pub(super) fn connection_any_close(headers: &http::HeaderMap) -> bool {
27    headers
28        .get_all(http::header::CONNECTION)
29        .iter()
30        .any(connection_close)
31}
32
33#[cfg(feature = "http1")]
34fn connection_has(value: &HeaderValue, needle: &str) -> bool {
35    if let Ok(s) = value.to_str() {
36        for val in s.split(',') {
37            if val.trim().eq_ignore_ascii_case(needle) {
38                return true;
39            }
40        }
41    }
42    false
43}
44
45#[cfg(feature = "http1")]
46pub(super) fn te_is_trailers(headers: &http::HeaderMap) -> bool {
47    header_value_list_has(headers.get_all(http::header::TE).into_iter(), "trailers")
48}
49
50#[cfg(feature = "http1")]
51fn header_value_list_has(values: http::header::ValueIter<'_, HeaderValue>, needle: &str) -> bool {
52    for value in values {
53        if let Ok(line) = value.to_str() {
54            for token in line.split(',') {
55                if token.trim().eq_ignore_ascii_case(needle) {
56                    return true;
57                }
58            }
59        }
60    }
61
62    false
63}
64
65#[cfg(all(feature = "http1", feature = "server"))]
66pub(super) fn content_length_parse(value: &HeaderValue) -> Option<u64> {
67    from_digits(value.as_bytes())
68}
69
70#[cfg(any(feature = "client", all(feature = "server", feature = "http2")))]
71pub(super) fn content_length_parse_all(headers: &HeaderMap) -> Option<u64> {
72    content_length_parse_all_values(headers.get_all(CONTENT_LENGTH).into_iter())
73}
74
75#[cfg(any(feature = "client", all(feature = "server", feature = "http2")))]
76pub(super) fn content_length_parse_all_values(values: ValueIter<'_, HeaderValue>) -> Option<u64> {
77    // If multiple Content-Length headers were sent, everything can still
78    // be alright if they all contain the same value, and all parse
79    // correctly. If not, then it's an error.
80
81    let mut content_length: Option<u64> = None;
82    for h in values {
83        if let Ok(line) = h.to_str() {
84            for v in line.split(',') {
85                let n = from_digits(v.trim().as_bytes())?;
86
87                if content_length.is_none() {
88                    content_length = Some(n);
89                } else if content_length != Some(n) {
90                    return None;
91                }
92            }
93        } else {
94            return None;
95        }
96    }
97
98    content_length
99}
100
101fn from_digits(bytes: &[u8]) -> Option<u64> {
102    // cannot use FromStr for u64, since it allows a signed prefix
103    let mut result = 0u64;
104    const RADIX: u64 = 10;
105
106    if bytes.is_empty() {
107        return None;
108    }
109
110    for &b in bytes {
111        // can't use char::to_digit, since we haven't verified these bytes
112        // are utf-8.
113        match b {
114            b'0'..=b'9' => {
115                result = result.checked_mul(RADIX)?;
116                result = result.checked_add(u64::from(b - b'0'))?;
117            }
118            _ => {
119                // not a DIGIT, get outta here!
120                return None;
121            }
122        }
123    }
124
125    Some(result)
126}
127
128#[cfg(all(feature = "http2", feature = "client"))]
129pub(super) fn method_has_defined_payload_semantics(method: &Method) -> bool {
130    !matches!(
131        *method,
132        Method::GET | Method::HEAD | Method::DELETE | Method::CONNECT
133    )
134}
135
136#[cfg(feature = "http2")]
137pub(super) fn set_content_length_if_missing(headers: &mut HeaderMap, len: u64) {
138    headers
139        .entry(CONTENT_LENGTH)
140        .or_insert_with(|| HeaderValue::from(len));
141}
142
143#[cfg(all(feature = "client", feature = "http1"))]
144pub(super) fn transfer_encoding_is_chunked(headers: &HeaderMap) -> bool {
145    is_chunked(headers.get_all(http::header::TRANSFER_ENCODING).into_iter())
146}
147
148#[cfg(all(feature = "client", feature = "http1"))]
149pub(super) fn is_chunked(mut encodings: ValueIter<'_, HeaderValue>) -> bool {
150    // chunked must always be the last encoding, according to spec
151    if let Some(line) = encodings.next_back() {
152        return is_chunked_(line);
153    }
154
155    false
156}
157
158#[cfg(feature = "http1")]
159pub(super) fn is_chunked_(value: &HeaderValue) -> bool {
160    // chunked must always be the last encoding, according to spec
161    if let Ok(s) = value.to_str() {
162        if let Some(encoding) = s.rsplit(',').next() {
163            return encoding.trim().eq_ignore_ascii_case("chunked");
164        }
165    }
166
167    false
168}
169
170#[cfg(all(feature = "client", feature = "http1"))]
171pub(super) fn add_chunked(mut entry: http::header::OccupiedEntry<'_, HeaderValue>) {
172    const CHUNKED: &str = "chunked";
173
174    if let Some(line) = entry.iter_mut().next_back() {
175        // + 2 for ", "
176        let new_cap = line.as_bytes().len() + CHUNKED.len() + 2;
177        let mut buf = BytesMut::with_capacity(new_cap);
178        buf.extend_from_slice(line.as_bytes());
179        buf.extend_from_slice(b", ");
180        buf.extend_from_slice(CHUNKED.as_bytes());
181
182        *line = HeaderValue::from_maybe_shared(buf.freeze())
183            .expect("original header value plus ascii is valid");
184        return;
185    }
186
187    entry.insert(HeaderValue::from_static(CHUNKED));
188}
189
190#[cfg(all(test, feature = "http1"))]
191mod tests {
192    use super::te_is_trailers;
193    use http::header::{HeaderValue, TE};
194    use http::HeaderMap;
195
196    #[test]
197    fn te_is_trailers_accepts_comma_separated_values() {
198        let mut headers = HeaderMap::new();
199        headers.insert(TE, HeaderValue::from_static("gzip, Trailers"));
200
201        assert!(te_is_trailers(&headers));
202    }
203
204    #[test]
205    fn te_is_trailers_accepts_multiple_header_lines() {
206        let mut headers = HeaderMap::new();
207        headers.append(TE, HeaderValue::from_static("gzip"));
208        headers.append(TE, HeaderValue::from_static("trailers"));
209
210        assert!(te_is_trailers(&headers));
211    }
212
213    #[test]
214    fn te_is_trailers_rejects_missing_trailers_token() {
215        let mut headers = HeaderMap::new();
216        headers.insert(TE, HeaderValue::from_static("gzip"));
217
218        assert!(!te_is_trailers(&headers));
219    }
220}