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 9e38be49bb hrw4u: Adds QP parameter indexing by name (#12853)
9e38be49bb is described below
commit 9e38be49bb710f070dee25b52c2b60510c12e9b1
Author: Leif Hedstrom <[email protected]>
AuthorDate: Mon Feb 9 16:20:41 2026 -0700
hrw4u: Adds QP parameter indexing by name (#12853)
* hrw4u: Adds QP parameter indexing by name
* Review comments
---
doc/admin-guide/configuration/hrw4u.en.rst | 98 ++++++++++++++--------
tools/hrw4u/src/hrw_symbols.py | 13 +--
tools/hrw4u/src/tables.py | 10 ++-
tools/hrw4u/tests/data/conds/query-param.ast.txt | 1 +
tools/hrw4u/tests/data/conds/query-param.input.txt | 25 ++++++
.../hrw4u/tests/data/conds/query-param.output.txt | 20 +++++
6 files changed, 125 insertions(+), 42 deletions(-)
diff --git a/doc/admin-guide/configuration/hrw4u.en.rst
b/doc/admin-guide/configuration/hrw4u.en.rst
index 8a3bed3d86..fb87f90992 100644
--- a/doc/admin-guide/configuration/hrw4u.en.rst
+++ b/doc/admin-guide/configuration/hrw4u.en.rst
@@ -174,42 +174,46 @@ Conditions
Below is a partial mapping of `header_rewrite` condition symbols to their
HRW4U equivalents:
-================================ ==================================
================================================
-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
-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 %{CLIENT-URL:<C>} =bar inbound.url.<C> == "bar" URL
component match, <:ref:`C<admin-plugins-header-rewrite-url-parts>`> is
``host``, ``path`` etc.
-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
-cond %{ID:UNIQUE} =... id.UNIQUE == "..."
(:ref:`Unique/request/process<admin-plugins-header-rewrite-id>`) transaction
identifier
-cond %{INTERNAL-TRANSACTION} internal() Check if
transaction is internally generated
-cond %{INBOUND:CLIENT-CERT:<X>} inbound.client-cert.<X> Access the
mTLS / client certificate details, on the inbound (client) connection
-cond %{INBOUND:SERVER-CERT:<X>} inbound.client-cert.<X> Access the
server (handshake) certificate details, on the inbound connection
-cond %{IP:CLIENT} ="..." inbound.ip == "..." Client's
IP address. Same as ``inbound.REMOTE_ADDR``
-cond %{IP:INBOUND} ="..." inbound.server == "..." ATS's IP
address to which the client connected
-cond %{IP:SERVER} ="..." outbound.ip == "..." Upstream
(next-hop) server IP address
-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 %{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 %{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
-================================ ==================================
================================================
+================================= ==================================
================================================
+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
+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 %{CLIENT-URL:<C>} =bar inbound.url.<C> == "bar" URL
component match, <:ref:`C<admin-plugins-header-rewrite-url-parts>`> is
``host``, ``path`` etc.
+cond %{CLIENT-URL:QUERY:<P>} =bar inbound.url.query.<P> == "bar" Extract
specific query parameter ``P`` from URL
+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 %{FROM-URL:QUERY:<P>} =bar from.url.query.<P> == "bar" Extract
specific query parameter ``P`` from remap ``From URL``
+cond %{HEADER:X} =fo {in,out}bound.{req,resp}.X == "fo" Context
sensitive header conditions
+cond %{ID:UNIQUE} =... id.UNIQUE == "..."
(:ref:`Unique/request/process<admin-plugins-header-rewrite-id>`) transaction
identifier
+cond %{INTERNAL-TRANSACTION} internal() Check if
transaction is internally generated
+cond %{INBOUND:CLIENT-CERT:<X>} inbound.client-cert.<X> Access
the mTLS / client certificate details, on the inbound (client) connection
+cond %{INBOUND:SERVER-CERT:<X>} inbound.client-cert.<X> Access
the server (handshake) certificate details, on the inbound connection
+cond %{IP:CLIENT} ="..." inbound.ip == "..." Client's
IP address. Same as ``inbound.REMOTE_ADDR``
+cond %{IP:INBOUND} ="..." inbound.server == "..." ATS's IP
address to which the client connected
+cond %{IP:SERVER} ="..." outbound.ip == "..." Upstream
(next-hop) server IP address
+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 %{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 %{TO-URL:QUERY:<P>} =bar to.url.query.<P> == "bar" Extract
specific query parameter ``P`` from remap ``To URL``
+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 %{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
+================================= ==================================
================================================
The conditions operating on headers and URLs are also available as operators.
E.g.:
@@ -698,6 +702,28 @@ limiting to the request.::
}
}
+Route Based on Query Parameter Value
+------------------------------------
+
+This rule extracts a specific query parameter value and uses it to make routing
+decisions or set custom headers. The ``query.<param_name>`` syntax allows
+extracting individual query parameter values::
+
+ REMAP {
+ if inbound.url.query.version == "v2" {
+ inbound.req.X-API-Version = "v2";
+ }
+ }
+
+ SEND_RESPONSE {
+ inbound.resp.X-Debug-Param = "{inbound.url.query.debug}";
+ }
+
+.. note::
+ Query parameter names are case-sensitive and matched as-is without URL
+ decoding. For example, ``inbound.url.query.my%20param`` matches the literal
+ parameter name ``my%20param``, not ``my param``.
+
References
==========
diff --git a/tools/hrw4u/src/hrw_symbols.py b/tools/hrw4u/src/hrw_symbols.py
index 3487027977..b4e24ced74 100644
--- a/tools/hrw4u/src/hrw_symbols.py
+++ b/tools/hrw4u/src/hrw_symbols.py
@@ -399,12 +399,15 @@ class InverseSymbolResolver(SymbolResolverBase):
tag = match.group(1)
payload = match.group(2)
- # Handle certificate tags explicitly to ensure proper parsing
+ # Handle multi-colon tags explicitly to ensure proper parsing
+ # Multi-colon parsing for certificate tags (CLIENT-CERT, SERVER-CERT)
and query parameter tags (QUERY)
original_inner = percent[2:-1]
- if ":" in original_inner and any(cert_tag in original_inner for
cert_tag in ["CLIENT-CERT", "SERVER-CERT"]):
- new_tag, new_payload = self.parse_percent_block(percent)
- if new_tag != tag or new_payload != payload:
- tag, payload = new_tag, new_payload
+ if ":" in original_inner:
+ if (any(cert_tag in original_inner for cert_tag in ["CLIENT-CERT",
"SERVER-CERT"]) or
+ (payload and payload.startswith("QUERY:"))):
+ new_tag, new_payload = self.parse_percent_block(percent)
+ if new_tag != tag or new_payload != payload:
+ tag, payload = new_tag, new_payload
if types.BooleanLiteral.contains(tag):
return tag, False
diff --git a/tools/hrw4u/src/tables.py b/tools/hrw4u/src/tables.py
index 94253fba0a..3b80c5e503 100644
--- a/tools/hrw4u/src/tables.py
+++ b/tools/hrw4u/src/tables.py
@@ -96,6 +96,7 @@ CONDITION_MAP: dict[str, MapParams] = {
# Prefix matches
"capture.": MapParams(target="LAST-CAPTURE", prefix=True,
validate=Validator.range(0, 9)),
+ "from.url.query.": MapParams(target="FROM-URL:QUERY", prefix=True,
validate=Validator.http_token(), sections=HTTP_SECTIONS),
"from.url.": MapParams(target="FROM-URL", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS),
sections=HTTP_SECTIONS),
"geo.": MapParams(target="GEO", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.GEO_FIELDS)),
"http.cntl.": MapParams(target="HTTP-CNTL", upper=True,
validate=Validator.suffix_group(SuffixGroup.HTTP_CNTL_FIELDS),
sections=HTTP_SECTIONS),
@@ -110,6 +111,7 @@ CONDITION_MAP: dict[str, MapParams] = {
"inbound.cookie.": MapParams(target="COOKIE", prefix=True,
validate=Validator.http_token(), sections=HTTP_SECTIONS,
rev={"reverse_fallback": "inbound.cookie."}),
"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.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),
"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}),
@@ -122,7 +124,9 @@ CONDITION_MAP: dict[str, MapParams] = {
"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.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}),
+ "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),
}
@@ -137,7 +141,11 @@ FALLBACK_TAG_MAP: dict[str, tuple[str, bool]] = {
"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:SAN": ("outbound.conn.server-cert.SAN.", 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),
+ "TO-URL:QUERY": ("to.url.query.", False)
}
# Context type to mapping name associations
diff --git a/tools/hrw4u/tests/data/conds/query-param.ast.txt
b/tools/hrw4u/tests/data/conds/query-param.ast.txt
new file mode 100644
index 0000000000..45607a346a
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/query-param.ast.txt
@@ -0,0 +1 @@
+(program (programItem (section REMAP { (sectionBody (conditional (ifStatement
if (condition (expression (term (factor (comparison (comparable
inbound.url.query.version) == (value "v2")))))) (block { (blockItem (statement
inbound.req.X-API-Version = (value "v2") ;)) })))) (sectionBody (conditional
(ifStatement if (condition (expression (term (factor (comparison (comparable
from.url.query.source) == (value "mobile")))))) (block { (blockItem (statement
inbound.req.X-Source = (value "mobile" [...]
diff --git a/tools/hrw4u/tests/data/conds/query-param.input.txt
b/tools/hrw4u/tests/data/conds/query-param.input.txt
new file mode 100644
index 0000000000..fa7ccf8d86
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/query-param.input.txt
@@ -0,0 +1,25 @@
+REMAP {
+ if inbound.url.query.version == "v2" {
+ inbound.req.X-API-Version = "v2";
+ }
+
+ if from.url.query.source == "mobile" {
+ inbound.req.X-Source = "mobile";
+ }
+
+ if to.url.query.target {
+ inbound.req.X-Target = "set";
+ }
+}
+
+SEND_REQUEST {
+ if outbound.url.query.backend == "fast" {
+ outbound.req.X-Priority = "high";
+ }
+}
+
+SEND_RESPONSE {
+ inbound.resp.X-Query-Sub = "{inbound.url.query.sub}";
+ inbound.resp.X-From-Param = "{from.url.query.param}";
+ inbound.resp.X-To-Param = "{to.url.query.redirect}";
+}
diff --git a/tools/hrw4u/tests/data/conds/query-param.output.txt
b/tools/hrw4u/tests/data/conds/query-param.output.txt
new file mode 100644
index 0000000000..9a771b1b13
--- /dev/null
+++ b/tools/hrw4u/tests/data/conds/query-param.output.txt
@@ -0,0 +1,20 @@
+cond %{REMAP_PSEUDO_HOOK} [AND]
+cond %{CLIENT-URL:QUERY:version} ="v2"
+ set-header X-API-Version "v2"
+
+cond %{REMAP_PSEUDO_HOOK} [AND]
+cond %{FROM-URL:QUERY:source} ="mobile"
+ set-header X-Source "mobile"
+
+cond %{REMAP_PSEUDO_HOOK} [AND]
+cond %{TO-URL:QUERY:target} ="" [NOT]
+ set-header X-Target "set"
+
+cond %{SEND_REQUEST_HDR_HOOK} [AND]
+cond %{NEXT-HOP:QUERY:backend} ="fast"
+ set-header X-Priority "high"
+
+cond %{SEND_RESPONSE_HDR_HOOK} [AND]
+ set-header X-Query-Sub "%{CLIENT-URL:QUERY:sub}"
+ set-header X-From-Param "%{FROM-URL:QUERY:param}"
+ set-header X-To-Param "%{TO-URL:QUERY:redirect}"