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 f781dfe384 escalate: Add x-escalate-redirect header (#1092) (#12447)
f781dfe384 is described below
commit f781dfe38494afbb7011723eb06b2c9543b7c908
Author: Brian Neradt <[email protected]>
AuthorDate: Fri Aug 15 19:19:12 2025 -0500
escalate: Add x-escalate-redirect header (#1092) (#12447)
If the escalate.so plugin escalates a request, add the
x-escalate-redirect header as an indicator that it did so. Adding the
header can be disabled via: @pparam=--no-redirect-header
---
doc/admin-guide/plugins/escalate.en.rst | 17 ++++++-
plugins/escalate/escalate.cc | 30 +++++++++++-
.../pluginTest/escalate/escalate.test.py | 56 +++++++++++++++-------
3 files changed, 85 insertions(+), 18 deletions(-)
diff --git a/doc/admin-guide/plugins/escalate.en.rst
b/doc/admin-guide/plugins/escalate.en.rst
index bf65ee3f13..945083ceef 100644
--- a/doc/admin-guide/plugins/escalate.en.rst
+++ b/doc/admin-guide/plugins/escalate.en.rst
@@ -43,6 +43,14 @@ when the origin server in the remap rule returns a 401,
This option sends the "pristine" Host: header (eg, the Host: header
that the client sent) to the escalated request.
+@pparam=--no-redirect-header
+ Controls whether to add the x-escalate-redirect header to escalated requests.
+ When enabled (default), the plugin adds an x-escalate-redirect header with
value "1"
+ to the client request when it escalates to a different origin server. This
header
+ can be used by downstream systems to identify requests that have been
escalated.
+ The header is only added if it doesn't already exist in the request.
+ Use --no-redirect-header to disable adding the x-escalate-redirect header.
+
@pparam=--escalate-non-get-methods
In general, the escalate plugin is used with a failover origin that serves a
cached backup of the original content. As a result, the default behavior is
@@ -68,10 +76,17 @@ With this line in :file:`remap.config` ::
Traffic Server would accept a request for ``cdn.example.com`` and, on a cache
miss, proxy the
request to ``origin.example.com``. If the response code from that server is a
401, 404, 410,
or 502, then Traffic Server would proxy the request to
``second-origin.example.com``, using a
-Host: header of ``cdn.example.com``.
+Host: header of ``cdn.example.com``. Additionally, an x-escalate-redirect
header with value "1"
+will be added to the escalated request.
+
+To disable adding the x-escalate-redirect header, use::
+
+ map cdn.example.com origin.example.com \
+ @plugin=escalate.so @pparam=401,404,410,502:second-origin.example.com
@pparam=--no-redirect-header
By default, only GET requests are escalated. To escalate non-GET requests as
well, you can use::
map cdn.example.com origin.example.com \
@plugin=escalate.so @pparam=401,404,410,502:second-origin.example.com
@pparam=--escalate-non-get-methods
+
diff --git a/plugins/escalate/escalate.cc b/plugins/escalate/escalate.cc
index 17ff8a40e3..67b6d77518 100644
--- a/plugins/escalate/escalate.cc
+++ b/plugins/escalate/escalate.cc
@@ -38,7 +38,8 @@
// Constants and some declarations
-const char PLUGIN_NAME[] = "escalate";
+const char PLUGIN_NAME[] = "escalate";
+const char REDIRECT_HEADER[] = "x-escalate-redirect";
static DbgCtl dbg_ctl{PLUGIN_NAME};
@@ -72,6 +73,7 @@ struct EscalationState {
StatusMapType status_map;
bool use_pristine = false;
bool escalate_non_get_methods = false;
+ bool add_redirect_header = true;
};
// Little helper function, to update the Host portion of a URL, and stringify
the result.
@@ -208,6 +210,30 @@ EscalateResponse(TSCont cont, TSEvent event, void *edata)
// Now update the Redirect URL, if set
if (url_str) {
TSHttpTxnRedirectUrlSet(txn, url_str, url_len); // Transfers ownership
+
+ // Add our x-escalate-redirect header marker if it doesn't already exist
and the option is enabled.
+ if (es->add_redirect_header) {
+ TSMBuffer bufp;
+ TSMLoc hdr_loc, field_loc;
+
+ if (TS_SUCCESS == TSHttpTxnClientReqGet(txn, &bufp, &hdr_loc)) {
+ field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, REDIRECT_HEADER,
sizeof(REDIRECT_HEADER) - 1);
+ if (field_loc == nullptr) {
+ if (TSMimeHdrFieldCreateNamed(bufp, hdr_loc, REDIRECT_HEADER,
sizeof(REDIRECT_HEADER) - 1, &field_loc) == TS_SUCCESS) {
+ if (TSMimeHdrFieldValueStringInsert(bufp, hdr_loc, field_loc, -1,
"1", 1) == TS_SUCCESS) {
+ TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc);
+ }
+ TSHandleMLocRelease(bufp, hdr_loc, field_loc);
+ Dbg(dbg_ctl, "Added x-escalate-redirect header to the client
request.");
+ }
+ } else {
+ Dbg(dbg_ctl, "x-escalate-redirect header already exists, not
adding.");
+ }
+ TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc);
+ } else {
+ TSError("[%s] Failed to get client request header to add
x-escalate-redirect header", PLUGIN_NAME);
+ }
+ }
}
// Set the transaction free ...
@@ -234,6 +260,8 @@ TSRemapNewInstance(int argc, char *argv[], void **instance,
char *errbuf, int er
// Ugly, but we set the precedence before with non-command line parsing of
args
if (0 == strncasecmp(argv[i], "--pristine", 10)) {
es->use_pristine = true;
+ } else if (0 == strncasecmp(argv[i], "--no-redirect-header", 20)) {
+ es->add_redirect_header = false;
} else if (0 == strncasecmp(argv[i], "--escalate-non-get-methods", 26)) {
es->escalate_non_get_methods = true;
} else {
diff --git a/tests/gold_tests/pluginTest/escalate/escalate.test.py
b/tests/gold_tests/pluginTest/escalate/escalate.test.py
index bd3e5675fc..a13f977c4b 100644
--- a/tests/gold_tests/pluginTest/escalate/escalate.test.py
+++ b/tests/gold_tests/pluginTest/escalate/escalate.test.py
@@ -34,31 +34,39 @@ class EscalateTest:
_replay_original_file: str = 'escalate_original.replay.yaml'
_replay_failover_file: str = 'escalate_failover.replay.yaml'
+ _process_counter: int = 0
- def __init__(self):
- '''Configure the test run.'''
- tr = Test.AddTestRun('Test escalate plugin.')
+ def __init__(self, disable_redirect_header: bool = False) -> None:
+ '''Configure the test run.
+ :param disable_redirect_header: Whether to use --no-redirect-header.
+ '''
+ tr = Test.AddTestRun(f'Test escalate plugin.
disable_redirect_header={disable_redirect_header}')
self._setup_dns(tr)
- self._setup_servers(tr)
- self._setup_ts(tr)
+ self._setup_servers(tr, disable_redirect_header)
+ self._setup_ts(tr, disable_redirect_header)
self._setup_client(tr)
+ EscalateTest._process_counter += 1
- def _setup_dns(self, tr: 'Process') -> None:
+ def _setup_dns(self, tr: 'TestRun') -> None:
'''Set up the DNS server.
:param tr: The test run to add the DNS server to.
'''
- self._dns = tr.MakeDNServer(f"dns", default='127.0.0.1')
+ process_name = f"dns_{EscalateTest._process_counter}"
+ self._dns = tr.MakeDNServer(process_name, default='127.0.0.1')
- def _setup_servers(self, tr: 'Process') -> None:
+ def _setup_servers(self, tr: 'TestRun', disable_redirect_header: bool) ->
None:
'''Set up the origin and failover servers.
:param tr: The test run to add the servers to.
+ :param disable_redirect_header: Whether ATS was configured with
--no-redirect-header.
'''
tr.Setup.Copy(self._replay_original_file)
tr.Setup.Copy(self._replay_failover_file)
- self._server_origin = tr.AddVerifierServerProcess(f"server_origin",
self._replay_original_file)
- self._server_failover =
tr.AddVerifierServerProcess(f"server_failover", self._replay_failover_file)
+ process_name = f"server_origin_{EscalateTest._process_counter}"
+ self._server_origin = tr.AddVerifierServerProcess(process_name,
self._replay_original_file)
+ process_name = f"server_failover_{EscalateTest._process_counter}"
+ self._server_failover = tr.AddVerifierServerProcess(process_name,
self._replay_failover_file)
self._server_origin.Streams.All += Testers.ContainsExpression(
'uuid: GET', "Verify the origin server received the GET request.")
@@ -72,6 +80,8 @@ class EscalateTest:
'uuid: POST_fail_not_escalated', "Verify the origin server
received the POST request that should not be escalated.")
self._server_origin.Streams.All += Testers.ExcludesExpression(
'uuid: GET_down_origin', "Verify the origin server did not receive
the down origin request.")
+ self._server_origin.Streams.All += Testers.ExcludesExpression(
+ 'x-escalate-redirect', "Verify the origin server should never
receive the x-escalate-redirect header.")
self._server_failover.Streams.All += Testers.ContainsExpression(
'uuid: GET_failed', "Verify the failover server received the
failed GET request.")
@@ -90,12 +100,21 @@ class EscalateTest:
'uuid: POST_fail_not_escalated',
"Verify the failover server did not receive the POST request that
should not be escalated.")
- def _setup_ts(self, tr: 'Process') -> None:
+ if disable_redirect_header:
+ self._server_failover.Streams.All += Testers.ExcludesExpression(
+ 'x-escalate-redirect', "Verify the failover server did not
receive the x-escalate-redirect header.")
+ else:
+ self._server_failover.Streams.All += Testers.ContainsExpression(
+ 'x-escalate-redirect: 1', "Verify the failover server received
the x-escalate-redirect header.")
+
+ def _setup_ts(self, tr: 'Process', disable_redirect_header: bool) -> None:
'''Set up Traffic Server.
:param tr: The test run to add Traffic Server to.
+ :param disable_redirect_header: Whether ATS should be configured with
--no-redirect-header.
'''
- self._ts = tr.MakeATSProcess(f"ts", enable_cache=False)
+ process_name = f"ts_{EscalateTest._process_counter}"
+ self._ts = tr.MakeATSProcess(process_name, enable_cache=False)
# Select a port that is guaranteed to not be used at the moment.
dead_port = get_port(self._ts, "dead_port")
self._ts.Disk.records_config.update(
@@ -107,15 +126,18 @@ class EscalateTest:
'proxy.config.http.redirect.actions': 'self:follow',
'proxy.config.http.number_of_redirections': 4,
})
+ params = ''
+ if disable_redirect_header:
+ params = '@pparam=--no-redirect-header'
self._ts.Disk.remap_config.AddLines(
[
f'map http://origin.server.com
http://backend.origin.server.com:{self._server_origin.Variables.http_port} '
- f'@plugin=escalate.so
@pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port}',
+ f'@plugin=escalate.so
@pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port}
{params}',
# Now create remap entries for the multiplexed hosts: one that
# verifies HTTP, and another that verifies HTTPS.
f'map http://down_origin.server.com
http://backend.down_origin.server.com:{dead_port} '
- f'@plugin=escalate.so
@pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port}
',
+ f'@plugin=escalate.so
@pparam=500,502:failover.server.com:{self._server_failover.Variables.http_port}
{params}',
])
def _setup_client(self, tr: 'Process') -> None:
@@ -123,7 +145,8 @@ class EscalateTest:
:param tr: The test run to add the client to.
'''
- client = tr.AddVerifierClientProcess(f"client",
self._replay_original_file, http_ports=[self._ts.Variables.port])
+ process_name = f"client_{EscalateTest._process_counter}"
+ client = tr.AddVerifierClientProcess(process_name,
self._replay_original_file, http_ports=[self._ts.Variables.port])
client.StartBefore(self._dns)
client.StartBefore(self._server_origin)
client.StartBefore(self._server_failover)
@@ -254,5 +277,6 @@ class EscalateNonGetMethodsTest:
client.Streams.All += Testers.ExcludesExpression(r'\[ERROR\]', 'Verify
there were no errors in the replay.')
-EscalateTest()
+EscalateTest(disable_redirect_header=False)
+EscalateTest(disable_redirect_header=True)
EscalateNonGetMethodsTest()