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::*;

const MINIMAL: &str = r#"
[meta]
name = "Test 1"
browser = "test"
version = 1
family = "chromium"
captured_against = "test-1.0"

[tls]
ciphers = ["TLS_AES_128_GCM_SHA256"]
curves = ["X25519"]
sigalgs = ["ecdsa_secp256r1_sha256"]

[h2]
pseudo_order = ["method", "authority", "scheme", "path"]
settings_order = ["header_table_size"]
"#;

fn scratch(name: &str) -> std::path::PathBuf {
    let stamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("clock after epoch")
        .as_nanos();
    let dir = std::env::temp_dir().join(format!("leyline-{name}-{stamp}"));
    std::fs::create_dir_all(&dir).expect("create scratch dir");
    dir
}

#[test]
fn load_reads_a_family_directory() {
    let dir = scratch("load");
    let family = dir.join("test");
    std::fs::create_dir_all(&family).expect("create family dir");
    std::fs::write(family.join("1.toml"), MINIMAL).expect("write profile");

    let reg = ProfileRegistry::load(&dir).expect("load scratch registry");
    assert_eq!(reg.profiles.len(), 1);
    let profile = reg.get("test", 1).expect("test 1 not found");
    assert_eq!(profile.meta.name, "Test 1");
    assert_eq!(profile.tls.ciphers, vec!["TLS_AES_128_GCM_SHA256"]);

    std::fs::remove_dir_all(&dir).expect("clean scratch dir");
}

#[test]
fn load_rejects_an_invalid_permutation() {
    let dir = scratch("invalid");
    let family = dir.join("test");
    std::fs::create_dir_all(&family).expect("create family dir");
    let bad = MINIMAL.replace(
        "sigalgs = [\"ecdsa_secp256r1_sha256\"]",
        "sigalgs = [\"ecdsa_secp256r1_sha256\"]\nextension_permutation = [0]",
    );
    std::fs::write(family.join("1.toml"), bad).expect("write profile");

    match ProfileRegistry::load(&dir) {
        Err(ProfileError::Parse { .. }) => {}
        Err(other) => panic!("expected a Parse error, got {other}"),
        Ok(_) => panic!("permutation must be validated"),
    }

    std::fs::remove_dir_all(&dir).expect("clean scratch dir");
}

#[test]
fn load_rejects_an_empty_directory() {
    let dir = scratch("empty");
    match ProfileRegistry::load(&dir) {
        Err(ProfileError::Empty { .. }) => {}
        Err(other) => panic!("expected an Empty error, got {other}"),
        Ok(_) => panic!("empty directory must not load"),
    }
    std::fs::remove_dir_all(&dir).expect("clean scratch dir");
}

#[test]
#[should_panic(expected = "built-in profile is statically valid")]
fn malformed_builtin_toml_panics_at_load() {
    let mut reg = ProfileRegistry::new();
    reg.load_toml("this is not a browser profile");
}

#[test]
fn builtin_loads_all_profiles() {
    let reg = ProfileRegistry::builtin();
    assert_eq!(
        reg.profiles.len(),
        crate::profile::Browser::all().len(),
        "registry count != Browser::all() length"
    );
}

#[test]
fn every_browser_variant_resolves() {
    let reg = ProfileRegistry::builtin();
    for browser in crate::profile::Browser::all().iter().copied() {
        assert!(
            reg.get_browser(browser).is_some(),
            "no profile for {browser}"
        );
    }
}

#[test]
fn every_profile_has_fingerprint() {
    let reg = ProfileRegistry::builtin();
    for browser in crate::profile::Browser::all().iter().copied() {
        let profile = reg.get_browser(browser).unwrap();
        let has_ja4 = profile.expected_ja4().is_some();
        let has_h2 = profile.expected_h2_fingerprint().is_some();
        assert!(
            has_ja4 || has_h2,
            "{browser} has no expected fingerprints in TOML"
        );
    }
}

#[test]
fn chrome147_profile_parses() {
    let reg = ProfileRegistry::builtin();
    let profile = reg.get("chrome", 147).expect("chrome 147 not found");
    assert_eq!(profile.meta.name, "Chrome 147");
    assert_eq!(profile.tls.ciphers.len(), 15);
    assert_eq!(profile.tls.curves.len(), 4);
    assert!(profile.tls.permute_extensions);
    assert!(profile.tls.ech_grease);
    assert_eq!(
        profile.h2.pseudo_order,
        vec!["method", "authority", "scheme", "path"]
    );
    assert!(profile.identity.contains_key("windows"));
}

#[test]
fn firefox148_has_extension_permutation() {
    let reg = ProfileRegistry::builtin();
    let profile = reg.get("firefox", 148).expect("firefox 148 not found");
    assert!(profile.tls.extension_permutation.is_some());
    assert_eq!(profile.tls.ciphers.len(), 17);
    assert_eq!(
        profile.h2.pseudo_order,
        vec!["method", "path", "authority", "scheme"]
    );
}

#[test]
fn firefox151_profile_parses() {
    let reg = ProfileRegistry::builtin();
    let profile = reg.get("firefox", 151).expect("firefox 151 not found");
    assert_eq!(profile.meta.name, "Firefox 151");
    assert_eq!(profile.tls.ciphers.len(), 16);
    assert_eq!(
        profile.h2.pseudo_order,
        vec!["method", "path", "authority", "scheme"]
    );
    assert_eq!(
        profile.expected_ja4(),
        Some("t13d1617h2_86a278354501_3cbfd9057e0d")
    );
}

#[test]
fn firefox152_profile_parses() {
    let reg = ProfileRegistry::builtin();
    let profile = reg.get("firefox", 152).expect("firefox 152 not found");
    assert_eq!(profile.meta.name, "Firefox 152");
    assert_eq!(profile.tls.ciphers.len(), 16);
    assert_eq!(
        profile.expected_ja4(),
        Some("t13d1617h2_86a278354501_3cbfd9057e0d")
    );
}

#[test]
fn firefox150_profile_parses() {
    let reg = ProfileRegistry::builtin();
    let profile = reg.get("firefox", 150).expect("firefox 150 not found");
    assert_eq!(profile.meta.name, "Firefox 150");
    assert_eq!(profile.tls.ciphers.len(), 17);
    assert_eq!(
        profile.h2.pseudo_order,
        vec!["method", "path", "authority", "scheme"]
    );
    assert_eq!(
        profile.expected_ja4(),
        Some("t13d1717h2_5b57614c22b0_3cbfd9057e0d")
    );
    assert_eq!(
        profile
            .tls
            .fingerprint
            .as_ref()
            .and_then(|fp| fp.resumed_ja4.as_deref()),
        Some("t13d1717h2_5b57614c22b0_e6dcd7ae0a9e")
    );
}