You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by bn...@apache.org on 2024/02/07 16:37:49 UTC
(trafficserver) branch master updated: stale_response: RFC 5861 Cache-Control Directives (#11017)
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 dacca3d08e stale_response: RFC 5861 Cache-Control Directives (#11017)
dacca3d08e is described below
commit dacca3d08e0eeac1200d9e3e0337900b2e0bcf79
Author: Brian Neradt <br...@gmail.com>
AuthorDate: Wed Feb 7 10:37:40 2024 -0600
stale_response: RFC 5861 Cache-Control Directives (#11017)
This adds a new experimental plugin called stale_response.so. It
implements the stale-while-revalidate and stale-if-error Cache-Control
directives that are described in RFC 5861.
---
doc/admin-guide/plugins/index.en.rst | 5 +
doc/admin-guide/plugins/stale_response.en.rst | 118 ++
plugins/experimental/CMakeLists.txt | 1 +
plugins/experimental/stale_response/BodyData.h | 121 +++
.../{ => stale_response}/CMakeLists.txt | 42 +-
plugins/experimental/stale_response/CacheUpdate.cc | 369 +++++++
plugins/experimental/stale_response/CacheUpdate.h | 40 +
.../experimental/stale_response/DirectiveParser.cc | 77 ++
.../experimental/stale_response/DirectiveParser.h | 96 ++
plugins/experimental/stale_response/MurmurHash3.cc | 334 ++++++
plugins/experimental/stale_response/MurmurHash3.h | 34 +
.../experimental/stale_response/NumberToString.cc | 74 ++
.../experimental/stale_response/NumberToString.h | 61 ++
.../experimental/stale_response/ServerIntercept.cc | 369 +++++++
.../experimental/stale_response/ServerIntercept.h | 30 +
.../experimental/stale_response/UrlComponents.h | 265 +++++
.../experimental/stale_response/stale_response.cc | 1132 ++++++++++++++++++++
.../experimental/stale_response/stale_response.h | 143 +++
plugins/experimental/stale_response/ts_wrap.h | 42 +
.../{ => stale_response/unit_tests}/CMakeLists.txt | 33 +-
.../stale_response/unit_tests/test_BodyData.cc | 62 ++
.../unit_tests/test_DirectiveParser.cc | 103 ++
.../stale_response/unit_tests/unit_test_main.cc | 25 +
.../stale_response/stale_response.test.py | 173 +++
.../stale_response_no_default.replay.yaml | 175 +++
.../stale_response_with_defaults.replay.yaml | 144 +++
.../stale_response_with_force_sie.replay.yaml | 82 ++
.../stale_response_with_force_swr.replay.yaml | 86 ++
28 files changed, 4188 insertions(+), 48 deletions(-)
diff --git a/doc/admin-guide/plugins/index.en.rst b/doc/admin-guide/plugins/index.en.rst
index ebfb10cc57..66987a533c 100644
--- a/doc/admin-guide/plugins/index.en.rst
+++ b/doc/admin-guide/plugins/index.en.rst
@@ -189,6 +189,7 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi
Slice <slice.en>
SSL Headers <sslheaders.en>
SSL Session Reuse <ssl_session_reuse.en>
+ Stale Response <stale_response.en>
STEK Share <stek_share.en>
System Statistics <system_stats.en>
Wasm <wasm.en>
@@ -271,6 +272,10 @@ directory of the |TS| source tree. Experimental plugins can be compiled by passi
:doc:`SSL Headers <sslheaders.en>`
Populate request headers with SSL session information.
+:doc:`Stale Response <stale_response.en>`
+ Implements handling of the ``stale-while-revalidate`` and ``stale-if-error``
+ ``Cache-Control`` directive extensions.
+
:doc:`STEK Share <stek_share.en>`
Coordinates STEK (Session Ticket Encryption Key) between ATS instances running in a group.
diff --git a/doc/admin-guide/plugins/stale_response.en.rst b/doc/admin-guide/plugins/stale_response.en.rst
new file mode 100644
index 0000000000..3e60753304
--- /dev/null
+++ b/doc/admin-guide/plugins/stale_response.en.rst
@@ -0,0 +1,118 @@
+.. 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:: ../../common.defs
+
+.. _admin-plugins-stale_response:
+
+
+Stale Response Plugin
+*********************
+
+This plugin adds ``stale-while-relavidate`` and ``stale-if-error``
+``Cache-Control`` directive functionality. The ``stale-while-revalidate``
+directive specifies a number of seconds that an item can be stale and used as a
+cached response while |TS| revalidates the cached item. The ``stale-if-error``
+directive specifies how stale a cached response can be and still be used as a
+response if the origin server replies with a 500, 502, 503, or 504 HTTP status.
+The plugin currently only supports the ``stale-if-error`` directive in
+responses. For more details, see `RFC 5861
+<https://www.rfc-editor.org/rfc/rfc5861>`_ for the specification of these
+directives.
+
+Building and Installation
+*************************
+
+The Stale Response plugin is an experimental plugin. To build it, pass
+``-DBUILD_EXPERIMENTAL_PLUGINS=ON`` to the ``cmake`` command when building |TS|.
+By default, that will build the ``stale_response.so`` plugin and install it in
+the ``libexec/trafficserver`` directory.
+
+Configuration
+*************
+
+The Stale Response plugin supports being used as either a global plugin or as a
+remap plugin. To configure |TS| to use the Stale Response plugin, simply add it
+to either the :file:`plugin.config` to configure it as a global plugin and
+restart traffic-server or add it to the desired remap lines in the
+:file:`remap.config` and reload |TS|. The following configurations are available
+for the plugin:
+
+Default values can be specified for responses that do not contain the directives:
+
+ * ``--stale-while-revalidate-default <time>`` set a default
+ ``stale-while-revalidate`` directive with a value of ``time`` for all
+ responses where the directive is not present.
+ * ``--stale-if-error-default <time>`` set a default
+ ``stale-if-err`` directive with a value of ``time`` for all
+ responses where the directive is not present.
+
+Also, minimum values can be set for responses that do or do not contain the
+directives:
+
+ * ``--force-stale-while-revalidate <time>`` set a minimum
+ ``stale-while-revalidate`` time for all responses.
+ * ``--force-stale-if-error <time>`` set a minimum ``stale-if-error`` time
+ for all responses.
+
+The plugin uses memory to temporarily store responses. By default, this is limited
+to 1 GB, but can be configured with: ``--max-memory-usage <size>``
+
+The plugin by default will only perform one asynchronous request at a time for
+a given URL. This can be changed with the ``--force-parallel-async`` option.
+
+Logging
+*******
+
+The plugin can log information about its behavior with respect to the
+``stale-while-revalidate`` and ``stale-if-error`` directives.
+
+ * ``--log-all`` enable logging of all stale responses for both ``stale-while-revalidate``
+ and ``stale-if-error`` directives.
+ * ``--log-stale-while-revalidate``enable logging of all stale responses due to
+ ``stale-while-revalidate`` directives.
+ * ``--log-stale-if-error`` enable logging of all stale responses due to
+ ``stale-if-error`` directives.
+ * ``--log-filename <name>`` set the Stale Response log to ``<name>.log``. The
+ log will be in the :ts:cv:`proxy.config.log.logfile_dir` directory.
+
+
+Statistics
+**********
+
+ * ``stale_response.swr.hit`` The number of times stale data was served for stale-while-relavidate.
+ * ``stale_response.swr.locked_miss`` The number of times stale data could not be served with stale-while-relavidate because of a lock.
+ * ``stale_response.sie.hit`` The number of times stale data was served for stale-if-error.
+ * ``stale_response.memory.over`` The number of times stale data could not be served because of memory constraints.
+
+
+Example
+*******
+
+To configure the plugin as a global plugin with a default
+``stale-while-revalidate`` and a default ``stale-if-error`` of 30 seconds, add
+the following to :file:`plugin.config`::
+
+ stale_response.so --stale-while-revalidate-default 30 --stale-if-error-default 30
+
+To configure the plugin the same way as a remap config plugin, add the following
+to an appropriate remap entry in :file:`remap.config`::
+
+ map http://example.com http://backend.example.com @plugin=stale_response.so \
+ @pparam=--stale-while-revalidate-default @pparam=30 \
+ @pparam=--stale-if-error-default @pparam=30
diff --git a/plugins/experimental/CMakeLists.txt b/plugins/experimental/CMakeLists.txt
index a01426dc15..c398457002 100644
--- a/plugins/experimental/CMakeLists.txt
+++ b/plugins/experimental/CMakeLists.txt
@@ -35,6 +35,7 @@ add_subdirectory(mp4)
add_subdirectory(rate_limit)
add_subdirectory(redo_cache_lookup)
add_subdirectory(sslheaders)
+add_subdirectory(stale_response)
add_subdirectory(stream_editor)
add_subdirectory(system_stats)
add_subdirectory(tls_bridge)
diff --git a/plugins/experimental/stale_response/BodyData.h b/plugins/experimental/stale_response/BodyData.h
new file mode 100644
index 0000000000..fd62ae8118
--- /dev/null
+++ b/plugins/experimental/stale_response/BodyData.h
@@ -0,0 +1,121 @@
+/** @file
+
+ Manage body data for the plugin.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#pragma once
+
+#include <cassert>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <vector>
+
+#include "ts_wrap.h"
+
+// this determines size of pointer array alloc/reallocs
+constexpr unsigned int c_chunk_add_count = 64;
+
+#define PLUGIN_TAG_BODY "stale_response_body"
+
+EXT_DBG_CTL(PLUGIN_TAG_BODY)
+
+/*-----------------------------------------------------------------------------------------------*/
+// This struct needs to newed not malloced
+struct BodyData {
+ BodyData();
+ ~BodyData() = default;
+
+ bool addChunk(const char *start, int64_t size);
+ size_t
+ getChunkCount() const
+ {
+ return chunk_list.size();
+ }
+ bool getChunk(uint32_t chunk_index, const char **start, int64_t *size) const;
+ bool removeChunk(uint32_t chunk);
+ int64_t
+ getSize() const
+ {
+ return total_size;
+ }
+
+ bool intercept_active = false;
+ bool key_hash_active = false;
+ uint32_t key_hash = 0;
+
+private:
+ struct Chunk {
+ Chunk(int64_t size, std::vector<char> &&start) : size(size), start(std::move(start)) {}
+
+ int64_t size = 0;
+ std::vector<char> start;
+ };
+ int64_t total_size = 0;
+ std::vector<Chunk> chunk_list;
+};
+
+/*-----------------------------------------------------------------------------------------------*/
+inline BodyData::BodyData()
+{
+ chunk_list.reserve(c_chunk_add_count);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+inline bool
+BodyData::addChunk(const char *start, int64_t size)
+{
+ assert(start != nullptr && size >= 0);
+ chunk_list.emplace_back(size, std::vector<char>(start, start + size));
+ total_size += size;
+ return true;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+inline bool
+BodyData::getChunk(uint32_t chunk_index, const char **start, int64_t *size) const
+{
+ assert(start != nullptr && size != nullptr);
+ bool bGood = false;
+ if (chunk_index < chunk_list.size()) {
+ bGood = true;
+ *size = chunk_list[chunk_index].size;
+ *start = chunk_list[chunk_index].start.data();
+ } else {
+ *size = 0;
+ *start = nullptr;
+ }
+ return bGood;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+inline bool
+BodyData::removeChunk(uint32_t chunk)
+{
+ bool bGood = false;
+ if (chunk < chunk_list.size() && !chunk_list[chunk].start.empty()) {
+ bGood = true;
+ chunk_list[chunk].start.clear();
+ }
+ return bGood;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/CMakeLists.txt b/plugins/experimental/stale_response/CMakeLists.txt
similarity index 54%
copy from plugins/experimental/CMakeLists.txt
copy to plugins/experimental/stale_response/CMakeLists.txt
index a01426dc15..f3b4425805 100644
--- a/plugins/experimental/CMakeLists.txt
+++ b/plugins/experimental/stale_response/CMakeLists.txt
@@ -15,27 +15,21 @@
#
#######################
-add_subdirectory(access_control)
-add_subdirectory(block_errors)
-add_subdirectory(cache_fill)
-add_subdirectory(cert_reporting_tool)
-add_subdirectory(cookie_remap)
-add_subdirectory(custom_redirect)
-add_subdirectory(fq_pacing)
-add_subdirectory(geoip_acl)
-add_subdirectory(header_freq)
-add_subdirectory(hook-trace)
-add_subdirectory(http_stats)
-add_subdirectory(icap)
-add_subdirectory(inliner)
-add_subdirectory(memcache)
-add_subdirectory(memory_profile)
-add_subdirectory(money_trace)
-add_subdirectory(mp4)
-add_subdirectory(rate_limit)
-add_subdirectory(redo_cache_lookup)
-add_subdirectory(sslheaders)
-add_subdirectory(stream_editor)
-add_subdirectory(system_stats)
-add_subdirectory(tls_bridge)
-add_subdirectory(url_sig)
+project(stale_response)
+
+add_atsplugin(
+ stale_response
+ DirectiveParser.cc
+ CacheUpdate.cc
+ MurmurHash3.cc
+ NumberToString.cc
+ ServerIntercept.cc
+ stale_response.cc
+)
+
+target_link_libraries(stale_response PRIVATE libswoc::libswoc)
+target_include_directories(stale_response PRIVATE "${libswoc_INCLUDE_DIRS}")
+
+if(BUILD_TESTING)
+ add_subdirectory(unit_tests)
+endif()
diff --git a/plugins/experimental/stale_response/CacheUpdate.cc b/plugins/experimental/stale_response/CacheUpdate.cc
new file mode 100644
index 0000000000..3e1f6363a9
--- /dev/null
+++ b/plugins/experimental/stale_response/CacheUpdate.cc
@@ -0,0 +1,369 @@
+/** @file
+
+ Cache Control Extensions Caching Utilities
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#include <cstring>
+#include <strings.h>
+#include <string>
+#include <netinet/in.h>
+#include <unistd.h>
+
+#include "stale_response.h"
+#include "BodyData.h"
+#include "CacheUpdate.h"
+#include "UrlComponents.h"
+#include "NumberToString.h"
+
+using namespace std;
+
+// unique url parameter that shoud never leave ATS
+static const char ASYNC_PARM[] = "swrasync=asyncmrl";
+static const int ASYNC_PARM_LEN = sizeof(ASYNC_PARM) - 1;
+// unique header that should never leave ATS
+const char SERVER_INTERCEPT_HEADER[] = "X-CCExtensions-Intercept";
+const int SERVER_INTERCEPT_HEADER_LEN = sizeof(SERVER_INTERCEPT_HEADER) - 1;
+
+/*-----------------------------------------------------------------------------------------------*/
+static char *
+convert_mime_hdr_to_string(TSMBuffer bufp, TSMLoc hdr_loc)
+{
+ TSIOBuffer output_buffer;
+ TSIOBufferReader reader;
+ int64_t total_avail;
+
+ TSIOBufferBlock block;
+ const char *block_start;
+ int64_t block_avail;
+
+ char *output_string;
+ int output_len;
+
+ output_buffer = TSIOBufferCreate();
+
+ if (!output_buffer) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] couldn't allocate IOBuffer", __FUNCTION__);
+ }
+
+ reader = TSIOBufferReaderAlloc(output_buffer);
+
+ /* This will print just MIMEFields and not
+ the http request line */
+ TSMimeHdrPrint(bufp, hdr_loc, output_buffer);
+
+ /* Find out how the big the complete header is by
+ seeing the total bytes in the buffer. We need to
+ look at the buffer rather than the first block to
+ see the size of the entire header */
+ total_avail = TSIOBufferReaderAvail(reader);
+
+ /* Allocate the string with an extra byte for the string
+ terminator */
+ output_string = static_cast<char *>(TSmalloc(total_avail + 1));
+ output_len = 0;
+
+ /* We need to loop over all the buffer blocks to make
+ sure we get the complete header since the header can
+ be in multiple blocks */
+ block = TSIOBufferReaderStart(reader);
+ while (block) {
+ block_start = TSIOBufferBlockReadStart(block, reader, &block_avail);
+
+ /* We'll get a block pointer back even if there is no data
+ left to read so check for this condition and break out of
+ the loop. A block with no data to read means we've exhausted
+ buffer of data since if there was more data on a later
+ block in the chain, this block would have been skipped over */
+ if (block_avail == 0) {
+ break;
+ }
+
+ memcpy(output_string + output_len, block_start, block_avail);
+ output_len += block_avail;
+
+ /* Consume the data so that we get to the next block */
+ TSIOBufferReaderConsume(reader, block_avail);
+
+ /* Get the next block now that we've consumed the
+ data off the last block */
+ block = TSIOBufferReaderStart(reader);
+ }
+
+ /* Terminate the string */
+ output_string[output_len] = '\0';
+ output_len++;
+
+ /* Free up the TSIOBuffer that we used to print out the header */
+ TSIOBufferReaderFree(reader);
+ TSIOBufferDestroy(output_buffer);
+
+ return output_string;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+has_trailing_parameter(TSMBuffer hdr_url_buf, TSMLoc hdr_url_loc)
+{
+ bool bFound = false;
+
+ TSMLoc url_loc;
+ TSHttpHdrUrlGet(hdr_url_buf, hdr_url_loc, &url_loc);
+ // create the new url
+ UrlComponents reqUrl;
+ reqUrl.populate(hdr_url_buf, url_loc);
+ string newQuery = reqUrl.getQuery();
+ string::size_type idx;
+ idx = newQuery.find(ASYNC_PARM);
+ if ((idx != string::npos) && (idx + ASYNC_PARM_LEN) == newQuery.length()) {
+ bFound = true;
+ }
+ TSHandleMLocRelease(hdr_url_buf, hdr_url_loc, url_loc);
+ TSDebug(PLUGIN_TAG, "[%s] %d", __FUNCTION__, bFound);
+ return bFound;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+add_trailing_parameter(TSMBuffer hdr_url_buf, TSMLoc hdr_url_loc)
+{
+ TSMLoc url_loc;
+ TSHttpHdrUrlGet(hdr_url_buf, hdr_url_loc, &url_loc);
+ // create the new url
+ UrlComponents reqUrl;
+ reqUrl.populate(hdr_url_buf, url_loc);
+ string newQuery = reqUrl.getQuery();
+
+ if (newQuery.length()) {
+ newQuery += "&";
+ newQuery += ASYNC_PARM;
+ } else {
+ newQuery += ASYNC_PARM;
+ }
+ reqUrl.setQuery(newQuery);
+ string newUrl;
+ reqUrl.construct(newUrl);
+ // parse ans set the new url
+ const char *start = newUrl.c_str();
+ const char *end = newUrl.size() + start;
+ TSUrlParse(hdr_url_buf, url_loc, &start, end);
+
+ TSDebug(PLUGIN_TAG, "[%s] [%s]", __FUNCTION__, newQuery.c_str());
+ TSHandleMLocRelease(hdr_url_buf, hdr_url_loc, url_loc);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+strip_trailing_parameter(TSMBuffer hdr_url_buf, TSMLoc hdr_url_loc)
+{
+ bool stripped = false;
+ TSMLoc url_loc;
+ TSHttpHdrUrlGet(hdr_url_buf, hdr_url_loc, &url_loc);
+ // create the new url
+ UrlComponents reqUrl;
+ reqUrl.populate(hdr_url_buf, url_loc);
+ string newQuery = reqUrl.getQuery();
+ string::size_type idx;
+ idx = newQuery.find(ASYNC_PARM);
+ if ((idx != string::npos) && (idx + ASYNC_PARM_LEN) == newQuery.length()) {
+ if (idx > 0) {
+ idx -= 1;
+ }
+ newQuery.erase(idx);
+ stripped = true;
+ }
+ if (stripped) {
+ TSUrlHttpQuerySet(hdr_url_buf, url_loc, newQuery.c_str(), newQuery.size());
+ }
+ TSHandleMLocRelease(hdr_url_buf, hdr_url_loc, url_loc);
+ TSDebug(PLUGIN_TAG, "[%s] stripped=%d [%s]", __FUNCTION__, stripped, newQuery.c_str());
+ return stripped;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+fix_connection_close(StateInfo *state)
+{
+ TSMLoc connection_hdr_loc, connection_hdr_dup_loc;
+ connection_hdr_loc = TSMimeHdrFieldFind(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, TS_MIME_FIELD_CONNECTION,
+ TS_MIME_LEN_CONNECTION);
+
+ while (connection_hdr_loc != TS_NULL_MLOC) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Found old Connection hdr", __FUNCTION__, state->req_info->key_hash);
+ connection_hdr_dup_loc =
+ TSMimeHdrFieldNextDup(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+ TSMimeHdrFieldRemove(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+ TSMimeHdrFieldDestroy(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+ TSHandleMLocRelease(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+ connection_hdr_loc = connection_hdr_dup_loc;
+ }
+
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Creating Connection:close hdr", __FUNCTION__, state->req_info->key_hash);
+ TSMimeHdrFieldCreateNamed(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, TS_MIME_FIELD_CONNECTION,
+ TS_MIME_LEN_CONNECTION, &connection_hdr_loc);
+ TSMimeHdrFieldValueStringInsert(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc, -1,
+ TS_HTTP_VALUE_CLOSE, TS_HTTP_LEN_CLOSE);
+ TSMimeHdrFieldAppend(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+ TSHandleMLocRelease(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, connection_hdr_loc);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+get_pristine_url(StateInfo *state)
+{
+ TSHttpTxn txnp = state->txnp;
+ TSMBuffer hdr_url_buf;
+ TSMLoc url_loc;
+ // get the pristine url only works after remap state
+ if (TSHttpTxnPristineUrlGet(txnp, &hdr_url_buf, &url_loc) == TS_SUCCESS) {
+ int url_length;
+ char *url = TSUrlStringGet(hdr_url_buf, url_loc, &url_length);
+ state->pristine_url = TSstrndup(url, url_length);
+ TSfree(url);
+ // release the buffer and loc
+ TSHandleMLocRelease(hdr_url_buf, TS_NULL_MLOC, url_loc);
+ TSDebug(PLUGIN_TAG, "[%s] {%u} pristine=[%s]", __FUNCTION__, state->req_info->key_hash, state->pristine_url);
+ } else {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} TSHttpTxnPristineUrlGet failed!", __FUNCTION__, state->req_info->key_hash);
+ }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+intercept_get_key(TSMBuffer bufp, TSMLoc hdr_loc, const char *name, int name_len, string &key)
+{
+ bool retval = false;
+ TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, name, name_len);
+ if (field_loc) {
+ int value_len;
+ const char *value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, 0, &value_len);
+ key.append(value, value_len);
+ retval = true;
+ }
+ TSHandleMLocRelease(bufp, hdr_loc, field_loc);
+ // TSDebug(PLUGIN_TAG, "[%s] key=[%s] found=%d",__FUNCTION__,name,key.c_str(),retval);
+ return retval;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+BodyData *
+intercept_check_request(StateInfo *state)
+{
+ uint32_t newKey = 0;
+ BodyData *pBodyFound = nullptr;
+ TSHttpTxn txnp = state->txnp;
+ uint32_t oldKey = state->req_info->key_hash;
+
+ if (!TSHttpTxnIsInternal(txnp)) {
+ TSDebug(PLUGIN_TAG, "[%s] Skipping external request", __FUNCTION__);
+ return pBodyFound;
+ }
+
+ TSMBuffer bufp;
+ TSMLoc hdr_loc;
+ if (TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc) != TS_SUCCESS) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] TSHttpTxnClientReqGet failed!", __FUNCTION__);
+ return pBodyFound;
+ }
+
+ bool valid_request = false;
+ int method_len;
+ const char *method = TSHttpHdrMethodGet(bufp, hdr_loc, &method_len);
+ if (!method) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] TSHttpHdrMethodGet failed!", __FUNCTION__);
+ } else {
+ if ((method_len == TS_HTTP_LEN_GET) && (strncasecmp(method, TS_HTTP_METHOD_GET, TS_HTTP_LEN_GET) == 0)) {
+ valid_request = true;
+ }
+ }
+
+ if (valid_request) {
+ string headerKey;
+ if (intercept_get_key(bufp, hdr_loc, SERVER_INTERCEPT_HEADER, SERVER_INTERCEPT_HEADER_LEN, headerKey)) {
+ base16_decode(reinterpret_cast<unsigned char *>(&newKey), headerKey.c_str(), headerKey.length());
+ pBodyFound = async_check_active(newKey, state->plugin_config);
+ if (pBodyFound) {
+ // header key can be differnt because of ATS port wierdness, so make state the same
+ state->req_info->key_hash = newKey;
+ } else {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] key miss %u this should not happen!", __FUNCTION__, newKey);
+ }
+ }
+ }
+
+ TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
+ TSDebug(PLUGIN_TAG, "[%s] {%u} oldKey=%u pBodyFound=%p", __FUNCTION__, newKey, oldKey, pBodyFound);
+ return pBodyFound;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+intercept_fetch_the_url(StateInfo *state)
+{
+ bool bGood = false;
+
+ if (state->pristine_url == nullptr) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} pristine url nullptr should not happen", __FUNCTION__, state->req_info->key_hash);
+ if (!async_remove_active(state->req_info->key_hash, state->plugin_config)) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] didnt delete async active", __FUNCTION__);
+ }
+ return bGood;
+ }
+
+ // encode the key_hash should alwasy be 8 chars
+ char tmpStr[10];
+ base16_encode(tmpStr, reinterpret_cast<unsigned char const *>(&(state->req_info->key_hash)), 4);
+ // create the request as a string
+ string get_request("");
+ get_request.append(TS_HTTP_METHOD_GET);
+ get_request.append(" ");
+ get_request.append(state->pristine_url);
+ get_request.append(" HTTP/1.1\r\n");
+ get_request.append(SERVER_INTERCEPT_HEADER);
+ get_request.append(": ");
+ get_request.append(tmpStr, 8);
+ get_request.append("\r\n");
+
+ char *allReqHeaders = convert_mime_hdr_to_string(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc);
+ get_request.append(allReqHeaders);
+ TSfree(allReqHeaders);
+ get_request.append("\r\n");
+
+ // TSDebug(PLUGIN_TAG, "[%s] req len %d ",__FUNCTION__,(int)get_request.length());
+ // TSDebug(PLUGIN_TAG, "[%s] reg \r\n|%s|\r\n",__FUNCTION__,get_request.c_str());
+
+ BodyData *pBody = async_check_active(state->req_info->key_hash, state->plugin_config);
+ if (pBody) {
+ // TSDebug(PLUGIN_TAG_BAD, "[%s] sleep 4",__FUNCTION__); sleep(4);
+ // This should be safe outside of locks
+ pBody->intercept_active = true;
+ TSFetchEvent event_ids = {0, 0, 0};
+ TSFetchUrl(get_request.data(), get_request.size(), state->req_info->client_addr, state->transaction_contp, NO_CALLBACK,
+ event_ids);
+ bGood = true;
+ TSDebug(PLUGIN_TAG, "[%s] {%u} length=%d", __FUNCTION__, state->req_info->key_hash, (int)pBody->getSize());
+ } else {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} cant find body", __FUNCTION__, state->req_info->key_hash);
+ }
+
+ return bGood;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/CacheUpdate.h b/plugins/experimental/stale_response/CacheUpdate.h
new file mode 100644
index 0000000000..0eb4aa6032
--- /dev/null
+++ b/plugins/experimental/stale_response/CacheUpdate.h
@@ -0,0 +1,40 @@
+/** @file
+
+ Cache Control Extensions Cache Utilities
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#pragma once
+
+#include "ts/ts.h"
+
+struct StateInfo;
+struct BodyData;
+
+bool has_trailing_parameter(TSMBuffer buf, TSMLoc http_hdr_loc);
+void add_trailing_parameter(TSMBuffer buf, TSMLoc http_hdr_loc);
+bool strip_trailing_parameter(TSMBuffer buf, TSMLoc http_hdr_loc);
+
+void get_pristine_url(StateInfo *state);
+void fix_connection_close(StateInfo *state);
+BodyData *intercept_check_request(StateInfo *state);
+bool intercept_fetch_the_url(StateInfo *state);
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/DirectiveParser.cc b/plugins/experimental/stale_response/DirectiveParser.cc
new file mode 100644
index 0000000000..25ca7c4a79
--- /dev/null
+++ b/plugins/experimental/stale_response/DirectiveParser.cc
@@ -0,0 +1,77 @@
+/** @file
+
+ Parse Cache-Control directives.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#include "DirectiveParser.h"
+
+#include "swoc/TextView.h"
+
+namespace
+{
+
+swoc::TextView MAX_AGE{"max-age"};
+swoc::TextView STALE_WHILE_REVALIDATE{"stale-while-revalidate"};
+swoc::TextView STALE_IF_ERROR{"stale-if-error"};
+
+} // anonymous namespace
+
+DirectiveParser::DirectiveParser(swoc::TextView CacheControlValue)
+{
+ while (CacheControlValue) {
+ swoc::TextView directive{CacheControlValue.take_prefix_if(&isspace)};
+
+ // All the directives we care about have a '=' in them.
+ swoc::TextView name{directive.take_prefix_at('=').trim_if(&isspace)};
+ if (!name) {
+ continue;
+ }
+
+ swoc::TextView value{directive.trim_if(&isspace)};
+ if (!value) {
+ continue;
+ }
+ // Directives are separated by commas, so trim if there is one.
+ value.trim(',');
+
+ if (name == MAX_AGE) {
+ this->_max_age = swoc::svtoi(value);
+ } else if (name == STALE_WHILE_REVALIDATE) {
+ this->_stale_while_revalidate_value = swoc::svtoi(value);
+ } else if (name == STALE_IF_ERROR) {
+ this->_stale_if_error_value = swoc::svtoi(value);
+ }
+ }
+}
+
+void
+DirectiveParser::merge(DirectiveParser const &other)
+{
+ if (other._max_age != -1) {
+ this->_max_age = other._max_age;
+ }
+ if (other._stale_while_revalidate_value != -1) {
+ this->_stale_while_revalidate_value = other._stale_while_revalidate_value;
+ }
+ if (other._stale_if_error_value != -1) {
+ this->_stale_if_error_value = other._stale_if_error_value;
+ }
+}
diff --git a/plugins/experimental/stale_response/DirectiveParser.h b/plugins/experimental/stale_response/DirectiveParser.h
new file mode 100644
index 0000000000..ab7f356cea
--- /dev/null
+++ b/plugins/experimental/stale_response/DirectiveParser.h
@@ -0,0 +1,96 @@
+/** @file
+
+ Parse Cache-Control directives.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#pragma once
+
+#include <ctime>
+#include "swoc/TextView.h"
+
+/** Parses the directives of a Cache-Control HTTP field value. */
+class DirectiveParser
+{
+public:
+ DirectiveParser() = default;
+
+ /** Construct a parser from a Cache-Control value.
+ * @param[in] CacheControlValue The value of a Cache-Control header field.
+ */
+ DirectiveParser(swoc::TextView CacheControlValue);
+
+ /** Merge the directives from another parser into this one.
+ *
+ * If a directive is present in both parsers, the value from the other parser is
+ * used.
+ *
+ * @param[in] other The parser to merge into this one.
+ */
+ void merge(DirectiveParser const &other);
+
+ /** Get the value of the max-age Cache-Control directive.
+ *
+ * @return The value of the max-age Cache-Control directive, or -1 if the
+ * directive was not present.
+ */
+ time_t
+ get_max_age() const
+ {
+ return _max_age;
+ };
+
+ /** Get the value of the stale-while-revalidate Cache-Control directive.
+ *
+ * @return The value of the stale-while-revalidate Cache-Control directive,
+ * or -1 if the directive was not present.
+ */
+ time_t
+ get_stale_while_revalidate() const
+ {
+ return _stale_while_revalidate_value;
+ }
+
+ /** Get the value of the stale-if-error Cache-Control directive.
+ *
+ * @return The value of the stale-if-error Cache-Control directive, or -1 if
+ * the directive was not present.
+ */
+ time_t
+ get_stale_if_error() const
+ {
+ return _stale_if_error_value;
+ };
+
+private:
+ /** The value of the max-age Cache-Control directive.
+ *
+ * A negative value indicates that the directive was not present. RFC 9111
+ * section 1.2.2 specifies that delta-seconds, used by max-age, must be
+ * non-negative, so this should be safe.
+ */
+ time_t _max_age = -1;
+
+ /// The value of the stale-while-revalidate Cache-Control directive.
+ time_t _stale_while_revalidate_value = -1;
+
+ /// The value of the stale-if-error Cache-Control directive.
+ time_t _stale_if_error_value = -1;
+};
diff --git a/plugins/experimental/stale_response/MurmurHash3.cc b/plugins/experimental/stale_response/MurmurHash3.cc
new file mode 100644
index 0000000000..600faeb502
--- /dev/null
+++ b/plugins/experimental/stale_response/MurmurHash3.cc
@@ -0,0 +1,334 @@
+/** @file
+
+ Copy of MurmurHash3 implementation.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+//-----------------------------------------------------------------------------
+// MurmurHash3 was written by Austin Appleby, and is placed in the public
+// domain. The author hereby disclaims copyright to this source code.
+
+// Note - The x86 and x64 versions do _not_ produce the same results, as the
+// algorithms are optimized for their respective platforms. You can still
+// compile and run any of them on any platform, but your performance with the
+// non-native version will be less than optimal.
+
+#include "MurmurHash3.h"
+
+//-----------------------------------------------------------------------------
+#define FORCE_INLINE inline __attribute__((always_inline))
+
+inline uint32_t
+rotl32(uint32_t x, int8_t r)
+{
+ return (x << r) | (x >> (32 - r));
+}
+
+inline uint64_t
+rotl64(uint64_t x, int8_t r)
+{
+ return (x << r) | (x >> (64 - r));
+}
+
+#define ROTL32(x, y) rotl32(x, y)
+#define ROTL64(x, y) rotl64(x, y)
+
+#define BIG_CONSTANT(x) (x##LLU)
+
+//-----------------------------------------------------------------------------
+// Block read - if your platform needs to do endian-swapping or can only
+// handle aligned reads, do the conversion here
+
+FORCE_INLINE uint32_t
+getblock32(const uint32_t *p, int i)
+{
+ return p[i];
+}
+
+FORCE_INLINE uint64_t
+getblock64(const uint64_t *p, int i)
+{
+ return p[i];
+}
+
+//-----------------------------------------------------------------------------
+// Finalization mix - force all bits of a hash block to avalanche
+
+FORCE_INLINE uint32_t
+fmix32(uint32_t h)
+{
+ h ^= h >> 16;
+ h *= 0x85ebca6b;
+ h ^= h >> 13;
+ h *= 0xc2b2ae35;
+ h ^= h >> 16;
+
+ return h;
+}
+
+//----------
+
+FORCE_INLINE uint64_t
+fmix64(uint64_t k)
+{
+ k ^= k >> 33;
+ k *= BIG_CONSTANT(0xff51afd7ed558ccd);
+ k ^= k >> 33;
+ k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53);
+ k ^= k >> 33;
+
+ return k;
+}
+
+//-----------------------------------------------------------------------------
+
+void
+MurmurHash3_x86_32(const void *key, int len, uint32_t seed, void *out)
+{
+ const uint8_t *data = (const uint8_t *)key;
+ const int nblocks = len / 4;
+
+ uint32_t h1 = seed;
+
+ const uint32_t c1 = 0xcc9e2d51;
+ const uint32_t c2 = 0x1b873593;
+
+ //----------
+ // body
+
+ const uint32_t *blocks = (const uint32_t *)(data + nblocks * 4);
+
+ for (int i = -nblocks; i; i++) {
+ uint32_t k1 = getblock32(blocks, i);
+
+ k1 *= c1;
+ k1 = ROTL32(k1, 15);
+ k1 *= c2;
+
+ h1 ^= k1;
+ h1 = ROTL32(h1, 13);
+ h1 = h1 * 5 + 0xe6546b64;
+ }
+
+ //----------
+ // tail
+
+ const uint8_t *tail = (const uint8_t *)(data + nblocks * 4);
+
+ uint32_t k1 = 0;
+
+ switch (len & 3) {
+ case 3:
+ k1 ^= tail[2] << 16;
+ // fallthrough
+ case 2:
+ k1 ^= tail[1] << 8;
+ // fallthrough
+ case 1:
+ k1 ^= tail[0];
+ k1 *= c1;
+ k1 = ROTL32(k1, 15);
+ k1 *= c2;
+ h1 ^= k1;
+ };
+
+ //----------
+ // finalization
+
+ h1 ^= len;
+
+ h1 = fmix32(h1);
+
+ *(uint32_t *)out = h1;
+}
+
+//-----------------------------------------------------------------------------
+
+void
+MurmurHash3_x86_128(const void *key, const int len, uint32_t seed, void *out)
+{
+ const uint8_t *data = (const uint8_t *)key;
+ const int nblocks = len / 16;
+
+ uint32_t h1 = seed;
+ uint32_t h2 = seed;
+ uint32_t h3 = seed;
+ uint32_t h4 = seed;
+
+ const uint32_t c1 = 0x239b961b;
+ const uint32_t c2 = 0xab0e9789;
+ const uint32_t c3 = 0x38b34ae5;
+ const uint32_t c4 = 0xa1e38b93;
+
+ //----------
+ // body
+
+ const uint32_t *blocks = (const uint32_t *)(data + nblocks * 16);
+
+ for (int i = -nblocks; i; i++) {
+ uint32_t k1 = getblock32(blocks, i * 4 + 0);
+ uint32_t k2 = getblock32(blocks, i * 4 + 1);
+ uint32_t k3 = getblock32(blocks, i * 4 + 2);
+ uint32_t k4 = getblock32(blocks, i * 4 + 3);
+
+ k1 *= c1;
+ k1 = ROTL32(k1, 15);
+ k1 *= c2;
+ h1 ^= k1;
+
+ h1 = ROTL32(h1, 19);
+ h1 += h2;
+ h1 = h1 * 5 + 0x561ccd1b;
+
+ k2 *= c2;
+ k2 = ROTL32(k2, 16);
+ k2 *= c3;
+ h2 ^= k2;
+
+ h2 = ROTL32(h2, 17);
+ h2 += h3;
+ h2 = h2 * 5 + 0x0bcaa747;
+
+ k3 *= c3;
+ k3 = ROTL32(k3, 17);
+ k3 *= c4;
+ h3 ^= k3;
+
+ h3 = ROTL32(h3, 15);
+ h3 += h4;
+ h3 = h3 * 5 + 0x96cd1c35;
+
+ k4 *= c4;
+ k4 = ROTL32(k4, 18);
+ k4 *= c1;
+ h4 ^= k4;
+
+ h4 = ROTL32(h4, 13);
+ h4 += h1;
+ h4 = h4 * 5 + 0x32ac3b17;
+ }
+
+ //----------
+ // tail
+
+ const uint8_t *tail = (const uint8_t *)(data + nblocks * 16);
+
+ uint32_t k1 = 0;
+ uint32_t k2 = 0;
+ uint32_t k3 = 0;
+ uint32_t k4 = 0;
+
+ switch (len & 15) {
+ case 15:
+ k4 ^= tail[14] << 16;
+ // fallthrough
+ case 14:
+ k4 ^= tail[13] << 8;
+ // fallthrough
+ case 13:
+ k4 ^= tail[12] << 0;
+ k4 *= c4;
+ k4 = ROTL32(k4, 18);
+ k4 *= c1;
+ h4 ^= k4;
+ // fallthrough
+ case 12:
+ k3 ^= tail[11] << 24;
+ // fallthrough
+ case 11:
+ k3 ^= tail[10] << 16;
+ // fallthrough
+ case 10:
+ k3 ^= tail[9] << 8;
+ // fallthrough
+ case 9:
+ k3 ^= tail[8] << 0;
+ k3 *= c3;
+ k3 = ROTL32(k3, 17);
+ k3 *= c4;
+ h3 ^= k3;
+ // fallthrough
+ case 8:
+ k2 ^= tail[7] << 24;
+ // fallthrough
+ case 7:
+ k2 ^= tail[6] << 16;
+ // fallthrough
+ case 6:
+ k2 ^= tail[5] << 8;
+ // fallthrough
+ case 5:
+ k2 ^= tail[4] << 0;
+ k2 *= c2;
+ k2 = ROTL32(k2, 16);
+ k2 *= c3;
+ h2 ^= k2;
+ // fallthrough
+ case 4:
+ k1 ^= tail[3] << 24;
+ // fallthrough
+ case 3:
+ k1 ^= tail[2] << 16;
+ // fallthrough
+ case 2:
+ k1 ^= tail[1] << 8;
+ // fallthrough
+ case 1:
+ k1 ^= tail[0] << 0;
+ k1 *= c1;
+ k1 = ROTL32(k1, 15);
+ k1 *= c2;
+ h1 ^= k1;
+ };
+
+ //----------
+ // finalization
+
+ h1 ^= len;
+ h2 ^= len;
+ h3 ^= len;
+ h4 ^= len;
+
+ h1 += h2;
+ h1 += h3;
+ h1 += h4;
+ h2 += h1;
+ h3 += h1;
+ h4 += h1;
+
+ h1 = fmix32(h1);
+ h2 = fmix32(h2);
+ h3 = fmix32(h3);
+ h4 = fmix32(h4);
+
+ h1 += h2;
+ h1 += h3;
+ h1 += h4;
+ h2 += h1;
+ h3 += h1;
+ h4 += h1;
+
+ ((uint32_t *)out)[0] = h1;
+ ((uint32_t *)out)[1] = h2;
+ ((uint32_t *)out)[2] = h3;
+ ((uint32_t *)out)[3] = h4;
+}
+
+//-----------------------------------------------------------------------------
diff --git a/plugins/experimental/stale_response/MurmurHash3.h b/plugins/experimental/stale_response/MurmurHash3.h
new file mode 100644
index 0000000000..7026d8bead
--- /dev/null
+++ b/plugins/experimental/stale_response/MurmurHash3.h
@@ -0,0 +1,34 @@
+/** @file
+
+ Copy of MurmurHash3 declarations.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+//-----------------------------------------------------------------------------
+// MurmurHash3 was written by Austin Appleby, and is placed in the public
+// domain. The author hereby disclaims copyright to this source code.
+
+#pragma once
+
+#include <cstdint>
+
+void MurmurHash3_x86_32(const void *key, int len, uint32_t seed, void *out);
+
+void MurmurHash3_x86_128(const void *key, int len, uint32_t seed, void *out);
diff --git a/plugins/experimental/stale_response/NumberToString.cc b/plugins/experimental/stale_response/NumberToString.cc
new file mode 100644
index 0000000000..95ac1a7a5a
--- /dev/null
+++ b/plugins/experimental/stale_response/NumberToString.cc
@@ -0,0 +1,74 @@
+/** @file
+
+ A brief file description
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#include "NumberToString.h"
+#include <cstring>
+
+/*-----------------------------------------------------------------------------------------------*/
+int
+base16_digit(char ch)
+{
+ if ('0' <= ch && ch <= '9') {
+ return ch - '0';
+ } else if ('A' <= ch && ch <= 'F') {
+ return ch - 'A' + 10;
+ } else if ('a' <= ch && ch <= 'f') {
+ return ch - 'a' + 10;
+ } else {
+ return -1;
+ }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+char *
+base16_encode(char *dst, unsigned char const *src, size_t len)
+{
+ char const hex[] = "0123456789abcdef";
+
+ size_t i;
+ for (i = 0; i != len; ++i) {
+ dst[i * 2 + 0] = hex[src[i] / 16];
+ dst[i * 2 + 1] = hex[src[i] % 16];
+ }
+ dst[i * 2 + 0] = '\0';
+
+ return dst;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+unsigned char *
+base16_decode(unsigned char *dst, char const *src, size_t len)
+{
+ unsigned char *p = dst;
+ for (size_t i = 0; i + 1 < len; i += 2) {
+ int msn = base16_digit(src[i + 0]);
+ int lsn = base16_digit(src[i + 1]);
+ if (msn < 0 || lsn < 0) {
+ break;
+ }
+ *p++ = static_cast<unsigned char>((msn << 4) | lsn);
+ }
+ return dst;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/NumberToString.h b/plugins/experimental/stale_response/NumberToString.h
new file mode 100644
index 0000000000..7fd047221b
--- /dev/null
+++ b/plugins/experimental/stale_response/NumberToString.h
@@ -0,0 +1,61 @@
+/** @file
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#pragma once
+
+#include <cstddef>
+
+/**
+ * Convert the base-16 digit <TT>ch</TT> into an integer. If the
+ * digit is invalid then <TT>-1</TT> is returned.
+ *
+ * @param ch the base-16 digit to convert.
+ * @return an integer in the range [0, 15] or -1 if the
+ * character is NOT a base-16 digit.
+ **/
+int base16_digit(char ch);
+
+/**
+ * Convert <TT>len</TT> bytes of binary data in <TT>src</TT> into
+ * <TT>2 * len + 1</TT> hexadecimal digits stored in <TT>dst</TT>.
+ *
+ * @param dst a buffer of length <TT>2 * len + 1</TT>.
+ * @param src the source binary data to convert.
+ * @param len the number of bytes of binary data in src
+ * to convert into hexadecimal.
+ * @return the dst pointer.
+ **/
+char *base16_encode(char *dst, unsigned char const *src, size_t len);
+
+/**
+ * Convert <TT>len</TT> hexadecimal digits in <TT>src</TT> into
+ * <TT>len / 2</TT> bytes of binary data stored in <TT>dst</TT>.
+ *
+ * @param dst a buffer of length <TT>len / 2</TT>.
+ * @param src the source hexadecimal digits to convert.
+ * @param len the number of bytes hexadecimal digits in
+ * src to convert into binary data. If this
+ * value is odd then the last digit is ignored.
+ * @return the dst pointer.
+ **/
+unsigned char *base16_decode(unsigned char *dst, char const *src, size_t len);
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/ServerIntercept.cc b/plugins/experimental/stale_response/ServerIntercept.cc
new file mode 100644
index 0000000000..50837819c5
--- /dev/null
+++ b/plugins/experimental/stale_response/ServerIntercept.cc
@@ -0,0 +1,369 @@
+/** @file
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#include "ServerIntercept.h"
+#include "BodyData.h"
+#include "stale_response.h"
+
+#include "ts/ts.h"
+#include "ts_wrap.h"
+
+#include <climits>
+#include <cstdio>
+#include <cstring>
+#include <search.h>
+#include <strings.h>
+
+#include <arpa/inet.h>
+
+using namespace std;
+
+#define PLUGIN_TAG_SERV "stale_response_intercept"
+const int64_t c_max_single_write = 64 * 1024;
+
+DEF_DBG_CTL(PLUGIN_TAG_SERV)
+
+/*-----------------------------------------------------------------------------------------------*/
+struct SContData {
+ struct IoHandle {
+ TSVIO vio;
+ TSIOBuffer buffer;
+ TSIOBufferReader reader;
+ IoHandle() : vio(0), buffer(0), reader(0){};
+ ~IoHandle()
+ {
+ if (reader) {
+ TSIOBufferReaderFree(reader);
+ }
+ if (buffer) {
+ TSIOBufferDestroy(buffer);
+ }
+ };
+ };
+
+ TSVConn net_vc;
+ TSCont contp;
+
+ IoHandle input;
+ IoHandle output;
+ TSHttpParser http_parser;
+ TSMBuffer req_hdr_bufp;
+ TSMLoc req_hdr_loc;
+ bool req_hdr_parsed;
+
+ bool conn_setup;
+ bool write_setup;
+
+ ConfigInfo *plugin_config;
+ BodyData *pBody;
+ uint32_t next_chunk_written;
+
+ SContData(TSCont cont)
+ : net_vc(0),
+ contp(cont),
+ input(),
+ output(),
+ req_hdr_bufp(0),
+ req_hdr_loc(0),
+ req_hdr_parsed(false),
+ conn_setup(false),
+ write_setup(false),
+ plugin_config(nullptr),
+ pBody(nullptr),
+ next_chunk_written(0)
+ {
+ http_parser = TSHttpParserCreate();
+ }
+
+ ~SContData()
+ {
+ TSDebug(PLUGIN_TAG_SERV, "[%s] Destroying continuation data", __FUNCTION__);
+ TSHttpParserDestroy(http_parser);
+ if (req_hdr_loc) {
+ TSHandleMLocRelease(req_hdr_bufp, TS_NULL_MLOC, req_hdr_loc);
+ }
+ if (req_hdr_bufp) {
+ TSMBufferDestroy(req_hdr_bufp);
+ }
+ };
+};
+
+/*-----------------------------------------------------------------------------------------------*/
+static bool
+connSetup(SContData *cont_data, TSVConn vconn)
+{
+ if (cont_data->conn_setup) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] SContData already init", __FUNCTION__);
+ return false;
+ }
+ cont_data->conn_setup = true;
+ cont_data->net_vc = vconn;
+
+ cont_data->input.buffer = TSIOBufferCreate();
+ cont_data->input.reader = TSIOBufferReaderAlloc(cont_data->input.buffer);
+ cont_data->input.vio = TSVConnRead(cont_data->net_vc, cont_data->contp, cont_data->input.buffer, INT_MAX);
+
+ cont_data->req_hdr_bufp = TSMBufferCreate();
+ cont_data->req_hdr_loc = TSHttpHdrCreate(cont_data->req_hdr_bufp);
+ TSHttpHdrTypeSet(cont_data->req_hdr_bufp, cont_data->req_hdr_loc, TS_HTTP_TYPE_REQUEST);
+
+ TSDebug(PLUGIN_TAG_SERV, "[%s] Done", __FUNCTION__);
+ return true;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+connShutdownDataDestory(SContData *cont_data)
+{
+ if (cont_data->net_vc) {
+ TSVConnClose(cont_data->net_vc);
+ }
+ // we need to destroy the body data object
+ if (cont_data->pBody->key_hash_active) {
+ if (!async_remove_active(cont_data->pBody->key_hash, cont_data->plugin_config)) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] didnt delete async active", __FUNCTION__);
+ }
+ } else {
+ delete cont_data->pBody;
+ }
+ // clean up my cont
+ TSContDestroy(cont_data->contp);
+ delete cont_data;
+ TSDebug(PLUGIN_TAG_SERV, "[%s] Done", __FUNCTION__);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static bool
+writeOutData(SContData *cont_data)
+{
+ int64_t total_current_write = 0;
+ uint32_t max_chunk_count = cont_data->pBody->getChunkCount();
+ for (uint32_t chunk_index = cont_data->next_chunk_written; chunk_index < max_chunk_count; chunk_index++) {
+ const char *start;
+ int64_t avail;
+ if (!cont_data->pBody->getChunk(chunk_index, &start, &avail)) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] Error while getting chunk_index %d", __FUNCTION__, chunk_index);
+ TSError("[%s] Error while getting chunk_index %d", __FUNCTION__, chunk_index);
+ break;
+ }
+ if (TSIOBufferWrite(cont_data->output.buffer, start, avail) != avail) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] Error while writing content avail=%d", __FUNCTION__, (int)avail);
+ }
+ cont_data->pBody->removeChunk(chunk_index);
+ total_current_write += avail;
+ cont_data->next_chunk_written = chunk_index + 1;
+ if (total_current_write >= c_max_single_write) {
+ break;
+ }
+ }
+ TSVIOReenable(cont_data->output.vio);
+
+ // TSDebug(PLUGIN_TAG_SERV, "[%s] written=%d done=%d", __FUNCTION__, (int)total_current_write,(cont_data->next_chunk_written >=
+ // max_chunk_count));
+ return (cont_data->next_chunk_written >= max_chunk_count);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+writeSetup(SContData *cont_data)
+{
+ if (!cont_data->write_setup) {
+ cont_data->write_setup = true;
+ cont_data->output.buffer = TSIOBufferCreate();
+ cont_data->output.reader = TSIOBufferReaderAlloc(cont_data->output.buffer);
+ cont_data->output.vio = TSVConnWrite(cont_data->net_vc, cont_data->contp, cont_data->output.reader, INT_MAX);
+ // set the total length to write
+ TSVIONBytesSet(cont_data->output.vio, cont_data->pBody->getSize());
+ TSDebug(PLUGIN_TAG_SERV, "[%s] Done length=%d", __FUNCTION__, (int)cont_data->pBody->getSize());
+ } else {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] Already init", __FUNCTION__);
+ }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static bool
+handleRead(SContData *cont_data)
+{
+ int avail = TSIOBufferReaderAvail(cont_data->input.reader);
+ if (avail == TS_ERROR) {
+ TSError("[%s] Error while getting number of bytes available", __FUNCTION__);
+ return false;
+ }
+
+ TSDebug(PLUGIN_TAG_SERV, "[%s] avail %d", __FUNCTION__, avail);
+
+ int consumed = 0;
+ if (avail > 0) {
+ int64_t data_len;
+ const char *data;
+ TSIOBufferBlock block = TSIOBufferReaderStart(cont_data->input.reader);
+ while (block != nullptr) {
+ data = TSIOBufferBlockReadStart(block, cont_data->input.reader, &data_len);
+
+ if (!cont_data->req_hdr_parsed) {
+ const char *endptr = data + data_len;
+ if (TSHttpHdrParseReq(cont_data->http_parser, cont_data->req_hdr_bufp, cont_data->req_hdr_loc, &data, endptr) ==
+ TS_PARSE_DONE) {
+ cont_data->req_hdr_parsed = true;
+ TSDebug(PLUGIN_TAG_SERV, "[%s] Parsed header", __FUNCTION__);
+ }
+ }
+ consumed += data_len;
+ block = TSIOBufferBlockNext(block);
+ }
+ }
+
+ TSIOBufferReaderConsume(cont_data->input.reader, consumed);
+
+ TSDebug(PLUGIN_TAG_SERV, "[%s] Consumed %d bytes from input vio, avail: %d", __FUNCTION__, consumed, avail);
+
+ // Modify the input VIO to reflect how much data we've completed.
+ TSVIONDoneSet(cont_data->input.vio, TSVIONDoneGet(cont_data->input.vio) + consumed);
+
+ if (!cont_data->req_hdr_parsed) {
+ TSDebug(PLUGIN_TAG_SERV, "[%s] Reenabling input vio need more header data", __FUNCTION__);
+ TSVIOReenable(cont_data->input.vio);
+ }
+
+ return true;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static int
+serverIntercept(TSCont contp, TSEvent event, void *edata)
+{
+ SContData *cont_data = static_cast<SContData *>(TSContDataGet(contp));
+ bool shutdown = false;
+
+ switch (event) {
+ case TS_EVENT_NET_ACCEPT:
+ TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} net accept event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+ if (!connSetup(cont_data, static_cast<TSVConn>(edata))) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} connSetup aleady initalized", __FUNCTION__, cont_data->pBody->key_hash);
+ }
+ break;
+
+ case TS_EVENT_NET_ACCEPT_FAILED:
+ // not sure why this would happen, but it does
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} net accept failed %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+ shutdown = true;
+ break;
+
+ case TS_EVENT_VCONN_READ_READY:
+ TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} vconn read ready event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+ if (!handleRead(cont_data)) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} handleRead failed", __FUNCTION__, cont_data->pBody->key_hash);
+ break;
+ }
+ // VCONN_READ_READY should not happen again since we dont reenable input.vio
+ if (cont_data->req_hdr_parsed && !cont_data->write_setup) {
+ writeSetup(cont_data);
+ writeOutData(cont_data);
+ }
+ break;
+
+ case TS_EVENT_VCONN_READ_COMPLETE:
+ case TS_EVENT_VCONN_EOS:
+ // intentional fall-through
+ TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} vconn read complete/eos event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+ // shutdown id we havent parsed headers and get a read eos/complete
+ if (!cont_data->req_hdr_parsed) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} read complete but headers not parsed", __FUNCTION__, cont_data->pBody->key_hash);
+ shutdown = true;
+ }
+ break;
+
+ case TS_EVENT_VCONN_WRITE_READY:
+ // TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} vconn write ready event %d", __FUNCTION__,cont_data->pBody->key_hash,event);
+ // trying not to write out the whole body at once if its big
+ writeOutData(cont_data);
+ break;
+
+ case TS_EVENT_VCONN_WRITE_COMPLETE:
+ TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} vconn write complete event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+ shutdown = true;
+ break;
+
+ case TS_EVENT_ERROR:
+ // todo: do some error handling here
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} error event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+ shutdown = true;
+ break;
+
+ default:
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} default event %d", __FUNCTION__, cont_data->pBody->key_hash, event);
+ break;
+ }
+
+ if (shutdown) {
+ connShutdownDataDestory(cont_data);
+ }
+
+ return 1;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+serverInterceptSetup(TSHttpTxn txnp, BodyData *pBody, ConfigInfo *plugin_config)
+{
+ // make sure we have data to deliver -- note called body but its headers+body
+ if (!pBody || (pBody->getSize() <= 0)) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] must have body and size > 0", __FUNCTION__);
+ // remove async active if pBody exists but size <= 0
+ if (pBody && pBody->key_hash_active) {
+ if (async_remove_active(pBody->key_hash, plugin_config)) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] removed async active due to pbody size <= 0", __FUNCTION__);
+ } else {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] failed to delete async active when pbody size <= 0", __FUNCTION__);
+ }
+ }
+ return false;
+ }
+ // make sure we have a contp
+ TSCont contp = TSContCreate(serverIntercept, TSMutexCreate());
+ if (!contp) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] Could not create intercept contp", __FUNCTION__);
+ // remove async active if couldn't create intercept contp
+ if (pBody->key_hash_active) {
+ if (async_remove_active(pBody->key_hash, plugin_config)) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] removed async active since couldn't create intercept contp", __FUNCTION__);
+ } else {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] failed to delete async active when couldn't create intercept contp", __FUNCTION__);
+ }
+ }
+ return false;
+ }
+ // create the data block for the cont
+ SContData *cont_data = new SContData(contp);
+ // set some pointers
+ cont_data->plugin_config = plugin_config;
+ cont_data->pBody = pBody;
+ // set the intercept and turn on caching
+ TSContDataSet(contp, cont_data);
+ TSHttpTxnServerIntercept(contp, txnp);
+ TSHttpTxnReqCacheableSet(txnp, 1);
+ TSHttpTxnRespCacheableSet(txnp, 1);
+ TSDebug(PLUGIN_TAG_SERV, "[%s] {%u} Success length=%d", __FUNCTION__, cont_data->pBody->key_hash,
+ (int)cont_data->pBody->getSize());
+ return true;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/ServerIntercept.h b/plugins/experimental/stale_response/ServerIntercept.h
new file mode 100644
index 0000000000..47f5d20ea1
--- /dev/null
+++ b/plugins/experimental/stale_response/ServerIntercept.h
@@ -0,0 +1,30 @@
+/** @file
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+#pragma once
+
+#include "ts/ts.h"
+
+struct BodyData;
+struct ConfigInfo;
+
+bool serverInterceptSetup(TSHttpTxn txnp, BodyData *pBody, ConfigInfo *plugin_config);
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/UrlComponents.h b/plugins/experimental/stale_response/UrlComponents.h
new file mode 100644
index 0000000000..6673b4ba2b
--- /dev/null
+++ b/plugins/experimental/stale_response/UrlComponents.h
@@ -0,0 +1,265 @@
+/** @file
+
+ URL helper
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#pragma once
+
+#include "ts/ts.h"
+
+#include <cstdio>
+#include <string>
+
+///////////////////////////////////////////////////////////////////////////////
+// Class holding one request URL's component.
+//
+
+struct UrlComponents {
+ UrlComponents() : _port(0) {}
+
+ void
+ populate(TSMBuffer bufp, TSMLoc urlLoc)
+ {
+ int scheme_len;
+ int host_len;
+ int path_len;
+ int query_len;
+ int matrix_len;
+
+ const char *scheme = TSUrlSchemeGet(bufp, urlLoc, &scheme_len);
+ const char *host = TSUrlHostGet(bufp, urlLoc, &host_len);
+ const char *path = TSUrlPathGet(bufp, urlLoc, &path_len);
+ const char *query = TSUrlHttpQueryGet(bufp, urlLoc, &query_len);
+ const char *matrix = TSUrlHttpParamsGet(bufp, urlLoc, &matrix_len);
+ _port = TSUrlPortGet(bufp, urlLoc);
+
+ _scheme.assign(scheme, scheme_len);
+ _host.assign(host, host_len);
+ _path.assign(path, path_len);
+ _query.assign(query, query_len);
+ _matrix.assign(matrix, matrix_len);
+ }
+
+ // get entire url (e.g.http://host/path?query)
+ void
+ construct(std::string &url)
+ {
+ // schemeExtra= :// = 3
+ // portExtra = :xxxxx = 6
+ // just in case extra = 32
+ size_t iLen = _scheme.size() + _host.size() + _path.size() + _query.size() + _matrix.size() + 3 + 6 + 32;
+ url.reserve(iLen);
+
+ const int bitAddPort = 1;
+ const int bitAddQuery = 1 << 1;
+ const int bitAddMatrix = 1 << 2;
+ int bitField = bitAddPort; // add port by default
+ if ((_scheme.compare("http") == 0 && _port == 80) || (_scheme.compare("https") == 0 && _port == 443)) {
+ bitField &= ~bitAddPort;
+ }
+ if (_query.size() != 0) {
+ bitField |= bitAddQuery;
+ }
+ if (_matrix.size() != 0) {
+ bitField |= bitAddMatrix;
+ }
+
+ switch (bitField) {
+ case 0: // default port, no query, no matrix
+ url = _scheme + "://" + _host + "/" + _path;
+ break;
+
+ case bitAddPort: { // port, no query, no matrix
+ char sTemp[PORT_BUFFER_SIZE];
+ url = _scheme + "://" + _host + ":";
+ snprintf(sTemp, PORT_BUFFER_SIZE, "%d", _port);
+ url += sTemp;
+ url += "/" + _path;
+ break;
+ }
+
+ case bitAddQuery: // default port, with query, no matrix
+ url = _scheme + "://" + _host + "/" + _path + "?" + _query;
+ break;
+ case bitAddQuery | bitAddMatrix: // default port, with query, with matrix (even possible?)
+ url = _scheme + "://" + _host + "/" + _path + ";" + _matrix + "?" + _query;
+ break;
+
+ case bitAddMatrix: // default port, no query, with matrix
+ url = _scheme + "://" + _host + "/" + _path + ";" + _matrix;
+ break;
+
+ case bitAddPort | bitAddQuery: // port, with query, no matrix
+ { // port, with query, with matrix (even possible?)
+ char sTemp[PORT_BUFFER_SIZE];
+ url = _scheme + "://" + _host + ":";
+ snprintf(sTemp, PORT_BUFFER_SIZE, "%d", _port);
+ url += sTemp;
+ url += "/" + _path + "?" + _query;
+ break;
+ }
+
+ case bitAddPort | bitAddQuery | bitAddMatrix: { // port, with query, with matrix (even possible?)
+ char sTemp[PORT_BUFFER_SIZE];
+ url = _scheme + "://" + _host + ":";
+ snprintf(sTemp, PORT_BUFFER_SIZE, "%d", _port);
+ url += sTemp;
+ url += "/" + _path + ";" + _matrix + "?" + _query;
+ break;
+ }
+
+ case bitAddPort | bitAddMatrix: { // port, no query, with matrix
+ char sTemp[PORT_BUFFER_SIZE];
+ url = _scheme + "://" + _host + ":";
+ snprintf(sTemp, PORT_BUFFER_SIZE, "%d", _port);
+ url += sTemp;
+ url += "/" + _path + ";" + _matrix;
+ break;
+ }
+ }
+ }
+
+ // get path w/query or matrix
+ void
+ getCompletePathString(std::string &p)
+ {
+ // schemeExtra= :// = 3
+ // portExtra = :xxxxx = 6
+ // just in case extra = 32
+ size_t iLen = _path.size() + _query.size() + _matrix.size() + 3 + 6 + 32;
+ p.reserve(iLen);
+
+ int bitField = 0;
+ const int bitAddQuery = 1 << 1;
+ const int bitAddMatrix = 1 << 2;
+ if (_query.size() != 0) {
+ bitField |= bitAddQuery;
+ }
+ if (_matrix.size() != 0) {
+ bitField |= bitAddMatrix;
+ }
+
+ switch (bitField) {
+ case bitAddQuery: // default path, with query, no matrix
+ p = "/" + _path + "?" + _query;
+ break;
+ case bitAddQuery | bitAddMatrix: // default path, with query, with matrix (even possible?)
+ p = "/" + _path + ";" + _matrix + "?" + _query;
+ break;
+
+ case bitAddMatrix: // default port, no query, with matrix
+ p = "/" + _path + ";" + _matrix;
+ break;
+
+ default:
+ case 0: // default path, no query, no matrix
+ p = "/" + _path;
+ break;
+ }
+ }
+
+ // get string w/port if different than scheme default
+ void
+ getCompleteHostString(std::string &host)
+ {
+ if ((_scheme.compare("http") == 0 && _port == 80) ||
+ (_scheme.compare("https") == 0 && _port == 443)) { // port is default for scheme
+ host = _host;
+ } else { // port is different than scheme default, so include
+ char sTemp[PORT_BUFFER_SIZE];
+ host = _host + ":";
+ snprintf(sTemp, PORT_BUFFER_SIZE, "%d", _port);
+ host += sTemp;
+ }
+ }
+
+ void
+ setScheme(std::string &s)
+ {
+ _scheme = s;
+ };
+ void
+ setHost(std::string &h)
+ {
+ _host = h;
+ };
+ void
+ setPath(std::string &p)
+ {
+ _path = p;
+ };
+ void
+ setQuery(std::string &q)
+ {
+ _query = q;
+ };
+ void
+ setMatrix(std::string &m)
+ {
+ _matrix = m;
+ };
+ void
+ setPoinht(int p)
+ {
+ _port = p;
+ };
+
+ const std::string &
+ getScheme()
+ {
+ return _scheme;
+ };
+ const std::string &
+ getHost()
+ {
+ return _host;
+ };
+ const std::string &
+ getPath()
+ {
+ return _path;
+ };
+ const std::string &
+ getQuery()
+ {
+ return _query;
+ };
+ const std::string &
+ getMatrix()
+ {
+ return _matrix;
+ };
+ int
+ getPort()
+ {
+ return _port;
+ };
+
+private:
+ constexpr static size_t PORT_BUFFER_SIZE = 10;
+
+ std::string _scheme;
+ std::string _host;
+ std::string _path;
+ std::string _query;
+ std::string _matrix;
+ int _port;
+};
diff --git a/plugins/experimental/stale_response/stale_response.cc b/plugins/experimental/stale_response/stale_response.cc
new file mode 100644
index 0000000000..4ef7626879
--- /dev/null
+++ b/plugins/experimental/stale_response/stale_response.cc
@@ -0,0 +1,1132 @@
+/** @file
+
+ Implements RFC 5861 (HTTP Cache-Control Extensions for Stale Content)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#include "stale_response.h"
+#include "BodyData.h"
+#include "CacheUpdate.h"
+#include "DirectiveParser.h"
+#include "MurmurHash3.h"
+#include "ServerIntercept.h"
+
+#include "swoc/TextView.h"
+#include "ts/apidefs.h"
+#include "ts/remap.h"
+#include "ts/remap_version.h"
+#include "ts_wrap.h"
+
+#include <arpa/inet.h>
+#include <cinttypes>
+#include <cassert>
+#include <cstdlib>
+#include <cstring>
+#include <ctime>
+#include <getopt.h>
+#include <string>
+
+using namespace std;
+
+const char PLUGIN_TAG[] = "stale_response";
+const char PLUGIN_TAG_BAD[] = "stale_response_bad";
+
+DEF_DBG_CTL(PLUGIN_TAG)
+DEF_DBG_CTL(PLUGIN_TAG_BAD)
+DEF_DBG_CTL(PLUGIN_TAG_BODY)
+
+static const char VENDOR_NAME[] = "Apache Software Foundation";
+static const char SUPPORT_EMAIL[] = "dev@trafficserver.apache.org";
+
+static const char HTTP_VALUE_STALE_WARNING[] = "110 Response is stale";
+static const char SIE_SERVER_INTERCEPT_HEADER[] = "@X-CCExtensions-Sie-Intercept";
+static const char HTTP_VALUE_SERVER_INTERCEPT[] = "1";
+
+/*-----------------------------------------------------------------------------------------------*/
+static ResponseInfo *
+create_response_info(void)
+{
+ ResponseInfo *resp_info = static_cast<ResponseInfo *>(TSmalloc(sizeof(ResponseInfo)));
+
+ resp_info->http_hdr_buf = TSMBufferCreate();
+ resp_info->http_hdr_loc = TSHttpHdrCreate(resp_info->http_hdr_buf);
+ resp_info->parser = TSHttpParserCreate();
+ resp_info->parsed = false;
+
+ return resp_info;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+free_response_info(ResponseInfo *resp_info)
+{
+ TSHandleMLocRelease(resp_info->http_hdr_buf, TS_NULL_MLOC, resp_info->http_hdr_loc);
+ TSMBufferDestroy(resp_info->http_hdr_buf);
+ TSHttpParserDestroy(resp_info->parser);
+ TSfree(resp_info);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static RequestInfo *
+create_request_info(TSHttpTxn txnp)
+{
+ RequestInfo *req_info = static_cast<RequestInfo *>(TSmalloc(sizeof(RequestInfo)));
+
+ TSMBuffer hdr_url_buf;
+ TSMLoc hdr_url_loc;
+
+ TSHttpTxnClientReqGet(txnp, &hdr_url_buf, &hdr_url_loc);
+
+ // this only seems to be correct/consistent if done in http read request header state
+ char *url = TSHttpTxnEffectiveUrlStringGet(txnp, &req_info->effective_url_length);
+ req_info->effective_url = TSstrndup(url, req_info->effective_url_length);
+ TSfree(url);
+
+ // copy the headers
+ req_info->http_hdr_buf = TSMBufferCreate();
+ TSHttpHdrClone(req_info->http_hdr_buf, hdr_url_buf, hdr_url_loc, &(req_info->http_hdr_loc));
+ // release the client request
+ TSHandleMLocRelease(hdr_url_buf, TS_NULL_MLOC, hdr_url_loc);
+
+ // It turns out that the client_addr field isn't used if the request
+ // is internal. Cannot fetch a real client address in that case anyway
+ if (!TSHttpTxnIsInternal(txnp)) {
+ // copy client addr
+ req_info->client_addr = reinterpret_cast<struct sockaddr *>(TSmalloc(sizeof(struct sockaddr)));
+ memmove(req_info->client_addr, TSHttpTxnClientAddrGet(txnp), sizeof(struct sockaddr));
+ } else {
+ req_info->client_addr = nullptr;
+ }
+
+ // create the lookup key fron the effective url
+ MurmurHash3_x86_32(req_info->effective_url, req_info->effective_url_length, c_hashSeed, &(req_info->key_hash));
+
+ TSDebug(PLUGIN_TAG, "[%s] {%u} url=[%s]", __FUNCTION__, req_info->key_hash, req_info->effective_url);
+
+ return req_info;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+free_request_info(RequestInfo *req_info)
+{
+ TSfree(req_info->effective_url);
+ TSHandleMLocRelease(req_info->http_hdr_buf, TS_NULL_MLOC, req_info->http_hdr_loc);
+ TSMBufferDestroy(req_info->http_hdr_buf);
+ TSfree(req_info->client_addr);
+ TSfree(req_info);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static StateInfo *
+create_state_info(TSHttpTxn txnp, TSCont contp)
+{
+ StateInfo *state = new StateInfo(txnp, contp);
+ state->req_info = create_request_info(txnp);
+ return state;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+free_state_info(StateInfo *state)
+{
+ // clean up states copy of url
+ if (state->pristine_url) {
+ TSfree(state->pristine_url);
+ }
+ state->pristine_url = nullptr;
+
+ // bunch of buffers state has created
+ if (state->req_io_buf_reader) {
+ TSIOBufferReaderFree(state->req_io_buf_reader);
+ }
+ state->req_io_buf_reader = nullptr;
+ if (state->req_io_buf) {
+ TSIOBufferDestroy(state->req_io_buf);
+ }
+ state->req_io_buf = nullptr;
+ if (state->resp_io_buf_reader) {
+ TSIOBufferReaderFree(state->resp_io_buf_reader);
+ }
+ state->resp_io_buf_reader = nullptr;
+ if (state->resp_io_buf) {
+ TSIOBufferDestroy(state->resp_io_buf);
+ }
+ state->resp_io_buf = nullptr;
+
+ // dont think these need cleanup
+ // state->r_vio
+ // state->w_vio
+ // state->vconn
+
+ // clean up request and response data state created
+ if (state->req_info) {
+ free_request_info(state->req_info);
+ }
+ state->req_info = nullptr;
+ if (state->resp_info) {
+ free_response_info(state->resp_info);
+ }
+ state->resp_info = nullptr;
+
+ // this should be null but check and delete
+ if (state->sie_body) {
+ delete state->sie_body;
+ }
+ state->sie_body = nullptr;
+
+ // this is a temp pointer do not delete
+ // state->cur_save_body
+
+ TSfree(state);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+int64_t
+aync_memory_total_add(ConfigInfo *plugin_config, int64_t change)
+{
+ int64_t total;
+ TSMutexLock(plugin_config->body_data_mutex);
+ plugin_config->body_data_memory_usage += change;
+ total = plugin_config->body_data_memory_usage;
+ TSMutexUnlock(plugin_config->body_data_mutex);
+ return total;
+}
+/*-----------------------------------------------------------------------------------------------*/
+inline int64_t
+aync_memory_total_get(ConfigInfo *plugin_config)
+{
+ return aync_memory_total_add(plugin_config, 0);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+BodyData *
+async_check_active(uint32_t key_hash, ConfigInfo *plugin_config)
+{
+ BodyData *pFound = nullptr;
+ TSMutexLock(plugin_config->body_data_mutex);
+ UintBodyMap::iterator pos = plugin_config->body_data->find(key_hash);
+ if (pos != plugin_config->body_data->end()) {
+ pFound = pos->second;
+ ;
+ }
+ TSMutexUnlock(plugin_config->body_data_mutex);
+
+ TSDebug(PLUGIN_TAG, "[%s] {%u} pFound=%p", __FUNCTION__, key_hash, pFound);
+ return pFound;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+async_check_and_add_active(uint32_t key_hash, ConfigInfo *plugin_config)
+{
+ bool isNew = false;
+ TSMutexLock(plugin_config->body_data_mutex);
+ UintBodyMap::iterator pos = plugin_config->body_data->find(key_hash);
+ if (pos == plugin_config->body_data->end()) {
+ BodyData *pNew = new BodyData();
+ pNew->key_hash = key_hash;
+ pNew->key_hash_active = true;
+ plugin_config->body_data->insert(make_pair(key_hash, pNew));
+ isNew = true;
+ }
+ int tempSize = plugin_config->body_data->size();
+ TSMutexUnlock(plugin_config->body_data_mutex);
+
+ TSDebug(PLUGIN_TAG, "[%s] {%u} isNew=%d size=%d", __FUNCTION__, key_hash, isNew, tempSize);
+ return isNew;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+bool
+add_header(TSMBuffer &reqp, TSMLoc &hdr_loc, string header, string value)
+{
+ bool bReturn = false;
+ if (value.size() <= 0) {
+ TSDebug(PLUGIN_TAG, "\tWould set header %s to an empty value, skipping", header.c_str());
+ } else {
+ TSMLoc new_field;
+
+ if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(reqp, hdr_loc, header.data(), header.size(), &new_field)) {
+ if (TS_SUCCESS == TSMimeHdrFieldValueStringInsert(reqp, hdr_loc, new_field, -1, value.data(), value.size())) {
+ if (TS_SUCCESS == TSMimeHdrFieldAppend(reqp, hdr_loc, new_field)) {
+ TSDebug(PLUGIN_TAG, "\tAdded header %s: %s", header.c_str(), value.c_str());
+ bReturn = true;
+ }
+ } else {
+ TSMimeHdrFieldDestroy(reqp, hdr_loc, new_field);
+ }
+ TSHandleMLocRelease(reqp, hdr_loc, new_field);
+ }
+ }
+ return bReturn;
+}
+/*-----------------------------------------------------------------------------------------------*/
+bool
+async_remove_active(uint32_t key_hash, ConfigInfo *plugin_config)
+{
+ bool wasActive = false;
+ TSMutexLock(plugin_config->body_data_mutex);
+ UintBodyMap::iterator pos = plugin_config->body_data->find(key_hash);
+ if (pos != plugin_config->body_data->end()) {
+ plugin_config->body_data_memory_usage -= (pos->second)->getSize();
+ delete pos->second;
+ plugin_config->body_data->erase(pos);
+ wasActive = true;
+ }
+ int tempSize = plugin_config->body_data->size();
+ TSMutexUnlock(plugin_config->body_data_mutex);
+
+ TSDebug(PLUGIN_TAG, "[%s] {%u} wasActive=%d size=%d", __FUNCTION__, key_hash, wasActive, tempSize);
+ return wasActive;
+}
+/*-----------------------------------------------------------------------------------------------*/
+bool
+async_intercept_active(uint32_t key_hash, ConfigInfo *plugin_config)
+{
+ bool interceptActive = false;
+ TSMutexLock(plugin_config->body_data_mutex);
+ UintBodyMap::iterator pos = plugin_config->body_data->find(key_hash);
+ if (pos != plugin_config->body_data->end()) {
+ interceptActive = pos->second->intercept_active;
+ }
+ TSMutexUnlock(plugin_config->body_data_mutex);
+
+ TSDebug(PLUGIN_TAG, "[%s] {%u} interceptActive=%d", __FUNCTION__, key_hash, interceptActive);
+ return interceptActive;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+send_stale_response(StateInfo *state)
+{
+ // force to use age header
+ TSHttpTxnConfigIntSet(state->txnp, TS_CONFIG_HTTP_INSERT_AGE_IN_RESPONSE, 1);
+ // add send response header hook for warning header
+ TSHttpTxnHookAdd(state->txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, state->transaction_contp);
+ // set cache as fresh
+ TSHttpTxnCacheLookupStatusSet(state->txnp, TS_CACHE_LOOKUP_HIT_FRESH);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static CachedHeaderInfo *
+get_cached_header_info(StateInfo *state)
+{
+ TSHttpTxn txnp = state->txnp;
+ CachedHeaderInfo *chi;
+ TSMBuffer cr_buf;
+ TSMLoc cr_hdr_loc, cr_date_loc, cr_cache_control_loc, cr_cache_control_dup_loc;
+ int cr_cache_control_count = 0;
+
+ chi = static_cast<CachedHeaderInfo *>(TSmalloc(sizeof(CachedHeaderInfo)));
+
+ chi->date = 0;
+ chi->max_age = 0;
+
+ // -1 is used as a placeholder for teh following two meaning that their
+ // respective directives were not in the Cache-Control header.
+ chi->stale_while_revalidate = -1;
+ chi->stale_if_error = -1;
+
+ if (TSHttpTxnCachedRespGet(txnp, &cr_buf, &cr_hdr_loc) == TS_SUCCESS) {
+ cr_date_loc = TSMimeHdrFieldFind(cr_buf, cr_hdr_loc, TS_MIME_FIELD_DATE, TS_MIME_LEN_DATE);
+ if (cr_date_loc != TS_NULL_MLOC) {
+ chi->date = TSMimeHdrFieldValueDateGet(cr_buf, cr_hdr_loc, cr_date_loc);
+ TSHandleMLocRelease(cr_buf, cr_hdr_loc, cr_date_loc);
+ }
+
+ cr_cache_control_loc = TSMimeHdrFieldFind(cr_buf, cr_hdr_loc, TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL);
+
+ while (cr_cache_control_loc != TS_NULL_MLOC) {
+ cr_cache_control_count = TSMimeHdrFieldValuesCount(cr_buf, cr_hdr_loc, cr_cache_control_loc);
+
+ DirectiveParser directives;
+ for (int i = 0; i < cr_cache_control_count; ++i) {
+ int val_len = 0;
+ char const *v = TSMimeHdrFieldValueStringGet(cr_buf, cr_hdr_loc, cr_cache_control_loc, i, &val_len);
+ TSDebug(PLUGIN_TAG, "Processing directives: %.*s", val_len, v);
+ swoc::TextView cache_control_value{v, static_cast<size_t>(val_len)};
+ directives.merge(DirectiveParser{cache_control_value});
+ }
+ TSDebug(PLUGIN_TAG, "max-age: %ld, stale-while-revalidate: %ld, stale-if-error: %ld", directives.get_max_age(),
+ directives.get_stale_while_revalidate(), directives.get_stale_if_error());
+ if (directives.get_max_age() >= 0) {
+ chi->max_age = directives.get_max_age();
+ }
+ if (directives.get_stale_while_revalidate() >= 0) {
+ chi->stale_while_revalidate = directives.get_stale_while_revalidate();
+ }
+ if (directives.get_stale_if_error() >= 0) {
+ chi->stale_if_error = directives.get_stale_if_error();
+ }
+
+ cr_cache_control_dup_loc = TSMimeHdrFieldNextDup(cr_buf, cr_hdr_loc, cr_cache_control_loc);
+ TSHandleMLocRelease(cr_buf, cr_hdr_loc, cr_cache_control_loc);
+ cr_cache_control_loc = cr_cache_control_dup_loc;
+ }
+ TSHandleMLocRelease(cr_buf, TS_NULL_MLOC, cr_hdr_loc);
+ }
+
+ TSDebug(PLUGIN_TAG, "[%s] item_count=%d max_age=%ld swr=%ld sie=%ld", __FUNCTION__, cr_cache_control_count, chi->max_age,
+ chi->stale_while_revalidate, chi->stale_if_error);
+
+ // load the config mins/default
+ if ((chi->stale_if_error == -1) && state->plugin_config->stale_if_error_default) {
+ chi->stale_if_error = state->plugin_config->stale_if_error_default;
+ }
+
+ if (state->plugin_config->stale_if_error_override > chi->stale_if_error) {
+ chi->stale_if_error = state->plugin_config->stale_if_error_override;
+ }
+
+ if ((chi->stale_while_revalidate == -1) && state->plugin_config->stale_while_revalidate_default) {
+ chi->stale_while_revalidate = state->plugin_config->stale_while_revalidate_default;
+ }
+
+ if (state->plugin_config->stale_while_revalidate_override > chi->stale_while_revalidate) {
+ chi->stale_while_revalidate = state->plugin_config->stale_while_revalidate_override;
+ }
+
+ // The callers use the stale-while-revalidate and stale-if-error values for
+ // calulations and do not expect nor need -1 values for non-existent
+ // directives as we did above. Now that we've handled the user configured
+ // defaults, we can assume "not set" is a value of 0.
+ chi->stale_while_revalidate = std::max(chi->stale_while_revalidate, 0l);
+ chi->stale_if_error = std::max(chi->stale_if_error, 0l);
+
+ TSDebug(PLUGIN_TAG, "[%s] after defaults item_count=%d max_age=%ld swr=%ld sie=%ld", __FUNCTION__, cr_cache_control_count,
+ chi->max_age, chi->stale_while_revalidate, chi->stale_if_error);
+
+ return chi;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+fetch_save_response(StateInfo *state, BodyData *pBody)
+{
+ TSIOBufferBlock block;
+ int64_t avail;
+ char const *start;
+ block = TSIOBufferReaderStart(state->resp_io_buf_reader);
+ while (block != nullptr) {
+ start = TSIOBufferBlockReadStart(block, state->resp_io_buf_reader, &avail);
+ if (avail > 0) {
+ pBody->addChunk(start, avail);
+ // increase body_data_memory_usage only if content stored in plugin_config->body_data
+ if (pBody->key_hash_active) {
+ aync_memory_total_add(state->plugin_config, avail);
+ }
+ }
+ block = TSIOBufferBlockNext(block);
+ }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+fetch_parse_response(StateInfo *state)
+{
+ TSIOBufferBlock block;
+ TSParseResult pr = TS_PARSE_CONT;
+ int64_t avail;
+ char const *start;
+
+ block = TSIOBufferReaderStart(state->resp_io_buf_reader);
+
+ while ((pr == TS_PARSE_CONT) && (block != nullptr)) {
+ start = TSIOBufferBlockReadStart(block, state->resp_io_buf_reader, &avail);
+ if (avail > 0) {
+ pr = TSHttpHdrParseResp(state->resp_info->parser, state->resp_info->http_hdr_buf, state->resp_info->http_hdr_loc, &start,
+ (start + avail));
+ }
+ block = TSIOBufferBlockNext(block);
+ }
+
+ if (pr != TS_PARSE_CONT) {
+ state->resp_info->status = TSHttpHdrStatusGet(state->resp_info->http_hdr_buf, state->resp_info->http_hdr_loc);
+ state->resp_info->parsed = true;
+ TSDebug(PLUGIN_TAG, "[%s] {%u} HTTP Status: %d", __FUNCTION__, state->req_info->key_hash, state->resp_info->status);
+ }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+fetch_read_the_data(StateInfo *state)
+{
+ // always save data
+ if (state->cur_save_body) {
+ fetch_save_response(state, state->cur_save_body);
+ } else {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] no BodyData", __FUNCTION__);
+ }
+ // get the resp code
+ if (!state->resp_info->parsed) {
+ fetch_parse_response(state);
+ }
+ // Consume data
+ int64_t avail = TSIOBufferReaderAvail(state->resp_io_buf_reader);
+ TSIOBufferReaderConsume(state->resp_io_buf_reader, avail);
+ TSVIONDoneSet(state->r_vio, TSVIONDoneGet(state->r_vio) + avail);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+fetch_finish(StateInfo *state)
+{
+ TSDebug(PLUGIN_TAG, "[%s] {%u} swr=%d sie=%d", __FUNCTION__, state->req_info->key_hash, state->swr_active, state->sie_active);
+ if (state->swr_active) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} SWR Unlock URL / Post request", __FUNCTION__, state->req_info->key_hash);
+ if (state->sie_active && valid_sie_status(state->resp_info->status)) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} SWR Bad Data skipping", __FUNCTION__, state->req_info->key_hash);
+ if (!async_remove_active(state->req_info->key_hash, state->plugin_config)) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} didnt delete async active", __FUNCTION__, state->req_info->key_hash);
+ }
+ } else {
+ // this will place the new data in cache by server intercept
+ intercept_fetch_the_url(state);
+ }
+ } else // state->sie_active
+ {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} SIE in sync path Reenable %d", __FUNCTION__, state->req_info->key_hash,
+ state->resp_info->status);
+ if (valid_sie_status(state->resp_info->status)) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} SIE sending stale data", __FUNCTION__, state->req_info->key_hash);
+ if (state->plugin_config->log_info.object &&
+ (state->plugin_config->log_info.all || state->plugin_config->log_info.stale_if_error)) {
+ CachedHeaderInfo *chi = get_cached_header_info(state);
+ TSTextLogObjectWrite(state->plugin_config->log_info.object, "stale-if-error: %ld - %ld < %ld + %ld %s", state->txn_start,
+ chi->date, chi->max_age, chi->stale_if_error, state->req_info->effective_url);
+ TSfree(chi);
+ }
+ // send out the stale data
+ send_stale_response(state);
+ } else {
+ TSDebug(PLUGIN_TAG, "[%s] SIE {%u} sending new data", __FUNCTION__, state->req_info->key_hash);
+ // load the data as if we are OS by ServerIntercept
+ BodyData *pBody = state->sie_body;
+ state->sie_body = nullptr;
+ // ServerIntercept will delete the body and send the data to the client
+ // Add sie_server_intercept header.
+ TSMBuffer buf;
+ TSMLoc hdr_loc;
+ TSHttpTxnClientReqGet(state->txnp, &buf, &hdr_loc);
+ if (!add_header(buf, hdr_loc, SIE_SERVER_INTERCEPT_HEADER, HTTP_VALUE_SERVER_INTERCEPT)) {
+ TSError("stale_response [%s] error inserting header %s", __FUNCTION__, SIE_SERVER_INTERCEPT_HEADER);
+ }
+ TSHandleMLocRelease(buf, TS_NULL_MLOC, hdr_loc);
+ serverInterceptSetup(state->txnp, pBody, state->plugin_config);
+ // This was TSHttpTxnNewCacheLookupDo -- now we just use the copy we have
+ }
+ TSHttpTxnReenable(state->txnp, TS_EVENT_HTTP_CONTINUE);
+ }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static int
+fetch_consume(TSCont contp, TSEvent event, void *edata)
+{
+ StateInfo *state;
+ state = static_cast<StateInfo *>(TSContDataGet(contp));
+
+ switch (event) {
+ case TS_EVENT_VCONN_WRITE_READY:
+ // We shouldn't get here because we specify the exact size of the buffer.
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Write Ready", __FUNCTION__, state->req_info->key_hash);
+ // fallthrough
+ case TS_EVENT_VCONN_WRITE_COMPLETE:
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Write Complete", __FUNCTION__, state->req_info->key_hash);
+ break;
+
+ case TS_EVENT_VCONN_READ_READY:
+ // save the data and parse header if needed
+ fetch_read_the_data(state);
+ TSVIOReenable(state->r_vio);
+ break;
+
+ case TS_EVENT_VCONN_READ_COMPLETE:
+ case TS_EVENT_VCONN_EOS:
+ case TS_EVENT_VCONN_INACTIVITY_TIMEOUT:
+ case TS_EVENT_ERROR:
+ // Don't free the reference to the state object
+ // The txnp object may already be freed at this point
+ if (event == TS_EVENT_VCONN_INACTIVITY_TIMEOUT) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Inactivity Timeout", __FUNCTION__, state->req_info->key_hash);
+ TSVConnAbort(state->vconn, TS_VC_CLOSE_ABORT);
+ } else {
+ if (event == TS_EVENT_VCONN_READ_COMPLETE) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Vconn Read Complete", __FUNCTION__, state->req_info->key_hash);
+ } else if (event == TS_EVENT_VCONN_EOS) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Vconn Eos", __FUNCTION__, state->req_info->key_hash);
+ } else if (event == TS_EVENT_ERROR) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Error Event", __FUNCTION__, state->req_info->key_hash);
+ }
+ TSVConnClose(state->vconn);
+ }
+
+ // I dont think we need this here but it should not hurt
+ fetch_read_the_data(state);
+ // we are done
+ fetch_finish(state);
+ // free state
+ free_state_info(state);
+ TSContDestroy(contp);
+ break;
+
+ default:
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} Unknown event %d.", __FUNCTION__, state->req_info->key_hash, event);
+ break;
+ }
+
+ return 0;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static int
+fetch_resource(TSCont contp, TSEvent, void *)
+{
+ StateInfo *state;
+ TSCont consume_contp;
+ state = static_cast<StateInfo *>(TSContDataGet(contp));
+
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Start swr=%d sie=%d ", __FUNCTION__, state->req_info->key_hash, state->swr_active,
+ state->sie_active);
+ consume_contp = TSContCreate(fetch_consume, TSMutexCreate());
+ TSContDataSet(consume_contp, state);
+
+ // create the response info swr may use this
+ state->resp_info = create_response_info();
+ // force a connection close header here seems to be needed
+ fix_connection_close(state);
+ // create some buffers
+ state->req_io_buf = TSIOBufferCreate();
+ state->req_io_buf_reader = TSIOBufferReaderAlloc(state->req_io_buf);
+ state->resp_io_buf = TSIOBufferCreate();
+ state->resp_io_buf_reader = TSIOBufferReaderAlloc(state->resp_io_buf);
+ // add in my trailing parameter -- stripped off post cache lookup
+ add_trailing_parameter(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc);
+ // copy all the headers into a buffer
+ TSHttpHdrPrint(state->req_info->http_hdr_buf, state->req_info->http_hdr_loc, state->req_io_buf);
+ TSIOBufferWrite(state->req_io_buf, "\r\n", 2);
+
+ // setup place to store body data
+ if (state->sie_body) {
+ state->cur_save_body = state->sie_body;
+ } else {
+ state->cur_save_body = async_check_active(state->req_info->key_hash, state->plugin_config);
+ }
+
+ // connect , setup read , write
+ assert(state->req_info->client_addr != nullptr);
+ state->vconn = TSHttpConnect(state->req_info->client_addr);
+ state->r_vio = TSVConnRead(state->vconn, consume_contp, state->resp_io_buf, INT64_MAX);
+ state->w_vio =
+ TSVConnWrite(state->vconn, consume_contp, state->req_io_buf_reader, TSIOBufferReaderAvail(state->req_io_buf_reader));
+
+ TSContDestroy(contp);
+
+ return 0;
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static void
+fetch_start(StateInfo *state, TSCont contp)
+{
+ TSCont fetch_contp = nullptr;
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Start swr=%d sie=%d ", __FUNCTION__, state->req_info->key_hash, state->swr_active,
+ state->sie_active);
+
+ ConfigInfo *plugin_config = static_cast<ConfigInfo *>(TSContDataGet(contp));
+
+ if (state->swr_active) {
+ bool isNew = async_check_and_add_active(state->req_info->key_hash, state->plugin_config);
+ // If already doing async lookup lets just close shop and go home
+ if (!isNew && !plugin_config->force_parallel_async) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} async in progress skip", __FUNCTION__, state->req_info->key_hash);
+ TSStatIntIncrement(state->plugin_config->rfc_stat_swr_hit_skip, 1);
+ // free state
+ TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+ free_state_info(state);
+ } else {
+ // get the pristine url for the server intercept
+ get_pristine_url(state);
+ fetch_contp = TSContCreate(fetch_resource, TSMutexCreate());
+ TSContDataSet(fetch_contp, state);
+ TSContScheduleOnPool(fetch_contp, 0, TS_THREAD_POOL_NET);
+ }
+ } else // state->sie_active
+ {
+ state->sie_body = new BodyData();
+ fetch_contp = TSContCreate(fetch_resource, TSMutexCreate());
+ TSContDataSet(fetch_contp, state);
+ TSContScheduleOnPool(fetch_contp, 0, TS_THREAD_POOL_NET);
+ }
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static int
+transaction_handler(TSCont contp, TSEvent event, void *edata)
+{
+ TSHttpStatus http_status = TS_HTTP_STATUS_NONE;
+ int status = 0;
+ StateInfo *state = nullptr;
+ TSMBuffer buf;
+ TSMLoc loc;
+
+ TSHttpTxn const txnp = static_cast<TSHttpTxn>(edata);
+ ConfigInfo *plugin_config = static_cast<ConfigInfo *>(TSContDataGet(contp));
+ switch (event) {
+ case TS_EVENT_HTTP_READ_REQUEST_HDR:
+ TSDebug(PLUGIN_TAG, "[%s] TS_EVENT_HTTP_READ_REQUEST_HDR", __FUNCTION__);
+ assert(false);
+ break;
+
+ case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE:
+ state = static_cast<StateInfo *>(TSUserArgGet(txnp, plugin_config->txn_slot));
+
+ // If the state has already gone, just move on
+ if (!state) {
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ break;
+ }
+
+ // get the cache status default to miss
+ if (TSHttpTxnCacheLookupStatusGet(txnp, &status) != TS_SUCCESS) {
+ status = TS_CACHE_LOOKUP_MISS;
+ TSDebug(PLUGIN_TAG_BAD, "[%s] TSHttpTxnCacheLookupStatusGet failed", __FUNCTION__);
+ }
+
+ if (TSHttpTxnIsInternal(txnp)) {
+ bool cache_fresh = (status == TS_CACHE_LOOKUP_HIT_FRESH);
+ TSDebug(PLUGIN_TAG, "[%s] {%u} CacheLookupComplete Internal fresh=%d", __FUNCTION__, state->req_info->key_hash, cache_fresh);
+
+ // We dont want our internal requests to ever hit cache
+ if (cache_fresh && state->intercept_request) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Set Cache to miss", __FUNCTION__, state->req_info->key_hash);
+ if (TSHttpTxnCacheLookupStatusSet(txnp, TS_CACHE_LOOKUP_MISS) != TS_SUCCESS) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} TSHttpTxnCacheLookupStatusSet failed", __FUNCTION__, state->req_info->key_hash);
+ }
+ } else if (cache_fresh) // I dont think this can happen
+ {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} cache fresh not in stripped or intercept", __FUNCTION__, state->req_info->key_hash);
+ }
+
+ TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+ free_state_info(state);
+ TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_REQUEST_HDR_HOOK, contp);
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ } else {
+ if (status == TS_CACHE_LOOKUP_HIT_STALE) {
+ // Get headers setup chi -- free chi when done
+ CachedHeaderInfo *chi = get_cached_header_info(state);
+ state->swr_active =
+ ((((state->txn_start - chi->date) + 1) < (chi->max_age + chi->stale_while_revalidate)) && chi->stale_while_revalidate);
+ state->sie_active = ((((state->txn_start - chi->date) + 1) < (chi->max_age + chi->stale_if_error)) && chi->stale_if_error);
+ state->over_max_memory = (aync_memory_total_get(plugin_config) > plugin_config->max_body_data_memory_usage);
+
+ TSDebug(PLUGIN_TAG, "[%s] {%u} CacheLookup Stale swr=%d sie=%d over=%d", __FUNCTION__, state->req_info->key_hash,
+ state->swr_active, state->sie_active, state->over_max_memory);
+ // see if we are using too much memory and if so do not swr/sie
+ if (state->over_max_memory) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} Over memory Usage %" PRId64, __FUNCTION__, state->req_info->key_hash,
+ aync_memory_total_get(plugin_config));
+ TSStatIntIncrement(state->plugin_config->rfc_stat_memory_over, 1);
+ }
+
+ if (state->swr_active) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} swr return stale - async refresh", __FUNCTION__, state->req_info->key_hash);
+ TSStatIntIncrement(plugin_config->rfc_stat_swr_hit, 1);
+ if (plugin_config->log_info.object && (plugin_config->log_info.all || plugin_config->log_info.stale_while_revalidate)) {
+ TSTextLogObjectWrite(plugin_config->log_info.object, "stale-while-revalidate: %ld - %ld < %ld + %ld [%s]",
+ state->txn_start, chi->date, chi->max_age, chi->stale_while_revalidate,
+ state->req_info->effective_url);
+ }
+ // send the data to the client
+ send_stale_response(state);
+ // do async if we are not over max
+ if (!state->over_max_memory) {
+ fetch_start(state, contp);
+ } else {
+ // since no fetch clean up state
+ TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+ free_state_info(state);
+ }
+ // reenable here
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ } else if (state->sie_active) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} sie wait response - return stale if 50x", __FUNCTION__, state->req_info->key_hash);
+ TSStatIntIncrement(plugin_config->rfc_stat_sie_hit, 1);
+ // lookup sync
+ if (!state->over_max_memory) {
+ fetch_start(state, contp);
+ } else // over max just send stale data reenable
+ {
+ send_stale_response(state);
+ // since no fetch clean up state and reenable
+ TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+ free_state_info(state);
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ }
+ // dont reenable here we are doing a sync call
+ } else {
+ // free state - reenable - had check
+ TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+ free_state_info(state);
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ }
+ TSfree(chi);
+ } else if (status != TS_CACHE_LOOKUP_HIT_FRESH) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} CacheLookup Miss/Skipped", __FUNCTION__, state->req_info->key_hash);
+
+ // this is just for stats
+ if (async_check_active(state->req_info->key_hash, plugin_config) != nullptr) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} not_stale aync in progress", __FUNCTION__, state->req_info->key_hash);
+ TSStatIntIncrement(plugin_config->rfc_stat_swr_miss_locked, 1);
+ }
+
+ // strip the async if we missed the internal fake cache lookup -- ats just misses ?
+ if (plugin_config->intercept_reroute) {
+ TSMBuffer buf;
+ TSMLoc hdr_loc;
+ TSHttpTxnClientReqGet(txnp, &buf, &hdr_loc);
+ if (strip_trailing_parameter(buf, hdr_loc)) {
+ TSDebug(PLUGIN_TAG_BAD, "[%s] {%u} missed fake internal cache lookup", __FUNCTION__, state->req_info->key_hash);
+ }
+ TSHandleMLocRelease(buf, TS_NULL_MLOC, hdr_loc);
+ }
+
+ // free state - reenable - had check
+ TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+ free_state_info(state);
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ } else // TS_CACHE_LOOKUP_HIT_FRESH
+ {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} CacheLookup Fresh", __FUNCTION__, state->req_info->key_hash);
+
+ // free state - reenable - had check
+ TSUserArgSet(state->txnp, state->plugin_config->txn_slot, nullptr);
+ free_state_info(state);
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ }
+ }
+ break;
+
+ case TS_EVENT_HTTP_SEND_REQUEST_HDR: {
+ TSDebug(PLUGIN_TAG, "[%s]: strip_trailing_parameter", __FUNCTION__);
+ TSHttpTxnServerReqGet(txnp, &buf, &loc);
+ strip_trailing_parameter(buf, loc);
+ TSHandleMLocRelease(buf, TS_NULL_MLOC, loc);
+
+ // reenable
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ }
+
+ break;
+
+ case TS_EVENT_HTTP_READ_RESPONSE_HDR:
+ // this should be internal request dont cache if valid sie error code -- no state variable
+ TSHttpTxnServerRespGet(txnp, &buf, &loc);
+ http_status = TSHttpHdrStatusGet(buf, loc);
+ if (valid_sie_status(http_status)) {
+ TSDebug(PLUGIN_TAG, "[%s] Set non-cachable %d", __FUNCTION__, http_status);
+ TSHttpTxnServerRespNoStoreSet(txnp, 1);
+ }
+ TSHandleMLocRelease(buf, TS_NULL_MLOC, loc);
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ break;
+
+ case TS_EVENT_HTTP_SEND_RESPONSE_HDR:
+ // add in the stale warning header -- no state variable
+ TSDebug(PLUGIN_TAG, "[%s] set warning header", __FUNCTION__);
+ TSHttpTxnClientRespGet(txnp, &buf, &loc);
+ if (!add_header(buf, loc, TS_MIME_FIELD_WARNING, HTTP_VALUE_STALE_WARNING)) {
+ TSError("stale_response [%s] error inserting header %s", __FUNCTION__, TS_MIME_FIELD_WARNING);
+ }
+ TSHandleMLocRelease(buf, TS_NULL_MLOC, loc);
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ break;
+
+ default:
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ break;
+ }
+
+ return 0;
+}
+
+ConfigInfo *
+parse_args(int argc, char const *argv[])
+{
+ if (argc <= 1) {
+ return nullptr;
+ }
+ ConfigInfo *plugin_config = new ConfigInfo();
+ int c;
+ optind = 1;
+ static const struct option longopts[] = {
+ {"log-all", no_argument, nullptr, 'a'},
+ {"log-stale-while-revalidate", no_argument, nullptr, 'b'},
+ {"log-stale-if-error", no_argument, nullptr, 'c'},
+ {"log-filename", required_argument, nullptr, 'd'},
+
+ {"force-stale-if-error", required_argument, nullptr, 'e'},
+ {"force-stale-while-revalidate", required_argument, nullptr, 'f'},
+ {"stale-if-error-default", required_argument, nullptr, 'g'},
+ {"stale-while-revalidate-default", required_argument, nullptr, 'h'},
+
+ {"intercept-reroute", no_argument, nullptr, 'i'},
+ {"max-memory-usage", required_argument, nullptr, 'j'},
+ {"force-parallel-async", no_argument, nullptr, 'k'},
+
+ // released a version that used "_" by mistake
+ {"force_stale_if_error", required_argument, nullptr, 'E'},
+ {"force_stale_while_revalidate", required_argument, nullptr, 'F'},
+ {"stale_if_error_default", required_argument, nullptr, 'G'},
+ {"stale_while_revalidate_default", required_argument, nullptr, 'H'},
+
+ {nullptr, 0, nullptr, 0 }
+ };
+
+ TSDebug(PLUGIN_TAG, "[%s] [%s]", __FUNCTION__, argv[1]);
+ while ((c = getopt_long(argc, const_cast<char *const *>(argv), "akref:EFGH:", longopts, nullptr)) != -1) {
+ switch (c) {
+ case 'a':
+ plugin_config->log_info.all = true;
+ break;
+ case 'b':
+ plugin_config->log_info.stale_while_revalidate = true;
+ break;
+ case 'c':
+ plugin_config->log_info.stale_if_error = true;
+ break;
+ case 'd':
+ plugin_config->log_info.filename = strdup(optarg);
+ break;
+
+ case 'e':
+ case 'E':
+ plugin_config->stale_if_error_override = atoi(optarg);
+ break;
+ case 'f':
+ case 'F':
+ plugin_config->stale_while_revalidate_override = atoi(optarg);
+ break;
+ case 'g':
+ case 'G':
+ plugin_config->stale_if_error_default = atoi(optarg);
+ break;
+ case 'h':
+ case 'H':
+ plugin_config->stale_while_revalidate_default = atoi(optarg);
+ break;
+
+ case 'i':
+ plugin_config->intercept_reroute = true;
+ break;
+
+ case 'j':
+ plugin_config->max_body_data_memory_usage = atoi(optarg);
+ break;
+
+ case 'k':
+ plugin_config->force_parallel_async = true;
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ if (plugin_config->log_info.all || plugin_config->log_info.stale_while_revalidate || plugin_config->log_info.stale_if_error) {
+ TSDebug(PLUGIN_TAG, "[%s] Logging to %s", __FUNCTION__, plugin_config->log_info.filename);
+ TSTextLogObjectCreate(plugin_config->log_info.filename, TS_LOG_MODE_ADD_TIMESTAMP, &(plugin_config->log_info.object));
+ }
+
+ TSDebug(PLUGIN_TAG, "[%s] global stale if error override = %d", __FUNCTION__, (int)plugin_config->stale_if_error_override);
+ TSDebug(PLUGIN_TAG, "[%s] global stale while revalidate override = %d", __FUNCTION__,
+ (int)plugin_config->stale_while_revalidate_override);
+ TSDebug(PLUGIN_TAG, "[%s] global stale if error default = %d", __FUNCTION__, (int)plugin_config->stale_if_error_default);
+ TSDebug(PLUGIN_TAG, "[%s] global stale while revalidate default = %d", __FUNCTION__,
+ (int)plugin_config->stale_while_revalidate_default);
+ TSDebug(PLUGIN_TAG, "[%s] global intercept reroute = %d", __FUNCTION__, (int)plugin_config->intercept_reroute);
+ TSDebug(PLUGIN_TAG, "[%s] global force parallel async = %d", __FUNCTION__, (int)plugin_config->force_parallel_async);
+ TSDebug(PLUGIN_TAG, "[%s] global max memory usage = %" PRId64, __FUNCTION__, plugin_config->max_body_data_memory_usage);
+
+ return plugin_config;
+}
+
+static void
+read_request_header_handler(TSHttpTxn const txnp, ConfigInfo *plugin_config)
+{
+ TSCont transaction_contp = TSContCreate(transaction_handler, nullptr);
+ TSContDataSet(transaction_contp, plugin_config);
+ // todo: move state create to not always happen -- issue: effective url string seems to change in dif states
+ StateInfo *state = create_state_info(txnp, transaction_contp);
+ TSUserArgSet(txnp, plugin_config->txn_slot, state);
+
+ if (TSHttpTxnIsInternal(txnp)) {
+ // This is insufficient if there are other plugins using TSHttpConnect
+ TSDebug(PLUGIN_TAG, "[%s] {%u} ReadRequestHdr Internal", __FUNCTION__, state->req_info->key_hash);
+ BodyData *pBody = intercept_check_request(state);
+ if (pBody) {
+ TSDebug(PLUGIN_TAG, "[%s] {%u} ReadRequestHdr Intercept", __FUNCTION__, state->req_info->key_hash);
+ // key hash will have whats in header here
+ serverInterceptSetup(txnp, pBody, plugin_config);
+ state->intercept_request = true;
+ } else {
+ // not sure this is needed since we wont serve intercept in this case
+ TSDebug(PLUGIN_TAG, "[%s] {%u} ReadRequestHdr add response hook", __FUNCTION__, state->req_info->key_hash);
+ // dont cache if valid sie status code to myself
+ TSHttpTxnHookAdd(txnp, TS_HTTP_READ_RESPONSE_HDR_HOOK, transaction_contp);
+ }
+ } else {
+ // should we use the data we just cached -- this doesnt seem to help
+ if (plugin_config->intercept_reroute) {
+ // see if we are in the middle of intercepting
+ if (async_intercept_active(state->req_info->key_hash, plugin_config)) {
+ // add the async to the end so we use the fake cached response
+ TSMBuffer buf;
+ TSMLoc hdr_loc;
+ TSHttpTxnClientReqGet(txnp, &buf, &hdr_loc);
+ add_trailing_parameter(buf, hdr_loc);
+ TSHandleMLocRelease(buf, TS_NULL_MLOC, hdr_loc);
+ TSDebug(PLUGIN_TAG, "[%s] {%u} add async parm to get fake cached item", __FUNCTION__, state->req_info->key_hash);
+ }
+ }
+ }
+
+ // always cache lookup
+ TSHttpTxnHookAdd(txnp, TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, transaction_contp);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+static int
+global_request_header_hook(TSCont contp, TSEvent event, void *edata)
+{
+ ConfigInfo *plugin_config = static_cast<ConfigInfo *>(TSContDataGet(contp));
+ TSHttpTxn const txnp = static_cast<TSHttpTxn>(edata);
+ read_request_header_handler(txnp, plugin_config);
+ TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
+ return TS_SUCCESS;
+}
+
+static void
+create_plugin_stats(ConfigInfo *plugin_config)
+{
+ plugin_config->rfc_stat_swr_hit =
+ TSStatCreate("stale_response.swr.hit", TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+ plugin_config->rfc_stat_swr_hit_skip =
+ TSStatCreate("stale_response.swr.hit.skip", TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+ plugin_config->rfc_stat_swr_miss_locked =
+ TSStatCreate("stale_response.swr.miss.locked", TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+ plugin_config->rfc_stat_sie_hit =
+ TSStatCreate("stale_response.sie.hit", TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+ plugin_config->rfc_stat_memory_over =
+ TSStatCreate("stale_response.memory.over", TS_RECORDDATATYPE_INT, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+void
+TSPluginInit(int argc, const char *argv[])
+{
+ TSPluginRegistrationInfo info;
+ info.plugin_name = const_cast<char *>(PLUGIN_TAG);
+ info.vendor_name = const_cast<char *>(VENDOR_NAME);
+ info.support_email = const_cast<char *>(SUPPORT_EMAIL);
+
+ if (TS_SUCCESS != TSPluginRegister(&info)) {
+ TSError("Plugin registration failed.");
+ return;
+ }
+ TSDebug(PLUGIN_TAG, "Plugin registration succeeded.");
+
+ TSMgmtString value = nullptr;
+ TSMgmtStringGet("proxy.config.http.server_session_sharing.pool", &value);
+ if (nullptr == value || 0 != strcasecmp(value, "global")) {
+ TSError("[stale-response] Server session pool must be set to 'global'");
+ assert(false);
+ }
+
+ // create the default ConfigInfo
+ ConfigInfo *plugin_config = parse_args(argc, argv);
+
+ // proxy.config.http.insert_age_in_response
+ if (TS_SUCCESS != TSUserArgIndexReserve(TS_USER_ARGS_TXN, PLUGIN_TAG, "reserve state info slot", &(plugin_config->txn_slot))) {
+ TSError("stale_response [%s] failed to user argument data. Plugin registration failed.", PLUGIN_TAG);
+ return;
+ }
+ TSCont main_contp = TSContCreate(global_request_header_hook, nullptr);
+ TSContDataSet(main_contp, plugin_config);
+ TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, main_contp);
+
+ create_plugin_stats(plugin_config);
+
+ TSDebug(PLUGIN_TAG, "[%s] Plugin Init Complete", __FUNCTION__);
+}
+
+/*-----------------------------------------------------------------------------------------------*/
+// Remap support.
+
+/**
+ * Remap initialization.
+ */
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+ CHECK_REMAP_API_COMPATIBILITY(api_info, errbuf, errbuf_size);
+ TSDebug(PLUGIN_TAG, "[%s] Plugin Remap Init Complete", __FUNCTION__);
+
+ return TS_SUCCESS;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char * /*errbuf */, int /* errbuf_size */)
+{
+ // second arg poses as the program name
+ --argc;
+ ++argv;
+ ConfigInfo *const plugin_config = parse_args(argc, const_cast<char const **>(argv));
+ *ih = static_cast<void *>(plugin_config);
+ if (TS_SUCCESS != TSUserArgIndexReserve(TS_USER_ARGS_TXN, PLUGIN_TAG, "reserve state info slot", &(plugin_config->txn_slot))) {
+ TSError("stale_response [%s] failed to user argument data. Plugin registration failed.", PLUGIN_TAG);
+ return TS_ERROR;
+ }
+ create_plugin_stats(plugin_config);
+ TSDebug(PLUGIN_TAG, "[%s] Plugin Remap New Instance Complete", __FUNCTION__);
+ return TS_SUCCESS;
+}
+
+void
+TSRemapDeleteInstance(void *ih)
+{
+ ConfigInfo *const plugin_config = static_cast<ConfigInfo *>(ih);
+ delete plugin_config;
+ TSDebug(PLUGIN_TAG, "[%s] Plugin Remap Delete Instance Complete", __FUNCTION__);
+}
+
+/**
+ * Remap entry point.
+ */
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo * /* rri */)
+{
+ ConfigInfo *plugin_config = static_cast<ConfigInfo *>(ih);
+ read_request_header_handler(txnp, plugin_config);
+ return TSREMAP_NO_REMAP;
+}
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/stale_response.h b/plugins/experimental/stale_response/stale_response.h
new file mode 100644
index 0000000000..9b642ba830
--- /dev/null
+++ b/plugins/experimental/stale_response/stale_response.h
@@ -0,0 +1,143 @@
+/** @file
+
+ Implements RFC 5861 (HTTP Cache-Control Extensions for Stale Content)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#pragma once
+
+#include "ts/apidefs.h"
+#include "ts_wrap.h"
+#include "ts/ts.h"
+
+#include <cstdint>
+#include <map>
+
+struct BodyData;
+
+using UintBodyMap = std::map<uint32_t, BodyData *>;
+
+const unsigned int c_hashSeed = 99991;
+extern const char PLUGIN_TAG[];
+extern const char PLUGIN_TAG_BAD[];
+
+EXT_DBG_CTL(PLUGIN_TAG)
+EXT_DBG_CTL(PLUGIN_TAG_BAD)
+
+struct LogInfo {
+ TSTextLogObject object = nullptr;
+ bool all = false;
+ bool stale_if_error = false;
+ bool stale_while_revalidate = false;
+ char const *filename = PLUGIN_TAG;
+};
+
+struct ConfigInfo {
+ ConfigInfo() : body_data{new UintBodyMap()}, body_data_mutex(TSMutexCreate()) {}
+ UintBodyMap *body_data = nullptr;
+ TSMutex body_data_mutex;
+ int64_t body_data_memory_usage = 0;
+ int txn_slot = 0;
+
+ bool intercept_reroute = false;
+ bool force_parallel_async = false;
+ int64_t max_body_data_memory_usage = c_default_max_body_data_memory_usage;
+
+ time_t stale_if_error_override = 0;
+ time_t stale_while_revalidate_override = 0;
+ time_t stale_if_error_default = 0;
+ time_t stale_while_revalidate_default = 0;
+
+ int rfc_stat_swr_hit = 0;
+ int rfc_stat_swr_hit_skip = 0;
+ int rfc_stat_swr_miss_locked = 0;
+ int rfc_stat_sie_hit = 0;
+ int rfc_stat_memory_over = 0;
+
+ LogInfo log_info;
+
+private:
+ static constexpr int64_t c_default_max_body_data_memory_usage = 1024 * 1024 * 1024; // default to 1 GB
+};
+
+struct CachedHeaderInfo {
+ time_t date;
+ time_t stale_while_revalidate;
+ time_t stale_if_error;
+ time_t max_age;
+};
+
+struct RequestInfo {
+ char *effective_url;
+ int effective_url_length;
+ TSMBuffer http_hdr_buf;
+ TSMLoc http_hdr_loc;
+ struct sockaddr *client_addr;
+ uint32_t key_hash;
+};
+
+struct ResponseInfo {
+ TSMBuffer http_hdr_buf;
+ TSMLoc http_hdr_loc;
+ TSHttpParser parser;
+ bool parsed;
+ TSHttpStatus status;
+};
+
+struct StateInfo {
+ StateInfo(TSHttpTxn txnp, TSCont contp)
+ : txnp{txnp}, transaction_contp{contp}, plugin_config{static_cast<ConfigInfo *>(TSContDataGet(contp))}
+ {
+ time(&this->txn_start);
+ }
+ TSHttpTxn txnp = nullptr;
+ TSCont transaction_contp = nullptr;
+ bool swr_active = false;
+ bool sie_active = false;
+ bool over_max_memory = false;
+ TSIOBuffer req_io_buf = nullptr;
+ TSIOBuffer resp_io_buf = nullptr;
+ TSIOBufferReader req_io_buf_reader = nullptr;
+ TSIOBufferReader resp_io_buf_reader = nullptr;
+ TSVIO r_vio = nullptr;
+ TSVIO w_vio = nullptr;
+ TSVConn vconn = nullptr;
+ RequestInfo *req_info = nullptr;
+ ResponseInfo *resp_info = nullptr;
+ time_t txn_start = 0;
+ ConfigInfo *plugin_config = nullptr;
+ char *pristine_url = nullptr;
+ BodyData *sie_body = nullptr;
+ BodyData *cur_save_body = nullptr;
+ bool intercept_request = false;
+};
+
+BodyData *async_check_active(uint32_t key_hash, ConfigInfo *plugin_config);
+bool async_check_and_add_active(uint32_t key_hash, ConfigInfo *plugin_config);
+bool async_remove_active(uint32_t key_hash, ConfigInfo *plugin_config);
+
+// 500, 502, 503, 504
+inline bool
+valid_sie_status(TSHttpStatus status)
+{
+ return ((status == 500) || ((status >= 502) && (status <= 504)));
+}
+
+/*-----------------------------------------------------------------------------------------------*/
diff --git a/plugins/experimental/stale_response/ts_wrap.h b/plugins/experimental/stale_response/ts_wrap.h
new file mode 100644
index 0000000000..ba7d4abddf
--- /dev/null
+++ b/plugins/experimental/stale_response/ts_wrap.h
@@ -0,0 +1,42 @@
+/** @file
+
+ Wrapper for ts.h, to allow for compatibility with multiple major versions.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#pragma once
+
+#include "ts/ts.h"
+
+#if TS_VERSION_MAJOR >= 10
+
+#define TSDebug(TAG_ID, ...) Dbg(stale_response_dbg_ctl_##TAG_ID, __VA_ARGS__)
+
+#define EXT_DBG_CTL(TAG_ID) extern DbgCtl stale_response_dbg_ctl_##TAG_ID;
+
+#define DEF_DBG_CTL(TAG_ID) DbgCtl stale_response_dbg_ctl_##TAG_ID{TAG_ID};
+
+#else
+
+#define EXT_DBG_CTL(TAG_ID)
+
+#define DEF_DBG_CTL(TAG_ID)
+
+#endif
diff --git a/plugins/experimental/CMakeLists.txt b/plugins/experimental/stale_response/unit_tests/CMakeLists.txt
similarity index 54%
copy from plugins/experimental/CMakeLists.txt
copy to plugins/experimental/stale_response/unit_tests/CMakeLists.txt
index a01426dc15..f1d73dae82 100644
--- a/plugins/experimental/CMakeLists.txt
+++ b/plugins/experimental/stale_response/unit_tests/CMakeLists.txt
@@ -15,27 +15,12 @@
#
#######################
-add_subdirectory(access_control)
-add_subdirectory(block_errors)
-add_subdirectory(cache_fill)
-add_subdirectory(cert_reporting_tool)
-add_subdirectory(cookie_remap)
-add_subdirectory(custom_redirect)
-add_subdirectory(fq_pacing)
-add_subdirectory(geoip_acl)
-add_subdirectory(header_freq)
-add_subdirectory(hook-trace)
-add_subdirectory(http_stats)
-add_subdirectory(icap)
-add_subdirectory(inliner)
-add_subdirectory(memcache)
-add_subdirectory(memory_profile)
-add_subdirectory(money_trace)
-add_subdirectory(mp4)
-add_subdirectory(rate_limit)
-add_subdirectory(redo_cache_lookup)
-add_subdirectory(sslheaders)
-add_subdirectory(stream_editor)
-add_subdirectory(system_stats)
-add_subdirectory(tls_bridge)
-add_subdirectory(url_sig)
+add_executable(
+ test_stale_response unit_test_main.cc test_DirectiveParser.cc test_BodyData.cc
+ ${PROJECT_SOURCE_DIR}/DirectiveParser.cc
+)
+
+target_include_directories(test_stale_response PRIVATE "${PROJECT_SOURCE_DIR}")
+target_link_libraries(test_stale_response PRIVATE ts::tsutil libswoc::libswoc catch2::catch2)
+
+add_test(NAME test_stale_response COMMAND test_stale_response)
diff --git a/plugins/experimental/stale_response/unit_tests/test_BodyData.cc b/plugins/experimental/stale_response/unit_tests/test_BodyData.cc
new file mode 100644
index 0000000000..0fb4d52a16
--- /dev/null
+++ b/plugins/experimental/stale_response/unit_tests/test_BodyData.cc
@@ -0,0 +1,62 @@
+/*
+ * 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 "BodyData.h"
+
+#include <catch.hpp>
+#include <string>
+#include <string_view>
+
+DEF_DBG_CTL(PLUGIN_TAG_BODY)
+
+std::string_view DATA_PACKET0{"The quick brown fox jumps over the lazy dog"};
+std::string_view DATA_PACKET1{"Now is the time for all good men to come to the aid of their country"};
+
+TEST_CASE("Body Data")
+{
+ BodyData pBody;
+ REQUIRE(pBody.getSize() == 0);
+ REQUIRE(pBody.getChunkCount() == 0);
+ char const *pData = 0;
+ int64_t dataLen = 0;
+ REQUIRE(pBody.getChunk(0, &pData, &dataLen) == false);
+
+ // First chunk.
+ pBody.addChunk(DATA_PACKET0.data(), DATA_PACKET0.size());
+ REQUIRE(pBody.getSize() == static_cast<int64_t>(DATA_PACKET0.size()));
+ REQUIRE(pBody.getChunkCount() == 1);
+ pBody.getChunk(0, &pData, &dataLen);
+ std::string dataRead0(pData, dataLen);
+ std::string dataOrig0(DATA_PACKET0.data(), DATA_PACKET0.size());
+ REQUIRE(dataRead0 == dataOrig0);
+
+ // Second chunk.
+ pBody.addChunk(DATA_PACKET1.data(), DATA_PACKET1.size());
+ REQUIRE(pBody.getSize() == static_cast<int64_t>(DATA_PACKET0.size() + DATA_PACKET1.size()));
+ REQUIRE(pBody.getChunkCount() == 2);
+ pBody.getChunk(1, &pData, &dataLen);
+ std::string dataRead1(pData, dataLen);
+ std::string dataOrig1(DATA_PACKET1.data(), DATA_PACKET1.size());
+ REQUIRE(dataRead1 == dataOrig1);
+
+ REQUIRE(pBody.removeChunk(0) == true);
+ REQUIRE(pBody.removeChunk(0) == false);
+ REQUIRE(pBody.removeChunk(1) == true);
+ REQUIRE(pBody.removeChunk(1) == false);
+ REQUIRE(pBody.removeChunk(97) == false);
+}
diff --git a/plugins/experimental/stale_response/unit_tests/test_DirectiveParser.cc b/plugins/experimental/stale_response/unit_tests/test_DirectiveParser.cc
new file mode 100644
index 0000000000..6556db5692
--- /dev/null
+++ b/plugins/experimental/stale_response/unit_tests/test_DirectiveParser.cc
@@ -0,0 +1,103 @@
+/*
+ * 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 "DirectiveParser.h"
+
+#include <catch.hpp>
+
+TEST_CASE("DirectiveParser Constructor")
+{
+ SECTION("Default constructor")
+ {
+ DirectiveParser parser;
+ REQUIRE(parser.get_max_age() == -1);
+ REQUIRE(parser.get_stale_while_revalidate() == -1);
+ REQUIRE(parser.get_stale_if_error() == -1);
+ }
+ SECTION("max-age")
+ {
+ DirectiveParser parser{"max-age=123"};
+ REQUIRE(parser.get_max_age() == 123);
+ REQUIRE(parser.get_stale_while_revalidate() == -1);
+ REQUIRE(parser.get_stale_if_error() == -1);
+ }
+ SECTION("stale-while-revalidate")
+ {
+ DirectiveParser parser{"stale-while-revalidate=123"};
+ REQUIRE(parser.get_max_age() == -1);
+ REQUIRE(parser.get_stale_while_revalidate() == 123);
+ REQUIRE(parser.get_stale_if_error() == -1);
+ }
+ SECTION("stale-if-error")
+ {
+ DirectiveParser parser{"stale-if-error=123"};
+ REQUIRE(parser.get_max_age() == -1);
+ REQUIRE(parser.get_stale_while_revalidate() == -1);
+ REQUIRE(parser.get_stale_if_error() == 123);
+ }
+ SECTION("other")
+ {
+ DirectiveParser parser{"s-maxage=123"};
+ REQUIRE(parser.get_max_age() == -1);
+ REQUIRE(parser.get_stale_while_revalidate() == -1);
+ REQUIRE(parser.get_stale_if_error() == -1);
+ }
+ SECTION("multiple")
+ {
+ DirectiveParser parser{"max-age=123, stale-while-revalidate=456, stale-if-error=789"};
+ REQUIRE(parser.get_max_age() == 123);
+ REQUIRE(parser.get_stale_while_revalidate() == 456);
+ REQUIRE(parser.get_stale_if_error() == 789);
+ }
+ SECTION("multiple with noise")
+ {
+ DirectiveParser parser{"max-age=123, s-maxage=456, stale-while-revalidate=789, must-understand, stale-if-error=012, public"};
+ REQUIRE(parser.get_max_age() == 123);
+ REQUIRE(parser.get_stale_while_revalidate() == 789);
+ REQUIRE(parser.get_stale_if_error() == 012);
+ }
+ SECTION("without commas")
+ {
+ DirectiveParser parser{"max-age=123 s-maxage=456 stale-while-revalidate=789 must-understand stale-if-error=012 public"};
+ REQUIRE(parser.get_max_age() == 123);
+ REQUIRE(parser.get_stale_while_revalidate() == 789);
+ REQUIRE(parser.get_stale_if_error() == 012);
+ }
+}
+
+TEST_CASE("DirectiveParser::merge")
+{
+ SECTION("Other replaces this")
+ {
+ DirectiveParser self{"max-age=123, stale-while-revalidate=456, stale-if-error=789"};
+ DirectiveParser other{"max-age=321, stale-while-revalidate=654, stale-if-error=987"};
+ self.merge(other);
+ REQUIRE(self.get_max_age() == 321);
+ REQUIRE(self.get_stale_while_revalidate() == 654);
+ REQUIRE(self.get_stale_if_error() == 987);
+ }
+ SECTION("Other unset does not replace this")
+ {
+ DirectiveParser self{"max-age=123, stale-while-revalidate=456, stale-if-error=789"};
+ DirectiveParser other{"max-age=321"};
+ self.merge(other);
+ REQUIRE(self.get_max_age() == 321);
+ REQUIRE(self.get_stale_while_revalidate() == 456);
+ REQUIRE(self.get_stale_if_error() == 789);
+ }
+}
diff --git a/plugins/experimental/stale_response/unit_tests/unit_test_main.cc b/plugins/experimental/stale_response/unit_tests/unit_test_main.cc
new file mode 100644
index 0000000000..6aed3a6309
--- /dev/null
+++ b/plugins/experimental/stale_response/unit_tests/unit_test_main.cc
@@ -0,0 +1,25 @@
+/** @file
+
+ This file used for catch based tests. It is the main() stub.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#define CATCH_CONFIG_MAIN
+#include "catch.hpp"
diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response.test.py b/tests/gold_tests/pluginTest/stale_response/stale_response.test.py
new file mode 100644
index 0000000000..3d458b2f95
--- /dev/null
+++ b/tests/gold_tests/pluginTest/stale_response/stale_response.test.py
@@ -0,0 +1,173 @@
+'''
+Verify correct stale_response plugin behavior
+'''
+# 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.
+
+from enum import Enum
+import os
+
+Test.Summary = '''
+Verify correct stale_response plugin behavior
+'''
+
+Test.SkipUnless(Condition.PluginExists('stale_response.so'),)
+
+
+class OptionType(Enum):
+ """Describe what options to pass to the plugin."""
+
+ NONE = 0
+ DEFAULT_DIRECTIVES = 1
+ FORCE_SWR = 2
+ FORCE_SIE = 2
+
+
+class TestStaleResponse:
+ """Verify correct stale_response.so plugin behavior."""
+
+ _replay_file: str
+ _ts_counter: int = 0
+ _server_counter: int = 0
+ _client_counter: int = 0
+
+ def __init__(self, option_type: OptionType, is_global: bool) -> None:
+ """Initialize the test.
+
+ :param option_type: The type of options to pass to the stale_response plugin.
+ """
+ self._option_type = option_type
+ self._is_global = is_global
+
+ plugin_type_description = "global" if is_global else "per-remap"
+ if option_type == OptionType.NONE:
+ self._replay_file = "stale_response_no_default.replay.yaml"
+ option_description = f"no stale_response plugin options: {plugin_type_description}"
+ elif option_type == OptionType.DEFAULT_DIRECTIVES:
+ self._replay_file = "stale_response_with_defaults.replay.yaml"
+ option_description = f"--stale-while-revalidate-default 30 --stale-if-error-default 30: {plugin_type_description}"
+ elif option_type == OptionType.FORCE_SWR:
+ self._replay_file = "stale_response_with_force_swr.replay.yaml"
+ option_description = f"--force-stale-while-revalidate 30: {plugin_type_description}"
+ elif option_type == OptionType.FORCE_SIE:
+ self._replay_file = "stale_response_with_force_sie.replay.yaml"
+ option_description = f"--force-stale-if-error 30: {plugin_type_description}"
+
+ tr = Test.AddTestRun(f"stale_response.so Options: {option_description}")
+
+ self.setupOriginServer(tr)
+ self.setupTS()
+ self.setupClient(tr)
+ self.verify_plugin_log()
+
+ def setupOriginServer(self, tr: 'TestRun') -> None:
+ """ Configure the server.
+
+ :param tr: The test run to add the server to.
+ """
+ name = f'server_{TestStaleResponse._server_counter}'
+ TestStaleResponse._server_counter += 1
+ self._server = tr.AddVerifierServerProcess(name, self._replay_file)
+
+ def setupTS(self) -> None:
+ """Configure the traffic server.
+
+ ATS is not configured for a TestRun because we need it to last longer to
+ ensure that the plugin's log is created.
+ """
+ name = f'ts_{TestStaleResponse._ts_counter}'
+ TestStaleResponse._ts_counter += 1
+ ts = Test.MakeATSProcess(name)
+ self._ts = ts
+
+ log_path = os.path.join(ts.Variables.LOGDIR, 'stale_responses.log')
+ ts.Disk.File(log_path, id='stale_responses_log')
+
+ remap_plugin_config = ""
+ if self._is_global:
+ plugin_command = 'stale_response.so --log-all --log-filename stale_responses'
+ if self._option_type == OptionType.DEFAULT_DIRECTIVES:
+ plugin_command += ' --stale-while-revalidate-default 30 --stale-if-error-default 30'
+ elif self._option_type == OptionType.FORCE_SWR:
+ plugin_command += ' --force-stale-while-revalidate 30'
+ elif self._option_type == OptionType.FORCE_SIE:
+ plugin_command += ' --force-stale-if-error 30'
+ ts.Disk.plugin_config.AddLine(plugin_command)
+ else:
+ # Configure the stale_response plugin for the remap rule.
+ remap_plugin_config = "@plugin=stale_response.so @pparam=--log-all @pparam=--log-filename @pparam=stale_responses"
+ if self._option_type == OptionType.DEFAULT_DIRECTIVES:
+ remap_plugin_config += ' @pparam=--stale-while-revalidate-default @pparam=30 @pparam=--stale-if-error-default @pparam=30'
+ elif self._option_type == OptionType.FORCE_SWR:
+ remap_plugin_config += ' @pparam=--force-stale-while-revalidate @pparam=30'
+ elif self._option_type == OptionType.FORCE_SIE:
+ remap_plugin_config += ' @pparam=--force-stale-if-error @pparam=30'
+
+ ts.Disk.records_config.update(
+ {
+ "proxy.config.diags.debug.enabled": 1,
+ "proxy.config.diags.debug.tags": "http|stale_response",
+ "proxy.config.http.server_session_sharing.pool": "global",
+ # Turn off negative revalidating so that we can test stale-if-error.
+ "proxy.config.http.negative_revalidating_enabled": 0,
+ })
+ ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.http_port}/ {remap_plugin_config}")
+
+ def setupClient(self, tr: 'TestRun') -> None:
+
+ name = f'client_{TestStaleResponse._client_counter}'
+ TestStaleResponse._client_counter += 1
+ p = tr.AddVerifierClientProcess(
+ name, self._replay_file, http_ports=[self._ts.Variables.port], other_args='--thread-limit 1')
+ p.StartBefore(self._server)
+ p.StartBefore(self._ts)
+ p.StillRunningAfter = self._ts
+
+ def verify_plugin_log(self) -> None:
+ """Verify the contents of the stale_response plugin log."""
+ tr = Test.AddTestRun("Verify stale_response plugin log")
+ name = f'log_waiter_{TestStaleResponse._ts_counter}'
+ log_waiter = tr.Processes.Process(name)
+ log_waiter.Command = 'sleep 30'
+ if self._option_type == OptionType.FORCE_SWR:
+ log_waiter.Ready = When.FileContains(self._ts.Disk.stale_responses_log.Name, "stale-while-revalidate:")
+ self._ts.Disk.stale_responses_log.Content += Testers.ContainsExpression(
+ "stale-while-revalidate:.*stale.jpeg", "Verify stale-while-revalidate directive is logged")
+ elif self._option_type == OptionType.FORCE_SIE:
+ log_waiter.Ready = When.FileContains(self._ts.Disk.stale_responses_log.Name, "stale-if-error:")
+ self._ts.Disk.stale_responses_log.Content += Testers.ContainsExpression(
+ "stale-if-error:.*error.jpeg", "Verify stale-if-error directive is logged")
+ else:
+ log_waiter.Ready = When.FileContains(self._ts.Disk.stale_responses_log.Name, "stale-if-error:")
+ self._ts.Disk.stale_responses_log.Content += Testers.ContainsExpression(
+ "stale-while-revalidate:.*stale.jpeg", "Verify stale-while-revalidate directive is logged")
+ self._ts.Disk.stale_responses_log.Content += Testers.ContainsExpression(
+ "stale-if-error:.*error.jpeg", "Verify stale-if-error directive is logged")
+ p = tr.Processes.Default
+ p.Command = 'echo "Waiting upon the stale response log."'
+ p.StartBefore(log_waiter)
+ p.StillRunningAfter = self._ts
+
+
+TestStaleResponse(OptionType.NONE, is_global=True)
+TestStaleResponse(OptionType.DEFAULT_DIRECTIVES, is_global=True)
+TestStaleResponse(OptionType.FORCE_SWR, is_global=True)
+TestStaleResponse(OptionType.FORCE_SIE, is_global=True)
+
+TestStaleResponse(OptionType.NONE, is_global=False)
+TestStaleResponse(OptionType.DEFAULT_DIRECTIVES, is_global=False)
+TestStaleResponse(OptionType.FORCE_SWR, is_global=False)
+TestStaleResponse(OptionType.FORCE_SIE, is_global=False)
diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_no_default.replay.yaml b/tests/gold_tests/pluginTest/stale_response/stale_response_no_default.replay.yaml
new file mode 100644
index 0000000000..35d72c5973
--- /dev/null
+++ b/tests/gold_tests/pluginTest/stale_response/stale_response_no_default.replay.yaml
@@ -0,0 +1,175 @@
+# 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.
+
+# This replay file tests the stale response plugin when no
+# --stale-while-revalidate-default and no --stale-if-error-default are
+# specified.
+
+sessions:
+
+- transactions:
+
+ - client-request:
+ method: GET
+ url: /pictures/stale.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, first-request ]
+
+ # Note: only a 1 second max-age.
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, image/jpeg ]
+ - [ Content-Length, 100 ]
+ - [ Connection, keep-alive ]
+ - [ Cache-Control, max-age=1 ]
+ - [ X-Response, first-response ]
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ Cache-Control, { value: max-age=1, as: equal } ]
+ - [ X-Response, { value: first-response, as: equal } ]
+
+ - client-request:
+
+ # Now age it out.
+ delay: 2s
+
+ method: GET
+ url: /pictures/stale.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, second-request ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, image/jpeg ]
+ - [ Content-Length, 100 ]
+ - [ Connection, keep-alive ]
+ # Note the stale-while-revalidate for the third response (not this).
+ # Also, verify that we can use comma separated directives.
+ - [ Cache-Control, "max-age=1, stale-while-revalidate=30" ]
+ - [ X-Response, second-response ]
+
+ # We better have gone back to the origin and gotten second-response.
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Response, { value: second-response, as: equal } ]
+
+ - client-request:
+
+ # Now age it out, but expect stale-while-revalidate to override.
+ delay: 2s
+
+ method: GET
+ url: /pictures/stale.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, third-request ]
+
+ # We don't expect the request to go to the origin.
+ server-response:
+ status: 404
+ reason: Not Found
+ headers:
+ fields:
+ - [ X-Response, third-response ]
+
+ # Expect the stale second response because of stale-while-revalidate.
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Response, { value: second-response, as: equal } ]
+
+ # Test stale-if-error.
+ - client-request:
+
+ method: GET
+ url: /pictures/error.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, fourth-request ]
+
+ # Populate a cached response for error.jpeg with stale-if-error.
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, image/jpeg ]
+ - [ Content-Length, 100 ]
+ - [ Connection, keep-alive ]
+ - [ Cache-Control, max-age=1 stale-if-error=30 ]
+ - [ X-Response, fourth-response ]
+
+ # We better have gone back to the origin and gotten second-response.
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Response, { value: fourth-response, as: equal } ]
+
+ # Now, age out the error.jpeg but reply with a 500. Expect the stale cached response.
+ - client-request:
+
+ delay: 2s
+
+ method: GET
+ url: /pictures/error.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, fifth-request ]
+
+ # Populate a cached response for error.jpeg with stale-if-error.
+ server-response:
+ status: 500
+ reason: Internal Server Error
+ headers:
+ fields:
+ - [ X-Response, fifth-response ]
+
+ # Expect the stale response due to stale-if-error.
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Response, { value: fourth-response, as: equal } ]
diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_with_defaults.replay.yaml b/tests/gold_tests/pluginTest/stale_response/stale_response_with_defaults.replay.yaml
new file mode 100644
index 0000000000..d3523292b8
--- /dev/null
+++ b/tests/gold_tests/pluginTest/stale_response/stale_response_with_defaults.replay.yaml
@@ -0,0 +1,144 @@
+# 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.
+
+# This replay file tests the stale response plugin when there are
+# --stale-while-revalidate-default and --stale-if-error-default specified.
+
+sessions:
+
+- transactions:
+
+ - client-request:
+ method: GET
+ url: /pictures/stale.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, first-request ]
+
+ # Note: only a 1 second max-age.
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, image/jpeg ]
+ - [ Content-Length, 100 ]
+ - [ Connection, keep-alive ]
+ - [ Cache-Control, "max-age=1" ]
+ - [ X-Response, first-response ]
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ Cache-Control, { value: max-age=1, as: equal } ]
+ - [ X-Response, { value: first-response, as: equal } ]
+
+ - client-request:
+
+ # Now age it out.
+ delay: 2s
+
+ method: GET
+ url: /pictures/stale.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, second-request ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, image/jpeg ]
+ - [ Content-Length, 100 ]
+ - [ Connection, keep-alive ]
+ - [ Cache-Control, max-age=1 ]
+ - [ X-Response, second-response ]
+
+ # Expect the cached response because of the default stale-while-revalidate.
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Response, { value: first-response, as: equal } ]
+
+
+ # Test stale-if-error.
+ - client-request:
+
+ method: GET
+ url: /pictures/error.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, third-request ]
+
+ # Populate a cached response for error.jpeg with no stale-if-error.
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, image/jpeg ]
+ - [ Content-Length, 100 ]
+ - [ Connection, keep-alive ]
+ # Explicitly turn off stale-while-revalidate so we can test the default stale-if-error.
+ - [ Cache-Control, "max-age=1, stale-while-revalidate=0" ]
+ - [ X-Response, third-response ]
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Response, { value: third-response, as: equal } ]
+
+ # Now, age out the error.jpeg but reply with a 500. Expect the stale cached response.
+ - client-request:
+
+ delay: 2s
+
+ method: GET
+ url: /pictures/error.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, fourth-request ]
+
+ # Populate a cached response for error.jpeg with stale-if-error.
+ server-response:
+ status: 500
+ reason: Internal Server Error
+ headers:
+ fields:
+ - [ X-Response, fourth-response ]
+
+ # Expect the stale response due to default stale-if-error.
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Response, { value: third-response, as: equal } ]
diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml b/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml
new file mode 100644
index 0000000000..10a8cd68f2
--- /dev/null
+++ b/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_sie.replay.yaml
@@ -0,0 +1,82 @@
+# 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.
+
+# This replay file tests the stale response plugin when
+# --stale-if-error-default is configured.
+
+sessions:
+
+- transactions:
+
+ # Test stale-if-error.
+ - client-request:
+
+ method: GET
+ url: /pictures/error.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, first-request ]
+
+ # Populate a cached response for error.jpeg with stale-if-error.
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, image/jpeg ]
+ - [ Content-Length, 100 ]
+ - [ Connection, keep-alive ]
+ # Configure a small stale-if-error.
+ - [ Cache-Control, max-age=1 stale-if-error=1 ]
+ - [ X-Response, first-response ]
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Response, { value: first-response, as: equal } ]
+
+ # Now, age out the error.jpeg but reply with a 500. Expect the stale cached response.
+ - client-request:
+
+ delay: 3s
+
+ method: GET
+ url: /pictures/error.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, second-request ]
+
+ # Populate a cached response for error.jpeg with stale-if-error.
+ server-response:
+ status: 500
+ reason: Internal Server Error
+ headers:
+ fields:
+ - [ X-Response, second-response ]
+
+ # Expect the stale response due to stale-if-error.
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Response, { value: first-response, as: equal } ]
diff --git a/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml b/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml
new file mode 100644
index 0000000000..a980bb5010
--- /dev/null
+++ b/tests/gold_tests/pluginTest/stale_response/stale_response_with_force_swr.replay.yaml
@@ -0,0 +1,86 @@
+# 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.
+
+# This replay file tests the stale response plugin when
+# --force-stale-while-revalidate is configured.
+
+sessions:
+
+- transactions:
+
+ - client-request:
+ method: GET
+ url: /pictures/stale.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, first-request ]
+
+ # Note: only a 1 second max-age.
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, image/jpeg ]
+ - [ Content-Length, 100 ]
+ - [ Connection, keep-alive ]
+ # The low stale-while-revalidate should be overridden by
+ # --force-stale-while-revalidate.
+ - [ Cache-Control, max-age=1 stale-while-revalidate=1 ]
+ - [ X-Response, first-response ]
+
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ Cache-Control, { value: "max-age=1 stale-while-revalidate=1", as: equal } ]
+ - [ X-Response, { value: first-response, as: equal } ]
+
+ - client-request:
+
+ # Now age it out.
+ delay: 3s
+
+ method: GET
+ url: /pictures/stale.jpeg
+ version: '1.1'
+ headers:
+ fields:
+ - [ Host, www.example.com ]
+ - [ Content-Type, image/jpeg ]
+ - [ uuid, second-request ]
+
+ server-response:
+ status: 200
+ reason: OK
+ headers:
+ fields:
+ - [ Content-Type, image/jpeg ]
+ - [ Content-Length, 100 ]
+ - [ Connection, keep-alive ]
+ - [ Cache-Control, max-age=1 ]
+ - [ X-Response, second-response ]
+
+ # Ensure that the forced larger stale-while-revalidate is used and we
+ # therefore get the cached response.
+ proxy-response:
+ status: 200
+ headers:
+ fields:
+ - [ X-Response, { value: first-response, as: equal } ]