You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by rr...@apache.org on 2020/05/08 15:17:00 UTC
[trafficserver] branch master updated: traffic_dump: refactor to
make transactions atomically written
This is an automated email from the ASF dual-hosted git repository.
rrm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 59ebfd8 traffic_dump: refactor to make transactions atomically written
59ebfd8 is described below
commit 59ebfd82a67bfdbe323c21cf304d08345540e461
Author: bneradt <bn...@verizonmedia.com>
AuthorDate: Fri May 1 18:55:18 2020 +0000
traffic_dump: refactor to make transactions atomically written
This refactor is occasioned by the need to make transaction writes
atomic. Mainly, this encapsulates session and transaction handling
in SessionData and TransactionData classes.
---
.gitignore | 1 +
plugins/experimental/traffic_dump/Makefile.inc | 28 +-
.../experimental/traffic_dump/global_variables.h | 25 +
plugins/experimental/traffic_dump/json_utils.cc | 178 ++++
plugins/experimental/traffic_dump/json_utils.h | 58 ++
.../experimental/traffic_dump/sensitive_fields.h | 54 ++
plugins/experimental/traffic_dump/session_data.cc | 486 +++++++++++
plugins/experimental/traffic_dump/session_data.h | 182 ++++
plugins/experimental/traffic_dump/traffic_dump.cc | 929 ++-------------------
.../experimental/traffic_dump/transaction_data.cc | 358 ++++++++
.../experimental/traffic_dump/transaction_data.h | 110 +++
.../traffic_dump/unit_tests/test_json_utils.cc | 59 ++
.../unit_tests/test_sensitive_fields.cc | 37 +
.../traffic_dump/unit_tests/unit_test_main.cc | 25 +
.../pluginTest/traffic_dump/gold/200.gold | 2 +-
.../traffic_dump/gold/4_byte_response_body.gold | 7 +
.../traffic_dump/gold/two_transactions.gold | 11 +
.../pluginTest/traffic_dump/traffic_dump.test.py | 125 ++-
.../pluginTest/traffic_dump/verify_replay.py | 6 +-
tests/tools/lib/replay_schema.json | 2 +-
20 files changed, 1796 insertions(+), 887 deletions(-)
diff --git a/.gitignore b/.gitignore
index 6642e9a..f03cfb4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -131,6 +131,7 @@ plugins/experimental/esi/*_test
plugins/experimental/slice/test_*
plugins/experimental/sslheaders/test_sslheaders
plugins/s3_auth/test_s3auth
+plugins/experimental/traffic_dump/test_*
plugins/esi/docnode_test
plugins/esi/gzip_test
diff --git a/plugins/experimental/traffic_dump/Makefile.inc b/plugins/experimental/traffic_dump/Makefile.inc
index ac87596..411a9f7 100644
--- a/plugins/experimental/traffic_dump/Makefile.inc
+++ b/plugins/experimental/traffic_dump/Makefile.inc
@@ -16,4 +16,30 @@
pkglib_LTLIBRARIES += experimental/traffic_dump/traffic_dump.la
-experimental_traffic_dump_traffic_dump_la_SOURCES = experimental/traffic_dump/traffic_dump.cc
+experimental_traffic_dump_traffic_dump_la_SOURCES = \
+ experimental/traffic_dump/global_variables.h \
+ experimental/traffic_dump/json_utils.cc \
+ experimental/traffic_dump/json_utils.h \
+ experimental/traffic_dump/sensitive_fields.h \
+ experimental/traffic_dump/session_data.cc \
+ experimental/traffic_dump/session_data.h \
+ experimental/traffic_dump/traffic_dump.cc \
+ experimental/traffic_dump/transaction_data.cc \
+ experimental/traffic_dump/transaction_data.h
+
+check_PROGRAMS += \
+ experimental/traffic_dump/test_traffic_dump
+
+experimental_traffic_dump_test_traffic_dump_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(abs_top_srcdir)/plugins/experimental/traffic_dump \
+ -I$(abs_top_srcdir)/tests/include
+
+experimental_traffic_dump_test_traffic_dump_SOURCES = \
+ experimental/traffic_dump/unit_tests/unit_test_main.cc \
+ experimental/traffic_dump/unit_tests/test_json_utils.cc \
+ experimental/traffic_dump/unit_tests/test_sensitive_fields.cc \
+ experimental/traffic_dump/json_utils.cc \
+ experimental/traffic_dump/sensitive_fields.h
+
+# vim: ft=make ts=8 sw=8 et:
diff --git a/plugins/experimental/traffic_dump/global_variables.h b/plugins/experimental/traffic_dump/global_variables.h
new file mode 100644
index 0000000..204ce20
--- /dev/null
+++ b/plugins/experimental/traffic_dump/global_variables.h
@@ -0,0 +1,25 @@
+/** @sensitive_fields.h
+ The set of fields considered user-sensitive.
+ @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
+
+namespace traffic_dump
+{
+char const constexpr *const debug_tag = "traffic_dump";
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/json_utils.cc b/plugins/experimental/traffic_dump/json_utils.cc
new file mode 100644
index 0000000..eb2ff37
--- /dev/null
+++ b/plugins/experimental/traffic_dump/json_utils.cc
@@ -0,0 +1,178 @@
+/** @json_utils.cc
+ Implementation of JSON formatting functions.
+ @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 <iomanip>
+#include <sstream>
+
+#include "json_utils.h"
+
+namespace
+{
+/** Write the content of buf to jsonfile between prevIdx (inclusive) and idx (not
+ * inclusive).
+ *
+ * This function is used to help deal with escaped characters with a low number
+ * of write calls. This is meant to be used like so: the caller inspects every
+ * character in a buffer, doing one of two things with each character:
+ *
+ * - The character does not need to be escaped. In this case, no call to
+ * write_buffered_content is made and the idx is advanced and prevIdx is not
+ * advanced. Eventually this character will be written once the contiguous set
+ * of non-escaped characters is collected.
+ *
+ * - The character needs to be escaped. In this case, it will call this
+ * function. Anything between prevIdx and up to (but not including) idx is
+ * written. The caller then writes the escaped sequence for the character
+ * pointed to by idx ("\\t" instead of '\t', for instance). Then prevIdx is set
+ * past the escaped character.
+ *
+ * Finally, once all characters are inspected, a final write_buffered_content
+ * call is made with idx set to one past the last character in buf. This
+ * results in all characters between prevIdx and the last character in buf
+ * (including that character) to be written.
+ *
+ * @param[in] buf The buffer containing characters that need to be written to jsonfile.
+ *
+ * @param[in,out] prevIdx The pointer to the beginning of the set of characters
+ * in buf that have not been written yet. This is always updated before
+ * function exit to one past idx.
+ *
+ * @param[in] idx The current character in buf being inspected.
+ *
+ * @param[out] jsonfile The stream to conditionally write buffer content to.
+ */
+inline void
+write_buffered_context(char const *buf, int64_t &prevIdx, int64_t idx, std::ostream &jsonfile)
+{
+ if (prevIdx < idx) {
+ jsonfile.write(buf + prevIdx, idx - prevIdx);
+ }
+ prevIdx = idx + 1;
+}
+
+int
+esc_json_out(const char *buf, int64_t len, std::ostream &jsonfile)
+{
+ if (buf == nullptr) {
+ return 0;
+ }
+ int64_t idx = 0, prevIdx = 0;
+ // For an explanation of the algorithm here, see the doxygen comment for
+ // write_buffered_content.
+ for (idx = 0; idx < len; idx++) {
+ char c = *(buf + idx);
+ switch (c) {
+ case '"':
+ case '\\': {
+ write_buffered_context(buf, prevIdx, idx, jsonfile);
+ jsonfile << "\\" << c;
+ break;
+ }
+ case '\b': {
+ write_buffered_context(buf, prevIdx, idx, jsonfile);
+ jsonfile << "\\b";
+ break;
+ }
+ case '\f': {
+ write_buffered_context(buf, prevIdx, idx, jsonfile);
+ jsonfile << "\\f";
+ break;
+ }
+ case '\n': {
+ write_buffered_context(buf, prevIdx, idx, jsonfile);
+ jsonfile << "\\n";
+ break;
+ }
+ case '\r': {
+ write_buffered_context(buf, prevIdx, idx, jsonfile);
+ jsonfile << "\\r";
+ break;
+ }
+ case '\t': {
+ write_buffered_context(buf, prevIdx, idx, jsonfile);
+ jsonfile << "\\t";
+ break;
+ }
+ default: {
+ if ('\x00' <= c && c <= '\x1f') {
+ write_buffered_context(buf, prevIdx, idx, jsonfile);
+ jsonfile << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(c);
+ }
+ break;
+ // else: The character does not need to be escaped. Do not call
+ // write_buffered_content so nothing is written and prevIdx remains
+ // pointing to the first character that needs to be written on the next
+ // call to write_buffered_content.
+ }
+ }
+ }
+ // Finally, call write_buffered_content to write out any data that has not
+ // been written yet.
+ write_buffered_context(buf, prevIdx, idx, jsonfile);
+
+ return len;
+}
+/** Escape characters in a string as needed and return the resultant escaped string.
+ *
+ * @param[in] s The characters that need to be escaped.
+ */
+std::string
+escape_json(std::string_view s)
+{
+ std::ostringstream o;
+ esc_json_out(s.data(), s.length(), o);
+ return o.str();
+}
+
+/** An escape_json overload for a char buffer.
+ *
+ * @param[in] buf The char buffer pointer with characters that need to be escaped.
+ *
+ * @param[in] size The size of the buf char array.
+ */
+std::string
+escape_json(char const *buf, int64_t size)
+{
+ std::ostringstream o;
+ esc_json_out(buf, size, o);
+ return o.str();
+}
+
+} // anonymous namespace
+
+namespace traffic_dump
+{
+std::string
+json_entry(std::string_view name, std::string_view value)
+{
+ return "\"" + escape_json(name) + "\":\"" + escape_json(value) + "\"";
+}
+
+std::string
+json_entry(std::string_view name, char const *value, int64_t size)
+{
+ return "\"" + escape_json(name) + "\":\"" + escape_json(value, size) + "\"";
+}
+
+std::string
+json_entry_array(std::string_view name, std::string_view value)
+{
+ return "[\"" + escape_json(name) + "\",\"" + escape_json(value) + "\"]";
+}
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/json_utils.h b/plugins/experimental/traffic_dump/json_utils.h
new file mode 100644
index 0000000..7d26012
--- /dev/null
+++ b/plugins/experimental/traffic_dump/json_utils.h
@@ -0,0 +1,58 @@
+/** @json_utils.h
+ JSON formatting functions.
+ @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 <string>
+#include <string_view>
+
+namespace traffic_dump
+{
+/** Create the name and value as an escaped JSON map entry.
+ *
+ * @param[in] name The key name for the map entry.
+ *
+ * @param[in] value The value to write.
+ *
+ * @return The JSON map string.
+ */
+std::string json_entry(std::string_view name, std::string_view value);
+
+/** Create the name and value as an escaped JSON map entry.
+ *
+ * @param[in] name The key name for the map entry.
+ *
+ * @param[in] value The buffer for the value to write.
+ *
+ * @param[in] size The size of the value buffer.
+ *
+ * @return The JSON map string.
+ */
+std::string json_entry(std::string_view name, char const *value, int64_t size);
+
+/** Create the name and value as an escaped JSON array entry.
+ *
+ * @param[in] name The key name for the map entry.
+ *
+ * @param[in] value The value to write for the JSON map entry.
+ *
+ * @return The JSON array string.
+ */
+std::string json_entry_array(std::string_view name, std::string_view value);
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/sensitive_fields.h b/plugins/experimental/traffic_dump/sensitive_fields.h
new file mode 100644
index 0000000..8e0db2d
--- /dev/null
+++ b/plugins/experimental/traffic_dump/sensitive_fields.h
@@ -0,0 +1,54 @@
+/** @sensitive_fields.h
+ Define the type used to store user-sensitive HTTP fields (such as cookies).
+ @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 <algorithm>
+#include <strings.h>
+#include <string>
+#include <string_view>
+#include <unordered_set>
+
+namespace traffic_dump
+{
+// A case-insensitive comparitor used for comparing HTTP field names.
+struct InsensitiveCompare {
+ bool
+ operator()(std::string_view a, std::string_view b) const
+ {
+ return strcasecmp(a.data(), b.data()) == 0;
+ }
+};
+
+// A case-insensitive hash functor for HTTP field names.
+struct StringHashByLower {
+public:
+ size_t
+ operator()(std::string_view str) const
+ {
+ std::string lower;
+ std::transform(str.begin(), str.end(), lower.begin(), [](unsigned char c) -> unsigned char { return std::tolower(c); });
+ return std::hash<std::string>()(lower);
+ }
+};
+
+/** The type used to store the set of user-sensitive HTTP fields, such as
+ * "Cookie" and "Set-Cookie". */
+using sensitive_fields_t = std::unordered_set<std::string, StringHashByLower, InsensitiveCompare>;
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/session_data.cc b/plugins/experimental/traffic_dump/session_data.cc
new file mode 100644
index 0000000..ffbdb50
--- /dev/null
+++ b/plugins/experimental/traffic_dump/session_data.cc
@@ -0,0 +1,486 @@
+/** @session_handler.h
+ Traffic Dump session handling implementation
+ @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 <arpa/inet.h>
+#include <chrono>
+#include <fcntl.h>
+#include <iomanip>
+#include <openssl/ssl.h>
+#include <netinet/in.h>
+#include <sstream>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "session_data.h"
+#include "global_variables.h"
+#include "transaction_data.h"
+
+namespace
+{
+/** The final string used to close a JSON session. */
+char const constexpr *const json_closing = "]}]}";
+} // namespace
+
+namespace traffic_dump
+{
+// Static member initialization.
+int SessionData::session_arg_index = -1;
+std::atomic<int64_t> SessionData::sample_pool_size = default_sample_pool_size;
+std::atomic<int64_t> SessionData::max_disk_usage = default_max_disk_usage;
+std::atomic<int64_t> SessionData::disk_usage = 0;
+ts::file::path SessionData::log_directory{default_log_directory};
+uint64_t SessionData::session_counter = 0;
+std::string SessionData::sni_filter;
+
+int
+SessionData::get_session_arg_index()
+{
+ return session_arg_index;
+}
+
+void
+SessionData::set_sample_pool_size(int64_t new_sample_size)
+{
+ sample_pool_size = new_sample_size;
+}
+
+void
+SessionData::reset_disk_usage()
+{
+ disk_usage = 0;
+}
+
+void
+SessionData::set_max_disk_usage(int64_t new_max_disk_usage)
+{
+ max_disk_usage = new_max_disk_usage;
+}
+
+bool
+SessionData::init(std::string_view log_directory, int64_t max_disk_usage, int64_t sample_size)
+{
+ SessionData::log_directory = log_directory;
+ SessionData::max_disk_usage = max_disk_usage;
+ SessionData::sample_pool_size = sample_size;
+
+ if (TS_SUCCESS != TSUserArgIndexReserve(TS_USER_ARGS_SSN, debug_tag, "Track log related data", &session_arg_index)) {
+ TSError("[%s] Unable to initialize plugin (disabled). Failed to reserve ssn arg.", traffic_dump::debug_tag);
+ return false;
+ }
+
+ TSCont ssncont = TSContCreate(global_session_handler, nullptr);
+ TSHttpHookAdd(TS_HTTP_SSN_START_HOOK, ssncont);
+ TSHttpHookAdd(TS_HTTP_SSN_CLOSE_HOOK, ssncont);
+
+ TSDebug(debug_tag, "Initialized with log directory: %s", SessionData::log_directory.c_str());
+ TSDebug(debug_tag, "Initialized with sample pool size %" PRId64 " bytes and disk limit %" PRId64 " bytes", sample_size,
+ max_disk_usage);
+ return true;
+}
+
+bool
+SessionData::init(std::string_view log_directory, int64_t max_disk_usage, int64_t sample_size, std::string_view sni_filter)
+{
+ if (!init(log_directory, max_disk_usage, sample_size)) {
+ return false;
+ }
+ SessionData::sni_filter = sni_filter;
+ TSDebug(debug_tag, "Filtering to only dump connections with SNI: %s", SessionData::sni_filter.c_str());
+ return true;
+}
+
+/** Create a TLS characteristics node.
+ *
+ * This function encapsulates the logic common between the client-side and
+ * server-side logic for populating the TLS node.
+ *
+ * @param[in] ssnp The pointer for this session.
+ *
+ * @return The node describing the TLS properties of this session.
+ */
+std::string
+get_tls_description_helper(TSVConn ssn_vc)
+{
+ TSSslConnection ssl_conn = TSVConnSslConnectionGet(ssn_vc);
+ SSL *ssl_obj = (SSL *)ssl_conn;
+ if (ssl_obj == nullptr) {
+ return "";
+ }
+ std::ostringstream tls_description;
+ tls_description << R"("tls":{)";
+ char const *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name);
+ if (sni_ptr != nullptr) {
+ std::string_view sni{sni_ptr};
+ if (!sni.empty()) {
+ tls_description << R"("sni":")" << sni << R"(",)";
+ }
+ }
+ tls_description << R"("verify_mode":")" << std::to_string(SSL_get_verify_mode(ssl_obj)) << R"(")";
+ tls_description << "}";
+ return tls_description.str();
+}
+
+/** Create a server-side TLS characteristics node.
+ *
+ * @param[in] ssnp The pointer for this session.
+ *
+ * @return The node describing the TLS properties of this session.
+ */
+std::string
+get_server_tls_description(TSHttpSsn ssnp)
+{
+ TSVConn ssn_vc = TSHttpSsnServerVConnGet(ssnp);
+ return get_tls_description_helper(ssn_vc);
+}
+
+/** Create a client-side TLS characteristics node.
+ *
+ * @param[in] ssnp The pointer for this session.
+ *
+ * @return The node describing the TLS properties of this session.
+ */
+std::string
+get_client_tls_description(TSHttpSsn ssnp)
+{
+ TSVConn ssn_vc = TSHttpSsnClientVConnGet(ssnp);
+ return get_tls_description_helper(ssn_vc);
+}
+
+/// A named boolean for callers who pass the is_client parameter.
+constexpr bool IS_CLIENT = true;
+
+/** Create the nodes that describe the session's sub-HTTP protocols.
+ *
+ * This function encapsulates the logic common between the client-side and
+ * server-side logic for describing the session's characteristics.
+ *
+ * This will create the string representing the "protocol" and "tls" nodes. The
+ * "tls" node will only be present if the connection is over SSL/TLS.
+ *
+ * @param[in] ssnp The pointer for this session.
+ *
+ * @return The description of the protocol stack and certain TLS attributes.
+ */
+std::string
+get_protocol_description_helper(TSHttpSsn ssnp, bool is_client)
+{
+ std::ostringstream protocol_description;
+ protocol_description << R"("protocol":[)";
+
+ char const *protocol[10];
+ int count = -1;
+ if (is_client) {
+ TSAssert(TS_SUCCESS == TSHttpSsnClientProtocolStackGet(ssnp, 10, protocol, &count));
+ } else {
+ // See the TODO below in the commented out defintion of get_server_protocol_description.
+ // TSAssert(TS_SUCCESS == TSHttpSsnServerProtocolStackGet(ssnp, 10, protocol, &count));
+ }
+ for (int i = 0; i < count; i++) {
+ if (i > 0) {
+ protocol_description << ",";
+ }
+ protocol_description << '"' << std::string(protocol[i]) << '"';
+ }
+
+ protocol_description << "]";
+ std::string tls_description;
+ if (is_client) {
+ tls_description = get_client_tls_description(ssnp);
+ } else {
+ tls_description = get_server_tls_description(ssnp);
+ }
+ if (!tls_description.empty()) {
+ protocol_description << "," << tls_description;
+ }
+ return protocol_description.str();
+}
+
+#if 0
+// See the TODO above the get_server_protocol_description declaration.
+//
+// It will be important to add this eventually, but
+// TSHttpSsnServerProtocolStackGet is not defined yet. Once it (or some other
+// mechanism for getting the server side stack) is implemented, we will call
+// this as a part of writing the server-response node.
+std::string
+SessionData::get_server_protocol_description(TSHttpSsn ssnp)
+{
+ return get_protocol_description_helper(ssnp, !IS_CLIENT);
+}
+#endif
+
+std::string
+SessionData::get_client_protocol_description(TSHttpSsn ssnp)
+{
+ return get_protocol_description_helper(ssnp, IS_CLIENT);
+}
+
+SessionData::SessionData()
+{
+ disk_io_mutex = TSMutexCreate();
+ aio_cont = TSContCreate(session_aio_handler, TSMutexCreate());
+ txn_cont = TSContCreate(TransactionData::global_transaction_handler, nullptr);
+}
+
+SessionData::~SessionData()
+{
+ if (disk_io_mutex) {
+ TSMutexDestroy(disk_io_mutex);
+ }
+ if (aio_cont) {
+ TSContDestroy(aio_cont);
+ }
+ if (txn_cont) {
+ TSContDestroy(txn_cont);
+ }
+}
+
+/*
+ * Note this assumes that the caller holds the disk_io_mutex lock. This is a
+ * private member function. The two publically accessible functions hold the
+ * lock before calling this.
+ */
+int
+SessionData::write_to_disk_no_lock(std::string_view content)
+{
+ char *pBuf = nullptr;
+ // Allocate a buffer for aio writing
+ if ((pBuf = static_cast<char *>(TSmalloc(sizeof(char) * content.size())))) {
+ memcpy(pBuf, content.data(), content.size());
+ if (TS_SUCCESS == TSAIOWrite(log_fd, write_offset, pBuf, content.size(), aio_cont)) {
+ // Update offset within file and aio events count
+ write_offset += content.size();
+ aio_count += 1;
+
+ TSMutexUnlock(disk_io_mutex);
+ return TS_SUCCESS;
+ }
+ TSfree(pBuf);
+ }
+ return TS_ERROR;
+}
+
+int
+SessionData::write_to_disk(std::string_view content)
+{
+ TSMutexLock(disk_io_mutex);
+ const int result = write_to_disk_no_lock(content);
+ TSMutexUnlock(disk_io_mutex);
+ return result;
+}
+
+int
+SessionData::write_transaction_to_disk(std::string_view content)
+{
+ TSMutexLock(disk_io_mutex);
+
+ int result = TS_SUCCESS;
+ if (has_written_first_transaction) {
+ // Prepend a comma.
+ std::string with_comma;
+ with_comma.reserve(content.size() + 1);
+ with_comma.insert(0, ",");
+ with_comma.insert(1, content);
+ result = write_to_disk_no_lock(with_comma);
+ } else {
+ result = write_to_disk_no_lock(content);
+ has_written_first_transaction = true;
+ }
+
+ TSMutexUnlock(disk_io_mutex);
+ return result;
+}
+
+int
+SessionData::session_aio_handler(TSCont contp, TSEvent event, void *edata)
+{
+ switch (event) {
+ case TS_EVENT_AIO_DONE: {
+ TSAIOCallback cb = static_cast<TSAIOCallback>(edata);
+ SessionData *ssnData = static_cast<SessionData *>(TSContDataGet(contp));
+ if (!ssnData) {
+ TSDebug(debug_tag, "session_aio_handler(): No valid ssnData. Abort.");
+ return TS_ERROR;
+ }
+ char *buf = TSAIOBufGet(cb);
+ TSMutexLock(ssnData->disk_io_mutex);
+
+ // Free the allocated buffer and update aio_count
+ if (buf) {
+ TSfree(buf);
+ if (--ssnData->aio_count == 0 && ssnData->ssn_closed) {
+ // check for ssn close, if closed, do clean up
+ TSContDataSet(contp, nullptr);
+ close(ssnData->log_fd);
+ TSMutexUnlock(ssnData->disk_io_mutex);
+ std::error_code ec;
+ ts::file::file_status st = ts::file::status(ssnData->log_name, ec);
+ if (!ec) {
+ disk_usage += ts::file::file_size(st);
+ TSDebug(debug_tag, "Finish a session with log file of %" PRIuMAX "bytes", ts::file::file_size(st));
+ }
+ delete ssnData;
+ return TS_SUCCESS;
+ }
+ }
+ TSMutexUnlock(ssnData->disk_io_mutex);
+ return TS_SUCCESS;
+ }
+ default:
+ TSDebug(debug_tag, "session_aio_handler(): unhandled events %d", event);
+ return TS_ERROR;
+ }
+ return TS_SUCCESS;
+}
+
+int
+SessionData::global_session_handler(TSCont contp, TSEvent event, void *edata)
+{
+ TSHttpSsn ssnp = static_cast<TSHttpSsn>(edata);
+
+ switch (event) {
+ case TS_EVENT_HTTP_SSN_START: {
+ // Grab session id for logging against a global value rather than the local
+ // session_counter.
+ int64_t id = TSHttpSsnIdGet(ssnp);
+
+ // If the user has asked for SNI filtering, filter on that first because
+ // any sampling will apply just to that subset of connections that match
+ // that SNI.
+ if (!sni_filter.empty()) {
+ TSVConn ssn_vc = TSHttpSsnClientVConnGet(ssnp);
+ TSSslConnection ssl_conn = TSVConnSslConnectionGet(ssn_vc);
+ SSL *ssl_obj = (SSL *)ssl_conn;
+ if (ssl_obj == nullptr) {
+ TSDebug(debug_tag, "global_session_handler(): Ignore non-HTTPS session %" PRId64 "...", id);
+ break;
+ }
+ char const *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name);
+ if (sni_ptr == nullptr) {
+ TSDebug(debug_tag, "global_session_handler(): Ignore HTTPS session with non-existent SNI.");
+ break;
+ } else {
+ const std::string_view sni{sni_ptr};
+ if (sni != sni_filter) {
+ TSDebug(debug_tag, "global_session_handler(): Ignore HTTPS session with non-filtered SNI: %s", sni_ptr);
+ break;
+ }
+ }
+ }
+ const auto this_session_count = session_counter++;
+ if (this_session_count % sample_pool_size != 0) {
+ TSDebug(debug_tag, "global_session_handler(): Ignore session %" PRId64 "...", id);
+ break;
+ } else if (disk_usage >= max_disk_usage) {
+ TSDebug(debug_tag, "global_session_handler(): Ignore session %" PRId64 "due to disk usage %" PRId64 "bytes", id,
+ disk_usage.load());
+ break;
+ }
+ // Beginning of a new session
+ /// Get epoch time
+ auto start = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch());
+
+ // Create new per session data
+ SessionData *ssnData = new SessionData;
+ TSUserArgSet(ssnp, session_arg_index, ssnData);
+
+ TSContDataSet(ssnData->aio_cont, ssnData);
+
+ // "protocol":(string),"tls":(string)
+ // The "tls" node will only be present if the session is over SSL/TLS.
+ std::string protocol_description = get_client_protocol_description(ssnp);
+
+ std::string beginning = R"({"meta":{"version":"1.0"},"sessions":[{)" + protocol_description + R"(,"connection-time":)" +
+ std::to_string(start.count()) + R"(,"transactions":[)";
+
+ // Use the session count's hex string as the filename.
+ std::stringstream stream;
+ stream << std::setw(16) << std::setfill('0') << std::hex << this_session_count;
+ std::string session_hex_name = stream.str();
+
+ // Use client ip as sub directory name
+ char client_str[INET6_ADDRSTRLEN];
+ sockaddr const *client_ip = TSHttpSsnClientAddrGet(ssnp);
+ if (AF_INET == client_ip->sa_family) {
+ inet_ntop(AF_INET, &(reinterpret_cast<sockaddr_in const *>(client_ip)->sin_addr), client_str, INET_ADDRSTRLEN);
+ } else if (AF_INET6 == client_ip->sa_family) {
+ inet_ntop(AF_INET6, &(reinterpret_cast<sockaddr_in6 const *>(client_ip)->sin6_addr), client_str, INET6_ADDRSTRLEN);
+ } else {
+ TSDebug(debug_tag, "global_session_handler(): Unknown address family.");
+ snprintf(client_str, INET6_ADDRSTRLEN, "unknown");
+ }
+
+ // Initialize AIO file
+ TSMutexLock(ssnData->disk_io_mutex);
+ if (ssnData->log_fd < 0) {
+ ts::file::path log_p = log_directory / ts::file::path(std::string(client_str, 3));
+ ts::file::path log_f = log_p / ts::file::path(session_hex_name);
+
+ // Create subdir if not existing
+ std::error_code ec;
+ ts::file::status(log_p, ec);
+ if (ec && mkdir(log_p.c_str(), 0755) == -1) {
+ TSDebug(debug_tag, "global_session_handler(): Failed to create dir %s", log_p.c_str());
+ TSError("[%s] Failed to create dir %s", debug_tag, log_p.c_str());
+ }
+
+ // Try to open log files for AIO
+ ssnData->log_fd = open(log_f.c_str(), O_RDWR | O_CREAT, S_IRWXU);
+ if (ssnData->log_fd < 0) {
+ TSMutexUnlock(ssnData->disk_io_mutex);
+ TSDebug(debug_tag, "global_session_handler(): Failed to open log files %s. Abort.", log_f.c_str());
+ TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
+ return TS_EVENT_HTTP_CONTINUE;
+ }
+ ssnData->log_name = log_f;
+ // Write log file beginning to disk
+ ssnData->write_to_disk(beginning);
+ }
+ TSMutexUnlock(ssnData->disk_io_mutex);
+
+ TSHttpSsnHookAdd(ssnp, TS_HTTP_TXN_START_HOOK, ssnData->txn_cont);
+ TSHttpSsnHookAdd(ssnp, TS_HTTP_TXN_CLOSE_HOOK, ssnData->txn_cont);
+ break;
+ }
+ case TS_EVENT_HTTP_SSN_CLOSE: {
+ // Write session and close the log file.
+ int64_t id = TSHttpSsnIdGet(ssnp);
+ TSDebug(debug_tag, "global_session_handler(): Closing session %" PRId64 "...", id);
+ // Retrieve SessionData
+ SessionData *ssnData = static_cast<SessionData *>(TSUserArgGet(ssnp, session_arg_index));
+ // If no valid ssnData, continue transaction as if nothing happened
+ if (!ssnData) {
+ TSDebug(debug_tag, "global_session_handler(): [TS_EVENT_HTTP_SSN_CLOSE] No ssnData found. Abort.");
+ TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
+ return TS_SUCCESS;
+ }
+ ssnData->write_to_disk(json_closing);
+ TSMutexLock(ssnData->disk_io_mutex);
+ ssnData->ssn_closed = true;
+ TSMutexUnlock(ssnData->disk_io_mutex);
+
+ break;
+ }
+ default:
+ break;
+ }
+ TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
+ return TS_SUCCESS;
+}
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/session_data.h b/plugins/experimental/traffic_dump/session_data.h
new file mode 100644
index 0000000..0094e72
--- /dev/null
+++ b/plugins/experimental/traffic_dump/session_data.h
@@ -0,0 +1,182 @@
+/** @session_handler.h
+ Traffic Dump session handling encapsulation.
+ @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 <atomic>
+#include <cstdlib>
+#include <string_view>
+
+#include "ts/ts.h"
+#include "tscore/ts_file.h"
+
+namespace traffic_dump
+{
+/** The information associated with an individual session.
+ *
+ * This class is responsible for containing the members associated with a
+ * particular session and defines the session handler callback.
+ */
+class SessionData
+{
+public:
+ /// By default, Traffic Dump logs will go into a directory called "dump".
+ static char const constexpr *const default_log_directory = "dump";
+ /// By default, 1 out of 1000 sessions will be dumped.
+ static constexpr int64_t default_sample_pool_size = 1000;
+ /// By default, logging will stop after 10 MB have been dumped.
+ static constexpr int64_t default_max_disk_usage = 10 * 1000 * 1000;
+
+private:
+ //
+ // Instance Variables
+ //
+
+ /// Log file descriptor for this session's dump file.
+ int log_fd = -1;
+ /// The count of the currently outstanding AIO operations.
+ int aio_count = 0;
+ /// The offset of the last point written to so for in the dump file for this
+ /// session.
+ int64_t write_offset = 0;
+ /// Whether this session has been closed.
+ bool ssn_closed = false;
+ /// The filename for this session's dump file.
+ ts::file::path log_name;
+ /// Whether the first transaction in this session has been written.
+ bool has_written_first_transaction = false;
+
+ TSCont aio_cont = nullptr; /// AIO continuation callback
+ TSCont txn_cont = nullptr; /// Transaction continuation callback
+ TSMutex disk_io_mutex = nullptr; /// AIO mutex
+
+ //
+ // Static Variables
+ //
+
+ // The index to be used for the TS API for storing this SessionData on a
+ // per-session basis.
+ static int session_arg_index;
+
+ /// The rate at which to dump sessions. Every one out of sample_pool_size will
+ /// be dumped.
+ static std::atomic<int64_t> sample_pool_size;
+ /// The maximum space logs should take up before stopping the dumping of new
+ /// sessions.
+ static std::atomic<int64_t> max_disk_usage;
+ /// The amount of bytes currently written to dump files.
+ static std::atomic<int64_t> disk_usage;
+
+ /// The directory into which to put the dump files.
+ static ts::file::path log_directory;
+
+ /// Only sessions with this SNI will be dumped (if set).
+ static std::string sni_filter;
+
+ /// The running counter of all sessions dumped by traffic_dump.
+ static uint64_t session_counter;
+
+public:
+ SessionData();
+ ~SessionData();
+
+ /** The getter for the session_arg_index value. */
+ static int get_session_arg_index();
+
+ /** Initialize the cross-session values of managing sessions.
+ *
+ * @return True if initialization is successful, false otherwise.
+ */
+ static bool init(std::string_view log_directory, int64_t max_disk_usage, int64_t sample_size);
+ static bool init(std::string_view log_directory, int64_t max_disk_usage, int64_t sample_size, std::string_view sni_filter);
+
+ /** Set the sample_pool_size to a new value.
+ *
+ * @param[in] new_sample_size The new value to set for sample_pool_size.
+ */
+ static void set_sample_pool_size(int64_t new_sample_size);
+
+ /** Reset the disk usage counter to 0. */
+ static void reset_disk_usage();
+
+ /** Set the max_disk_usage to a new value.
+ *
+ * @param[in] new_max_disk_usage The new value to set for max_disk_usage.
+ */
+ static void set_max_disk_usage(int64_t new_max_disk_usage);
+
+#if 0
+ // TODO: This will eventually be used by TransactionData to dump
+ // the server protocol description in the "server-response" node,
+ // but the TS API does not yet support this.
+
+ /** Get the JSON string that describes the server session stack.
+ *
+ * @param[in] ssnp The reference to the server session.
+ *
+ * @return A JSON description of the server protocol stack.
+ */
+ static std::string get_server_protocol_description(TSHttpSsn ssnp);
+#endif
+
+ /** Write the string to the session's dump file.
+ *
+ * @param[in] content The content to write to the file.
+ *
+ * @return TS_SUCCESS if the write is successfully scheduled with the AIO
+ * system, TS_ERROR otherwise.
+ */
+ int write_to_disk(std::string_view content);
+
+ /** Write the transaction to the session's dump file.
+ *
+ * @param[in] content The transaction content to write to the file.
+ *
+ * @return TS_SUCCESS if the write is successfully scheduled with the AIO
+ * system, TS_ERROR otherwise.
+ */
+ int write_transaction_to_disk(std::string_view content);
+
+private:
+ /** Write the string to the session's dump file.
+ *
+ * This assumes that the caller acquired the required AIO lock.
+ *
+ * @param[in] content The content to write to the file.
+ *
+ * @return TS_SUCCESS if the write is successfully scheduled with the AIO
+ * system, TS_ERROR otherwise.
+ */
+ int write_to_disk_no_lock(std::string_view content);
+
+ /** Get the JSON string that describes the client session stack.
+ *
+ * @param[in] ssnp The reference to the client session.
+ *
+ * @return A JSON description of the client protocol stack.
+ */
+ static std::string get_client_protocol_description(TSHttpSsn ssnp);
+
+ /** The handler callback for when async IO is done. Used for cleanup. */
+ static int session_aio_handler(TSCont contp, TSEvent event, void *edata);
+
+ /** The handler callback for session events. */
+ static int global_session_handler(TSCont contp, TSEvent event, void *edata);
+};
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/traffic_dump.cc b/plugins/experimental/traffic_dump/traffic_dump.cc
index f37bb7b..271b3b8 100644
--- a/plugins/experimental/traffic_dump/traffic_dump.cc
+++ b/plugins/experimental/traffic_dump/traffic_dump.cc
@@ -18,867 +18,71 @@
limitations under the License.
*/
-#include <cinttypes>
-
-#include <cstdio>
-#include <cstdlib>
-#include <cstring>
#include <getopt.h>
-#include <unistd.h>
-
-#include <sys/types.h>
-#include <fcntl.h>
-#include <cerrno>
-#include <arpa/inet.h>
-#include <netinet/in.h>
-#include <openssl/ssl.h>
-
-#include <algorithm>
-#include <sstream>
-#include <iomanip>
-#include <chrono>
-#include <atomic>
-#include <string>
-#include <string_view>
-#include <unordered_set>
-
-#include "tscore/ts_file.h"
-#include "tscpp/util/TextView.h"
-#include "ts/ts.h"
-
-namespace
-{
-const char *PLUGIN_NAME = "traffic_dump";
-const std::string closing = "]}]}";
-uint64_t session_counter = 0;
-
-std::string defaut_sensitive_field_value;
-
-// A case-insensitive comparitor used for comparing HTTP field names.
-struct InsensitiveCompare {
- bool
- operator()(std::string_view a, std::string_view b) const
- {
- return strcasecmp(a, b) == 0;
- }
-};
-
-struct StringHashByLower {
-public:
- size_t
- operator()(const std::string &str) const
- {
- std::string lower;
- std::transform(str.begin(), str.end(), lower.begin(), [](unsigned char c) -> unsigned char { return std::tolower(c); });
- return std::hash<std::string>()(lower);
- }
-};
-
-/// Fields considered sensitive because they may contain user-private
-/// information. These fields are replaced with auto-generated generic content by
-/// default. To override this behavior, the user should specify their own fields
-/// they consider sensitive with --sensitive-fields.
-///
-/// While these are specified with case, they are matched case-insensitively.
-std::unordered_set<std::string, StringHashByLower, InsensitiveCompare> default_sensitive_fields = {
- "Set-Cookie",
- "Cookie",
-};
-
-/// The set of fields, default and user-specified, that are sensitive and whose
-/// values will be replaced with auto-generated generic content.
-std::unordered_set<std::string, StringHashByLower, InsensitiveCompare> sensitive_fields;
-
-ts::file::path log_path{"dump"}; // default log directory
-std::string sni_filter; // The SNI requested for filtering against.
-int s_arg_idx = 0; // Session Arg Index to pass on session data
-std::atomic<int64_t> sample_pool_size(1000); // Sampling ratio
-std::atomic<int64_t> max_disk_usage(10000000); //< Max disk space for logs (approximate)
-std::atomic<int64_t> disk_usage(0); //< Actual disk usage
-// handler declaration
-int session_aio_handler(TSCont contp, TSEvent event, void *edata);
-int session_txn_handler(TSCont contp, TSEvent event, void *edata);
-
-/// Custom structure for per session data
-struct SsnData {
- int log_fd = -1; //< Log file descriptor
- int aio_count = 0; //< Active AIO counts
- int64_t write_offset = 0; //< AIO write offset
- bool first = true; //< First Transaction
- bool ssn_closed = false; //< Session closed flag
- ts::file::path log_name; //< Log file path
-
- TSCont aio_cont = nullptr; //< AIO callback
- TSCont txn_cont = nullptr; //< Transaction callback
- TSMutex disk_io_mutex = nullptr; //< AIO mutex
-
- SsnData()
- {
- disk_io_mutex = TSMutexCreate();
- aio_cont = TSContCreate(session_aio_handler, TSMutexCreate());
- txn_cont = TSContCreate(session_txn_handler, nullptr);
- }
-
- ~SsnData()
- {
- if (disk_io_mutex) {
- TSMutexDestroy(disk_io_mutex);
- }
- if (aio_cont) {
- TSContDestroy(aio_cont);
- }
- if (txn_cont) {
- TSContDestroy(txn_cont);
- }
- }
-
- /// write_to_disk(): Takes a string object and writes to file via AIO
- int
- write_to_disk(const std::string &body)
- {
- TSMutexLock(disk_io_mutex);
- char *pBuf = nullptr;
- // Allocate a buffer for aio writing
- if ((pBuf = static_cast<char *>(TSmalloc(sizeof(char) * body.size())))) {
- memcpy(pBuf, body.c_str(), body.size());
- if (TS_SUCCESS == TSAIOWrite(log_fd, write_offset, pBuf, body.size(), aio_cont)) {
- // Update offset within file and aio events count
- write_offset += body.size();
- aio_count += 1;
-
- TSMutexUnlock(disk_io_mutex);
- return TS_SUCCESS;
- }
- TSfree(pBuf);
- }
- TSMutexUnlock(disk_io_mutex);
- return TS_ERROR;
- }
-};
-
-/// Local helper functions about json formatting
-/// min_write(): Inline function for repeating code
-inline void
-min_write(const char *buf, int64_t &prevIdx, int64_t &idx, std::ostream &jsonfile)
-{
- if (prevIdx < idx) {
- jsonfile.write(buf + prevIdx, idx - prevIdx);
- }
- prevIdx = idx + 1;
-}
-
-/// esc_json_out(): Escape characters in a buffer and output to ofstream object
-/// in a way to minimize ofstream operations
-int
-esc_json_out(const char *buf, int64_t len, std::ostream &jsonfile)
-{
- if (buf == nullptr) {
- return 0;
- }
- int64_t idx, prevIdx = 0;
- for (idx = 0; idx < len; idx++) {
- char c = *(buf + idx);
- switch (c) {
- case '"':
- case '\\': {
- min_write(buf, prevIdx, idx, jsonfile);
- jsonfile << "\\" << c;
- break;
- }
- case '\b': {
- min_write(buf, prevIdx, idx, jsonfile);
- jsonfile << "\\b";
- break;
- }
- case '\f': {
- min_write(buf, prevIdx, idx, jsonfile);
- jsonfile << "\\f";
- break;
- }
- case '\n': {
- min_write(buf, prevIdx, idx, jsonfile);
- jsonfile << "\\n";
- break;
- }
- case '\r': {
- min_write(buf, prevIdx, idx, jsonfile);
- jsonfile << "\\r";
- break;
- }
- case '\t': {
- min_write(buf, prevIdx, idx, jsonfile);
- jsonfile << "\\t";
- break;
- }
- default: {
- if ('\x00' <= c && c <= '\x1f') {
- min_write(buf, prevIdx, idx, jsonfile);
- jsonfile << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<int>(c);
- }
- break;
- }
- }
- }
- min_write(buf, prevIdx, idx, jsonfile);
-
- return len;
-}
-
-/// escape_json(): escape chars in a string and returns json string
-std::string
-escape_json(std::string_view s)
-{
- std::ostringstream o;
- esc_json_out(s.data(), s.length(), o);
- return o.str();
-}
-std::string
-escape_json(const char *buf, int64_t size)
-{
- std::ostringstream o;
- esc_json_out(buf, size, o);
- return o.str();
-}
-
-inline std::string
-json_entry(std::string const &name, const char *value, int64_t size)
-{
- return "\"" + escape_json(name) + "\":\"" + escape_json(value, size) + "\"";
-}
-
-/// json_entry_array(): Formats to array-style entry i.e. ["field","value"]
-inline std::string
-json_entry_array(std::string_view name, std::string_view value)
-{
- return "[\"" + escape_json(name) + "\", \"" + escape_json(value) + "\"]";
-}
-
-/** Remove the scheme prefix from the url.
- *
- * @return The view without the scheme prefix.
- */
-std::string_view
-remove_scheme_prefix(std::string_view url)
-{
- const auto scheme_separator = url.find("://");
- if (scheme_separator == std::string::npos) {
- return url;
- }
- url.remove_prefix(scheme_separator + 3);
- return url;
-}
-
-/// Write the content node.
-//
-/// "content"
-/// "encoding"
-/// "size"
-std::string
-write_content_node(int64_t num_body_bytes)
-{
- return std::string(R"(,"content":{"encoding":"plain","size":)" + std::to_string(num_body_bytes) + '}');
-}
-
-/** Initialize the generic sensitive field to be dumped. This is used instead
- * of the sensitive field values seen on the wire.
- */
-void
-initialize_default_sensitive_field()
-{
- // 128 KB is the maximum size supported for all headers, so this size should
- // be plenty large for our needs.
- constexpr size_t default_field_size = 128 * 1024;
- defaut_sensitive_field_value.resize(default_field_size);
-
- char *field_buffer = defaut_sensitive_field_value.data();
- for (auto i = 0u; i < default_field_size; i += 8) {
- sprintf(field_buffer, "%07x ", i / 8);
- field_buffer += 8;
- }
-}
-
-/** Inspect the field to see whether it is sensitive and return a generic value
- * of equal size to the original if it is.
- *
- * @param[in] name The field name to inspect.
- * @param[in] original_value The field value to inspect.
- *
- * @return The value traffic_dump should dump for the given field.
- */
-std::string_view
-replace_sensitive_fields(std::string_view name, std::string_view original_value)
-{
- auto search = sensitive_fields.find(std::string(name));
- if (search == sensitive_fields.end()) {
- return original_value;
- }
- auto new_value_size = original_value.size();
- if (original_value.size() > defaut_sensitive_field_value.size()) {
- new_value_size = defaut_sensitive_field_value.size();
- TSError("[%s] Encountered a sensitive field value larger than our default "
- "field size. Default size: %zu, incoming field size: %zu",
- PLUGIN_NAME, defaut_sensitive_field_value.size(), original_value.size());
- }
- return std::string_view{defaut_sensitive_field_value.data(), new_value_size};
-}
-
-/// Read the txn information from TSMBuffer and write the header information.
-/// This function does not write the content node.
-std::string
-write_message_node_no_content(TSMBuffer &buffer, TSMLoc &hdr_loc)
-{
- std::string result = "{";
- int len = 0;
- const char *cp = nullptr;
- TSMLoc url_loc = nullptr;
-
- // Log scheme+method+request-target or status+reason based on header type
- if (TSHttpHdrTypeGet(buffer, hdr_loc) == TS_HTTP_TYPE_REQUEST) {
- // 1. "version"
- int version = TSHttpHdrVersionGet(buffer, hdr_loc);
- result += R"("version":")" + std::to_string(TS_HTTP_MAJOR(version)) + "." + std::to_string(TS_HTTP_MINOR(version)) + '"';
-
- TSAssert(TS_SUCCESS == TSHttpHdrUrlGet(buffer, hdr_loc, &url_loc));
- // 2. "scheme":
- cp = TSUrlSchemeGet(buffer, url_loc, &len);
- TSDebug(PLUGIN_NAME, "write_message_node(): found scheme %.*s ", len, cp);
- result += "," + json_entry("scheme", cp, len);
-
- // 3. "method":(string)
- cp = TSHttpHdrMethodGet(buffer, hdr_loc, &len);
- TSDebug(PLUGIN_NAME, "write_message_node(): found method %.*s ", len, cp);
- result += "," + json_entry("method", cp, len);
-
- // 4. "url"
- cp = TSUrlHostGet(buffer, url_loc, &len);
- std::string_view host{cp, static_cast<size_t>(len)};
-
- char *url = TSUrlStringGet(buffer, url_loc, &len);
- std::string_view url_string{url, static_cast<size_t>(len)};
-
- if (host.empty()) {
- // TSUrlStringGet will add the scheme to the URL, even if the request
- // target doesn't contain it. However, we cannot just always remove the
- // scheme because the original request target may include it. We assume
- // here that a URL with a scheme but not a host is artificial and thus
- // we remove it.
- url_string = remove_scheme_prefix(url_string);
- }
-
- TSDebug(PLUGIN_NAME, "write_message_node(): found host target %.*s", static_cast<int>(url_string.size()), url_string.data());
- result += "," + json_entry("url", url_string.data(), url_string.size());
- TSfree(url);
- TSHandleMLocRelease(buffer, hdr_loc, url_loc);
- } else {
- // 1. "status":(string)
- result += R"("status":)" + std::to_string(TSHttpHdrStatusGet(buffer, hdr_loc));
- // 2. "reason":(string)
- cp = TSHttpHdrReasonGet(buffer, hdr_loc, &len);
- result += "," + json_entry("reason", cp, len);
- // 3. "encoding"
- }
-
- // "headers": [[name(string), value(string)]]
- result += R"(,"headers":{"encoding":"esc_json", "fields": [)";
- TSMLoc field_loc = TSMimeHdrFieldGet(buffer, hdr_loc, 0);
- while (field_loc) {
- TSMLoc next_field_loc;
- const char *name = nullptr;
- const char *value = nullptr;
- int name_len = 0, value_len = 0;
- // Append to "fields" list if valid value exists
- if ((name = TSMimeHdrFieldNameGet(buffer, hdr_loc, field_loc, &name_len)) && name_len) {
- std::string_view name_view{name, static_cast<size_t>(name_len)};
- value = TSMimeHdrFieldValueStringGet(buffer, hdr_loc, field_loc, -1, &value_len);
- std::string_view value_view{value, static_cast<size_t>(value_len)};
- std::string_view new_value = replace_sensitive_fields(name_view, value_view);
- result += json_entry_array(name_view, new_value);
- }
-
- next_field_loc = TSMimeHdrFieldNext(buffer, hdr_loc, field_loc);
- TSHandleMLocRelease(buffer, hdr_loc, field_loc);
- if ((field_loc = next_field_loc) != nullptr) {
- result += ",";
- }
- }
- return result += "]}";
-}
-
-/// Read the txn information from TSMBuffer and write the header information including
-/// the content node describing the body characteristics.
-std::string
-write_message_node(TSMBuffer &buffer, TSMLoc &hdr_loc, int64_t num_body_bytes)
-{
- std::string result = write_message_node_no_content(buffer, hdr_loc);
- result += write_content_node(num_body_bytes);
- return result + "}";
-}
-
-// Per session AIO handler: update AIO counts and clean up
-int
-session_aio_handler(TSCont contp, TSEvent event, void *edata)
-{
- switch (event) {
- case TS_EVENT_AIO_DONE: {
- TSAIOCallback cb = static_cast<TSAIOCallback>(edata);
- SsnData *ssnData = static_cast<SsnData *>(TSContDataGet(contp));
- if (!ssnData) {
- TSDebug(PLUGIN_NAME, "session_aio_handler(): No valid ssnData. Abort.");
- return TS_ERROR;
- }
- char *buf = TSAIOBufGet(cb);
- TSMutexLock(ssnData->disk_io_mutex);
-
- // Free the allocated buffer and update aio_count
- if (buf) {
- TSfree(buf);
- if (--ssnData->aio_count == 0 && ssnData->ssn_closed) {
- // check for ssn close, if closed, do clean up
- TSContDataSet(contp, nullptr);
- close(ssnData->log_fd);
- TSMutexUnlock(ssnData->disk_io_mutex);
- std::error_code ec;
- ts::file::file_status st = ts::file::status(ssnData->log_name, ec);
- if (!ec) {
- disk_usage += ts::file::file_size(st);
- TSDebug(PLUGIN_NAME, "Finish a session with log file of %" PRIuMAX "bytes", ts::file::file_size(st));
- }
- delete ssnData;
- return TS_SUCCESS;
- }
- }
- TSMutexUnlock(ssnData->disk_io_mutex);
- return TS_SUCCESS;
- }
- default:
- TSDebug(PLUGIN_NAME, "session_aio_handler(): unhandled events %d", event);
- return TS_ERROR;
- }
- return TS_SUCCESS;
-}
-
-// Transaction handler: writes headers to the log file using AIO
-int
-session_txn_handler(TSCont contp, TSEvent event, void *edata)
-{
- TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
-
- // Retrieve SsnData
- TSHttpSsn ssnp = TSHttpTxnSsnGet(txnp);
- SsnData *ssnData = static_cast<SsnData *>(TSUserArgGet(ssnp, s_arg_idx));
-
- // If no valid ssnData, continue transaction as if nothing happened
- if (!ssnData) {
- TSDebug(PLUGIN_NAME, "session_txn_handler(): No ssnData found. Abort.");
- TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
- return TS_SUCCESS;
- }
-
- std::string txn_info;
- switch (event) {
- case TS_EVENT_HTTP_TXN_START: {
- // Get UUID
- char uuid[TS_CRUUID_STRING_LEN + 1];
- TSAssert(TS_SUCCESS == TSClientRequestUuidGet(txnp, uuid));
- std::string_view uuid_view{uuid, strnlen(uuid, TS_CRUUID_STRING_LEN)};
-
- // Generate per transaction json records
- if (!ssnData->first) {
- txn_info += ",";
- }
- ssnData->first = false;
-
- // "uuid":(string)
- txn_info += "{";
- // "connection-time":(number)
- TSHRTime start_time;
- TSHttpTxnMilestoneGet(txnp, TS_MILESTONE_UA_BEGIN, &start_time);
- txn_info += "\"connection-time\":" + std::to_string(start_time);
-
- // The uuid is a header field for each message in the transaction. Use the
- // "all" node to apply to each message.
- std::string_view name = "uuid";
- txn_info += ",\"all\":{\"headers\":{\"fields\":[" + json_entry_array(name, uuid_view);
- txn_info += "]}}";
- ssnData->write_to_disk(txn_info);
- break;
- }
-
- case TS_EVENT_HTTP_READ_REQUEST_HDR: {
- // This hook is registered globally, not at TS_EVENT_HTTP_SSN_START in
- // global_ssn_handler(). As such, this handler will be called with every
- // transaction. However, we know that we are dumping this transaction
- // because there is a ssnData associated with it.
-
- // We must grab the client request information before remap happens because
- // the remap process modifies the request buffer.
- TSMBuffer buffer;
- TSMLoc hdr_loc;
- if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &buffer, &hdr_loc)) {
- TSDebug(PLUGIN_NAME, "Found client request");
- // We don't have an accurate view of the body size until TXN_CLOSE so we hold
- // off on writing the content:size node until then.
- txn_info += R"(,"client-request":)" + write_message_node_no_content(buffer, hdr_loc);
- TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
- buffer = nullptr;
- }
- ssnData->write_to_disk(txn_info);
- break;
- }
-
- case TS_EVENT_HTTP_TXN_CLOSE: {
- // proxy-request/response headers
- TSMBuffer buffer;
- TSMLoc hdr_loc;
- if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &buffer, &hdr_loc)) {
- txn_info += write_content_node(TSHttpTxnClientReqBodyBytesGet(txnp)) + "}";
- TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
- buffer = nullptr;
- }
- if (TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &buffer, &hdr_loc)) {
- TSDebug(PLUGIN_NAME, "Found proxy request");
- txn_info += R"(,"proxy-request":)" + write_message_node(buffer, hdr_loc, TSHttpTxnServerReqBodyBytesGet(txnp));
- TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
- buffer = nullptr;
- }
- if (TS_SUCCESS == TSHttpTxnServerRespGet(txnp, &buffer, &hdr_loc)) {
- TSDebug(PLUGIN_NAME, "Found server response");
- txn_info += R"(,"server-response":)" + write_message_node(buffer, hdr_loc, TSHttpTxnServerRespBodyBytesGet(txnp));
- TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
- buffer = nullptr;
- }
- if (TS_SUCCESS == TSHttpTxnClientRespGet(txnp, &buffer, &hdr_loc)) {
- TSDebug(PLUGIN_NAME, "Found proxy response");
- txn_info += R"(,"proxy-response":)" + write_message_node(buffer, hdr_loc, TSHttpTxnClientRespBodyBytesGet(txnp));
- TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
- buffer = nullptr;
- }
-
- txn_info += "}";
- ssnData->write_to_disk(txn_info);
- break;
- }
- default:
- TSDebug(PLUGIN_NAME, "session_txn_handler(): Unhandled events %d", event);
- TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
- return TS_ERROR;
- }
-
- TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
- return TS_SUCCESS;
-}
-
-/** Create a TLS characteristics node.
- *
- * This function encapsulates the logic common between the client-side and
- * server-side logic for populating the TLS node.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The node describing the TLS properties of this session.
- */
-std::string
-get_tls_description_helper(TSVConn ssn_vc)
-{
- TSSslConnection ssl_conn = TSVConnSslConnectionGet(ssn_vc);
- SSL *ssl_obj = (SSL *)ssl_conn;
- if (ssl_obj == nullptr) {
- return "";
- }
- std::ostringstream tls_description;
- tls_description << R"("tls":{)";
- const char *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name);
- if (sni_ptr != nullptr) {
- std::string_view sni{sni_ptr};
- if (!sni.empty()) {
- tls_description << R"("sni":")" << sni << R"(")";
- }
- }
- tls_description << R"(,"verify_mode":")" << std::to_string(SSL_get_verify_mode(ssl_obj)) << R"(")";
- tls_description << "}";
- return tls_description.str();
-}
-
-/** Create a server-side TLS characteristics node.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The node describing the TLS properties of this session.
- */
-std::string
-get_server_tls_description(TSHttpSsn ssnp)
-{
- TSVConn ssn_vc = TSHttpSsnServerVConnGet(ssnp);
- return get_tls_description_helper(ssn_vc);
-}
-
-/** Create a client-side TLS characteristics node.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The node describing the TLS properties of this session.
- */
-std::string
-get_client_tls_description(TSHttpSsn ssnp)
-{
- TSVConn ssn_vc = TSHttpSsnClientVConnGet(ssnp);
- return get_tls_description_helper(ssn_vc);
-}
-
-/// A named boolean for callers who pass the is_client parameter.
-constexpr bool IS_CLIENT = true;
-
-/** Create the nodes that describe the session's sub-HTTP protocols.
- *
- * This function encapsulates the logic common between the client-side and
- * server-side logic for describing the session's characteristics.
- *
- * This will create the string representing the "protocol" and "tls" nodes. The
- * "tls" node will only be present if the connection is over SSL/TLS.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The description of the protocol stack and certain TLS attributes.
- */
-std::string
-get_protocol_description_helper(TSHttpSsn ssnp, bool is_client)
-{
- std::ostringstream protocol_description;
- protocol_description << R"("protocol":[)";
-
- const char *protocol[10];
- int count = -1;
- if (is_client) {
- TSAssert(TS_SUCCESS == TSHttpSsnClientProtocolStackGet(ssnp, 10, protocol, &count));
- } else {
- // See the TODO below in the commented out defintion of get_server_protocol_description.
- // TSAssert(TS_SUCCESS == TSHttpSsnServerProtocolStackGet(ssnp, 10, protocol, &count));
- }
- for (int i = 0; i < count; i++) {
- if (i > 0) {
- protocol_description << ",";
- }
- protocol_description << '"' << std::string(protocol[i]) << '"';
- }
-
- protocol_description << "]";
- std::string tls_description;
- if (is_client) {
- tls_description = get_client_tls_description(ssnp);
- } else {
- tls_description = get_server_tls_description(ssnp);
- }
- if (!tls_description.empty()) {
- protocol_description << "," << tls_description;
- }
- return protocol_description.str();
-}
-#if 0
-// TODO It will be important to add this eventually, but
-// TSHttpSsnServerProtocolStackGet is not defined yet. Once it (or some other
-// mechanism for getting the server side stack) is implemented, we will call
-// this as a part of writing the server-response node.
+#include "global_variables.h"
+#include "session_data.h"
+#include "transaction_data.h"
-/** Generate the nodes describing the server session.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The description of the protocol stack and certain TLS attributes.
- */
-std::string
-get_server_protocol_description(TSHttpSsn ssnp)
-{
- return get_protocol_description_helper(ssnp, !IS_CLIENT);
-}
-#endif
-
-/** Generate the nodes describing the client session.
- *
- * @param[in] ssnp The pointer for this session.
- *
- * @return The description of the protocol stack and certain TLS attributes.
- */
-std::string
-get_client_protocol_description(TSHttpSsn ssnp)
+namespace traffic_dump
{
- return get_protocol_description_helper(ssnp, IS_CLIENT);
-}
-
-// Session handler for global hooks; Assign per-session data structure and log files
+/// Handle LIFECYCLE_MSG from traffic_ctl.
static int
-global_ssn_handler(TSCont contp, TSEvent event, void *edata)
+global_message_handler(TSCont contp, TSEvent event, void *edata)
{
- TSHttpSsn ssnp = static_cast<TSHttpSsn>(edata);
-
switch (event) {
- // Also handles LIFECYCLE_MSG from traffic_ctl
case TS_EVENT_LIFECYCLE_MSG: {
TSPluginMsg *msg = static_cast<TSPluginMsg *>(edata);
- // String view of plugin message prefix
static constexpr std::string_view PLUGIN_PREFIX("traffic_dump."_sv);
std::string_view tag(msg->tag, strlen(msg->tag));
-
if (tag.substr(0, PLUGIN_PREFIX.size()) == PLUGIN_PREFIX) {
tag.remove_prefix(PLUGIN_PREFIX.size());
if (tag == "sample") {
- sample_pool_size = static_cast<int64_t>(strtol(static_cast<const char *>(msg->data), nullptr, 0));
- TSDebug(PLUGIN_NAME, "TS_EVENT_LIFECYCLE_MSG: Received Msg to change sample size to %" PRId64 "bytes",
- sample_pool_size.load());
+ const auto new_sample_size = static_cast<int64_t>(strtol(static_cast<char const *>(msg->data), nullptr, 0));
+ TSDebug(debug_tag, "TS_EVENT_LIFECYCLE_MSG: Received Msg to change sample size to %" PRId64 "bytes", new_sample_size);
+ SessionData::set_sample_pool_size(new_sample_size);
} else if (tag == "reset") {
- disk_usage = 0;
- TSDebug(PLUGIN_NAME, "TS_EVENT_LIFECYCLE_MSG: Received Msg to reset disk usage counter");
+ TSDebug(debug_tag, "TS_EVENT_LIFECYCLE_MSG: Received Msg to reset disk usage counter");
+ SessionData::reset_disk_usage();
} else if (tag == "limit") {
- max_disk_usage = static_cast<int64_t>(strtol(static_cast<const char *>(msg->data), nullptr, 0));
- TSDebug(PLUGIN_NAME, "TS_EVENT_LIFECYCLE_MSG: Received Msg to change max disk usage to %" PRId64 "bytes",
- max_disk_usage.load());
+ const auto new_max_disk_usage = static_cast<int64_t>(strtol(static_cast<char const *>(msg->data), nullptr, 0));
+ TSDebug(debug_tag, "TS_EVENT_LIFECYCLE_MSG: Received Msg to change max disk usage to %" PRId64 "bytes", new_max_disk_usage);
+ SessionData::set_max_disk_usage(new_max_disk_usage);
}
}
return TS_SUCCESS;
}
- case TS_EVENT_HTTP_SSN_START: {
- // Grab session id for logging against a global value rather than the local
- // session_counter.
- int64_t id = TSHttpSsnIdGet(ssnp);
-
- // If the user has asked for SNI filtering, filter on that first because
- // any sampling will apply just to that subset of connections that match
- // that SNI.
- if (!sni_filter.empty()) {
- TSVConn ssn_vc = TSHttpSsnClientVConnGet(ssnp);
- TSSslConnection ssl_conn = TSVConnSslConnectionGet(ssn_vc);
- SSL *ssl_obj = (SSL *)ssl_conn;
- if (ssl_obj == nullptr) {
- TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore non-HTTPS session %" PRId64 "...", id);
- break;
- }
- const char *sni_ptr = SSL_get_servername(ssl_obj, TLSEXT_NAMETYPE_host_name);
- if (sni_ptr == nullptr) {
- TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore HTTPS session with non-existent SNI.");
- break;
- } else {
- const std::string_view sni{sni_ptr};
- if (sni != sni_filter) {
- TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore HTTPS session with non-filtered SNI: %s", sni_ptr);
- break;
- }
- }
- }
- const auto this_session_count = session_counter++;
- if (this_session_count % sample_pool_size != 0) {
- TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore session %" PRId64 "...", id);
- break;
- } else if (disk_usage >= max_disk_usage) {
- TSDebug(PLUGIN_NAME, "global_ssn_handler(): Ignore session %" PRId64 "due to disk usage %" PRId64 "bytes", id,
- disk_usage.load());
- break;
- }
- // Beginning of a new session
- /// Get epoch time
- auto start = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch());
-
- // Create new per session data
- SsnData *ssnData = new SsnData;
- TSUserArgSet(ssnp, s_arg_idx, ssnData);
-
- TSContDataSet(ssnData->aio_cont, ssnData);
-
- // "protocol":(string),"tls":(string)
- // The "tls" node will only be present if the session is over SSL/TLS.
- std::string protocol_description = get_client_protocol_description(ssnp);
-
- std::string beginning = R"({"meta":{"version":"1.0"},"sessions":[{)" + protocol_description + R"(,"connection-time":)" +
- std::to_string(start.count()) + R"(,"transactions":[)";
-
- // Use the session count's hex string as the filename.
- std::stringstream stream;
- stream << std::setw(16) << std::setfill('0') << std::hex << this_session_count;
- std::string session_hex_name = stream.str();
-
- // Use client ip as sub directory name
- char client_str[INET6_ADDRSTRLEN];
- const sockaddr *client_ip = TSHttpSsnClientAddrGet(ssnp);
- if (AF_INET == client_ip->sa_family) {
- inet_ntop(AF_INET, &(reinterpret_cast<const sockaddr_in *>(client_ip)->sin_addr), client_str, INET_ADDRSTRLEN);
- } else if (AF_INET6 == client_ip->sa_family) {
- inet_ntop(AF_INET6, &(reinterpret_cast<const sockaddr_in6 *>(client_ip)->sin6_addr), client_str, INET6_ADDRSTRLEN);
- } else {
- TSDebug(PLUGIN_NAME, "global_ssn_handler(): Unknown address family.");
- snprintf(client_str, INET6_ADDRSTRLEN, "unknown");
- }
-
- // Initialize AIO file
- TSMutexLock(ssnData->disk_io_mutex);
- if (ssnData->log_fd < 0) {
- ts::file::path log_p = log_path / ts::file::path(std::string(client_str, 3));
- ts::file::path log_f = log_p / ts::file::path(session_hex_name);
-
- // Create subdir if not existing
- std::error_code ec;
- ts::file::status(log_p, ec);
- if (ec && mkdir(log_p.c_str(), 0755) == -1) {
- TSDebug(PLUGIN_NAME, "global_ssn_handler(): Failed to create dir %s", log_p.c_str());
- TSError("[%s] Failed to create dir %s", PLUGIN_NAME, log_p.c_str());
- }
-
- // Try to open log files for AIO
- ssnData->log_fd = open(log_f.c_str(), O_RDWR | O_CREAT, S_IRWXU);
- if (ssnData->log_fd < 0) {
- TSMutexUnlock(ssnData->disk_io_mutex);
- TSDebug(PLUGIN_NAME, "global_ssn_handler(): Failed to open log files %s. Abort.", log_f.c_str());
- TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
- return TS_EVENT_HTTP_CONTINUE;
- }
- ssnData->log_name = log_f;
- // Write log file beginning to disk
- ssnData->write_to_disk(beginning);
- }
- TSMutexUnlock(ssnData->disk_io_mutex);
-
- TSHttpSsnHookAdd(ssnp, TS_HTTP_TXN_START_HOOK, ssnData->txn_cont);
- TSHttpSsnHookAdd(ssnp, TS_HTTP_TXN_CLOSE_HOOK, ssnData->txn_cont);
- break;
- }
- case TS_EVENT_HTTP_SSN_CLOSE: {
- // Write session and close the log file.
- int64_t id = TSHttpSsnIdGet(ssnp);
- TSDebug(PLUGIN_NAME, "global_ssn_handler(): Closing session %" PRId64 "...", id);
- // Retrieve SsnData
- SsnData *ssnData = static_cast<SsnData *>(TSUserArgGet(ssnp, s_arg_idx));
- // If no valid ssnData, continue transaction as if nothing happened
- if (!ssnData) {
- TSDebug(PLUGIN_NAME, "global_ssn_handler(): [TS_EVENT_HTTP_SSN_CLOSE] No ssnData found. Abort.");
- TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
- return TS_SUCCESS;
- }
- ssnData->write_to_disk(closing);
- TSMutexLock(ssnData->disk_io_mutex);
- ssnData->ssn_closed = true;
- TSMutexUnlock(ssnData->disk_io_mutex);
-
- break;
- }
default:
- break;
+ TSDebug(debug_tag, "session_aio_handler(): unhandled events %d", event);
+ return TS_ERROR;
}
- TSHttpSsnReenable(ssnp, TS_EVENT_HTTP_CONTINUE);
- return TS_SUCCESS;
}
-} // End of anonymous namespace
+} // namespace traffic_dump
void
-TSPluginInit(int argc, const char *argv[])
+TSPluginInit(int argc, char const *argv[])
{
- TSDebug(PLUGIN_NAME, "initializing plugin");
+ TSDebug(traffic_dump::debug_tag, "initializing plugin");
TSPluginRegistrationInfo info;
info.plugin_name = "traffic_dump";
info.vendor_name = "Apache Software Foundation";
info.support_email = "dev@trafficserver.apache.org";
+ if (TS_SUCCESS != TSPluginRegister(&info)) {
+ TSError("[%s] Unable to initialize plugin (disabled). Failed to register plugin.", traffic_dump::debug_tag);
+ return;
+ }
+
bool sensitive_fields_were_specified = false;
+ traffic_dump::sensitive_fields_t user_specified_fields;
+ ts::file::path log_dir{traffic_dump::SessionData::default_log_directory};
+ int64_t sample_pool_size = traffic_dump::SessionData::default_sample_pool_size;
+ int64_t max_disk_usage = traffic_dump::SessionData::default_max_disk_usage;
+ std::string sni_filter;
+
/// Commandline options
static const struct option longopts[] = {
{"logdir", required_argument, nullptr, 'l'}, {"sample", required_argument, nullptr, 's'},
@@ -905,18 +109,17 @@ TSPluginInit(int argc, const char *argv[])
if (filter_field.empty()) {
continue;
}
- sensitive_fields.emplace(filter_field);
+ user_specified_fields.emplace(filter_field);
}
break;
}
case 'n': {
// --sni-filter is used to filter sessions based upon an SNI.
sni_filter = std::string(optarg);
- TSDebug(PLUGIN_NAME, "Filtering to only dump connections with SNI: %s", sni_filter.c_str());
break;
}
case 'l': {
- log_path = ts::file::path{optarg};
+ log_dir = ts::file::path{optarg};
break;
}
case 's': {
@@ -931,57 +134,41 @@ TSPluginInit(int argc, const char *argv[])
break;
default:
- TSDebug(PLUGIN_NAME, "Unexpected options.");
- TSError("[%s] Unexpected options error.", PLUGIN_NAME);
+ TSDebug(traffic_dump::debug_tag, "Unexpected options.");
+ TSError("[%s] Unexpected options error.", traffic_dump::debug_tag);
return;
}
}
-
- if (!sensitive_fields_were_specified) {
- // The user did not provide their own list of sensitive fields. Use the
- // default.
- sensitive_fields.merge(default_sensitive_fields);
+ if (!log_dir.is_absolute()) {
+ log_dir = ts::file::path(TSInstallDirGet()) / log_dir;
}
-
- std::string sensitive_fields_string;
- bool is_first = true;
- for (const auto &field : sensitive_fields) {
- if (!is_first) {
- sensitive_fields_string += ", ";
+ if (sni_filter.empty()) {
+ if (!traffic_dump::SessionData::init(log_dir.view(), max_disk_usage, sample_pool_size)) {
+ TSError("[%s] Failed to initialize session state.", traffic_dump::debug_tag);
+ return;
+ }
+ } else {
+ if (!traffic_dump::SessionData::init(log_dir.view(), max_disk_usage, sample_pool_size, sni_filter)) {
+ TSError("[%s] Failed to initialize session state with an SNI filter.", traffic_dump::debug_tag);
+ return;
}
- is_first = false;
- sensitive_fields_string += field;
- }
- TSDebug(PLUGIN_NAME, "Sensitive fields for which generic values will be dumped: %s", sensitive_fields_string.c_str());
-
- // Make absolute path if not
- if (!log_path.is_absolute()) {
- log_path = ts::file::path(TSInstallDirGet()) / log_path;
}
- TSDebug(PLUGIN_NAME, "Initialized with log directory: %s", log_path.c_str());
- if (TS_SUCCESS != TSPluginRegister(&info)) {
- TSError("[%s] Unable to initialize plugin (disabled). Failed to register plugin.", PLUGIN_NAME);
- } else if (TS_SUCCESS != TSUserArgIndexReserve(TS_USER_ARGS_SSN, PLUGIN_NAME, "Track log related data", &s_arg_idx)) {
- TSError("[%s] Unable to initialize plugin (disabled). Failed to reserve ssn arg.", PLUGIN_NAME);
+ if (sensitive_fields_were_specified) {
+ if (!traffic_dump::TransactionData::init(std::move(user_specified_fields))) {
+ TSError("[%s] Failed to initialize transaction state with user-specified fields.", traffic_dump::debug_tag);
+ return;
+ }
} else {
- initialize_default_sensitive_field();
-
- /// Add global hooks
- TSCont ssncont = TSContCreate(global_ssn_handler, nullptr);
- TSHttpHookAdd(TS_HTTP_SSN_START_HOOK, ssncont);
- TSHttpHookAdd(TS_HTTP_SSN_CLOSE_HOOK, ssncont);
-
- // Register the collecting of client-request headers at the global level so
- // we can process requests before other plugins. (Global hooks are
- // processed before session and transaction ones.)
- TSCont txn_cont = TSContCreate(session_txn_handler, nullptr);
- TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, txn_cont);
-
- TSLifecycleHookAdd(TS_LIFECYCLE_MSG_HOOK, ssncont);
- TSDebug(PLUGIN_NAME, "Initialized with sample pool size %" PRId64 " bytes and disk limit %" PRId64 " bytes",
- sample_pool_size.load(), max_disk_usage.load());
+ // The user did not provide their own list of sensitive fields. Use the
+ // default.
+ if (!traffic_dump::TransactionData::init()) {
+ TSError("[%s] Failed to initialize transaction state.", traffic_dump::debug_tag);
+ return;
+ }
}
+ TSCont message_continuation = TSContCreate(traffic_dump::global_message_handler, nullptr);
+ TSLifecycleHookAdd(TS_LIFECYCLE_MSG_HOOK, message_continuation);
return;
}
diff --git a/plugins/experimental/traffic_dump/transaction_data.cc b/plugins/experimental/traffic_dump/transaction_data.cc
new file mode 100644
index 0000000..4951f2f
--- /dev/null
+++ b/plugins/experimental/traffic_dump/transaction_data.cc
@@ -0,0 +1,358 @@
+/** @txn_data.cc
+ Implementation of Traffic Dump transaction data.
+ @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 "transaction_data.h"
+#include "global_variables.h"
+#include "json_utils.h"
+#include "session_data.h"
+
+namespace traffic_dump
+{
+int TransactionData::transaction_arg_index = 0;
+sensitive_fields_t TransactionData::sensitive_fields;
+
+std::string defaut_sensitive_field_value;
+
+/// Fields considered sensitive because they may contain user-private
+/// information. These fields are replaced with auto-generated generic content by
+/// default. To override this behavior, the user should specify their own fields
+/// they consider sensitive with --sensitive-fields.
+///
+/// While these are specified with case, they are matched case-insensitively.
+sensitive_fields_t default_sensitive_fields = {
+ "Set-Cookie",
+ "Cookie",
+};
+
+void
+TransactionData::initialize_default_sensitive_field()
+{
+ // 128 KB is the maximum size supported for all headers, so this size should
+ // be plenty large for our needs.
+ constexpr size_t default_field_size = 128 * 1024;
+ defaut_sensitive_field_value.resize(default_field_size);
+
+ char *field_buffer = defaut_sensitive_field_value.data();
+ for (auto i = 0u; i < default_field_size; i += 8) {
+ sprintf(field_buffer, "%07x ", i / 8);
+ field_buffer += 8;
+ }
+}
+
+/// The set of fields, default and user-specified, that are sensitive and whose
+/// values will be replaced with auto-generated generic content.
+sensitive_fields_t sensitive_fields;
+
+std::string
+TransactionData::get_sensitive_field_description()
+{
+ std::string sensitive_fields_string;
+ bool is_first = true;
+ for (const auto &field : sensitive_fields) {
+ if (!is_first) {
+ sensitive_fields_string += ", ";
+ }
+ is_first = false;
+ sensitive_fields_string += field;
+ }
+ return sensitive_fields_string;
+}
+
+bool
+TransactionData::init(sensitive_fields_t &&new_fields)
+{
+ sensitive_fields = std::move(new_fields);
+ return init_helper();
+}
+
+bool
+TransactionData::init()
+{
+ sensitive_fields = default_sensitive_fields;
+ return init_helper();
+}
+
+std::string_view
+TransactionData::replace_sensitive_fields(std::string_view name, std::string_view original_value)
+{
+ auto search = sensitive_fields.find(std::string(name));
+ if (search == sensitive_fields.end()) {
+ return original_value;
+ }
+ auto new_value_size = original_value.size();
+ if (original_value.size() > defaut_sensitive_field_value.size()) {
+ new_value_size = defaut_sensitive_field_value.size();
+ TSError("[%s] Encountered a sensitive field value larger than our default "
+ "field size. Default size: %zu, incoming field size: %zu",
+ debug_tag, defaut_sensitive_field_value.size(), original_value.size());
+ }
+ return std::string_view{defaut_sensitive_field_value.data(), new_value_size};
+}
+
+std::string
+TransactionData::write_content_node(int64_t num_body_bytes)
+{
+ return std::string(R"(,"content":{"encoding":"plain","size":)" + std::to_string(num_body_bytes) + '}');
+}
+
+std::string
+TransactionData::write_message_node_no_content(TSMBuffer &buffer, TSMLoc &hdr_loc)
+{
+ std::string result = "{";
+ int len = 0;
+ char const *cp = nullptr;
+ TSMLoc url_loc = nullptr;
+
+ // Log scheme+method+request-target or status+reason based on header type
+ if (TSHttpHdrTypeGet(buffer, hdr_loc) == TS_HTTP_TYPE_REQUEST) {
+ // 1. "version"
+ int version = TSHttpHdrVersionGet(buffer, hdr_loc);
+ result += R"("version":")" + std::to_string(TS_HTTP_MAJOR(version)) + "." + std::to_string(TS_HTTP_MINOR(version)) + '"';
+
+ TSAssert(TS_SUCCESS == TSHttpHdrUrlGet(buffer, hdr_loc, &url_loc));
+ // 2. "scheme":
+ cp = TSUrlSchemeGet(buffer, url_loc, &len);
+ TSDebug(debug_tag, "write_message_node(): found scheme %.*s ", len, cp);
+ result += "," + traffic_dump::json_entry("scheme", cp, len);
+
+ // 3. "method":(string)
+ cp = TSHttpHdrMethodGet(buffer, hdr_loc, &len);
+ TSDebug(debug_tag, "write_message_node(): found method %.*s ", len, cp);
+ result += "," + traffic_dump::json_entry("method", cp, len);
+
+ // 4. "url"
+ cp = TSUrlHostGet(buffer, url_loc, &len);
+ std::string_view host{cp, static_cast<size_t>(len)};
+
+ char *url = TSUrlStringGet(buffer, url_loc, &len);
+ std::string_view url_string{url, static_cast<size_t>(len)};
+
+ if (host.empty()) {
+ // TSUrlStringGet will add the scheme to the URL, even if the request
+ // target doesn't contain it. However, we cannot just always remove the
+ // scheme because the original request target may include it. We assume
+ // here that a URL with a scheme but not a host is artificial and thus
+ // we remove it.
+ url_string = remove_scheme_prefix(url_string);
+ }
+
+ TSDebug(debug_tag, "write_message_node(): found host target %.*s", static_cast<int>(url_string.size()), url_string.data());
+ result += "," + traffic_dump::json_entry("url", url_string);
+ TSfree(url);
+ TSHandleMLocRelease(buffer, hdr_loc, url_loc);
+ } else {
+ // 1. "status":(string)
+ result += R"("status":)" + std::to_string(TSHttpHdrStatusGet(buffer, hdr_loc));
+ // 2. "reason":(string)
+ cp = TSHttpHdrReasonGet(buffer, hdr_loc, &len);
+ result += "," + traffic_dump::json_entry("reason", cp, len);
+ // 3. "encoding"
+ }
+
+ // "headers": [[name(string), value(string)]]
+ result += R"(,"headers":{"encoding":"esc_json", "fields": [)";
+ TSMLoc field_loc = TSMimeHdrFieldGet(buffer, hdr_loc, 0);
+ while (field_loc) {
+ TSMLoc next_field_loc;
+ char const *name = nullptr;
+ char const *value = nullptr;
+ int name_len = 0, value_len = 0;
+ // Append to "fields" list if valid value exists
+ if ((name = TSMimeHdrFieldNameGet(buffer, hdr_loc, field_loc, &name_len)) && name_len) {
+ std::string_view name_view{name, static_cast<size_t>(name_len)};
+ value = TSMimeHdrFieldValueStringGet(buffer, hdr_loc, field_loc, -1, &value_len);
+ std::string_view value_view{value, static_cast<size_t>(value_len)};
+ std::string_view new_value = replace_sensitive_fields(name_view, value_view);
+ result += traffic_dump::json_entry_array(name_view, new_value);
+ }
+
+ next_field_loc = TSMimeHdrFieldNext(buffer, hdr_loc, field_loc);
+ TSHandleMLocRelease(buffer, hdr_loc, field_loc);
+ if ((field_loc = next_field_loc) != nullptr) {
+ result += ",";
+ }
+ }
+ return result += "]}";
+}
+
+std::string
+TransactionData::write_message_node(TSMBuffer &buffer, TSMLoc &hdr_loc, int64_t num_body_bytes)
+{
+ std::string result = write_message_node_no_content(buffer, hdr_loc);
+ result += write_content_node(num_body_bytes);
+ return result + "}";
+}
+
+std::string_view
+TransactionData::remove_scheme_prefix(std::string_view url)
+{
+ const auto scheme_separator = url.find("://");
+ if (scheme_separator == std::string::npos) {
+ return url;
+ }
+ url.remove_prefix(scheme_separator + 3);
+ return url;
+}
+
+bool
+TransactionData::init_helper()
+{
+ initialize_default_sensitive_field();
+ const std::string sensitive_fields_string = get_sensitive_field_description();
+ TSDebug(debug_tag, "Sensitive fields for which generic values will be dumped: %s", sensitive_fields_string.c_str());
+
+ if (TS_SUCCESS !=
+ TSUserArgIndexReserve(TS_USER_ARGS_TXN, traffic_dump::debug_tag, "Track transaction related data", &transaction_arg_index)) {
+ TSError("[%s] Unable to initialize plugin (disabled). Failed to reserve transaction arg.", traffic_dump::debug_tag);
+ return false;
+ }
+
+ // Register the collecting of client-request headers at the global level so
+ // we can process requests before other plugins. (Global hooks are
+ // processed before session and transaction ones.)
+ TSCont txn_cont = TSContCreate(global_transaction_handler, nullptr);
+ TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, txn_cont);
+ return true;
+}
+
+// Transaction handler: writes headers to the log file using AIO
+int
+TransactionData::global_transaction_handler(TSCont contp, TSEvent event, void *edata)
+{
+ TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
+
+ // Retrieve SessionData
+ TSHttpSsn ssnp = TSHttpTxnSsnGet(txnp);
+ SessionData *ssnData = static_cast<SessionData *>(TSUserArgGet(ssnp, SessionData::get_session_arg_index()));
+
+ // If no valid ssnData, continue transaction as if nothing happened. This transaction
+ // must be filtered out by our filter criteria.
+ if (!ssnData) {
+ TSDebug(debug_tag, "session_txn_handler(): No ssnData found. Abort.");
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ return TS_SUCCESS;
+ }
+
+ switch (event) {
+ case TS_EVENT_HTTP_TXN_START: {
+ // We will piece together JSON content accumulated across several hooks of
+ // the transaction. The catch is that hooks across transactions in a
+ // session may fire interleaved in HTTP/2. Thus, in order to get
+ // non-garbled JSON content, we accumulate the data for an entire
+ // transaction and write that atomically once the transaction is completed.
+ TransactionData *txnData = new TransactionData;
+ TSUserArgSet(txnp, transaction_arg_index, txnData);
+ // Get UUID
+ char uuid[TS_CRUUID_STRING_LEN + 1];
+ TSAssert(TS_SUCCESS == TSClientRequestUuidGet(txnp, uuid));
+ std::string_view uuid_view{uuid, strnlen(uuid, TS_CRUUID_STRING_LEN)};
+
+ // Generate per transaction json records
+ txnData->txn_json += "{";
+ // "connection-time":(number)
+ TSHRTime start_time;
+ TSHttpTxnMilestoneGet(txnp, TS_MILESTONE_UA_BEGIN, &start_time);
+ txnData->txn_json += "\"connection-time\":" + std::to_string(start_time);
+
+ // "uuid":(string)
+ // The uuid is a header field for each message in the transaction. Use the
+ // "all" node to apply to each message.
+ std::string_view name = "uuid";
+ txnData->txn_json += ",\"all\":{\"headers\":{\"fields\":[" + json_entry_array(name, uuid_view);
+ txnData->txn_json += "]}}";
+ break;
+ }
+
+ case TS_EVENT_HTTP_READ_REQUEST_HDR: {
+ TransactionData *txnData = static_cast<TransactionData *>(TSUserArgGet(txnp, transaction_arg_index));
+ if (!txnData) {
+ TSError("[%s] No transaction data found for the header hook we registered for.", traffic_dump::debug_tag);
+ break;
+ }
+ // This hook is registered globally, not at TS_EVENT_HTTP_SSN_START in
+ // global_session_handler(). As such, this handler will be called with every
+ // transaction. However, we know that we are dumping this transaction
+ // because there is a ssnData associated with it.
+
+ // We must grab the client request information before remap happens because
+ // the remap process modifies the request buffer.
+ TSMBuffer buffer;
+ TSMLoc hdr_loc;
+ if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &buffer, &hdr_loc)) {
+ TSDebug(debug_tag, "Found client request");
+ // We don't have an accurate view of the body size until TXN_CLOSE so we hold
+ // off on writing the content:size node until then.
+ txnData->txn_json += R"(,"client-request":)" + txnData->write_message_node_no_content(buffer, hdr_loc);
+ TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
+ buffer = nullptr;
+ }
+ break;
+ }
+
+ case TS_EVENT_HTTP_TXN_CLOSE: {
+ TransactionData *txnData = static_cast<TransactionData *>(TSUserArgGet(txnp, SessionData::get_session_arg_index()));
+ if (!txnData) {
+ TSError("[%s] No transaction data found for the close hook we registered for.", traffic_dump::debug_tag);
+ break;
+ }
+ // proxy-request/response headers
+ TSMBuffer buffer;
+ TSMLoc hdr_loc;
+ if (TS_SUCCESS == TSHttpTxnClientReqGet(txnp, &buffer, &hdr_loc)) {
+ txnData->txn_json += txnData->write_content_node(TSHttpTxnClientReqBodyBytesGet(txnp)) + "}";
+ TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
+ buffer = nullptr;
+ }
+ if (TS_SUCCESS == TSHttpTxnServerReqGet(txnp, &buffer, &hdr_loc)) {
+ TSDebug(debug_tag, "Found proxy request");
+ txnData->txn_json +=
+ R"(,"proxy-request":)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnServerReqBodyBytesGet(txnp));
+ TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
+ buffer = nullptr;
+ }
+ if (TS_SUCCESS == TSHttpTxnServerRespGet(txnp, &buffer, &hdr_loc)) {
+ TSDebug(debug_tag, "Found server response");
+ txnData->txn_json +=
+ R"(,"server-response":)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnServerRespBodyBytesGet(txnp));
+ TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
+ buffer = nullptr;
+ }
+ if (TS_SUCCESS == TSHttpTxnClientRespGet(txnp, &buffer, &hdr_loc)) {
+ TSDebug(debug_tag, "Found proxy response");
+ txnData->txn_json +=
+ R"(,"proxy-response":)" + txnData->write_message_node(buffer, hdr_loc, TSHttpTxnClientRespBodyBytesGet(txnp));
+ TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr_loc);
+ buffer = nullptr;
+ }
+
+ txnData->txn_json += "}";
+ ssnData->write_transaction_to_disk(txnData->txn_json);
+ delete txnData;
+ break;
+ }
+ default:
+ TSDebug(debug_tag, "session_txn_handler(): Unhandled events %d", event);
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
+ return TS_ERROR;
+ }
+
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ return TS_SUCCESS;
+}
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/transaction_data.h b/plugins/experimental/traffic_dump/transaction_data.h
new file mode 100644
index 0000000..c5b5139
--- /dev/null
+++ b/plugins/experimental/traffic_dump/transaction_data.h
@@ -0,0 +1,110 @@
+/** @txn_data.h
+ Traffic Dump data specific to transactions.
+ @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 <string>
+
+#include "ts/ts.h"
+
+#include "sensitive_fields.h"
+
+namespace traffic_dump
+{
+/** The information associated with an single transaction.
+ *
+ * This class is responsible for containing the members associated with a
+ * particular transaction and defines the transaction handler callback.
+ */
+class TransactionData
+{
+private:
+ /** The string for the JSON content of this transaction. */
+ std::string txn_json;
+
+ // The index to be used for the TS API for storing this TransactionData on a
+ // per-transaction basis.
+ static int transaction_arg_index;
+
+ /// The set of fields, default and user-specified, that are sensitive and
+ /// whose values will be replaced with auto-generated generic content.
+ static sensitive_fields_t sensitive_fields;
+
+public:
+ /** Initialize TransactionData, using the provided sensitive fields.
+ *
+ * @return True if initialization is successful, false otherwise.
+ */
+ static bool init(sensitive_fields_t &&sensitive_fields);
+
+ /** Initialize TransactionData, using default sensitive fields.
+ * @return True if initialization is successful, false otherwise.
+ */
+ static bool init();
+
+ /// Read the txn information from TSMBuffer and write the header information.
+ /// This function does not write the content node.
+ std::string write_message_node_no_content(TSMBuffer &buffer, TSMLoc &hdr_loc);
+
+ /// Read the txn information from TSMBuffer and write the header information including
+ /// the content node describing the body characteristics.
+ std::string write_message_node(TSMBuffer &buffer, TSMLoc &hdr_loc, int64_t num_body_bytes);
+
+ /// The handler callback for transaction events.
+ static int global_transaction_handler(TSCont contp, TSEvent event, void *edata);
+
+private:
+ /** Common logic for the init overloads. */
+ static bool init_helper();
+
+ /** Initialize the generic sensitive field to be dumped. This is used instead
+ * of the sensitive field values seen on the wire.
+ */
+ static void initialize_default_sensitive_field();
+
+ /** Return a separated string representing the HTTP fields considered sensitive.
+ *
+ * @return A comma-separated string representing the sensitive HTTP fields.
+ */
+ static std::string get_sensitive_field_description();
+
+ /** Inspect the field to see whether it is sensitive and return a generic value
+ * of equal size to the original if it is.
+ *
+ * @param[in] name The field name to inspect.
+ * @param[in] original_value The field value to inspect.
+ *
+ * @return The value traffic_dump should dump for the given field.
+ */
+ std::string_view replace_sensitive_fields(std::string_view name, std::string_view original_value);
+
+ /// Write the content JSON node for an HTTP message.
+ //
+ /// "content"
+ /// "encoding"
+ /// "size"
+ std::string write_content_node(int64_t num_body_bytes);
+
+ /** Remove the scheme prefix from the url.
+ *
+ * @return The view without the scheme prefix.
+ */
+ std::string_view remove_scheme_prefix(std::string_view url);
+};
+
+} // namespace traffic_dump
diff --git a/plugins/experimental/traffic_dump/unit_tests/test_json_utils.cc b/plugins/experimental/traffic_dump/unit_tests/test_json_utils.cc
new file mode 100644
index 0000000..4c27a91
--- /dev/null
+++ b/plugins/experimental/traffic_dump/unit_tests/test_json_utils.cc
@@ -0,0 +1,59 @@
+/*
+ * 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 "json_utils.h"
+
+#include <cstring>
+#include <catch.hpp>
+
+using namespace traffic_dump;
+
+TEST_CASE("JsonUtilsMap", "[json_entry]")
+{
+ SECTION("Test the string_view value overload")
+ {
+ CHECK(std::string(R"("name":"value")") == json_entry("name", "value"));
+ CHECK(std::string(R"("":"value")") == json_entry("", "value"));
+ CHECK(std::string(R"("name":"")") == json_entry("name", ""));
+ }
+
+ SECTION("Test the char buffer value overload")
+ {
+ CHECK(std::string(R"("name":"value")") == json_entry("name", "value", strlen("value")));
+ CHECK(std::string(R"("name":"val")") == json_entry("name", "value", 3));
+ CHECK(std::string(R"("":"value")") == json_entry("", "value", strlen("value")));
+ CHECK(std::string(R"("name":"")") == json_entry("name", "", 0));
+ }
+
+ SECTION("Test that escaped characters are encoded as expected")
+ {
+ // Note that the raw strings on the left, i.e., R"(...)", leaves "\b" as
+ // two characters, not a single escaped one. The escaped characters on
+ // the right, by contrast, such as '\b', is a single escaped character.
+ CHECK(std::string(R"("name":"val\bue")") == json_entry("name", "val\bue"));
+ CHECK(std::string(R"("name":"\\value")") == json_entry("name", "\\value"));
+ CHECK(std::string(R"("name":"value\f")") == json_entry("name", "value\f"));
+ CHECK(std::string(R"("na\rme":"\tva\nlue\f")") == json_entry("na\rme", "\tva\nlue\f"));
+ CHECK(std::string(R"("\r":"\t\n\f")") == json_entry("\r", "\t\n\f"));
+ }
+}
+
+TEST_CASE("JsonUtilsArray", "[json_entry_array]")
+{
+ CHECK(std::string(R"(["name","value"])") == json_entry_array("name", "value"));
+}
diff --git a/plugins/experimental/traffic_dump/unit_tests/test_sensitive_fields.cc b/plugins/experimental/traffic_dump/unit_tests/test_sensitive_fields.cc
new file mode 100644
index 0000000..19845c9
--- /dev/null
+++ b/plugins/experimental/traffic_dump/unit_tests/test_sensitive_fields.cc
@@ -0,0 +1,37 @@
+/*
+ * 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 "sensitive_fields.h"
+
+#include <cstring>
+#include <catch.hpp>
+
+using namespace traffic_dump;
+
+TEST_CASE("SensitiveFields", "[sensitive_fields_t]")
+{
+ sensitive_fields_t test_fields = {"one", "tWO", "THREE"};
+ CHECK(test_fields.count("one") == 1);
+ CHECK(test_fields.count("oNe") == 1);
+
+ CHECK(test_fields.count("tWO") == 1);
+ CHECK(test_fields.count("two") == 1);
+
+ CHECK(test_fields.count("THREE") == 1);
+ CHECK(test_fields.count("three") == 1);
+}
diff --git a/plugins/experimental/traffic_dump/unit_tests/unit_test_main.cc b/plugins/experimental/traffic_dump/unit_tests/unit_test_main.cc
new file mode 100644
index 0000000..6aed3a6
--- /dev/null
+++ b/plugins/experimental/traffic_dump/unit_tests/unit_test_main.cc
@@ -0,0 +1,25 @@
+/** @file
+
+ This file used for catch based tests. It is the main() stub.
+
+ @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.
+ */
+
+#define CATCH_CONFIG_MAIN
+#include "catch.hpp"
diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold b/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold
index 0ac91ba..1fe8660 100644
--- a/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold
+++ b/tests/gold_tests/pluginTest/traffic_dump/gold/200.gold
@@ -8,6 +8,6 @@
< Content-Length: 0
< Set-Cookie: classified_not_for_logging
< Date: ``
-< Age: ``
+``
< Server: ATS/``
``
diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/4_byte_response_body.gold b/tests/gold_tests/pluginTest/traffic_dump/gold/4_byte_response_body.gold
new file mode 100644
index 0000000..81846e6
--- /dev/null
+++ b/tests/gold_tests/pluginTest/traffic_dump/gold/4_byte_response_body.gold
@@ -0,0 +1,7 @@
+``
+> GET /cache_test HTTP/1.1
+``
+< HTTP/1.1 200 OK
+< Cache-Control: max-age=300
+< Content-Length: 4
+``
diff --git a/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold b/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold
new file mode 100644
index 0000000..9ac048c
--- /dev/null
+++ b/tests/gold_tests/pluginTest/traffic_dump/gold/two_transactions.gold
@@ -0,0 +1,11 @@
+``
+> GET /first HTTP/1.1
+> Host: www.example.com
+``
+< HTTP/1.1 200 OK
+``
+> GET /second HTTP/1.1
+> Host: www.example.com
+``
+< HTTP/1.1 200 OK
+``
diff --git a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py
index 5cf9f24..7ba42f2 100644
--- a/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py
+++ b/tests/gold_tests/pluginTest/traffic_dump/traffic_dump.test.py
@@ -29,7 +29,7 @@ Test.SkipUnless(
# Configure the origin server.
server = Test.MakeOriginServer("server")
-request_header = {"headers": "GET / HTTP/1.1\r\n"
+request_header = {"headers": "GET /one HTTP/1.1\r\n"
"Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
"timestamp": "1469733493.993", "body": ""}
response_header = {"headers": "HTTP/1.1 200 OK"
@@ -37,7 +37,15 @@ response_header = {"headers": "HTTP/1.1 200 OK"
"\r\nSet-Cookie: classified_not_for_logging\r\n\r\n",
"timestamp": "1469733493.993", "body": ""}
server.addResponse("sessionfile.log", request_header, response_header)
-request_header = {"headers": "GET /one HTTP/1.1\r\n"
+request_header = {"headers": "GET /two HTTP/1.1\r\n"
+ "Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
+ "timestamp": "1469733493.993", "body": ""}
+response_header = {"headers": "HTTP/1.1 200 OK"
+ "\r\nConnection: close\r\nContent-Length: 0"
+ "\r\nSet-Cookie: classified_not_for_logging\r\n\r\n",
+ "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionfile.log", request_header, response_header)
+request_header = {"headers": "GET /three HTTP/1.1\r\n"
"Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
"timestamp": "1469733493.993", "body": ""}
response_header = {"headers": "HTTP/1.1 200 OK"
@@ -52,13 +60,38 @@ response_header = {"headers": "HTTP/1.1 200 OK"
"\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
"timestamp": "1469733493.993", "body": ""}
server.addResponse("sessionfile.log", request_header, response_header)
+request_header = {"headers": "GET /cache_test HTTP/1.1\r\n"
+ "Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
+ "timestamp": "1469733493.993", "body": ""}
+response_header = {"headers": "HTTP/1.1 200 OK"
+ "\r\nConnection: close\r\nCache-Control: max-age=300\r\n"
+ "Content-Length: 4\r\n\r\n",
+ "timestamp": "1469733493.993", "body": "1234"}
+server.addResponse("sessionfile.log", request_header, response_header)
+request_header = {"headers": "GET /first HTTP/1.1\r\n"
+ "Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
+ "timestamp": "1469733493.993", "body": ""}
+response_header = {"headers": "HTTP/1.1 200 OK"
+ "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
+ "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionfile.log", request_header, response_header)
+request_header = {"headers": "GET /second HTTP/1.1\r\n"
+ "Host: www.example.com\r\nContent-Length: 0\r\n\r\n",
+ "timestamp": "1469733493.993", "body": ""}
+response_header = {"headers": "HTTP/1.1 200 OK"
+ "\r\nConnection: close\r\nContent-Length: 0\r\n\r\n",
+ "timestamp": "1469733493.993", "body": ""}
+server.addResponse("sessionfile.log", request_header, response_header)
-# Define ATS and configure
+# Define ATS and configure it.
ts = Test.MakeATSProcess("ts")
replay_dir = os.path.join(ts.RunDirectory, "ts", "log")
ts.Disk.records_config.update({
'proxy.config.diags.debug.enabled': 1,
'proxy.config.diags.debug.tags': 'traffic_dump',
+ 'proxy.config.http.cache.http': 1,
+ 'proxy.config.http.wait_for_cache': 1,
+ 'proxy.config.http.insert_age_in_response': 0,
})
ts.Disk.remap_config.AddLine(
'map / http://127.0.0.1:{0}'.format(server.Variables.Port)
@@ -68,6 +101,17 @@ ts.Disk.plugin_config.AddLine(
'traffic_dump.so --logdir {0} --sample 1 --limit 1000000000 '
'--sensitive-fields "cookie,set-cookie,x-request-1,x-request-2"'.format(replay_dir)
)
+# Configure logging of transactions. This is helpful for the cache test below.
+ts.Disk.logging_yaml.AddLines(
+ '''
+logging:
+ formats:
+ - name: basic
+ format: "%<cluc>: Read result: %<crc>:%<crsc>:%<chm>, Write result: %<cwr>"
+ logs:
+ - filename: transactions
+ format: basic
+'''.split('\n'))
# Set up trafficserver expectations.
ts.Disk.diags_log.Content = Testers.ContainsExpression(
@@ -92,6 +136,12 @@ replay_file_session_3 = os.path.join(replay_dir, "127", "0000000000000002")
ts.Disk.File(replay_file_session_3, exists=True)
replay_file_session_4 = os.path.join(replay_dir, "127", "0000000000000003")
ts.Disk.File(replay_file_session_4, exists=True)
+replay_file_session_5 = os.path.join(replay_dir, "127", "0000000000000004")
+ts.Disk.File(replay_file_session_5, exists=True)
+replay_file_session_6 = os.path.join(replay_dir, "127", "0000000000000005")
+ts.Disk.File(replay_file_session_6, exists=True)
+replay_file_session_7 = os.path.join(replay_dir, "127", "0000000000000006")
+ts.Disk.File(replay_file_session_7, exists=True)
#
# Test 1: Verify the correct behavior of two transactions across two sessions.
@@ -103,19 +153,19 @@ tr = Test.AddTestRun("First transaction")
tr.Processes.Default.StartBefore(server, ready=When.PortOpen(server.Variables.Port))
tr.Processes.Default.StartBefore(Test.Processes.ts)
tr.Processes.Default.Command = \
- ('curl --http1.1 http://127.0.0.1:{0} -H"Cookie: donotlogthis" '
+ ('curl --http1.1 http://127.0.0.1:{0}/one -H"Cookie: donotlogthis" '
'-H"Host: www.example.com" -H"X-Request-1: ultra_sensitive" --verbose'.format(
ts.Variables.port))
tr.Processes.Default.ReturnCode = 0
tr.Processes.Default.Streams.stderr = "gold/200.gold"
tr.StillRunningAfter = server
tr.StillRunningAfter = ts
-session_1_protocols = "tcp,ipv4"
+http_protocols = "tcp,ipv4"
# Execute the second transaction.
tr = Test.AddTestRun("Second transaction")
tr.Processes.Default.Command = \
- ('curl http://127.0.0.1:{0}/one -H"Host: www.example.com" '
+ ('curl http://127.0.0.1:{0}/two -H"Host: www.example.com" '
'-H"X-Request-2: also_very_sensitive" --verbose'.format(
ts.Variables.port))
tr.Processes.Default.ReturnCode = 0
@@ -137,7 +187,7 @@ tr.Processes.Default.Command = 'python3 {0} {1} {2} {3} --client-protocols "{4}"
os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'),
replay_file_session_1,
sensitive_fields_arg,
- session_1_protocols)
+ http_protocols)
tr.Processes.Default.ReturnCode = 0
tr.StillRunningAfter = server
tr.StillRunningAfter = ts
@@ -145,7 +195,7 @@ tr.StillRunningAfter = ts
# Verify the properties of the replay file for the second transaction.
tr = Test.AddTestRun("Verify the json content of the second session")
tr.Setup.CopyAs(verify_replay, Test.RunDirectory)
-tr.Processes.Default.Command = "python3 {0} {1} {2} {3} --request-target '/one'".format(
+tr.Processes.Default.Command = "python3 {0} {1} {2} {3} --request-target '/two'".format(
verify_replay,
os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'),
replay_file_session_2,
@@ -163,7 +213,7 @@ tr = Test.AddTestRun("Make a request with an explicit target.")
request_target = "http://localhost:{0}/candy".format(ts.Variables.port)
tr.Processes.Default.Command = (
'curl --request-target "{0}" '
- 'http://127.0.0.1:{1} -H"Host: www.example.com" --verbose'.format(
+ 'http://127.0.0.1:{1}/three -H"Host: www.example.com" --verbose'.format(
request_target, ts.Variables.port))
tr.Processes.Default.ReturnCode = 0
tr.Processes.Default.Streams.stderr = "gold/explicit_target.gold"
@@ -187,7 +237,6 @@ tr.StillRunningAfter = ts
# Test 3: Verify correct handling of a POST with body data.
#
-# Verify that an explicit path in the request line is recorded.
tr = Test.AddTestRun("Make a POST request with a body.")
request_target = "http://localhost:{0}/post_with_body".format(ts.Variables.port)
@@ -216,3 +265,59 @@ tr.Processes.Default.Command = \
tr.Processes.Default.ReturnCode = 0
tr.StillRunningAfter = server
tr.StillRunningAfter = ts
+
+#
+# Test 4: Verify correct handling of a response produced out of the cache.
+#
+tr = Test.AddTestRun("Make a request for an uncached object.")
+tr.Processes.Default.Command = \
+ ('curl --http1.1 http://127.0.0.1:{0}/cache_test -H"Host: www.example.com" --verbose'.format(
+ ts.Variables.port))
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stderr = "gold/4_byte_response_body.gold"
+tr.StillRunningAfter = server
+tr.StillRunningAfter = ts
+
+tr = Test.AddTestRun("Repeat the previous request: should be cached now.")
+tr.Processes.Default.Command = \
+ ('curl --http1.1 http://127.0.0.1:{0}/cache_test -H"Host: www.example.com" --verbose'.format(
+ ts.Variables.port))
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stderr = "gold/4_byte_response_body.gold"
+tr.StillRunningAfter = server
+tr.StillRunningAfter = ts
+
+tr = Test.AddTestRun("Verify that the cached response's replay file looks appropriate.")
+tr.Setup.CopyAs(verify_replay, Test.RunDirectory)
+tr.Processes.Default.Command = 'python3 {0} {1} {2} --client-protocols "{3}"'.format(
+ verify_replay,
+ os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'),
+ replay_file_session_6,
+ http_protocols)
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = server
+tr.StillRunningAfter = ts
+
+#
+# Test 5: Verify correct handling of two transactions in a session.
+#
+tr = Test.AddTestRun("Conduct two transactions in the same session.")
+tr.Processes.Default.Command = \
+ ('curl --http1.1 http://127.0.0.1:{0}/first -H"Host: www.example.com" --verbose --next '
+ 'curl --http1.1 http://127.0.0.1:{0}/second -H"Host: www.example.com" --verbose'
+ .format(ts.Variables.port))
+tr.Processes.Default.ReturnCode = 0
+tr.Processes.Default.Streams.stderr = "gold/two_transactions.gold"
+tr.StillRunningAfter = server
+tr.StillRunningAfter = ts
+
+tr = Test.AddTestRun("Verify that the dump file can be read.")
+tr.Setup.CopyAs(verify_replay, Test.RunDirectory)
+tr.Processes.Default.Command = 'python3 {0} {1} {2} --client-protocols "{3}"'.format(
+ verify_replay,
+ os.path.join(Test.Variables.AtsTestToolsDir, 'lib', 'replay_schema.json'),
+ replay_file_session_7,
+ http_protocols)
+tr.Processes.Default.ReturnCode = 0
+tr.StillRunningAfter = server
+tr.StillRunningAfter = ts
diff --git a/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py b/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py
index b48ae4a..e48819c 100644
--- a/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py
+++ b/tests/gold_tests/pluginTest/traffic_dump/verify_replay.py
@@ -51,8 +51,8 @@ def validate_json(schema_json, replay_json):
"""
try:
jsonschema.validate(instance=replay_json, schema=schema_json)
- except jsonschema.ValidationError:
- print("The replay file does not validate against the schema.")
+ except jsonschema.ValidationError as e:
+ print("The replay file does not validate against the schema: {}".format(e))
return False
else:
return True
@@ -72,7 +72,7 @@ def verify_there_was_a_transaction(replay_json):
print("There are no transactions in the replay file.")
return False
transaction = transactions[0]
- if not ('client-request' in transaction and 'server-response' in transaction):
+ if not ('client-request' in transaction and 'proxy-response' in transaction):
print("There was not request and response in the transaction of the replay file.")
return False
diff --git a/tests/tools/lib/replay_schema.json b/tests/tools/lib/replay_schema.json
index 5e15281..328c13d 100644
--- a/tests/tools/lib/replay_schema.json
+++ b/tests/tools/lib/replay_schema.json
@@ -41,7 +41,7 @@
"items": {
"description": "Transaction",
"type": "object",
- "required": [ "client-request", "server-response" ],
+ "required": [ "client-request", "proxy-response" ],
"properties": {
"uuid": {
"description": "UUID to identify this specific transaction.",