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


##########
plugins/experimental/jax_fingerprint/userarg.cc:
##########
@@ -26,95 +26,235 @@
 #include "fingerprint_registry.h"
 
 #include <cstring>
+#include <vector>
 
 namespace
 {
 
+// All plugin instances share this slot. It is deliberately not named after 
--export or described with
+// REGISTRY_DESCRIPTION, so other plugins do not mistake it for the 
fingerprint registry.
+constexpr char SESSION_ARG_NAME[]        = "jax_fingerprint.log_field";
+constexpr char SESSION_ARG_DESCRIPTION[] = "jax_fingerprint private 
--log-field fingerprint copies";
+
+/** The fingerprints the --log-field callbacks of all plugin instances saved 
on a session. */
+class SessionFingerprints
+{
+public:
+  void
+  set(std::string_view method_name, std::string_view fingerprint, 
std::optional<uint64_t> txn_id)
+  {
+    Entry *entry = find(method_name);
+    if (entry == nullptr) {
+      entry              = &_entries.emplace_back();
+      entry->method_name = method_name;
+    }
+    entry->fingerprint = fingerprint;
+    entry->txn_id      = txn_id;
+  }
+
+  std::string const *
+  get(std::string_view method_name, std::optional<uint64_t> txn_id) const
+  {
+    for (auto const &entry : _entries) {
+      if (entry.method_name == method_name) {
+        return entry.txn_id == txn_id ? &entry.fingerprint : nullptr;
+      }
+    }
+    return nullptr;
+  }
+
+  /** Remove the copy for @a method_name.
+   *
+   * @return true if no copies remain.
+   */
+  bool
+  remove(std::string_view method_name)
+  {
+    std::erase_if(_entries, [method_name](Entry const &entry) { return 
entry.method_name == method_name; });
+    return _entries.empty();
+  }
+
+private:
+  struct Entry {
+    // References a PluginConfig method name, which outlives every session.
+    std::string_view        method_name;
+    std::string             fingerprint;
+    std::optional<uint64_t> txn_id;
+  };
+
+  Entry *
+  find(std::string_view method_name)
+  {
+    for (auto &entry : _entries) {
+      if (entry.method_name == method_name) {
+        return &entry;
+      }
+    }
+    return nullptr;
+  }
+
+  std::vector<Entry> _entries;
+};
+
 char const *
 user_arg_type_name(TSUserArgType type)
 {
   switch (type) {
   case TS_USER_ARGS_VCONN:
     return "vconn";
+  case TS_USER_ARGS_SSN:
+    return "ssn";
   case TS_USER_ARGS_TXN:
     return "txn";
   default:
     return "unknown";
   }
 }
 
-} // anonymous namespace
-
 int
-reserve_user_arg(PluginConfig &config)
+reserve_slot(TSUserArgType type, PluginConfig const &config, int &index)
 {
-  TSUserArgType type = config.method.type == Method::Type::CONNECTION_BASED ? 
TS_USER_ARGS_VCONN : TS_USER_ARGS_TXN;
-  const char   *name = config.export_name.empty() ? PLUGIN_NAME : 
config.export_name.c_str();
+  const char *name = config.export_name.empty() ? PLUGIN_NAME : 
config.export_name.c_str();
 
   const char *description = nullptr;
-  if (TSUserArgIndexNameLookup(type, name, &config.user_arg_index, 
&description) == TS_SUCCESS) {
+  if (TSUserArgIndexNameLookup(type, name, &index, &description) == 
TS_SUCCESS) {
     if (description == nullptr || std::strcmp(description, 
jax_fingerprint::REGISTRY_DESCRIPTION) != 0) {
       TSError("[%s] User arg '%s' is already reserved with an incompatible 
data contract", PLUGIN_NAME, name);
       return TS_ERROR;
     }
-  } else if (TSUserArgIndexReserve(type, name, 
jax_fingerprint::REGISTRY_DESCRIPTION, &config.user_arg_index) == TS_SUCCESS) {
-    Dbg(dbg_ctl, "Reserved shared user_arg slot: type=%s, name=%s, index=%d", 
user_arg_type_name(type), name,
-        config.user_arg_index);
+  } else if (TSUserArgIndexReserve(type, name, 
jax_fingerprint::REGISTRY_DESCRIPTION, &index) == TS_SUCCESS) {
+    Dbg(dbg_ctl, "Reserved shared user_arg slot: type=%s, name=%s, index=%d", 
user_arg_type_name(type), name, index);
   } else {
     Dbg(dbg_ctl, "Failed to reserve shared user_arg slot: type=%s, name=%s", 
user_arg_type_name(type), name);
     return TS_ERROR;
   }
 
   Dbg(dbg_ctl, "Using shared user_arg: type=%s, name=%s, method=%.*s, 
index=%d", user_arg_type_name(type), name,
-      static_cast<int>(config.method.name.size()), config.method.name.data(), 
config.user_arg_index);
+      static_cast<int>(config.method.name.size()), config.method.name.data(), 
index);
   return TS_SUCCESS;
 }
 
+ContextMap *
+get_map(void *container, int index)
+{
+  auto *registry = static_cast<jax_fingerprint::RegistryV1 
*>(TSUserArgGet(container, index));
+  return registry == nullptr ? nullptr : static_cast<ContextMap *>(registry);
+}
+
 void
-set_user_arg(void *container, PluginConfig &config, JAxContext *ctx)
+set_context(void *container, int index, std::string_view method_name, 
JAxContext *ctx)
 {
-  auto       *registry = static_cast<jax_fingerprint::RegistryV1 
*>(TSUserArgGet(container, config.user_arg_index));
-  ContextMap *map      = registry == nullptr ? nullptr : 
static_cast<ContextMap *>(registry);
+  ContextMap *map = get_map(container, index);
   if (map == nullptr) {
     map = new ContextMap();
-    TSUserArgSet(container, config.user_arg_index, 
static_cast<jax_fingerprint::RegistryV1 *>(map));
+    TSUserArgSet(container, index, static_cast<jax_fingerprint::RegistryV1 
*>(map));
   }
-  map->set(config.method.name, ctx);
+  map->set(method_name, ctx);
 }
 
 JAxContext *
-get_user_arg(void *container, PluginConfig &config)
+get_context(void *container, int index, std::string_view method_name)
 {
-  auto       *registry = static_cast<jax_fingerprint::RegistryV1 
*>(TSUserArgGet(container, config.user_arg_index));
-  ContextMap *map      = registry == nullptr ? nullptr : 
static_cast<ContextMap *>(registry);
-  if (map == nullptr) {
-    return nullptr;
+  ContextMap *map = get_map(container, index);
+  return map == nullptr ? nullptr : map->get(method_name);
+}
+
+void
+cleanup_context(void *container, int index, std::string_view method_name)
+{
+  ContextMap *map = get_map(container, index);
+  if (map != nullptr) {
+    // Remove this plugin's context from the map.
+    map->remove(method_name);
+
+    // If the map is now empty, delete it and clear the user arg.
+    if (map->empty()) {
+      delete map;
+      TSUserArgSet(container, index, nullptr);
+    }
   }
-  return map->get(config.method.name);
+}
+
+} // anonymous namespace
+
+int
+reserve_user_arg(PluginConfig &config)
+{
+  TSUserArgType type = config.method.type == Method::Type::CONNECTION_BASED ? 
TS_USER_ARGS_VCONN : TS_USER_ARGS_TXN;
+  return reserve_slot(type, config, config.user_arg_index);
+}
+
+void
+set_user_arg(void *container, PluginConfig &config, JAxContext *ctx)
+{
+  set_context(container, config.user_arg_index, config.method.name, ctx);
+}
+
+JAxContext *
+get_user_arg(void *container, PluginConfig &config)
+{
+  return get_context(container, config.user_arg_index, config.method.name);
 }
 
 void
 refresh_user_arg(void *container, PluginConfig &config)
 {
-  auto *registry = static_cast<jax_fingerprint::RegistryV1 
*>(TSUserArgGet(container, config.user_arg_index));
-  if (registry != nullptr) {
-    static_cast<ContextMap *>(registry)->refresh(config.method.name);
+  if (ContextMap *map = get_map(container, config.user_arg_index); map != 
nullptr) {
+    map->refresh(config.method.name);
   }
 }
 
 void
 cleanup_user_arg(void *container, PluginConfig &config)
 {
-  auto       *registry = static_cast<jax_fingerprint::RegistryV1 
*>(TSUserArgGet(container, config.user_arg_index));
-  ContextMap *map      = registry == nullptr ? nullptr : 
static_cast<ContextMap *>(registry);
-  if (map != nullptr) {
-    // Remove this plugin's context from the map.
-    map->remove(config.method.name);
+  cleanup_context(container, config.user_arg_index, config.method.name);
+}
 
-    // If the map is now empty, delete it and clear the user arg.
-    if (map->empty()) {
-      delete map;
-      TSUserArgSet(container, config.user_arg_index, nullptr);
+int
+reserve_session_user_arg(PluginConfig &config)
+{
+  int        &index       = config.session_user_arg_index;
+  const char *description = nullptr;
+
+  if (TSUserArgIndexNameLookup(TS_USER_ARGS_SSN, SESSION_ARG_NAME, &index, 
&description) == TS_SUCCESS) {
+    if (description == nullptr || std::strcmp(description, 
SESSION_ARG_DESCRIPTION) != 0) {
+      TSError("[%s] User arg '%s' is already reserved by another plugin", 
PLUGIN_NAME, SESSION_ARG_NAME);
+      return TS_ERROR;
     }
+  } else if (TSUserArgIndexReserve(TS_USER_ARGS_SSN, SESSION_ARG_NAME, 
SESSION_ARG_DESCRIPTION, &index) != TS_SUCCESS) {
+    Dbg(dbg_ctl, "Failed to reserve the session user_arg slot: name=%s", 
SESSION_ARG_NAME);
+    return TS_ERROR;
+  }
+
+  Dbg(dbg_ctl, "Using session user_arg: name=%s, method=%.*s, index=%d", 
SESSION_ARG_NAME,
+      static_cast<int>(config.method.name.size()), config.method.name.data(), 
index);
+  return TS_SUCCESS;
+}
+
+void
+save_session_fingerprint(TSHttpSsn ssnp, PluginConfig &config, 
std::string_view fingerprint, std::optional<uint64_t> txn_id)
+{
+  auto *copies = static_cast<SessionFingerprints *>(TSUserArgGet(ssnp, 
config.session_user_arg_index));
+  if (copies == nullptr) {
+    copies = new SessionFingerprints();
+    TSUserArgSet(ssnp, config.session_user_arg_index, copies);

Review Comment:
   With JA4H `--log-field` over HTTP/3, each session allocates this cache, but 
its only cleanup hook, SSN_CLOSE, is never dispatched by `HQSession`. 
`Http3App` deletes the session directly without freeing user-arg payloads, so 
the cache and its strings leak across closed connections. Provide an HTTP/3 
teardown path, or avoid allocating these copies on HTTP/3 until cleanup is 
supported.



##########
plugins/experimental/jax_fingerprint/plugin.cc:
##########
@@ -447,6 +524,12 @@ TSPluginInit(int argc, char const **argv)
   } else {
     TSHttpHookAdd(TS_HTTP_TXN_CLOSE_HOOK, cont);
   }
+  if (config->session_user_arg_index != -1) {
+    if (config->method.type == Method::Type::CONNECTION_BASED) {
+      TSHttpHookAdd(TS_HTTP_SSN_START_HOOK, cont);

Review Comment:
   Registering this global SSN_START hook makes HTTP/3 connections enter a 
callout path with a null session mutex, so enabling JA3/JA4 `--log-field` can 
crash ATS. `HQSession::new_connection()` dispatches the hook without 
initializing the mutex, and `ProxySession::state_api_callout()` dereferences it 
at `src/proxy/ProxySession.cc:127`. Initialize the HTTP/3 session mutex before 
dispatching session hooks.



##########
plugins/experimental/jax_fingerprint/plugin.cc:
##########
@@ -310,11 +310,82 @@ handle_read_request_hdr(void *edata, PluginConfig &config)
   return TS_SUCCESS;
 }
 
+int
+handle_ssn_start(void *edata, PluginConfig &config)
+{
+  TSHttpSsn ssnp = static_cast<TSHttpSsn>(edata);
+
+  if (TSVConn vconn = TSHttpSsnClientVConnGet(ssnp); vconn != nullptr) {
+    if (JAxContext *ctx = get_user_arg(vconn, config); ctx != nullptr) {
+      save_session_fingerprint(ssnp, config, ctx->get_fingerprint());
+    }
+  }
+
+  TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
+  return TS_SUCCESS;
+}
+
+int
+handle_ssn_close(void *edata, PluginConfig &config)
+{
+  TSHttpSsn ssnp = static_cast<TSHttpSsn>(edata);
+
+  // HQSession never fires SSN_CLOSE, so on HTTP/3 the copy saved by the last 
transaction is not freed.
+  cleanup_session_user_arg(ssnp, config);
+
+  TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
+  return TS_SUCCESS;
+}
+
+/** Find the fingerprint the --log-field callback logs for a transaction.
+ *
+ * @param[in] txnp The transaction being logged.
+ * @param[in] config The plugin configuration.
+ * @return The fingerprint, or nullptr if none was generated for the 
transaction.
+ */
+std::string const *
+get_log_fingerprint(TSHttpTxn txnp, PluginConfig &config)
+{
+  TSHttpSsn ssnp = TSHttpTxnSsnGet(txnp);
+
+  if (config.method.type != Method::Type::CONNECTION_BASED) {
+    if (JAxContext *ctx = get_user_arg(txnp, config); ctx != nullptr) {
+      return &ctx->get_fingerprint();
+    }
+    if (ssnp == nullptr) {
+      return nullptr;
+    }
+    // The session copy may belong to another transaction: an earlier 
keep-alive transaction when this one never
+    // got a fingerprint, or, on HTTP/2, a later stream that closed first.
+    return get_session_fingerprint(ssnp, config, TSHttpTxnIdGet(txnp));
+  }
+
+  if (ssnp == nullptr) {
+    return nullptr;
+  }
+  // The client vconn is gone if ATS closed the client connection before 
logging the transaction, as it does
+  // for Connection: close requests and client timeouts.
+  if (TSVConn vconn = TSHttpSsnClientVConnGet(ssnp); vconn != nullptr) {
+    if (JAxContext *ctx = get_user_arg(vconn, config); ctx != nullptr) {
+      return &ctx->get_fingerprint();
+    }
+  }
+  return get_session_fingerprint(ssnp, config);
+}
+
 int
 handle_http_txn_close(void *edata, PluginConfig &config)
 {
   TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
 
+  // ATS logs the transaction after its TXN_CLOSE hooks, so keep a copy for 
the --log-field callback.
+  if (config.session_user_arg_index != -1) {
+    JAxContext *ctx  = get_user_arg(txnp, config);

Review Comment:
   If a JA4H instance without `--log-field` is loaded before one with logging 
enabled and both share the same export registry, the first TXN_CLOSE hook 
deletes their shared context without saving it. The logging instance then has 
nothing to copy, so its field always logs `-`. Preserve contexts needed by 
registered log fields before the first shared-context cleanup, regardless of 
which instance enabled logging.



##########
plugins/experimental/jax_fingerprint/userarg.cc:
##########
@@ -26,95 +26,235 @@
 #include "fingerprint_registry.h"
 
 #include <cstring>
+#include <vector>
 
 namespace
 {
 
+// All plugin instances share this slot. It is deliberately not named after 
--export or described with
+// REGISTRY_DESCRIPTION, so other plugins do not mistake it for the 
fingerprint registry.
+constexpr char SESSION_ARG_NAME[]        = "jax_fingerprint.log_field";
+constexpr char SESSION_ARG_DESCRIPTION[] = "jax_fingerprint private 
--log-field fingerprint copies";
+
+/** The fingerprints the --log-field callbacks of all plugin instances saved 
on a session. */
+class SessionFingerprints
+{
+public:
+  void
+  set(std::string_view method_name, std::string_view fingerprint, 
std::optional<uint64_t> txn_id)
+  {
+    Entry *entry = find(method_name);
+    if (entry == nullptr) {
+      entry              = &_entries.emplace_back();
+      entry->method_name = method_name;
+    }
+    entry->fingerprint = fingerprint;
+    entry->txn_id      = txn_id;
+  }
+
+  std::string const *
+  get(std::string_view method_name, std::optional<uint64_t> txn_id) const
+  {
+    for (auto const &entry : _entries) {
+      if (entry.method_name == method_name) {
+        return entry.txn_id == txn_id ? &entry.fingerprint : nullptr;
+      }
+    }
+    return nullptr;
+  }
+
+  /** Remove the copy for @a method_name.
+   *
+   * @return true if no copies remain.
+   */
+  bool
+  remove(std::string_view method_name)
+  {
+    std::erase_if(_entries, [method_name](Entry const &entry) { return 
entry.method_name == method_name; });
+    return _entries.empty();
+  }
+
+private:
+  struct Entry {
+    // References a PluginConfig method name, which outlives every session.
+    std::string_view        method_name;
+    std::string             fingerprint;
+    std::optional<uint64_t> txn_id;
+  };
+
+  Entry *
+  find(std::string_view method_name)
+  {
+    for (auto &entry : _entries) {
+      if (entry.method_name == method_name) {
+        return &entry;
+      }
+    }
+    return nullptr;
+  }
+
+  std::vector<Entry> _entries;
+};
+
 char const *
 user_arg_type_name(TSUserArgType type)
 {
   switch (type) {
   case TS_USER_ARGS_VCONN:
     return "vconn";
+  case TS_USER_ARGS_SSN:
+    return "ssn";
   case TS_USER_ARGS_TXN:
     return "txn";
   default:
     return "unknown";
   }
 }
 
-} // anonymous namespace
-
 int
-reserve_user_arg(PluginConfig &config)
+reserve_slot(TSUserArgType type, PluginConfig const &config, int &index)
 {
-  TSUserArgType type = config.method.type == Method::Type::CONNECTION_BASED ? 
TS_USER_ARGS_VCONN : TS_USER_ARGS_TXN;
-  const char   *name = config.export_name.empty() ? PLUGIN_NAME : 
config.export_name.c_str();
+  const char *name = config.export_name.empty() ? PLUGIN_NAME : 
config.export_name.c_str();
 
   const char *description = nullptr;
-  if (TSUserArgIndexNameLookup(type, name, &config.user_arg_index, 
&description) == TS_SUCCESS) {
+  if (TSUserArgIndexNameLookup(type, name, &index, &description) == 
TS_SUCCESS) {
     if (description == nullptr || std::strcmp(description, 
jax_fingerprint::REGISTRY_DESCRIPTION) != 0) {
       TSError("[%s] User arg '%s' is already reserved with an incompatible 
data contract", PLUGIN_NAME, name);
       return TS_ERROR;
     }
-  } else if (TSUserArgIndexReserve(type, name, 
jax_fingerprint::REGISTRY_DESCRIPTION, &config.user_arg_index) == TS_SUCCESS) {
-    Dbg(dbg_ctl, "Reserved shared user_arg slot: type=%s, name=%s, index=%d", 
user_arg_type_name(type), name,
-        config.user_arg_index);
+  } else if (TSUserArgIndexReserve(type, name, 
jax_fingerprint::REGISTRY_DESCRIPTION, &index) == TS_SUCCESS) {
+    Dbg(dbg_ctl, "Reserved shared user_arg slot: type=%s, name=%s, index=%d", 
user_arg_type_name(type), name, index);
   } else {
     Dbg(dbg_ctl, "Failed to reserve shared user_arg slot: type=%s, name=%s", 
user_arg_type_name(type), name);
     return TS_ERROR;
   }
 
   Dbg(dbg_ctl, "Using shared user_arg: type=%s, name=%s, method=%.*s, 
index=%d", user_arg_type_name(type), name,
-      static_cast<int>(config.method.name.size()), config.method.name.data(), 
config.user_arg_index);
+      static_cast<int>(config.method.name.size()), config.method.name.data(), 
index);
   return TS_SUCCESS;
 }
 
+ContextMap *
+get_map(void *container, int index)
+{
+  auto *registry = static_cast<jax_fingerprint::RegistryV1 
*>(TSUserArgGet(container, index));
+  return registry == nullptr ? nullptr : static_cast<ContextMap *>(registry);
+}
+
 void
-set_user_arg(void *container, PluginConfig &config, JAxContext *ctx)
+set_context(void *container, int index, std::string_view method_name, 
JAxContext *ctx)
 {
-  auto       *registry = static_cast<jax_fingerprint::RegistryV1 
*>(TSUserArgGet(container, config.user_arg_index));
-  ContextMap *map      = registry == nullptr ? nullptr : 
static_cast<ContextMap *>(registry);
+  ContextMap *map = get_map(container, index);
   if (map == nullptr) {
     map = new ContextMap();
-    TSUserArgSet(container, config.user_arg_index, 
static_cast<jax_fingerprint::RegistryV1 *>(map));
+    TSUserArgSet(container, index, static_cast<jax_fingerprint::RegistryV1 
*>(map));
   }
-  map->set(config.method.name, ctx);
+  map->set(method_name, ctx);
 }
 
 JAxContext *
-get_user_arg(void *container, PluginConfig &config)
+get_context(void *container, int index, std::string_view method_name)
 {
-  auto       *registry = static_cast<jax_fingerprint::RegistryV1 
*>(TSUserArgGet(container, config.user_arg_index));
-  ContextMap *map      = registry == nullptr ? nullptr : 
static_cast<ContextMap *>(registry);
-  if (map == nullptr) {
-    return nullptr;
+  ContextMap *map = get_map(container, index);
+  return map == nullptr ? nullptr : map->get(method_name);
+}
+
+void
+cleanup_context(void *container, int index, std::string_view method_name)
+{
+  ContextMap *map = get_map(container, index);
+  if (map != nullptr) {
+    // Remove this plugin's context from the map.
+    map->remove(method_name);
+
+    // If the map is now empty, delete it and clear the user arg.
+    if (map->empty()) {
+      delete map;
+      TSUserArgSet(container, index, nullptr);
+    }
   }
-  return map->get(config.method.name);
+}
+
+} // anonymous namespace
+
+int
+reserve_user_arg(PluginConfig &config)
+{
+  TSUserArgType type = config.method.type == Method::Type::CONNECTION_BASED ? 
TS_USER_ARGS_VCONN : TS_USER_ARGS_TXN;
+  return reserve_slot(type, config, config.user_arg_index);
+}
+
+void
+set_user_arg(void *container, PluginConfig &config, JAxContext *ctx)
+{
+  set_context(container, config.user_arg_index, config.method.name, ctx);
+}
+
+JAxContext *
+get_user_arg(void *container, PluginConfig &config)
+{
+  return get_context(container, config.user_arg_index, config.method.name);
 }
 
 void
 refresh_user_arg(void *container, PluginConfig &config)
 {
-  auto *registry = static_cast<jax_fingerprint::RegistryV1 
*>(TSUserArgGet(container, config.user_arg_index));
-  if (registry != nullptr) {
-    static_cast<ContextMap *>(registry)->refresh(config.method.name);
+  if (ContextMap *map = get_map(container, config.user_arg_index); map != 
nullptr) {
+    map->refresh(config.method.name);
   }
 }
 
 void
 cleanup_user_arg(void *container, PluginConfig &config)
 {
-  auto       *registry = static_cast<jax_fingerprint::RegistryV1 
*>(TSUserArgGet(container, config.user_arg_index));
-  ContextMap *map      = registry == nullptr ? nullptr : 
static_cast<ContextMap *>(registry);
-  if (map != nullptr) {
-    // Remove this plugin's context from the map.
-    map->remove(config.method.name);
+  cleanup_context(container, config.user_arg_index, config.method.name);
+}
 
-    // If the map is now empty, delete it and clear the user arg.
-    if (map->empty()) {
-      delete map;
-      TSUserArgSet(container, config.user_arg_index, nullptr);
+int
+reserve_session_user_arg(PluginConfig &config)
+{
+  int        &index       = config.session_user_arg_index;
+  const char *description = nullptr;
+
+  if (TSUserArgIndexNameLookup(TS_USER_ARGS_SSN, SESSION_ARG_NAME, &index, 
&description) == TS_SUCCESS) {
+    if (description == nullptr || std::strcmp(description, 
SESSION_ARG_DESCRIPTION) != 0) {
+      TSError("[%s] User arg '%s' is already reserved by another plugin", 
PLUGIN_NAME, SESSION_ARG_NAME);
+      return TS_ERROR;
     }
+  } else if (TSUserArgIndexReserve(TS_USER_ARGS_SSN, SESSION_ARG_NAME, 
SESSION_ARG_DESCRIPTION, &index) != TS_SUCCESS) {
+    Dbg(dbg_ctl, "Failed to reserve the session user_arg slot: name=%s", 
SESSION_ARG_NAME);
+    return TS_ERROR;
+  }
+
+  Dbg(dbg_ctl, "Using session user_arg: name=%s, method=%.*s, index=%d", 
SESSION_ARG_NAME,
+      static_cast<int>(config.method.name.size()), config.method.name.data(), 
index);
+  return TS_SUCCESS;
+}
+
+void
+save_session_fingerprint(TSHttpSsn ssnp, PluginConfig &config, 
std::string_view fingerprint, std::optional<uint64_t> txn_id)
+{
+  auto *copies = static_cast<SessionFingerprints *>(TSUserArgGet(ssnp, 
config.session_user_arg_index));
+  if (copies == nullptr) {
+    copies = new SessionFingerprints();
+    TSUserArgSet(ssnp, config.session_user_arg_index, copies);
+  }
+  copies->set(config.method.name, fingerprint, txn_id);

Review Comment:
   The shared session cache is keyed only by method, losing the isolation 
provided by `--export`. With two JA4 instances using distinct export names and 
disjoint `--servernames` filters, the nonmatching instance's log field 
retrieves the matching instance's cached fingerprint instead of `-`. Include 
`config.user_arg_index` alongside the method in the cache's save, lookup, and 
removal keys.



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