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"

Reply via email to