Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions doc/admin-guide/configuration/hrw4u.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ cond %{ACCESS:/path} access("/path") File exists
cond %{CACHE} =hit-fresh cache() == "hit-fresh" Cache lookup result status
cond %{CIDR:24,48} =ip cidr(24,48) == "ip" Match masked client IP address
cond %{CLIENT-HEADER:X} =foo inbound.req.X == "foo" Original client request header
cond %{SERVER-HEADER:X} =foo outbound.req.X == "foo" Server request header (sent to origin)
cond %{CLIENT-URL:<C>} =bar inbound.url.<C> == "bar" URL component match, <:ref:`C<admin-plugins-header-rewrite-url-parts>`> is ``host``, ``path`` etc.
cond %{SERVER-URL:<C>} =bar outbound.url.<C> == "bar" Server request URL component (sent to origin)
cond %{COOKIE:foo} =bar {in,out}bound.cookie.foo == "bar" Check a cookie value
cond %{FROM-URL:<C>} =bar from.url.<C> == "bar" Remap ``From URL`` component match, <:ref:`C<admin-plugins-header-rewrite-url-parts>`> is ``host`` etc.
cond %{HEADER:X} =fo {in,out}bound.{req,resp}.X == "fo" Context sensitive header conditions
Expand All @@ -195,22 +197,36 @@ cond %{IP:SERVER} ="..." outbound.ip == "..." Upstream (ne
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:<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 %{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 %{TXN-COUNT} >10 txn-count() > 10 Number of transactions on client connection
cond %{URL:<C> =bar {in,out}bound.url.<C> == "bar" Context aware URL component match
cond %{URL:<C> =bar inbound.url.<C> == "bar" Context aware URL component match (use ``inbound.url`` or ``outbound.url``)
cond %{GEO:<C>} =bar geo.<C> == "bar" IP to Geo mapping. <:ref:`C<admin-plugins-header-rewrite-geo>`> is country, asn, etc.
cond %{STATUS} =200 inbound.status ==200 Origin http status code
cond %{TCP-INFO} tcp.info TCP Info struct field values
cond %{HTTP-CNTL:<C>} http.cntl.<C> Check the state of the <:ref:`C<admin-plugins-header-rewrite-set-http-cntl>`> HTTP control
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
Expand Down Expand Up @@ -271,9 +287,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::
Expand Down
33 changes: 33 additions & 0 deletions doc/admin-guide/plugins/header_rewrite.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~
::
Expand All @@ -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
~~~~
::
Expand Down
29 changes: 27 additions & 2 deletions plugins/header_rewrite/conditions.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 6 additions & 4 deletions plugins/header_rewrite/conditions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -271,7 +273,7 @@ class ConditionHeader : public Condition
bool eval(const Resources &res) override;

private:
bool _client;
HeaderType _type;
};

// url
Expand All @@ -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"); }

Expand Down
6 changes: 5 additions & 1 deletion plugins/header_rewrite/factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
15 changes: 14 additions & 1 deletion plugins/header_rewrite/resources.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -172,6 +179,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;
Expand Down
2 changes: 2 additions & 0 deletions plugins/header_rewrite/resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ class Resources
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ autest:
args:
- "rules/query_sub_key.conf"

- from: "http://www.example.com/from_14/"
to: "http://backend.ex:{SERVER_HTTP_PORT}/to_14/"
plugins:
- name: "header_rewrite.so"
args:
- "rules/rule_server_conditions.conf"


# Proxy verifier sessions
Expand Down Expand Up @@ -1074,3 +1080,30 @@ sessions:
headers:
fields:
- [ X-Query-Sub, { as: absent } ]

# Test 30: SERVER-HEADER and SERVER-URL conditions
- transactions:
- client-request:
method: "GET"
version: "1.1"
url: /from_14/test
headers:
fields:
- [ Host, www.example.com ]
- [ uuid, 36 ]

server-response:
status: 200
reason: OK
headers:
fields:
- [ Connection, close ]

proxy-response:
status: 200
headers:
fields:
- [ X-Server-Path, { value: "to_14/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 } ]
Original file line number Diff line number Diff line change
@@ -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_14\//
set-header X-Path-Match "Yes"
2 changes: 1 addition & 1 deletion tools/hrw4u/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "hrw4u"
version = "1.4.1"
version = "1.4.4"
description = "HRW4U CLI tool for Apache Traffic Server header rewrite rules"
authors = [
{name = "Leif Hedstrom", email = "leif@apache.org"}
Expand Down
8 changes: 6 additions & 2 deletions tools/hrw4u/src/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"inbound.req.": MapParams(target="CLIENT-HEADER", prefix=True, validate=Validator.http_header_name(), sections=HTTP_SECTIONS, rev={"reverse_fallback": "inbound.req."}),
"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.": 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}),
Expand All @@ -120,15 +121,18 @@
"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.": 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.": 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.": 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),
"SERVER-HEADER": ("outbound.req.", False),
"SERVER-URL": ("outbound.url.", False),
"NEXT-HOP": ("nexthop.", False),
"COOKIE": ("inbound.cookie.", False),
"INBOUND:CLIENT-CERT": ("inbound.conn.client-cert.", False),
"INBOUND:SERVER-CERT": ("inbound.conn.server-cert.", False),
Expand Down
1 change: 1 addition & 0 deletions tools/hrw4u/src/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def get_keywords_with_descriptions(cls) -> dict[str, str]:

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(
{
Expand Down
1 change: 1 addition & 0 deletions tools/hrw4u/tests/data/conds/nexthop.ast.txt
Original file line number Diff line number Diff line change
@@ -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 { (blockItem (statement inbound.resp.X-Next-Host = (value "{nexthop.host}") ;)) (blockItem (statement inbound.resp.X-Next-Port = (value "{nexthop.port}") ;)) (blockItem (statement inbound.resp.X-Next-Strategy = (value "{nexthop.strategy}") ;)) })))) })) <EOF>)
Loading