diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst index 8a3bed3d862..fb87f909926 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:} =bar inbound.url. == "bar" URL component match, <:ref:`C`> is ``host``, ``path`` etc. -cond %{COOKIE:foo} =bar {in,out}bound.cookie.foo == "bar" Check a cookie value -cond %{FROM-URL:} =bar from.url. == "bar" Remap ``From URL`` component match, <:ref:`C`> 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`) transaction identifier -cond %{INTERNAL-TRANSACTION} internal() Check if transaction is internally generated -cond %{INBOUND:CLIENT-CERT:} inbound.client-cert. Access the mTLS / client certificate details, on the inbound (client) connection -cond %{INBOUND:SERVER-CERT:} inbound.client-cert. 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:} ="bar" outbound.url. == "bar" Next-hop URL component match, <:ref:`C`> is ``host`` etc. -cond %{NOW:} ="..." now. == "..." Current date/time in format, <:ref:`U`> selects time unit -cond %{OUTBOUND:CLIENT-CERT:} outbound.client-cert. Access the mTLS / client certificate details, on the outbound (upstream) connection -cond %{OUTbOUND:SERVER-CERT:} outbound.client-cert. 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:} =bar to.url. == "bar" Remap ``To URL`` component match, <:ref:`C`> is ``host`` etc. -cond %{TXN-COUNT} >10 txn-count() > 10 Number of transactions on client connection -cond %{URL: =bar {in,out}bound.url. == "bar" Context aware URL component match -cond %{GEO:} =bar geo. == "bar" IP to Geo mapping. <:ref:`C`> 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:} http.cntl. Check the state of the <:ref:`C`> HTTP control -cond %{INBOUND:} {in,out}bound.conn. inbound (:ref:`client, user agent`) 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:} =bar inbound.url. == "bar" URL component match, <:ref:`C`> is ``host``, ``path`` etc. +cond %{CLIENT-URL:QUERY:

} =bar inbound.url.query.

== "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:} =bar from.url. == "bar" Remap ``From URL`` component match, <:ref:`C`> is ``host`` etc. +cond %{FROM-URL:QUERY:

} =bar from.url.query.

== "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`) transaction identifier +cond %{INTERNAL-TRANSACTION} internal() Check if transaction is internally generated +cond %{INBOUND:CLIENT-CERT:} inbound.client-cert. Access the mTLS / client certificate details, on the inbound (client) connection +cond %{INBOUND:SERVER-CERT:} inbound.client-cert. 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:} ="bar" outbound.url. == "bar" Next-hop URL component match, <:ref:`C`> is ``host`` etc. +cond %{NEXT-HOP:QUERY:

} =bar outbound.url.query.

== "bar" Extract specific query parameter ``P`` from next-hop URL +cond %{NOW:} ="..." now. == "..." Current date/time in format, <:ref:`U`> selects time unit +cond %{OUTBOUND:CLIENT-CERT:} outbound.client-cert. Access the mTLS / client certificate details, on the outbound (upstream) connection +cond %{OUTbOUND:SERVER-CERT:} outbound.client-cert. 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:} =bar to.url. == "bar" Remap ``To URL`` component match, <:ref:`C`> is ``host`` etc. +cond %{TO-URL:QUERY:

} =bar to.url.query.

== "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: =bar {in,out}bound.url. == "bar" Context aware URL component match +cond %{GEO:} =bar geo. == "bar" IP to Geo mapping. <:ref:`C`> 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:} http.cntl. Check the state of the <:ref:`C`> HTTP control +cond %{INBOUND:} {in,out}bound.conn. inbound (:ref:`client, user agent`) 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.`` 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 34870279775..b4e24ced74e 100644 --- a/tools/hrw4u/src/hrw_symbols.py +++ b/tools/hrw4u/src/hrw_symbols.py @@ -399,12 +399,15 @@ def percent_to_ident_or_func(self, percent: str, section: SectionType | None) -> 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 94253fba0a9..3b80c5e5034 100644 --- a/tools/hrw4u/src/tables.py +++ b/tools/hrw4u/src/tables.py @@ -96,6 +96,7 @@ # 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 @@ "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 @@ "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 @@ "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 00000000000..45607a346a7 --- /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") ;)) })))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor to.url.query.target)))) (block { (blockItem (statement inbound.req.X-Target = (value "set") ;)) })))) })) (programItem (section SEND_REQUEST { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable outbound.url.query.backend) == (value "fast")))))) (block { (blockItem (statement outbound.req.X-Priority = (value "high") ;)) })))) })) (programItem (section SEND_RESPONSE { (sectionBody (statement inbound.resp.X-Query-Sub = (value "{inbound.url.query.sub}") ;)) (sectionBody (statement inbound.resp.X-From-Param = (value "{from.url.query.param}") ;)) (sectionBody (statement inbound.resp.X-To-Param = (value "{to.url.query.redirect}") ;)) })) ) 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 00000000000..fa7ccf8d862 --- /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 00000000000..9a771b1b134 --- /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}"