Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 85 additions & 23 deletions rust/ja4/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,29 +120,8 @@ impl HttpStats {
.ok()
.map(str::to_owned);

let mut has_cookie_header = false;
let mut has_referer_header = false;
let headers = http2
.values("http2.header.name")
.filter_map(|s| {
// Just as in HTTP/1.x, header field names are strings of ASCII
// characters that are compared in a case-insensitive fashion. However,
// header field names MUST be converted to lowercase prior to their
// encoding in HTTP/2. A request or response containing uppercase
// header field names MUST be treated as malformed
//
// Reference: https://www.rfc-editor.org/rfc/rfc7540.html#section-8.1.2
if s == "cookie" {
has_cookie_header = true;
None
} else if s == "referer" {
has_referer_header = true;
None
} else {
Some(s.to_owned())
}
})
.collect();
let (headers, has_cookie_header, has_referer_header) =
collect_http2_headers(http2.values("http2.header.name"));

// Reference: https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.5
let cookie_pairs = cookie_pairs(http2.values("http2.headers.cookie")).collect();
Expand Down Expand Up @@ -219,6 +198,42 @@ impl HttpStats {
}
}

/// Collects JA4H-relevant header names from `http2.header.name` values,
/// dropping pseudo-headers and noting the presence of `cookie` and `referer`
/// headers, which JA4H counts separately.
fn collect_http2_headers<'a>(names: impl Iterator<Item = &'a str>) -> (Vec<String>, bool, bool) {
let mut has_cookie = false;
let mut has_referer = false;
let headers = names
.filter_map(|s| match s {
// Just as in HTTP/1.x, header field names are strings of ASCII
// characters that are compared in a case-insensitive fashion. However,
// header field names MUST be converted to lowercase prior to their
// encoding in HTTP/2. A request or response containing uppercase
// header field names MUST be treated as malformed
//
// Reference: https://www.rfc-editor.org/rfc/rfc7540.html#section-8.1.2

// Pseudo-headers (`:method`, `:scheme`, ...) are not HTTP header fields
// and are excluded from the fingerprint, matching the Python and
// Wireshark implementations.
//
// Reference: https://www.rfc-editor.org/rfc/rfc9113.html#section-8.3
_ if s.starts_with(':') => None,
"cookie" => {
has_cookie = true;
None
}
"referer" => {
has_referer = true;
None
}
_ => Some(s.to_owned()),
})
.collect();
(headers, has_cookie, has_referer)
}

fn cookie_pairs<'a, I>(cookies: I) -> impl Iterator<Item = (String, Option<String>)> + 'a
where
I: IntoIterator<Item = &'a str> + 'a,
Expand Down Expand Up @@ -557,6 +572,53 @@ mod tests {
.assert_eq(&serde_json::to_string_pretty(&out).unwrap());
}

#[test]
fn test_http2_pseudo_headers_do_not_affect_ja4h() {
let (headers, has_cookie_header, has_referer_header) = collect_http2_headers(
[
":method",
":scheme",
":authority",
":path",
"sec-ch-ua",
"sec-ch-ua-mobile",
"sec-ch-ua-platform",
"upgrade-insecure-requests",
"user-agent",
"accept",
"sec-fetch-site",
"sec-fetch-mode",
"sec-fetch-user",
"sec-fetch-dest",
"accept-encoding",
"accept-language",
]
.into_iter(),
);

let stats = HttpStats {
packet: None,
req_method: HttpRequestMethod::Get,
version: HttpVersion::Http2,
has_cookie_header,
has_referer_header,
language: Some("en-US,en;q=0.9".to_owned()),
headers,
cookie_pairs: Vec::new(),
};

let out = stats.into_out(FormatFlags {
with_raw: true,
..Default::default()
});
expect![[r#"
{
"ja4h": "ge20nn12enus_60f823d07c94_000000000000_000000000000",
"ja4h_r": "ge20nn12enus_sec-ch-ua,sec-ch-ua-mobile,sec-ch-ua-platform,upgrade-insecure-requests,user-agent,accept,sec-fetch-site,sec-fetch-mode,sec-fetch-user,sec-fetch-dest,accept-encoding,accept-language__"
}"#]]
.assert_eq(&serde_json::to_string_pretty(&out).unwrap());
}

#[test]
fn test_cookie_pairs() {
// No cookies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ expression: output
ja4l_c: 30_64
ja4l_s: 5749_56
http:
- ja4h: ge20nn16enus_0f5a7a41a252_000000000000_000000000000
- ja4h: ge20nn12enus_60f823d07c94_000000000000_000000000000
- stream: 0
transport: udp
src: 2001:db8:1::1
Expand All @@ -27,4 +27,3 @@ expression: output
ja4s: q130200_1301_234ea6891581
ja4l_c: 113_64
ja4l_s: 9285_56

30 changes: 15 additions & 15 deletions rust/ja4/src/snapshots/ja4__insta@http2-with-cookies.pcapng.snap
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ expression: output
ja4l_c: 47_128
ja4l_s: 44840_117
http:
- ja4h: ge20cn23enus_641f0b6ae3f0_c7713052b7e4_348cad68b6fb
- ja4h: ge20cn17enus_949f364da66f_e43af2e8abfe_015bb0ca5596
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr22enus_265608141a12_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cn19enus_cb83bf27b7a9_c7713052b7e4_348cad68b6fb
- ja4h: ge20cn13enus_8e33b43baae9_e43af2e8abfe_015bb0ca5596
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
- ja4h: ge20cr18enus_40430d236f7c_10ff48fdaa11_ac323afc21f7
Loading