This is an automated email from the ASF dual-hosted git repository.

zwoop 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 cd4b79363e HRW/HRW4U: Adds SERVER-HEADER & SERVER-URL (#12840)
cd4b79363e is described below

commit cd4b79363ec56e35b6c6d5f45ee66fc0b998a6d3
Author: Leif Hedstrom <[email protected]>
AuthorDate: Mon Mar 16 10:59:35 2026 -0700

    HRW/HRW4U: Adds SERVER-HEADER & SERVER-URL (#12840)
    
    - Address CoPilot review comments
---
 doc/admin-guide/configuration/hrw4u.en.rst         | 25 ++++++++++++---
 doc/admin-guide/plugins/header_rewrite.en.rst      | 33 ++++++++++++++++++++
 plugins/header_rewrite/conditions.cc               | 29 +++++++++++++++--
 plugins/header_rewrite/conditions.h                | 10 +++---
 plugins/header_rewrite/factory.cc                  |  6 +++-
 plugins/header_rewrite/resources.cc                | 29 +++++++++++++----
 plugins/header_rewrite/resources.h                 |  2 ++
 .../header_rewrite_bundle.replay.yaml              | 36 +++++++++++++++++++++-
 .../rules/rule_server_conditions.conf              | 32 +++++++++++++++++++
 tools/hrw4u/src/hrw_symbols.py                     |  2 +-
 tools/hrw4u/src/tables.py                          | 21 +++++++------
 tools/hrw4u/src/types.py                           |  1 +
 tools/hrw4u/tests/data/conds/nexthop.ast.txt       |  1 +
 tools/hrw4u/tests/data/conds/nexthop.input.txt     | 13 ++++++++
 tools/hrw4u/tests/data/conds/nexthop.output.txt    |  9 ++++++
 tools/hrw4u/tests/data/conds/outbound.output.txt   |  4 +--
 .../hrw4u/tests/data/conds/query-param.output.txt  |  2 +-
 .../hrw4u/tests/data/examples/all-nonsense.ast.txt |  2 +-
 .../tests/data/examples/all-nonsense.input.txt     |  2 +-
 .../tests/data/examples/all-nonsense.output.txt    |  8 ++---
 .../hrw4u/tests/data/hooks/send_request.output.txt |  2 +-
 tools/hrw4u/tests/test_lsp.py                      |  9 ++++--
 22 files changed, 236 insertions(+), 42 deletions(-)

diff --git a/doc/admin-guide/configuration/hrw4u.en.rst 
b/doc/admin-guide/configuration/hrw4u.en.rst
index af294dfefa..9e5c8fb39f 100644
--- a/doc/admin-guide/configuration/hrw4u.en.rst
+++ b/doc/admin-guide/configuration/hrw4u.en.rst
@@ -236,7 +236,7 @@ Conditions
 Below is a partial mapping of `header_rewrite` condition symbols to their 
HRW4U equivalents:
 
 ================================= ================================== 
================================================
-Header Rewrite                     HRW4U                             
Description
+Header Rewrite                    HRW4U                              
Description
 ================================= ================================== 
================================================
 cond %{ACCESS:/path}              access("/path")                    File 
exists at "/path" and is accessible by ATS
 cond %{CACHE} =hit-fresh          cache() == "hit-fresh"             Cache 
lookup result status
@@ -258,12 +258,13 @@ cond %{IP:SERVER} ="..."          outbound.ip == "..."    
           Upstream (n
 cond %{IP:OUTBOUND} ="..."        outbound.server == "..."           ATS's 
outbound IP address, connecting upstream
 cond %{LAST-CAPTURE:<#>} ="..."   capture.<#> == "..."               Last 
capture group from regex match (range: `0-9`)
 cond %{METHOD} =GET               inbound.method == "GET"            HTTP 
method match
-cond %{NEXT-HOP:<C>} ="bar"       outbound.url.<C> == "bar"          Next-hop 
URL component match, <:ref:`C<admin-plugins-header-rewrite-url-parts>`> is 
``host`` etc.
-cond %{NEXT-HOP:QUERY:<P>} =bar   outbound.url.query.<P> == "bar"    Extract 
specific query parameter ``P`` from next-hop URL
+cond %{NEXT-HOP:<C>} ="bar"       nexthop.<C> == "bar"               Next-hop 
destination, ``<C>`` is ``host``, ``port``, or ``strategy``
 cond %{NOW:<U>} ="..."            now.<U> == "..."                   Current 
date/time in format,  <:ref:`U<admin-plugins-header-rewrite-geo>`> selects time 
unit
 cond %{OUTBOUND:CLIENT-CERT:<X>}  outbound.client-cert.<X>           Access 
the mTLS / client certificate details, on the outbound (upstream) connection
 cond %{OUTbOUND:SERVER-CERT:<X>}  outbound.client-cert.<X>           Access 
the server (handshake) certificate details, on the outbound connection
 cond %{RANDOM:500} >250           random(500) > 250                  Random 
number between 0 and the specified range
+cond %{SERVER-HEADER:X} =foo      outbound.req.X == "foo"            Server 
request header (sent to origin)
+cond %{SERVER-URL:<C>} =bar       outbound.url.<C> == "bar"          Server 
request URL component (sent to origin)
 cond %{SSN-TXN-COUNT} >10         ssn-txn-count() > 10               Number of 
transactions on server connection
 cond %{TO-URL:<C>} =bar           to.url.<C> == "bar"                Remap 
``To URL`` component match, <:ref:`C<admin-plugins-header-rewrite-url-parts>`> 
is ``host`` etc.
 cond %{TO-URL:QUERY:<P>} =bar     to.url.query.<P> == "bar"          Extract 
specific query parameter ``P`` from remap ``To URL``
@@ -276,6 +277,20 @@ cond %{HTTP-CNTL:<C>}             http.cntl.<C>            
          Check the s
 cond %{INBOUND:<C>}               {in,out}bound.conn.<c>             inbound 
(:ref:`client, user agent<admin-plugins-header-rewrite-inbound>`) connection to 
ATS
 ================================= ================================== 
================================================
 
+.. note::
+    **Header and URL prefix summary:**
+
+    - ``inbound.req.<header>`` → ``CLIENT-HEADER`` - Headers from the client 
request
+    - ``outbound.req.<header>`` → ``SERVER-HEADER`` - Headers in the request 
sent to origin
+    - ``inbound.url.<part>`` → ``CLIENT-URL`` - URL from the original client 
request
+    - ``outbound.url.<part>`` → ``SERVER-URL`` - URL in the request sent to 
origin (after remapping)
+    - ``nexthop.<field>`` → ``NEXT-HOP`` - Network destination info (host, 
port, strategy)
+
+    The distinction between ``outbound.url`` and ``nexthop`` is important:
+
+    - ``outbound.url`` is the HTTP request URL (what's in the request 
line/Host header)
+    - ``nexthop`` is the network destination (where ATS connects, may be a 
parent proxy)
+
 The conditions operating on headers and URLs are also available as operators. 
E.g.:
 
 .. code-block:: none
@@ -336,9 +351,9 @@ HRW4U provides a special ``+=`` operator for adding 
headers::
 
 The ``+=`` operator only works with the following pre-defined symbols:
 
-- ``inbound.req.<header>`` - Client request headers
+- ``inbound.req.<header>`` - Client request headers (maps to ``CLIENT-HEADER``)
 - ``inbound.resp.<header>`` - Origin response headers
-- ``outbound.req.<header>`` - Outbound request headers (context-restricted)
+- ``outbound.req.<header>`` - Server request headers (maps to 
``SERVER-HEADER``)
 - ``outbound.resp.<header>`` - Outbound response headers (context-restricted)
 
 .. note::
diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst 
b/doc/admin-guide/plugins/header_rewrite.en.rst
index 3dafdae924..58ac015f79 100644
--- a/doc/admin-guide/plugins/header_rewrite.en.rst
+++ b/doc/admin-guide/plugins/header_rewrite.en.rst
@@ -369,6 +369,25 @@ header operated on by this condition will be a comma 
separated string of the
 values from every occurrence of the header. More details are provided in
 `Repeated Headers`_ below.
 
+SERVER-HEADER
+~~~~~~~~~~~~~
+::
+
+    cond %{SERVER-HEADER:<name>} <operand>
+
+Value of the header ``<name>`` from the request sent to the origin server
+(regardless of the hook context in which the rule is being evaluated). This is
+useful when you need to check headers that have been modified or added during
+the request processing before being sent to the origin. Note that some headers
+may appear in an HTTP message more than once. In these cases, the value of the
+header operated on by this condition will be a comma separated string of the
+values from every occurrence of the header. More details are provided in
+`Repeated Headers`_ below.
+
+Note that the server request headers are only available after the
+``SEND_REQUEST_HDR_HOOK`` has been reached. Using this condition in earlier
+hooks will result in an empty value.
+
 CLIENT-URL
 ~~~~~~~~~~
 ::
@@ -385,6 +404,20 @@ phase of the transaction.  This happens when there is no 
host in the incoming UR
 and only set as a host header.  During the remap phase the host header is 
copied
 to the CLIENT-URL.  Use CLIENT-HEADER:Host if you are going to match the host.
 
+SERVER-URL
+~~~~~~~~~~
+::
+
+    cond %{SERVER-URL:<part>} <operand>
+
+The URL of the request being sent to the origin server. This is the URL after
+any remapping and modifications have been applied. The ``<part>`` may be
+specified according to the options documented in `URL Parts`_.
+
+Note that the server request URL is only available after the
+``SEND_REQUEST_HDR_HOOK`` has been reached. Using this condition in earlier
+hooks will result in an empty value.
+
 CIDR
 ~~~~
 ::
diff --git a/plugins/header_rewrite/conditions.cc 
b/plugins/header_rewrite/conditions.cc
index 4a4ddc60a4..faf4a19a52 100644
--- a/plugins/header_rewrite/conditions.cc
+++ b/plugins/header_rewrite/conditions.cc
@@ -224,12 +224,20 @@ ConditionHeader::append_value(std::string &s, const 
Resources &res)
   TSMLoc    hdr_loc;
   int       len;
 
-  if (_client) {
+  switch (_type) {
+  case CLIENT:
     bufp    = res.client_bufp;
     hdr_loc = res.client_hdr_loc;
-  } else {
+    break;
+  case SERVER:
+    bufp    = res.server_bufp;
+    hdr_loc = res.server_hdr_loc;
+    break;
+  case HEADER:
+  default:
     bufp    = res.bufp;
     hdr_loc = res.hdr_loc;
+    break;
   }
 
   if (bufp && hdr_loc) {
@@ -272,8 +280,13 @@ ConditionUrl::initialize(Parser &p)
   Condition::initialize(p);
 
   auto match = std::make_unique<MatcherType>(_cond_op);
+
   match->set(p.get_arg(), mods());
   _matcher = std::move(match);
+
+  if (_type == SERVER) {
+    require_resources(RSRC_SERVER_REQUEST_HEADERS);
+  }
 }
 
 void
@@ -318,6 +331,18 @@ ConditionUrl::append_value(std::string &s, const Resources 
&res)
       TSError("[%s] Error getting the pristine URL", PLUGIN_NAME);
       return;
     }
+  } else if (_type == SERVER) {
+    Dbg(pi_dbg_ctl, "   Using the server request url");
+    bufp = res.server_bufp;
+    if (bufp && res.server_hdr_loc) {
+      if (TSHttpHdrUrlGet(bufp, res.server_hdr_loc, &url) != TS_SUCCESS) {
+        TSError("[%s] Error getting the server request URL", PLUGIN_NAME);
+        return;
+      }
+    } else {
+      Dbg(pi_dbg_ctl, "   Server request not available");
+      return;
+    }
   } else if (res._rri != nullptr) {
     // called at the remap hook
     bufp = res._rri->requestBufp;
diff --git a/plugins/header_rewrite/conditions.h 
b/plugins/header_rewrite/conditions.h
index 6893709d78..4393ecbab0 100644
--- a/plugins/header_rewrite/conditions.h
+++ b/plugins/header_rewrite/conditions.h
@@ -255,9 +255,11 @@ class ConditionHeader : public Condition
   using SelfType    = ConditionHeader;
 
 public:
-  explicit ConditionHeader(bool client = false) : _client(client)
+  enum HeaderType { HEADER, CLIENT, SERVER };
+
+  explicit ConditionHeader(HeaderType type = HEADER) : _type(type)
   {
-    Dbg(dbg_ctl, "Calling CTOR for ConditionHeader, client %d", client);
+    Dbg(dbg_ctl, "Calling CTOR for ConditionHeader, type %d", 
static_cast<int>(type));
   }
 
   // noncopyable
@@ -271,7 +273,7 @@ protected:
   bool eval(const Resources &res) override;
 
 private:
-  bool _client;
+  HeaderType _type;
 };
 
 // url
@@ -282,7 +284,7 @@ class ConditionUrl : public Condition
   using SelfType    = ConditionUrl;
 
 public:
-  enum UrlType { CLIENT, URL, FROM, TO };
+  enum UrlType { CLIENT, URL, FROM, TO, SERVER };
 
   explicit ConditionUrl(const UrlType type) : _type(type) { Dbg(dbg_ctl, 
"Calling CTOR for ConditionUrl"); }
 
diff --git a/plugins/header_rewrite/factory.cc 
b/plugins/header_rewrite/factory.cc
index 6e5cc4bd06..ffc999f3e0 100644
--- a/plugins/header_rewrite/factory.cc
+++ b/plugins/header_rewrite/factory.cc
@@ -132,9 +132,13 @@ condition_factory(const std::string &cond)
   } else if (c_name == "HEADER") { // This condition adapts to the hook
     c = new ConditionHeader();
   } else if (c_name == "CLIENT-HEADER") {
-    c = new ConditionHeader(true);
+    c = new ConditionHeader(ConditionHeader::CLIENT);
+  } else if (c_name == "SERVER-HEADER") {
+    c = new ConditionHeader(ConditionHeader::SERVER);
   } else if (c_name == "CLIENT-URL") { // This condition adapts to the hook
     c = new ConditionUrl(ConditionUrl::CLIENT);
+  } else if (c_name == "SERVER-URL") {
+    c = new ConditionUrl(ConditionUrl::SERVER);
   } else if (c_name == "URL") {
     c = new ConditionUrl(ConditionUrl::URL);
   } else if (c_name == "FROM-URL") {
diff --git a/plugins/header_rewrite/resources.cc 
b/plugins/header_rewrite/resources.cc
index 0d4d6e4433..78450147b9 100644
--- a/plugins/header_rewrite/resources.cc
+++ b/plugins/header_rewrite/resources.cc
@@ -33,7 +33,6 @@ void
 Resources::gather(const ResourceIDs ids, TSHttpHookID hook)
 {
   Dbg(pi_dbg_ctl, "Building resources, hook=%s", TSHttpHookNameLookup(hook));
-
   Dbg(pi_dbg_ctl, "Gathering resources for hook %s with IDs %d", 
TSHttpHookNameLookup(hook), ids);
 
   // If we need the client request headers, make sure it's also available in 
the client vars.
@@ -45,6 +44,14 @@ Resources::gather(const ResourceIDs ids, TSHttpHookID hook)
     }
   }
 
+  if (ids & RSRC_SERVER_REQUEST_HEADERS) {
+    Dbg(pi_dbg_ctl, "\tAdding TXN server request header buffers");
+    if (TSHttpTxnServerReqGet(state.txnp, &server_bufp, &server_hdr_loc) != 
TS_SUCCESS) {
+      Dbg(pi_dbg_ctl, "could not gather bufp/hdr_loc for server request");
+      // Not a fatal error - server request may not be available in all hooks
+    }
+  }
+
   switch (hook) {
   case TS_HTTP_READ_RESPONSE_HDR_HOOK:
     // Read response headers from server
@@ -63,12 +70,16 @@ Resources::gather(const ResourceIDs ids, TSHttpHookID hook)
 
   case TS_HTTP_SEND_REQUEST_HDR_HOOK:
     Dbg(pi_dbg_ctl, "Processing TS_HTTP_SEND_REQUEST_HDR_HOOK");
-    // Read request headers to server
     if (ids & RSRC_SERVER_REQUEST_HEADERS) {
-      Dbg(pi_dbg_ctl, "\tAdding TXN server request header buffers");
-      if (TSHttpTxnServerReqGet(state.txnp, &bufp, &hdr_loc) != TS_SUCCESS) {
-        Dbg(pi_dbg_ctl, "could not gather bufp/hdr_loc for request");
-        return;
+      if (server_bufp && server_hdr_loc) {
+        bufp    = server_bufp;
+        hdr_loc = server_hdr_loc;
+      } else {
+        Dbg(pi_dbg_ctl, "\tAdding TXN server request header buffers");
+        if (TSHttpTxnServerReqGet(state.txnp, &bufp, &hdr_loc) != TS_SUCCESS) {
+          Dbg(pi_dbg_ctl, "could not gather bufp/hdr_loc for request");
+          return;
+        }
       }
     }
     break;
@@ -172,6 +183,12 @@ Resources::destroy()
     }
   }
 
+  if (server_bufp && (server_bufp != bufp) && (server_bufp != client_bufp)) {
+    if (server_hdr_loc && (server_hdr_loc != hdr_loc) && (server_hdr_loc != 
client_hdr_loc)) {
+      TSHandleMLocRelease(server_bufp, TS_NULL_MLOC, server_hdr_loc);
+    }
+  }
+
 #if TS_HAS_CRIPTS
   delete client_conn;
   delete server_conn;
diff --git a/plugins/header_rewrite/resources.h 
b/plugins/header_rewrite/resources.h
index 8170595645..2fb45b08c4 100644
--- a/plugins/header_rewrite/resources.h
+++ b/plugins/header_rewrite/resources.h
@@ -115,6 +115,8 @@ public:
   TSMLoc              hdr_loc        = nullptr;
   TSMBuffer           client_bufp    = nullptr;
   TSMLoc              client_hdr_loc = nullptr;
+  TSMBuffer           server_bufp    = nullptr;
+  TSMLoc              server_hdr_loc = nullptr;
 #if TS_HAS_CRIPTS
   cripts::Transaction         state; // This now holds txpn / ssnp
   cripts::Client::Connection *client_conn = nullptr;
diff --git 
a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml 
b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
index 8e6e9a995c..81245d5b0d 100644
--- 
a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
+++ 
b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml
@@ -162,6 +162,13 @@ autest:
             args:
               - "rules/complex_logics.conf"
 
+      - from: "http://www.example.com/from_16/";
+        to: "http://backend.ex:{SERVER_HTTP_PORT}/to_16/";
+        plugins:
+          - name: "header_rewrite.so"
+            args:
+              - "rules/rule_server_conditions.conf"
+
     log_validation:
       traffic_out:
         excludes:
@@ -1123,7 +1130,7 @@ sessions:
       status: 200
 
 # ==========================================================================
-# Tests 31-52: Complex GROUP logic tests (rules/complex_logics.conf)
+# Tests 31-56: Complex GROUP logic tests (rules/complex_logics.conf)
 # ==========================================================================
 
 # Test 31: GROUP [OR] - only A header present (should match via group)
@@ -1783,3 +1790,30 @@ sessions:
       headers:
         fields:
         - [ X-Group-First-Result, { as: absent } ]
+
+# Test 63: SERVER-HEADER and SERVER-URL conditions
+- transactions:
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /from_16/test
+      headers:
+        fields:
+        - [ Host, www.example.com ]
+        - [ uuid, 63 ]
+
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Connection, close ]
+
+    proxy-response:
+      status: 200
+      headers:
+        fields:
+        - [ X-Server-Path, { value: "to_16/test", as: equal } ]
+        - [ X-Marker-Found, { value: "Yes", as: equal } ]
+        - [ X-Server-Host-Header, { value: "backend.ex", as: contains } ]
+        - [ X-Path-Match, { value: "Yes", as: equal } ]
diff --git 
a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf 
b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf
new file mode 100644
index 0000000000..e46e2d39da
--- /dev/null
+++ 
b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf
@@ -0,0 +1,32 @@
+#
+# 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.
+
+# Test SERVER-HEADER and SERVER-URL conditions
+cond %{SEND_REQUEST_HDR_HOOK}
+  set-header X-Server-Marker "ATS-Processed"
+
+cond %{SEND_RESPONSE_HDR_HOOK}
+  set-header X-Server-Path "%{SERVER-URL:PATH}"
+  set-header X-Server-Host-Header "%{SERVER-HEADER:Host}"
+
+cond %{SEND_RESPONSE_HDR_HOOK} [AND]
+cond %{SERVER-HEADER:X-Server-Marker} ="ATS-Processed"
+  set-header X-Marker-Found "Yes"
+
+cond %{SEND_RESPONSE_HDR_HOOK} [AND]
+cond %{SERVER-URL:PATH} /^to_16\//
+  set-header X-Path-Match "Yes"
diff --git a/tools/hrw4u/src/hrw_symbols.py b/tools/hrw4u/src/hrw_symbols.py
index 001358ddb1..2eaa9608cc 100644
--- a/tools/hrw4u/src/hrw_symbols.py
+++ b/tools/hrw4u/src/hrw_symbols.py
@@ -182,7 +182,7 @@ class InverseSymbolResolver(SymbolResolverBase):
                 if tag == "HEADER":
                     return f"{self.get_prefix_for_context('header_condition', 
section)}{suffix}", False
                 else:
-                    return f"{lhs_prefix}{suffix}", False
+                    return f"{lhs_prefix}{suffix.replace(':', '.')}", False
         return None
 
     def _should_lowercase_suffix(self, tag_match: str, lhs_prefix: str) -> 
bool:
diff --git a/tools/hrw4u/src/tables.py b/tools/hrw4u/src/tables.py
index 3b80c5e503..86d2de67a7 100644
--- a/tools/hrw4u/src/tables.py
+++ b/tools/hrw4u/src/tables.py
@@ -113,6 +113,7 @@ CONDITION_MAP: dict[str, MapParams] = {
     "inbound.resp.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}, rev={"reverse_context": "header_condition"}),
     "inbound.url.query.": MapParams(target="CLIENT-URL:QUERY", prefix=True, 
validate=Validator.http_token(), sections=HTTP_SECTIONS),
     "inbound.url.": MapParams(target="CLIENT-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections=HTTP_SECTIONS),
+    "nexthop.": MapParams(target="NEXT-HOP", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.NEXTHOP_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}, rev={"reverse_fallback": "nexthop."}),
     "now.": MapParams(target="NOW", upper=True, 
validate=Validator.suffix_group(SuffixGroup.DATE_FIELDS)),
     "outbound.conn.client-cert.SAN.": 
MapParams(target="OUTBOUND:CLIENT-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
     "outbound.conn.server-cert.SAN.": 
MapParams(target="OUTBOUND:SERVER-CERT:SAN", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
@@ -122,29 +123,31 @@ CONDITION_MAP: dict[str, MapParams] = {
     "outbound.conn.server-cert.": MapParams(target="OUTBOUND:SERVER-CERT", 
upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
     "outbound.conn.": MapParams(target="OUTBOUND", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.CONN_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
     "outbound.cookie.": MapParams(target="COOKIE", prefix=True, 
validate=Validator.http_token(), sections={SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}, rev={"reverse_fallback": 
"inbound.cookie."}),
-    "outbound.req.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}, rev={"reverse_context": 
"header_condition"}),
+    "outbound.req.": MapParams(target="SERVER-HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}, rev={"reverse_fallback": 
"outbound.req."}),
     "outbound.resp.": MapParams(target="HEADER", prefix=True, 
validate=Validator.http_header_name(), sections={SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}, rev={"reverse_context": "header_condition"}),
-    "outbound.url.query.": MapParams(target="NEXT-HOP:QUERY", prefix=True, 
validate=Validator.http_token(), sections={SectionType.PRE_REMAP, 
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}),
-    "outbound.url.": MapParams(target="NEXT-HOP", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST, 
SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}),
+    "outbound.url.query.": MapParams(target="SERVER-URL:QUERY", prefix=True, 
validate=Validator.http_token(), sections={SectionType.SEND_REQUEST, 
SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}),
+    "outbound.url.": MapParams(target="SERVER-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, 
SectionType.SEND_RESPONSE}, rev={"reverse_fallback": "outbound.url."}),
     "to.url.query.": MapParams(target="TO-URL:QUERY", prefix=True, 
validate=Validator.http_token(), sections=HTTP_SECTIONS),
     "to.url.": MapParams(target="TO-URL", upper=True, prefix=True, 
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), 
sections=HTTP_SECTIONS),
 }
 
 FALLBACK_TAG_MAP: dict[str, tuple[str, bool]] = {
-    "HEADER": ("header_condition", True),
     "CLIENT-HEADER": ("inbound.req.", False),
+    "CLIENT-URL:QUERY": ("inbound.url.query.", False),
     "COOKIE": ("inbound.cookie.", False),
+    "FROM-URL:QUERY": ("from.url.query.", False),
+    "HEADER": ("header_condition", True),
     "INBOUND:CLIENT-CERT": ("inbound.conn.client-cert.", False),
-    "INBOUND:SERVER-CERT": ("inbound.conn.server-cert.", False),
     "INBOUND:CLIENT-CERT:SAN": ("inbound.conn.client-cert.SAN.", False),
+    "INBOUND:SERVER-CERT": ("inbound.conn.server-cert.", False),
     "INBOUND:SERVER-CERT:SAN": ("inbound.conn.server-cert.SAN.", False),
+    "NEXT-HOP": ("nexthop.", False),
     "OUTBOUND:CLIENT-CERT": ("outbound.conn.client-cert.", False),
-    "OUTBOUND:SERVER-CERT": ("outbound.conn.server-cert.", False),
     "OUTBOUND:CLIENT-CERT:SAN": ("outbound.conn.client-cert.SAN.", False),
+    "OUTBOUND:SERVER-CERT": ("outbound.conn.server-cert.", False),
     "OUTBOUND:SERVER-CERT:SAN": ("outbound.conn.server-cert.SAN.", False),
-    "CLIENT-URL:QUERY": ("inbound.url.query.", False),
-    "NEXT-HOP:QUERY": ("outbound.url.query.", False),
-    "FROM-URL:QUERY": ("from.url.query.", False),
+    "SERVER-HEADER": ("outbound.req.", False),
+    "SERVER-URL": ("outbound.url.", False),
     "TO-URL:QUERY": ("to.url.query.", False)
 }
 
diff --git a/tools/hrw4u/src/types.py b/tools/hrw4u/src/types.py
index 0cacafe673..3db3e1bfab 100644
--- a/tools/hrw4u/src/types.py
+++ b/tools/hrw4u/src/types.py
@@ -80,6 +80,7 @@ class LanguageKeyword(Enum):
 
 class SuffixGroup(Enum):
     URL_FIELDS = frozenset({"SCHEME", "HOST", "PORT", "PATH", "QUERY", "URL"})
+    NEXTHOP_FIELDS = frozenset({"HOST", "PORT", "STRATEGY"})
     GEO_FIELDS = frozenset({"COUNTRY", "COUNTRY-ISO", "ASN", "ASN-NAME"})
     CONN_FIELDS = frozenset(
         {
diff --git a/tools/hrw4u/tests/data/conds/nexthop.ast.txt 
b/tools/hrw4u/tests/data/conds/nexthop.ast.txt
new file mode 100644
index 0000000000..ebf488cbd9
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/nexthop.ast.txt
@@ -0,0 +1 @@
+(program (programItem (section SEND_REQUEST { (sectionBody (conditional 
(ifStatement if (condition (expression (term (factor (comparison (comparable 
nexthop.host) == (value "parent.example.com")))))) (block { (blockItem 
(statement outbound.req.X-Via-Parent = (value "yes") ;)) })))) })) (programItem 
(section SEND_RESPONSE { (sectionBody (conditional (ifStatement if (condition 
(expression (term (factor (comparison (comparable outbound.req.X-Via-Parent) == 
(value "yes")))))) (block { (block [...]
diff --git a/tools/hrw4u/tests/data/conds/nexthop.input.txt 
b/tools/hrw4u/tests/data/conds/nexthop.input.txt
new file mode 100644
index 0000000000..a36effce81
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/nexthop.input.txt
@@ -0,0 +1,13 @@
+SEND_REQUEST {
+    if nexthop.host == "parent.example.com" {
+        outbound.req.X-Via-Parent = "yes";
+    }
+}
+
+SEND_RESPONSE {
+    if outbound.req.X-Via-Parent == "yes" {
+        inbound.resp.X-Next-Host = "{nexthop.host}";
+        inbound.resp.X-Next-Port = "{nexthop.port}";
+        inbound.resp.X-Next-Strategy = "{nexthop.strategy}";
+    }
+}
diff --git a/tools/hrw4u/tests/data/conds/nexthop.output.txt 
b/tools/hrw4u/tests/data/conds/nexthop.output.txt
new file mode 100644
index 0000000000..e4df4bcc25
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/nexthop.output.txt
@@ -0,0 +1,9 @@
+cond %{SEND_REQUEST_HDR_HOOK} [AND]
+cond %{NEXT-HOP:HOST} ="parent.example.com"
+    set-header X-Via-Parent "yes"
+
+cond %{SEND_RESPONSE_HDR_HOOK} [AND]
+cond %{SERVER-HEADER:X-Via-Parent} ="yes"
+    set-header X-Next-Host "%{NEXT-HOP:HOST}"
+    set-header X-Next-Port "%{NEXT-HOP:PORT}"
+    set-header X-Next-Strategy "%{NEXT-HOP:STRATEGY}"
diff --git a/tools/hrw4u/tests/data/conds/outbound.output.txt 
b/tools/hrw4u/tests/data/conds/outbound.output.txt
index db30ea0f49..fa2638c23a 100644
--- a/tools/hrw4u/tests/data/conds/outbound.output.txt
+++ b/tools/hrw4u/tests/data/conds/outbound.output.txt
@@ -1,3 +1,3 @@
 cond %{SEND_REQUEST_HDR_HOOK} [AND]
-cond %{NEXT-HOP:HOST} /foo|bar/
-    set-header X-Valid "%{NEXT-HOP:PORT}"
+cond %{SERVER-URL:HOST} /foo|bar/
+    set-header X-Valid "%{SERVER-URL:PORT}"
diff --git a/tools/hrw4u/tests/data/conds/query-param.output.txt 
b/tools/hrw4u/tests/data/conds/query-param.output.txt
index 9a771b1b13..0e612b5e9a 100644
--- a/tools/hrw4u/tests/data/conds/query-param.output.txt
+++ b/tools/hrw4u/tests/data/conds/query-param.output.txt
@@ -11,7 +11,7 @@ cond %{TO-URL:QUERY:target} ="" [NOT]
     set-header X-Target "set"
 
 cond %{SEND_REQUEST_HDR_HOOK} [AND]
-cond %{NEXT-HOP:QUERY:backend} ="fast"
+cond %{SERVER-URL:QUERY:backend} ="fast"
     set-header X-Priority "high"
 
 cond %{SEND_RESPONSE_HDR_HOOK} [AND]
diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt 
b/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt
index bc4bfcfed1..f404d038eb 100644
--- a/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt
+++ b/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt
@@ -1 +1 @@
-(program (programItem (section (varSection VARS { (variables (variablesItem 
(commentLine # Boolean and integer state you can flip/use across sections)) 
(variablesItem (variableDecl FlagA : bool ;)) (variablesItem (variableDecl 
FlagB : bool ;)) (variablesItem (variableDecl Cnt8 : int8 ;)) (variablesItem 
(variableDecl Big16 : int16 ;))) }))) (programItem (section REMAP { 
(sectionBody (commentLine # Plugin controls)) (sectionBody (statement 
http.cntl.TXN_DEBUG = (value true) ;)) (sectionBod [...]
+(program (programItem (section (varSection VARS { (variables (variablesItem 
(commentLine # Boolean and integer state you can flip/use across sections)) 
(variablesItem (variableDecl FlagA : bool ;)) (variablesItem (variableDecl 
FlagB : bool ;)) (variablesItem (variableDecl Cnt8 : int8 ;)) (variablesItem 
(variableDecl Big16 : int16 ;))) }))) (programItem (section REMAP { 
(sectionBody (commentLine # Plugin controls)) (sectionBody (statement 
http.cntl.TXN_DEBUG = (value true) ;)) (sectionBod [...]
diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.input.txt 
b/tools/hrw4u/tests/data/examples/all-nonsense.input.txt
index e9d62ea221..cefa165556 100644
--- a/tools/hrw4u/tests/data/examples/all-nonsense.input.txt
+++ b/tools/hrw4u/tests/data/examples/all-nonsense.input.txt
@@ -116,7 +116,7 @@ READ_REQUEST {
 }
 
 SEND_REQUEST {
-  # Use NEXT-HOP information to adjust Host header
+  # Use server URL information to adjust Host header
   if (outbound.url.host == "www.firstparent.com") {
     outbound.req.Host = "vhost.firstparent.com";
   } elif (outbound.url.host == "www.secondparent.com") {
diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.output.txt 
b/tools/hrw4u/tests/data/examples/all-nonsense.output.txt
index a51a6fbd1e..bb15eb068e 100644
--- a/tools/hrw4u/tests/data/examples/all-nonsense.output.txt
+++ b/tools/hrw4u/tests/data/examples/all-nonsense.output.txt
@@ -137,14 +137,14 @@ cond %{GROUP:END}
 # assign int16
 
 cond %{SEND_REQUEST_HDR_HOOK} [AND]
-# Use NEXT-HOP information to adjust Host header
+# Use server URL information to adjust Host header
 cond %{GROUP}
-    cond %{NEXT-HOP:HOST} ="www.firstparent.com"
+    cond %{SERVER-URL:HOST} ="www.firstparent.com"
 cond %{GROUP:END}
     set-header Host "vhost.firstparent.com"
 elif
     cond %{GROUP}
-        cond %{NEXT-HOP:HOST} ="www.secondparent.com"
+        cond %{SERVER-URL:HOST} ="www.secondparent.com"
     cond %{GROUP:END}
         set-header Host "vhost.secondparent.com"
 # Demonstrate HTTP control read and write
@@ -200,7 +200,7 @@ elif
 cond %{SEND_RESPONSE_HDR_HOOK} [AND]
     set-header Cache-Control "public, max-age=60"
     set-header X-Now 
"%{NOW:YEAR}-%{NOW:MONTH}-%{NOW:DAY}T%{NOW:HOUR}:%{NOW:MINUTE}"
-    set-header X-Ports "in=%{CLIENT-URL:PORT};out=%{NEXT-HOP:PORT}"
+    set-header X-Ports "in=%{CLIENT-URL:PORT};out=%{SERVER-URL:PORT}"
     set-header X-IPs "client=%{IP:CLIENT};server=%{IP:SERVER}"
     set-header X-ID "%{ID:UNIQUE}"
     set-header ATS-Geo-Country "%{GEO:COUNTRY}"
diff --git a/tools/hrw4u/tests/data/hooks/send_request.output.txt 
b/tools/hrw4u/tests/data/hooks/send_request.output.txt
index 65ee8a3f13..43bd0bba95 100644
--- a/tools/hrw4u/tests/data/hooks/send_request.output.txt
+++ b/tools/hrw4u/tests/data/hooks/send_request.output.txt
@@ -1,3 +1,3 @@
 cond %{SEND_REQUEST_HDR_HOOK} [AND]
-cond %{HEADER:X-Send-Request} ="yes"
+cond %{SERVER-HEADER:X-Send-Request} ="yes"
     rm-header X-Send-Request
diff --git a/tools/hrw4u/tests/test_lsp.py b/tools/hrw4u/tests/test_lsp.py
index a3881456f5..3ad4b80e3e 100644
--- a/tools/hrw4u/tests/test_lsp.py
+++ b/tools/hrw4u/tests/test_lsp.py
@@ -433,7 +433,7 @@ def 
test_multi_section_inbound_always_allowed(shared_lsp_client) -> None:
 
 def test_outbound_restrictions_batch(shared_lsp_client) -> None:
     """Batch test outbound restrictions - outbound features have 
section-specific availability."""
-    # outbound.url. is available in PRE_REMAP through SEND_REQUEST, plus 
READ_RESPONSE, SEND_RESPONSE
+    # outbound.url. (SERVER-URL) is only available from SEND_REQUEST onwards 
(server request must exist)
     # outbound.cookie. is only available from SEND_REQUEST onwards
     http_sections = ["PRE_REMAP", "REMAP", "READ_REQUEST", "SEND_REQUEST", 
"READ_RESPONSE"]
 
@@ -454,8 +454,11 @@ def test_outbound_restrictions_batch(shared_lsp_client) -> 
None:
         # outbound.cookie. is only available from SEND_REQUEST onwards
         if section in ["SEND_REQUEST", "READ_RESPONSE"]:
             assert len(outbound_cookie_items) > 0, f"outbound.cookie. should 
be in {section}"
-        # outbound.url. is available in all these sections
-        assert len(outbound_url_items) > 0, f"outbound.url. should be in 
{section}"
+        # outbound.url. (SERVER-URL) is only available from SEND_REQUEST 
onwards
+        if section in ["SEND_REQUEST", "READ_RESPONSE"]:
+            assert len(outbound_url_items) > 0, f"outbound.url. should be in 
{section}"
+        else:
+            assert len(outbound_url_items) == 0, f"outbound.url. should NOT be 
in {section}"
 
 
 def test_specific_outbound_conn_completions(shared_lsp_client) -> None:


Reply via email to