leyline-http 0.1.5

HTTP client that impersonates Chrome, Firefox, and Safari on the wire: TLS (JA3/JA4), HTTP/2, and HTTP/3 fingerprints
Documentation
use super::*;

pub(in crate::pool::h1) fn validate(
    method: &str,
    headers: &[(String, String)],
) -> Result<(), H1PooledError> {
    if !is_valid_token(method) {
        return Err(H1PooledError::Config(format!(
            "invalid HTTP method `{method}`: non-token bytes not allowed"
        )));
    }
    for (name, value) in headers {
        if !is_valid_token(name) {
            return Err(H1PooledError::Config(format!(
                "invalid header name `{name}`: non-token bytes not allowed"
            )));
        }
        if !is_valid_header_value(value) {
            return Err(H1PooledError::Config(format!(
                "invalid value for header `{name}`: control characters not allowed"
            )));
        }
    }
    Ok(())
}

pub(super) fn target(url: &url::Url, target: H1Target) -> Result<(String, String), H1PooledError> {
    let host = url
        .host_str()
        .ok_or_else(|| H1PooledError::Config("no host in URL".into()))?;
    let port = url.port_or_known_default().ok_or_else(|| {
        H1PooledError::Config(format!("no default port for scheme {}", url.scheme()))
    })?;
    let authority = crate::util::authority(url, host, port);
    let path = crate::util::request_target(url);
    let request_target = match target {
        H1Target::OriginForm => path.to_owned(),
        H1Target::AbsoluteForm => {
            format!("{}://{}{}", url.scheme(), authority, path)
        }
    };
    if !is_valid_request_target(&request_target) {
        return Err(H1PooledError::Config(
            "invalid request target: control characters not allowed".to_string(),
        ));
    }
    Ok((request_target, authority))
}

pub(super) fn frame(method: &str, headers: &mut Vec<(String, String)>, body: H1Body) -> Framing {
    let has_cl = contains_header(headers, "content-length");
    let has_te = contains_header(headers, "transfer-encoding");
    match body {
        H1Body::Empty => {
            if method_typically_has_body(method) && !has_cl && !has_te {
                headers.push(("Content-Length".into(), "0".into()));
            }
            Framing::None
        }
        H1Body::Buffered(b) => {
            if !has_cl && !has_te {
                headers.push(("Content-Length".into(), b.len().to_string()));
            }
            Framing::Buffered(b)
        }
        H1Body::FixedStream { stream, length } => {
            if !has_cl && !has_te {
                headers.push(("Content-Length".into(), length.to_string()));
            }
            Framing::Fixed { stream }
        }
        H1Body::ChunkedStream { stream } => {
            if !has_te {
                headers.push(("Transfer-Encoding".into(), "chunked".into()));
            }
            Framing::Chunked { stream }
        }
    }
}

pub(super) fn head(method: &str, request_target: &str, headers: &[(String, String)]) -> Vec<u8> {
    let mut req = Vec::new();
    req.extend_from_slice(format!("{method} {request_target} HTTP/1.1\r\n").as_bytes());
    for (name, value) in headers {
        let name = h1_header_name(name);
        req.extend_from_slice(name.as_bytes());
        req.extend_from_slice(b": ");
        req.extend_from_slice(value.as_bytes());
        req.extend_from_slice(b"\r\n");
    }
    req.extend_from_slice(b"\r\n");
    req
}