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: