Skip to main content

servo_fetch/
headers.rs

1//! Custom request-header parsing and validation, shared by every front-end.
2
3use http::header::{HeaderMap, HeaderName, HeaderValue};
4
5use crate::error::{Error, Result};
6
7/// Hop-by-hop and message-framing headers the engine manages.
8const RESERVED: &[&str] = &[
9    "host",
10    "content-length",
11    "connection",
12    "transfer-encoding",
13    "te",
14    "trailer",
15    "upgrade",
16    "keep-alive",
17];
18
19/// Headers with a dedicated option; steer callers there instead of raw headers.
20fn steer(name: &str) -> Option<&'static str> {
21    match name {
22        "user-agent" => Some("the user-agent option"),
23        "cookie" => Some("the cookies option"),
24        _ => None,
25    }
26}
27
28/// Build a validated [`HeaderMap`] from `name`/`value` pairs (e.g. a JSON object).
29pub fn from_pairs<I, K, V>(pairs: I) -> Result<HeaderMap>
30where
31    I: IntoIterator<Item = (K, V)>,
32    K: AsRef<str>,
33    V: AsRef<str>,
34{
35    let mut headers = HeaderMap::new();
36    for (name, value) in pairs {
37        let (name, value) = validate(name.as_ref(), value.as_ref())?;
38        headers.append(name, value);
39    }
40    Ok(headers)
41}
42
43/// Build a validated [`HeaderMap`] from curl-style `Name: Value` lines
44/// (`Name;` sends an empty value).
45pub fn parse_lines<I, S>(lines: I) -> Result<HeaderMap>
46where
47    I: IntoIterator<Item = S>,
48    S: AsRef<str>,
49{
50    let mut headers = HeaderMap::new();
51    for line in lines {
52        let (name, value) = split_line(line.as_ref())?;
53        let (name, value) = validate(&name, &value)?;
54        headers.append(name, value);
55    }
56    Ok(headers)
57}
58
59fn validate(name: &str, value: &str) -> Result<(HeaderName, HeaderValue)> {
60    let name = HeaderName::from_bytes(name.trim().as_bytes())
61        .map_err(|_| Error::invalid_header(format!("invalid header name '{}'", name.trim())))?;
62    if let Some(hint) = steer(name.as_str()) {
63        return Err(Error::invalid_header(format!(
64            "header '{name}' cannot be set as a custom header; use {hint} instead"
65        )));
66    }
67    if RESERVED.contains(&name.as_str()) {
68        return Err(Error::invalid_header(format!(
69            "header '{name}' is managed by the engine and cannot be overridden"
70        )));
71    }
72    let value = HeaderValue::from_str(value)
73        .map_err(|_| Error::invalid_header(format!("invalid value for header '{name}'")))?;
74    Ok((name, value))
75}
76
77fn split_line(line: &str) -> Result<(String, String)> {
78    match line.split_once(':') {
79        Some((name, value)) => {
80            let value = value.trim_start();
81            if value.is_empty() {
82                return Err(malformed(line));
83            }
84            Ok((name.to_owned(), value.to_owned()))
85        }
86        None => match line.strip_suffix(';') {
87            Some(name) if !name.is_empty() => Ok((name.to_owned(), String::new())),
88            _ => Err(malformed(line)),
89        },
90    }
91}
92
93fn malformed(line: &str) -> Error {
94    Error::invalid_header(format!(
95        "invalid header '{line}': expected 'Name: Value' (use 'Name;' for an empty value)"
96    ))
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn parses_and_appends_lines() {
105        let h = parse_lines(["X-A: 1", "X-B: two"]).unwrap();
106        assert_eq!(h.get("x-a").unwrap(), "1", "first header");
107        assert_eq!(h.get("x-b").unwrap(), "two", "second header");
108    }
109
110    #[test]
111    fn empty_value_via_trailing_semicolon() {
112        let h = parse_lines(["X-Empty;"]).unwrap();
113        assert_eq!(h.get("x-empty").unwrap(), "", "semicolon sends empty value");
114    }
115
116    #[test]
117    fn from_pairs_builds_map() {
118        let h = from_pairs([("X-A", "1"), ("X-A", "2")]).unwrap();
119        let values: Vec<_> = h.get_all("x-a").iter().collect();
120        assert_eq!(values.len(), 2, "duplicate names are appended");
121    }
122
123    #[test]
124    fn rejects_crlf_injection() {
125        assert!(parse_lines(["X-A: a\r\nEvil: b"]).is_err(), "CRLF must be rejected");
126        assert!(from_pairs([("X-A", "a\r\nEvil: b")]).is_err(), "CRLF must be rejected");
127    }
128
129    #[test]
130    fn rejects_reserved_and_steered() {
131        assert!(parse_lines(["Host: x"]).is_err(), "framing header rejected");
132        assert!(parse_lines(["User-Agent: x"]).is_err(), "user-agent steered");
133        assert!(parse_lines(["Cookie: a=1"]).is_err(), "cookie steered");
134    }
135
136    #[test]
137    fn rejects_malformed_lines() {
138        assert!(parse_lines(["no-colon"]).is_err(), "missing colon");
139        assert!(parse_lines(["X-Foo:"]).is_err(), "empty value needs 'Name;'");
140    }
141}