servo-fetch 0.14.0

Fetch, render, and extract web content as Markdown, JSON, or screenshots with an embedded Servo browser engine. No Chromium required.
//! Custom request-header parsing and validation, shared by every front-end.

use http::header::{HeaderMap, HeaderName, HeaderValue};

use crate::error::{Error, Result};

/// Hop-by-hop and message-framing headers the engine manages.
const RESERVED: &[&str] = &[
    "host",
    "content-length",
    "connection",
    "transfer-encoding",
    "te",
    "trailer",
    "upgrade",
    "keep-alive",
];

/// Headers with a dedicated option; steer callers there instead of raw headers.
fn steer(name: &str) -> Option<&'static str> {
    match name {
        "user-agent" => Some("the user-agent option"),
        "cookie" => Some("the cookies option"),
        _ => None,
    }
}

/// Build a validated [`HeaderMap`] from `name`/`value` pairs (e.g. a JSON object).
pub fn from_pairs<I, K, V>(pairs: I) -> Result<HeaderMap>
where
    I: IntoIterator<Item = (K, V)>,
    K: AsRef<str>,
    V: AsRef<str>,
{
    let mut headers = HeaderMap::new();
    for (name, value) in pairs {
        let (name, value) = validate(name.as_ref(), value.as_ref())?;
        headers.append(name, value);
    }
    Ok(headers)
}

/// Build a validated [`HeaderMap`] from curl-style `Name: Value` lines
/// (`Name;` sends an empty value).
pub fn parse_lines<I, S>(lines: I) -> Result<HeaderMap>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    let mut headers = HeaderMap::new();
    for line in lines {
        let (name, value) = split_line(line.as_ref())?;
        let (name, value) = validate(&name, &value)?;
        headers.append(name, value);
    }
    Ok(headers)
}

const MAX_WIRE_HEADERS: usize = 128;
const MAX_WIRE_HEADER_BYTES: usize = 64 * 1024;
/// Validate a header map against the same limits as the worker wire format.
pub(crate) fn validate_map(headers: &HeaderMap) -> Result<()> {
    from_wire_pairs(
        headers
            .iter()
            .map(|(name, value)| (name.as_str().to_owned(), value.as_bytes().to_vec()))
            .collect(),
    )
    .map(drop)
}

pub(crate) fn from_wire_pairs(headers: Vec<(String, Vec<u8>)>) -> Result<HeaderMap> {
    if headers.len() > MAX_WIRE_HEADERS {
        return Err(Error::invalid_header("too many custom headers in worker request"));
    }
    let mut total = 0usize;
    let mut out = HeaderMap::new();
    for (name, value) in headers {
        total = total.saturating_add(name.len()).saturating_add(value.len());
        if total > MAX_WIRE_HEADER_BYTES {
            return Err(Error::invalid_header(
                "custom headers in worker request exceed maximum bytes",
            ));
        }
        let name = validate_name(&name)?;
        let value = HeaderValue::from_bytes(&value)
            .map_err(|_| Error::invalid_header(format!("invalid value for header '{name}'")))?;
        out.append(name, value);
    }
    Ok(out)
}
fn validate(name: &str, value: &str) -> Result<(HeaderName, HeaderValue)> {
    let name = validate_name(name.trim())?;
    let value = HeaderValue::from_str(value)
        .map_err(|_| Error::invalid_header(format!("invalid value for header '{name}'")))?;
    Ok((name, value))
}
fn validate_name(name: &str) -> Result<HeaderName> {
    let name = HeaderName::from_bytes(name.as_bytes())
        .map_err(|_| Error::invalid_header(format!("invalid header name '{name}'")))?;
    if let Some(hint) = steer(name.as_str()) {
        return Err(Error::invalid_header(format!(
            "header '{name}' cannot be set as a custom header; use {hint} instead"
        )));
    }
    if RESERVED.contains(&name.as_str()) {
        return Err(Error::invalid_header(format!(
            "header '{name}' is managed by the engine and cannot be overridden"
        )));
    }
    Ok(name)
}

fn split_line(line: &str) -> Result<(String, String)> {
    match line.split_once(':') {
        Some((name, value)) => {
            let value = value.trim_start();
            if value.is_empty() {
                return Err(malformed(line));
            }
            Ok((name.to_owned(), value.to_owned()))
        }
        None => match line.strip_suffix(';') {
            Some(name) if !name.is_empty() => Ok((name.to_owned(), String::new())),
            _ => Err(malformed(line)),
        },
    }
}

fn malformed(line: &str) -> Error {
    Error::invalid_header(format!(
        "invalid header '{line}': expected 'Name: Value' (use 'Name;' for an empty value)"
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_and_appends_lines() {
        let h = parse_lines(["X-A: 1", "X-B: two"]).unwrap();
        assert_eq!(h.get("x-a").unwrap(), "1", "first header");
        assert_eq!(h.get("x-b").unwrap(), "two", "second header");
    }

    #[test]
    fn empty_value_via_trailing_semicolon() {
        let h = parse_lines(["X-Empty;"]).unwrap();
        assert_eq!(h.get("x-empty").unwrap(), "", "semicolon sends empty value");
    }

    #[test]
    fn from_pairs_builds_map() {
        let h = from_pairs([("X-A", "1"), ("X-A", "2")]).unwrap();
        let values: Vec<_> = h.get_all("x-a").iter().collect();
        assert_eq!(values.len(), 2, "duplicate names are appended");
    }

    #[test]
    fn rejects_crlf_injection() {
        assert!(parse_lines(["X-A: a\r\nEvil: b"]).is_err(), "CRLF must be rejected");
        assert!(from_pairs([("X-A", "a\r\nEvil: b")]).is_err(), "CRLF must be rejected");
    }

    #[test]
    fn rejects_reserved_and_steered() {
        assert!(parse_lines(["Host: x"]).is_err(), "framing header rejected");
        assert!(parse_lines(["User-Agent: x"]).is_err(), "user-agent steered");
        assert!(parse_lines(["Cookie: a=1"]).is_err(), "cookie steered");
    }

    #[test]
    fn rejects_malformed_lines() {
        assert!(parse_lines(["no-colon"]).is_err(), "missing colon");
        assert!(parse_lines(["X-Foo:"]).is_err(), "empty value needs 'Name;'");
    }
}