soth-mitm 0.2.2

Rust intercepting proxy crate with deterministic handler/event contracts for SOTH.
Documentation
use super::HttpHeader;
use std::io;

pub(crate) fn canonicalize_http_headers(headers: Vec<HttpHeader>) -> io::Result<Vec<HttpHeader>> {
    let mut canonical = Vec::with_capacity(headers.len());
    for header in headers {
        let name = header.name.trim();
        if name.is_empty() || !is_valid_http_header_name(name) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "invalid HTTP header name",
            ));
        }

        let value = header.value.trim();
        if !is_valid_http_header_value(value) {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "invalid HTTP header value",
            ));
        }

        canonical.push(HttpHeader {
            name: name.to_ascii_lowercase(),
            value: value.to_string(),
        });
    }
    Ok(canonical)
}

fn is_valid_http_header_name(name: &str) -> bool {
    name.bytes().all(is_http_token_char)
}

fn is_http_token_char(byte: u8) -> bool {
    byte.is_ascii_alphanumeric()
        || matches!(
            byte,
            b'!' | b'#'
                | b'$'
                | b'%'
                | b'&'
                | b'\''
                | b'*'
                | b'+'
                | b'-'
                | b'.'
                | b'^'
                | b'_'
                | b'`'
                | b'|'
                | b'~'
        )
}

fn is_valid_http_header_value(value: &str) -> bool {
    !value
        .as_bytes()
        .iter()
        .any(|byte| matches!(*byte, 0x00..=0x08 | 0x0A..=0x1F | 0x7F))
}

pub(crate) fn parse_content_length(headers: &[HttpHeader]) -> io::Result<Option<u64>> {
    let mut value = None;
    for header in headers {
        if header.name.eq_ignore_ascii_case("content-length") {
            for raw in header.value.split(',') {
                let trimmed = raw.trim();
                if trimmed.is_empty() {
                    return Err(io::Error::new(
                        io::ErrorKind::InvalidData,
                        "invalid Content-Length value",
                    ));
                }
                if !trimmed.bytes().all(|byte| byte.is_ascii_digit()) {
                    return Err(io::Error::new(
                        io::ErrorKind::InvalidData,
                        "invalid Content-Length value",
                    ));
                }
                let parsed = trimmed.parse::<u64>().map_err(|_| {
                    io::Error::new(io::ErrorKind::InvalidData, "invalid Content-Length value")
                })?;
                if let Some(existing) = value {
                    if existing != parsed {
                        return Err(io::Error::new(
                            io::ErrorKind::InvalidData,
                            "conflicting Content-Length values",
                        ));
                    }
                } else {
                    value = Some(parsed);
                }
            }
        }
    }
    Ok(value)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ParsedTransferEncoding {
    pub(crate) chunked: bool,
}

pub(crate) fn parse_transfer_encoding(
    headers: &[HttpHeader],
) -> io::Result<ParsedTransferEncoding> {
    let mut parsed = ParsedTransferEncoding { chunked: false };
    for header in headers {
        if !header.name.eq_ignore_ascii_case("transfer-encoding") {
            continue;
        }
        for coding in header.value.split(',') {
            let token = coding.split(';').next().unwrap_or("").trim();
            if token.is_empty() {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    "invalid Transfer-Encoding value",
                ));
            }
            if token.eq_ignore_ascii_case("chunked") {
                if parsed.chunked {
                    return Err(io::Error::new(
                        io::ErrorKind::InvalidData,
                        "invalid Transfer-Encoding value",
                    ));
                }
                parsed.chunked = true;
                continue;
            }
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "unsupported Transfer-Encoding value",
            ));
        }
    }
    Ok(parsed)
}