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()

Reply via email to