Copilot commented on code in PR #12995:
URL: https://github.com/apache/trafficserver/pull/12995#discussion_r2962988044


##########
doc/admin-guide/plugins/jax_fingerprint.en.rst:
##########
@@ -0,0 +1,180 @@
+.. Licensed to the Apache Software Foundation (ASF) under one
+   or more contributor license agreements.  See the NOTICE file
+   distributed with this work for additional information
+   regarding copyright ownership.  The ASF licenses this file
+   to you under the Apache License, Version 2.0 (the
+   "License"); you may not use this file except in compliance
+   with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing,
+   software distributed under the License is distributed on an
+   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+   KIND, either express or implied.  See the License for the
+   specific language governing permissions and limitations
+   under the License.
+
+.. include:: ../../common.defs
+
+.. _admin-plugins-jax-fingerprint:
+
+JAx Fingerprint Plugin
+**********************
+
+Description
+===========
+
+The JAx Fingerprint plugin generates TLS client fingerprints based on the JA4 
or JA3 algorithm designed by John Althouse.

Review Comment:
   The description claims the plugin generates fingerprints based on JA4 or 
JA3, but the plugin also supports `--method JA4H` (and the doc later documents 
it). Update this sentence to include JA4H (HTTP fingerprinting) so the overview 
matches the supported methods.
   ```suggestion
   The JAx Fingerprint plugin generates TLS and HTTP client fingerprints based 
on the JA4, JA4H, or JA3 algorithms designed by John Althouse.
   ```



##########
plugins/experimental/jax_fingerprint/plugin.cc:
##########
@@ -0,0 +1,463 @@
+/** @file
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include "plugin.h"
+#include "config.h"
+#include "context.h"
+#include "userarg.h"
+#include "method.h"
+#include "header.h"
+#include "log.h"
+
+#include "ja4/ja4_method.h"
+#include "ja4h/ja4h_method.h"
+#include "ja3/ja3_method.h"
+
+#include <ts/apidefs.h>
+#include <ts/ts.h>
+#include <ts/remap.h>
+#include <ts/remap_version.h>
+
+#include <getopt.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <version>
+
+DbgCtl dbg_ctl{PLUGIN_NAME};
+
+namespace
+{
+
+} // end anonymous namespace
+
+static bool
+read_config_option(int argc, char const *argv[], PluginConfig &config)
+{
+  const struct option longopts[] = {
+    {"standalone",   no_argument,       nullptr, 's'},
+    {"method",       required_argument, nullptr, 'M'}, // JA4, JA4H, or JA3
+    {"mode",         required_argument, nullptr, 'm'}, // overwrite, keep, or 
append
+    {"header",       required_argument, nullptr, 'h'},
+    {"via-header",   required_argument, nullptr, 'v'},
+    {"log-filename", required_argument, nullptr, 'f'},
+    {"servernames",  required_argument, nullptr, 'S'},
+    {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.");
+      break;
+    case 'M':
+      if (strcmp("JA4", optarg) == 0) {
+        config.method = ja4_method::method;
+      } else if (strcmp("JA4H", optarg) == 0) {
+        config.method = ja4h_method::method;
+      } else if (strcmp("JA3", optarg) == 0) {
+        config.method = ja3_method::method;
+      } else {
+        Dbg(dbg_ctl, "Unexpected method: %s", optarg);
+        return false;
+      }
+      break;
+    case 'm':
+      if (strcmp("overwrite", optarg) == 0) {
+        config.mode = Mode::OVERWRITE;
+      } else if (strcmp("keep", optarg) == 0) {
+        config.mode = Mode::KEEP;
+      } else if (strcmp("append", optarg) == 0) {
+        config.mode = Mode::APPEND;
+      } else {
+        Dbg(dbg_ctl, "Unexpected mode: %s", optarg);
+        return false;
+      }
+      break;
+    case 'h':
+      config.header_name = {optarg, strlen(optarg)};
+      break;
+    case 'v':
+      config.via_header_name = {optarg, strlen(optarg)};
+      break;
+    case 'f':
+      config.log_filename = {optarg, strlen(optarg)};
+      break;
+    case 's':
+      config.standalone = true;
+      break;
+    case 'S':
+      for (std::string_view input(optarg, strlen(optarg)); !input.empty();) {
+        auto pos = input.find(',');
+        config.servernames.emplace(input.substr(0, pos));
+        input.remove_prefix(pos == std::string_view::npos ? input.size() : pos 
+ 1);
+      }
+      break;
+    case 0:
+    case -1:
+      break;
+    default:
+      Dbg(dbg_ctl, "Unexpected options error.");
+      return false;
+    }
+  }
+
+  Dbg(dbg_ctl, "JAx method is %s", config.method.name);
+  Dbg(dbg_ctl, "JAx mode is %d", static_cast<int>(config.mode));
+  Dbg(dbg_ctl, "JAx header is %s", !config.header_name.empty() ? 
config.header_name.c_str() : "DISABLED");
+  Dbg(dbg_ctl, "JAx via-header is %s", !config.via_header_name.empty() ? 
config.via_header_name.c_str() : "DISABLED");
+  Dbg(dbg_ctl, "JAx log file is %s", !config.log_filename.empty() ? 
config.log_filename.c_str() : "DISABLED");
+  for (auto &&servername : config.servernames) {
+    Dbg(dbg_ctl, "%s", servername.c_str());
+  }
+
+  return true;
+}
+
+void
+modify_headers(JAxContext *ctx, TSHttpTxn txnp, PluginConfig &config)
+{
+  if (!ctx->get_fingerprint().empty()) {
+    switch (config.mode) {
+    case Mode::KEEP:
+      if (!config.header_name.empty() && !has_header(txnp, 
config.header_name)) {
+        set_header(txnp, config.header_name, ctx->get_fingerprint());
+      }
+      if (!config.via_header_name.empty() && !has_header(txnp, 
config.via_header_name)) {
+        set_via_header(txnp, config.via_header_name);
+      }
+      break;
+    case Mode::OVERWRITE:
+      if (!config.header_name.empty()) {

Review Comment:
   `Mode::KEEP` falls through into `Mode::OVERWRITE` because there is no 
`break;` after the KEEP case. That causes existing client headers to be 
overwritten even when KEEP is selected, defeating the documented semantics. Add 
a `break` after the KEEP handling (or restructure the switch to avoid 
accidental fallthrough).



##########
plugins/experimental/jax_fingerprint/header.cc:
##########
@@ -0,0 +1,160 @@
+/** @file
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include "plugin.h"
+#include "header.h"
+
+#include "ts/ts.h"
+
+#include <string>
+#include <string_view>
+
+static void
+put_header(TSHttpTxn txnp, const std::string &name, const std::string &value, 
bool overwrite)
+{
+  TSMBuffer bufp;
+  TSMLoc    hdr_loc;
+  if (TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) {
+    Dbg(dbg_ctl, "Failed to get headers.");
+    return;
+  }
+
+  TSMLoc target = TSMimeHdrFieldFind(bufp, hdr_loc, name.c_str(), 
name.length());
+  if (target == TS_NULL_MLOC) {
+    // Add - Create a new field with the value
+    Dbg(dbg_ctl, "Add %s: %s", name.c_str(), value.c_str());
+    TSMimeHdrFieldCreateNamed(bufp, hdr_loc, name.c_str(), name.length(), 
&target);
+    TSMimeHdrFieldValueStringSet(bufp, hdr_loc, target, -1, value.c_str(), 
value.length());
+    TSMimeHdrFieldAppend(bufp, hdr_loc, target);
+    TSHandleMLocRelease(bufp, hdr_loc, target);
+  } else if (overwrite) {
+    // Replace - Set the value to the first field and remove all duplicate 
fields
+    Dbg(dbg_ctl, "Replace %s field value with %s", name.c_str(), 
value.c_str());
+    TSMLoc tmp   = nullptr;
+    bool   first = true;
+    while (target) {
+      tmp = TSMimeHdrFieldNextDup(bufp, hdr_loc, target);
+      if (first) {
+        first = false;
+        TSMimeHdrFieldValueStringSet(bufp, hdr_loc, target, -1, value.c_str(), 
value.size());
+      } else {
+        TSMimeHdrFieldDestroy(bufp, hdr_loc, target);
+      }
+      TSHandleMLocRelease(bufp, hdr_loc, target);
+      target = tmp;
+    }
+  } else {
+    // Append - Find the last duplicate field and set the value to it
+    Dbg(dbg_ctl, "Append %s to %s field value", value.c_str(), name.c_str());
+    TSMLoc dup = TSMimeHdrFieldNextDup(bufp, hdr_loc, target);
+    while (dup != TS_NULL_MLOC) {
+      TSHandleMLocRelease(bufp, hdr_loc, target);
+      target = dup;
+      dup    = TSMimeHdrFieldNextDup(bufp, hdr_loc, target);
+    }
+    TSMimeHdrFieldValueStringInsert(bufp, hdr_loc, target, -1, value.c_str(), 
value.length());
+    TSHandleMLocRelease(bufp, hdr_loc, target);
+  }
+
+  TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
+}
+
+static void
+put_via_header(TSHttpTxn txnp, const std::string &via_header, bool overwrite)
+{
+  TSMgmtString proxy_name = nullptr;
+  if (TS_SUCCESS == TSMgmtStringGet("proxy.config.proxy_name", &proxy_name)) {
+    put_header(txnp, via_header, proxy_name, overwrite);
+    TSfree(proxy_name);
+  } else {
+    TSError("[%s] Failed to get proxy name for %s, set 
'proxy.config.proxy_name' in records.config", PLUGIN_NAME,
+            via_header.c_str());
+    put_header(txnp, via_header, "unknown", overwrite);
+  }
+}
+
+void
+set_header(TSHttpTxn txnp, const std::string &header, const std::string 
&fingerprint)
+{
+  put_header(txnp, header, fingerprint, true);
+}
+
+void
+append_header(TSHttpTxn txnp, const std::string &header, const std::string 
&fingerprint)
+{
+  put_header(txnp, header, fingerprint, false);
+}
+
+void
+set_via_header(TSHttpTxn txnp, const std::string &via_header)
+{
+  put_via_header(txnp, via_header, true);
+}
+
+void
+append_via_header(TSHttpTxn txnp, const std::string &via_header)
+{
+  put_via_header(txnp, via_header, false);
+}
+
+void
+remove_header(TSHttpTxn txnp, const std::string &header)
+{
+  TSMBuffer bufp;
+  TSMLoc    hdr_loc;
+  if (TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) {
+    Dbg(dbg_ctl, "Failed to get headers.");
+    return;
+  }
+
+  TSMLoc target = TSMimeHdrFieldFind(bufp, hdr_loc, header.c_str(), 
header.length());
+  if (target != TS_NULL_MLOC) {
+    // Remove all
+    Dbg(dbg_ctl, "Remove all %s field", header.c_str());
+    TSMLoc tmp = nullptr;
+    while (target) {
+      tmp = TSMimeHdrFieldNextDup(bufp, hdr_loc, target);
+      TSMimeHdrFieldDestroy(bufp, hdr_loc, target);
+      TSHandleMLocRelease(bufp, hdr_loc, target);
+      target = tmp;
+    }
+  }
+  TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
+}
+
+bool
+has_header(TSHttpTxn txnp, const std::string &header)
+{
+  TSMBuffer bufp;
+  TSMLoc    hdr_loc;
+  if (TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc)) {
+    Dbg(dbg_ctl, "Failed to get headers.");
+    return false;
+  }
+
+  TSMLoc target = TSMimeHdrFieldFind(bufp, hdr_loc, header.c_str(), 
header.length());
+  if (target == TS_NULL_MLOC) {
+    return false;
+  } else {
+    TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
+    return true;
+  }

Review Comment:
   `has_header()` leaks header handles: when the field is not found it returns 
`false` without releasing `hdr_loc`, and when it is found it releases `hdr_loc` 
but never releases the `target` field loc. Always `TSHandleMLocRelease()` both 
`target` (when non-null) and `hdr_loc` before returning to avoid per-txn handle 
leaks.
   ```suggestion
     bool   found  = (target != TS_NULL_MLOC);
   
     if (found) {
       TSHandleMLocRelease(bufp, hdr_loc, target);
     }
   
     TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
   
     return found;
   ```



##########
plugins/experimental/jax_fingerprint/plugin.cc:
##########
@@ -0,0 +1,463 @@
+/** @file
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+ */
+
+#include "plugin.h"
+#include "config.h"
+#include "context.h"
+#include "userarg.h"
+#include "method.h"
+#include "header.h"
+#include "log.h"
+
+#include "ja4/ja4_method.h"
+#include "ja4h/ja4h_method.h"
+#include "ja3/ja3_method.h"
+
+#include <ts/apidefs.h>
+#include <ts/ts.h>
+#include <ts/remap.h>
+#include <ts/remap_version.h>
+
+#include <getopt.h>
+
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <version>
+
+DbgCtl dbg_ctl{PLUGIN_NAME};
+
+namespace
+{
+
+} // end anonymous namespace
+
+static bool
+read_config_option(int argc, char const *argv[], PluginConfig &config)
+{
+  const struct option longopts[] = {
+    {"standalone",   no_argument,       nullptr, 's'},
+    {"method",       required_argument, nullptr, 'M'}, // JA4, JA4H, or JA3
+    {"mode",         required_argument, nullptr, 'm'}, // overwrite, keep, or 
append
+    {"header",       required_argument, nullptr, 'h'},
+    {"via-header",   required_argument, nullptr, 'v'},
+    {"log-filename", required_argument, nullptr, 'f'},
+    {"servernames",  required_argument, nullptr, 'S'},
+    {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.");
+      break;
+    case 'M':
+      if (strcmp("JA4", optarg) == 0) {
+        config.method = ja4_method::method;
+      } else if (strcmp("JA4H", optarg) == 0) {
+        config.method = ja4h_method::method;
+      } else if (strcmp("JA3", optarg) == 0) {
+        config.method = ja3_method::method;
+      } else {
+        Dbg(dbg_ctl, "Unexpected method: %s", optarg);
+        return false;
+      }
+      break;
+    case 'm':
+      if (strcmp("overwrite", optarg) == 0) {
+        config.mode = Mode::OVERWRITE;
+      } else if (strcmp("keep", optarg) == 0) {
+        config.mode = Mode::KEEP;
+      } else if (strcmp("append", optarg) == 0) {
+        config.mode = Mode::APPEND;
+      } else {
+        Dbg(dbg_ctl, "Unexpected mode: %s", optarg);
+        return false;
+      }
+      break;
+    case 'h':
+      config.header_name = {optarg, strlen(optarg)};
+      break;
+    case 'v':
+      config.via_header_name = {optarg, strlen(optarg)};
+      break;
+    case 'f':
+      config.log_filename = {optarg, strlen(optarg)};
+      break;
+    case 's':
+      config.standalone = true;
+      break;
+    case 'S':
+      for (std::string_view input(optarg, strlen(optarg)); !input.empty();) {
+        auto pos = input.find(',');
+        config.servernames.emplace(input.substr(0, pos));
+        input.remove_prefix(pos == std::string_view::npos ? input.size() : pos 
+ 1);
+      }
+      break;
+    case 0:
+    case -1:
+      break;
+    default:
+      Dbg(dbg_ctl, "Unexpected options error.");
+      return false;
+    }
+  }
+
+  Dbg(dbg_ctl, "JAx method is %s", config.method.name);
+  Dbg(dbg_ctl, "JAx mode is %d", static_cast<int>(config.mode));
+  Dbg(dbg_ctl, "JAx header is %s", !config.header_name.empty() ? 
config.header_name.c_str() : "DISABLED");
+  Dbg(dbg_ctl, "JAx via-header is %s", !config.via_header_name.empty() ? 
config.via_header_name.c_str() : "DISABLED");
+  Dbg(dbg_ctl, "JAx log file is %s", !config.log_filename.empty() ? 
config.log_filename.c_str() : "DISABLED");
+  for (auto &&servername : config.servernames) {
+    Dbg(dbg_ctl, "%s", servername.c_str());
+  }
+
+  return true;
+}
+
+void
+modify_headers(JAxContext *ctx, TSHttpTxn txnp, PluginConfig &config)
+{
+  if (!ctx->get_fingerprint().empty()) {
+    switch (config.mode) {
+    case Mode::KEEP:
+      if (!config.header_name.empty() && !has_header(txnp, 
config.header_name)) {
+        set_header(txnp, config.header_name, ctx->get_fingerprint());
+      }
+      if (!config.via_header_name.empty() && !has_header(txnp, 
config.via_header_name)) {
+        set_via_header(txnp, config.via_header_name);
+      }
+      break;
+    case Mode::OVERWRITE:
+      if (!config.header_name.empty()) {
+        set_header(txnp, config.header_name, ctx->get_fingerprint());
+      }
+      if (!config.via_header_name.empty()) {
+        set_via_header(txnp, config.via_header_name);
+      }
+      break;
+    case Mode::APPEND:
+      if (!config.header_name.empty()) {
+        append_header(txnp, config.header_name, ctx->get_fingerprint());
+      }
+      if (!config.via_header_name.empty()) {
+        append_via_header(txnp, config.via_header_name);
+      }
+      break;
+    default:
+      break;
+    }
+  } else {
+    Dbg(dbg_ctl, "No fingerprint attached to vconn!");
+    if (config.mode == Mode::OVERWRITE) {
+      if (!config.header_name.empty()) {
+        remove_header(txnp, config.header_name);
+      }
+      if (!config.via_header_name.empty()) {
+        remove_header(txnp, config.via_header_name);
+      }
+    }
+  }
+}
+
+int
+handle_client_hello(void *edata, PluginConfig &config)
+{
+  TSVConn     vconn = static_cast<TSVConn>(edata);
+  JAxContext *ctx   = get_user_arg(vconn, config);
+
+  if (!config.servernames.empty()) {
+    const char *servername;
+    int         servername_len;
+    servername = TSVConnSslSniGet(vconn, &servername_len);
+    if (servername != nullptr && servername_len > 0) {
+#ifdef __cpp_lib_generic_unordered_lookup
+      if (!config.servernames.contains(std::string_view(servername, 
servername_len))) {
+#else
+      if (!config.servernames.contains({servername, 
static_cast<size_t>(servername_len)})) {
+#endif
+        Dbg(dbg_ctl, "Server name %.*s is not in the server name set", 
servername_len, servername);
+        TSVConnReenable(vconn);
+        return TS_SUCCESS;
+      }
+    } else {
+      Dbg(dbg_ctl, "No SNI present but server name filtering is configured; 
skipping fingerprint generation");
+      TSVConnReenable(vconn);
+      return TS_SUCCESS;
+    }
+  }
+
+  if (nullptr == ctx) {
+    ctx = new JAxContext(config.method.name, TSNetVConnRemoteAddrGet(vconn));
+    set_user_arg(vconn, config, ctx);
+  }
+
+  if (config.method.on_client_hello) {
+    config.method.on_client_hello(ctx, vconn);
+  }
+
+  TSVConnReenable(vconn);
+
+  return TS_SUCCESS;
+}
+
+int
+handle_read_request_hdr(void *edata, PluginConfig &config)
+{
+  TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
+  if (txnp == nullptr) {
+    Dbg(dbg_ctl, "Failed to get txn object.");
+    return TS_SUCCESS;
+  }
+
+  TSHttpSsn ssnp = TSHttpTxnSsnGet(txnp);
+  if (ssnp == nullptr) {
+    Dbg(dbg_ctl, "Failed to get ssn object.");
+    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+    return TS_SUCCESS;
+  }
+
+  TSVConn vconn = TSHttpSsnClientVConnGet(ssnp);
+  if (vconn == nullptr) {
+    Dbg(dbg_ctl, "Failed to get vconn object.");
+    TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+    return TS_SUCCESS;
+  }
+
+  void *container;
+  if (config.method.type == Method::Type::CONNECTION_BASED) {
+    container = vconn;
+  } else {
+    container = txnp;
+  }
+  JAxContext *ctx = get_user_arg(container, config);
+  if (nullptr == ctx) {
+    ctx = new JAxContext(config.method.name, TSNetVConnRemoteAddrGet(vconn));
+    set_user_arg(container, config, ctx);
+  }
+
+  if (config.method.on_request) {
+    config.method.on_request(ctx, txnp);
+  }
+
+  if (!config.log_filename.empty()) {
+    log_fingerprint(ctx, config.log_handle);
+  }
+
+  modify_headers(ctx, txnp, config);
+
+  return TS_SUCCESS;
+}
+
+int
+handle_http_txn_close(void *edata, PluginConfig &config)
+{
+  TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
+
+  delete get_user_arg(txnp, config);
+  set_user_arg(txnp, config, nullptr);
+
+  TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+  return TS_SUCCESS;
+}
+
+int
+handle_vconn_close(void *edata, PluginConfig &config)
+{
+  TSVConn vconn = static_cast<TSVConn>(edata);
+
+  delete get_user_arg(vconn, config);
+  set_user_arg(vconn, config, nullptr);
+
+  TSVConnReenable(vconn);
+  return TS_SUCCESS;
+}
+
+int
+main_handler(TSCont cont, TSEvent event, void *edata)
+{
+  int ret;
+
+  auto config = static_cast<PluginConfig *>(TSContDataGet(cont));
+
+  switch (event) {
+  case TS_EVENT_SSL_CLIENT_HELLO:
+    ret = handle_client_hello(edata, *config);
+    break;
+  case TS_EVENT_HTTP_READ_REQUEST_HDR:
+    ret = handle_read_request_hdr(edata, *config);
+    TSHttpTxnReenable(static_cast<TSHttpTxn>(edata), TS_EVENT_HTTP_CONTINUE);

Review Comment:
   `main_handler()` always calls `TSHttpTxnReenable()` after 
`handle_read_request_hdr()`, but `handle_read_request_hdr()` also calls 
`TSHttpTxnReenable()` on some early-return error paths. The TS API explicitly 
warns that reenabling twice is a serious error; this can lead to crashes or 
undefined behavior. Ensure reenabling happens in exactly one place (e.g., only 
in `main_handler()` and remove reenables from `handle_read_request_hdr()`), or 
gate the reenable based on a return value/flag.
   ```suggestion
       if (ret == TS_SUCCESS) {
         TSHttpTxnReenable(static_cast<TSHttpTxn>(edata), 
TS_EVENT_HTTP_CONTINUE);
       }
   ```



##########
plugins/experimental/jax_fingerprint/ja4/ja4.h:
##########
@@ -0,0 +1,171 @@
+/** @file
+
+  @section license License
+
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <iterator>
+#include <string>
+#include <vector>
+
+namespace JA4
+{
+
+constexpr char PORTION_DELIMITER{'_'};
+
+enum class Protocol {
+  DTLS = 'd',
+  QUIC = 'q',
+  TLS  = 't',
+};
+
+enum class SNI {
+  to_domain = 'd',
+  to_IP     = 'i',
+};
+
+/**
+ * Represents the data sent in a TLS Client Hello needed for JA4 fingerprints.
+ */
+class TLSClientHelloSummary
+{
+public:
+  using difference_type = 
std::iterator_traits<std::vector<std::uint16_t>::iterator>::difference_type;
+
+  Protocol      protocol;
+  std::uint16_t TLS_version{0}; // 0 is not the default, this is only to not 
have it un-initialized.
+  std::string   ALPN;
+
+  std::vector<std::uint16_t> const &get_ciphers() const;
+  void                              add_cipher(std::uint16_t cipher);
+
+  std::vector<std::uint16_t> const &get_extensions() const;
+  void                              add_extension(std::uint16_t extension);
+
+  /**
+   * Get the number of ciphers excluding GREASE values.
+   *
+   * @return Returns the count of non-GREASE ciphers.
+   */
+  difference_type get_cipher_count() const;
+
+  /**
+   * Get the number of extensions excluding GREASE values.
+   *
+   * @return Returns the count of non-GREASE extensions.
+   */
+  difference_type get_extension_count() const;
+
+  /** Get the SNI type, domain or IP.
+   *
+   * @return Returns SNI::to_domain or SNI::to_IP.
+   */
+  SNI get_SNI_type() const;
+
+private:
+  std::vector<std::uint16_t> _ciphers;
+  std::vector<std::uint16_t> _extensions;
+  int                        _extension_count_including_sni_and_alpn{0};
+  SNI                        _SNI_type{SNI::to_IP};
+};
+
+/**
+ * Calculate the a portion of the JA4 fingerprint for the given client hello.
+ *
+ * The a portion of the fingerprint encodes the protocol, TLS version, SNI
+ * type, number of cipher suites, number of extensions, and first ALPN value.
+ *
+ * For more information see:
+ * https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md.
+ *
+ * @param TLS_summary The TLS client hello.
+ * @return Returns a string containing the a portion of the JA4 fingerprint.
+ */
+std::string make_JA4_a_raw(TLSClientHelloSummary const &TLS_summary);
+
+/**
+ * Calculate the b portion of the JA4 fingerprint for the given client hello.
+ *
+ * The b portion of the fingerprint is a comma-delimited list of lowercase hex
+ * numbers representing the cipher suites in sorted order. GREASE values are
+ * ignored.
+ *
+ * For more information see:
+ * https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md.
+ *
+ * @param TLS_summary The TLS client hello.
+ * @return Returns a string containing the b portion of the JA4 fingerprint.
+ */
+std::string make_JA4_b_raw(TLSClientHelloSummary const &TLS_summary);
+
+/**
+ * Calculate the c portion of the JA4 fingerprint for the given client hello.
+ *
+ * The b portion of the fingerprint is a comma-delimited list of lowercase hex
+ * numbers representing the extensions in sorted order. GREASE values and the
+ * SNI and ALPN extensions are ignored.
+ *
+ * For more information see:
+ * https://github.com/FoxIO-LLC/ja4/blob/main/technical_details/JA4.md.
+ *
+ * @param TLS_summary The TLS client hello.
+ * @return Returns a string containing the c portion of the JA4 fingerprint.
+ */
+std::string make_JA4_c_raw(TLSClientHelloSummary const &TLS_summary);
+
+/**
+ * Calculate the JA4 fingerprint for the given TLS client hello.
+ *
+ * @param TLS_summary The TLS client hello. If there was no ALPN in the
+ * Client Hello, TLS_summary.ALPN should either be empty or set to "00".
+ * Behavior when the number of digits in TLS_summary.TLS_version is greater
+ * than 2, the number of digits in TLS_summary.ALPN is greater than 2
+ * (except when TLS_summary.ALPN is empty) is unspecified.
+ * @param UnaryOp hasher A hash function. For a specification-compliant
+ * JA4 fingerprint, this should be a sha256 hash.
+ * @return Returns a string containing the JA4 fingerprint.
+ */
+template <typename UnaryOp>
+std::string
+make_JA4_fingerprint(TLSClientHelloSummary const &TLS_summary, UnaryOp hasher)
+{
+  std::string result;
+  result.append(make_JA4_a_raw(TLS_summary));
+  result.push_back(JA4::PORTION_DELIMITER);
+  result.append(hasher(make_JA4_b_raw(TLS_summary)).substr(0, 12));
+  result.push_back(JA4::PORTION_DELIMITER);
+  result.append(hasher(make_JA4_c_raw(TLS_summary)).substr(0, 12));
+  return result;
+}
+
+/**
+ * Check whether @a value is a GREASE value.
+ *
+ * These are reserved extensions randomly advertised to keep implementations
+ * well lubricated. They are ignored in all parts of JA4 because of their
+ * random nature.
+ *
+ * @return Returns true if the value is a GREASE value, fales otherwise.

Review Comment:
   Typo in the doxygen comment: "fales" should be "false".
   ```suggestion
    * @return Returns true if the value is a GREASE value, false otherwise.
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to