use http::header::{HeaderName, HeaderValue};
use once_cell::sync::Lazy;
use regex::Regex;
use sha2::{Digest, Sha256};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Ja4Protocol {
TCP,
QUIC,
}
impl std::fmt::Display for Ja4Protocol {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Ja4Protocol::TCP => write!(f, "TCP"),
Ja4Protocol::QUIC => write!(f, "QUIC"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Ja4SniType {
Domain,
IP,
None,
}
impl std::fmt::Display for Ja4SniType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Ja4SniType::Domain => write!(f, "Domain"),
Ja4SniType::IP => write!(f, "IP"),
Ja4SniType::None => write!(f, "None"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ja4Fingerprint {
pub raw: String,
pub protocol: Ja4Protocol,
pub tls_version: u8,
pub sni_type: Ja4SniType,
pub cipher_count: u8,
pub ext_count: u8,
pub alpn: String,
pub cipher_hash: String,
pub ext_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ja4hFingerprint {
pub raw: String,
pub method: String,
pub http_version: u8,
pub has_cookie: bool,
pub has_referer: bool,
pub accept_lang: String,
pub header_hash: String,
pub cookie_hash: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClientFingerprint {
pub ja4: Option<Ja4Fingerprint>,
pub ja4h: Ja4hFingerprint,
pub combined_hash: String,
}
#[derive(Debug, Clone)]
pub struct Ja4Analysis {
pub fingerprint: Ja4Fingerprint,
pub suspicious: bool,
pub issues: Vec<String>,
pub estimated_client: String,
}
#[derive(Debug, Clone)]
pub struct Ja4hAnalysis {
pub fingerprint: Ja4hFingerprint,
pub suspicious: bool,
pub issues: Vec<String>,
}
static JA4_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^([tq])(\d{2})([di]?)([0-9a-f]{2})([0-9a-f]{2})([a-z0-9]{2})_([0-9a-f]{12})_([0-9a-f]{12})$")
.expect("JA4 regex should compile")
});
static JA4H_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-z]{2}\d{2}[cn][rn][a-z0-9]{2}_[0-9a-f]{12}_[0-9a-f]{12}$")
.expect("JA4H regex should compile")
});
static METHOD_MAP: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("GET", "ge");
m.insert("POST", "po");
m.insert("PUT", "pu");
m.insert("DELETE", "de");
m.insert("HEAD", "he");
m.insert("OPTIONS", "op");
m.insert("PATCH", "pa");
m.insert("CONNECT", "co");
m.insert("TRACE", "tr");
m
});
static ALPN_MAP: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
let mut m = HashMap::new();
m.insert("h1", "http/1.1");
m.insert("h2", "h2");
m.insert("h3", "h3");
m.insert("00", "unknown");
m
});
static EXCLUDED_HEADERS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
let mut s = HashSet::new();
s.insert("cookie");
s.insert("referer");
s.insert(":method");
s.insert(":path");
s.insert(":scheme");
s.insert(":authority");
s.insert("host");
s.insert("content-length");
s.insert("content-type");
s
});
pub fn parse_ja4_from_header(header: Option<&str>) -> Option<Ja4Fingerprint> {
let header = header?;
let normalized = header.trim();
if normalized.is_empty() || normalized.len() > 100 {
return None;
}
let caps = JA4_REGEX.captures(normalized)?;
let protocol_char = caps.get(1)?.as_str();
let version_str = caps.get(2)?.as_str();
let sni_char = caps.get(3).map(|m| m.as_str()).unwrap_or("");
let cipher_count_str = caps.get(4)?.as_str();
let ext_count_str = caps.get(5)?.as_str();
let alpn_str = caps.get(6)?.as_str();
let cipher_hash = caps.get(7)?.as_str();
let ext_hash = caps.get(8)?.as_str();
let tls_version = version_str.parse::<u8>().ok()?;
let cipher_count = u8::from_str_radix(cipher_count_str, 16).ok()?;
let ext_count = u8::from_str_radix(ext_count_str, 16).ok()?;
if !(10..=13).contains(&tls_version) {
return None;
}
if cipher_hash.len() != 12 || ext_hash.len() != 12 {
return None;
}
let protocol = if protocol_char.to_lowercase() == "q" {
Ja4Protocol::QUIC
} else {
Ja4Protocol::TCP
};
let sni_type = match sni_char {
"d" => Ja4SniType::Domain,
"i" => Ja4SniType::IP,
_ => Ja4SniType::None,
};
let alpn = ALPN_MAP
.get(alpn_str.to_lowercase().as_str())
.copied()
.unwrap_or(alpn_str)
.to_string();
Some(Ja4Fingerprint {
raw: normalized.to_lowercase(),
protocol,
tls_version,
sni_type,
cipher_count,
ext_count,
alpn,
cipher_hash: cipher_hash.to_lowercase(),
ext_hash: ext_hash.to_lowercase(),
})
}
pub struct HttpHeaders<'a> {
pub headers: &'a [(HeaderName, HeaderValue)],
pub method: &'a str,
pub http_version: &'a str,
}
pub fn generate_ja4h(request: &HttpHeaders<'_>) -> Ja4hFingerprint {
let method = METHOD_MAP
.get(request.method.to_uppercase().as_str())
.copied()
.unwrap_or("xx")
.to_string();
let http_version = get_http_version(request.http_version);
let mut cookie_value: Option<&str> = None;
let mut referer_value: Option<&str> = None;
let mut accept_lang_value: Option<&str> = None;
for (name, value) in request.headers.iter() {
let Ok(value_str) = value.to_str() else {
continue;
};
match name.as_str() {
"cookie" => cookie_value = Some(value_str),
"referer" => referer_value = Some(value_str),
"accept-language" => accept_lang_value = Some(value_str),
_ => {}
}
}
let has_cookie = cookie_value.is_some();
let has_referer = referer_value.is_some();
let cookie_flag = if has_cookie { "c" } else { "n" };
let referer_flag = if has_referer { "r" } else { "n" };
let accept_lang = extract_accept_lang(accept_lang_value);
let header_hash = hash_headers(request.headers);
let cookie_hash = if let Some(cookies) = cookie_value {
hash_cookies(cookies)
} else {
"000000000000".to_string()
};
let raw = format!(
"{}{}{}{}{}_{}_{}",
method, http_version, cookie_flag, referer_flag, accept_lang, header_hash, cookie_hash
);
Ja4hFingerprint {
raw,
method,
http_version,
has_cookie,
has_referer,
accept_lang,
header_hash,
cookie_hash,
}
}
fn get_http_version(version: &str) -> u8 {
match version {
"2.0" | "2" => 20,
"3.0" | "3" => 30,
"1.0" => 10,
_ => 11, }
}
fn extract_accept_lang(header: Option<&str>) -> String {
let Some(value) = header else {
return "00".to_string();
};
if value.is_empty() {
return "00".to_string();
}
let first_lang = value
.split(',')
.next()
.and_then(|s| s.split(';').next())
.and_then(|s| s.split('-').next())
.map(|s| s.trim().to_lowercase())
.unwrap_or_default();
if first_lang.len() < 2 {
return "00".to_string();
}
first_lang[..2].to_string()
}
fn hash_headers(headers: &[(HeaderName, HeaderValue)]) -> String {
let mut names: Vec<&str> = headers
.iter()
.map(|(name, _)| name.as_str())
.filter(|name| !EXCLUDED_HEADERS.contains(*name))
.collect();
if names.is_empty() {
return "000000000000".to_string();
}
names.sort();
sha256_first12(&names.join(","))
}
fn hash_cookies(cookie_header: &str) -> String {
let mut cookie_names: Vec<String> = cookie_header
.split(';')
.filter_map(|c| {
let name = c.split('=').next()?.trim().to_lowercase();
if name.is_empty() {
None
} else {
Some(name)
}
})
.collect();
if cookie_names.is_empty() {
return "000000000000".to_string();
}
cookie_names.sort();
sha256_first12(&cookie_names.join(","))
}
pub fn extract_client_fingerprint(
ja4_header: Option<&str>,
request: &HttpHeaders<'_>,
) -> ClientFingerprint {
let ja4 = parse_ja4_from_header(ja4_header);
let ja4h = generate_ja4h(request);
let mut hasher = Sha256::new();
if let Some(ref fp) = ja4 {
hasher.update(fp.raw.as_bytes());
}
hasher.update(ja4h.raw.as_bytes());
let result = hasher.finalize();
let combined_hash = hex::encode(&result[..8]);
ClientFingerprint {
ja4,
ja4h,
combined_hash,
}
}
pub fn sha256_first12(input: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
let result = hasher.finalize();
hex::encode(&result[..6]) }
pub fn is_valid_ja4(fingerprint: &str) -> bool {
parse_ja4_from_header(Some(fingerprint)).is_some()
}
pub fn is_valid_ja4h(fingerprint: &str) -> bool {
JA4H_REGEX.is_match(&fingerprint.to_lowercase())
}
pub fn fingerprints_match(fp1: Option<&str>, fp2: Option<&str>) -> bool {
match (fp1, fp2) {
(Some(a), Some(b)) => a.to_lowercase() == b.to_lowercase(),
_ => false,
}
}
pub fn matches_pattern(fingerprint: &str, pattern: &str) -> bool {
if fingerprint.is_empty() || pattern.is_empty() {
return false;
}
let escaped = regex::escape(pattern);
let regex_pattern = escaped.replace(r"\*", ".*");
match Regex::new(&format!("^(?i){}$", regex_pattern)) {
Ok(re) => re.is_match(fingerprint),
Err(_) => false,
}
}
pub fn analyze_ja4(fingerprint: &Ja4Fingerprint) -> Ja4Analysis {
let mut issues = Vec::new();
if fingerprint.tls_version < 12 {
issues.push(format!(
"Outdated TLS version: 1.{}",
fingerprint.tls_version - 10
));
}
if fingerprint.tls_version >= 13 && fingerprint.alpn == "unknown" {
issues.push("Missing ALPN with TLS 1.3 (unusual for browsers)".to_string());
}
if fingerprint.cipher_count < 5 {
issues.push(format!(
"Low cipher count: {} (typical browsers offer 10+)",
fingerprint.cipher_count
));
}
if fingerprint.ext_count < 5 {
issues.push(format!(
"Low extension count: {} (typical browsers have 10+)",
fingerprint.ext_count
));
}
let estimated_client = estimate_client_from_ja4(fingerprint);
Ja4Analysis {
fingerprint: fingerprint.clone(),
suspicious: !issues.is_empty(),
issues,
estimated_client,
}
}
fn estimate_client_from_ja4(fingerprint: &Ja4Fingerprint) -> String {
if fingerprint.tls_version >= 13 && fingerprint.alpn == "h2" && fingerprint.cipher_count >= 10 {
return "modern-browser".to_string();
}
if fingerprint.tls_version == 12 && fingerprint.alpn == "h2" {
return "browser".to_string();
}
if fingerprint.alpn == "http/1.1" && fingerprint.tls_version >= 12 {
return "api-client".to_string();
}
if fingerprint.tls_version < 12 || fingerprint.cipher_count < 5 || fingerprint.ext_count < 5 {
return "bot-or-script".to_string();
}
"unknown".to_string()
}
pub fn analyze_ja4h(fingerprint: &Ja4hFingerprint) -> Ja4hAnalysis {
let mut issues = Vec::new();
if fingerprint.accept_lang == "00" {
issues.push("No Accept-Language header (unusual for browsers)".to_string());
}
if fingerprint.http_version == 10 {
issues.push("HTTP/1.0 (very rare, possibly script)".to_string());
}
Ja4hAnalysis {
fingerprint: fingerprint.clone(),
suspicious: !issues.is_empty(),
issues,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_limit_usize(env_key: &str, default: usize, min: usize) -> usize {
std::env::var(env_key)
.ok()
.and_then(|value| value.parse::<usize>().ok())
.map(|value| value.max(min).min(default))
.unwrap_or(default)
}
fn header(name: &str, value: &str) -> (HeaderName, HeaderValue) {
let header_name = HeaderName::from_bytes(name.as_bytes()).expect("valid header name");
let header_value = HeaderValue::from_str(value).expect("valid header value");
(header_name, header_value)
}
#[test]
fn test_parse_ja4_valid_tcp_tls13() {
let result = parse_ja4_from_header(Some("t13d1516h2_8daaf6152771_e5627efa2ab1"));
assert!(result.is_some());
let fp = result.unwrap();
assert_eq!(fp.protocol, Ja4Protocol::TCP);
assert_eq!(fp.tls_version, 13);
assert_eq!(fp.sni_type, Ja4SniType::Domain);
assert_eq!(fp.cipher_count, 0x15); assert_eq!(fp.ext_count, 0x16); assert_eq!(fp.alpn, "h2");
assert_eq!(fp.cipher_hash, "8daaf6152771");
assert_eq!(fp.ext_hash, "e5627efa2ab1");
}
#[test]
fn test_parse_ja4_valid_quic_tls13() {
let result = parse_ja4_from_header(Some("q13d0a0bh3_1234567890ab_abcdef123456"));
assert!(result.is_some());
let fp = result.unwrap();
assert_eq!(fp.protocol, Ja4Protocol::QUIC);
assert_eq!(fp.tls_version, 13);
assert_eq!(fp.sni_type, Ja4SniType::Domain);
assert_eq!(fp.alpn, "h3");
}
#[test]
fn test_parse_ja4_valid_tls12_ip_sni() {
let result = parse_ja4_from_header(Some("t12i0c10h1_aabbccddeeff_112233445566"));
assert!(result.is_some());
let fp = result.unwrap();
assert_eq!(fp.tls_version, 12);
assert_eq!(fp.sni_type, Ja4SniType::IP);
assert_eq!(fp.alpn, "http/1.1");
}
#[test]
fn test_parse_ja4_valid_no_sni() {
let result = parse_ja4_from_header(Some("t130510h2_aabbccddeeff_112233445566"));
assert!(result.is_some());
let fp = result.unwrap();
assert_eq!(fp.sni_type, Ja4SniType::None);
}
#[test]
fn test_parse_ja4_invalid_format() {
assert!(parse_ja4_from_header(Some("invalid")).is_none());
assert!(parse_ja4_from_header(Some("")).is_none());
assert!(parse_ja4_from_header(None).is_none());
assert!(parse_ja4_from_header(Some("t13d1516h2_short_hash")).is_none());
}
#[test]
fn test_parse_ja4_too_long() {
let long_input = "a".repeat(200);
assert!(parse_ja4_from_header(Some(&long_input)).is_none());
}
#[test]
fn test_parse_ja4_case_insensitive() {
let result1 = parse_ja4_from_header(Some("T13D1516H2_8DAAF6152771_E5627EFA2AB1"));
let result2 = parse_ja4_from_header(Some("t13d1516h2_8daaf6152771_e5627efa2ab1"));
assert!(result1.is_some());
assert!(result2.is_some());
assert_eq!(result1.unwrap().raw, result2.unwrap().raw);
}
#[test]
fn test_generate_ja4h_basic() {
let headers = vec![
header("Accept", "text/html"),
header("User-Agent", "Mozilla/5.0"),
];
let request = HttpHeaders {
headers: &headers,
method: "GET",
http_version: "1.1",
};
let result = generate_ja4h(&request);
assert_eq!(result.method, "ge");
assert_eq!(result.http_version, 11);
assert!(!result.has_cookie);
assert!(!result.has_referer);
assert_eq!(result.accept_lang, "00");
assert_eq!(result.cookie_hash, "000000000000");
}
#[test]
fn test_generate_ja4h_with_cookie() {
let headers = vec![
header("Cookie", "session=abc123; user=test"),
header("Accept", "text/html"),
];
let request = HttpHeaders {
headers: &headers,
method: "POST",
http_version: "2.0",
};
let result = generate_ja4h(&request);
assert_eq!(result.method, "po");
assert_eq!(result.http_version, 20);
assert!(result.has_cookie);
assert!(!result.has_referer);
assert_ne!(result.cookie_hash, "000000000000");
}
#[test]
fn test_generate_ja4h_with_referer() {
let headers = vec![
header("Referer", "https://example.com"),
header("Accept", "text/html"),
];
let request = HttpHeaders {
headers: &headers,
method: "GET",
http_version: "1.1",
};
let result = generate_ja4h(&request);
assert!(!result.has_cookie);
assert!(result.has_referer);
}
#[test]
fn test_generate_ja4h_accept_language() {
let headers = vec![header("Accept-Language", "en-US,en;q=0.9,fr;q=0.8")];
let request = HttpHeaders {
headers: &headers,
method: "GET",
http_version: "1.1",
};
let result = generate_ja4h(&request);
assert_eq!(result.accept_lang, "en");
}
#[test]
fn test_generate_ja4h_french_language() {
let headers = vec![header("Accept-Language", "fr-FR,fr;q=0.9,en;q=0.8")];
let request = HttpHeaders {
headers: &headers,
method: "GET",
http_version: "1.1",
};
let result = generate_ja4h(&request);
assert_eq!(result.accept_lang, "fr");
}
#[test]
fn test_generate_ja4h_http_versions() {
for (version, expected) in [("1.0", 10), ("1.1", 11), ("2.0", 20), ("3.0", 30)] {
let headers: Vec<(HeaderName, HeaderValue)> = Vec::new();
let request = HttpHeaders {
headers: &headers,
method: "GET",
http_version: version,
};
let result = generate_ja4h(&request);
assert_eq!(
result.http_version, expected,
"Failed for version {}",
version
);
}
}
#[test]
fn test_generate_ja4h_all_methods() {
let methods = [
("GET", "ge"),
("POST", "po"),
("PUT", "pu"),
("DELETE", "de"),
("HEAD", "he"),
("OPTIONS", "op"),
("PATCH", "pa"),
("CONNECT", "co"),
("TRACE", "tr"),
];
for (method, expected) in methods {
let headers: Vec<(HeaderName, HeaderValue)> = Vec::new();
let request = HttpHeaders {
headers: &headers,
method,
http_version: "1.1",
};
let result = generate_ja4h(&request);
assert_eq!(result.method, expected, "Failed for method {}", method);
}
}
#[test]
fn test_extract_client_fingerprint_with_ja4() {
let headers = vec![header("Accept", "text/html")];
let request = HttpHeaders {
headers: &headers,
method: "GET",
http_version: "1.1",
};
let result =
extract_client_fingerprint(Some("t13d1516h2_8daaf6152771_e5627efa2ab1"), &request);
assert!(result.ja4.is_some());
assert_eq!(result.combined_hash.len(), 16);
}
#[test]
fn test_extract_client_fingerprint_without_ja4() {
let headers = vec![header("Accept", "text/html")];
let request = HttpHeaders {
headers: &headers,
method: "GET",
http_version: "1.1",
};
let result = extract_client_fingerprint(None, &request);
assert!(result.ja4.is_none());
assert_eq!(result.combined_hash.len(), 16);
}
#[test]
fn test_sha256_first12() {
let result = sha256_first12("test");
assert_eq!(result.len(), 12);
assert_eq!(result, "9f86d081884c");
}
#[test]
fn test_is_valid_ja4() {
assert!(is_valid_ja4("t13d1516h2_8daaf6152771_e5627efa2ab1"));
assert!(!is_valid_ja4("invalid"));
assert!(!is_valid_ja4(""));
}
#[test]
fn test_is_valid_ja4h() {
assert!(is_valid_ja4h("ge11cnrn_a1b2c3d4e5f6_000000000000"));
assert!(!is_valid_ja4h("invalid"));
assert!(!is_valid_ja4h(""));
}
#[test]
fn test_fingerprints_match() {
assert!(fingerprints_match(Some("ABC"), Some("abc")));
assert!(fingerprints_match(Some("abc"), Some("ABC")));
assert!(!fingerprints_match(Some("abc"), Some("def")));
assert!(!fingerprints_match(None, Some("abc")));
assert!(!fingerprints_match(Some("abc"), None));
assert!(!fingerprints_match(None, None));
}
#[test]
fn test_matches_pattern() {
assert!(matches_pattern(
"t13d1516h2_8daaf6152771_e5627efa2ab1",
"t13*"
));
assert!(!matches_pattern(
"t12d1516h2_8daaf6152771_e5627efa2ab1",
"t13*"
));
assert!(matches_pattern(
"t13d1516h2_8daaf6152771_e5627efa2ab1",
"*_8daaf6152771_*"
));
assert!(matches_pattern(
"t13d1516h2_8daaf6152771_e5627efa2ab1",
"*e5627efa2ab1"
));
}
#[test]
fn test_analyze_ja4_modern_browser() {
let fp = Ja4Fingerprint {
raw: "t13d1516h2_8daaf6152771_e5627efa2ab1".to_string(),
protocol: Ja4Protocol::TCP,
tls_version: 13,
sni_type: Ja4SniType::Domain,
cipher_count: 15,
ext_count: 16,
alpn: "h2".to_string(),
cipher_hash: "8daaf6152771".to_string(),
ext_hash: "e5627efa2ab1".to_string(),
};
let analysis = analyze_ja4(&fp);
assert!(!analysis.suspicious);
assert_eq!(analysis.estimated_client, "modern-browser");
}
#[test]
fn test_analyze_ja4_suspicious_bot() {
let fp = Ja4Fingerprint {
raw: "t100302h1_8daaf6152771_e5627efa2ab1".to_string(),
protocol: Ja4Protocol::TCP,
tls_version: 10,
sni_type: Ja4SniType::None,
cipher_count: 3,
ext_count: 2,
alpn: "http/1.1".to_string(),
cipher_hash: "8daaf6152771".to_string(),
ext_hash: "e5627efa2ab1".to_string(),
};
let analysis = analyze_ja4(&fp);
assert!(analysis.suspicious);
assert!(!analysis.issues.is_empty());
assert_eq!(analysis.estimated_client, "bot-or-script");
}
#[test]
fn test_analyze_ja4h_normal() {
let fp = Ja4hFingerprint {
raw: "ge11cren_a1b2c3d4e5f6_aabbccddeeff".to_string(),
method: "ge".to_string(),
http_version: 11,
has_cookie: true,
has_referer: true,
accept_lang: "en".to_string(),
header_hash: "a1b2c3d4e5f6".to_string(),
cookie_hash: "aabbccddeeff".to_string(),
};
let analysis = analyze_ja4h(&fp);
assert!(!analysis.suspicious);
assert!(analysis.issues.is_empty());
}
#[test]
fn test_analyze_ja4h_suspicious() {
let fp = Ja4hFingerprint {
raw: "ge10nn00_a1b2c3d4e5f6_000000000000".to_string(),
method: "ge".to_string(),
http_version: 10,
has_cookie: false,
has_referer: false,
accept_lang: "00".to_string(),
header_hash: "a1b2c3d4e5f6".to_string(),
cookie_hash: "000000000000".to_string(),
};
let analysis = analyze_ja4h(&fp);
assert!(analysis.suspicious);
assert!(analysis.issues.iter().any(|i| i.contains("HTTP/1.0")));
assert!(analysis
.issues
.iter()
.any(|i| i.contains("Accept-Language")));
}
#[test]
#[cfg_attr(not(feature = "heavy-tests"), ignore)]
fn test_ja4_parsing_performance() {
let input = "t13d1516h2_8daaf6152771_e5627efa2ab1";
let iterations = test_limit_usize("SYNAPSE_TEST_PERF_ITERATIONS", 10_000, 100);
eprintln!("test_ja4_parsing_performance: iterations={}", iterations);
let start = std::time::Instant::now();
for _ in 0..iterations {
let _ = parse_ja4_from_header(Some(input));
}
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 500,
"JA4 parsing too slow: {:?}",
elapsed
);
}
#[test]
#[cfg_attr(not(feature = "heavy-tests"), ignore)]
fn test_ja4h_generation_performance() {
let headers = vec![
header("Accept", "text/html"),
header("User-Agent", "Mozilla/5.0"),
header("Accept-Language", "en-US"),
header("Cookie", "session=abc; user=test"),
];
let request = HttpHeaders {
headers: &headers,
method: "GET",
http_version: "1.1",
};
let start = std::time::Instant::now();
let iterations = test_limit_usize("SYNAPSE_TEST_PERF_ITERATIONS", 10_000, 100);
eprintln!(
"test_ja4h_generation_performance: iterations={}",
iterations
);
for _ in 0..iterations {
let _ = generate_ja4h(&request);
}
let elapsed = start.elapsed();
#[cfg(debug_assertions)]
let max_time_ms = 1000;
#[cfg(not(debug_assertions))]
let max_time_ms = 200;
assert!(
elapsed.as_millis() < max_time_ms,
"JA4H generation too slow: {:?}",
elapsed
);
}
}