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}"

Reply via email to