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


##########
plugins/experimental/jax_fingerprint/plugin.cc:
##########
@@ -0,0 +1,444 @@
+/** @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:
+      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;
+      }

Review Comment:
   SNI filtering: when `--servernames` is configured but the connection has no 
SNI (`TSVConnSslSniGet` returns null/len<=0), this code falls through and still 
generates a fingerprint. That conflicts with the documented behavior “only for 
specified server names”. Consider treating missing/empty SNI as “not in set” 
and skipping fingerprint generation in that case.
   ```suggestion
       if (servername == nullptr || servername_len <= 0) {
         Dbg(dbg_ctl, "No SNI present but server name filtering is configured; 
skipping fingerprint generation");
         TSVConnReenable(vconn);
         return TS_SUCCESS;
       }
   #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;
   ```



##########
plugins/experimental/jax_fingerprint/plugin.cc:
##########
@@ -0,0 +1,444 @@
+/** @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:
+      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;
+      }
+    }
+  }
+
+  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);
+    break;
+  case TS_EVENT_HTTP_TXN_CLOSE:
+    ret = handle_http_txn_close(edata, *config);
+    break;
+  case TS_EVENT_VCONN_CLOSE:
+    ret = handle_vconn_close(edata, *config);
+    break;
+  default:
+    Dbg(dbg_ctl, "Unexpected event %d.", event);
+    // We ignore the event, but we don't want to reject the connection.
+    ret = TS_SUCCESS;
+  }
+
+  return ret;
+}
+
+void
+TSPluginInit(int argc, char const **argv)
+{
+  TSPluginRegistrationInfo info;
+  info.plugin_name   = PLUGIN_NAME;
+  info.vendor_name   = PLUGIN_VENDOR;
+  info.support_email = PLUGIN_SUPPORT_EMAIL;
+
+  if (TS_SUCCESS != TSPluginRegister(&info)) {
+    TSError("[%s] Failed to register.", PLUGIN_NAME);
+    return;
+  }
+
+  PluginConfig *config = new PluginConfig();
+  config->plugin_type  = PluginType::GLOBAL;
+
+  if (!read_config_option(argc, argv, *config)) {
+    TSError("[%s] Failed to parse options.", PLUGIN_NAME);
+    return;
+  }
+
+  if (!config->log_filename.empty()) {
+    if (!create_log_file(config->log_filename, config->log_handle)) {
+      TSError("[%s] Failed to create log.", PLUGIN_NAME);
+      return;
+    } else {
+      Dbg(dbg_ctl, "Created log file.");
+    }
+  }
+
+  reserve_user_arg(*config);
+
+  TSCont cont = TSContCreate(main_handler, nullptr);
+  TSContDataSet(cont, config);
+  if (config->method.on_client_hello) {
+    TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, cont);
+  }
+  if (config->standalone) {
+    TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, cont);
+  }
+  if (config->method.type == Method::Type::CONNECTION_BASED) {
+    TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, cont);
+  } else {
+    TSHttpHookAdd(TS_HTTP_TXN_CLOSE_HOOK, cont);
+  }
+}
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  Dbg(dbg_ctl, "JAx Remap Plugin initializing..");
+  CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size);
+
+  return TS_SUCCESS;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf 
ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */)
+{
+  Dbg(dbg_ctl, "New instance for client matching %s to %s", argv[0], argv[1]);
+  auto config         = new PluginConfig();
+  config->plugin_type = PluginType::REMAP;
+
+  // Parse parameters
+  if (!read_config_option(argc - 1, const_cast<const char **>(argv + 1), 
*config)) {
+    delete config;
+    Dbg(dbg_ctl, "Bad arguments");
+    return TS_ERROR;
+  }
+
+  // Create a log file
+  if (!config->log_filename.empty()) {
+    if (!create_log_file(config->log_filename, config->log_handle)) {
+      TSError("[%s] Failed to create log.", PLUGIN_NAME);

Review Comment:
   In `TSRemapNewInstance()`, if `create_log_file()` fails you return 
`TS_ERROR` but never `delete config`, leaking the per-instance config on each 
failed instantiation. Clean up `config` on this failure path before returning.
   ```suggestion
         TSError("[%s] Failed to create log.", PLUGIN_NAME);
         delete config;
   ```



##########
plugins/experimental/jax_fingerprint/context.cc:
##########
@@ -0,0 +1,71 @@
+/** @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 "context.h"
+
+JAxContext::JAxContext(const char *method_name, sockaddr const *s_sockaddr) : 
_method_name(method_name)
+{
+  _addr[0] = '\0';
+
+  if (s_sockaddr == nullptr) {
+    return;
+  }
+
+  switch (s_sockaddr->sa_family) {
+  case AF_INET:
+    inet_ntop(AF_INET, &reinterpret_cast<const sockaddr_in 
*>(s_sockaddr)->sin_addr, _addr, INET_ADDRSTRLEN);
+    break;
+  case AF_INET6:
+    inet_ntop(AF_INET6, &reinterpret_cast<const sockaddr_in6 
*>(s_sockaddr)->sin6_addr, _addr, INET6_ADDRSTRLEN);
+    break;
+  case AF_UNIX:
+    strncpy(_addr, reinterpret_cast<const sockaddr_un 
*>(s_sockaddr)->sun_path, sizeof(_addr) - 1);
+    _addr[sizeof(_addr) - 1] = '\0';

Review Comment:
   `AF_UNIX` case falls through into `default` without an explicit `break` / 
`[[fallthrough]]`. This can trigger compiler warnings and makes the intent 
unclear. Add a `break;` after handling `AF_UNIX` (or annotate the fallthrough).
   ```suggestion
       _addr[sizeof(_addr) - 1] = '\0';
       break;
   ```



##########
plugins/experimental/jax_fingerprint/ja4h/ja4h.cc:
##########
@@ -0,0 +1,139 @@
+/** @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 "ja4h.h"
+#include <openssl/sha.h>
+
+Extractor::Extractor(TSHttpTxn txnp) : _txn(txnp)
+{
+  TSHttpTxnClientReqGet(txnp, &(this->_request), &(this->_req_hdr));
+}
+
+Extractor::~Extractor()
+{
+  if (this->_request != nullptr) {
+    TSHandleMLocRelease(this->_request, TS_NULL_MLOC, this->_req_hdr);
+  }
+}
+
+std::string_view
+Extractor::get_method()
+{
+  if (this->_request == nullptr) {
+    return "";
+  }
+
+  int         method_len;
+  const char *method = TSHttpHdrMethodGet(this->_request, this->_req_hdr, 
&method_len);
+
+  return {method, static_cast<size_t>(method_len)};
+}
+
+int
+Extractor::get_version()
+{
+  if (TSHttpTxnClientProtocolStackContains(this->_txn, "h2")) {
+    return 2 << 16;
+  } else if (TSHttpTxnClientProtocolStackContains(this->_txn, "h3")) {
+    return 3 << 16;
+  } else {
+    return TSHttpHdrVersionGet(this->_request, this->_req_hdr);
+  }
+}
+
+bool
+Extractor::has_cookie_field()
+{
+  TSMLoc mloc = TSMimeHdrFieldFind(this->_request, this->_req_hdr, 
TS_MIME_FIELD_COOKIE, TS_MIME_LEN_COOKIE);
+  if (mloc) {
+    TSHandleMLocRelease(this->_request, this->_req_hdr, mloc);
+  }
+  return mloc != TS_NULL_MLOC;
+}
+
+bool
+Extractor::has_referer_field()
+{
+  TSMLoc mloc = TSMimeHdrFieldFind(this->_request, this->_req_hdr, 
TS_MIME_FIELD_REFERER, TS_MIME_LEN_REFERER);
+  if (mloc) {
+    TSHandleMLocRelease(this->_request, this->_req_hdr, mloc);
+  }
+  return mloc != TS_NULL_MLOC;
+}
+
+int
+Extractor::get_field_count()
+{
+  return TSMimeHdrFieldsCount(this->_request, this->_req_hdr);
+}
+
+std::string_view
+Extractor::get_accept_language()
+{
+  TSMLoc mloc = TSMimeHdrFieldFind(this->_request, this->_req_hdr, 
TS_MIME_FIELD_ACCEPT_LANGUAGE, TS_MIME_LEN_ACCEPT_LANGUAGE);
+  if (mloc == TS_NULL_MLOC) {
+    return {};
+  }
+  int         value_len;
+  const char *value = TSMimeHdrFieldValueStringGet(this->_request, 
this->_req_hdr, mloc, 0, &value_len);
+  TSHandleMLocRelease(this->_request, this->_req_hdr, mloc);
+  return {value, static_cast<size_t>(value_len)};
+}
+
+void
+Extractor::get_headers_hash(unsigned char out[32])
+{
+  SHA256_CTX sha256ctx;
+  SHA256_Init(&sha256ctx);
+
+  TSMLoc field_loc = TSMimeHdrFieldGet(this->_request, this->_req_hdr, 0);
+
+  while (field_loc != TS_NULL_MLOC) {
+    int   field_name_len;
+    char *field_name = const_cast<char 
*>(TSMimeHdrFieldNameGet(this->_request, this->_req_hdr, field_loc, 
&field_name_len));
+    bool  do_hash    = true;
+    if (field_name_len == TS_MIME_LEN_COOKIE) {
+      auto field_name_sv = std::string_view(field_name, 
static_cast<size_t>(field_name_len));
+      if (std::equal(field_name_sv.begin(), field_name_sv.end(), 
std::string_view("cookie").begin(),
+                     [](char c1, char c2) { return c1 == std::tolower(c2); })) 
{
+        do_hash = false;
+      };
+    } else if (field_name_len == TS_MIME_LEN_REFERER) {
+      auto field_name_sv = std::string_view(field_name, 
static_cast<size_t>(field_name_len));
+      if (std::equal(field_name_sv.begin(), field_name_sv.end(), 
std::string_view("referer").begin(),
+                     [](char c1, char c2) { return c1 == std::tolower(c2); })) 
{

Review Comment:
   The cookie/referer exclusion check is intended to be case-insensitive, but 
the comparator lowercases the literal (`c2`) instead of the incoming header 
name (`c1`). As a result, headers like "Cookie" / "Referer" won’t match and 
will be hashed, changing the JA4H_b value. Consider comparing `tolower(c1)` vs 
`tolower(c2)` (with unsigned-char casts) or using an existing case-insensitive 
helper.
   ```suggestion
                        [](char c1, char c2) {
                          return std::tolower(static_cast<unsigned char>(c1)) 
== std::tolower(static_cast<unsigned char>(c2));
                        })) {
           do_hash = false;
         };
       } else if (field_name_len == TS_MIME_LEN_REFERER) {
         auto field_name_sv = std::string_view(field_name, 
static_cast<size_t>(field_name_len));
         if (std::equal(field_name_sv.begin(), field_name_sv.end(), 
std::string_view("referer").begin(),
                        [](char c1, char c2) {
                          return std::tolower(static_cast<unsigned char>(c1)) 
== std::tolower(static_cast<unsigned char>(c2));
                        })) {
   ```



##########
plugins/experimental/jax_fingerprint/userarg.cc:
##########
@@ -0,0 +1,71 @@
+/** @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 "userarg.h"
+
+void
+reserve_user_arg(PluginConfig &config)
+{
+  char name[strlen(PLUGIN_NAME) + strlen(config.method.name) + 1];
+  name[0] = '\0';
+  strcat(name, PLUGIN_NAME);
+  strcat(name, config.method.name);
+
+  TSUserArgType type;
+  if (config.method.type == Method::Type::CONNECTION_BASED) {
+    type = TS_USER_ARGS_VCONN;
+  } else {
+    type = TS_USER_ARGS_TXN;
+  }
+  TSUserArgIndexReserve(type, name, "used to pass JAx context between hooks", 
&config.user_arg_index);
+  Dbg(dbg_ctl, "user_arg_name: %s, user_arg_index: %d", name, 
config.user_arg_index);

Review Comment:
   `TSUserArgIndexReserve()` / `TSUserArgIndexNameLookup()` return a 
`TSReturnCode`, but the result is ignored. If reserve/lookup fails, 
`config.user_arg_index` can remain -1 and later `TSUserArgSet/Get` will operate 
on an invalid index. Consider checking the return code and failing plugin init 
/ instance creation when the index cannot be obtained.



##########
plugins/experimental/jax_fingerprint/plugin.cc:
##########
@@ -0,0 +1,444 @@
+/** @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:
+      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;
+      }
+    }
+  }
+
+  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);
+    break;
+  case TS_EVENT_HTTP_TXN_CLOSE:
+    ret = handle_http_txn_close(edata, *config);
+    break;
+  case TS_EVENT_VCONN_CLOSE:
+    ret = handle_vconn_close(edata, *config);
+    break;
+  default:
+    Dbg(dbg_ctl, "Unexpected event %d.", event);
+    // We ignore the event, but we don't want to reject the connection.
+    ret = TS_SUCCESS;
+  }
+
+  return ret;
+}
+
+void
+TSPluginInit(int argc, char const **argv)
+{
+  TSPluginRegistrationInfo info;
+  info.plugin_name   = PLUGIN_NAME;
+  info.vendor_name   = PLUGIN_VENDOR;
+  info.support_email = PLUGIN_SUPPORT_EMAIL;
+
+  if (TS_SUCCESS != TSPluginRegister(&info)) {
+    TSError("[%s] Failed to register.", PLUGIN_NAME);
+    return;
+  }
+
+  PluginConfig *config = new PluginConfig();
+  config->plugin_type  = PluginType::GLOBAL;
+
+  if (!read_config_option(argc, argv, *config)) {
+    TSError("[%s] Failed to parse options.", PLUGIN_NAME);
+    return;
+  }
+
+  if (!config->log_filename.empty()) {
+    if (!create_log_file(config->log_filename, config->log_handle)) {
+      TSError("[%s] Failed to create log.", PLUGIN_NAME);
+      return;
+    } else {
+      Dbg(dbg_ctl, "Created log file.");
+    }
+  }
+
+  reserve_user_arg(*config);
+
+  TSCont cont = TSContCreate(main_handler, nullptr);
+  TSContDataSet(cont, config);
+  if (config->method.on_client_hello) {
+    TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, cont);
+  }
+  if (config->standalone) {
+    TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, cont);
+  }
+  if (config->method.type == Method::Type::CONNECTION_BASED) {
+    TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, cont);
+  } else {
+    TSHttpHookAdd(TS_HTTP_TXN_CLOSE_HOOK, cont);
+  }
+}
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  Dbg(dbg_ctl, "JAx Remap Plugin initializing..");
+  CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size);
+
+  return TS_SUCCESS;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf 
ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */)
+{
+  Dbg(dbg_ctl, "New instance for client matching %s to %s", argv[0], argv[1]);
+  auto config         = new PluginConfig();
+  config->plugin_type = PluginType::REMAP;
+
+  // Parse parameters
+  if (!read_config_option(argc - 1, const_cast<const char **>(argv + 1), 
*config)) {
+    delete config;
+    Dbg(dbg_ctl, "Bad arguments");
+    return TS_ERROR;
+  }
+
+  // Create a log file
+  if (!config->log_filename.empty()) {
+    if (!create_log_file(config->log_filename, config->log_handle)) {
+      TSError("[%s] Failed to create log.", PLUGIN_NAME);
+      return TS_ERROR;
+    } else {
+      Dbg(dbg_ctl, "Created log file.");
+    }
+  }
+
+  reserve_user_arg(*config);
+
+  // Create continuation
+  if (config->standalone) {
+    config->handler = TSContCreate(main_handler, nullptr);
+    if (config->method.on_client_hello) {
+      TSHttpHookAdd(TS_SSL_CLIENT_HELLO_HOOK, config->handler);
+    }
+    if (config->method.type == Method::Type::CONNECTION_BASED) {
+      TSHttpHookAdd(TS_VCONN_CLOSE_HOOK, config->handler);
+    } else {
+      TSHttpHookAdd(TS_HTTP_TXN_CLOSE_HOOK, config->handler);
+    }
+    TSContDataSet(config->handler, config);
+  }
+
+  *ih = static_cast<void *>(config);
+
+  return TS_SUCCESS;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  auto config = static_cast<PluginConfig *>(ih);
+
+  if (!config || !rri) {
+    TSError("[%s] Invalid private data or RRI or handler.", PLUGIN_NAME);
+    return TSREMAP_NO_REMAP;
+  }
+
+  handle_read_request_hdr(rh, *config);
+
+  return TSREMAP_NO_REMAP;
+}
+
+void
+TSRemapDeleteInstance(void *ih)
+{
+  auto config = static_cast<PluginConfig *>(ih);
+  if (config->handler) {
+    TSContDestroy(config->handler);
+  }
+  delete config;
+}

Review Comment:
   `TSRemapDeleteInstance()` destroys the continuation and deletes `config`, 
but never closes a per-instance `log_handle` created in `TSRemapNewInstance()`. 
On remap.config reloads this can leak file descriptors. Consider calling 
`TSTextLogObjectDestroy(config->log_handle)` (when non-null) before deleting 
the config.



##########
plugins/experimental/jax_fingerprint/ja4h/ja4h.cc:
##########
@@ -0,0 +1,139 @@
+/** @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 "ja4h.h"
+#include <openssl/sha.h>

Review Comment:
   This file uses `std::equal` and `std::tolower` but doesn’t include the 
standard headers that declare them. Add the needed includes (e.g., 
`<algorithm>` / `<cctype>`) to avoid relying on transitive includes.
   ```suggestion
   #include <openssl/sha.h>
   #include <algorithm>
   #include <cctype>
   ```



##########
plugins/experimental/jax_fingerprint/config.h:
##########
@@ -0,0 +1,66 @@
+/** @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 "ts/ts.h"
+#include "method.h"
+
+#include <string>
+#include <unordered_set>
+
+enum class Mode : int {
+  OVERWRITE,
+  KEEP,
+  APPEND,
+};
+
+enum class PluginType : int {
+  GLOBAL,
+  REMAP,
+};
+
+// This hash function enables looking up the set by a string_view without 
making a temporal string object.

Review Comment:
   Spelling: “temporal string object” should be “temporary string object”.
   ```suggestion
   // This hash function enables looking up the set by a string_view without 
making a temporary string object.
   ```



##########
plugins/experimental/jax_fingerprint/ja4/ja4_method.cc:
##########
@@ -0,0 +1,153 @@
+/** @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 "ts/ts.h"
+
+#include "../plugin.h"
+#include "../context.h"
+#include "ja4_method.h"
+#include "ja4.h"
+
+#include <openssl/sha.h>
+#include <openssl/ssl.h>
+
+constexpr unsigned int EXT_ALPN{0x10};
+constexpr unsigned int EXT_SUPPORTED_VERSIONS{0x2b};
+
+namespace ja4_method
+{
+
+void on_client_hello(JAxContext *, TSVConn);
+
+struct Method method = {
+  "JA4",
+  Method::Type::CONNECTION_BASED,
+  on_client_hello,
+  nullptr,
+};
+
+} // namespace ja4_method
+
+static std::uint16_t
+get_version(TSClientHello ch)
+{
+  unsigned char const *buf{};
+  std::size_t          buflen{};
+  if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_SUPPORTED_VERSIONS, 
&buf, &buflen)) {
+    std::uint16_t max_version{0};
+    size_t        n_versions = buf[0];
+    for (size_t i = 1; i + 1 < buflen && i < (n_versions * 2) + 1; i += 2) {
+      std::uint16_t version = (buf[i] << 8) | buf[i + 1];
+      if (!JA4::is_GREASE(version) && version > max_version) {
+        max_version = version;
+      }
+    }
+    return max_version;
+  } else {
+    Dbg(dbg_ctl, "No supported_versions extension... using legacy version.");
+    return ch.get_version();
+  }
+}
+
+static std::string
+get_first_ALPN(TSClientHello ch)
+{
+  unsigned char const *buf{};
+  std::size_t          buflen{};
+  std::string          result{""};
+  if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_ALPN, &buf, &buflen)) {
+    // The first two bytes are a 16bit encoding of the total length.
+    unsigned char first_ALPN_length{buf[2]};
+    TSAssert(buflen > 4);
+    TSAssert(0 != first_ALPN_length);

Review Comment:
   In `get_first_ALPN()`, `buf[2]` is read before validating that the extension 
buffer is large enough, and `TSAssert(buflen > 4)` may be compiled out in some 
builds. Add explicit `buflen` bounds checks before indexing to avoid 
out-of-bounds reads on malformed client hellos.
   ```suggestion
       if (buflen <= 3) {
         return result;
       }
   
       unsigned char first_ALPN_length{buf[2]};
       if (first_ALPN_length == 0) {
         return result;
       }
   
       if (buflen < 3 + static_cast<std::size_t>(first_ALPN_length)) {
         return result;
       }
   
       TSAssert(buflen >= 3 + static_cast<std::size_t>(first_ALPN_length));
       TSAssert(0 != first_ALPN_length);
   ```



##########
plugins/experimental/jax_fingerprint/ja4/ja4.cc:
##########
@@ -0,0 +1,175 @@
+/** @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 "ja4.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <iterator>
+#include <string>
+#include <string_view>
+
+static char        convert_protocol_to_char(JA4::Protocol protocol);
+static std::string convert_TLS_version_to_string(std::uint16_t TLS_version);
+static char        convert_SNI_to_char(JA4::SNI SNI_type);
+static std::string convert_count_to_two_digit_string(std::size_t count);
+static std::string convert_ALPN_to_two_char_string(std::string_view ALPN);
+static void        remove_trailing_character(std::string &s);
+static std::string hexify(std::uint16_t n);
+
+namespace
+{
+constexpr std::size_t U16_HEX_BUF_SIZE{4};
+} // end anonymous namespace
+
+std::string
+JA4::make_JA4_a_raw(TLSClientHelloSummary const &TLS_summary)
+{
+  std::string result;
+  result.reserve(9);
+  result.push_back(convert_protocol_to_char(TLS_summary.protocol));
+  result.append(convert_TLS_version_to_string(TLS_summary.TLS_version));
+  result.push_back(convert_SNI_to_char(TLS_summary.get_SNI_type()));
+  
result.append(convert_count_to_two_digit_string(TLS_summary.get_cipher_count()));
+  
result.append(convert_count_to_two_digit_string(TLS_summary.get_extension_count()));
+  result.append(convert_ALPN_to_two_char_string(TLS_summary.ALPN));
+  return result;
+}
+
+static char
+convert_protocol_to_char(JA4::Protocol protocol)
+{
+  return static_cast<char>(protocol);
+}
+
+static std::string
+convert_TLS_version_to_string(std::uint16_t TLS_version)
+{
+  switch (TLS_version) {
+  case 0x304:
+    return "13";
+  case 0x303:
+    return "12";
+  case 0x302:
+    return "11";
+  case 0x301:
+    return "10";
+  case 0x300:
+    return "s3";
+  case 0x200:
+    return "s2";
+  case 0x100:
+    return "s1";
+  case 0xfeff:
+    return "d1";
+  case 0xfefd:
+    return "d2";
+  case 0xfefc:
+    return "d3";
+  default:
+    return "00";
+  }
+}
+
+static char
+convert_SNI_to_char(JA4::SNI SNI_type)
+{
+  return static_cast<char>(SNI_type);
+}
+
+static std::string
+convert_count_to_two_digit_string(std::size_t count)
+{
+  std::string result;
+  if (count <= 9) {
+    result.push_back('0');
+  }
+  // We could also clamp the lower bound to 1 since there must be at least 1
+  // cipher, but 0 is more helpful for debugging if the cipher list is empty.
+  result.append(std::to_string(std::clamp(count, std::size_t{0}, 
std::size_t{99})));
+  return result;
+}
+
+std::string
+convert_ALPN_to_two_char_string(std::string_view ALPN)
+{
+  std::string result;
+  if (ALPN.empty()) {
+    result = "00";
+  } else {
+    result.push_back(ALPN.front());
+    result.push_back(ALPN.back());
+  }
+  return result;
+}

Review Comment:
   Linkage mismatch: `convert_ALPN_to_two_char_string` is forward-declared as 
`static` (internal linkage) but defined here without `static`, which will fail 
to compile due to conflicting linkage. Make the definition `static` (or remove 
`static` from the forward declaration).



##########
doc/admin-guide/plugins/jax_fingerprint.en.rst:
##########
@@ -0,0 +1,179 @@
+.. 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.
+
+Fingerprints can be used for:
+
+* Client identification and tracking
+* Bot detection and mitigation
+* Security analytics and threat intelligence
+* Understanding client implementation patterns
+
+
+Plugin Configuration
+====================
+
+You can use the plugin as a global plugin, a remap plugin, or both.
+
+To use the plugin as a global plugin, add the following line to 
:file:`plugin.config`::
+
+    jax_fingerprint.so --standalone
+
+To use the plugin as a remap plugin, append the following line to a remap rule 
on :file:`remap.config`::
+
+    @plugin=jax_fingerprint.so @pparam=--standalone
+
+To use the plugin as both global and remap plugin (hybrid setup), have the 
both without `--standalone` option.
+
+
+.. option:: --standalone
+
+This option enables you to use the plugin as either a global plugin, or a 
remap plugin. In other
+words, the option needs to be specified if you do not use the hybrid setup.
+
+.. option:: --method <JA4|JA4H|JA3>
+
+Fingerprinting method (e.g. JA4, JA3, etc.) to use.
+
+.. option:: --mode <overwrite|keep|append>
+
+This option specifies what to do if requests from clients have the header 
names that are specified
+by `--header` and/or `--via-header`. Available setting values are "overwrite", 
"keep" and "append".
+
+.. option:: --servernames <servername1,servername2,...>
+
+This option specifies server name(s) for which the plugin generates 
fingerprints.
+The value must be provided as a single comma separated value (no space) of 
server names.
+
+.. option:: --header <header_name>
+
+This option specifies the name of the header field where the plugin stores the 
generated fingerprint value. If not specified, header generation will be 
suppressed.
+
+.. option:: --via-header <via_header_name>
+
+This option specifies the name of the header field where the plugin stores the 
generated fingerprint-via value. If not specified, header generation will be 
suppressed.
+
+.. option:: --log-filename <filename>
+
+This option specifies the filename for the plugin log file. If not specified, 
log output will be suppressed.
+
+
+Plugin Behavior
+===============
+
+Global plugin setup
+-------------------
+
+Global plugin setup is the best if you:
+ * Need a fingerprint on every request
+
+Remap plugin setup
+------------------
+
+Remap plugin setup is the best if you:
+ * Need a fingerprint only on specific paths, or
+ * Cannot use Global plugin setup
+
+.. note:: For JA3 and JA4, fingerprints are always generated at the beginning 
of connections. Using remap plugin setup only reduces the overhead of adding 
HTTP headers and logging.
+
+Hybrid setup
+------------
+
+Hybrid setup is the best if you:
+ * Need a fingerprint only for specific server names (in TLS SNI extension), 
and
+ * Need a fingerprint only on specific paths
+
+
+Log Output
+==========
+
+The plugin output a log file in the Traffic Server log directory (typically 
``/var/log/trafficserver/``) if a log filename is
+specified by `--log-filename` option.

Review Comment:
   Grammar: “The plugin output a log file …” should be “The plugin outputs a 
log file …”.
   ```suggestion
   The plugin outputs a log file in the Traffic Server log directory (typically 
``/var/log/trafficserver/``) if a log filename is
   specified by the ``--log-filename`` option.
   ```



##########
plugins/experimental/jax_fingerprint/ja4/ja4_method.cc:
##########
@@ -0,0 +1,153 @@
+/** @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 "ts/ts.h"
+
+#include "../plugin.h"
+#include "../context.h"
+#include "ja4_method.h"
+#include "ja4.h"
+
+#include <openssl/sha.h>
+#include <openssl/ssl.h>
+
+constexpr unsigned int EXT_ALPN{0x10};
+constexpr unsigned int EXT_SUPPORTED_VERSIONS{0x2b};
+
+namespace ja4_method
+{
+
+void on_client_hello(JAxContext *, TSVConn);
+
+struct Method method = {
+  "JA4",
+  Method::Type::CONNECTION_BASED,
+  on_client_hello,
+  nullptr,
+};
+
+} // namespace ja4_method
+
+static std::uint16_t
+get_version(TSClientHello ch)
+{
+  unsigned char const *buf{};
+  std::size_t          buflen{};
+  if (TS_SUCCESS == TSClientHelloExtensionGet(ch, EXT_SUPPORTED_VERSIONS, 
&buf, &buflen)) {
+    std::uint16_t max_version{0};
+    size_t        n_versions = buf[0];
+    for (size_t i = 1; i + 1 < buflen && i < (n_versions * 2) + 1; i += 2) {
+      std::uint16_t version = (buf[i] << 8) | buf[i + 1];
+      if (!JA4::is_GREASE(version) && version > max_version) {
+        max_version = version;
+      }
+    }

Review Comment:
   `supported_versions` parsing: the first byte is the length in bytes of the 
versions list, not a count of versions. The loop bounds currently mix these 
concepts (`n_versions * 2`), which can lead to reading extra data if `buflen` 
includes more than the vector length. Consider iterating `for (i = 1; i + 1 < 1 
+ buf[0]; i += 2)` (and validating `buflen >= 1 + buf[0]`) to follow the TLS 
encoding.
   ```suggestion
       if (buflen < 1) {
         Dbg(dbg_ctl, "Malformed supported_versions extension (no length 
byte)... using legacy version.");
         return ch.get_version();
       }
   
       std::uint8_t versions_len = buf[0];
       if (buflen < static_cast<std::size_t>(1 + versions_len)) {
         Dbg(dbg_ctl, "Malformed supported_versions extension (truncated 
vector)... using legacy version.");
         return ch.get_version();
       }
   
       std::uint16_t max_version{0};
       for (std::size_t i = 1; i + 1 < static_cast<std::size_t>(1 + 
versions_len); i += 2) {
         std::uint16_t version = (buf[i] << 8) | buf[i + 1];
         if (!JA4::is_GREASE(version) && version > max_version) {
           max_version = version;
         }
       }
   ```



##########
plugins/experimental/jax_fingerprint/plugin.cc:
##########
@@ -0,0 +1,444 @@
+/** @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:
+      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;
+      }
+    }
+  }
+
+  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);
+    break;
+  case TS_EVENT_HTTP_TXN_CLOSE:
+    ret = handle_http_txn_close(edata, *config);
+    break;
+  case TS_EVENT_VCONN_CLOSE:
+    ret = handle_vconn_close(edata, *config);
+    break;
+  default:
+    Dbg(dbg_ctl, "Unexpected event %d.", event);
+    // We ignore the event, but we don't want to reject the connection.
+    ret = TS_SUCCESS;
+  }
+
+  return ret;
+}
+
+void
+TSPluginInit(int argc, char const **argv)
+{
+  TSPluginRegistrationInfo info;
+  info.plugin_name   = PLUGIN_NAME;
+  info.vendor_name   = PLUGIN_VENDOR;
+  info.support_email = PLUGIN_SUPPORT_EMAIL;
+
+  if (TS_SUCCESS != TSPluginRegister(&info)) {
+    TSError("[%s] Failed to register.", PLUGIN_NAME);
+    return;
+  }
+
+  PluginConfig *config = new PluginConfig();
+  config->plugin_type  = PluginType::GLOBAL;
+
+  if (!read_config_option(argc, argv, *config)) {
+    TSError("[%s] Failed to parse options.", PLUGIN_NAME);
+    return;
+  }
+
+  if (!config->log_filename.empty()) {
+    if (!create_log_file(config->log_filename, config->log_handle)) {
+      TSError("[%s] Failed to create log.", PLUGIN_NAME);

Review Comment:
   `TSPluginInit()` allocates `PluginConfig` and then returns early on 
option-parse failure or log creation failure without deleting `config` (and 
without cleaning up any created log handle). This leaks on startup 
misconfiguration; consider deleting `config` (and destroying the log object if 
created) on all failure paths before returning.
   ```suggestion
       TSError("[%s] Failed to parse options.", PLUGIN_NAME);
       delete config;
       return;
     }
   
     if (!config->log_filename.empty()) {
       if (!create_log_file(config->log_filename, config->log_handle)) {
         TSError("[%s] Failed to create log.", PLUGIN_NAME);
         delete config;
   ```



##########
plugins/experimental/jax_fingerprint/ja4/ja4.cc:
##########
@@ -0,0 +1,175 @@
+/** @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 "ja4.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <iterator>
+#include <string>
+#include <string_view>
+
+static char        convert_protocol_to_char(JA4::Protocol protocol);
+static std::string convert_TLS_version_to_string(std::uint16_t TLS_version);
+static char        convert_SNI_to_char(JA4::SNI SNI_type);
+static std::string convert_count_to_two_digit_string(std::size_t count);
+static std::string convert_ALPN_to_two_char_string(std::string_view ALPN);
+static void        remove_trailing_character(std::string &s);
+static std::string hexify(std::uint16_t n);
+
+namespace
+{
+constexpr std::size_t U16_HEX_BUF_SIZE{4};
+} // end anonymous namespace
+
+std::string
+JA4::make_JA4_a_raw(TLSClientHelloSummary const &TLS_summary)
+{
+  std::string result;
+  result.reserve(9);
+  result.push_back(convert_protocol_to_char(TLS_summary.protocol));
+  result.append(convert_TLS_version_to_string(TLS_summary.TLS_version));
+  result.push_back(convert_SNI_to_char(TLS_summary.get_SNI_type()));
+  
result.append(convert_count_to_two_digit_string(TLS_summary.get_cipher_count()));
+  
result.append(convert_count_to_two_digit_string(TLS_summary.get_extension_count()));
+  result.append(convert_ALPN_to_two_char_string(TLS_summary.ALPN));
+  return result;
+}
+
+static char
+convert_protocol_to_char(JA4::Protocol protocol)
+{
+  return static_cast<char>(protocol);
+}
+
+static std::string
+convert_TLS_version_to_string(std::uint16_t TLS_version)
+{
+  switch (TLS_version) {
+  case 0x304:
+    return "13";
+  case 0x303:
+    return "12";
+  case 0x302:
+    return "11";
+  case 0x301:
+    return "10";
+  case 0x300:
+    return "s3";
+  case 0x200:
+    return "s2";
+  case 0x100:
+    return "s1";
+  case 0xfeff:
+    return "d1";
+  case 0xfefd:
+    return "d2";
+  case 0xfefc:
+    return "d3";
+  default:
+    return "00";
+  }
+}
+
+static char
+convert_SNI_to_char(JA4::SNI SNI_type)
+{
+  return static_cast<char>(SNI_type);
+}
+
+static std::string
+convert_count_to_two_digit_string(std::size_t count)
+{
+  std::string result;
+  if (count <= 9) {
+    result.push_back('0');
+  }
+  // We could also clamp the lower bound to 1 since there must be at least 1
+  // cipher, but 0 is more helpful for debugging if the cipher list is empty.
+  result.append(std::to_string(std::clamp(count, std::size_t{0}, 
std::size_t{99})));
+  return result;
+}
+
+std::string
+convert_ALPN_to_two_char_string(std::string_view ALPN)
+{
+  std::string result;
+  if (ALPN.empty()) {
+    result = "00";
+  } else {
+    result.push_back(ALPN.front());
+    result.push_back(ALPN.back());
+  }
+  return result;
+}
+
+std::string
+JA4::make_JA4_b_raw(TLSClientHelloSummary const &TLS_summary)
+{
+  std::string result;
+  result.reserve(12);
+  std::vector temp = TLS_summary.get_ciphers();
+  std::sort(temp.begin(), temp.end());
+
+  for (auto cipher : temp) {
+    result.append(hexify(cipher));
+    result.push_back(',');
+  }
+  remove_trailing_character(result);
+  return result;
+}
+
+std::string
+JA4::make_JA4_c_raw(TLSClientHelloSummary const &TLS_summary)
+{
+  std::string result;
+  result.reserve(12);
+  std::vector temp = TLS_summary.get_extensions();
+  std::sort(temp.begin(), temp.end());
+
+  for (auto extension : temp) {
+    result.append(hexify(extension));
+    result.push_back(',');
+  }
+  remove_trailing_character(result);
+  return result;
+}
+
+void
+remove_trailing_character(std::string &s)
+{
+  if (!s.empty()) {
+    s.pop_back();
+  }
+}
+
+std::string
+hexify(std::uint16_t n)
+{
+  char result[U16_HEX_BUF_SIZE + 1]{};
+  std::snprintf(result, sizeof(result), "%.4x", n);
+  return result;
+}

Review Comment:
   Same linkage issue as above: `remove_trailing_character` and `hexify` are 
declared `static` at file scope but defined without `static`, which will cause 
a compile error. Ensure the definitions match the `static` declarations (or 
drop `static` consistently).



##########
plugins/experimental/jax_fingerprint/plugin.cc:
##########
@@ -0,0 +1,444 @@
+/** @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:
+      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;

Review Comment:
   In `Mode::KEEP`, `modify_headers()` currently does nothing even when 
`--header` / `--via-header` are configured, so the plugin will never emit 
headers in KEEP mode. If KEEP is meant to preserve an existing client header 
but still add the fingerprint when absent (as described in the PR), consider: 
only skip when the header already exists; otherwise set it like OVERWRITE 
without removing/overwriting existing duplicates.



##########
doc/admin-guide/plugins/jax_fingerprint.en.rst:
##########
@@ -0,0 +1,179 @@
+.. 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.
+
+Fingerprints can be used for:
+
+* Client identification and tracking
+* Bot detection and mitigation
+* Security analytics and threat intelligence
+* Understanding client implementation patterns
+
+
+Plugin Configuration
+====================
+
+You can use the plugin as a global plugin, a remap plugin, or both.
+
+To use the plugin as a global plugin, add the following line to 
:file:`plugin.config`::
+
+    jax_fingerprint.so --standalone
+
+To use the plugin as a remap plugin, append the following line to a remap rule 
on :file:`remap.config`::
+
+    @plugin=jax_fingerprint.so @pparam=--standalone
+
+To use the plugin as both global and remap plugin (hybrid setup), have the 
both without `--standalone` option.

Review Comment:
   Docs: “To use the plugin as both global and remap plugin (hybrid setup), 
have the both …” is ungrammatical and hard to understand. Consider rephrasing 
to clearly describe the hybrid setup (e.g., load globally without 
`--standalone` and add as remap without `--standalone`).
   ```suggestion
   To use the plugin in a hybrid setup (both global and remap), configure it in 
both :file:`plugin.config` and :file:`remap.config` without the `--standalone` 
option.
   ```



-- 
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