This is an automated email from the ASF dual-hosted git repository. cmcfarlen pushed a commit to branch 10.2.x in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit 376d814545c65a6400e380536f0183c1679cf818 Author: Leif Hedstrom <[email protected]> AuthorDate: Tue Feb 10 17:25:57 2026 -0700 hrw4u: Allow negation with the 'in' keyword (#12871) * hrw4u: Allow negation with the 'in' keyword * Addresses CoPilot's concerns (cherry picked from commit 948586f87f21c8d391fd8a948fabf88bc004b6bc) --- doc/admin-guide/configuration/hrw4u.en.rst | 25 ++++++++++++++++++++++ tools/hrw4u/grammar/hrw4u.g4 | 2 ++ tools/hrw4u/src/hrw_symbols.py | 4 +++- tools/hrw4u/src/visitor.py | 10 +++++++-- .../hrw4u/tests/data/conds/double-negation.ast.txt | 1 + .../tests/data/conds/double-negation.input.txt | 5 +++++ .../tests/data/conds/double-negation.output.txt | 5 +++++ tools/hrw4u/tests/data/conds/exceptions.txt | 2 ++ tools/hrw4u/tests/data/conds/not-in-ip.ast.txt | 1 + tools/hrw4u/tests/data/conds/not-in-ip.input.txt | 5 +++++ tools/hrw4u/tests/data/conds/not-in-ip.output.txt | 3 +++ tools/hrw4u/tests/data/conds/not-in-sets.ast.txt | 1 + tools/hrw4u/tests/data/conds/not-in-sets.input.txt | 5 +++++ .../hrw4u/tests/data/conds/not-in-sets.output.txt | 3 +++ 14 files changed, 69 insertions(+), 3 deletions(-) diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst index 2a4da8478c..e2a054383e 100644 --- a/doc/admin-guide/configuration/hrw4u.en.rst +++ b/doc/admin-guide/configuration/hrw4u.en.rst @@ -429,6 +429,7 @@ Operator HRW4U Syntax Description ~ foo ~ /pattern/ Regular expression match !~ foo !~ /pattern/ Regular expression non-match in [...] foo in ["a", "b"] Membership in a list of values +!in [...] foo !in ["a", "b"] Negated membership in a list of values ==================== ========================= ============================================ Modifiers @@ -611,6 +612,30 @@ Query string when the Origin server times out or the connection is refused:: } } +Flag Unrecognized ASNs +---------------------- + +This rule flags requests whose origin ASN is not in a known allowlist, +using the negated membership operator ``!in``:: + + REMAP { + if geo.ASN !in ["64496", "64511"] { + inbound.req.X-Known-ASN = "false"; + } + } + +Restrict to Internal Networks +----------------------------- + +This rule rejects requests that do not originate from known internal +IP ranges, using ``!in`` with an IP range:: + + REMAP { + if inbound.ip !in {192.168.0.0/16, 10.0.0.0/8} { + http.status = 403; + } + } + Check for existence of a header ------------------------------- diff --git a/tools/hrw4u/grammar/hrw4u.g4 b/tools/hrw4u/grammar/hrw4u.g4 index 5db49f51be..889bbddc56 100644 --- a/tools/hrw4u/grammar/hrw4u.g4 +++ b/tools/hrw4u/grammar/hrw4u.g4 @@ -192,7 +192,9 @@ comparison : comparable (EQUALS | NEQ | GT | LT) value modifier? | comparable (TILDE | NOT_TILDE) regex modifier? | comparable IN set modifier? + | comparable '!' IN set modifier? | comparable IN iprange + | comparable '!' IN iprange ; modifier diff --git a/tools/hrw4u/src/hrw_symbols.py b/tools/hrw4u/src/hrw_symbols.py index b4e24ced74..558827d6db 100644 --- a/tools/hrw4u/src/hrw_symbols.py +++ b/tools/hrw4u/src/hrw_symbols.py @@ -382,7 +382,9 @@ class InverseSymbolResolver(SymbolResolverBase): return t if " ~ " in t and " !~ " not in t: return t.replace(" ~ ", " !~ ", 1) - if any(op in t for op in (" in ", " > ", " < ")): + if " in " in t: + return t.replace(" in ", " !in ", 1) + if any(op in t for op in (" > ", " < ")): return f"!({t})" if t.endswith(')'): return f"!{t}" diff --git a/tools/hrw4u/src/visitor.py b/tools/hrw4u/src/visitor.py index 922187a6f7..ad240db05d 100644 --- a/tools/hrw4u/src/visitor.py +++ b/tools/hrw4u/src/visitor.py @@ -74,7 +74,7 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor): def _make_condition(self, cond_text: str, last: bool = False, negate: bool = False) -> str: self._dbg(f"make_condition: {cond_text} last={last} negate={negate}") - self._cond_state.not_ |= negate + self._cond_state.not_ ^= negate self._cond_state.last = last return f"cond {cond_text}" @@ -480,7 +480,13 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor): if not lhs: return operator = ctx.getChild(1) - negate = operator.symbol.type in (hrw4uParser.NEQ, hrw4uParser.NOT_TILDE) + + # Detect negation: '!=' and '!~' are single tokens (NEQ, NOT_TILDE), + # but '!in' is two separate tokens ('!' + IN). + if operator.getText() == '!': + negate = True + else: + negate = operator.symbol.type in (hrw4uParser.NEQ, hrw4uParser.NOT_TILDE) match ctx: case _ if ctx.value(): diff --git a/tools/hrw4u/tests/data/conds/double-negation.ast.txt b/tools/hrw4u/tests/data/conds/double-negation.ast.txt new file mode 100644 index 0000000000..c81c66c953 --- /dev/null +++ b/tools/hrw4u/tests/data/conds/double-negation.ast.txt @@ -0,0 +1 @@ +(program (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor ! (factor ( (expression (term (factor (comparison (comparable inbound.req.X-Debug) != (value "keep"))))) )))))) (block { (blockItem (statement inbound.req.X-Found = (value "yes") ;)) })))) })) <EOF>) diff --git a/tools/hrw4u/tests/data/conds/double-negation.input.txt b/tools/hrw4u/tests/data/conds/double-negation.input.txt new file mode 100644 index 0000000000..0e0fb4abec --- /dev/null +++ b/tools/hrw4u/tests/data/conds/double-negation.input.txt @@ -0,0 +1,5 @@ +REMAP { + if !(inbound.req.X-Debug != "keep") { + inbound.req.X-Found = "yes"; + } +} diff --git a/tools/hrw4u/tests/data/conds/double-negation.output.txt b/tools/hrw4u/tests/data/conds/double-negation.output.txt new file mode 100644 index 0000000000..9ee7a7dfe0 --- /dev/null +++ b/tools/hrw4u/tests/data/conds/double-negation.output.txt @@ -0,0 +1,5 @@ +cond %{REMAP_PSEUDO_HOOK} [AND] +cond %{GROUP} + cond %{CLIENT-HEADER:X-Debug} ="keep" +cond %{GROUP:END} + set-header X-Found "yes" diff --git a/tools/hrw4u/tests/data/conds/exceptions.txt b/tools/hrw4u/tests/data/conds/exceptions.txt index 792af0226f..85aecfe361 100644 --- a/tools/hrw4u/tests/data/conds/exceptions.txt +++ b/tools/hrw4u/tests/data/conds/exceptions.txt @@ -3,3 +3,5 @@ # # Implicit = comparisons implicit-cmp.input: u4wrh +# Double negation !(x != y) cancels to x == y, reverse can't reconstruct the original form +double-negation.input: hrw4u diff --git a/tools/hrw4u/tests/data/conds/not-in-ip.ast.txt b/tools/hrw4u/tests/data/conds/not-in-ip.ast.txt new file mode 100644 index 0000000000..8e73c073d8 --- /dev/null +++ b/tools/hrw4u/tests/data/conds/not-in-ip.ast.txt @@ -0,0 +1 @@ +(program (programItem (section SEND_REQUEST { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.ip) ! in (iprange { (ip (ipv4 192.168.0.0/16)) , (ip (ipv4 10.0.0.0/8)) })))))) (block { (blockItem (statement outbound.req.X-External = (value "true") ;)) })))) })) <EOF>) diff --git a/tools/hrw4u/tests/data/conds/not-in-ip.input.txt b/tools/hrw4u/tests/data/conds/not-in-ip.input.txt new file mode 100644 index 0000000000..abec2a1739 --- /dev/null +++ b/tools/hrw4u/tests/data/conds/not-in-ip.input.txt @@ -0,0 +1,5 @@ +SEND_REQUEST { + if inbound.ip !in {192.168.0.0/16, 10.0.0.0/8} { + outbound.req.X-External = "true"; + } +} diff --git a/tools/hrw4u/tests/data/conds/not-in-ip.output.txt b/tools/hrw4u/tests/data/conds/not-in-ip.output.txt new file mode 100644 index 0000000000..52d846d786 --- /dev/null +++ b/tools/hrw4u/tests/data/conds/not-in-ip.output.txt @@ -0,0 +1,3 @@ +cond %{SEND_REQUEST_HDR_HOOK} [AND] +cond %{IP:CLIENT} {192.168.0.0/16,10.0.0.0/8} [NOT] + set-header X-External "true" diff --git a/tools/hrw4u/tests/data/conds/not-in-sets.ast.txt b/tools/hrw4u/tests/data/conds/not-in-sets.ast.txt new file mode 100644 index 0000000000..1dba01808f --- /dev/null +++ b/tools/hrw4u/tests/data/conds/not-in-sets.ast.txt @@ -0,0 +1 @@ +(program (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable geo.ASN) ! in (set [ (value "64496") , (value "64511") ])))))) (block { (blockItem (statement inbound.req.X-Cloud = (value "false") ;)) })))) })) <EOF>) diff --git a/tools/hrw4u/tests/data/conds/not-in-sets.input.txt b/tools/hrw4u/tests/data/conds/not-in-sets.input.txt new file mode 100644 index 0000000000..ae6cd9f3bb --- /dev/null +++ b/tools/hrw4u/tests/data/conds/not-in-sets.input.txt @@ -0,0 +1,5 @@ +REMAP { + if geo.ASN !in ["64496", "64511"] { + inbound.req.X-Cloud = "false"; + } +} diff --git a/tools/hrw4u/tests/data/conds/not-in-sets.output.txt b/tools/hrw4u/tests/data/conds/not-in-sets.output.txt new file mode 100644 index 0000000000..43e88f1fa1 --- /dev/null +++ b/tools/hrw4u/tests/data/conds/not-in-sets.output.txt @@ -0,0 +1,3 @@ +cond %{REMAP_PSEUDO_HOOK} [AND] +cond %{GEO:ASN} ("64496","64511") [NOT] + set-header X-Cloud "false"
