This is an automated email from the ASF dual-hosted git repository.
bnolsen 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 421bfb6eb Add support for CMCD-Request header nor field to prefetch
plugin (#9232)
421bfb6eb is described below
commit 421bfb6ebed04bbab2e7ef4f139016d2413a9177
Author: Brian Olsen <[email protected]>
AuthorDate: Fri Mar 10 16:04:58 2023 -0700
Add support for CMCD-Request header nor field to prefetch plugin (#9232)
* prefetch: Add support for cmcd-request nor prefetch handling
* prefetch: restore failing behavior on missing cachekey
* prefetch: make cmcd autest more robust
* prefetch: cmcd percent decode and scrub query params
* revert debug statement change
---------
Co-authored-by: Brian Olsen <[email protected]>
---
doc/admin-guide/plugins/prefetch.en.rst | 24 ++
plugins/prefetch/common.h | 1 +
plugins/prefetch/configs.cc | 12 +-
plugins/prefetch/configs.h | 11 +-
plugins/prefetch/fetch.cc | 16 +-
plugins/prefetch/fetch.h | 5 +-
plugins/prefetch/plugin.cc | 227 ++++++++++----
.../pluginTest/prefetch/header_rewrite.conf | 19 ++
.../pluginTest/prefetch/prefetch_cmcd.test.py | 337 +++++++++++++++++++++
.../pluginTest/prefetch/prefetch_cmcd0.gold | 13 +
.../pluginTest/prefetch/prefetch_cmcd1.gold | 12 +
.../prefetch_simple.gold | 0
.../prefetch_simple.test.py | 0
13 files changed, 608 insertions(+), 69 deletions(-)
diff --git a/doc/admin-guide/plugins/prefetch.en.rst
b/doc/admin-guide/plugins/prefetch.en.rst
index 74312d934..3a23d8dc3 100644
--- a/doc/admin-guide/plugins/prefetch.en.rst
+++ b/doc/admin-guide/plugins/prefetch.en.rst
@@ -170,6 +170,29 @@ specified with an integer followed by a colon, e.g.
``{8:$2+2}``,
causing the resulting number to be padded with leading zeroes if it
has fewer digits than the width.
+CMCD (Common Media Client Data) CMCD-Request header with nor field
+------------------------------------------------------------------
+
+If the ``--cmcd-nor`` option is specified the Cmcd-Request header with nor
field is handled.
+
+With setup ::
+
+ map http://example.com http://origin.com \
+ @plugin=cachekey.so @pparam=--remove-all-params=true \
+ @plugin=prefetch.so \
+ @pparam=--cmcd-nor=true
+
+If the incoming request is ::
+
+ http://example-seed.com/path/someitem.m4a
+
+with header ::
+
+ Cmcd-Request: nor="otheritem.m4a"
+
+The following URL will be requested to be prefetched ::
+
+ http://example-seed.com/path/otheritem.m4a
Overhead from **next object** prefetch
--------------------------------------
@@ -238,6 +261,7 @@ Plugin parameters
- ``true`` - configures the plugin run on the **front-tier**,
- ``false`` - to be run on the **back-tier**.
* ``--api-header`` - the header used by the plugin internally, also used to
mark a prefetch request to the next tier in dual-tier usage.
+* ``--cmcd-nor`` - prefetch for a Cmcd-Request header with nor field.
* ``--fetch-policy`` - fetch policy
- ``simple`` - this policy just makes sure there are no same concurrent
prefetches triggered (default and always used in combination with any other
policy)
- ``lru:n`` - this policy uses LRU to identify "hot" objects and triggers
prefetch if the object is not found. `n` is the size of the LRU
diff --git a/plugins/prefetch/common.h b/plugins/prefetch/common.h
index 35bbab0d9..8c0ae36ae 100644
--- a/plugins/prefetch/common.h
+++ b/plugins/prefetch/common.h
@@ -31,6 +31,7 @@
#include <vector>
typedef std::string String;
+typedef std::string_view StringView;
typedef std::set<std::string> StringSet;
typedef std::list<std::string> StringList;
typedef std::vector<std::string> StringVector;
diff --git a/plugins/prefetch/configs.cc b/plugins/prefetch/configs.cc
index 21c2ffa81..aa869b563 100644
--- a/plugins/prefetch/configs.cc
+++ b/plugins/prefetch/configs.cc
@@ -57,6 +57,7 @@ PrefetchConfig::init(int argc, char *argv[])
static const struct option longopt[] = {
{const_cast<char *>("front"), optional_argument, nullptr,
'f'},
{const_cast<char *>("api-header"), optional_argument, nullptr,
'h'},
+ {const_cast<char *>("cmcd-nor"), optional_argument, nullptr,
'd'},
{const_cast<char *>("next-header"), optional_argument, nullptr,
'n'},
{const_cast<char *>("fetch-policy"), optional_argument, nullptr,
'p'},
{const_cast<char *>("fetch-count"), optional_argument, nullptr,
'c'},
@@ -68,15 +69,15 @@ PrefetchConfig::init(int argc, char *argv[])
{const_cast<char *>("metrics-prefix"), optional_argument, nullptr,
'm'},
{const_cast<char *>("exact-match"), optional_argument, nullptr,
'y'},
{const_cast<char *>("log-name"), optional_argument, nullptr,
'l'},
- {nullptr, 0, nullptr, 0 }
+ {nullptr, 0, nullptr, 0
},
};
bool status = true;
optind = 0;
/* argv contains the "to" and "from" URLs. Skip the first so that the second
one poses as the program name. */
- argc--;
- argv++;
+ --argc;
+ ++argv;
for (;;) {
int opt;
@@ -97,6 +98,10 @@ PrefetchConfig::init(int argc, char *argv[])
setApiHeader(optarg);
break;
+ case 'd': /* --cmcd-nor */
+ _cmcd_nor = ::isTrue(optarg);
+ break;
+
case 'n': /* --next-header */
setNextHeader(optarg);
break;
@@ -167,6 +172,7 @@ PrefetchConfig::finalize()
PrefetchDebug("front-end: %s", (_front ? "true" : "false"));
PrefetchDebug("exact match: %s", (_exactMatch ? "true" : "false"));
PrefetchDebug("query key: %s", _queryKey.c_str());
+ PrefetchDebug("cncd-nor: %s", (_front ? "true" : "false"));
PrefetchDebug("API header name: %s", _apiHeader.c_str());
PrefetchDebug("next object header name: %s", _nextHeader.c_str());
PrefetchDebug("fetch policy parameters: %s", _fetchPolicy.c_str());
diff --git a/plugins/prefetch/configs.h b/plugins/prefetch/configs.h
index 1996f4fce..c1058c5e9 100644
--- a/plugins/prefetch/configs.h
+++ b/plugins/prefetch/configs.h
@@ -35,8 +35,8 @@ class PrefetchConfig
{
public:
PrefetchConfig()
- : _apiHeader("X-AppleCDN-Prefetch"),
- _nextHeader("X-AppleCDN-Prefetch-Next"),
+ : _apiHeader("X-CDN-Prefetch"),
+ _nextHeader("X-CDN-Prefetch-Next"),
_replaceHost(),
_namespace("default"),
_metricsPrefix("prefetch.stats")
@@ -111,6 +111,12 @@ public:
return _exactMatch;
}
+ bool
+ isCmcdNor() const
+ {
+ return _cmcd_nor;
+ }
+
void
setFetchCount(const char *optarg)
{
@@ -208,5 +214,6 @@ private:
unsigned _fetchMax = 0;
bool _front = false;
bool _exactMatch = false;
+ bool _cmcd_nor = false;
MultiPattern _nextPaths;
};
diff --git a/plugins/prefetch/fetch.cc b/plugins/prefetch/fetch.cc
index a114be055..d1a9b3e98 100644
--- a/plugins/prefetch/fetch.cc
+++ b/plugins/prefetch/fetch.cc
@@ -409,11 +409,12 @@ BgFetch::~BgFetch()
bool
BgFetch::schedule(BgFetchState *state, const PrefetchConfig &config, bool
askPermission, TSMBuffer requestBuffer,
- TSMLoc requestHeaderLoc, TSHttpTxn txnp, const char *path,
size_t pathLen, const String &cachekey)
+ TSMLoc requestHeaderLoc, TSHttpTxn txnp, const char *path,
size_t pathLen, const String &cachekey,
+ bool removeQuery)
{
bool ret = false;
BgFetch *fetch = new BgFetch(state, config, askPermission);
- if (fetch->init(requestBuffer, requestHeaderLoc, txnp, path, pathLen,
cachekey)) {
+ if (fetch->init(requestBuffer, requestHeaderLoc, txnp, path, pathLen,
cachekey, removeQuery)) {
fetch->schedule();
ret = true;
} else {
@@ -451,7 +452,7 @@ BgFetch::addBytes(int64_t b)
*/
bool
BgFetch::init(TSMBuffer reqBuffer, TSMLoc reqHdrLoc, TSHttpTxn txnp, const
char *fetchPath, size_t fetchPathLen,
- const String &cachekey)
+ const String &cachekey, bool removeQuery)
{
TSAssert(TS_NULL_MLOC == _headerLoc);
TSAssert(TS_NULL_MLOC == _urlLoc);
@@ -506,6 +507,15 @@ BgFetch::init(TSMBuffer reqBuffer, TSMLoc reqHdrLoc,
TSHttpTxn txnp, const char
return false;
}
+ /* Remove the query string */
+ if (removeQuery) {
+ if (TS_SUCCESS == TSUrlHttpQuerySet(_mbuf, _urlLoc, "", 0)) {
+ PrefetchDebug("original query string removed");
+ } else {
+ PrefetchError("failed to remove original query string");
+ }
+ }
+
/* Now set or remove the prefetch API header */
const String &header = _config.getApiHeader();
if (_config.isFront()) {
diff --git a/plugins/prefetch/fetch.h b/plugins/prefetch/fetch.h
index 0471e1e71..3f72e4ea8 100644
--- a/plugins/prefetch/fetch.h
+++ b/plugins/prefetch/fetch.h
@@ -169,13 +169,14 @@ class BgFetch
{
public:
static bool schedule(BgFetchState *state, const PrefetchConfig &config, bool
askPermission, TSMBuffer requestBuffer,
- TSMLoc requestHeaderLoc, TSHttpTxn txnp, const char
*path, size_t pathLen, const String &cachekey);
+ TSMLoc requestHeaderLoc, TSHttpTxn txnp, const char
*path, size_t pathLen, const String &cachekey,
+ bool removeQuery = false);
private:
BgFetch(BgFetchState *state, const PrefetchConfig &config, bool lock);
~BgFetch();
bool init(TSMBuffer requestBuffer, TSMLoc requestHeaderLoc, TSHttpTxn txnp,
const char *fetchPath, size_t fetchPathLen,
- const String &cacheKey);
+ const String &cacheKey, bool removeQuery = false);
void schedule();
static int handler(TSCont contp, TSEvent event, void * /* edata ATS_UNUSED
*/);
bool saveIp(TSHttpTxn txnp);
diff --git a/plugins/prefetch/plugin.cc b/plugins/prefetch/plugin.cc
index ef1d2991d..7306cfbb7 100644
--- a/plugins/prefetch/plugin.cc
+++ b/plugins/prefetch/plugin.cc
@@ -20,9 +20,7 @@
* @file plugin.cc
* @brief traffic server plugin entry points.
*/
-
#include <sstream>
-#include <iomanip>
#include "ts/ts.h" /* ATS API */
@@ -227,8 +225,12 @@ appendCacheKey(const TSHttpTxn txnp, const TSMBuffer
reqBuffer, String &key)
TSfree(static_cast<void *>(url));
ret = true;
}
+ } else {
+ PrefetchDebug("Failure lookup up cache url");
}
TSHandleMLocRelease(reqBuffer, TS_NULL_MLOC, keyLoc);
+ } else {
+ PrefetchDebug("Failure creating url");
}
if (!ret) {
@@ -348,6 +350,79 @@ getPristineUrlQuery(TSHttpTxn txnp)
return pristineQuery;
}
+static constexpr StringView CmcdHeader{"Cmcd-Request"};
+static constexpr StringView CmcdNorFieldPrefix{"nor="};
+static constexpr StringView CmcdNrrFieldPrefix{"nrr="};
+
+/**
+ * @brief Look for and return the nor field of any Cmcd-Request header
+ *
+ * @param txnp HTTP transaction structure
+ * @param buffer request TSMBuffer
+ * @param hdrloc request TSMLoc
+ * @return unquoted relative cmcd path from the nor field
+ *
+ * If the 'nrr' field is encountered the 'nor' field will be ignored.
+ *
+ * sample header:
+ * Cmcd-Request: mtp=103600,bl=153500,nor="14_176.mp4a"
+ */
+static String
+getCmcdNor(const TSMBuffer buffer, const TSMLoc hdrloc)
+{
+ String relpath;
+ bool hasnrr = false; // don't prefetch if range request
+
+ const TSMLoc cmcdloc = TSMimeHdrFieldFind(buffer, hdrloc, CmcdHeader.data(),
CmcdHeader.length());
+ if (TS_NULL_MLOC != cmcdloc) {
+ // iterate through the fields
+ const int cnt = TSMimeHdrFieldValuesCount(buffer, hdrloc, cmcdloc);
+ for (int ind = 0; ind < cnt; ++ind) {
+ int flen = 0;
+ const char *const fval = TSMimeHdrFieldValueStringGet(buffer, hdrloc,
cmcdloc, ind, &flen);
+
+ StringView fv(fval, flen);
+ PrefetchDebug("cmcd-request field: '%.*s'", (int)fv.length(), fv.data());
+ if (0 == fv.compare(0, CmcdNrrFieldPrefix.length(), CmcdNrrFieldPrefix))
{
+ PrefetchDebug("cmcd-request nrr field encountered, skipping
prefetch!");
+ hasnrr = true;
+ break;
+ }
+
+ if (0 == fv.compare(0, CmcdNorFieldPrefix.length(), CmcdNorFieldPrefix))
{
+ fv.remove_prefix(CmcdNorFieldPrefix.length());
+ if (fv.front() == '"') {
+ fv.remove_prefix(1);
+ }
+ if (fv.back() == '"') {
+ fv.remove_suffix(1);
+ }
+
+ PrefetchDebug("Extracted nor field: '%.*s'", (int)fv.length(),
fv.data());
+
+ // Undo any percent encoding
+ char buf[8192];
+ size_t blen = sizeof(buf);
+ if (TS_SUCCESS == TSStringPercentDecode(fv.data(), fv.length(), buf,
blen, &blen)) {
+ relpath.assign(buf, blen);
+ } else {
+ PrefetchDebug("Error percent decoding nor field: '%.*s'",
(int)fv.length(), fv.data());
+ }
+ }
+ }
+ TSHandleMLocRelease(buffer, hdrloc, cmcdloc);
+ } else {
+ PrefetchDebug("No Cmcd-Request header found");
+ }
+
+ // don't prefetch if range request
+ if (hasnrr) {
+ relpath.clear();
+ }
+
+ return relpath;
+}
+
/**
* @brief short-cut to set the response .
*/
@@ -425,19 +500,26 @@ contHandleFetch(const TSCont contp, TSEvent event, void
*edata)
TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);
PrefetchConfig &config = data->_inst->_config;
BgFetchState *state = data->_inst->_state;
- TSMBuffer reqBuffer;
- TSMLoc reqHdrLoc;
+ TSMBuffer reqBuffer = nullptr;
+ TSMLoc reqHdrLoc = TS_NULL_MLOC;
PrefetchDebug("event: %s (%d)", getEventName(event), event);
TSEvent retEvent = TS_EVENT_HTTP_CONTINUE;
- if ((event == TS_EVENT_HTTP_POST_REMAP || event ==
TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE ||
- event == TS_EVENT_HTTP_SEND_RESPONSE_HDR) &&
- TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &reqBuffer, &reqHdrLoc)) {
- PrefetchError("failed to get client request");
- TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
- return 0;
+ // For these cases we need to access the client request
+ switch (event) {
+ case TS_EVENT_HTTP_POST_REMAP:
+ case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE:
+ case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
+ if (TS_SUCCESS != TSHttpTxnClientReqGet(txnp, &reqBuffer, &reqHdrLoc)) {
+ PrefetchError("failed to get client request");
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_ERROR);
+ return 0;
+ }
+ break;
+ default:
+ break;
}
switch (event) {
@@ -506,69 +588,96 @@ contHandleFetch(const TSCont contp, TSEvent event, void
*edata)
if (data->frontend()) {
/* front-end instance */
- String currentPath = getPristineUrlPath(txnp);
- String currentQuery = getPristineUrlQuery(txnp);
- bool hasValidQuery = false;
+ const String currentPath = getPristineUrlPath(txnp);
+ const String currentQuery = getPristineUrlQuery(txnp);
+ bool hasValidQuery = false;
// If there is a --fetch-query defined in the config, and that string is
found in the querystring, assume it is
// valid, and prefer the --fetch-query over the --fetch-path-pattern(s).
- if (!config.getQueryKeyName().empty() &&
currentQuery.find(config.getQueryKeyName()) != std::string::npos) {
+ if (!config.getQueryKeyName().empty() &&
currentQuery.find(config.getQueryKeyName()) != String::npos) {
PrefetchDebug("Setting hasValidQuery to true");
hasValidQuery = true;
}
- if (!hasValidQuery && data->firstPass() && data->_fetchable &&
!config.getNextPath().empty() && respToTriggerPrefetch(txnp)) {
- /* Trigger all necessary background fetches based on the next path
pattern */
+ if (data->firstPass() && data->_fetchable &&
respToTriggerPrefetch(txnp)) {
+ // If configured to handle Cmcd-Request nor field.
+ // This still allows other prefetch configurations to work.
+ if (config.isCmcdNor()) {
+ PrefetchDebug("Considering cmcd nor request");
- if (!currentPath.empty()) {
- unsigned total = config.getFetchCount();
- for (unsigned i = 0; i < total; ++i) {
- PrefetchDebug("generating prefetch request %d/%d", i + 1, total);
- String expandedPath;
+ TSAssert(nullptr != reqBuffer);
+ TSAssert(TS_NULL_MLOC != reqHdrLoc);
- if (config.getNextPath().replace(currentPath, expandedPath)) {
- PrefetchDebug("replaced: %s", expandedPath.c_str());
- expand(expandedPath);
- PrefetchDebug("expanded: %s cachekey: %s", expandedPath.c_str(),
data->_cachekey.c_str());
+ const String relpath = getCmcdNor(reqBuffer, reqHdrLoc);
+ if (!relpath.empty()) {
+ PrefetchDebug("Current path: '%s'", currentPath.c_str());
+ PrefetchDebug("Parsed cmcd nor relpath: '%s'", relpath.c_str());
- BgFetch::schedule(state, config, /* askPermission */ false,
reqBuffer, reqHdrLoc, txnp, expandedPath.c_str(),
- expandedPath.length(), data->_cachekey);
- } else {
- /* We should be here only if the pattern replacement fails
(match already checked) */
- PrefetchError("failed to process the pattern");
+ const String::size_type lsi = currentPath.find_last_of("/");
+ const String nextPath = currentPath.substr(0, lsi + 1) +
relpath;
- /* If the first or any matches fails there must be something
wrong so don't continue */
- break;
- }
- currentPath.assign(expandedPath);
+ PrefetchDebug("Next cmcd nor path: '%s'", nextPath.c_str());
+
+ constexpr bool askPermission = false;
+ constexpr bool removeQuery = true;
+ BgFetch::schedule(state, config, askPermission, reqBuffer,
reqHdrLoc, txnp, nextPath.c_str(), nextPath.length(),
+ data->_cachekey, removeQuery);
}
- } else {
- PrefetchDebug("failed to get current path");
}
- }
- if (hasValidQuery && data->firstPass() && data->_fetchable &&
respToTriggerPrefetch(txnp)) {
- /* Trigger all necessary background fetches based on the query
string(s) */
-
- PrefetchDebug("currentQuery: %s", currentQuery.c_str());
- size_t lastSlashIndex = currentPath.find_last_of("/");
- size_t keyLen = config.getQueryKeyName().size();
- unsigned done = 1;
- std::istringstream cStringStream(currentQuery);
- std::string param;
-
- while (getline(cStringStream, param, '&')) {
- if (param.find(config.getQueryKeyName()) != 0) {
- continue;
- }
- if (config.getFetchCount() < done++) {
- break;
+
+ if (!hasValidQuery && !config.getNextPath().empty()) {
+ /* Trigger all necessary background fetches based on the next path
pattern */
+
+ if (!currentPath.empty()) {
+ const unsigned total = config.getFetchCount();
+ String workingPath = currentPath;
+ for (unsigned i = 0; i < total; ++i) {
+ PrefetchDebug("generating prefetch request %d/%d", i + 1, total);
+ String expandedPath;
+
+ if (config.getNextPath().replace(workingPath, expandedPath)) {
+ PrefetchDebug("replaced: %s", expandedPath.c_str());
+ expand(expandedPath);
+ PrefetchDebug("expanded: %s cachekey: %s",
expandedPath.c_str(), data->_cachekey.c_str());
+
+ BgFetch::schedule(state, config, /* askPermission */ false,
reqBuffer, reqHdrLoc, txnp, expandedPath.c_str(),
+ expandedPath.length(), data->_cachekey);
+ } else {
+ /* We should be here only if the pattern replacement fails
(match already checked) */
+ PrefetchError("failed to process the pattern");
+
+ /* If the first or any matches fails there must be something
wrong so don't continue */
+ break;
+ }
+ workingPath.assign(expandedPath);
+ }
+ } else {
+ PrefetchDebug("failed to get current path");
}
- std::string nextFile = param.substr(keyLen + 1); // +1 for the '='
- std::string nextPath = currentPath.substr(0, lastSlashIndex + 1) +
nextFile;
+ } else if (hasValidQuery) {
+ /* Trigger all necessary background fetches based on the query
string(s) */
+
+ PrefetchDebug("currentQuery: %s", currentQuery.c_str());
+ const size_t lastSlashIndex = currentPath.find_last_of("/");
+ const size_t keyLen = config.getQueryKeyName().size();
+ unsigned done = 1;
+ std::istringstream cStringStream(currentQuery);
+ String param;
+
+ while (getline(cStringStream, param, '&')) {
+ if (param.find(config.getQueryKeyName()) != 0) {
+ continue;
+ }
+ if (config.getFetchCount() < done++) {
+ break;
+ }
+ String nextFile = param.substr(keyLen + 1); // +1 for the '='
+ String nextPath = currentPath.substr(0, lastSlashIndex + 1) +
nextFile;
- PrefetchDebug("nextPath %s, cacheKey %s", nextPath.c_str(),
data->_cachekey.c_str());
- BgFetch::schedule(state, config, /* askPermission */ false,
reqBuffer, reqHdrLoc, txnp, nextPath.c_str(),
- nextPath.length(), data->_cachekey);
+ PrefetchDebug("nextPath %s, cacheKey %s", nextPath.c_str(),
data->_cachekey.c_str());
+ BgFetch::schedule(state, config, /* askPermission */ false,
reqBuffer, reqHdrLoc, txnp, nextPath.c_str(),
+ nextPath.length(), data->_cachekey);
+ }
}
}
}
@@ -720,7 +829,7 @@ TSRemapDoRemap(void *instance, TSHttpTxn txnp,
TSRemapRequestInfo *rri)
/* Make sure we handle only URLs that match the path pattern on the
front-end + first-pass, cancel otherwise */
bool handleFetch = true;
- if (front && firstPass) {
+ if (front && firstPass && !config.isCmcdNor()) {
/* Front-end plug-in instance + first pass. */
if (config.getNextPath().empty()) {
/* No next path pattern specified then pass this request untouched.
*/
diff --git a/tests/gold_tests/pluginTest/prefetch/header_rewrite.conf
b/tests/gold_tests/pluginTest/prefetch/header_rewrite.conf
new file mode 100644
index 000000000..04631fa6a
--- /dev/null
+++ b/tests/gold_tests/pluginTest/prefetch/header_rewrite.conf
@@ -0,0 +1,19 @@
+#
+# 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.
+
+cond %{TXN_START_HOOK}
+set-header x-debug x-cache-key
diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd.test.py
b/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd.test.py
new file mode 100644
index 000000000..43017edc0
--- /dev/null
+++ b/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd.test.py
@@ -0,0 +1,337 @@
+'''
+'''
+# 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.
+
+import os
+import urllib.parse
+
+Test.Summary = '''
+Test prefetch.so plugin (simple mode).
+'''
+
+origin = Test.MakeOriginServer("origin")
+
+asset_name = 'request.txt'
+pf_name = 'prefetch.txt'
+pf_header = f'Cmcd-Request: foo=12,nor="{pf_name}",bar=42'
+
+request_header = {
+ "headers":
+ f"GET /tests/{asset_name} HTTP/1.1\r\n"
+ "Host: does.not.matter\r\n" # But cant be omitted
+ f"{pf_header}\r\n"
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": ""
+}
+response_header = {
+ "headers":
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "Cache-control: max-age=60\r\n"
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": f"This is the body for {asset_name}\n"
+}
+origin.addResponse("sessionlog.json", request_header, response_header)
+
+# query string
+query_name = 'query?this=foo&that'
+query_pf_name = 'query?bar=baz'
+query_pf_header = f'Cmcd-Request: nor="{query_pf_name}"'
+
+# nor field may be percent encoded
+query_pf_perc_name = urllib.parse.quote(query_pf_name)
+query_pf_perc_header = f'Cmcd-Request: nor="{query_pf_perc_name}"'
+
+request_header = {
+ "headers":
+ f"GET /tests/{query_name} HTTP/1.1\r\n"
+ "Host: does.not.matter\r\n" # But cant be omitted
+ f"{query_pf_perc_header}\r\n"
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": ""
+}
+response_header = {
+ "headers":
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "Cache-control: max-age=60\r\n"
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": f"This is the body for {query_name}\n"
+}
+origin.addResponse("sessionlog.json", request_header, response_header)
+
+# setup the prefetched assets
+names = [pf_name, query_pf_name]
+
+for name in names:
+ request_header = {
+ "headers":
+ f"GET /tests/{name} HTTP/1.1\r\n"
+ "Host: does.not.matter\r\n" # But cant be omitted
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": ""
+ }
+ response_header = {
+ "headers":
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "Cache-control: max-age=60\r\n"
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": f"This is the body for {name}\n"
+ }
+ origin.addResponse("sessionlog.json", request_header, response_header)
+
+# prefetch from root
+root_name = 'root.txt'
+root_header = f'Cmcd-Request: nor="rooted"'
+
+request_header = {
+ "headers":
+ f"GET /{root_name} HTTP/1.1\r\n"
+ "Host: does.not.matter\r\n" # But cant be omitted
+ f"{root_header}\r\n"
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": ""
+}
+response_header = {
+ "headers":
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "Cache-control: max-age=60\r\n"
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": f"This is the body for {root_name}\n"
+}
+origin.addResponse("sessionlog.json", request_header, response_header)
+
+request_header = {
+ "headers":
+ f"GET /rooted HTTP/1.1\r\n"
+ "Host: does.not.matter\r\n" # But cant be omitted
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": ""
+}
+response_header = {
+ "headers":
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "Cache-control: max-age=60\r\n"
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": f"This is the body for rooted\n"
+}
+origin.addResponse("sessionlog.json", request_header, response_header)
+
+# ignore if cmcd-request nrr= found
+crr_name = 'crr.txt'
+crr_header = f'Cmcd-Request: foo=12,nor="{crr_name}",bar=42,nrr="0-"'
+request_header = {
+ "headers":
+ f"GET /tests/{crr_name} HTTP/1.1\r\n"
+ "Host: does.not.matter\r\n" # But cant be omitted
+ f"{crr_header}\r\n"
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": ""
+}
+response_header = {
+ "headers":
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "Cache-control: max-age=60\r\n"
+ "\r\n",
+ "timestamp": "1469733493.993",
+ "body": f"This is the body for {crr_name}\n"
+}
+origin.addResponse("sessionlog.json", request_header, response_header)
+
+# allows for multiple ats on localhost
+dns = Test.MakeDNServer("dns")
+
+# next hop trafficserver instance
+ts1 = Test.MakeATSProcess("ts1")
+ts1.Disk.records_config.update({
+ 'proxy.config.diags.debug.enabled': 1,
+ 'proxy.config.diags.debug.tags': 'prefetch|http',
+ 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}",
+ 'proxy.config.dns.resolv_conf': "NULL",
+ 'proxy.config.http.parent_proxy.self_detect': 0,
+})
+dns.addRecords(records={f"ts1": ["127.0.0.1"]})
+ts1.Disk.remap_config.AddLine(
+ f"map / http://127.0.0.1:{origin.Variables.Port}" +
+ " @plugin=cachekey.so @pparam==--sort-params=true"
+ " @plugin=prefetch.so @pparam==--front=false"
+)
+
+ts1.Disk.logging_yaml.AddLines(
+ '''
+logging:
+ formats:
+ - name: custom
+ format: '%<cquuc> %<pssc> %<crc> %<cwr> %<pscl> %<{X-CDN-Prefetch}cqh>'
+ logs:
+ - filename: transaction
+ format: custom
+'''.split("\n")
+)
+
+ts0 = Test.MakeATSProcess("ts0")
+ts0.Disk.records_config.update({
+ 'proxy.config.diags.debug.enabled': 1,
+ 'proxy.config.diags.debug.tags': 'prefetch|http',
+ 'proxy.config.dns.nameservers': f"127.0.0.1:{dns.Variables.Port}",
+ 'proxy.config.dns.resolv_conf': "NULL",
+ 'proxy.config.http.parent_proxy.self_detect': 0,
+})
+
+dns.addRecords(records={f"ts0": ["127.0.0.1"]})
+ts0.Disk.remap_config.AddLine(
+ f"map http://ts0 http://ts1:{ts1.Variables.port}" +
+ " @plugin=cachekey.so @pparam=--sort-params=true"
+ " @plugin=prefetch.so" +
+ " @pparam=--front=true" +
+ " @pparam=--fetch-policy=simple" +
+ " @pparam=--cmcd-nor=true"
+)
+
+ts0.Disk.logging_yaml.AddLines(
+ '''
+logging:
+ formats:
+ - name: custom
+ format: '%<cquuc> %<pssc> %<crc> %<cwr> %<pscl> %<{X-CDN-Prefetch}cqh>'
+ logs:
+ - filename: transaction
+ format: custom
+'''.split("\n")
+)
+
+
+# start everything up
+tr = Test.AddTestRun()
+tr.Processes.Default.StartBefore(origin)
+tr.Processes.Default.StartBefore(dns)
+tr.Processes.Default.StartBefore(ts0)
+tr.Processes.Default.StartBefore(ts1)
+tr.Processes.Default.Command = 'echo start TS, TSH_N, HTTP origin and DNS.'
+tr.Processes.Default.ReturnCode = 0
+
+# attempt to get normal asset
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+ f"curl --verbose --proxy 127.0.0.1:{ts0.Variables.port}
http://ts0/tests/{asset_name}"
+)
+tr.Processes.Default.ReturnCode = 0
+
+# issue curl form same asset, with prefetch
+tr = Test.AddTestRun()
+tr.DelayStart = 1
+tr.Processes.Default.Command = (
+ f"curl --verbose --proxy 127.0.0.1:{ts0.Variables.port}
http://ts0/tests/{asset_name} -H \'{pf_header}\'"
+)
+tr.Processes.Default.ReturnCode = 0
+
+# fetch the prefetched asset (only cached on ts1)
+tr = Test.AddTestRun()
+tr.DelayStart = 1
+tr.Processes.Default.Command = (
+ f"curl --verbose --proxy 127.0.0.1:{ts0.Variables.port}
http://ts0/tests/{pf_name}"
+)
+tr.Processes.Default.ReturnCode = 0
+
+# attempt to prefetch again
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+ f"curl --verbose --proxy 127.0.0.1:{ts0.Variables.port}
http://ts0/tests/{asset_name} -H \'{pf_header}\'"
+)
+tr.Processes.Default.ReturnCode = 0
+
+# request the prefetched asset
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+ f"curl --verbose --proxy 127.0.0.1:{ts0.Variables.port}
http://ts0/tests/{pf_name}"
+)
+tr.Processes.Default.ReturnCode = 0
+
+# prefetch using query params with query prefetch perc encoded
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+ f"curl --verbose --proxy 127.0.0.1:{ts0.Variables.port}
\'http://ts0/tests/{query_name}\' -H \'{query_pf_perc_header}\'"
+)
+tr.Processes.Default.ReturnCode = 0
+
+# request the prefetched asset without perc encoding
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+ f"curl --verbose --proxy 127.0.0.1:{ts0.Variables.port}
\'http://ts0/tests/{query_pf_name}\'"
+)
+tr.Processes.Default.ReturnCode = 0
+
+# ensure root path prefetch works
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+ f"curl --verbose --proxy 127.0.0.1:{ts0.Variables.port}
\'http://ts0/{root_name}\' -H \'{root_header}\'"
+)
+tr.Processes.Default.ReturnCode = 0
+
+# ensure request with nrr= field is skipped
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+ f"curl --verbose --proxy 127.0.0.1:{ts0.Variables.port}
\'http://ts0/{crr_name}\' -H \'{crr_header}\'"
+)
+tr.Processes.Default.ReturnCode = 0
+
+condwaitpath = os.path.join(Test.Variables.AtsTestToolsDir, 'condwait')
+
+# look for ts transaction log
+ts0log = os.path.join(ts0.Variables.LOGDIR, 'transaction.log')
+tr = Test.AddTestRun()
+ps = tr.Processes.Default
+ps.Command = (
+ condwaitpath + ' 60 1 -f ' + ts0log
+)
+
+# look for ts1 transaction log
+ts1log = os.path.join(ts1.Variables.LOGDIR, 'transaction.log')
+tr = Test.AddTestRun()
+ps = tr.Processes.Default
+ps.Command = (
+ condwaitpath + ' 60 1 -f ' + ts1log
+)
+
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+ f"cat {ts0log}"
+)
+tr.Streams.stdout = "prefetch_cmcd0.gold"
+tr.Processes.Default.ReturnCode = 0
+
+tr = Test.AddTestRun()
+tr.Processes.Default.Command = (
+ f"cat {ts1log}"
+)
+tr.Streams.stdout = "prefetch_cmcd1.gold"
+tr.Processes.Default.ReturnCode = 0
diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd0.gold
b/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd0.gold
new file mode 100644
index 000000000..84aeaf45f
--- /dev/null
+++ b/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd0.gold
@@ -0,0 +1,13 @@
+http://ts0/tests/request.txt 200 TCP_MISS FIN 33 -
+http://ts0/tests/request.txt 200 TCP_HIT - 33 -
+http://ts0/tests/prefetch.txt 200 TCP_MISS - 16 tests/request.txt
+http://ts0/tests/prefetch.txt 200 TCP_MISS FIN 34 -
+http://ts0/tests/request.txt 200 ``_HIT - 33 -
+http://ts0/tests/prefetch.txt 208 TCP_HIT - 20 tests/request.txt
+http://ts0/tests/prefetch.txt 200 ``_HIT - 34 -
+http://ts0/tests/query?this=foo&that 200 TCP_MISS FIN 41 -
+http://ts0/tests/query?bar=baz 200 TCP_MISS - 16 tests/query
+http://ts0/tests/query?bar=baz 200 TCP_MISS FIN 35 -
+http://ts0/root.txt 200 TCP_MISS FIN 30 -
+http://ts0/rooted 200 TCP_MISS - 16 root.txt
+http://ts0/crr.txt 404 TCP_MISS - 5 -
diff --git a/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd1.gold
b/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd1.gold
new file mode 100644
index 000000000..1d2b7fcea
--- /dev/null
+++ b/tests/gold_tests/pluginTest/prefetch/prefetch_cmcd1.gold
@@ -0,0 +1,12 @@
+http://ts1:``/tests/request.txt 200 TCP_MISS FIN 33 -
+http://ts1:``/tests/prefetch.txt 200 TCP_MISS - 16 tests/request.txt
+http://ts1:``/tests/prefetch.txt 200 TCP_MISS FIN 34 -
+http://ts1:``/tests/prefetch.txt 200 TCP_HIT - 34 -
+http://ts1:``/tests/query?this=foo&that 200 TCP_MISS FIN 41 -
+http://ts1:``/tests/query?bar=baz 200 TCP_MISS - 16 tests/query
+http://ts1:``/tests/query?bar=baz 200 TCP_MISS FIN 35 -
+http://ts1:``/tests/query?bar=baz 200 TCP_HIT - 35 -
+http://ts1:``/root.txt 200 TCP_MISS FIN 30 -
+http://ts1:``/rooted 200 TCP_MISS - 16 root.txt
+http://ts1:``/rooted 200 TCP_MISS FIN 28 -
+http://ts1:``/crr.txt 404 TCP_MISS - 5 -
diff --git a/tests/gold_tests/pluginTest/prefetch_simple/prefetch_simple.gold
b/tests/gold_tests/pluginTest/prefetch/prefetch_simple.gold
similarity index 100%
rename from tests/gold_tests/pluginTest/prefetch_simple/prefetch_simple.gold
rename to tests/gold_tests/pluginTest/prefetch/prefetch_simple.gold
diff --git
a/tests/gold_tests/pluginTest/prefetch_simple/prefetch_simple.test.py
b/tests/gold_tests/pluginTest/prefetch/prefetch_simple.test.py
similarity index 100%
rename from tests/gold_tests/pluginTest/prefetch_simple/prefetch_simple.test.py
rename to tests/gold_tests/pluginTest/prefetch/prefetch_simple.test.py