leyline-http 0.1.1

HTTP client that mimics browsers on the wire
Documentation
use crate::audit::{hash12, non_grease_cipher_ids, non_grease_ext_ids};
use crate::iana::sigalg_id;

#[derive(Debug)]
pub struct Ja4Input<'a> {
    pub ciphers: &'a [String],
    pub sigalgs: &'a [String],
    pub curves: &'a [String],
    pub extension_ids: &'a [u16],
    pub tls_version: &'a str,
    pub has_sni: bool,
    pub alpn: &'a str,
}

pub fn compute_ja4(input: &Ja4Input<'_>) -> String {
    let section_a = compute_section_a(input);
    let section_b = compute_section_b(input);
    let section_c = compute_section_c(input);
    format!("{section_a}_{section_b}_{section_c}")
}

fn compute_section_a(input: &Ja4Input<'_>) -> String {
    let proto = "t";
    let version = match input.tls_version {
        "1.3" => "13",
        "1.2" => "12",
        "1.1" => "11",
        "1.0" => "10",
        _ => "00",
    };

    let sni = if input.has_sni { "d" } else { "i" };

    let cipher_count = non_grease_cipher_ids(input.ciphers).len().min(99);

    let ext_count = non_grease_ext_ids(input.extension_ids).len().min(99);

    let alpn = if input.alpn.is_empty() {
        "00".to_string()
    } else {
        let bytes = input.alpn.as_bytes();
        let first = bytes[0] as char;
        let last = bytes[bytes.len() - 1] as char;
        format!("{first}{last}")
    };

    format!("{proto}{version}{sni}{cipher_count:02}{ext_count:02}{alpn}")
}

fn compute_section_b(input: &Ja4Input<'_>) -> String {
    let mut ids = non_grease_cipher_ids(input.ciphers);

    ids.sort();

    let s: String = ids
        .iter()
        .map(|id| format!("{id:04x}"))
        .collect::<Vec<_>>()
        .join(",");

    hash12(&s)
}

fn compute_section_c(input: &Ja4Input<'_>) -> String {
    let mut ext_ids: Vec<u16> = non_grease_ext_ids(input.extension_ids)
        .into_iter()
        .filter(|id| *id != 0x0000 && *id != 0x0010)
        .collect();
    ext_ids.sort();

    let ext_str: String = ext_ids
        .iter()
        .map(|id| format!("{id:04x}"))
        .collect::<Vec<_>>()
        .join(",");

    let sigalg_str: String = input
        .sigalgs
        .iter()
        .filter_map(|s| sigalg_id(s))
        .map(|id| format!("{id:04x}"))
        .collect::<Vec<_>>()
        .join(",");

    let combined = format!("{ext_str}_{sigalg_str}");
    hash12(&combined)
}

#[cfg(test)]
mod tests;