This is an automated email from the ASF dual-hosted git repository.
bneradt 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 6a31675855 Implement probe-full-json feature for xdebug plugin (#12460)
6a31675855 is described below
commit 6a31675855358fafd09053d639433fddd5304750
Author: Brian Neradt <[email protected]>
AuthorDate: Tue Aug 19 13:28:00 2025 -0500
Implement probe-full-json feature for xdebug plugin (#12460)
* Add an xdebug probe test
* xdebug: use header files; xdebug namespace
* Implement probe-full-json feature for xdebug plugin
The probe-full-json directive emits the same data as probe but in a
complete JSON object containing nodes for client/server request headers,
response body, and client/server response headers.
---
doc/admin-guide/plugins/xdebug.en.rst | 69 ++++++++-
plugins/xdebug/CMakeLists.txt | 2 +-
plugins/xdebug/xdebug.cc | 168 +++++++++++----------
plugins/xdebug/xdebug_headers.cc | 139 +++++++++++++----
plugins/xdebug/xdebug_headers.h | 83 ++++++++++
plugins/xdebug/xdebug_transforms.cc | 58 ++++++-
plugins/xdebug/xdebug_transforms.h | 51 +++++++
plugins/xdebug/xdebug_types.h | 58 +++++++
.../pluginTest/xdebug/x_probe/x_probe.replay.yaml | 58 +++++++
.../pluginTest/xdebug/x_probe/x_probe.test.py | 80 ++++++++++
.../xdebug/x_probe_full_json/gold/jq.gold | 3 +
.../x_probe_full_json.replay.yaml | 57 +++++++
.../x_probe_full_json/x_probe_full_json.test.py | 98 ++++++++++++
13 files changed, 808 insertions(+), 116 deletions(-)
diff --git a/doc/admin-guide/plugins/xdebug.en.rst
b/doc/admin-guide/plugins/xdebug.en.rst
index 987477cbb8..8946f03959 100644
--- a/doc/admin-guide/plugins/xdebug.en.rst
+++ b/doc/admin-guide/plugins/xdebug.en.rst
@@ -53,9 +53,18 @@ selectively by passing header names to ``--enable`` option.
--enable=x-remap,x-cache
This enables ``X-Remap`` and ``X-Cache``. If a client's request has
-``X-Debug: x-remap, x-cache, probe``, XDebug will only injects ``X-Reamp`` and
+``X-Debug: x-remap, x-cache, probe``, XDebug will only inject ``X-Remap`` and
``X-Cache``.
+To enable the JSON transaction header probe functionality:
+
+::
+
+ --enable=probe-full-json
+
+This allows clients to request ``X-Debug: probe-full-json`` to receive request
+and response header information in a structured JSON format.
+
Debugging Headers
=================
@@ -87,6 +96,64 @@ Probe
- Response Headers from Proxy B -> Proxy A
- Response Headers from Proxy A -> Client
+Probe-Full-JSON
+ Similar to `Probe` but formats the output as a complete JSON object
+ containing request and response headers. The response body is modified to
+ include client request headers, proxy request headers, the original server
+ response body, server response headers, and proxy response headers in a
+ structured JSON format. In contrast to Probe, the response content with
+ this feature is parsable with JSON parsing tools like ``jq``. Because the
+ body is altered, it disables writing to cache and changes the Content-Type
+ to ``application/json``.
+
+ JSON Nodes:
+
+ - ``client-request``: Headers from the client to the proxy.
+ - ``proxy-request``: Headers from the proxy to the origin server (if
applicable).
+ - ``server-body``: The original response body content from the origin
server.
+ - ``server-response``: Headers from the origin server to the proxy.
+ - ``proxy-response``: Headers from the proxy to the client.
+
+ Here's an example of the JSON output from the `x_probe_full_json` test::
+
+ $ curl -s -H"uuid: 1" -H "Host: example.com" -H "X-Debug:
probe-full-json" http://127.0.0.1:61003/test | jq
+ {
+ "client-request": {
+ "start-line": "GET http://127.0.0.1:61000/test HTTP/1.1",
+ "uuid": "1",
+ "host": "127.0.0.1:61000",
+ "x-request": "from-client"
+ },
+ "proxy-request": {
+ "start-line": "GET /test HTTP/1.1",
+ "uuid": "1",
+ "host": "127.0.0.1:61000",
+ "x-request": "from-client",
+ "Client-ip": "127.0.0.1",
+ "X-Forwarded-For": "127.0.0.1",
+ "Via": "http/1.1
traffic_server[f47ffc16-0a20-441e-b17d-6e3cb044e025]
(ApacheTrafficServer/10.2.0)"
+ },
+ "server-body": "Original server response",
+ "server-response": {
+ "start-line": "HTTP/1.1 200 ",
+ "content-type": "text/html",
+ "content-length": "24",
+ "x-response": "from-origin",
+ "Date": "Tue, 19 Aug 2025 15:02:07 GMT"
+ },
+ "proxy-response": {
+ "start-line": "HTTP/1.1 200 OK",
+ "content-type": "application/json",
+ "x-response": "from-origin",
+ "Date": "Tue, 19 Aug 2025 15:02:07 GMT",
+ "Age": "0",
+ "Transfer-Encoding": "chunked",
+ "Connection": "keep-alive",
+ "Server": "ATS/10.2.0",
+ "X-Original-Content-Type": "text/html"
+ }
+ }
+
X-Cache-Key
The ``X-Cache-Key`` header contains the URL that identifies the HTTP
object in the
Traffic Server cache. This header is particularly useful if a custom cache
diff --git a/plugins/xdebug/CMakeLists.txt b/plugins/xdebug/CMakeLists.txt
index 17ae23e7de..506617617c 100644
--- a/plugins/xdebug/CMakeLists.txt
+++ b/plugins/xdebug/CMakeLists.txt
@@ -15,6 +15,6 @@
#
#######################
-add_atsplugin(xdebug xdebug.cc)
+add_atsplugin(xdebug xdebug.cc xdebug_headers.cc xdebug_transforms.cc)
target_link_libraries(xdebug PRIVATE libswoc::libswoc)
verify_global_plugin(xdebug)
diff --git a/plugins/xdebug/xdebug.cc b/plugins/xdebug/xdebug.cc
index ecc36ec8f5..ea97c6b805 100644
--- a/plugins/xdebug/xdebug.cc
+++ b/plugins/xdebug/xdebug.cc
@@ -16,13 +16,12 @@
* limitations under the License.
*/
+#include <climits>
#include <cstdlib>
#include <cstdio>
#include <cstdio>
#include <strings.h>
-#include <sstream>
#include <cstring>
-#include <atomic>
#include <memory>
#include <getopt.h>
#include <cstdint>
@@ -37,37 +36,21 @@
#include "swoc/TextView.h"
#include "tscpp/api/Cleanup.h"
-namespace
-{
-DbgCtl dbg_ctl{"xdebug"};
-
-struct BodyBuilder {
- atscppapi::TSContUniqPtr transform_connp;
- atscppapi::TSIOBufferUniqPtr output_buffer;
- // It's important that output_reader comes after output_buffer so it will be
deleted first.
- atscppapi::TSIOBufferReaderUniqPtr output_reader;
- TSVIO output_vio = nullptr;
- bool wrote_prebody = false;
- bool wrote_body = false;
- bool hdr_ready = false;
- std::atomic_flag wrote_postbody;
-
- int64_t nbytes = 0;
-};
-
-struct XDebugTxnAuxData {
- std::unique_ptr<BodyBuilder> body_builder;
- unsigned xheaders = 0;
-};
+#include "xdebug_types.h"
+#include "xdebug_headers.h"
+#include "xdebug_transforms.h"
-atscppapi::TxnAuxMgrData mgrData;
-
-using AuxDataMgr = atscppapi::TxnAuxDataMgr<XDebugTxnAuxData, mgrData>;
+namespace xdebug
+{
+namespace
+{
+ DbgCtl dbg_ctl{"xdebug"};
+ DbgCtl dbg_ctl_xform{"xdebug_transform"};
} // end anonymous namespace
-#include "xdebug_headers.cc"
-#include "xdebug_transforms.cc"
+// Definition of the auxiliary data manager
+atscppapi::TxnAuxMgrData mgrData;
static struct {
const char *str;
@@ -75,57 +58,60 @@ static struct {
} xDebugHeader = {nullptr, 0};
enum {
- XHEADER_X_CACHE_KEY = 1u << 2,
- XHEADER_X_MILESTONES = 1u << 3,
- XHEADER_X_CACHE = 1u << 4,
- XHEADER_X_GENERATION = 1u << 5,
- XHEADER_X_TRANSACTION_ID = 1u << 6,
- XHEADER_X_DUMP_HEADERS = 1u << 7,
- XHEADER_X_REMAP = 1u << 8,
- XHEADER_X_PROBE_HEADERS = 1u << 9,
- XHEADER_X_PSELECT_KEY = 1u << 10,
- XHEADER_X_CACHE_INFO = 1u << 11,
- XHEADER_X_EFFECTIVE_URL = 1u << 12,
- XHEADER_VIA = 1u << 13,
- XHEADER_DIAGS = 1u << 14,
- XHEADER_ALL = UINT_MAX
+ XHEADER_X_CACHE_KEY = 1u << 2,
+ XHEADER_X_MILESTONES = 1u << 3,
+ XHEADER_X_CACHE = 1u << 4,
+ XHEADER_X_GENERATION = 1u << 5,
+ XHEADER_X_TRANSACTION_ID = 1u << 6,
+ XHEADER_X_DUMP_HEADERS = 1u << 7,
+ XHEADER_X_REMAP = 1u << 8,
+ XHEADER_X_PROBE_HEADERS = 1u << 9,
+ XHEADER_X_PSELECT_KEY = 1u << 10,
+ XHEADER_X_CACHE_INFO = 1u << 11,
+ XHEADER_X_EFFECTIVE_URL = 1u << 12,
+ XHEADER_VIA = 1u << 13,
+ XHEADER_DIAGS = 1u << 14,
+ XHEADER_X_PROBE_FULL_JSON = 1u << 15,
+ XHEADER_ALL = UINT_MAX
};
static unsigned int allowedHeaders = 0;
-constexpr std::string_view HEADER_NAME_X_CACHE_KEY = "x-cache-key";
-constexpr std::string_view HEADER_NAME_X_MILESTONES = "x-milestones";
-constexpr std::string_view HEADER_NAME_X_CACHE = "x-cache";
-constexpr std::string_view HEADER_NAME_X_GENERATION = "x-cache-generation";
-constexpr std::string_view HEADER_NAME_X_TRANSACTION_ID = "x-transaction-id";
-constexpr std::string_view HEADER_NAME_X_DUMP_HEADERS = "x-dump-headers";
-constexpr std::string_view HEADER_NAME_X_REMAP = "x-remap";
-constexpr std::string_view HEADER_NAME_X_PROBE_HEADERS = "probe";
-constexpr std::string_view HEADER_NAME_X_PSELECT_KEY =
"x-parentselection-key";
-constexpr std::string_view HEADER_NAME_X_CACHE_INFO = "x-cache-info";
-constexpr std::string_view HEADER_NAME_X_EFFECTIVE_URL = "x-effective-url";
-constexpr std::string_view HEADER_NAME_VIA = "via";
-constexpr std::string_view HEADER_NAME_DIAGS = "diags";
-constexpr std::string_view HEADER_NAME_ALL = "all";
+constexpr std::string_view HEADER_NAME_X_CACHE_KEY = "x-cache-key";
+constexpr std::string_view HEADER_NAME_X_MILESTONES = "x-milestones";
+constexpr std::string_view HEADER_NAME_X_CACHE = "x-cache";
+constexpr std::string_view HEADER_NAME_X_GENERATION =
"x-cache-generation";
+constexpr std::string_view HEADER_NAME_X_TRANSACTION_ID = "x-transaction-id";
+constexpr std::string_view HEADER_NAME_X_DUMP_HEADERS = "x-dump-headers";
+constexpr std::string_view HEADER_NAME_X_REMAP = "x-remap";
+constexpr std::string_view HEADER_NAME_X_PROBE_HEADERS = "probe";
+constexpr std::string_view HEADER_NAME_X_PSELECT_KEY =
"x-parentselection-key";
+constexpr std::string_view HEADER_NAME_X_CACHE_INFO = "x-cache-info";
+constexpr std::string_view HEADER_NAME_X_EFFECTIVE_URL = "x-effective-url";
+constexpr std::string_view HEADER_NAME_VIA = "via";
+constexpr std::string_view HEADER_NAME_DIAGS = "diags";
+constexpr std::string_view HEADER_NAME_X_PROBE_FULL_JSON = "probe-full-json";
+constexpr std::string_view HEADER_NAME_ALL = "all";
constexpr struct XHeader {
std::string_view name;
unsigned int flag;
} header_flags[] = {
- {HEADER_NAME_X_CACHE_KEY, XHEADER_X_CACHE_KEY },
- {HEADER_NAME_X_MILESTONES, XHEADER_X_MILESTONES },
- {HEADER_NAME_X_CACHE, XHEADER_X_CACHE },
- {HEADER_NAME_X_GENERATION, XHEADER_X_GENERATION },
- {HEADER_NAME_X_TRANSACTION_ID, XHEADER_X_TRANSACTION_ID},
- {HEADER_NAME_X_DUMP_HEADERS, XHEADER_X_DUMP_HEADERS },
- {HEADER_NAME_X_REMAP, XHEADER_X_REMAP },
- {HEADER_NAME_X_PROBE_HEADERS, XHEADER_X_PROBE_HEADERS },
- {HEADER_NAME_X_PSELECT_KEY, XHEADER_X_PSELECT_KEY },
- {HEADER_NAME_X_CACHE_INFO, XHEADER_X_CACHE_INFO },
- {HEADER_NAME_X_EFFECTIVE_URL, XHEADER_X_EFFECTIVE_URL },
- {HEADER_NAME_VIA, XHEADER_VIA },
- {HEADER_NAME_DIAGS, XHEADER_DIAGS },
- {HEADER_NAME_ALL, XHEADER_ALL },
+ {HEADER_NAME_X_CACHE_KEY, XHEADER_X_CACHE_KEY },
+ {HEADER_NAME_X_MILESTONES, XHEADER_X_MILESTONES },
+ {HEADER_NAME_X_CACHE, XHEADER_X_CACHE },
+ {HEADER_NAME_X_GENERATION, XHEADER_X_GENERATION },
+ {HEADER_NAME_X_TRANSACTION_ID, XHEADER_X_TRANSACTION_ID },
+ {HEADER_NAME_X_DUMP_HEADERS, XHEADER_X_DUMP_HEADERS },
+ {HEADER_NAME_X_REMAP, XHEADER_X_REMAP },
+ {HEADER_NAME_X_PROBE_HEADERS, XHEADER_X_PROBE_HEADERS },
+ {HEADER_NAME_X_PSELECT_KEY, XHEADER_X_PSELECT_KEY },
+ {HEADER_NAME_X_CACHE_INFO, XHEADER_X_CACHE_INFO },
+ {HEADER_NAME_X_EFFECTIVE_URL, XHEADER_X_EFFECTIVE_URL },
+ {HEADER_NAME_VIA, XHEADER_VIA },
+ {HEADER_NAME_DIAGS, XHEADER_DIAGS },
+ {HEADER_NAME_X_PROBE_FULL_JSON, XHEADER_X_PROBE_FULL_JSON},
+ {HEADER_NAME_ALL, XHEADER_ALL },
};
static TSCont XInjectHeadersCont = nullptr;
@@ -447,7 +433,7 @@ InjectEffectiveURLHeader(TSHttpTxn txn, TSMBuffer buffer,
TSMLoc hdr)
}
static void
-InjectOriginalContentTypeHeader(TSMBuffer buffer, TSMLoc hdr)
+InjectOriginalContentTypeHeader(TSMBuffer buffer, TSMLoc hdr, bool
is_full_json)
{
TSMLoc ct_field = TSMimeHdrFieldFind(buffer, hdr,
TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE);
if (TS_NULL_MLOC != ct_field) {
@@ -466,7 +452,12 @@ InjectOriginalContentTypeHeader(TSMBuffer buffer, TSMLoc
hdr)
}
TSMimeHdrFieldValuesClear(buffer, hdr, ct_field);
- TSReleaseAssert(TSMimeHdrFieldValueStringSet(buffer, hdr, ct_field, -1,
"text/plain", lengthof("text/plain")) == TS_SUCCESS);
+ if (is_full_json) {
+ TSReleaseAssert(TSMimeHdrFieldValueStringSet(buffer, hdr, ct_field, -1,
"application/json", lengthof("application/json")) ==
+ TS_SUCCESS);
+ } else {
+ TSReleaseAssert(TSMimeHdrFieldValueStringSet(buffer, hdr, ct_field, -1,
"text/plain", lengthof("text/plain")) == TS_SUCCESS);
+ }
}
static void
@@ -587,10 +578,11 @@ XInjectResponseHeaders(TSCont /* contp */, TSEvent event,
void *edata)
log_headers(txn, buffer, hdr, "ClientResponse");
}
- if (xheaders & XHEADER_X_PROBE_HEADERS) {
- InjectOriginalContentTypeHeader(buffer, hdr);
+ if (xheaders & XHEADER_X_PROBE_HEADERS || xheaders &
XHEADER_X_PROBE_FULL_JSON) {
+ InjectOriginalContentTypeHeader(buffer, hdr, xheaders &
XHEADER_X_PROBE_FULL_JSON);
BodyBuilder *data = AuxDataMgr::data(txn).body_builder.get();
- Dbg(dbg_ctl_xform, "XInjectResponseHeaders(): client resp header ready");
+ Dbg(dbg_ctl_xform, "XInjectResponseHeaders(): client resp header ready
(probe-full-json: %d)",
+ xheaders & XHEADER_X_PROBE_FULL_JSON);
if (data == nullptr) {
TSHttpTxnReenable(txn, TS_EVENT_HTTP_ERROR);
return TS_ERROR;
@@ -696,6 +688,12 @@ XScanRequestHeaders(TSCont /* contp */, TSEvent event,
void *edata)
#define header_field_eq(name, vptr, vlen) (((int)name.size() == vlen) &&
(strncasecmp(name.data(), vptr, vlen) == 0))
+ // A couple convenience variables for probing.
+ bool const is_probe_headers =
+ header_field_eq(HEADER_NAME_X_PROBE_HEADERS, value, vsize) &&
(XHEADER_X_PROBE_HEADERS & allowedHeaders);
+ bool const is_probe_full_json =
+ header_field_eq(HEADER_NAME_X_PROBE_FULL_JSON, value, vsize) &&
(XHEADER_X_PROBE_FULL_JSON & allowedHeaders);
+
if (header_field_eq(HEADER_NAME_X_CACHE_KEY, value, vsize)) {
xheaders |= XHEADER_X_CACHE_KEY & allowedHeaders;
} else if (header_field_eq(HEADER_NAME_X_CACHE_INFO, value, vsize)) {
@@ -717,13 +715,18 @@ XScanRequestHeaders(TSCont /* contp */, TSEvent event,
void *edata)
// Enable diagnostics for DebugTxn()'s only
TSHttpTxnCntlSet(txn, TS_HTTP_CNTL_TXN_DEBUG, true);
- } else if (header_field_eq(HEADER_NAME_X_PROBE_HEADERS, value, vsize) &&
(XHEADER_X_PROBE_HEADERS & allowedHeaders)) {
- xheaders |= XHEADER_X_PROBE_HEADERS;
+ } else if (is_probe_headers || is_probe_full_json) {
+ // You can't do both at the same time. If both are passed, full JSON
takes precedence.
+ if (is_probe_headers && is_probe_full_json) {
+ TSError("[xdebug] Both probe-headers and probe-full-json are
enabled. Choosing probe-full-json.");
+ }
+ xheaders |= is_probe_full_json ? XHEADER_X_PROBE_FULL_JSON :
XHEADER_X_PROBE_HEADERS;
auto &auxData = AuxDataMgr::data(txn);
// prefix request headers and postfix response headers
BodyBuilder *data = new BodyBuilder();
+ data->probe_type = is_probe_full_json ? ProbeType::PROBE_FULL_JSON :
ProbeType::PROBE_STANDARD;
auxData.body_builder.reset(data);
TSVConn connp = TSTransformCreate(body_transform, txn);
@@ -839,11 +842,18 @@ updateAllowedHeaders(const char *optarg)
TSfree(list);
}
+} // namespace xdebug
+
void
TSPluginInit(int argc, const char *argv[])
{
+ using namespace xdebug;
+
Dbg(dbg_ctl, "initializing plugin");
+ // Initialize transforms module
+ init_transforms();
+
static const struct option longopt[] = {
{const_cast<char *>("header"), required_argument, nullptr, 'h' },
{const_cast<char *>("enable"), required_argument, nullptr, 'e' },
@@ -903,6 +913,4 @@ TSPluginInit(int argc, const char *argv[])
XDeleteDebugHdrCont = TSContCreate(XDeleteDebugHdr, nullptr);
TSReleaseAssert(XDeleteDebugHdrCont);
TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK,
TSContCreate(XScanRequestHeaders, nullptr));
-
- gethostname(Hostname, 1024);
}
diff --git a/plugins/xdebug/xdebug_headers.cc b/plugins/xdebug/xdebug_headers.cc
index f7edbd8ba4..c383b0f6c7 100644
--- a/plugins/xdebug/xdebug_headers.cc
+++ b/plugins/xdebug/xdebug_headers.cc
@@ -17,8 +17,10 @@
limitations under the License.
*/
+#include "xdebug_headers.h"
+
#include <cstdlib>
-#include <stdio.h>
+#include <cstdio>
#include <cstdio>
#include <strings.h>
#include <string_view>
@@ -28,14 +30,19 @@
#define DEBUG_TAG_LOG_HEADERS "xdebug.headers"
+namespace xdebug
+{
+
namespace
{
-DbgCtl dbg_ctl_hdrs{DEBUG_TAG_LOG_HEADERS};
+ DbgCtl dbg_ctl_hdrs{DEBUG_TAG_LOG_HEADERS};
}
class EscapeCharForJson
{
public:
+ EscapeCharForJson(bool full_json) : _full_json(full_json) {}
+
std::string_view
operator()(char const &c)
{
@@ -44,19 +51,23 @@ public:
}
if ((IN_NAME == _state) && (':' == c)) {
_state = BEFORE_VALUE;
- return {"' : '"};
+ if (_full_json) {
+ return {R"(":")"};
+ } else {
+ return {"' : '"};
+ }
}
if ('\r' == c) {
return {""};
}
if ('\n' == c) {
- std::string_view result{_after_value()};
+ std::string_view result{_after_value(_full_json)};
if (BEFORE_NAME == _state) {
return {""};
} else if (BEFORE_VALUE == _state) {
// Failsafe -- missing value -- this should never happen.
- result = _missing_value();
+ result = _missing_value(_full_json);
}
_state = BEFORE_NAME;
return result;
@@ -87,50 +98,70 @@ public:
// After last header line, back up and throw away everything but the closing
quote.
//
static std::size_t
- backup()
+ backup(bool full_json)
{
- return _after_value().size() - 1;
+ return _after_value(full_json).size() - 1;
}
private:
+ /** The separator content between fields. */
static std::string_view
- _missing_value()
+ _after_value(bool full_json)
{
- return {"' : '',\n\t'"};
- }
-
- static std::string_view
- _after_name()
- {
- return {_missing_value().data(), 5};
+ if (full_json) {
+ return {R"(",")"};
+ } else {
+ return {"',\n\t'"};
+ }
}
+ /** The separator content when there is an empty value.
+ *
+ * This is hopefully never used.
+ */
static std::string_view
- _after_value()
+ _missing_value(bool full_json)
{
- return {_missing_value().data() + 5, 5};
+ if (full_json) {
+ return {R"(":")"};
+ } else {
+ return {"' : '','\n\t"};
+ }
}
+private:
enum _State { BEFORE_NAME, IN_NAME, BEFORE_VALUE, IN_VALUE };
+ /// Initial state is BEFORE_VALUE to parse the start line.
_State _state{BEFORE_VALUE};
+
+ /** Whether to print the headers for x-probe-full-json.
+ *
+ * The legacy "probe" format is not JSON-compliant. The new "probe-full-json"
+ * format is JSON-compliant.
+ */
+ bool _full_json = false;
};
///////////////////////////////////////////////////////////////////////////
// Dump a header on stderr, useful together with Dbg().
void
-print_headers(TSMBuffer bufp, TSMLoc hdr_loc, std::stringstream &ss)
+print_headers(TSMBuffer bufp, TSMLoc hdr_loc, std::stringstream &ss, bool
full_json)
{
TSIOBuffer output_buffer;
TSIOBufferReader reader;
TSIOBufferBlock block;
const char *block_start;
int64_t block_avail;
- EscapeCharForJson escape_char_for_json;
+ EscapeCharForJson escape_char_for_json{full_json};
output_buffer = TSIOBufferCreate();
reader = TSIOBufferReaderAlloc(output_buffer);
- ss << "\t'Start-Line' : '";
+ if (full_json) {
+ ss << R"("start-line":")";
+ } else {
+ ss << "\t'Start-Line' : '";
+ }
// Print all message header lines.
TSHttpHdrPrint(bufp, hdr_loc, output_buffer);
@@ -146,7 +177,7 @@ print_headers(TSMBuffer bufp, TSMLoc hdr_loc,
std::stringstream &ss)
block = TSIOBufferReaderStart(reader);
} while (block && block_avail != 0);
- ss.seekp(-escape_char_for_json.backup(), std::ios_base::end);
+ ss.seekp(-escape_char_for_json.backup(full_json), std::ios_base::end);
/* Free up the TSIOBuffer that we used to print out the header */
TSIOBufferReaderFree(reader);
@@ -160,7 +191,7 @@ log_headers(TSHttpTxn /* txn ATS_UNUSED */, TSMBuffer bufp,
TSMLoc hdr_loc, char
{
if (dbg_ctl_hdrs.on()) {
std::stringstream output;
- print_headers(bufp, hdr_loc, output);
+ print_headers(bufp, hdr_loc, output, FULL_JSON);
Dbg(dbg_ctl_hdrs, "\n=============\n %s headers are... \n %s", type_msg,
output.str().c_str());
}
}
@@ -172,13 +203,13 @@ print_request_headers(TSHttpTxn txn, std::stringstream
&output)
TSMLoc hdr_loc;
if (TSHttpTxnClientReqGet(txn, &buf_c, &hdr_loc) == TS_SUCCESS) {
output << "{'type':'request', 'side':'client', 'headers': {\n";
- print_headers(buf_c, hdr_loc, output);
+ print_headers(buf_c, hdr_loc, output, !FULL_JSON);
output << "\n\t}}";
TSHandleMLocRelease(buf_c, TS_NULL_MLOC, hdr_loc);
}
if (TSHttpTxnServerReqGet(txn, &buf_s, &hdr_loc) == TS_SUCCESS) {
output << ",{'type':'request', 'side':'server', 'headers': {\n";
- print_headers(buf_s, hdr_loc, output);
+ print_headers(buf_s, hdr_loc, output, !FULL_JSON);
output << "\n\t}}";
TSHandleMLocRelease(buf_s, TS_NULL_MLOC, hdr_loc);
}
@@ -191,14 +222,70 @@ print_response_headers(TSHttpTxn txn, std::stringstream
&output)
TSMLoc hdr_loc;
if (TSHttpTxnServerRespGet(txn, &buf_s, &hdr_loc) == TS_SUCCESS) {
output << "{'type':'response', 'side':'server', 'headers': {\n";
- print_headers(buf_s, hdr_loc, output);
+ print_headers(buf_s, hdr_loc, output, !FULL_JSON);
output << "\n\t}},";
TSHandleMLocRelease(buf_s, TS_NULL_MLOC, hdr_loc);
}
if (TSHttpTxnClientRespGet(txn, &buf_c, &hdr_loc) == TS_SUCCESS) {
output << "{'type':'response', 'side':'client', 'headers': {\n";
- print_headers(buf_c, hdr_loc, output);
+ print_headers(buf_c, hdr_loc, output, !FULL_JSON);
output << "\n\t}}";
TSHandleMLocRelease(buf_c, TS_NULL_MLOC, hdr_loc);
}
}
+
+void
+print_request_headers_full_json(TSHttpTxn txn, std::stringstream &output)
+{
+ TSMBuffer buf_c, buf_s;
+ TSMLoc hdr_loc;
+
+ bool has_client = false;
+
+ Dbg(dbg_ctl_hdrs, "Printing client request headers for full JSON");
+ if (TSHttpTxnClientReqGet(txn, &buf_c, &hdr_loc) == TS_SUCCESS) {
+ output << "{\"client-request\":{";
+ print_headers(buf_c, hdr_loc, output, FULL_JSON);
+ output << "}";
+ has_client = true;
+ TSHandleMLocRelease(buf_c, TS_NULL_MLOC, hdr_loc);
+ }
+
+ if (TSHttpTxnServerReqGet(txn, &buf_s, &hdr_loc) == TS_SUCCESS) {
+ if (has_client) {
+ output << ",";
+ }
+ output << "\"proxy-request\":{";
+ print_headers(buf_s, hdr_loc, output, FULL_JSON);
+ output << "}";
+ TSHandleMLocRelease(buf_s, TS_NULL_MLOC, hdr_loc);
+ }
+}
+
+void
+print_response_headers_full_json(TSHttpTxn txn, std::stringstream &output)
+{
+ TSMBuffer buf_c, buf_s;
+ TSMLoc hdr_loc;
+
+ bool has_server = false;
+
+ if (TSHttpTxnServerRespGet(txn, &buf_s, &hdr_loc) == TS_SUCCESS) {
+ output << "\"server-response\":{";
+ print_headers(buf_s, hdr_loc, output, FULL_JSON);
+ output << "}";
+ has_server = true;
+ TSHandleMLocRelease(buf_s, TS_NULL_MLOC, hdr_loc);
+ }
+
+ if (TSHttpTxnClientRespGet(txn, &buf_c, &hdr_loc) == TS_SUCCESS) {
+ if (has_server) {
+ output << ",";
+ }
+ output << "\"proxy-response\":{";
+ print_headers(buf_c, hdr_loc, output, FULL_JSON);
+ output << "}}";
+ TSHandleMLocRelease(buf_c, TS_NULL_MLOC, hdr_loc);
+ }
+}
+} // namespace xdebug
diff --git a/plugins/xdebug/xdebug_headers.h b/plugins/xdebug/xdebug_headers.h
new file mode 100644
index 0000000000..0276b5ff4b
--- /dev/null
+++ b/plugins/xdebug/xdebug_headers.h
@@ -0,0 +1,83 @@
+/** @file
+ *
+ * XDebug plugin headers functionality declarations.
+ *
+ * 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 <sstream>
+#include "ts/ts.h"
+
+namespace xdebug
+{
+
+/**
+ * Whether to print the headers for the "probe-full-json" format.
+ */
+static constexpr bool FULL_JSON = true;
+
+/**
+ * Print headers to a stringstream with JSON-like formatting.
+ * @param bufp The TSMBuffer containing the headers.
+ * @param hdr_loc The TSMLoc for the headers.
+ * @param ss The stringstream to write to.
+ * @param full_json Whether to print the headers in a compliant JSON
+ * format. The legacy "probe" format is not JSON-compliant. The new
+ * "probe-full-json" format is JSON-compliant.
+ */
+void print_headers(TSMBuffer bufp, TSMLoc hdr_loc, std::stringstream &ss, bool
full_json);
+
+/**
+ * Log headers to debug for debugging purposes.
+ * @param txn The transaction.
+ * @param bufp The TSMBuffer containing the headers.
+ * @param hdr_loc The TSMLoc for the headers.
+ * @param type_msg Description of the header type.
+ */
+void log_headers(TSHttpTxn txn, TSMBuffer bufp, TSMLoc hdr_loc, char const
*type_msg);
+
+/**
+ * Print request headers in the "probe" format.
+ * @param txn The transaction.
+ * @param output The stringstream to write to.
+ */
+void print_request_headers(TSHttpTxn txn, std::stringstream &output);
+
+/**
+ * Print response headers in the "probe" format.
+ * @param txn The transaction.
+ * @param output The stringstream to write to.
+ */
+void print_response_headers(TSHttpTxn txn, std::stringstream &output);
+
+/**
+ * Print request headers in JSON format for probe-full-json.
+ * @param txn The transaction.
+ * @param output The stringstream to write to.
+ */
+void print_request_headers_full_json(TSHttpTxn txn, std::stringstream &output);
+
+/**
+ * Print response headers in JSON format for probe-full-json.
+ * @param txn The transaction.
+ * @param output The stringstream to write to.
+ */
+void print_response_headers_full_json(TSHttpTxn txn, std::stringstream
&output);
+
+} // namespace xdebug
diff --git a/plugins/xdebug/xdebug_transforms.cc
b/plugins/xdebug/xdebug_transforms.cc
index 3c4fe1c79f..8369e30b30 100644
--- a/plugins/xdebug/xdebug_transforms.cc
+++ b/plugins/xdebug/xdebug_transforms.cc
@@ -17,19 +17,30 @@
limitations under the License.
*/
-#include <limits.h>
-#include <stdio.h>
-#include <string.h>
-#include <functional>
+#include "xdebug_types.h"
+#include "xdebug_headers.h"
+
+#include <unistd.h>
+#include <sstream>
+#include <cinttypes>
#include "ts/ts.h"
+namespace xdebug
+{
+
static const std::string_view MultipartBoundary{"\r\n--- ATS xDebug Probe
Injection Boundary ---\r\n\r\n"};
static char Hostname[1024];
static DbgCtl dbg_ctl_xform{"xdebug_transform"};
+void
+init_transforms()
+{
+ gethostname(Hostname, 1024);
+}
+
static std::string
getPreBody(TSHttpTxn txn)
{
@@ -41,6 +52,15 @@ getPreBody(TSHttpTxn txn)
return output.str();
}
+static std::string
+getPreBodyFullJson(TSHttpTxn txn)
+{
+ std::stringstream output;
+ print_request_headers_full_json(txn, output);
+ output << R"(,"server-body": ")";
+ return output.str();
+}
+
static std::string
getPostBody(TSHttpTxn txn)
{
@@ -52,12 +72,27 @@ getPostBody(TSHttpTxn txn)
return output.str();
}
-static void
+static std::string
+getPostBodyFullJson(TSHttpTxn txn)
+{
+ std::stringstream output;
+ output << R"(",)"; // Close the origin-body field.
+ print_response_headers_full_json(txn, output);
+ output << '\n';
+ return output.str();
+}
+
+void
writePostBody(TSHttpTxn txn, BodyBuilder *data)
{
if (data->wrote_body && data->hdr_ready &&
!data->wrote_postbody.test_and_set()) {
Dbg(dbg_ctl_xform, "body_transform(): Writing postbody headers...");
- std::string postbody = getPostBody(txn);
+ std::string postbody;
+ if (data->probe_type == ProbeType::PROBE_STANDARD) {
+ postbody = getPostBody(txn);
+ } else {
+ postbody = getPostBodyFullJson(txn);
+ }
TSIOBufferWrite(data->output_buffer.get(), postbody.data(),
postbody.length());
data->nbytes += postbody.length();
TSVIONBytesSet(data->output_vio, data->nbytes);
@@ -65,7 +100,7 @@ writePostBody(TSHttpTxn txn, BodyBuilder *data)
}
}
-static int
+int
body_transform(TSCont contp, TSEvent event, void * /* edata ATS_UNUSED */)
{
TSHttpTxn txn = static_cast<TSHttpTxn>(TSContDataGet(contp));
@@ -102,7 +137,12 @@ body_transform(TSCont contp, TSEvent event, void * /*
edata ATS_UNUSED */)
if (data->wrote_prebody == false) {
Dbg(dbg_ctl_xform, "body_transform(): Writing prebody headers...");
- std::string prebody = getPreBody(txn);
+ std::string prebody;
+ if (data->probe_type == ProbeType::PROBE_STANDARD) {
+ prebody = getPreBody(txn);
+ } else {
+ prebody = getPreBodyFullJson(txn);
+ }
TSIOBufferWrite(data->output_buffer.get(), prebody.data(),
prebody.length()); // write prebody
data->wrote_prebody = true;
data->nbytes += prebody.length();
@@ -142,3 +182,5 @@ body_transform(TSCont contp, TSEvent event, void * /* edata
ATS_UNUSED */)
}
return 0;
}
+
+} // namespace xdebug
diff --git a/plugins/xdebug/xdebug_transforms.h
b/plugins/xdebug/xdebug_transforms.h
new file mode 100644
index 0000000000..f1ce35b8b8
--- /dev/null
+++ b/plugins/xdebug/xdebug_transforms.h
@@ -0,0 +1,51 @@
+/** @file
+ *
+ * XDebug plugin transforms functionality declarations.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "ts/ts.h"
+#include "xdebug_types.h" // Required for BodyBuilder
+
+namespace xdebug
+{
+
+/**
+ * Initialize the hostname for the transforms module.
+ */
+void init_transforms();
+
+/**
+ * Write the post body data (called after body is complete).
+ * @param txn The transaction.
+ * @param data The BodyBuilder data.
+ */
+void writePostBody(TSHttpTxn txn, BodyBuilder *data);
+
+/**
+ * Main body transformation continuation handler.
+ * @param contp The continuation.
+ * @param event The event type.
+ * @param edata Event data (unused).
+ * @return Status code.
+ */
+int body_transform(TSCont contp, TSEvent event, void *edata);
+
+} // namespace xdebug
diff --git a/plugins/xdebug/xdebug_types.h b/plugins/xdebug/xdebug_types.h
new file mode 100644
index 0000000000..d6f696feea
--- /dev/null
+++ b/plugins/xdebug/xdebug_types.h
@@ -0,0 +1,58 @@
+/** @file
+ *
+ * XDebug plugin common types and definitions.
+ *
+ * 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 <memory>
+#include <string>
+#include "ts/ts.h"
+#include "tscpp/api/Cleanup.h"
+
+namespace xdebug
+{
+
+enum class ProbeType { PROBE_STANDARD, PROBE_FULL_JSON };
+
+struct BodyBuilder {
+ atscppapi::TSContUniqPtr transform_connp;
+ atscppapi::TSIOBufferUniqPtr output_buffer;
+ // It's important that output_reader comes after output_buffer so it will be
deleted first.
+ atscppapi::TSIOBufferReaderUniqPtr output_reader;
+ TSVIO output_vio = nullptr;
+ bool wrote_prebody = false;
+ bool wrote_body = false;
+ bool hdr_ready = false;
+ std::atomic_flag wrote_postbody;
+ ProbeType probe_type = ProbeType::PROBE_STANDARD;
+
+ int64_t nbytes = 0;
+};
+
+struct XDebugTxnAuxData {
+ std::unique_ptr<BodyBuilder> body_builder;
+ unsigned xheaders = 0;
+};
+
+extern atscppapi::TxnAuxMgrData mgrData;
+using AuxDataMgr = atscppapi::TxnAuxDataMgr<XDebugTxnAuxData, mgrData>;
+
+} // namespace xdebug
diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.replay.yaml
b/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.replay.yaml
new file mode 100644
index 0000000000..4dd69b515e
--- /dev/null
+++ b/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.replay.yaml
@@ -0,0 +1,58 @@
+# 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.
+
+sessions:
+
+- transactions:
+
+ - client-request:
+ method: GET
+ url: /test
+ version: '1.1'
+ headers:
+ fields:
+ - [ uuid, '1' ]
+ - [ Host, example.com ]
+ - [ X-Debug, probe ]
+ - [ User-Agent, "XDebug-Test/1.0" ]
+
+ proxy-request:
+ headers:
+ fields:
+ - [ X-Debug, { as: absent } ]
+ - [ User-Agent, { value: "XDebug-Test/1.0", as: equal } ]
+
+ server-response:
+ status: 200
+ headers:
+ fields:
+ - [ Content-Type, "text/html" ]
+ - [ Content-Length, "20" ]
+ - [ X-Server, "TestOrigin" ]
+ content:
+ encoding: plain
+ data: "Original server body"
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ Content-Type, { value: "text/plain", as: equal } ]
+ - [ X-Original-Content-Type, { value: "text/html", as: equal } ]
+ - [ X-Server, { value: "TestOrigin", as: contains } ]
+ content:
+ data: "Original server body"
+ verify: { as: contains }
diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.test.py
b/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.test.py
new file mode 100644
index 0000000000..303223ea8e
--- /dev/null
+++ b/tests/gold_tests/pluginTest/xdebug/x_probe/x_probe.test.py
@@ -0,0 +1,80 @@
+'''
+Verify xdebug plugin probe functionality.
+'''
+# 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.
+
+Test.Summary = 'Test xdebug plugin probe functionality'
+Test.ContinueOnFail = True
+Test.SkipUnless(Condition.PluginExists('xdebug.so'))
+
+
+class XDebugProbeTest:
+ """
+ Test the xdebug probe functionality which transforms the response body
+ to include request and response headers in a multipart format.
+
+ The probe feature:
+ - Changes Content-Type to text/plain
+ - Injects request headers before the original body
+ - Injects response headers after the original body
+ - Uses multipart boundary separators
+ - Disables caching due to body modification
+ """
+
+ _replay_file: str = "x_probe.replay.yaml"
+
+ def __init__(self) -> None:
+ tr = Test.AddTestRun("xdebug probe test")
+ self._setupOriginServer(tr)
+ self._setupTS(tr)
+ self._setupClient(tr)
+
+ def _setupOriginServer(self, tr: 'TestRun') -> None:
+ """Configure the origin server using Proxy Verifier.
+ :param tr: TestRun to add the server to.
+ """
+ self._server = tr.AddVerifierServerProcess("server", self._replay_file)
+
+ def _setupTS(self, tr: 'TestRun') -> None:
+ """Configure ATS with xdebug plugin enabled for probe functionality.
+ :param tr: TestRun to add the ATS process to.
+ """
+ self._ts = Test.MakeATSProcess("ts")
+
+ self._ts.Disk.records_config.update({
+ "proxy.config.diags.debug.enabled": 1,
+ "proxy.config.diags.debug.tags": "xdebug",
+ })
+
+ self._ts.Disk.plugin_config.AddLine('xdebug.so --enable=probe')
+ self._ts.Disk.remap_config.AddLine(f"map /
http://127.0.0.1:{self._server.Variables.http_port}")
+
+ def _setupClient(self, tr: 'TestRun') -> None:
+ """Test basic probe functionality with header injection.
+ :param tr: TestRun to add the test to.
+ """
+ tr.Processes.Default.StartBefore(self._server)
+ tr.Processes.Default.StartBefore(self._ts)
+ tr.AddVerifierClientProcess("client", self._replay_file,
http_ports=[self._ts.Variables.port])
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+ 'ATS xDebug Probe Injection Boundary', "ATS xDebug Probe Injection
Boundary should be present")
+ tr.Processes.Default.Streams.stdout +=
Testers.ContainsExpression('xDebugProbeAt', "xDebugProbeAt should be present")
+
+
+# Execute the test
+XDebugProbeTest()
diff --git a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq.gold
b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq.gold
new file mode 100644
index 0000000000..27fb0a3b1b
--- /dev/null
+++ b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/gold/jq.gold
@@ -0,0 +1,3 @@
+"1"
+"Original server response"
+"from-origin"
diff --git
a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml
b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml
new file mode 100644
index 0000000000..7012f9c280
--- /dev/null
+++
b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.replay.yaml
@@ -0,0 +1,57 @@
+# 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.
+
+sessions:
+
+- transactions:
+
+ - client-request:
+ method: GET
+ url: /test
+ version: '1.1'
+ headers:
+ fields:
+ - [ uuid, '1' ]
+ - [ Host, example.com ]
+ - [ X-Debug, probe-full-json ]
+ - [ X-Request, "from-client"]
+
+ proxy-request:
+ headers:
+ fields:
+ - [ x-debug, { as: absent } ]
+
+ server-response:
+ status: 200
+ headers:
+ fields:
+ - [ Content-Type, "text/html" ]
+ - [ Content-Length, "24" ]
+ - [ X-Response, "from-origin" ]
+ content:
+ encoding: plain
+ data: "Original server response"
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ Content-Type, { value: "application/json", as: equal } ]
+ - [ X-Response, { value: "from-origin", as: contains } ]
+ - [ X-Original-Content-Type, { value: "text/html", as: equal } ]
+ content:
+ data: "Original server response"
+ verify: { as: contains }
diff --git
a/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.test.py
b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.test.py
new file mode 100644
index 0000000000..4f92e423f3
--- /dev/null
+++
b/tests/gold_tests/pluginTest/xdebug/x_probe_full_json/x_probe_full_json.test.py
@@ -0,0 +1,98 @@
+'''
+Verify xdebug plugin probe-full-json functionality.
+'''
+# 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.
+
+Test.Summary = 'Test xdebug plugin probe-full-json functionality'
+Test.ContinueOnFail = True
+Test.SkipUnless(Condition.PluginExists('xdebug.so'))
+Test.SkipUnless(Condition.HasProgram("jq", "jq is required to validate JSON
output"))
+
+
+class XDebugProbeFullJsonTest:
+ """
+ Test the xdebug probe-full-json functionality which transforms the
response body
+ to include request headers, response body, and response headers in a
complete JSON format.
+
+ The probe-full-json feature:
+ - Changes Content-Type to text/plain
+ - Generates a complete JSON object containing all debug information
+ - Includes client/server request headers, response body, and client/server
response headers
+ - Disables caching due to body modification
+ """
+
+ _replay_file: str = "x_probe_full_json.replay.yaml"
+
+ def __init__(self) -> None:
+ self._servers_are_started: bool = False
+ self._setupOriginServer()
+ self._setupTS()
+ self._setupClient()
+ self._setupJqValidation()
+
+ def _setupOriginServer(self) -> None:
+ """Configure the origin server using Proxy Verifier.
+ """
+ self._server = Test.MakeVerifierServerProcess("server",
self._replay_file)
+
+ def _setupTS(self) -> None:
+ """Configure ATS with xdebug plugin enabled for probe-full-json
functionality.
+ """
+ self._ts = Test.MakeATSProcess("ts")
+
+ self._ts.Disk.records_config.update({
+ "proxy.config.diags.debug.enabled": 1,
+ "proxy.config.diags.debug.tags": "xdebug",
+ })
+
+ self._ts.Disk.plugin_config.AddLine('xdebug.so
--enable=probe-full-json')
+ self._ts.Disk.remap_config.AddLine(f"map /
http://127.0.0.1:{self._server.Variables.http_port}")
+
+ def _startServersIfNeeded(self, tr: 'TestRun') -> None:
+ '''Start the servers if they are not already started.
+ :param tr: TestRun to add the test to.
+ '''
+ if not self._servers_are_started:
+ tr.Processes.Default.StartBefore(self._server)
+ tr.Processes.Default.StartBefore(self._ts)
+ self._servers_are_started = True
+
+ def _setupClient(self) -> None:
+ """Test basic probe-full-json functionality with JSON output.
+ """
+ tr = Test.AddTestRun("Verify probe-full-json functionality")
+ self._startServersIfNeeded(tr)
+ tr.AddVerifierClientProcess("client", self._replay_file,
http_ports=[self._ts.Variables.port])
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
+ 'X-Original-Content-Type', "X-Original-Content-Type should be
present")
+
+ def _setupJqValidation(self) -> None:
+ """Use curl to get the response body and pipe through jq to validate
JSON.
+ """
+ tr = Test.AddTestRun("Verify JSON output")
+ self._startServersIfNeeded(tr)
+ tr.MakeCurlCommand(
+ f'-s -H"uuid: 1" -H "Host: example.com" -H "X-Debug:
probe-full-json" '
+ f'http://127.0.0.1:{self._ts.Variables.port}/test | '
+ "jq
'.\"client-request\".\"uuid\",.\"server-body\",.\"proxy-response\".\"x-response\"'")
+ tr.Processes.Default.ReturnCode = 0
+ tr.Processes.Default.Streams.stdout = "gold/jq.gold"
+
+
+# Execute the test
+XDebugProbeFullJsonTest()