| /* | |
| * 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 <cctype> | |
| #include <climits> | |
| #include <cstdlib> | |
| #include <cstdio> | |
| #include <cstdio> | |
| #include <strings.h> | |
| #include <cstring> | |
| #include <memory> | |
| #include <getopt.h> | |
| #include <cstdint> | |
| #include <cinttypes> | |
| #include <string_view> | |
| #include <algorithm> | |
| #include <unistd.h> | |
| #include <ts/ts.h> | |
| #include "tscore/ink_defs.h" | |
| #include "tsutil/PostScript.h" | |
| #include "swoc/TextView.h" | |
| #include "tscpp/api/Cleanup.h" | |
| #include "xdebug_types.h" | |
| #include "xdebug_headers.h" | |
| #include "xdebug_transforms.h" | |
| #include "xdebug_utils.h" | |
| namespace xdebug | |
| { | |
| namespace | |
| { | |
| DbgCtl dbg_ctl{"xdebug"}; | |
| DbgCtl dbg_ctl_xform{"xdebug_transform"}; | |
| } // end anonymous namespace | |
| // Definition of the auxiliary data manager | |
| atscppapi::TxnAuxMgrData mgrData; | |
| static struct { | |
| char *str; | |
| int len; | |
| } 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_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_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_X_PROBE_FULL_JSON, XHEADER_X_PROBE_FULL_JSON}, | |
| {HEADER_NAME_ALL, XHEADER_ALL }, | |
| }; | |
| static TSCont XInjectHeadersCont = nullptr; | |
| static TSCont XDeleteDebugHdrCont = nullptr; | |
| // Return the length of a string literal. | |
| template <int N> | |
| unsigned | |
| lengthof(const char (&)[N]) | |
| { | |
| return N - 1; | |
| } | |
| static TSMLoc | |
| FindOrMakeHdrField(TSMBuffer buffer, TSMLoc hdr, const char *name, unsigned len) | |
| { | |
| TSMLoc field; | |
| field = TSMimeHdrFieldFind(buffer, hdr, name, len); | |
| if (field == TS_NULL_MLOC) { | |
| if (TSMimeHdrFieldCreateNamed(buffer, hdr, name, len, &field) == TS_SUCCESS) { | |
| TSReleaseAssert(TSMimeHdrFieldAppend(buffer, hdr, field) == TS_SUCCESS); | |
| } | |
| } | |
| return field; | |
| } | |
| static void | |
| InjectGenerationHeader(TSHttpTxn txn, TSMBuffer buffer, TSMLoc hdr) | |
| { | |
| TSMgmtInt value; | |
| TSMLoc dst = TS_NULL_MLOC; | |
| if (TSHttpTxnConfigIntGet(txn, TS_CONFIG_HTTP_CACHE_GENERATION, &value) == TS_SUCCESS) { | |
| dst = FindOrMakeHdrField(buffer, hdr, "X-Cache-Generation", lengthof("X-Cache-Generation")); | |
| if (dst != TS_NULL_MLOC) { | |
| TSReleaseAssert(TSMimeHdrFieldValueInt64Set(buffer, hdr, dst, -1 /* idx */, value) == TS_SUCCESS); | |
| } | |
| } | |
| if (dst != TS_NULL_MLOC) { | |
| TSHandleMLocRelease(buffer, hdr, dst); | |
| } | |
| } | |
| static void | |
| InjectCacheKeyHeader(TSHttpTxn txn, TSMBuffer buffer, TSMLoc hdr) | |
| { | |
| TSMLoc url = TS_NULL_MLOC; | |
| TSMLoc dst = TS_NULL_MLOC; | |
| struct { | |
| char *ptr; | |
| int len; | |
| } strval = {nullptr, 0}; | |
| Dbg(dbg_ctl, "attempting to inject X-Cache-Key header"); | |
| if (TSUrlCreate(buffer, &url) != TS_SUCCESS) { | |
| goto done; | |
| } | |
| if (TSHttpTxnCacheLookupUrlGet(txn, buffer, url) != TS_SUCCESS) { | |
| goto done; | |
| } | |
| strval.ptr = TSUrlStringGet(buffer, url, &strval.len); | |
| if (strval.ptr == nullptr || strval.len == 0) { | |
| goto done; | |
| } | |
| // Create a new response header field. | |
| dst = FindOrMakeHdrField(buffer, hdr, "X-Cache-Key", lengthof("X-Cache-Key")); | |
| if (dst == TS_NULL_MLOC) { | |
| goto done; | |
| } | |
| // Now copy the cache lookup URL into the response header. | |
| TSReleaseAssert(TSMimeHdrFieldValueStringInsert(buffer, hdr, dst, -1 /* idx */, strval.ptr, strval.len) == TS_SUCCESS); | |
| done: | |
| if (dst != TS_NULL_MLOC) { | |
| TSHandleMLocRelease(buffer, hdr, dst); | |
| } | |
| if (url != TS_NULL_MLOC) { | |
| TSHandleMLocRelease(buffer, TS_NULL_MLOC, url); | |
| } | |
| TSfree(strval.ptr); | |
| } | |
| static void | |
| InjectCacheInfoHeader(TSHttpTxn txn, TSMBuffer buffer, TSMLoc hdr) | |
| { | |
| TSMLoc dst = TS_NULL_MLOC; | |
| TSMgmtInt volume; | |
| const char *path; | |
| Dbg(dbg_ctl, "attempting to inject X-Cache-Info header"); | |
| if ((path = TSHttpTxnCacheDiskPathGet(txn, nullptr)) == nullptr) { | |
| goto done; | |
| } | |
| if (TSHttpTxnInfoIntGet(txn, TS_TXN_INFO_CACHE_VOLUME, &volume) != TS_SUCCESS) { | |
| goto done; | |
| } | |
| // Create a new response header field. | |
| dst = FindOrMakeHdrField(buffer, hdr, "X-Cache-Info", lengthof("X-Cache-Info")); | |
| if (dst == TS_NULL_MLOC) { | |
| goto done; | |
| } | |
| char value[1024]; | |
| snprintf(value, sizeof(value), "path=%s; volume=%" PRId64, path, volume); | |
| // Now copy the CacheDisk info into the response header. | |
| TSReleaseAssert(TSMimeHdrFieldValueStringInsert(buffer, hdr, dst, -1 /* idx */, value, std::strlen(value)) == TS_SUCCESS); | |
| done: | |
| if (dst != TS_NULL_MLOC) { | |
| TSHandleMLocRelease(buffer, hdr, dst); | |
| } | |
| } | |
| static void | |
| InjectCacheHeader(TSHttpTxn txn, TSMBuffer buffer, TSMLoc hdr) | |
| { | |
| TSMLoc dst = TS_NULL_MLOC; | |
| int status; | |
| static const char *names[] = { | |
| "miss", // TS_CACHE_LOOKUP_MISS, | |
| "hit-stale", // TS_CACHE_LOOKUP_HIT_STALE, | |
| "hit-fresh", // TS_CACHE_LOOKUP_HIT_FRESH, | |
| "skipped" // TS_CACHE_LOOKUP_SKIPPED | |
| }; | |
| Dbg(dbg_ctl, "attempting to inject X-Cache header"); | |
| // Create a new response header field. | |
| dst = FindOrMakeHdrField(buffer, hdr, "X-Cache", lengthof("X-Cache")); | |
| if (dst == TS_NULL_MLOC) { | |
| goto done; | |
| } | |
| if (TSHttpTxnCacheLookupStatusGet(txn, &status) == TS_ERROR) { | |
| // If the cache lookup hasn't happened yes, TSHttpTxnCacheLookupStatusGet will fail. | |
| TSReleaseAssert(TSMimeHdrFieldValueStringInsert(buffer, hdr, dst, -1 /* idx */, "none", 4) == TS_SUCCESS); | |
| } else { | |
| const char *msg = (status < 0 || status >= static_cast<int>(countof(names))) ? "unknown" : names[status]; | |
| TSReleaseAssert(TSMimeHdrFieldValueStringInsert(buffer, hdr, dst, -1 /* idx */, msg, -1) == TS_SUCCESS); | |
| } | |
| done: | |
| if (dst != TS_NULL_MLOC) { | |
| TSHandleMLocRelease(buffer, hdr, dst); | |
| } | |
| } | |
| struct milestone { | |
| TSMilestonesType mstype; | |
| const char *msname; | |
| }; | |
| static void | |
| InjectMilestonesHeader(TSHttpTxn txn, TSMBuffer buffer, TSMLoc hdr) | |
| { | |
| // The set of milestones we can publish. Some milestones happen after | |
| // this hook, so we skip those ... | |
| static const milestone milestones[] = { | |
| {TS_MILESTONE_UA_BEGIN, "UA-BEGIN" }, | |
| {TS_MILESTONE_UA_FIRST_READ, "UA-FIRST-READ" }, | |
| {TS_MILESTONE_UA_READ_HEADER_DONE, "UA-READ-HEADER-DONE" }, | |
| {TS_MILESTONE_UA_BEGIN_WRITE, "UA-BEGIN-WRITE" }, | |
| {TS_MILESTONE_UA_CLOSE, "UA-CLOSE" }, | |
| {TS_MILESTONE_SERVER_FIRST_CONNECT, "SERVER-FIRST-CONNECT" }, | |
| {TS_MILESTONE_SERVER_CONNECT, "SERVER-CONNECT" }, | |
| {TS_MILESTONE_SERVER_CONNECT_END, "SERVER-CONNECT-END" }, | |
| {TS_MILESTONE_SERVER_BEGIN_WRITE, "SERVER-BEGIN-WRITE" }, | |
| {TS_MILESTONE_SERVER_FIRST_READ, "SERVER-FIRST-READ" }, | |
| {TS_MILESTONE_SERVER_READ_HEADER_DONE, "SERVER-READ-HEADER-DONE"}, | |
| {TS_MILESTONE_SERVER_CLOSE, "SERVER-CLOSE" }, | |
| {TS_MILESTONE_CACHE_OPEN_READ_BEGIN, "CACHE-OPEN-READ-BEGIN" }, | |
| {TS_MILESTONE_CACHE_OPEN_READ_END, "CACHE-OPEN-READ-END" }, | |
| {TS_MILESTONE_CACHE_OPEN_WRITE_BEGIN, "CACHE-OPEN-WRITE-BEGIN" }, | |
| {TS_MILESTONE_CACHE_OPEN_WRITE_END, "CACHE-OPEN-WRITE-END" }, | |
| {TS_MILESTONE_DNS_LOOKUP_BEGIN, "DNS-LOOKUP-BEGIN" }, | |
| {TS_MILESTONE_DNS_LOOKUP_END, "DNS-LOOKUP-END" }, | |
| // SM_START is deliberately excluded because as all the times are printed relative to it | |
| // it would always be zero. | |
| {TS_MILESTONE_SM_FINISH, "SM-FINISH" }, | |
| {TS_MILESTONE_PLUGIN_ACTIVE, "PLUGIN-ACTIVE" }, | |
| {TS_MILESTONE_PLUGIN_TOTAL, "PLUGIN-TOTAL" }, | |
| }; | |
| TSMLoc dst = TS_NULL_MLOC; | |
| TSHRTime epoch; | |
| // TS_MILESTONE_SM_START is stamped when the HTTP transaction is born. The slow | |
| // log feature publishes the other times as seconds relative to this epoch. Let's | |
| // do the same. | |
| TSHttpTxnMilestoneGet(txn, TS_MILESTONE_SM_START, &epoch); | |
| // Create a new response header field. | |
| dst = FindOrMakeHdrField(buffer, hdr, "X-Milestones", lengthof("X-Milestones")); | |
| if (dst == TS_NULL_MLOC) { | |
| goto done; | |
| } | |
| for (unsigned i = 0; i < countof(milestones); ++i) { | |
| TSHRTime time = 0; | |
| char hdrval[64]; | |
| // If we got a milestone (it's in nanoseconds), convert it to seconds relative to | |
| // the start of the transaction. We don't get milestone values for portions of the | |
| // state machine the request doesn't traverse. | |
| TSHttpTxnMilestoneGet(txn, milestones[i].mstype, &time); | |
| if (time > 0) { | |
| double elapsed = static_cast<double>(time - epoch) / 1000000000.0; | |
| int len = snprintf(hdrval, sizeof(hdrval), "%s=%1.9lf", milestones[i].msname, elapsed); | |
| TSReleaseAssert(TSMimeHdrFieldValueStringInsert(buffer, hdr, dst, -1 /* idx */, hdrval, len) == TS_SUCCESS); | |
| } | |
| } | |
| done: | |
| if (dst != TS_NULL_MLOC) { | |
| TSHandleMLocRelease(buffer, hdr, dst); | |
| } | |
| } | |
| static const char NotFound[] = "Not-Found"; | |
| // The returned string must be freed with TSfree() unless it is equal to the constant NotFound. | |
| // | |
| static const char * | |
| getRemapUrlStr(TSHttpTxn txnp, TSReturnCode (*remapUrlGetFunc)(TSHttpTxn, TSMLoc *), int &urlStrLen) | |
| { | |
| TSMLoc urlLoc; | |
| TSReturnCode rc = remapUrlGetFunc(txnp, &urlLoc); | |
| if (rc != TS_SUCCESS) { | |
| urlStrLen = sizeof(NotFound) - 1; | |
| return NotFound; | |
| } | |
| char *urlStr = TSUrlStringGet(nullptr, urlLoc, &urlStrLen); | |
| // Be defensive. | |
| if ((urlStrLen == 0) and urlStr) { | |
| TSError("[xdebug] non-null remap URL string with zero length"); | |
| TSfree(urlStr); | |
| urlStr = nullptr; | |
| } | |
| if (!urlStr) { | |
| urlStrLen = sizeof(NotFound) - 1; | |
| return NotFound; | |
| } | |
| return urlStr; | |
| } | |
| static void | |
| InjectRemapHeader(TSHttpTxn txn, TSMBuffer buffer, TSMLoc hdr) | |
| { | |
| TSMLoc dst = FindOrMakeHdrField(buffer, hdr, "X-Remap", lengthof("X-Remap")); | |
| if (TS_NULL_MLOC != dst) { | |
| int fromUrlStrLen, toUrlStrLen; | |
| const char *fromUrlStr = getRemapUrlStr(txn, TSRemapFromUrlGet, fromUrlStrLen); | |
| const char *toUrlStr = getRemapUrlStr(txn, TSRemapToUrlGet, toUrlStrLen); | |
| char buf[2048]; | |
| int len = snprintf(buf, sizeof(buf), "from=%.*s, to=%.*s", fromUrlStrLen, fromUrlStr, toUrlStrLen, toUrlStr); | |
| if (fromUrlStr != NotFound) { | |
| TSfree(const_cast<char *>(fromUrlStr)); | |
| } | |
| if (toUrlStr != NotFound) { | |
| TSfree(const_cast<char *>(toUrlStr)); | |
| } | |
| if (len <= 0) { | |
| Dbg(dbg_ctl, "skipping X-Remap header injection because snprintf returned %d", len); | |
| } else { | |
| if (static_cast<size_t>(len) >= sizeof(buf)) { | |
| Dbg(dbg_ctl, "truncating X-Remap header from %d to %zu bytes", len, sizeof(buf) - 1); | |
| len = sizeof(buf) - 1; | |
| } | |
| TSReleaseAssert(TSMimeHdrFieldValueStringInsert(buffer, hdr, dst, -1 /* idx */, buf, len) == TS_SUCCESS); | |
| } | |
| TSHandleMLocRelease(buffer, hdr, dst); | |
| } | |
| } | |
| static void | |
| InjectEffectiveURLHeader(TSHttpTxn txn, TSMBuffer buffer, TSMLoc hdr) | |
| { | |
| struct { | |
| char *ptr; | |
| int len; | |
| } strval = {nullptr, 0}; | |
| Dbg(dbg_ctl, "attempting to inject X-Effective-URL header"); | |
| strval.ptr = TSHttpTxnEffectiveUrlStringGet(txn, &strval.len); | |
| if (strval.ptr != nullptr && strval.len > 0) { | |
| TSMLoc dst = FindOrMakeHdrField(buffer, hdr, "X-Effective-URL", lengthof("X-Effective-URL")); | |
| if (dst != TS_NULL_MLOC) { | |
| char buf[16 * 1024]; | |
| int len = snprintf(buf, sizeof(buf), "\"%s\"", strval.ptr); | |
| if (len == strval.len + 2 && len <= static_cast<int>(sizeof(buf)) - 1) { // Only copy back if len expected and within buffer. | |
| TSReleaseAssert(TSMimeHdrFieldValueStringInsert(buffer, hdr, dst, -1 /* idx */, buf, len) == TS_SUCCESS); | |
| } | |
| TSHandleMLocRelease(buffer, hdr, dst); | |
| } | |
| } | |
| TSfree(strval.ptr); | |
| } | |
| static void | |
| 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) { | |
| int original_content_type_len = 0; | |
| const char *original_content_type = TSMimeHdrFieldValueStringGet(buffer, hdr, ct_field, -1, &original_content_type_len); | |
| if (original_content_type != nullptr) { | |
| TSMLoc dst = FindOrMakeHdrField(buffer, hdr, "X-Original-Content-Type", lengthof("X-Original-Content-Type")); | |
| TSReleaseAssert(TS_NULL_MLOC != dst); | |
| TSReleaseAssert(TSMimeHdrFieldValueStringInsert(buffer, hdr, dst, -1 /* idx */, original_content_type, | |
| original_content_type_len) == TS_SUCCESS); | |
| } | |
| } else { | |
| if (TSMimeHdrFieldCreateNamed(buffer, hdr, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE, &ct_field) == TS_SUCCESS) { | |
| TSReleaseAssert(TSMimeHdrFieldAppend(buffer, hdr, ct_field) == TS_SUCCESS); | |
| } | |
| } | |
| TSMimeHdrFieldValuesClear(buffer, hdr, ct_field); | |
| 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 | |
| InjectTxnUuidHeader(TSHttpTxn txn, TSMBuffer buffer, TSMLoc hdr) | |
| { | |
| TSMLoc dst = FindOrMakeHdrField(buffer, hdr, "X-Transaction-ID", lengthof("X-Transaction-ID")); | |
| if (TS_NULL_MLOC != dst) { | |
| char buf[TS_UUID_STRING_LEN + 22]; // Padded for int64_t (20) + 1 ('-') + 1 ('\0') | |
| TSUuid uuid = TSProcessUuidGet(); | |
| int len = snprintf(buf, sizeof(buf), "%s-%" PRIu64 "", TSUuidStringGet(uuid), TSHttpTxnIdGet(txn)); | |
| TSReleaseAssert(TSMimeHdrFieldValueStringInsert(buffer, hdr, dst, -1 /* idx */, buf, len) == TS_SUCCESS); | |
| TSHandleMLocRelease(buffer, hdr, dst); | |
| } | |
| } | |
| static void | |
| InjectParentSelectionKeyHeader(TSHttpTxn txn, TSMBuffer buffer, TSMLoc hdr) | |
| { | |
| TSMLoc url = TS_NULL_MLOC; | |
| TSMLoc dst = TS_NULL_MLOC; | |
| struct { | |
| char *ptr; | |
| int len; | |
| } strval = {nullptr, 0}; | |
| Dbg(dbg_ctl, "attempting to inject X-ParentSelection-Key header"); | |
| if (TSUrlCreate(buffer, &url) != TS_SUCCESS) { | |
| goto done; | |
| } | |
| if (TSHttpTxnParentSelectionUrlGet(txn, buffer, url) != TS_SUCCESS) { | |
| goto done; | |
| } | |
| strval.ptr = TSUrlStringGet(buffer, url, &strval.len); | |
| if (strval.ptr == nullptr || strval.len == 0) { | |
| goto done; | |
| } | |
| // Create a new response header field. | |
| dst = FindOrMakeHdrField(buffer, hdr, "X-ParentSelection-Key", lengthof("X-ParentSelection-Key")); | |
| if (dst == TS_NULL_MLOC) { | |
| goto done; | |
| } | |
| // Now copy the parent selection lookup URL into the response header. | |
| TSReleaseAssert(TSMimeHdrFieldValueStringInsert(buffer, hdr, dst, -1 /* idx */, strval.ptr, strval.len) == TS_SUCCESS); | |
| done: | |
| if (dst != TS_NULL_MLOC) { | |
| TSHandleMLocRelease(buffer, hdr, dst); | |
| } | |
| if (url != TS_NULL_MLOC) { | |
| TSHandleMLocRelease(buffer, TS_NULL_MLOC, url); | |
| } | |
| TSfree(strval.ptr); | |
| } | |
| static int | |
| XInjectResponseHeaders(TSCont /* contp */, TSEvent event, void *edata) | |
| { | |
| TSHttpTxn txn = static_cast<TSHttpTxn>(edata); | |
| TSMBuffer buffer; | |
| TSMLoc hdr; | |
| TSReleaseAssert(event == TS_EVENT_HTTP_SEND_RESPONSE_HDR); | |
| unsigned xheaders = AuxDataMgr::data(txn).xheaders; | |
| if (xheaders == 0) { | |
| goto done; | |
| } | |
| if (TSHttpTxnClientRespGet(txn, &buffer, &hdr) == TS_ERROR) { | |
| goto done; | |
| } | |
| if (xheaders & XHEADER_X_CACHE_KEY) { | |
| InjectCacheKeyHeader(txn, buffer, hdr); | |
| } | |
| if (xheaders & XHEADER_X_CACHE_INFO) { | |
| InjectCacheInfoHeader(txn, buffer, hdr); | |
| } | |
| if (xheaders & XHEADER_X_CACHE) { | |
| InjectCacheHeader(txn, buffer, hdr); | |
| } | |
| if (xheaders & XHEADER_X_MILESTONES) { | |
| InjectMilestonesHeader(txn, buffer, hdr); | |
| } | |
| if (xheaders & XHEADER_X_GENERATION) { | |
| InjectGenerationHeader(txn, buffer, hdr); | |
| } | |
| if (xheaders & XHEADER_X_TRANSACTION_ID) { | |
| InjectTxnUuidHeader(txn, buffer, hdr); | |
| } | |
| if (xheaders & XHEADER_X_REMAP) { | |
| InjectRemapHeader(txn, buffer, hdr); | |
| } | |
| if (xheaders & XHEADER_X_EFFECTIVE_URL) { | |
| InjectEffectiveURLHeader(txn, buffer, hdr); | |
| } | |
| // intentionally placed after all injected headers. | |
| if (xheaders & XHEADER_X_DUMP_HEADERS) { | |
| log_headers(txn, buffer, hdr, "ClientResponse"); | |
| } | |
| if (xheaders & XHEADER_X_PROBE_HEADERS || xheaders & XHEADER_X_PROBE_FULL_JSON) { | |
| BodyBuilder *data = AuxDataMgr::data(txn).body_builder.get(); | |
| InjectOriginalContentTypeHeader(buffer, hdr, xheaders & XHEADER_X_PROBE_FULL_JSON); | |
| 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; | |
| } | |
| data->hdr_ready = true; | |
| writePostBody(txn, data); | |
| } | |
| if (xheaders & XHEADER_X_PSELECT_KEY) { | |
| InjectParentSelectionKeyHeader(txn, buffer, hdr); | |
| } | |
| done: | |
| TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE); | |
| return TS_EVENT_NONE; | |
| } | |
| static bool | |
| isFwdFieldValue(std::string_view value, intmax_t &fwdCnt) | |
| { | |
| static const swoc::TextView paramName("fwd"); | |
| if (value.size() < paramName.size()) { | |
| return false; | |
| } | |
| swoc::TextView tvVal(value); | |
| if (strcasecmp(paramName, tvVal.prefix(paramName.size())) != 0) { | |
| return false; | |
| } | |
| tvVal.remove_prefix(paramName.size()); | |
| if (tvVal.size() == 0) { | |
| // Value is 'fwd' with no '=<count>'. | |
| fwdCnt = -1; | |
| return true; | |
| } | |
| const char httpSpace[] = " \t"; | |
| tvVal.ltrim(httpSpace); | |
| if (tvVal[0] != '=') { | |
| return false; | |
| } | |
| tvVal.remove_prefix(1); | |
| tvVal.ltrim(httpSpace); | |
| size_t sz = tvVal.size(); | |
| intmax_t i = swoc::svtoi(tvVal, &tvVal); | |
| if ((tvVal.size() != sz) or (i < 0)) { | |
| // There were crud characters after the number, or the number was negative. | |
| return false; | |
| } | |
| fwdCnt = i; | |
| return true; | |
| } | |
| /** Resolve encoding based on response Content-Type before body transformation. */ | |
| static int | |
| XResolveEncoding(TSCont /* contp */, TSEvent event, void *edata) | |
| { | |
| TSHttpTxn txn = static_cast<TSHttpTxn>(edata); | |
| TSMLoc hdr = TS_NULL_MLOC, ct_field = TS_NULL_MLOC; | |
| TSMBuffer buffer; | |
| // Make sure TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE) is called before exiting function. | |
| ts::PostScript ps([=]() -> void { TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE); }); | |
| TSReleaseAssert(event == TS_EVENT_HTTP_READ_RESPONSE_HDR); | |
| auto &auxData = AuxDataMgr::data(txn); | |
| BodyBuilder *data = auxData.body_builder.get(); | |
| // Only resolve encoding for probe-full-json. | |
| if (data == nullptr || data->probe_type != ProbeType::PROBE_FULL_JSON) { | |
| return TS_EVENT_NONE; | |
| } | |
| if (TSHttpTxnServerRespGet(txn, &buffer, &hdr) == TS_ERROR) { | |
| return TS_EVENT_NONE; | |
| } | |
| ct_field = TSMimeHdrFieldFind(buffer, hdr, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE); | |
| if (TS_NULL_MLOC != ct_field) { | |
| int ct_len = 0; | |
| const char *ct_value = TSMimeHdrFieldValueStringGet(buffer, hdr, ct_field, -1, &ct_len); | |
| if (ct_value != nullptr && ct_len > 0) { | |
| if (xdebug::is_textual_content_type(std::string_view{ct_value, static_cast<size_t>(ct_len)})) { | |
| Dbg(dbg_ctl_xform, "Content-type of \"%.*s\": is textual, using escape encoding", ct_len, ct_value); | |
| data->body_encoding = BodyEncoding_t::ESCAPE; | |
| } else { | |
| Dbg(dbg_ctl_xform, "Content-type of \"%.*s\": is not textual, using hex encoding", ct_len, ct_value); | |
| data->body_encoding = BodyEncoding_t::HEX; | |
| } | |
| } | |
| TSHandleMLocRelease(buffer, hdr, ct_field); | |
| } | |
| TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr); | |
| return TS_EVENT_NONE; | |
| } | |
| // Scan the client request headers and determine which debug headers they | |
| // want in the response. | |
| static int | |
| XScanRequestHeaders(TSCont /* contp */, TSEvent event, void *edata) | |
| { | |
| TSHttpTxn txn = static_cast<TSHttpTxn>(edata); | |
| unsigned xheaders = 0; | |
| intmax_t fwdCnt = 0; | |
| TSMLoc field, next; | |
| TSMBuffer buffer; | |
| TSMLoc hdr; | |
| // Make sure TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE) is called before exiting function. | |
| // | |
| ts::PostScript ps([=]() -> void { TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE); }); | |
| TSReleaseAssert(event == TS_EVENT_HTTP_READ_REQUEST_HDR); | |
| if (TSHttpTxnClientReqGet(txn, &buffer, &hdr) == TS_ERROR) { | |
| return TS_EVENT_NONE; | |
| } | |
| Dbg(dbg_ctl, "scanning for %s header values", xDebugHeader.str); | |
| // Walk the X-Debug header values and determine what to inject into the response. | |
| field = TSMimeHdrFieldFind(buffer, hdr, xDebugHeader.str, xDebugHeader.len); | |
| while (field != TS_NULL_MLOC) { | |
| int count = TSMimeHdrFieldValuesCount(buffer, hdr, field); | |
| for (int i = 0; i < count; ++i) { | |
| const char *value; | |
| int vsize; | |
| value = TSMimeHdrFieldValueStringGet(buffer, hdr, field, i, &vsize); | |
| if (value == nullptr || vsize == 0) { | |
| continue; | |
| } | |
| Dbg(dbg_ctl, "Validating value: '%.*s'", vsize, value); | |
| #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 is_probe_full_json = false; | |
| xdebug::BodyEncoding_t requested_encoding = xdebug::BodyEncoding_t::AUTO; | |
| if (XHEADER_X_PROBE_FULL_JSON & allowedHeaders) { | |
| swoc::TextView tv{value, static_cast<size_t>(vsize)}; | |
| if (xdebug::parse_probe_full_json_field_value(tv, requested_encoding)) { | |
| is_probe_full_json = true; | |
| } | |
| } | |
| 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)) { | |
| xheaders |= XHEADER_X_CACHE_INFO & allowedHeaders; | |
| } else if (header_field_eq(HEADER_NAME_X_MILESTONES, value, vsize)) { | |
| xheaders |= XHEADER_X_MILESTONES & allowedHeaders; | |
| } else if (header_field_eq(HEADER_NAME_X_CACHE, value, vsize)) { | |
| xheaders |= XHEADER_X_CACHE & allowedHeaders; | |
| } else if (header_field_eq(HEADER_NAME_X_GENERATION, value, vsize)) { | |
| xheaders |= XHEADER_X_GENERATION & allowedHeaders; | |
| } else if (header_field_eq(HEADER_NAME_X_TRANSACTION_ID, value, vsize)) { | |
| xheaders |= XHEADER_X_TRANSACTION_ID & allowedHeaders; | |
| } else if (header_field_eq(HEADER_NAME_X_REMAP, value, vsize)) { | |
| xheaders |= XHEADER_X_REMAP & allowedHeaders; | |
| } else if (header_field_eq(HEADER_NAME_VIA, value, vsize) && (XHEADER_VIA & allowedHeaders)) { | |
| // If the client requests the Via header, enable verbose Via debugging for this transaction. | |
| TSHttpTxnConfigIntSet(txn, TS_CONFIG_HTTP_INSERT_RESPONSE_VIA_STR, 3); | |
| } else if (header_field_eq(HEADER_NAME_DIAGS, value, vsize) && (XHEADER_DIAGS & allowedHeaders)) { | |
| // Enable diagnostics for DebugTxn()'s only | |
| TSHttpTxnCntlSet(txn, TS_HTTP_CNTL_TXN_DEBUG, true); | |
| } 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; | |
| if (is_probe_full_json) { | |
| data->body_encoding = requested_encoding; | |
| } | |
| auxData.body_builder.reset(data); | |
| TSVConn connp = TSTransformCreate(body_transform, txn); | |
| data->transform_connp.reset(connp); | |
| TSContDataSet(connp, txn); | |
| TSHttpTxnHookAdd(txn, TS_HTTP_RESPONSE_TRANSFORM_HOOK, connp); | |
| // Add hook to resolve encoding before body transformation (unless | |
| // explicitly set via =hex, etc.). | |
| if (is_probe_full_json && data->body_encoding == BodyEncoding_t::AUTO) { | |
| TSVConn encoding_connp = TSTransformCreate(XResolveEncoding, txn); | |
| data->resolve_encoding_connp.reset(encoding_connp); | |
| TSContDataSet(encoding_connp, txn); | |
| TSHttpTxnHookAdd(txn, TS_HTTP_READ_RESPONSE_HDR_HOOK, encoding_connp); | |
| } | |
| // disable writing to cache because we are injecting data into the body. | |
| TSHttpTxnCntlSet(txn, TS_HTTP_CNTL_RESPONSE_CACHEABLE, false); | |
| TSHttpTxnCntlSet(txn, TS_HTTP_CNTL_REQUEST_CACHEABLE, false); | |
| TSHttpTxnCntlSet(txn, TS_HTTP_CNTL_SERVER_NO_STORE, true); | |
| TSHttpTxnTransformedRespCache(txn, 0); | |
| TSHttpTxnUntransformedRespCache(txn, 0); | |
| } else if (header_field_eq(HEADER_NAME_X_PSELECT_KEY, value, vsize)) { | |
| xheaders |= XHEADER_X_PSELECT_KEY & allowedHeaders; | |
| } else if (isFwdFieldValue(std::string_view(value, vsize), fwdCnt)) { | |
| if (fwdCnt > 0) { | |
| // Decrement forward count in X-Debug header. | |
| char newVal[128]; | |
| snprintf(newVal, sizeof(newVal), "fwd=%" PRIiMAX, fwdCnt - 1); | |
| TSMimeHdrFieldValueStringSet(buffer, hdr, field, i, newVal, std::strlen(newVal)); | |
| } | |
| } else if (header_field_eq(HEADER_NAME_X_EFFECTIVE_URL, value, vsize)) { | |
| xheaders |= XHEADER_X_EFFECTIVE_URL & allowedHeaders; | |
| } else { | |
| Dbg(dbg_ctl, "ignoring unrecognized debug tag '%.*s'", vsize, value); | |
| } | |
| } | |
| #undef header_field_eq | |
| // Get the next duplicate. | |
| next = TSMimeHdrFieldNextDup(buffer, hdr, field); | |
| // Now release our reference. | |
| TSHandleMLocRelease(buffer, hdr, field); | |
| // And go to the next field. | |
| field = next; | |
| } | |
| if (xheaders) { | |
| Dbg(dbg_ctl, "adding response hook for header mask %p and forward count %" PRIiMAX, reinterpret_cast<void *>(xheaders), fwdCnt); | |
| TSHttpTxnHookAdd(txn, TS_HTTP_SEND_RESPONSE_HDR_HOOK, XInjectHeadersCont); | |
| AuxDataMgr::data(txn).xheaders = xheaders; | |
| if (fwdCnt == 0) { | |
| // X-Debug header has to be deleted, but not too soon for other plugins to see it. | |
| TSHttpTxnHookAdd(txn, TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, XDeleteDebugHdrCont); | |
| } | |
| } | |
| // Release the request header MLoc. | |
| TSHandleMLocRelease(buffer, TS_NULL_MLOC, hdr); | |
| return TS_EVENT_NONE; | |
| } | |
| // Continuation function to delete the x-debug header. | |
| // | |
| static int | |
| XDeleteDebugHdr(TSCont /* contp */, TSEvent event, void *edata) | |
| { | |
| TSHttpTxn txn = static_cast<TSHttpTxn>(edata); | |
| TSMLoc hdr, field; | |
| TSMBuffer buffer; | |
| // Make sure TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE) is called before exiting function. | |
| // | |
| ts::PostScript ps([=]() -> void { TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE); }); | |
| TSReleaseAssert(event == TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE); | |
| if (TSHttpTxnClientReqGet(txn, &buffer, &hdr) == TS_ERROR) { | |
| return TS_EVENT_NONE; | |
| } | |
| field = TSMimeHdrFieldFind(buffer, hdr, xDebugHeader.str, xDebugHeader.len); | |
| if (field == TS_NULL_MLOC) { | |
| return TS_EVENT_NONE; | |
| } | |
| if (TSMimeHdrFieldDestroy(buffer, hdr, field) == TS_ERROR) { | |
| TSError("Failure destroying %s header", xDebugHeader.str); | |
| } | |
| TSHandleMLocRelease(buffer, hdr, field); | |
| return TS_EVENT_NONE; | |
| } | |
| static void | |
| updateAllowedHeaders(const char *optarg) | |
| { | |
| char *list; | |
| char *token; | |
| char *last; | |
| list = TSstrdup(optarg); | |
| std::transform(list, list + strlen(list), list, ::tolower); | |
| for (token = strtok_r(list, ",", &last); token; token = strtok_r(nullptr, ",", &last)) { | |
| const auto ite = std::find_if(std::begin(header_flags), std::end(header_flags), | |
| [token](const struct XHeader &x) { return x.name.compare(token) == 0; }); | |
| if (ite != std::end(header_flags)) { | |
| Dbg(dbg_ctl, "Enabled allowed header name: %s", token); | |
| allowedHeaders |= ite->flag; | |
| } else { | |
| Dbg(dbg_ctl, "Unknown header name: %s", token); | |
| TSError("[xdebug] Unknown header name: %s", token); | |
| } | |
| } | |
| 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' }, | |
| {nullptr, no_argument, nullptr, '\0'} | |
| }; | |
| TSPluginRegistrationInfo info; | |
| info.plugin_name = (char *)"xdebug"; | |
| info.vendor_name = (char *)"Apache Software Foundation"; | |
| info.support_email = (char *)"dev@trafficserver.apache.org"; | |
| if (TSPluginRegister(&info) != TS_SUCCESS) { | |
| TSError("[xdebug] Plugin registration failed"); | |
| } | |
| // Parse the arguments | |
| while (true) { | |
| int opt = getopt_long(argc, const_cast<char *const *>(argv), "", longopt, nullptr); | |
| switch (opt) { | |
| case 'h': | |
| Dbg(dbg_ctl, "Setting header: %s", optarg); | |
| TSfree(xDebugHeader.str); // The option can be repeated, so the earlier value is not leaked | |
| xDebugHeader.str = TSstrdup(optarg); | |
| break; | |
| case 'e': | |
| updateAllowedHeaders(optarg); | |
| break; | |
| } | |
| if (opt == -1) { | |
| break; | |
| } | |
| } | |
| if (allowedHeaders == 0) { | |
| TSError("[xdebug] No features are enabled"); | |
| } | |
| if (nullptr == xDebugHeader.str) { | |
| xDebugHeader.str = TSstrdup("X-Debug"); // We malloc this, for consistency for future plugin unload events | |
| } | |
| xDebugHeader.len = strlen(xDebugHeader.str); | |
| // Make xDebugHeader available to other plugins, as a C-style string. | |
| // | |
| int idx = -1; | |
| auto ret = TSUserArgIndexReserve(TS_USER_ARGS_GLB, "XDebugHeader", "XDebug header name", &idx); | |
| TSReleaseAssert(ret == TS_SUCCESS); | |
| TSReleaseAssert(idx >= 0); | |
| TSUserArgSet(nullptr, idx, xDebugHeader.str); | |
| AuxDataMgr::init("xdebug"); | |
| // Setup the global hook | |
| XInjectHeadersCont = TSContCreate(XInjectResponseHeaders, nullptr); | |
| TSReleaseAssert(XInjectHeadersCont); | |
| XDeleteDebugHdrCont = TSContCreate(XDeleteDebugHdr, nullptr); | |
| TSReleaseAssert(XDeleteDebugHdrCont); | |
| TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, TSContCreate(XScanRequestHeaders, nullptr)); | |
| } |