This is an automated email from the ASF dual-hosted git repository.

bneradt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new de11357dbc Fix preserve logic to check for any header in fingerprint 
group (#12811)
de11357dbc is described below

commit de11357dbc2129749bdc375cd3e37340dcdb0b8e
Author: Brian Neradt <[email protected]>
AuthorDate: Wed Jan 28 20:26:28 2026 -0600

    Fix preserve logic to check for any header in fingerprint group (#12811)
    
    Problem:
    When --preserve was enabled and a request passed through multiple proxies,
    each header was checked individually. This could result in mismatched
    fingerprint data - for example, x-ja3-raw being added by a downstream proxy
    while x-ja3-sig was preserved from an upstream proxy.
    
    Solution:
    The JA3 and JA4 fingerprint plugins now check if ANY header in a fingerprint
    group exists before adding headers. If any header in the group exists, ALL
    headers in that group are skipped.
    
    Changes:
    - ja3_fingerprint: Added group-level checks for JA3 headers
    - ja4_fingerprint: Added --preserve option and group-level check for JA4 
headers
    - Updated tests to verify group-level preserve behavior
---
 plugins/experimental/ja4_fingerprint/plugin.cc     | 66 ++++++++++++++++++--
 plugins/ja3_fingerprint/ja3_fingerprint.cc         | 50 ++++++++++-----
 .../ja3_fingerprint/ja3_fingerprint.test.py        | 16 +++--
 .../ja3_fingerprint_global.replay.yaml             |  5 +-
 .../ja3_fingerprint/modify-incoming-proxy.gold     |  3 +-
 ...ng-proxy.gold => modify-sent-proxy-global.gold} |  3 +-
 ...ent-proxy.gold => modify-sent-proxy-remap.gold} |  1 +
 .../ja4_fingerprint/ja4_fingerprint.replay.yaml    | 72 +++++++++++++++++++++-
 .../ja4_fingerprint/ja4_fingerprint.test.py        | 53 +++++++++++++++-
 9 files changed, 235 insertions(+), 34 deletions(-)

diff --git a/plugins/experimental/ja4_fingerprint/plugin.cc 
b/plugins/experimental/ja4_fingerprint/plugin.cc
index ada54ed1c9..16419d4566 100644
--- a/plugins/experimental/ja4_fingerprint/plugin.cc
+++ b/plugins/experimental/ja4_fingerprint/plugin.cc
@@ -31,11 +31,13 @@
 #include <openssl/ssl.h>
 
 #include <arpa/inet.h>
+#include <getopt.h>
 #include <netinet/in.h>
 
 #include <cstddef>
 #include <cstdint>
 #include <cstdio>
+#include <cstring>
 #include <memory>
 #include <string>
 #include <string_view>
@@ -79,8 +81,37 @@ constexpr int          SSL_SUCCESS{1};
 
 DbgCtl dbg_ctl{PLUGIN_NAME};
 
+int global_preserve_enabled{0};
+
 } // end anonymous namespace
 
+static bool
+read_config_option(int argc, char const *argv[], int &preserve)
+{
+  const struct option longopts[] = {
+    {"preserve", no_argument, &preserve, 1},
+    {nullptr,    0,           nullptr,   0}
+  };
+
+  optind = 0;
+  int opt{0};
+  while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "", 
longopts, nullptr)) >= 0) {
+    switch (opt) {
+    case '?':
+      Dbg(dbg_ctl, "Unrecognized command argument.");
+    case 0:
+    case -1:
+      break;
+    default:
+      Dbg(dbg_ctl, "Unexpected options error.");
+      return false;
+    }
+  }
+
+  Dbg(dbg_ctl, "JA4 preserve is %s", (preserve == 1) ? "enabled" : "disabled");
+  return true;
+}
+
 static int *
 get_user_arg_index()
 {
@@ -112,12 +143,16 @@ make_word(unsigned char lowbyte, unsigned char highbyte)
 }
 
 void
-TSPluginInit(int /* argc ATS_UNUSED */, char const ** /* argv ATS_UNUSED */)
+TSPluginInit(int argc, char const **argv)
 {
   if (!register_plugin()) {
     TSError("[%s] Failed to register.", PLUGIN_NAME);
     return;
   }
+  if (!read_config_option(argc, argv, global_preserve_enabled)) {
+    TSError("[%s] Failed to parse options.", PLUGIN_NAME);
+    return;
+  }
   reserve_user_arg();
   if (!create_log_file()) {
     TSError("[%s] Failed to create log.", PLUGIN_NAME);
@@ -334,12 +369,36 @@ handle_read_request_hdr(TSCont cont, TSEvent event, void 
*edata)
   return TS_SUCCESS;
 }
 
+// Check if a header field exists in the request.
+static bool
+header_exists(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len)
+{
+  TSMLoc loc = TSMimeHdrFieldFind(bufp, hdr_loc, field, field_len);
+  if (loc != TS_NULL_MLOC) {
+    TSHandleMLocRelease(bufp, hdr_loc, loc);
+    return true;
+  }
+  return false;
+}
+
 void
 append_JA4_headers(TSCont /* cont ATS_UNUSED */, TSHttpTxn txnp, std::string 
const *fingerprint)
 {
   TSMBuffer bufp;
   TSMLoc    hdr_loc;
-  if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) {
+  if (TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) {
+    Dbg(dbg_ctl, "Failed to get headers.");
+    return;
+  }
+
+  // When preserve is enabled, check if ANY JA4 header exists. If so, skip
+  // adding ALL JA4 headers to avoid mismatched fingerprint data when requests
+  // traverse multiple proxies.
+  bool const ja4_header_exists = header_exists(bufp, hdr_loc, "ja4", 3) ||
+                                 header_exists(bufp, hdr_loc, 
JA4_VIA_HEADER.data(), static_cast<int>(JA4_VIA_HEADER.length()));
+  bool const skip_ja4_headers = global_preserve_enabled && ja4_header_exists;
+
+  if (!skip_ja4_headers) {
     append_to_field(bufp, hdr_loc, "ja4", 3, fingerprint->data(), 
fingerprint->size());
 
     TSMgmtString proxy_name = nullptr;
@@ -351,9 +410,6 @@ append_JA4_headers(TSCont /* cont ATS_UNUSED */, TSHttpTxn 
txnp, std::string con
     append_to_field(bufp, hdr_loc, JA4_VIA_HEADER.data(), 
static_cast<int>(JA4_VIA_HEADER.length()), proxy_name,
                     static_cast<int>(std::strlen(proxy_name)));
     TSfree(proxy_name);
-
-  } else {
-    Dbg(dbg_ctl, "Failed to get headers.");
   }
 
   TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
diff --git a/plugins/ja3_fingerprint/ja3_fingerprint.cc 
b/plugins/ja3_fingerprint/ja3_fingerprint.cc
index 5a9bb2ce76..4f9f3028cf 100644
--- a/plugins/ja3_fingerprint/ja3_fingerprint.cc
+++ b/plugins/ja3_fingerprint/ja3_fingerprint.cc
@@ -194,6 +194,18 @@ append_to_field(TSMBuffer bufp, TSMLoc hdr_loc, const char 
*field, int field_len
   TSHandleMLocRelease(bufp, hdr_loc, target);
 }
 
+// Check if a header field exists in the request.
+static bool
+header_exists(TSMBuffer bufp, TSMLoc hdr_loc, char const *field, int field_len)
+{
+  TSMLoc loc = TSMimeHdrFieldFind(bufp, hdr_loc, field, field_len);
+  if (loc != TS_NULL_MLOC) {
+    TSHandleMLocRelease(bufp, hdr_loc, loc);
+    return true;
+  }
+  return false;
+}
+
 static ja3_data *
 create_ja3_data(TSVConn const ssl_vc)
 {
@@ -258,23 +270,31 @@ modify_ja3_headers(TSCont contp, TSHttpTxn txnp, ja3_data 
const *ja3_vconn_data)
     TSAssert(TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &bufp, &hdr_loc));
   }
 
-  TSMgmtString proxy_name = nullptr;
-  if (TS_SUCCESS != TSMgmtStringGet("proxy.config.proxy_name", &proxy_name)) {
-    TSError("[%s] Failed to get proxy name for %s, set 
'proxy.config.proxy_name' in records.config", PLUGIN_NAME,
-            JA3_VIA_HEADER.data());
-    proxy_name = TSstrdup("unknown");
-  }
-  append_to_field(bufp, hdr_loc, JA3_VIA_HEADER.data(), 
static_cast<int>(JA3_VIA_HEADER.length()), proxy_name,
-                  static_cast<int>(std::strlen(proxy_name)), preserve_flag);
-  TSfree(proxy_name);
+  // When preserve is enabled, check if ANY JA3 header exists. If so, skip
+  // adding ALL JA3 headers to avoid mismatched fingerprint data when requests
+  // traverse multiple proxies.
+  bool const ja3_header_exists = header_exists(bufp, hdr_loc, 
JA3_VIA_HEADER.data(), static_cast<int>(JA3_VIA_HEADER.length())) ||
+                                 header_exists(bufp, hdr_loc, "x-ja3-sig", 9) 
|| header_exists(bufp, hdr_loc, "x-ja3-raw", 9);
+  bool const skip_ja3_headers = preserve_flag && ja3_header_exists;
+
+  if (!skip_ja3_headers) {
+    TSMgmtString proxy_name = nullptr;
+    if (TS_SUCCESS != TSMgmtStringGet("proxy.config.proxy_name", &proxy_name)) 
{
+      TSError("[%s] Failed to get proxy name for %s, set 
'proxy.config.proxy_name' in records.config", PLUGIN_NAME,
+              JA3_VIA_HEADER.data());
+      proxy_name = TSstrdup("unknown");
+    }
+    append_to_field(bufp, hdr_loc, JA3_VIA_HEADER.data(), 
static_cast<int>(JA3_VIA_HEADER.length()), proxy_name,
+                    static_cast<int>(std::strlen(proxy_name)), false);
 
-  // Add JA3 md5 fingerprints
-  append_to_field(bufp, hdr_loc, "x-ja3-sig", 9, ja3_vconn_data->md5_string, 
32, preserve_flag);
+    // Add JA3 md5 fingerprints.
+    append_to_field(bufp, hdr_loc, "x-ja3-sig", 9, ja3_vconn_data->md5_string, 
32, false);
 
-  // If raw string is configured, added JA3 raw string to header as well
-  if (raw_flag) {
-    append_to_field(bufp, hdr_loc, "x-ja3-raw", 9, 
ja3_vconn_data->ja3_string.data(), ja3_vconn_data->ja3_string.size(),
-                    preserve_flag);
+    // If raw string is configured, add JA3 raw string to header as well.
+    if (raw_flag) {
+      append_to_field(bufp, hdr_loc, "x-ja3-raw", 9, 
ja3_vconn_data->ja3_string.data(), ja3_vconn_data->ja3_string.size(), false);
+    }
+    TSfree(proxy_name);
   }
   TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
 
diff --git 
a/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint.test.py 
b/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint.test.py
index e94c82196a..89eb57ee88 100644
--- a/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint.test.py
+++ b/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint.test.py
@@ -81,13 +81,15 @@ class JA3FingerprintTest:
         self._server.Streams.All += 
Testers.ContainsExpression("https-request", "Verify the HTTPS request was 
received.")
         self._server.Streams.All += 
Testers.ContainsExpression("http2-request", "Verify the HTTP/2 request was 
received.")
         if not self._test_remap:
-            # Verify --preserve worked.
-            self._server.Streams.All += Testers.ContainsExpression("x-ja3-raw: 
.*,", "Verify the new raw header was added.")
+            # The first request has no existing JA3 headers, so headers are 
added.
             self._server.Streams.All += Testers.ContainsExpression(
-                "x-ja3-raw: first-signature", "Verify the already-existing raw 
header was preserved.")
-            self._server.Streams.All += Testers.ExcludesExpression(
-                "x-ja3-raw: first-signature;", "Verify no extra values were 
added due to preserve.")
+                "x-ja3-raw: .*,", "Verify the new raw header was added.", 
reflags=re.IGNORECASE)
             self._server.Streams.All += Testers.ContainsExpression("x-ja3-via: 
test.proxy.com", "The x-ja3-via string was added.")
+            # The second request has existing JA3 headers. With --preserve,
+            # no new JA3 headers are added (including x-ja3-sig). The replay
+            # file verifies x-ja3-sig is absent for the http2 transaction.
+            self._server.Streams.All += Testers.ContainsExpression(
+                "x-ja3-raw: first-signature", "Verify the already-existing raw 
header was preserved.", reflags=re.IGNORECASE)
 
     def _configure_trafficserver(self) -> None:
         """Configure Traffic Server to be used in the test."""
@@ -189,8 +191,10 @@ class JA3FingerprintTest:
 
         if self._modify_incoming:
             p.Streams.All += "modify-incoming-proxy.gold"
+        elif self._test_remap:
+            p.Streams.All += "modify-sent-proxy-remap.gold"
         else:
-            p.Streams.All += "modify-sent-proxy.gold"
+            p.Streams.All += "modify-sent-proxy-global.gold"
 
 
 JA3FingerprintTest(test_remap=False, modify_incoming=False)
diff --git 
a/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_global.replay.yaml
 
b/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_global.replay.yaml
index bd42ddd9cc..f0c1452871 100644
--- 
a/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_global.replay.yaml
+++ 
b/tests/gold_tests/pluginTest/ja3_fingerprint/ja3_fingerprint_global.replay.yaml
@@ -86,13 +86,14 @@ sessions:
       content:
         size: 399
 
+    # With --preserve and any JA3 header present, no new headers are added.
     proxy-request:
       headers:
         fields:
         - [ x-request, { value: 'http2-request', as: equal } ]
         - [ x-ja3-via, { value: 'first-via', as: equal } ]
-        - [ X-JA3-Sig, { as: present } ]
-        - [ X-JA3-Raw, { as: present } ]
+        - [ x-ja3-raw, { value: 'first-signature', as: equal } ]
+        - [ x-ja3-sig, { as: absent } ]
 
     server-response:
       headers:
diff --git 
a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold 
b/tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold
index a8cd3a41bb..480b97c39d 100644
--- a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold
+++ b/tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold
@@ -2,5 +2,6 @@
 -- State Machine Id``
 POST /some/path/http2``
 ``
-x-ja3-sig: ``
+x-ja3-raw: first-signature``
+x-ja3-via: first-via``
 ``
diff --git 
a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold 
b/tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-global.gold
similarity index 65%
copy from tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold
copy to 
tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-global.gold
index a8cd3a41bb..480b97c39d 100644
--- a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-incoming-proxy.gold
+++ b/tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-global.gold
@@ -2,5 +2,6 @@
 -- State Machine Id``
 POST /some/path/http2``
 ``
-x-ja3-sig: ``
+x-ja3-raw: first-signature``
+x-ja3-via: first-via``
 ``
diff --git a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy.gold 
b/tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-remap.gold
similarity index 89%
rename from tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy.gold
rename to 
tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-remap.gold
index a8cd3a41bb..271809823d 100644
--- a/tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy.gold
+++ b/tests/gold_tests/pluginTest/ja3_fingerprint/modify-sent-proxy-remap.gold
@@ -3,4 +3,5 @@
 POST /some/path/http2``
 ``
 x-ja3-sig: ``
+x-ja3-via: ``
 ``
diff --git 
a/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.replay.yaml 
b/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.replay.yaml
index f22de8c76d..5c0309f15a 100644
--- a/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.replay.yaml
+++ b/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.replay.yaml
@@ -18,6 +18,8 @@ meta:
   version: "1.0"
 
 sessions:
+
+# Session 1: No pre-existing JA4 headers - new headers should be added.
 - protocol:
   - name: http
     version: 1
@@ -33,11 +35,12 @@ sessions:
         fields:
         - [ Connection, keep-alive ]
         - [ Content-Length, 0 ]
+        - [ uuid, no-existing-headers ]
 
     proxy-request:
       headers:
         fields:
-        - [ ja4, { as: contains } ]
+        - [ ja4, { as: present } ]
         - [ x-ja4-via, { value: 'test.proxy.com', as: equal } ]
 
     server-response:
@@ -46,3 +49,70 @@ sessions:
       content:
         encoding: plain
         data: Yay!
+
+# Session 2: Pre-existing JA4 headers - with preserve, no new headers added.
+- protocol:
+  - name: http
+    version: 1
+  - name: tcp
+  - name: ip
+
+  transactions:
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /resource-with-headers
+      headers:
+        fields:
+        - [ Connection, keep-alive ]
+        - [ Content-Length, 0 ]
+        - [ uuid, existing-ja4-headers ]
+        - [ ja4, upstream-fingerprint ]
+        - [ x-ja4-via, upstream.proxy.com ]
+
+    # With --preserve and existing JA4 headers, no new headers should be added.
+    proxy-request:
+      headers:
+        fields:
+        - [ ja4, { value: 'upstream-fingerprint', as: equal } ]
+        - [ x-ja4-via, { value: 'upstream.proxy.com', as: equal } ]
+
+    server-response:
+      status: 200
+      reason: OK
+      content:
+        encoding: plain
+        data: Preserved!
+
+# Session 3: Only x-ja4-via exists - should still trigger preserve for all.
+- protocol:
+  - name: http
+    version: 1
+  - name: tcp
+  - name: ip
+
+  transactions:
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /resource-via-only
+      headers:
+        fields:
+        - [ Connection, keep-alive ]
+        - [ Content-Length, 0 ]
+        - [ uuid, existing-via-only ]
+        - [ x-ja4-via, upstream.proxy.com ]
+
+    # With --preserve and only x-ja4-via present, no JA4 headers should be 
added.
+    proxy-request:
+      headers:
+        fields:
+        - [ ja4, { as: absent } ]
+        - [ x-ja4-via, { value: 'upstream.proxy.com', as: equal } ]
+
+    server-response:
+      status: 200
+      reason: OK
+      content:
+        encoding: plain
+        data: Via only!
diff --git 
a/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.test.py 
b/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.test.py
index ea241579df..71de43cd51 100644
--- a/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.test.py
+++ b/tests/gold_tests/pluginTest/ja4_fingerprint/ja4_fingerprint.test.py
@@ -38,13 +38,15 @@ class TestJA4Fingerprint:
     server_counter: int = 0
     ts_counter: int = 0
 
-    def __init__(self, name: str, /, autorun: bool) -> None:
+    def __init__(self, name: str, /, autorun: bool, use_preserve: bool = 
False) -> None:
         '''Initialize the test.
 
         :param name: The name of the test.
+        :param use_preserve: Whether to use the --preserve flag.
         '''
         self.name = name
         self.autorun = autorun
+        self.use_preserve = use_preserve
 
     def _init_run(self) -> 'TestRun':
         '''Initialize processes for the test run.'''
@@ -127,7 +129,10 @@ class TestJA4Fingerprint:
 
         ts.Disk.ssl_multicert_config.AddLine(f'dest_ip=* 
ssl_cert_name=server.pem ssl_key_name=server.key')
 
-        ts.Disk.plugin_config.AddLine(f'ja4_fingerprint.so')
+        plugin_args = 'ja4_fingerprint.so'
+        if self.use_preserve:
+            plugin_args += ' --preserve'
+        ts.Disk.plugin_config.AddLine(plugin_args)
 
         log_path = os.path.join(ts.Variables.LOGDIR, "ja4_fingerprint.log")
         ts.Disk.File(log_path, id='log_file')
@@ -146,9 +151,51 @@ class TestJA4Fingerprint:
                  'then a JA4 header should be attached.')
 def test1(params: TestParams) -> None:
     client = params['tr'].Processes.Default
-    params['tr'].MakeCurlCommand('-k -v 
"https://localhost:{0}/resource";'.format(params['port_one']), ts=params['ts'])
+    params['tr'].MakeCurlCommand(
+        '-k -v -H "uuid: no-existing-headers" 
"https://localhost:{0}/resource";'.format(params['port_one']), ts=params['ts'])
 
     client.ReturnCode = 0
     client.Streams.stdout += Testers.ContainsExpression(r'Yay!', 'We should 
receive the expected body.')
     params['ts'].Disk.traffic_out.Content += Testers.ContainsExpression(
         r'JA4 fingerprint:', 'We should receive the expected log message.')
+
+
[email protected]('With --preserve, existing JA4 headers should be 
preserved.', use_preserve=True)
+def test_preserve(params: TestParams) -> None:
+    '''Test that --preserve skips adding headers when JA4 headers exist.'''
+    tr = params['tr']
+    client = tr.Processes.Default
+    server = params['server_one']
+
+    # Request 1: No existing JA4 headers - should add them.
+    tr.MakeCurlCommand(
+        '-k -v -H "uuid: no-existing-headers" 
"https://localhost:{0}/resource";'.format(params['port_one']), ts=params['ts'])
+    client.ReturnCode = 0
+    client.Streams.stdout += Testers.ContainsExpression(r'Yay!', 'First 
request should succeed.')
+
+    # Request 2: With existing JA4 headers - should preserve them.
+    tr2 = Test.AddTestRun('Verify preserve skips adding JA4 headers when they 
exist.')
+    tr2.MakeCurlCommand(
+        '-k -v -H "uuid: existing-ja4-headers" -H "ja4: upstream-fingerprint" 
-H "x-ja4-via: upstream.proxy.com" '
+        
'"https://localhost:{0}/resource-with-headers";'.format(params['port_one']),
+        ts=params['ts'])
+    tr2.Processes.Default.ReturnCode = 0
+    tr2.Processes.Default.Streams.stdout += 
Testers.ContainsExpression(r'Preserved!', 'Second request should preserve 
headers.')
+
+    # Request 3: With only x-ja4-via - should also trigger preserve.
+    tr3 = Test.AddTestRun('Verify preserve triggers when only x-ja4-via 
exists.')
+    tr3.MakeCurlCommand(
+        '-k -v -H "uuid: existing-via-only" -H "x-ja4-via: upstream.proxy.com" 
'
+        '"https://localhost:{0}/resource-via-only";'.format(params['port_one']),
+        ts=params['ts'])
+    tr3.Processes.Default.ReturnCode = 0
+    tr3.Processes.Default.Streams.stdout += Testers.ContainsExpression(r'Via 
only!', 'Third request should succeed.')
+
+    # Verify the replay file's proxy-request validations pass (checked 
per-request).
+    # Also verify via Proxy Verifier's validation logs.
+    server.Streams.All += Testers.ContainsExpression(
+        r'Equals Success.*"/resource-with-headers".*ja4.*upstream-fingerprint',
+        'Preserved ja4 header should match original value.',
+        reflags=re.DOTALL)
+    server.Streams.All += Testers.ContainsExpression(
+        r'Absence Success.*"/resource-via-only".*ja4', 'ja4 should be absent 
when only x-ja4-via exists.', reflags=re.DOTALL)

Reply via email to