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 leyline::h2::config::PseudoOrder;
use leyline::h2::connection::PseudoHeaders;
use leyline::h2::error::{ErrorCode, H2Error};

const CHROME_ORDER: [PseudoOrder; 4] = [
    PseudoOrder::Method,
    PseudoOrder::Authority,
    PseudoOrder::Scheme,
    PseudoOrder::Path,
];

fn names<'a>(list: &[(&'a str, &'a str)]) -> Vec<&'a str> {
    list.iter().map(|(n, _)| *n).collect()
}

#[test]
fn classic_connect_omits_scheme_and_path() {
    let p = PseudoHeaders {
        method: "CONNECT".into(),
        scheme: "https".into(),
        authority: "example.com:443".into(),
        path: "/unused".into(),
        protocol: None,
    };
    let (list, len) = p.build_pseudo_list(&CHROME_ORDER).unwrap();
    let list = &list[..len];
    assert_eq!(names(list), vec![":method", ":authority"]);
    assert_eq!(list[0].1, "CONNECT");
    assert_eq!(list[1].1, "example.com:443");
}

#[test]
fn extended_connect_emits_protocol() {
    let p = PseudoHeaders {
        method: "CONNECT".into(),
        scheme: "https".into(),
        authority: "example.com:443".into(),
        path: "/chat".into(),
        protocol: Some("websocket".into()),
    };
    let (list, len) = p.build_pseudo_list(&CHROME_ORDER).unwrap();
    let list = &list[..len];
    assert_eq!(
        names(list),
        vec![":method", ":authority", ":scheme", ":path", ":protocol"]
    );
    assert_eq!(list[4].1, "websocket");
}

#[test]
fn non_connect_emits_all_four_pseudos() {
    let p = PseudoHeaders {
        method: "GET".into(),
        scheme: "https".into(),
        authority: "example.com".into(),
        path: "/".into(),
        protocol: None,
    };
    let (list, len) = p.build_pseudo_list(&CHROME_ORDER).unwrap();
    let list = &list[..len];
    assert_eq!(
        names(list),
        vec![":method", ":authority", ":scheme", ":path"]
    );
}

#[test]
fn non_connect_with_protocol_still_appends_it() {
    let p = PseudoHeaders {
        method: "GET".into(),
        scheme: "https".into(),
        authority: "example.com".into(),
        path: "/".into(),
        protocol: Some("websocket".into()),
    };
    let (list, len) = p.build_pseudo_list(&CHROME_ORDER).unwrap();
    let list = &list[..len];
    assert_eq!(list.last().map(|(n, _)| *n), Some(":protocol"));
}

#[test]
fn connect_without_authority_rejected() {
    let p = PseudoHeaders {
        method: "CONNECT".into(),
        scheme: "https".into(),
        authority: "".into(),
        path: "/".into(),
        protocol: None,
    };
    let err = p.build_pseudo_list(&CHROME_ORDER).unwrap_err();
    match err {
        H2Error::Connection { code, .. } => assert_eq!(code, ErrorCode::ProtocolError),
        other => panic!("expected Connection error, got {other:?}"),
    }
}

#[test]
fn pseudo_order_honoured_for_non_connect() {
    let order = [
        PseudoOrder::Method,
        PseudoOrder::Path,
        PseudoOrder::Authority,
        PseudoOrder::Scheme,
    ];
    let p = PseudoHeaders {
        method: "POST".into(),
        scheme: "https".into(),
        authority: "example.com".into(),
        path: "/api".into(),
        protocol: None,
    };
    let (list, len) = p.build_pseudo_list(&order).unwrap();
    let list = &list[..len];
    assert_eq!(
        names(list),
        vec![":method", ":path", ":authority", ":scheme"]
    );
}