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 crate::iana::{cipher_name, sigalg_name};
use crate::profile::TlsProfile;

use super::parse_list;

const HASH_LEN: usize = 12;

const SERVER_NAME: u16 = 0x0000;

const ALPN: u16 = 0x0010;

fn is_hash(part: &str) -> bool {
    part.len() == HASH_LEN && part.bytes().all(|b| b.is_ascii_hexdigit())
}

impl TlsProfile {
    pub(super) fn apply_ja4(&mut self, raw: &str) -> Result<(), String> {
        let parts: Vec<&str> = raw.trim().split('_').collect();
        let [header, ciphers, extensions, sigalgs] = parts.as_slice() else {
            if parts.len() == 3 && parts[1..].iter().all(|part| is_hash(part)) {
                return Err(
                    "this is the hashed JA4 form; its SHA-256 sections cannot be inverted to \
                            cipher and extension lists. Pass the raw JA4_r (or JA4_ro) string"
                        .to_owned(),
                );
            }
            return Err(format!(
                "expected 4 underscore-separated sections (header_ciphers_extensions_sigalgs), got {}",
                parts.len()
            ));
        };
        let mut flags = header.chars();
        if flags.next() != Some('t') {
            return Err(format!(
                "header {header:?} is not a TCP TLS fingerprint (it must start with 't')"
            ));
        }
        if flags.nth(2) != Some('d') {
            return Err(format!(
                "header {header:?} has no SNI flag 'd'; leyline always sends server_name"
            ));
        }
        self.ciphers =
            self.iana_names(&parse_list(ciphers, 16, "cipher")?, cipher_name, "cipher")?;
        self.sigalgs = self.iana_names(
            &parse_list(sigalgs, 16, "signature algorithm")?,
            sigalg_name,
            "signature algorithm",
        )?;
        let mut ids = vec![SERVER_NAME, ALPN];
        ids.extend(parse_list(extensions, 16, "extension")?);
        self.apply_extensions(&ids)?;
        self.extension_permutation = None;
        Ok(())
    }
}