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]