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 2919403219 hrw4u: Add support for += / add-header (#12588)
2919403219 is described below
commit 2919403219f02cc450723a3bc43c38e11105c148
Author: Leif Hedstrom <[email protected]>
AuthorDate: Tue Oct 21 21:31:41 2025 -0700
hrw4u: Add support for += / add-header (#12588)
---
doc/admin-guide/configuration/hrw4u.en.rst | 23 ++
tools/hrw4u/grammar/hrw4u.g4 | 2 +
tools/hrw4u/src/common.py | 1 +
tools/hrw4u/src/generators.py | 20 +-
tools/hrw4u/src/hrw_symbols.py | 28 +-
tools/hrw4u/src/kg_visitor.py | 33 +-
tools/hrw4u/src/lsp/completions.py | 16 +-
tools/hrw4u/src/lsp/hover.py | 43 ++-
tools/hrw4u/src/suggestions.py | 9 +-
tools/hrw4u/src/symbols.py | 82 +++--
tools/hrw4u/src/symbols_base.py | 22 +-
tools/hrw4u/src/tables.py | 350 ++++++---------------
tools/hrw4u/src/types.py | 76 ++++-
tools/hrw4u/src/visitor.py | 12 +
tools/hrw4u/tests/data/hooks/remap.ast.txt | 2 +-
tools/hrw4u/tests/data/hooks/remap.input.txt | 2 +
tools/hrw4u/tests/data/hooks/remap.output.txt | 2 +
tools/hrw4u/tests/data/ops/exceptions.txt | 2 +
.../tests/data/ops/http_cntl_valid_bools.ast.txt | 2 +-
.../data/ops/http_cntl_valid_bools.output.txt | 2 +-
tools/hrw4u/tests/data/ops/qsa.output.txt | 2 +-
21 files changed, 370 insertions(+), 361 deletions(-)
diff --git a/doc/admin-guide/configuration/hrw4u.en.rst
b/doc/admin-guide/configuration/hrw4u.en.rst
index 80beec6afe..39002b919d 100644
--- a/doc/admin-guide/configuration/hrw4u.en.rst
+++ b/doc/admin-guide/configuration/hrw4u.en.rst
@@ -231,6 +231,7 @@ The preference is the assignment style when appropriate.
============================= =================================
================================================
Header Rewrite HRW4U Description
============================= =================================
================================================
+add-header X-bar foo inbound.{req,resp}.x-Bar += "bar" Add the header
to (possibly) an existing header
counter my_stat counter("my_stat") Increment
internal counter
rm-client-header X-Foo inbound.req.X-Foo = "" Remove a
client request header
rm-cookie foo {in,out}bound.cookie.foo = "" Remove the
cookie named foo
@@ -254,6 +255,28 @@ set-status-reason "No" http.status.reason = "no"
Set the response
set-http-cntl http.cntl.<C> = bool Turn on/off
<:ref:`C<admin-plugins-header-rewrite-set-http-cntl>`> controllers
============================= =================================
================================================
+Adding Headers with the += Operator
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+HRW4U provides a special ``+=`` operator for adding headers::
+
+ REMAP {
+ # Using += to add a header (maps to add-header)
+ inbound.req.X-Custom-Header += "new-value";
+ }
+
+The ``+=`` operator only works with the following pre-defined symbols:
+
+- ``inbound.req.<header>`` - Client request headers
+- ``inbound.resp.<header>`` - Origin response headers
+- ``outbound.req.<header>`` - Outbound request headers (context-restricted)
+- ``outbound.resp.<header>`` - Outbound response headers (context-restricted)
+
+.. note::
+ The ``+=`` operator differs from ``=`` in that ``=`` will replace/set the
header value (mapping to
+ ``set-header``), while ``+=`` will add a new instance of the header
(mapping to ``add-header``).
+ This is important for headers that can have multiple values, such as
``Set-Cookie`` or custom headers.
+
In addition to those operators above, HRW4U supports the following special
operators without arguments:
================= ============================ ================================
diff --git a/tools/hrw4u/grammar/hrw4u.g4 b/tools/hrw4u/grammar/hrw4u.g4
index 852d72c056..a91e314336 100644
--- a/tools/hrw4u/grammar/hrw4u.g4
+++ b/tools/hrw4u/grammar/hrw4u.g4
@@ -69,6 +69,7 @@ LBRACKET : '[';
RBRACKET : ']';
EQUALS : '==';
EQUAL : '=';
+PLUSEQUAL : '+=';
NEQ : '!=';
GT : '>';
LT : '<';
@@ -127,6 +128,7 @@ statement
: BREAK SEMICOLON
| functionCall SEMICOLON
| lhs=IDENT EQUAL value SEMICOLON
+ | lhs=IDENT PLUSEQUAL value SEMICOLON
| op=IDENT SEMICOLON
;
diff --git a/tools/hrw4u/src/common.py b/tools/hrw4u/src/common.py
index c05a4ad40b..28478933c0 100644
--- a/tools/hrw4u/src/common.py
+++ b/tools/hrw4u/src/common.py
@@ -66,6 +66,7 @@ class SystemDefaults:
class HeaderOperations:
"""Operation constants for various resource types"""
OPERATIONS: Final = (MagicStrings.RM_HEADER.value,
MagicStrings.SET_HEADER.value)
+ ADD_OPERATION: Final = MagicStrings.ADD_HEADER.value
COOKIE_OPERATIONS: Final = (MagicStrings.RM_COOKIE.value,
MagicStrings.SET_COOKIE.value)
DESTINATION_OPERATIONS: Final = (MagicStrings.RM_DESTINATION.value,
MagicStrings.SET_DESTINATION.value)
diff --git a/tools/hrw4u/src/generators.py b/tools/hrw4u/src/generators.py
index c5924b541e..31a547cd71 100644
--- a/tools/hrw4u/src/generators.py
+++ b/tools/hrw4u/src/generators.py
@@ -40,20 +40,22 @@ class TableGenerator:
"""Extract clean tag name from %{TAG:payload} format."""
return tag.strip().removeprefix('%{').removesuffix('}').split(':')[0]
- def generate_reverse_condition_map(self, condition_map: tuple[tuple[str,
tuple], ...]) -> dict[str, str]:
+ def generate_reverse_condition_map(self, condition_map: tuple[tuple[str,
Any], ...]) -> dict[str, str]:
"""Generate reverse condition mapping from forward condition map."""
reverse_map = {}
- for ident_key, (tag, _, _, _, _, _) in condition_map:
+ for ident_key, params in condition_map:
if not ident_key.endswith('.'):
- clean_tag = self._clean_tag(tag)
- reverse_map[clean_tag] = ident_key
+ tag = params.target if params else None
+ if tag:
+ clean_tag = self._clean_tag(tag)
+ reverse_map[clean_tag] = ident_key
return reverse_map
- def generate_reverse_function_map(self, function_map: tuple[tuple[str,
tuple], ...]) -> dict[str, str]:
+ def generate_reverse_function_map(self, function_map: tuple[tuple[str,
Any], ...]) -> dict[str, str]:
"""Generate reverse function mapping from forward function map."""
- return {tag: func_name for func_name, (tag, _) in function_map}
+ return {params.target: func_name for func_name, params in function_map}
@cache
def generate_section_hook_mapping(self) -> dict[str, str]:
@@ -70,7 +72,9 @@ class TableGenerator:
from hrw4u.tables import CONDITION_MAP
ip_mapping = {}
- for condition_key, (tag, *_, reverse_info) in CONDITION_MAP.items():
+ for condition_key, params in CONDITION_MAP.items():
+ tag = params.target if params else None
+ reverse_info = params.rev if params else None
if reverse_info and reverse_info.get("reverse_tag") == "IP":
payload = reverse_info.get("reverse_payload")
if payload:
@@ -162,7 +166,7 @@ def get_reverse_condition_map(condition_map: dict[str,
tuple]) -> dict[str, str]
return
_table_generator.generate_reverse_condition_map(tuple(condition_map.items()))
-def get_reverse_function_map(function_map: dict[str, tuple]) -> dict[str, str]:
+def get_reverse_function_map(function_map: dict[str, Any]) -> dict[str, str]:
"""Get reverse function mapping."""
return
_table_generator.generate_reverse_function_map(tuple(function_map.items()))
diff --git a/tools/hrw4u/src/hrw_symbols.py b/tools/hrw4u/src/hrw_symbols.py
index b25049cff0..3487027977 100644
--- a/tools/hrw4u/src/hrw_symbols.py
+++ b/tools/hrw4u/src/hrw_symbols.py
@@ -42,8 +42,9 @@ class InverseSymbolResolver(SymbolResolverBase):
return reverse_map
# Fallback to building from condition map if not available
result = {}
- for ident_key, (tag, _, uppercase, *_) in self._condition_map.items():
+ for ident_key, params in self._condition_map.items():
if not ident_key.endswith("."):
+ tag = params.target
tag_key =
tag.strip().removeprefix("%{").removesuffix("}").split(":", 1)[0]
result[tag_key] = ident_key
return result
@@ -55,8 +56,10 @@ class InverseSymbolResolver(SymbolResolverBase):
return reverse_map
# Fallback to building from condition map if not available
result = []
- for ident_key, (tag, _, uppercase, *_) in self._condition_map.items():
+ for ident_key, params in self._condition_map.items():
if ident_key.endswith("."):
+ tag = params.target
+ uppercase = params.upper if params else False
result.append((tag, ident_key, uppercase))
return result
@@ -65,7 +68,7 @@ class InverseSymbolResolver(SymbolResolverBase):
"""Cached reverse function mapping."""
if reverse_map := tables.REVERSE_RESOLUTION_MAP.get('FUNCTIONS'):
return reverse_map
- return {tag: fn_name for fn_name, (tag, _) in
self._function_map.items()}
+ return {params.target: fn_name for fn_name, params in
self._function_map.items()}
@cached_property
def _rev_sections(self) -> dict[str, str]:
@@ -138,8 +141,10 @@ class InverseSymbolResolver(SymbolResolverBase):
elif tag == "IP":
return None
- for key, (mapped_tag, _, _, restricted, _, _) in
self._condition_map.items():
+ for key, params in self._condition_map.items():
+ mapped_tag = params.target
tag_part = mapped_tag.replace("%{", "").replace("}",
"").split(":")[0]
+ restricted = params.sections if params else None
if tag_part == tag:
if not restricted or not section or section not in restricted:
pass
@@ -279,12 +284,15 @@ class InverseSymbolResolver(SymbolResolverBase):
qargs = [status_code,
self._rewrite_inline_percents(f'"{url_arg}"', section)]
elif name == "add-header" and args:
+ # Convert add-header command to += syntax for reverse mapping
header_name = args[0]
prefix = self.get_prefix_for_context("header_ops", section)
prefixed_header = f"{prefix}{header_name}"
- processed_args = [self._rewrite_inline_percents(arg, section) for
arg in args[1:]]
- qargs = [prefixed_header] + processed_args
+ if len(args) > 1:
+ value = self._rewrite_inline_percents(args[1], section)
+ return f"{prefixed_header} += {value}"
+ raise SymbolResolutionError("add-header", "Missing value for
add-header")
elif name == "set-plugin-cntl" and len(args) >= 2:
qualifier = args[0]
value = args[1]
@@ -472,12 +480,14 @@ class InverseSymbolResolver(SymbolResolverBase):
rewritten_value = self._rewrite_inline_percents(value,
section)
return f"{var_name} = {rewritten_value}"
- for lhs_key, (commands, _, uppercase, _) in
tables.OPERATOR_MAP.items():
+ for lhs_key, params in tables.OPERATOR_MAP.items():
+ commands = params.target if params else None
if (isinstance(commands, (list, tuple)) and cmd in commands) or
(cmd == commands):
+ uppercase = params.upper if params else False
return self._handle_operator_command(cmd, toks, lhs_key,
uppercase, section)
- for name, (forward_cmd, _) in tables.STATEMENT_FUNCTION_MAP.items():
- if forward_cmd == cmd:
+ for name, params in tables.STATEMENT_FUNCTION_MAP.items():
+ if params.target == cmd:
return self._handle_statement_function(name, args, section,
op_state)
raise SymbolResolutionError(line, f"Unknown operator: {cmd}")
diff --git a/tools/hrw4u/src/kg_visitor.py b/tools/hrw4u/src/kg_visitor.py
index 78178dadeb..8b4b88929a 100644
--- a/tools/hrw4u/src/kg_visitor.py
+++ b/tools/hrw4u/src/kg_visitor.py
@@ -151,48 +151,55 @@ class KnowledgeGraphVisitor(hrw4uVisitor, BaseHRWVisitor):
"description": f"Apache Traffic Server hook for
{hrw4u_section}"
}, f"ats_hook:{ats_hook}")
- for op_pattern, (command, validator, uppercase, restricted_sections)
in OPERATOR_MAP.items():
+ for op_pattern, params in OPERATOR_MAP.items():
+ validator = params.validate if params else None
+ restricted_sections = params.sections if params else None
+ command = params.target if params else None
self._add_node(
"SemanticOperator", {
"pattern": op_pattern,
"hrw_operator": str(command) if isinstance(command, str)
else str(command),
- "validates_uppercase": uppercase,
+ "validates_uppercase": params.upper if params else False,
"has_validator": validator is not None,
"restricted_sections": [s.value for s in
restricted_sections] if restricted_sections else None,
"description": f"Operator pattern {op_pattern} ->
{command}"
}, f"sem_op:{op_pattern}")
- for cond_pattern, (tag, validator, uppercase, restricted,
default_expr, reverse_info) in CONDITION_MAP.items():
+ for cond_pattern, params in CONDITION_MAP.items():
+ validator = params.validate if params else None
+ restricted = params.sections if params else None
+ reverse_info = params.rev if params else None
+ tag = params.target if params else None
self._add_node(
"SemanticCondition", {
"pattern": cond_pattern,
"hrw_condition": tag,
- "validates_uppercase": uppercase,
+ "validates_uppercase": params.upper if params else False,
"has_validator": validator is not None,
"restricted_sections": [s.value for s in restricted] if
restricted else None,
- "has_default_expression": default_expr,
+ "has_default_expression": params.prefix if params else
False,
"reverse_mapping": reverse_info,
"description": f"Condition pattern {cond_pattern} -> {tag}"
}, f"sem_cond:{cond_pattern}")
- for func_name, (tag, validator) in FUNCTION_MAP.items():
+ for func_name, params in FUNCTION_MAP.items():
self._add_node(
"SemanticFunction", {
"name": func_name,
- "hrw_condition": tag,
- "has_validator": validator is not None,
+ "hrw_condition": params.target,
+ "has_validator": params.validate is not None,
"type": "condition_function",
- "description": f"Function {func_name} -> %{{{tag}}}"
+ "description": f"Function {func_name} ->
%{{{params.target}}}"
}, f"sem_func:{func_name}")
- for func_name, (command, validator) in STATEMENT_FUNCTION_MAP.items():
+ for func_name, params in STATEMENT_FUNCTION_MAP.items():
self._add_node(
"SemanticFunction", {
"name": func_name,
- "hrw_operator": command,
- "has_validator": validator is not None,
+ "hrw_operator": params.target,
+ "has_validator": params.validate is not None,
"type": "statement_function",
- "description": f"Statement function {func_name} ->
{command}"
+ "description": f"Statement function {func_name} ->
{params.target}"
}, f"sem_stmt_func:{func_name}")
for suffix_group in SuffixGroup:
diff --git a/tools/hrw4u/src/lsp/completions.py
b/tools/hrw4u/src/lsp/completions.py
index ab13ad6abc..1e8cf04197 100644
--- a/tools/hrw4u/src/lsp/completions.py
+++ b/tools/hrw4u/src/lsp/completions.py
@@ -224,18 +224,22 @@ class CompletionProvider:
seen_labels = set()
# Add condition completions
- for key, (tag, _, _, sections, _, _) in tables.CONDITION_MAP.items():
+ for key, params in tables.CONDITION_MAP.items():
if key.startswith(base_prefix) and key not in seen_labels:
seen_labels.add(key)
+ sections = params.sections if params else None
+ tag = params.target if params else None
item = self.builder.condition_completion(key, tag, sections,
current_section, replacement_range)
if item:
completions.append(item.to_lsp_dict())
# Add operator completions
- for key, (commands, _, _, sections) in tables.OPERATOR_MAP.items():
+ for key, params in tables.OPERATOR_MAP.items():
if key.startswith(base_prefix) and key not in seen_labels:
seen_labels.add(key)
+ sections = params.sections if params else None
+ commands = params.target if params else None
item = self.builder.operator_completion(key, commands,
sections, current_section, replacement_range)
if item:
@@ -248,13 +252,13 @@ class CompletionProvider:
completions = []
# Regular functions
- for func_name, (tag, _) in tables.FUNCTION_MAP.items():
- item = self.builder.function_completion(func_name, tag, "Function")
+ for func_name, params in tables.FUNCTION_MAP.items():
+ item = self.builder.function_completion(func_name, params.target,
"Function")
completions.append(item.to_lsp_dict())
# Statement functions
- for func_name, (tag, _) in tables.STATEMENT_FUNCTION_MAP.items():
- item = self.builder.function_completion(func_name, tag,
"Statement")
+ for func_name, params in tables.STATEMENT_FUNCTION_MAP.items():
+ item = self.builder.function_completion(func_name, params.target,
"Statement")
completions.append(item.to_lsp_dict())
return completions
diff --git a/tools/hrw4u/src/lsp/hover.py b/tools/hrw4u/src/lsp/hover.py
index 343fc2545d..6a6f6c92c5 100644
--- a/tools/hrw4u/src/lsp/hover.py
+++ b/tools/hrw4u/src/lsp/hover.py
@@ -403,8 +403,15 @@ class OperatorHoverProvider:
# Check exact matches first
if operator in OPERATOR_MAP:
- commands, _, is_prefix, sections = OPERATOR_MAP[operator]
- cmd_str = commands if isinstance(commands, str) else ' /
'.join(commands)
+ params = OPERATOR_MAP[operator]
+ commands = params.target if params else None
+ if isinstance(commands, str):
+ cmd_str = commands
+ elif commands:
+ cmd_str = ' / '.join(commands)
+ else:
+ cmd_str = "unknown"
+ sections = params.sections if params else None
section_info = ""
if sections:
@@ -415,10 +422,17 @@ class OperatorHoverProvider:
f"**{operator}** - HRW4U Operator\n\n" + f"**Maps to:**
`{cmd_str}`{section_info}")
# Check prefix matches
- for key, (commands, _, is_prefix, sections) in OPERATOR_MAP.items():
- if is_prefix and operator.startswith(key):
- cmd_str = commands if isinstance(commands, str) else ' /
'.join(commands)
+ for key, params in OPERATOR_MAP.items():
+ if key.endswith('.') and operator.startswith(key):
+ commands = params.target if params else None
+ if isinstance(commands, str):
+ cmd_str = commands
+ elif commands:
+ cmd_str = ' / '.join(commands)
+ else:
+ cmd_str = "unknown"
suffix = operator[len(key):]
+ sections = params.sections if params else None
section_info = ""
if sections:
@@ -431,7 +445,9 @@ class OperatorHoverProvider:
# Check condition map
if operator in CONDITION_MAP:
- tag, _, is_prefix, sections, _, _ = CONDITION_MAP[operator]
+ params = CONDITION_MAP[operator]
+ tag = params.target if params else None
+ sections = params.sections if params else None
section_info = ""
if sections:
@@ -442,9 +458,11 @@ class OperatorHoverProvider:
f"**{operator}** - HRW4U Condition\n\n" + f"**Maps to:**
`{tag}`{section_info}")
# Check condition prefix matches
- for key, (tag, _, is_prefix, sections, is_conditional, _) in
CONDITION_MAP.items():
- if is_prefix and operator.startswith(key):
+ for key, params in CONDITION_MAP.items():
+ if key.endswith('.') and operator.startswith(key):
+ tag = params.target if params else None
suffix = operator[len(key):]
+ sections = params.sections if params else None
section_info = ""
if sections:
@@ -581,14 +599,15 @@ class FunctionHoverProvider:
# Fallback to basic documentation
if function_name in FUNCTION_MAP:
- tag, _ = FUNCTION_MAP[function_name]
+ params = FUNCTION_MAP[function_name]
return HoverInfoProvider.create_hover_info(
- f"**{function_name}()** - HRW4U Function\n\n" + f"**Maps to:**
`{tag}`\n\n" + f"Used in conditional expressions.")
+ f"**{function_name}()** - HRW4U Function\n\n" + f"**Maps to:**
`{params.target}`\n\n" +
+ f"Used in conditional expressions.")
if function_name in STATEMENT_FUNCTION_MAP:
- tag, _ = STATEMENT_FUNCTION_MAP[function_name]
+ params = STATEMENT_FUNCTION_MAP[function_name]
return HoverInfoProvider.create_hover_info(
- f"**{function_name}()** - HRW4U Statement Function\n\n" +
f"**Maps to:** `{tag}`\n\n" +
+ f"**{function_name}()** - HRW4U Statement Function\n\n" +
f"**Maps to:** `{params.target}`\n\n" +
f"Used as statements in code blocks.")
return HoverInfoProvider.create_hover_info(f"**{function_name}()** -
Unknown HRW4U function")
diff --git a/tools/hrw4u/src/suggestions.py b/tools/hrw4u/src/suggestions.py
index c1570e847a..13ce94c30b 100644
--- a/tools/hrw4u/src/suggestions.py
+++ b/tools/hrw4u/src/suggestions.py
@@ -85,11 +85,12 @@ class SuggestionEngine:
def _is_symbol_valid_in_section(self, symbol: str, section: SectionType,
context_type: str) -> bool:
table_map = tables.OPERATOR_MAP if context_type == 'assignment' else
tables.CONDITION_MAP
- tuple_index = 3 if context_type == 'assignment' else 3
- for key, data in table_map.items():
- if (key == symbol or key == f"{symbol}.") and data[tuple_index]:
- return section in data[tuple_index]
+ for key, params in table_map.items():
+ if key == symbol or key == f"{symbol}.":
+ sections = params.sections if params else None
+ if sections:
+ return section in sections
return True
diff --git a/tools/hrw4u/src/symbols.py b/tools/hrw4u/src/symbols.py
index 1d88305da8..a0f66de5fe 100644
--- a/tools/hrw4u/src/symbols.py
+++ b/tools/hrw4u/src/symbols.py
@@ -39,8 +39,8 @@ class SymbolResolver(SymbolResolverBase):
def get_statement_spec(self, name: str) -> tuple[str, Callable[[str],
None] | None]:
# Use cached lookup from base class
- if result := self._lookup_statement_function_cached(name):
- return result
+ if params := self._lookup_statement_function_cached(name):
+ return params.target, params.validate
raise SymbolResolutionError(name, "Unknown operator or invalid
standalone use")
def declare_variable(self, name: str, type_name: str) -> str:
@@ -63,32 +63,31 @@ class SymbolResolver(SymbolResolverBase):
def resolve_assignment(self, name: str, value: str, section: SectionType |
None = None) -> str:
with self.debug_context("resolve_assignment", name, value, section):
- for op_key, (commands, validator, uppercase, restricted_sections)
in self._operator_map.items():
+ for op_key, params in self._operator_map.items():
if op_key.endswith("."):
if name.startswith(op_key):
- self.validate_section_access(name, section,
restricted_sections)
+ self.validate_section_access(name, section,
params.sections if params else None)
qualifier = name[len(op_key):]
- if uppercase:
+ if params and params.upper:
qualifier = qualifier.upper()
- if validator:
- validator(qualifier)
+ if params and params.validate:
+ params.validate(qualifier)
- # Add boolean value validation for http.cntl
assignments.
+ # Add boolean value validation for http.cntl
assignments
if op_key == "http.cntl.":
types.SuffixGroup.BOOL_FIELDS.validate(value)
+ commands = params.target if params else None
if isinstance(commands, (list, tuple)):
- if value == '""':
- return f"{commands[0]} {qualifier}"
- else:
- return f"{commands[1]} {qualifier} {value}"
- else:
- return f"{commands} {qualifier} {value}"
+ return f"{commands[0 if value == '\"\"' else 1]}
{qualifier}" + ("" if value == '""' else f" {value}")
+ return f"{commands} {qualifier} {value}"
+
elif name == op_key:
- self.validate_section_access(name, section,
restricted_sections)
- if validator:
- validator(value)
- return f"{commands} {value}"
+ # Exact match - validate and return
+ self.validate_section_access(name, section,
params.sections if params else None)
+ if params and params.validate:
+ params.validate(value)
+ return f"{params.target if params else None} {value}"
if resolved_lhs := self.symbol_for(name):
if resolved_rhs := self.symbol_for(value):
@@ -105,26 +104,51 @@ class SymbolResolver(SymbolResolverBase):
error.add_symbol_suggestion(suggestions)
raise error
+ def resolve_add_assignment(self, name: str, value: str, section:
SectionType | None = None) -> str:
+ """Resolve += assignment, if it is supported for the given operator."""
+ with self.debug_context("resolve_add_assignment", name, value,
section):
+ for op_key, params in self._operator_map.items():
+ if op_key.endswith(".") and name.startswith(op_key) and params
and params.add:
+ self.validate_section_access(name, section,
params.sections)
+ qualifier = name[len(op_key):]
+ if params.validate:
+ params.validate(qualifier)
+
+ from hrw4u.common import HeaderOperations
+ return f"{HeaderOperations.ADD_OPERATION} {qualifier}
{value}"
+
+ # += not allowed if no matching operator with 'add' flag found
+ error = SymbolResolutionError(name, "+= operator is not supported
for this assignment")
+ error.add_note("Only operators with 'add' flag support +=")
+ raise error
+
def resolve_condition(self, name: str, section: SectionType | None = None)
-> tuple[str, bool]:
with self.debug_context("resolve_condition", name, section):
if symbol := self.symbol_for(name):
return symbol.as_cond(), False
- if condition_info := self._lookup_condition_cached(name):
- tag, _, _, restricted, default_expr, _ = condition_info
+ if params := self._lookup_condition_cached(name):
+ tag = params.target if params else None
+ restricted = params.sections if params else None
self.validate_section_access(name, section, restricted)
- return tag, default_expr
+ # For exact matches, default_expr is determined by whether
it's a prefix pattern
+ return tag, False
# Check prefix matches using base class utility
prefix_matches = self.find_prefix_matches(name,
self._condition_map)
- for prefix, (tag, validator, uppercase, restricted, default_expr,
_) in prefix_matches:
+ for prefix, params in prefix_matches:
+ tag = params.target if params else None
+ validator = params.validate if params else None
+ restricted = params.sections if params else None
+
self.validate_section_access(name, section, restricted)
suffix = name[len(prefix):]
- suffix_norm = suffix.upper() if uppercase else suffix
+ suffix_norm = suffix.upper() if (params and params.upper) else
suffix
if validator:
validator(suffix_norm)
resolved = f"%{{{tag}:{suffix_norm}}}"
- return resolved, default_expr
+ # For prefix matches, default_expr is True (indicated by
prefix flag)
+ return resolved, (params.prefix if params else False)
error = SymbolResolutionError(name, "Unknown condition symbol")
declared_vars = list(self._symbols.keys())
@@ -135,8 +159,9 @@ class SymbolResolver(SymbolResolverBase):
def resolve_function(self, func_name: str, args: list[str], strip_quotes:
bool = False) -> str:
with self.debug_context("resolve_function", func_name, args):
- if function_info := self._lookup_function_cached(func_name):
- tag, validator = function_info
+ if params := self._lookup_function_cached(func_name):
+ tag = params.target
+ validator = params.validate
if validator:
validator(args)
@@ -156,8 +181,9 @@ class SymbolResolver(SymbolResolverBase):
def resolve_statement_func(self, func_name: str, args: list[str]) -> str:
with self.debug_context("resolve_statement_func", func_name, args):
- if function_info :=
self._lookup_statement_function_cached(func_name):
- command, validator = function_info
+ if params := self._lookup_statement_function_cached(func_name):
+ command = params.target
+ validator = params.validate
if validator:
validator(args)
diff --git a/tools/hrw4u/src/symbols_base.py b/tools/hrw4u/src/symbols_base.py
index c0103ea9a0..5749065074 100644
--- a/tools/hrw4u/src/symbols_base.py
+++ b/tools/hrw4u/src/symbols_base.py
@@ -38,22 +38,19 @@ class SymbolResolverBase:
# Cached table access for performance - Python 3.11+ cached_property
@cached_property
- def _condition_map(
- self) -> dict[str, tuple[str, Callable[[str], None] | None, bool,
set[SectionType] | None, bool, dict | None]]:
+ def _condition_map(self) -> dict[str, types.MapParams]:
return tables.CONDITION_MAP
@cached_property
- def _operator_map(
- self
- ) -> dict[str, tuple[str | list[str] | tuple[str, ...], Callable[[str],
None] | None, bool, set[SectionType] | None]]:
+ def _operator_map(self) -> dict[str, types.MapParams]:
return tables.OPERATOR_MAP
@cached_property
- def _function_map(self) -> dict[str, tuple[str, Callable[[list[str]],
None] | None]]:
+ def _function_map(self) -> dict[str, types.MapParams]:
return tables.FUNCTION_MAP
@cached_property
- def _statement_function_map(self) -> dict[str, tuple[str,
Callable[[list[str]], None] | None]]:
+ def _statement_function_map(self) -> dict[str, types.MapParams]:
return tables.STATEMENT_FUNCTION_MAP
@cached_property
@@ -65,22 +62,19 @@ class SymbolResolverBase:
raise SymbolResolutionError(name, f"{name} is not available in the
{section.value} section")
@lru_cache(maxsize=256)
- def _lookup_condition_cached(
- self, name: str) -> tuple[str, Callable[[str], None] | None, bool,
set[SectionType] | None, bool, dict | None] | None:
+ def _lookup_condition_cached(self, name: str) -> types.MapParams | None:
return self._condition_map.get(name)
@lru_cache(maxsize=256)
- def _lookup_operator_cached(
- self, name: str
- ) -> tuple[str | list[str] | tuple[str, ...], Callable[[str], None] |
None, bool, set[SectionType] | None] | None:
+ def _lookup_operator_cached(self, name: str) -> types.MapParams | None:
return self._operator_map.get(name)
@lru_cache(maxsize=128)
- def _lookup_function_cached(self, name: str) -> tuple[str,
Callable[[list[str]], None] | None] | None:
+ def _lookup_function_cached(self, name: str) -> types.MapParams | None:
return self._function_map.get(name)
@lru_cache(maxsize=128)
- def _lookup_statement_function_cached(self, name: str) -> tuple[str,
Callable[[list[str]], None] | None] | None:
+ def _lookup_statement_function_cached(self, name: str) -> types.MapParams
| None:
return self._statement_function_map.get(name)
def _debug_enter(self, method_name: str, *args: Any) -> None:
diff --git a/tools/hrw4u/src/tables.py b/tools/hrw4u/src/tables.py
index 7454f69276..85d93394ba 100644
--- a/tools/hrw4u/src/tables.py
+++ b/tools/hrw4u/src/tables.py
@@ -20,274 +20,102 @@ from typing import Final, Callable
from dataclasses import dataclass
from hrw4u.generators import get_complete_reverse_resolution_map
from hrw4u.validation import Validator
-import hrw4u.types as types
+from hrw4u.types import MapParams, SuffixGroup
from hrw4u.states import SectionType
from hrw4u.common import HeaderOperations
-OPERATOR_MAP: dict[str, tuple[str | list[str] | tuple[str, ...],
Callable[[str], None] | None, bool, set[SectionType] | None]] = {
- "http.cntl.": ("set-http-cntl",
Validator.suffix_group(types.SuffixGroup.HTTP_CNTL_FIELDS), True, None),
- "http.status.reason": ("set-status-reason", Validator.quoted_or_simple(),
False, None),
- "http.status": ("set-status", Validator.range(0, 999), False, None),
- "inbound.conn.dscp": ("set-conn-dscp", Validator.nbit_int(6), False, None),
- "inbound.conn.mark": ("set-conn-mark", Validator.nbit_int(32), False,
None),
- "outbound.conn.dscp":
- ("set-conn-dscp", Validator.nbit_int(6), False,
{SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
- "outbound.conn.mark":
- ("set-conn-mark", Validator.nbit_int(32), False,
{SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
- "inbound.cookie.": (HeaderOperations.COOKIE_OPERATIONS,
Validator.http_token(), False, None),
- "inbound.req.": (HeaderOperations.OPERATIONS,
Validator.http_header_name(), False, None),
- "inbound.resp.body": ("set-body", Validator.quoted_or_simple(), False,
None),
- "inbound.resp.": (HeaderOperations.OPERATIONS,
Validator.http_header_name(), False, None),
- "inbound.status.reason": ("set-status-reason",
Validator.quoted_or_simple(), False, None),
- "inbound.status": ("set-status", Validator.range(0, 999), False, None),
- "inbound.url.": (HeaderOperations.DESTINATION_OPERATIONS,
Validator.suffix_group(types.SuffixGroup.URL_FIELDS), True, None),
- "outbound.cookie.":
- (
- HeaderOperations.COOKIE_OPERATIONS, Validator.http_token(), False,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST}),
- "outbound.req.":
- (
- HeaderOperations.OPERATIONS, Validator.http_header_name(), False,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST}),
- "outbound.resp.":
- (
- HeaderOperations.OPERATIONS, Validator.http_header_name(), False,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
- "outbound.status.reason":
- (
- "set-status-reason", Validator.quoted_or_simple(), False,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
- "outbound.status":
- (
- "set-status", Validator.range(0, 999), False,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
- "outbound.url.":
- (
- HeaderOperations.DESTINATION_OPERATIONS,
Validator.suffix_group(types.SuffixGroup.URL_FIELDS), True,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST})
+# yapf: disable
+OPERATOR_MAP: dict[str, MapParams] = {
+ "http.cntl.": MapParams(target="set-http-cntl", upper=True,
validate=Validator.suffix_group(SuffixGroup.HTTP_CNTL_FIELDS)),
+ "http.status.reason": MapParams(target="set-status-reason",
validate=Validator.quoted_or_simple()),
+ "http.status": MapParams(target="set-status", validate=Validator.range(0,
999)),
+ "inbound.conn.dscp": MapParams(target="set-conn-dscp",
validate=Validator.nbit_int(6)),
+ "inbound.conn.mark": MapParams(target="set-conn-mark",
validate=Validator.nbit_int(32)),
+ "outbound.conn.dscp": MapParams(target="set-conn-dscp",
validate=Validator.nbit_int(6), sections={SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST}),
+ "outbound.conn.mark": MapParams(target="set-conn-mark",
validate=Validator.nbit_int(32), sections={SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST}),
+ "inbound.cookie.": MapParams(target=HeaderOperations.COOKIE_OPERATIONS,
validate=Validator.http_token()),
+ "inbound.req.": MapParams(target=HeaderOperations.OPERATIONS, add=True,
validate=Validator.http_header_name()),
+ "inbound.resp.body": MapParams(target="set-body",
validate=Validator.quoted_or_simple()),
+ "inbound.resp.": MapParams(target=HeaderOperations.OPERATIONS, add=True,
validate=Validator.http_header_name()),
+ "inbound.status.reason": MapParams(target="set-status-reason",
validate=Validator.quoted_or_simple()),
+ "inbound.status": MapParams(target="set-status",
validate=Validator.range(0, 999)),
+ "inbound.url.": MapParams(target=HeaderOperations.DESTINATION_OPERATIONS,
upper=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
+ "outbound.cookie.": MapParams(target=HeaderOperations.COOKIE_OPERATIONS,
validate=Validator.http_token(), sections={SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST}),
+ "outbound.req.": MapParams(target=HeaderOperations.OPERATIONS, add=True,
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST}),
+ "outbound.resp.": MapParams(target=HeaderOperations.OPERATIONS, add=True,
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
+ "outbound.status.reason": MapParams(target="set-status-reason",
validate=Validator.quoted_or_simple(), sections={SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
+ "outbound.status": MapParams(target="set-status",
validate=Validator.range(0, 999), sections={SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST}),
+ "outbound.url.": MapParams(target=HeaderOperations.DESTINATION_OPERATIONS,
upper=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS),
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST})
}
-STATEMENT_FUNCTION_MAP: dict[str, tuple[str, Callable[[list[str]], None] |
None]] = {
- "add-header":
- ("add-header", Validator.arg_count(2).arg_at(0,
Validator.http_header_name()).arg_at(1, Validator.quoted_or_simple())),
- "counter": ("counter", Validator.arg_count(1).quoted_or_simple()),
- "set-debug": ("set-debug", Validator.arg_count(0)),
- "no-op": ("no-op", Validator.arg_count(0)),
- "remove_query": ("rm-destination QUERY",
Validator.arg_count(1).quoted_or_simple()),
- "keep_query": ("rm-destination QUERY",
Validator.arg_count(1).quoted_or_simple()),
- "run-plugin": ("run-plugin", Validator.min_args(1).quoted_or_simple()),
- "set-body-from": ("set-body-from",
Validator.arg_count(1).quoted_or_simple()),
- "set-config": ("set-config", Validator.arg_count(2).quoted_or_simple()),
- "set-redirect":
- ("set-redirect", Validator.arg_count(2).arg_at(0, Validator.range(300,
399)).arg_at(1, Validator.quoted_or_simple())),
- "skip-remap":
- ("skip-remap",
Validator.arg_count(1).suffix_group(types.SuffixGroup.BOOL_FIELDS)._add(Validator.normalize_arg_at(0))),
- "set-plugin-cntl":
- (
- "set-plugin-cntl",
Validator.arg_count(2)._add(Validator.normalize_arg_at(0)).arg_at(
- 0,
Validator.suffix_group(types.SuffixGroup.PLUGIN_CNTL_FIELDS))._add(Validator.normalize_arg_at(1))._add(
-
Validator.conditional_arg_validation(types.SuffixGroup.PLUGIN_CNTL_MAPPING.value))),
+STATEMENT_FUNCTION_MAP: dict[str, MapParams] = {
+ "add-header": MapParams(target="add-header",
validate=Validator.arg_count(2).arg_at(0,
Validator.http_header_name()).arg_at(1, Validator.quoted_or_simple())),
+ "counter": MapParams(target="counter",
validate=Validator.arg_count(1).quoted_or_simple()),
+ "set-debug": MapParams(target="set-debug",
validate=Validator.arg_count(0)),
+ "no-op": MapParams(target="no-op", validate=Validator.arg_count(0)),
+ "remove_query": MapParams(target="rm-destination QUERY",
validate=Validator.arg_count(1).quoted_or_simple()),
+ "keep_query": MapParams(target="rm-destination QUERY",
validate=Validator.arg_count(1).quoted_or_simple()),
+ "run-plugin": MapParams(target="run-plugin",
validate=Validator.min_args(1).quoted_or_simple()),
+ "set-body-from": MapParams(target="set-body-from",
validate=Validator.arg_count(1).quoted_or_simple()),
+ "set-config": MapParams(target="set-config",
validate=Validator.arg_count(2).quoted_or_simple()),
+ "set-redirect": MapParams(target="set-redirect",
validate=Validator.arg_count(2).arg_at(0, Validator.range(300, 399)).arg_at(1,
Validator.quoted_or_simple())),
+ "skip-remap": MapParams(target="skip-remap",
validate=Validator.arg_count(1).suffix_group(SuffixGroup.BOOL_FIELDS)._add(Validator.normalize_arg_at(0))),
+ "set-plugin-cntl": MapParams(target="set-plugin-cntl",
validate=Validator.arg_count(2)._add(Validator.normalize_arg_at(0)).arg_at(0,
Validator.suffix_group(SuffixGroup.PLUGIN_CNTL_FIELDS))._add(Validator.normalize_arg_at(1))._add(Validator.conditional_arg_validation(SuffixGroup.PLUGIN_CNTL_MAPPING.value))),
}
-FUNCTION_MAP = {
- "access": ("ACCESS", Validator.arg_count(1).quoted_or_simple()),
- "cache": ("CACHE", Validator.arg_count(0)),
- "cidr": ("CIDR", Validator.arg_count(2).arg_at(0, Validator.range(1,
32)).arg_at(1, Validator.range(1, 128))),
- "internal": ("INTERNAL-TRANSACTION", Validator.arg_count(0)),
- "random": ("RANDOM", Validator.arg_count(1).nbit_int(32)),
- "ssn-txn-count": ("SSN-TXN-COUNT", Validator.arg_count(0)),
- "txn-count": ("TXN-COUNT", Validator.arg_count(0)),
+FUNCTION_MAP: dict[str, MapParams] = {
+ "access": MapParams(target="ACCESS",
validate=Validator.arg_count(1).quoted_or_simple()),
+ "cache": MapParams(target="CACHE", validate=Validator.arg_count(0)),
+ "cidr": MapParams(target="CIDR", validate=Validator.arg_count(2).arg_at(0,
Validator.range(1, 32)).arg_at(1, Validator.range(1, 128))),
+ "internal": MapParams(target="INTERNAL-TRANSACTION",
validate=Validator.arg_count(0)),
+ "random": MapParams(target="RANDOM",
validate=Validator.arg_count(1).nbit_int(32)),
+ "ssn-txn-count": MapParams(target="SSN-TXN-COUNT",
validate=Validator.arg_count(0)),
+ "txn-count": MapParams(target="TXN-COUNT",
validate=Validator.arg_count(0)),
}
-CONDITION_MAP: dict[str, tuple[str, Callable[[str], None] | None, bool,
set[SectionType] | None, bool, dict | None]] = {
+CONDITION_MAP: dict[str, MapParams] = {
# Exact matches with reverse mapping info
- "inbound.ip": ("%{IP:CLIENT}", None, False, None, False, {
- "reverse_tag": "IP",
- "reverse_payload": "CLIENT"
- }),
- "inbound.method": ("%{METHOD}", None, False, None, False, {
- "reverse_tag": "METHOD",
- "ambiguous": True
- }),
- "inbound.server": ("%{IP:INBOUND}", None, False, None, False, {
- "reverse_tag": "IP",
- "reverse_payload": "INBOUND"
- }),
- "inbound.status": ("%{STATUS}", None, False, None, False, {
- "reverse_tag": "STATUS",
- "ambiguous": True
- }),
- "now": ("%{NOW}", None, False, None, False, None),
- "outbound.ip":
- (
- "%{IP:SERVER}",
- None,
- False,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST},
- False,
- {
- "reverse_tag": "IP",
- "reverse_payload": "SERVER"
- },
- ),
- "outbound.method":
- (
- "%{METHOD}",
- None,
- False,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST},
- False,
- {
- "reverse_tag": "METHOD",
- "ambiguous": True
- },
- ),
- "outbound.server":
- (
- "%{IP:OUTBOUND}",
- None,
- False,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST},
- False,
- {
- "reverse_tag": "IP",
- "reverse_payload": "OUTBOUND"
- },
- ),
- "outbound.status":
- (
- "%{STATUS}",
- None,
- False,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST, SectionType.SEND_REQUEST},
- False,
- {
- "reverse_tag": "STATUS",
- "ambiguous": True
- },
- ),
- "tcp.info": ("%{TCP-INFO}", None, False, None, False, None),
+ "inbound.ip": MapParams(target="%{IP:CLIENT}", rev={"reverse_tag": "IP",
"reverse_payload": "CLIENT"}),
+ "inbound.method": MapParams(target="%{METHOD}", rev={"reverse_tag":
"METHOD", "ambiguous": True}),
+ "inbound.server": MapParams(target="%{IP:INBOUND}", rev={"reverse_tag":
"IP", "reverse_payload": "INBOUND"}),
+ "inbound.status": MapParams(target="%{STATUS}", rev={"reverse_tag":
"STATUS", "ambiguous": True}),
+ "now": MapParams(target="%{NOW}"),
+ "outbound.ip": MapParams(target="%{IP:SERVER}",
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST},
rev={"reverse_tag": "IP", "reverse_payload": "SERVER"}),
+ "outbound.method": MapParams(target="%{METHOD}",
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST},
rev={"reverse_tag": "METHOD", "ambiguous": True}),
+ "outbound.server": MapParams(target="%{IP:OUTBOUND}",
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST},
rev={"reverse_tag": "IP", "reverse_payload": "OUTBOUND"}),
+ "outbound.status": MapParams(target="%{STATUS}",
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST,
SectionType.SEND_REQUEST}, rev={"reverse_tag": "STATUS", "ambiguous": True}),
+ "tcp.info": MapParams(target="%{TCP-INFO}"),
- # Prefix matches with reverse mapping info
- "capture.": ("LAST-CAPTURE", Validator.range(0, 9), False, None, True,
None),
- "from.url.": ("FROM-URL",
Validator.suffix_group(types.SuffixGroup.URL_FIELDS), True, None, True, None),
- "geo.": ("GEO", Validator.suffix_group(types.SuffixGroup.GEO_FIELDS),
True, None, True, None),
- "http.cntl.": ("HTTP-CNTL",
Validator.suffix_group(types.SuffixGroup.HTTP_CNTL_FIELDS), True, None, False,
None),
- "id.": ("ID", Validator.suffix_group(types.SuffixGroup.ID_FIELDS), True,
None, False, None),
- "inbound.conn.client-cert.SAN.":
- ("INBOUND:CLIENT-CERT:SAN",
Validator.suffix_group(types.SuffixGroup.SAN_FIELDS), True, None, True, None),
- "inbound.conn.server-cert.SAN.":
- ("INBOUND:SERVER-CERT:SAN",
Validator.suffix_group(types.SuffixGroup.SAN_FIELDS), True, None, True, None),
- "inbound.conn.client-cert.san.":
- ("INBOUND:CLIENT-CERT:SAN",
Validator.suffix_group(types.SuffixGroup.SAN_FIELDS), True, None, True, None),
- "inbound.conn.server-cert.san.":
- ("INBOUND:SERVER-CERT:SAN",
Validator.suffix_group(types.SuffixGroup.SAN_FIELDS), True, None, True, None),
- "inbound.conn.client-cert.":
- ("INBOUND:CLIENT-CERT",
Validator.suffix_group(types.SuffixGroup.CERT_FIELDS), True, None, True, None),
- "inbound.conn.server-cert.":
- ("INBOUND:SERVER-CERT",
Validator.suffix_group(types.SuffixGroup.CERT_FIELDS), True, None, True, None),
- "inbound.conn.": ("INBOUND",
Validator.suffix_group(types.SuffixGroup.CONN_FIELDS), True, None, True, None),
- "inbound.cookie.": ("COOKIE", Validator.http_token(), False, None, True, {
- "reverse_fallback": "inbound.cookie."
- }),
- "inbound.req.": ("CLIENT-HEADER", Validator.http_header_name(), False,
None, True, {
- "reverse_fallback": "inbound.req."
- }),
- "inbound.resp.": ("HEADER", Validator.http_header_name(), False, None,
True, {
- "reverse_context": "header_condition"
- }),
- "inbound.url.": ("CLIENT-URL",
Validator.suffix_group(types.SuffixGroup.URL_FIELDS), True, None, True, None),
- "now.": ("NOW", Validator.suffix_group(types.SuffixGroup.DATE_FIELDS),
True, None, False, None),
- "outbound.conn.client-cert.SAN.":
- (
- "OUTBOUND:CLIENT-CERT:SAN",
- Validator.suffix_group(types.SuffixGroup.SAN_FIELDS),
- True,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST},
- True,
- None,
- ),
- "outbound.conn.server-cert.SAN.":
- (
- "OUTBOUND:SERVER-CERT:SAN",
- Validator.suffix_group(types.SuffixGroup.SAN_FIELDS),
- True,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST},
- True,
- None,
- ),
- "outbound.conn.client-cert.san.":
- (
- "OUTBOUND:CLIENT-CERT:SAN",
- Validator.suffix_group(types.SuffixGroup.SAN_FIELDS),
- True,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST},
- True,
- None,
- ),
- "outbound.conn.server-cert.san.":
- (
- "OUTBOUND:SERVER-CERT:SAN",
- Validator.suffix_group(types.SuffixGroup.SAN_FIELDS),
- True,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST},
- True,
- None,
- ),
- "outbound.conn.client-cert.":
- (
- "OUTBOUND:CLIENT-CERT",
- Validator.suffix_group(types.SuffixGroup.CERT_FIELDS),
- True,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST},
- True,
- None,
- ),
- "outbound.conn.server-cert.":
- (
- "OUTBOUND:SERVER-CERT",
- Validator.suffix_group(types.SuffixGroup.CERT_FIELDS),
- True,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST},
- True,
- None,
- ),
- "outbound.conn.":
- (
- "OUTBOUND", Validator.suffix_group(types.SuffixGroup.CONN_FIELDS),
True,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST}, True, None),
- "outbound.cookie.":
- (
- "COOKIE", Validator.http_token(), False, {SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST}, True, {
- "reverse_fallback": "inbound.cookie."
- }),
- "outbound.req.":
- (
- "HEADER", Validator.http_header_name(), False,
{SectionType.PRE_REMAP, SectionType.REMAP,
-
SectionType.READ_REQUEST}, True, {
-
"reverse_context": "header_condition"
- }),
- "outbound.resp.":
- (
- "HEADER",
- Validator.http_header_name(),
- False,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST, SectionType.SEND_REQUEST},
- True,
- {
- "reverse_context": "header_condition"
- },
- ),
- "outbound.url.":
- (
- "NEXT-HOP",
- Validator.suffix_group(types.SuffixGroup.URL_FIELDS),
- True,
- {SectionType.PRE_REMAP, SectionType.REMAP,
SectionType.READ_REQUEST},
- True,
- None,
- ),
- "to.url.": ("TO-URL",
Validator.suffix_group(types.SuffixGroup.URL_FIELDS), True, None, True, None),
+ # Prefix matches
+ "capture.": MapParams(target="LAST-CAPTURE", prefix=True,
validate=Validator.range(0, 9)),
+ "from.url.": MapParams(target="FROM-URL", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
+ "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)),
+ "id.": MapParams(target="ID", upper=True,
validate=Validator.suffix_group(SuffixGroup.ID_FIELDS)),
+ "inbound.conn.client-cert.SAN.":
MapParams(target="INBOUND:CLIENT-CERT:SAN", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS)),
+ "inbound.conn.server-cert.SAN.":
MapParams(target="INBOUND:SERVER-CERT:SAN", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS)),
+ "inbound.conn.client-cert.san.":
MapParams(target="INBOUND:CLIENT-CERT:SAN", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS)),
+ "inbound.conn.server-cert.san.":
MapParams(target="INBOUND:SERVER-CERT:SAN", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS)),
+ "inbound.conn.client-cert.": MapParams(target="INBOUND:CLIENT-CERT",
upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS)),
+ "inbound.conn.server-cert.": MapParams(target="INBOUND:SERVER-CERT",
upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS)),
+ "inbound.conn.": MapParams(target="INBOUND", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.CONN_FIELDS)),
+ "inbound.cookie.": MapParams(target="COOKIE", prefix=True,
validate=Validator.http_token(), rev={"reverse_fallback": "inbound.cookie."}),
+ "inbound.req.": MapParams(target="CLIENT-HEADER", prefix=True,
validate=Validator.http_header_name(), rev={"reverse_fallback":
"inbound.req."}),
+ "inbound.resp.": MapParams(target="HEADER", prefix=True,
validate=Validator.http_header_name(), rev={"reverse_context":
"header_condition"}),
+ "inbound.url.": MapParams(target="CLIENT-URL", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
+ "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.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+ "outbound.conn.server-cert.SAN.":
MapParams(target="OUTBOUND:SERVER-CERT:SAN", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS),
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+ "outbound.conn.client-cert.san.":
MapParams(target="OUTBOUND:CLIENT-CERT:SAN", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS),
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+ "outbound.conn.server-cert.san.":
MapParams(target="OUTBOUND:SERVER-CERT:SAN", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS),
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+ "outbound.conn.client-cert.": MapParams(target="OUTBOUND:CLIENT-CERT",
upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS),
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+ "outbound.conn.server-cert.": MapParams(target="OUTBOUND:SERVER-CERT",
upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS),
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+ "outbound.conn.": MapParams(target="OUTBOUND", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.CONN_FIELDS),
sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}),
+ "outbound.cookie.": MapParams(target="COOKIE", prefix=True,
validate=Validator.http_token(), sections={SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST}, rev={"reverse_fallback":
"inbound.cookie."}),
+ "outbound.req.": MapParams(target="HEADER", prefix=True,
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST}, rev={"reverse_context":
"header_condition"}),
+ "outbound.resp.": MapParams(target="HEADER", prefix=True,
validate=Validator.http_header_name(), sections={SectionType.PRE_REMAP,
SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST},
rev={"reverse_context": "header_condition"}),
+ "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}),
+ "to.url.": MapParams(target="TO-URL", upper=True, prefix=True,
validate=Validator.suffix_group(SuffixGroup.URL_FIELDS)),
}
FALLBACK_TAG_MAP: dict[str, tuple[str, bool]] = {
@@ -314,6 +142,7 @@ CONTEXT_TYPE_MAP: dict[str, str | tuple[str, str]] = {
# Operator command mappings for reverse resolution
OPERATOR_COMMAND_MAP: dict[str, tuple[str, str, Callable, Callable]] = {
+ "add-header": ("header_ops", "header", lambda toks: toks[1], lambda qual:
qual),
"set-header": ("header_ops", "header", lambda toks: toks[1], lambda qual:
qual),
"rm-header": ("header_ops", "header", lambda toks: toks[1], lambda qual:
qual),
"set-cookie": ("cookie_ops", "cookie", lambda toks: toks[1], lambda qual:
qual),
@@ -321,6 +150,7 @@ OPERATOR_COMMAND_MAP: dict[str, tuple[str, str, Callable,
Callable]] = {
"set-destination": ("destination_ops", "destination", lambda toks:
toks[1].lower(), lambda qual: qual),
"rm-destination": ("destination_ops", "destination", lambda toks:
toks[1].lower(), lambda qual: qual)
}
+# yapf: enable
REVERSE_RESOLUTION_MAP = get_complete_reverse_resolution_map()
@@ -412,23 +242,19 @@ class LSPPatternMatcher:
@classmethod
def match_any_pattern(cls, expression: str) -> PatternMatch | None:
"""Try to match expression against all pattern types."""
- # Try field patterns first (most specific)
+
if match := cls.match_field_pattern(expression):
return match
- # Try certificate patterns
if match := cls.match_certificate_pattern(expression):
return match
- # Try connection patterns
if match := cls.match_connection_pattern(expression):
return match
- # Try header patterns
if match := cls.match_header_pattern(expression):
return match
- # Try cookie patterns
if match := cls.match_cookie_pattern(expression):
return match
diff --git a/tools/hrw4u/src/types.py b/tools/hrw4u/src/types.py
index 213e084a59..4185f78840 100644
--- a/tools/hrw4u/src/types.py
+++ b/tools/hrw4u/src/types.py
@@ -19,10 +19,14 @@ from __future__ import annotations
from enum import Enum
from dataclasses import dataclass
-from typing import Self
+from typing import Self, Callable, TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from hrw4u.states import SectionType
class MagicStrings(str, Enum):
+ ADD_HEADER = "add-header"
RM_HEADER = "rm-header"
SET_HEADER = "set-header"
RM_COOKIE = "rm-cookie"
@@ -166,3 +170,73 @@ class Symbol:
def as_operator(self, value: str) -> str:
return f"{self.var_type.op_tag} {self.index} {value}"
+
+
+class MapParams:
+ """Map parameters for table entries combining flags and metadata.
+ """
+
+ def __init__(
+ self,
+ upper: bool = False,
+ add: bool = False,
+ prefix: bool = False,
+ validate: Callable[[str], None] | None = None,
+ sections: set[SectionType] | None = None,
+ rev: dict | None = None,
+ target: str | list[str] | tuple[str, ...] | None = None) -> None:
+ object.__setattr__(
+ self, '_params', {
+ 'upper': upper,
+ 'add': add,
+ 'prefix': prefix,
+ 'validate': validate,
+ 'sections': sections,
+ 'rev': rev,
+ 'target': target
+ })
+
+ def __getattr__(self, name: str):
+ if name.startswith('_'):
+ raise AttributeError(f"'{type(self).__name__}' object has no
attribute '{name}'")
+ return self._params.get(name, False if name in ('upper', 'add',
'prefix') else None)
+
+ def __setattr__(self, name: str, value: object) -> None:
+ """Prevent modification after initialization (immutable)."""
+ raise AttributeError(f"'{type(self).__name__}' object is immutable")
+
+ def __repr__(self) -> str:
+ non_defaults = []
+ for k, v in self._params.items():
+ if k in ('upper', 'add', 'prefix'):
+ if v:
+ non_defaults.append(f"{k}=True")
+ elif v is not None:
+ if isinstance(v, set):
+ non_defaults.append(f"{k}={{{', '.join(str(s) for s in
v)}}}")
+ elif k == 'validate':
+ non_defaults.append(f"{k}=<validator>")
+ else:
+ non_defaults.append(f"{k}=...")
+
+ if not non_defaults:
+ return "MapParams()"
+ return f"MapParams({', '.join(non_defaults)})"
+
+ def __hash__(self) -> int:
+ hashable_items = []
+ for k, v in self._params.items():
+ if isinstance(v, set):
+ hashable_items.append((k, frozenset(v)))
+ elif isinstance(v, dict):
+ hashable_items.append((k, frozenset(v.items()) if v else None))
+ elif callable(v):
+ hashable_items.append((k, id(v)))
+ else:
+ hashable_items.append((k, v))
+ return hash(frozenset(hashable_items))
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, MapParams):
+ return NotImplemented
+ return self._params == other._params
diff --git a/tools/hrw4u/src/visitor.py b/tools/hrw4u/src/visitor.py
index 5d35cb4ca6..c038cdc241 100644
--- a/tools/hrw4u/src/visitor.py
+++ b/tools/hrw4u/src/visitor.py
@@ -356,6 +356,18 @@ class HRW4UVisitor(hrw4uVisitor, BaseHRWVisitor):
self.emit_statement(out)
return
+ case _ if ctx.PLUSEQUAL():
+ if ctx.lhs is None:
+ raise SymbolResolutionError("assignment", "Missing
left-hand side in += assignment")
+ lhs = ctx.lhs.text
+ rhs = ctx.value().getText()
+ if rhs.startswith('"') and rhs.endswith('"'):
+ rhs = self._substitute_strings(rhs, ctx)
+ self._dbg(f"add assignment: {lhs} += {rhs}")
+ out = self.symbol_resolver.resolve_add_assignment(lhs,
rhs, self.current_section)
+ self.emit_statement(out)
+ return
+
case _:
if ctx.op is None:
raise SymbolResolutionError("operator", "Missing
operator in statement")
diff --git a/tools/hrw4u/tests/data/hooks/remap.ast.txt
b/tools/hrw4u/tests/data/hooks/remap.ast.txt
index 0b229e4e48..c8f2a6d632 100644
--- a/tools/hrw4u/tests/data/hooks/remap.ast.txt
+++ b/tools/hrw4u/tests/data/hooks/remap.ast.txt
@@ -1 +1 @@
-(program (programItem (section REMAP { (sectionBody (conditional (ifStatement
if (condition (expression (term (factor (comparison (comparable
inbound.req.X-Remap) == (value "yes")))))) (block { (blockItem (statement
inbound.req.X-Remap = (value "") ;)) })) (elseClause else (block { (blockItem
(statement inbound.req.X-Remap = (value "It was not yes") ;)) })))) })) <EOF>)
+(program (programItem (section REMAP { (sectionBody (conditional (ifStatement
if (condition (expression (term (factor (comparison (comparable
inbound.req.X-Remap) == (value "yes")))))) (block { (blockItem (statement
inbound.req.X-Remap = (value "") ;)) (blockItem (statement
inbound.req.X-Appended += (value "HRW4U") ;)) })) (elseClause else (block {
(blockItem (statement inbound.req.X-Remap = (value "It was not yes") ;))
(blockItem (statement inbound.req.X-Appended = (value "") ;)) })))) [...]
diff --git a/tools/hrw4u/tests/data/hooks/remap.input.txt
b/tools/hrw4u/tests/data/hooks/remap.input.txt
index 0b31ed8c1c..1dd3c4d14b 100644
--- a/tools/hrw4u/tests/data/hooks/remap.input.txt
+++ b/tools/hrw4u/tests/data/hooks/remap.input.txt
@@ -1,7 +1,9 @@
REMAP {
if inbound.req.X-Remap == "yes" {
inbound.req.X-Remap = "";
+ inbound.req.X-Appended += "HRW4U";
} else {
inbound.req.X-Remap = "It was not yes";
+ inbound.req.X-Appended = "";
}
}
diff --git a/tools/hrw4u/tests/data/hooks/remap.output.txt
b/tools/hrw4u/tests/data/hooks/remap.output.txt
index 243fbb65c7..d151e91028 100644
--- a/tools/hrw4u/tests/data/hooks/remap.output.txt
+++ b/tools/hrw4u/tests/data/hooks/remap.output.txt
@@ -1,5 +1,7 @@
cond %{REMAP_PSEUDO_HOOK} [AND]
cond %{CLIENT-HEADER:X-Remap} ="yes"
rm-header X-Remap
+ add-header X-Appended "HRW4U"
else
set-header X-Remap "It was not yes"
+ rm-header X-Appended
diff --git a/tools/hrw4u/tests/data/ops/exceptions.txt
b/tools/hrw4u/tests/data/ops/exceptions.txt
index b96ebf7092..9628625f81 100644
--- a/tools/hrw4u/tests/data/ops/exceptions.txt
+++ b/tools/hrw4u/tests/data/ops/exceptions.txt
@@ -3,3 +3,5 @@
# QSA (Query String Append) is a reverse-only test
qsa.input: u4wrh
+# HTTP-CNTL valid bools can not reverse back to the original input
+http_cntl_valid_bools.input: hrw4u
diff --git a/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.ast.txt
b/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.ast.txt
index 6029280f56..870d41622d 100644
--- a/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.ast.txt
+++ b/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.ast.txt
@@ -1 +1 @@
-(program (section SEND_RESPONSE { (sectionBody (statement http.cntl.LOGGING =
(value TRUE) ;)) (sectionBody (statement http.cntl.TXN_DEBUG = (value FALSE)
;)) (sectionBody (statement http.cntl.REQ_CACHEABLE = (value YES) ;))
(sectionBody (statement http.cntl.RESP_CACHEABLE = (value NO) ;)) (sectionBody
(statement http.cntl.SERVER_NO_STORE = (value ON) ;)) (sectionBody (statement
http.cntl.SKIP_REMAP = (value OFF) ;)) (sectionBody (statement
http.cntl.INTERCEPT_RETRY = (value 1) ;)) (sect [...]
+(program (programItem (section SEND_RESPONSE { (sectionBody (statement
http.cntl.LOGGING = (value TRUE) ;)) (sectionBody (statement
http.cntl.TXN_DEBUG = (value FALSE) ;)) (sectionBody (statement
http.cntl.REQ_CACHEABLE = (value YES) ;)) (sectionBody (statement
http.cntl.RESP_CACHEABLE = (value NO) ;)) (sectionBody (statement
http.cntl.SERVER_NO_STORE = (value ON) ;)) (sectionBody (statement
http.cntl.SKIP_REMAP = (value OFF) ;)) (sectionBody (statement
http.cntl.INTERCEPT_RETRY = (value [...]
diff --git a/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.output.txt
b/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.output.txt
index d90fac00d6..9fbb2661e2 100644
--- a/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.output.txt
+++ b/tools/hrw4u/tests/data/ops/http_cntl_valid_bools.output.txt
@@ -15,4 +15,4 @@ cond %{SEND_RESPONSE_HDR_HOOK} [AND]
set-http-cntl INTERCEPT_RETRY off
set-http-cntl LOGGING True
set-http-cntl TXN_DEBUG False
- set-http-cntl LOGGING TRue
\ No newline at end of file
+ set-http-cntl LOGGING TRue
diff --git a/tools/hrw4u/tests/data/ops/qsa.output.txt
b/tools/hrw4u/tests/data/ops/qsa.output.txt
index 6ea3587798..03e002907a 100644
--- a/tools/hrw4u/tests/data/ops/qsa.output.txt
+++ b/tools/hrw4u/tests/data/ops/qsa.output.txt
@@ -2,4 +2,4 @@
# test, because in hrw4u, we don't use QSA.
cond %{REMAP_PSEUDO_HOOK} [AND]
cond %{GEO:COUNTRY} =SE
- set-redirect 302 https://www.example.com/SE/%{CLIENT-URL:PATH} [QSA]
+ set-redirect 302
"https://www.example.com/SE/%{CLIENT-URL:PATH}?%{CLIENT-URL:QUERY}"