Hi.

I thought to see how claude.ai can help HAProxy and ask claude.ai to add the feature JA4H Fingerprint.

Attached the 3 patches.
What's your opinion on that?

Regards
Aleks
From 49d0d604bb010a957fe2280a7a470d7e1541f556 Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Wed, 28 Jan 2026 20:05:53 +0100
Subject: [PATCH 3/3] MINOR: doc: add Doc for JA4H

This Patch adds the Documentation of the feature JA4H Fingerprint.
The related issue is https://github.com/haproxy/haproxy/issues/2495

Signed-off-by: Aleksandar Lazic <[email protected]>
---
 doc/configuration.txt | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/doc/configuration.txt b/doc/configuration.txt
index 4de08f5043..9aff209bdd 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -26607,6 +26607,7 @@ http_auth_pass                                     string
 http_auth_type                                     string
 http_auth_user                                     string
 http_first_req                                     boolean
+ja4h                                               string
 method                                             integer
 path                                               string
 pathq                                              string
@@ -26824,6 +26825,39 @@ http_first_req : boolean
   from some requests when a request is not the first one, or to help grouping
   requests in the logs.
 
+ja4h : string
+  Returns the JA4H HTTP fingerprint of the current HTTP request. JA4H is an
+  HTTP client fingerprinting method that creates a unique identifier based on
+  HTTP request characteristics. The fingerprint format is:
+
+      {method}{version}{cookie}{referer}{hdr_count}_{al_hash}_{hdr_hash}_{ck_hash}
+
+  Where:
+    - method    : 2 lowercase characters representing the HTTP method
+                  (ge=GET, po=POST, he=HEAD, pu=PUT, de=DELETE, co=CONNECT,
+                  op=OPTIONS, tr=TRACE, xx=OTHER)
+    - version   : 2 characters for HTTP version (10=HTTP/1.0, 11=HTTP/1.1,
+                  20=HTTP/2, 30=HTTP/3)
+    - cookie    : 'c' if a Cookie header is present, 'n' otherwise
+    - referer   : 'r' if a Referer header is present, 'n' otherwise
+    - hdr_count : 2-digit zero-padded count of request headers (00-99)
+    - al_hash   : 12-character truncated hash of the Accept-Language header
+                  value (000000000000 if absent)
+    - hdr_hash  : 12-character truncated hash of sorted, comma-separated
+                  header names
+    - ck_hash   : 12-character truncated hash of sorted, comma-separated
+                  cookie names (000000000000 if no cookies)
+
+  Example output: "ge11cr06_8672ab955a25_580289e42181_da4ff4feb377"
+
+  This can be useful for HTTP client fingerprinting, bot detection, and
+  traffic analysis. The fingerprint remains consistent for requests with
+  identical HTTP characteristics.
+
+  Example:
+        http-request set-header X-JA4H %[ja4h]
+        http-request capture ja4h len 55
+
 method : integer + string
   Returns an integer value corresponding to the method in the HTTP request. For
   example, "GET" equals 1 (check sources to establish the matching). Value 9
-- 
2.43.0

From 1864b7a62c371b2b931fd7669f16fef8595a436f Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Wed, 28 Jan 2026 20:05:08 +0100
Subject: [PATCH 2/3] MINOR: reg-tests: Add tests for JA4H

This Patch adds the reg-test for the feature JA4H Fingerprint.
The related issue is https://github.com/haproxy/haproxy/issues/2495

Signed-off-by: Aleksandar Lazic <[email protected]>
---
 reg-tests/sample_fetches/ja4h.vtc | 303 ++++++++++++++++++++++++++++++
 1 file changed, 303 insertions(+)
 create mode 100644 reg-tests/sample_fetches/ja4h.vtc

diff --git a/reg-tests/sample_fetches/ja4h.vtc b/reg-tests/sample_fetches/ja4h.vtc
new file mode 100644
index 0000000000..7170db291c
--- /dev/null
+++ b/reg-tests/sample_fetches/ja4h.vtc
@@ -0,0 +1,303 @@
+varnishtest "ja4h sample fetch Test"
+
+feature ignore_unknown_macro
+
+# TEST - 1
+# Basic JA4H fingerprint with GET request, cookies, and referer
+server s1 {
+    rxreq
+    txresp
+} -start
+
+haproxy h1 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+
+    defaults
+	timeout client 30s
+	timeout server 30s
+	timeout connect 30s
+        mode http
+
+    frontend fe
+        bind "fd@${fe}"
+        http-request set-var(txn.ja4h) ja4h
+        http-response set-header x-ja4h %[var(txn.ja4h)]
+
+        default_backend be
+
+    backend be
+        server srv1 ${s1_addr}:${s1_port}
+} -start
+
+client c1 -connect ${h1_fe_sock} {
+    txreq -req GET -url "/" \
+        -hdr "Host: example.com" \
+        -hdr "User-Agent: TestClient/1.0" \
+        -hdr "Accept: text/html" \
+        -hdr "Accept-Language: en-US,en;q=0.9" \
+        -hdr "Cookie: session=abc123; user=test" \
+        -hdr "Referer: http://example.com/page";
+    rxresp
+    expect resp.status == 200
+    # JA4H format: {method}{version}{cookie}{referer}{hdr_count}_{accept_lang_hash}_{headers_hash}_{cookies_hash}
+    # ge = GET, 11 = HTTP/1.1, c = cookie present, r = referer present, 06 = 6 headers
+    expect resp.http.x-ja4h ~ "^ge11cr06_[0-9a-f]{12}_[0-9a-f]{12}_[0-9a-f]{12}$"
+} -run
+
+# TEST - 2
+# POST request without cookies or referer
+server s2 {
+    rxreq
+    txresp
+} -start
+
+haproxy h2 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+
+    defaults
+	timeout client 30s
+	timeout server 30s
+	timeout connect 30s
+        mode http
+
+    frontend fe
+        bind "fd@${fe}"
+        http-request set-var(txn.ja4h) ja4h
+        http-response set-header x-ja4h %[var(txn.ja4h)]
+
+        default_backend be
+
+    backend be
+        server srv2 ${s2_addr}:${s2_port}
+} -start
+
+client c2 -connect ${h2_fe_sock} {
+    txreq -req POST -url "/api/data" \
+        -hdr "Host: api.example.com" \
+        -hdr "Content-Type: application/json" \
+        -hdr "Content-Length: 0"
+    rxresp
+    expect resp.status == 200
+    # po = POST, 11 = HTTP/1.1, n = no cookie, n = no referer, 03 = 3 headers
+    expect resp.http.x-ja4h ~ "^po11nn03_000000000000_[0-9a-f]{12}_000000000000$"
+} -run
+
+# TEST - 3
+# HEAD request with cookie but no referer
+server s3 {
+    rxreq
+    txresp
+} -start
+
+haproxy h3 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+
+    defaults
+	timeout client 30s
+	timeout server 30s
+	timeout connect 30s
+        mode http
+
+    frontend fe
+        bind "fd@${fe}"
+        http-request set-var(txn.ja4h) ja4h
+        http-response set-header x-ja4h %[var(txn.ja4h)]
+
+        default_backend be
+
+    backend be
+        server srv3 ${s3_addr}:${s3_port}
+} -start
+
+client c3 -connect ${h3_fe_sock} {
+    txreq -req HEAD -url "/" \
+        -hdr "Host: example.com" \
+        -hdr "Cookie: token=xyz789"
+    rxresp
+    expect resp.status == 200
+    # he = HEAD, 11 = HTTP/1.1, c = cookie present, n = no referer, 02 = 2 headers
+    expect resp.http.x-ja4h ~ "^he11cn02_000000000000_[0-9a-f]{12}_[0-9a-f]{12}$"
+} -run
+
+# TEST - 4
+# PUT request with referer but no cookie
+server s4 {
+    rxreq
+    txresp
+} -start
+
+haproxy h4 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+
+    defaults
+	timeout client 30s
+	timeout server 30s
+	timeout connect 30s
+        mode http
+
+    frontend fe
+        bind "fd@${fe}"
+        http-request set-var(txn.ja4h) ja4h
+        http-response set-header x-ja4h %[var(txn.ja4h)]
+
+        default_backend be
+
+    backend be
+        server srv4 ${s4_addr}:${s4_port}
+} -start
+
+client c4 -connect ${h4_fe_sock} {
+    txreq -req PUT -url "/resource" \
+        -hdr "Host: example.com" \
+        -hdr "Referer: http://example.com/edit"; \
+        -hdr "Content-Length: 0"
+    rxresp
+    expect resp.status == 200
+    # pu = PUT, 11 = HTTP/1.1, n = no cookie, r = referer present, 03 = 3 headers
+    expect resp.http.x-ja4h ~ "^pu11nr03_000000000000_[0-9a-f]{12}_000000000000$"
+} -run
+
+# TEST - 5
+# DELETE request
+server s5 {
+    rxreq
+    txresp
+} -start
+
+haproxy h5 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+
+    defaults
+	timeout client 30s
+	timeout server 30s
+	timeout connect 30s
+        mode http
+
+    frontend fe
+        bind "fd@${fe}"
+        http-request set-var(txn.ja4h) ja4h
+        http-response set-header x-ja4h %[var(txn.ja4h)]
+
+        default_backend be
+
+    backend be
+        server srv5 ${s5_addr}:${s5_port}
+} -start
+
+client c5 -connect ${h5_fe_sock} {
+    txreq -req DELETE -url "/item/123" \
+        -hdr "Host: api.example.com"
+    rxresp
+    expect resp.status == 200
+    # de = DELETE, 11 = HTTP/1.1, n = no cookie, n = no referer, 01 = 1 header
+    expect resp.http.x-ja4h ~ "^de11nn01_000000000000_[0-9a-f]{12}_000000000000$"
+} -run
+
+# TEST - 6
+# OPTIONS request
+server s6 {
+    rxreq
+    txresp
+} -start
+
+haproxy h6 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+
+    defaults
+	timeout client 30s
+	timeout server 30s
+	timeout connect 30s
+        mode http
+
+    frontend fe
+        bind "fd@${fe}"
+        http-request set-var(txn.ja4h) ja4h
+        http-response set-header x-ja4h %[var(txn.ja4h)]
+
+        default_backend be
+
+    backend be
+        server srv6 ${s6_addr}:${s6_port}
+} -start
+
+client c6 -connect ${h6_fe_sock} {
+    txreq -req OPTIONS -url "*" \
+        -hdr "Host: example.com"
+    rxresp
+    expect resp.status == 200
+    # op = OPTIONS, 11 = HTTP/1.1, n = no cookie, n = no referer, 01 = 1 header
+    expect resp.http.x-ja4h ~ "^op11nn01_000000000000_[0-9a-f]{12}_000000000000$"
+} -run
+
+# TEST - 7
+# Verify consistent hashing - same request should produce same fingerprint
+server s7 {
+    rxreq
+    txresp
+    rxreq
+    txresp
+} -start
+
+haproxy h7 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+
+    defaults
+	timeout client 30s
+	timeout server 30s
+	timeout connect 30s
+        mode http
+
+    frontend fe
+        bind "fd@${fe}"
+        http-request set-var(txn.ja4h) ja4h
+        http-response set-header x-ja4h %[var(txn.ja4h)]
+
+        default_backend be
+
+    backend be
+        server srv7 ${s7_addr}:${s7_port}
+} -start
+
+client c7 -connect ${h7_fe_sock} {
+    txreq -req GET -url "/" \
+        -hdr "Host: test.example.com" \
+        -hdr "Accept: */*" \
+        -hdr "Accept-Language: fr-FR" \
+        -hdr "Cookie: id=12345"
+    rxresp
+    expect resp.status == 200
+    expect resp.http.x-ja4h ~ "^ge11cn04_[0-9a-f]{12}_[0-9a-f]{12}_[0-9a-f]{12}$"
+} -run
+
+client c7b -connect ${h7_fe_sock} {
+    # Same request again should produce identical fingerprint
+    txreq -req GET -url "/" \
+        -hdr "Host: test.example.com" \
+        -hdr "Accept: */*" \
+        -hdr "Accept-Language: fr-FR" \
+        -hdr "Cookie: id=12345"
+    rxresp
+    expect resp.status == 200
+    expect resp.http.x-ja4h ~ "^ge11cn04_[0-9a-f]{12}_[0-9a-f]{12}_[0-9a-f]{12}$"
+} -run
-- 
2.43.0

From a0c8b9b02049ac2c117c91b79506f0967c21aded Mon Sep 17 00:00:00 2001
From: Aleksandar Lazic <[email protected]>
Date: Wed, 28 Jan 2026 13:21:08 +0100
Subject: [PATCH 1/3] MINOR: sample: Add JA4H Sample fetch

This Patch adds the feature to generate JA4H Fingerprint.
The related issue is https://github.com/haproxy/haproxy/issues/2495

Signed-off-by: Aleksandar Lazic <[email protected]>
---
 src/http_fetch.c | 289 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 289 insertions(+)

diff --git a/src/http_fetch.c b/src/http_fetch.c
index b8b6f616c0..da59d2de98 100644
--- a/src/http_fetch.c
+++ b/src/http_fetch.c
@@ -39,6 +39,7 @@
 #include <haproxy/log.h>
 #include <haproxy/tools.h>
 #include <haproxy/version.h>
+#include <haproxy/xxhash.h>
 
 
 /* this struct is used between calls to smp_fetch_hdr() or smp_fetch_cookie() */
@@ -2196,6 +2197,293 @@ static int smp_fetch_url32_src(const struct arg *args, struct sample *smp, const
 	return 1;
 }
 
+/************************************************************************/
+/*                          JA4H HTTP Fingerprint                       */
+/************************************************************************/
+
+/* Comparison function for sorting strings (for JA4H) */
+static int ja4h_strcmp(const void *a, const void *b)
+{
+	const struct ist *s1 = (const struct ist *)a;
+	const struct ist *s2 = (const struct ist *)b;
+	size_t min_len = s1->len < s2->len ? s1->len : s2->len;
+	int cmp = strncasecmp(s1->ptr, s2->ptr, min_len);
+	if (cmp != 0)
+		return cmp;
+	return (int)s1->len - (int)s2->len;
+}
+
+/* Convert 6 bytes to 12 hex characters */
+static void ja4h_hash_to_hex(uint64_t hash, char *out)
+{
+	static const char hex[] = "0123456789abcdef";
+	int i;
+	/* Use first 6 bytes (12 hex chars) of the hash */
+	for (i = 0; i < 6; i++) {
+		out[i * 2] = hex[(hash >> (56 - i * 8)) >> 4 & 0xf];
+		out[i * 2 + 1] = hex[(hash >> (56 - i * 8)) & 0xf];
+	}
+}
+
+/* JA4H HTTP Fingerprint
+ * Format: {method}{version}{cookie}{referer}{num_headers}_{accept-lang-hash}_{headers-hash}_{cookies-hash}
+ * Example: ge11cr12_a2b1c3d4e5f6_1a2b3c4d5e6f_a1b2c3d4e5f6
+ *
+ * - method: 2 lowercase chars (ge=GET, po=POST, he=HEAD, pu=PUT, de=DELETE, co=CONNECT, op=OPTIONS, tr=TRACE, xx=OTHER)
+ * - version: 2 chars (10=HTTP/1.0, 11=HTTP/1.1, 20=HTTP/2, 30=HTTP/3)
+ * - cookie: 'c' if Cookie header present, 'n' otherwise
+ * - referer: 'r' if Referer header present, 'n' otherwise
+ * - num_headers: 2-digit zero-padded count of headers (00-99)
+ * - accept-lang-hash: 12-char truncated XXH3 hash of Accept-Language value (or 000000000000 if absent)
+ * - headers-hash: 12-char truncated XXH3 hash of sorted, comma-separated header names
+ * - cookies-hash: 12-char truncated XXH3 hash of sorted, comma-separated cookie names (or 000000000000 if no cookies)
+ */
+static int smp_fetch_ja4h(const struct arg *args, struct sample *smp, const char *kw, void *private)
+{
+	struct channel *chn = SMP_REQ_CHN(smp);
+	struct htx *htx = smp_prefetch_htx(smp, chn, NULL, 1);
+	struct http_txn *txn;
+	struct htx_sl *sl;
+	struct buffer *result;
+	struct http_hdr_ctx ctx;
+	int32_t pos;
+	char method_str[3] = "xx";
+	char version_str[3] = "00";
+	char cookie_flag = 'n';
+	char referer_flag = 'n';
+	int hdr_count = 0;
+	uint64_t accept_lang_hash = 0;
+	uint64_t headers_hash = 0;
+	uint64_t cookies_hash = 0;
+	char accept_lang_hex[13] = "000000000000";
+	char headers_hex[13] = "000000000000";
+	char cookies_hex[13] = "000000000000";
+	struct ist *hdr_names = NULL;
+	struct ist *cookie_names = NULL;
+	int hdr_names_count = 0;
+	int cookie_names_count = 0;
+	int hdr_names_alloc = 64;
+	int cookie_names_alloc = 32;
+	struct buffer *hdr_buf = NULL;
+	struct buffer *cookie_buf = NULL;
+	struct ist *tmp = NULL;
+	struct ist vsn;
+	int i;
+
+	if (!htx)
+		return 0;
+
+	txn = (smp->strm ? smp->strm->txn : NULL);
+	if (!txn)
+		return 0;
+
+	sl = http_get_stline(htx);
+	if (!sl)
+		return 0;
+
+	/* Extract method - 2 lowercase chars */
+	switch (txn->meth) {
+	case HTTP_METH_GET:     method_str[0] = 'g'; method_str[1] = 'e'; break;
+	case HTTP_METH_POST:    method_str[0] = 'p'; method_str[1] = 'o'; break;
+	case HTTP_METH_HEAD:    method_str[0] = 'h'; method_str[1] = 'e'; break;
+	case HTTP_METH_PUT:     method_str[0] = 'p'; method_str[1] = 'u'; break;
+	case HTTP_METH_DELETE:  method_str[0] = 'd'; method_str[1] = 'e'; break;
+	case HTTP_METH_CONNECT: method_str[0] = 'c'; method_str[1] = 'o'; break;
+	case HTTP_METH_OPTIONS: method_str[0] = 'o'; method_str[1] = 'p'; break;
+	case HTTP_METH_TRACE:   method_str[0] = 't'; method_str[1] = 'r'; break;
+	default:                method_str[0] = 'x'; method_str[1] = 'x'; break;
+	}
+
+	/* Extract HTTP version */
+	if (sl->flags & HTX_SL_F_VER_11) {
+		version_str[0] = '1';
+		version_str[1] = '1';
+	} else {
+		/* Check version string for HTTP/1.0, HTTP/2, HTTP/3 */
+		vsn = htx_sl_req_vsn(sl);
+		if (vsn.len >= 8 && vsn.ptr[5] == '1' && vsn.ptr[7] == '0') {
+			version_str[0] = '1';
+			version_str[1] = '0';
+		} else if (vsn.len >= 6 && vsn.ptr[5] == '2') {
+			version_str[0] = '2';
+			version_str[1] = '0';
+		} else if (vsn.len >= 6 && vsn.ptr[5] == '3') {
+			version_str[0] = '3';
+			version_str[1] = '0';
+		} else {
+			version_str[0] = '1';
+			version_str[1] = '0';
+		}
+	}
+
+	/* Allocate arrays for header and cookie names */
+	hdr_names = malloc(hdr_names_alloc * sizeof(*hdr_names));
+	cookie_names = malloc(cookie_names_alloc * sizeof(*cookie_names));
+	if (!hdr_names || !cookie_names)
+		goto error;
+
+	/* Iterate through all headers */
+	for (pos = htx_get_first(htx); pos != -1; pos = htx_get_next(htx, pos)) {
+		struct htx_blk *blk = htx_get_blk(htx, pos);
+		enum htx_blk_type type = htx_get_blk_type(blk);
+		struct ist name, value;
+
+		if (type == HTX_BLK_EOH)
+			break;
+		if (type != HTX_BLK_HDR)
+			continue;
+
+		name = htx_get_blk_name(htx, blk);
+		value = htx_get_blk_value(htx, blk);
+
+		/* Check for Cookie header */
+		if (isteqi(name, ist("Cookie"))) {
+			cookie_flag = 'c';
+		}
+
+		/* Check for Referer header */
+		if (isteqi(name, ist("Referer"))) {
+			referer_flag = 'r';
+		}
+
+		/* Check for Accept-Language header */
+		if (isteqi(name, ist("Accept-Language"))) {
+			accept_lang_hash = XXH3(value.ptr, value.len, 0);
+			ja4h_hash_to_hex(accept_lang_hash, accept_lang_hex);
+		}
+
+		/* Add header name to list */
+		if (hdr_names_count >= hdr_names_alloc) {
+			hdr_names_alloc *= 2;
+			tmp = realloc(hdr_names, hdr_names_alloc * sizeof(*hdr_names));
+			if (!tmp)
+				goto error;
+			hdr_names = tmp;
+		}
+		hdr_names[hdr_names_count++] = name;
+		hdr_count++;
+	}
+
+	/* Sort header names alphabetically (case-insensitive) */
+	if (hdr_names_count > 0)
+		qsort(hdr_names, hdr_names_count, sizeof(*hdr_names), ja4h_strcmp);
+
+	/* Build comma-separated sorted header names string and hash it */
+	hdr_buf = get_trash_chunk();
+	if (!hdr_buf)
+		goto error;
+
+	for (i = 0; i < hdr_names_count; i++) {
+		if (i > 0)
+			chunk_appendf(hdr_buf, ",");
+		chunk_istcat(hdr_buf, hdr_names[i]);
+	}
+
+	if (hdr_buf->data > 0) {
+		headers_hash = XXH3(hdr_buf->area, hdr_buf->data, 0);
+		ja4h_hash_to_hex(headers_hash, headers_hex);
+	}
+
+	/* Extract and hash cookie names if Cookie header exists */
+	if (cookie_flag == 'c') {
+		ctx.blk = NULL;
+		while (http_find_header(htx, ist("Cookie"), &ctx, 0)) {
+			char *ptr = ctx.value.ptr;
+			char *end = ptr + ctx.value.len;
+
+			while (ptr < end) {
+				char *name_start, *name_end;
+
+				/* Skip whitespace */
+				while (ptr < end && (*ptr == ' ' || *ptr == '\t'))
+					ptr++;
+
+				if (ptr >= end)
+					break;
+
+				name_start = ptr;
+
+				/* Find end of cookie name (until '=' or ';') */
+				while (ptr < end && *ptr != '=' && *ptr != ';')
+					ptr++;
+
+				name_end = ptr;
+
+				/* Skip the value */
+				if (ptr < end && *ptr == '=') {
+					ptr++;
+					while (ptr < end && *ptr != ';')
+						ptr++;
+				}
+
+				/* Skip semicolon and whitespace */
+				if (ptr < end && *ptr == ';')
+					ptr++;
+
+				/* Add cookie name if valid */
+				if (name_end > name_start) {
+					if (cookie_names_count >= cookie_names_alloc) {
+						cookie_names_alloc *= 2;
+						tmp = realloc(cookie_names, cookie_names_alloc * sizeof(*cookie_names));
+						if (!tmp)
+							goto error;
+						cookie_names = tmp;
+					}
+					cookie_names[cookie_names_count].ptr = name_start;
+					cookie_names[cookie_names_count].len = name_end - name_start;
+					cookie_names_count++;
+				}
+			}
+		}
+
+		/* Sort cookie names alphabetically */
+		if (cookie_names_count > 0)
+			qsort(cookie_names, cookie_names_count, sizeof(*cookie_names), ja4h_strcmp);
+
+		/* Build comma-separated sorted cookie names string and hash it */
+		cookie_buf = get_trash_chunk();
+		if (!cookie_buf)
+			goto error;
+
+		for (i = 0; i < cookie_names_count; i++) {
+			if (i > 0)
+				chunk_appendf(cookie_buf, ",");
+			chunk_istcat(cookie_buf, cookie_names[i]);
+		}
+
+		if (cookie_buf->data > 0) {
+			cookies_hash = XXH3(cookie_buf->area, cookie_buf->data, 0);
+			ja4h_hash_to_hex(cookies_hash, cookies_hex);
+		}
+	}
+
+	/* Build final JA4H string */
+	result = get_trash_chunk();
+	if (!result)
+		goto error;
+
+	/* Cap header count at 99 for the 2-digit format */
+	if (hdr_count > 99)
+		hdr_count = 99;
+
+	chunk_appendf(result, "%s%s%c%c%02d_%s_%s_%s",
+		      method_str, version_str, cookie_flag, referer_flag, hdr_count,
+		      accept_lang_hex, headers_hex, cookies_hex);
+
+	free(hdr_names);
+	free(cookie_names);
+
+	smp->data.type = SMP_T_STR;
+	smp->data.u.str = *result;
+	smp->flags = SMP_F_VOL_HDR;
+	return 1;
+
+error:
+	free(hdr_names);
+	free(cookie_names);
+	return 0;
+}
+
 /************************************************************************/
 /*                          Other utility functions                     */
 /************************************************************************/
@@ -2291,6 +2579,7 @@ static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, {
 	{ "http_auth",          smp_fetch_http_auth,          ARG1(1,USR),      NULL,    SMP_T_BOOL, SMP_USE_HRQHV },
 	{ "http_auth_group",    smp_fetch_http_auth_grp,      ARG1(1,USR),      NULL,    SMP_T_STR,  SMP_USE_HRQHV },
 	{ "http_first_req",     smp_fetch_http_first_req,     0,                NULL,    SMP_T_BOOL, SMP_USE_HRQHP },
+	{ "ja4h",               smp_fetch_ja4h,               0,                NULL,    SMP_T_STR,  SMP_USE_HRQHV },
 	{ "method",             smp_fetch_meth,               0,                NULL,    SMP_T_METH, SMP_USE_HRQHP },
 	{ "path",               smp_fetch_path,               0,                NULL,    SMP_T_STR,  SMP_USE_HRQHV },
 	{ "pathq",              smp_fetch_path,               0,                NULL,    SMP_T_STR,  SMP_USE_HRQHV },
-- 
2.43.0

Reply via email to