Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ci/rat-regex.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
.*\.config$
.*\.yaml$
.*\.gold$
.*\.hrw4u$
^\.gitignore$
^\.gitmodules$
^\.perltidyrc$
Expand Down
95 changes: 95 additions & 0 deletions doc/admin-guide/configuration/hrw4u.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,101 @@ or when integrating with existing header_rewrite rules that reference specific s
addition, a remap configuration can use ``@PPARAM`` to set one of these slot variables explicitly
as part of the configuration.

Procedures
----------

Procedures allow you to define reusable blocks of rules that can be called from
multiple sections or files. A procedure is a named, parameterized block of
conditions and operators that expands inline at the call site.

Defining Procedures
^^^^^^^^^^^^^^^^^^^

Procedures are declared with the ``procedure`` keyword and must use a qualified
name with the ``::`` namespace separator::

procedure local::add-debug-header($tag) {
inbound.req.X-Debug = "$tag";
}

The namespace prefix (``local::`` in this example) groups related procedures.
Parameters are prefixed with ``$`` and substituted at the call site.

Procedures may be defined in the same file as the sections that use them, or in
separate ``.hrw4u`` files loaded with the ``use`` directive. Procedure declarations
must appear before any section blocks.

Using Procedures
^^^^^^^^^^^^^^^^

Call a procedure from any section by its qualified name::

procedure local::set-cache-headers($ttl) {
outbound.resp.Cache-Control = "max-age=$ttl";
outbound.resp.X-Cache-TTL = "$ttl";
}

READ_RESPONSE {
local::set-cache-headers("3600");
}

SEND_RESPONSE {
local::set-cache-headers("0");
}

The procedure body is expanded inline — each section gets its own copy with
the correct hook context.

Procedure Files and ``use``
^^^^^^^^^^^^^^^^^^^^^^^^^^^

For larger projects, procedures can be organized into separate files and loaded
with the ``use`` directive. The ``use`` spec maps to a file path: ``use Acme::Common``
loads ``Acme/Common.hrw4u`` from the procedures search path.

The ``--procedures-path`` flag specifies where to search::

hrw4u --procedures-path /etc/trafficserver/procedures rules.hrw4u

Given this file structure::

/etc/trafficserver/procedures/
└── Acme/
└── Common.hrw4u

Where ``Acme/Common.hrw4u`` contains::

procedure Acme::add-security-headers() {
outbound.resp.X-Frame-Options = "DENY";
outbound.resp.X-Content-Type-Options = "nosniff";
}

Then in ``rules.hrw4u``::

use Acme::Common

READ_RESPONSE {
Acme::add-security-headers();
}

The ``use`` directive enforces namespace consistency: all procedures in a file
loaded via ``use Acme::Common`` must use the ``Acme::`` namespace prefix.

Parameters and Defaults
^^^^^^^^^^^^^^^^^^^^^^^

Procedures support positional parameters with optional defaults::

procedure local::tag-request($env, $version = "v1") {
inbound.req.X-Env = "$env";
inbound.req.X-Version = "$version";
}

REMAP {
local::tag-request("prod");
# $version defaults to "v1"
}

Groups
------

Expand Down
3 changes: 2 additions & 1 deletion tools/hrw4u/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ UTILS_FILES=src/symbols_base.py \
SRC_FILES_HRW4U=src/visitor.py \
src/symbols.py \
src/suggestions.py \
src/kg_visitor.py
src/kg_visitor.py \
src/procedures.py

ALL_HRW4U_FILES=$(SHARED_FILES) $(UTILS_FILES) $(SRC_FILES_HRW4U)

Expand Down
41 changes: 37 additions & 4 deletions tools/hrw4u/grammar/hrw4u.g4
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@ TRUE : [tT][rR][uU][eE];
FALSE : [fF][aA][lL][sS][eE];
WITH : 'with';
BREAK : 'break';
USE : 'use';
PROCEDURE : 'procedure';

REGEX : '/' ( '\\/' | ~[/\r\n] )* '/' ;
STRING : '"' ( '\\' . | ~["\\\r\n] )* '"' ;
STRING : '"' ( ESCAPED_BLOCK | '\\' . | ~["\\\r\n] )* '"' ;

// {{ ... }} is an escape hatch — contents are passed through verbatim, inner quotes allowed
fragment ESCAPED_BLOCK : '{{' ( ~'}' | '}' ~'}' )* '}}';

IPV4_LITERAL
: (OCTET '.' OCTET '.' OCTET '.' OCTET ('/' IPV4_CIDR)?)
Expand Down Expand Up @@ -59,8 +64,13 @@ fragment IPV6_CIDR : '3'[3-9]
| '12'[0-8]
;

// Qualified identifier: Namespace::Name (one or more :: segments).
QUALIFIED_IDENT : [a-zA-Z_][a-zA-Z0-9_-]* ('::' [a-zA-Z_][a-zA-Z0-9_-]*)+
;

IDENT : [a-zA-Z_][a-zA-Z0-9_@.-]* ;
NUMBER : [0-9]+ ;
DOLLAR : '$';
LPAREN : '(';
RPAREN : ')';
LBRACE : '{';
Expand Down Expand Up @@ -89,14 +99,36 @@ WS : [ \t\r\n]+ -> skip ;
// Parser Rules
// -----------------------------
program
: programItem+ EOF
: programItem* EOF
;

programItem
: section
: useDirective
| procedureDecl
| section
| commentLine
;

useDirective
: USE QUALIFIED_IDENT
;

procedureDecl
: PROCEDURE QUALIFIED_IDENT LPAREN paramList? RPAREN block
;

paramList
: param (COMMA param)*
;

param
: DOLLAR IDENT (EQUAL value)?
;

paramRef
: DOLLAR IDENT
;

section
: varSection
| name=IDENT LBRACE sectionBody+ RBRACE
Expand Down Expand Up @@ -211,7 +243,7 @@ comparable
;

functionCall
: funcName=IDENT LPAREN argumentList? RPAREN
: funcName=(IDENT | QUALIFIED_IDENT) LPAREN argumentList? RPAREN
;

argumentList
Expand Down Expand Up @@ -251,6 +283,7 @@ value
| ident=IDENT
| ip
| iprange
| paramRef
;

commentLine
Expand Down
3 changes: 2 additions & 1 deletion tools/hrw4u/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "hrw4u"
version = "1.4.1"
version = "1.5.0"
description = "HRW4U CLI tool for Apache Traffic Server header rewrite rules"
authors = [
{name = "Leif Hedstrom", email = "leif@apache.org"}
Expand Down Expand Up @@ -76,6 +76,7 @@ markers = [
"examples: marks tests for all header_rewrite docs examples",
"reverse: marks tests for reverse conversion (header_rewrite -> hrw4u)",
"ast: marks tests for AST validation",
"procedures: marks tests for procedure expansion",
]

[dependency-groups]
Expand Down
30 changes: 29 additions & 1 deletion tools/hrw4u/scripts/hrw4u
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,38 @@

from __future__ import annotations

import argparse
import os
from pathlib import Path
from typing import Any

from hrw4u.hrw4uLexer import hrw4uLexer
from hrw4u.hrw4uParser import hrw4uParser
from hrw4u.visitor import HRW4UVisitor
from hrw4u.common import run_main


def _add_args(parser: argparse.ArgumentParser, output_group: argparse._MutuallyExclusiveGroup) -> None:
output_group.add_argument(
"--output",
choices=["hrw", "hrw4u"],
default="hrw",
help="Output format: hrw (header_rewrite, default) or hrw4u (expand procedures inline)")
parser.add_argument(
"--procedures-path",
metavar="DIR[:DIR...]",
dest="procedures_path",
default="",
help="Colon-separated list of directories to search for procedure files")


def _visitor_kwargs(args: argparse.Namespace) -> dict[str, Any]:
kwargs: dict[str, Any] = {}
if args.procedures_path:
kwargs['proc_search_paths'] = [Path(p) for p in args.procedures_path.split(os.pathsep) if p]
return kwargs


def main() -> None:
"""Main entry point for the hrw4u script."""
run_main(
Expand All @@ -34,7 +60,9 @@ def main() -> None:
visitor_class=HRW4UVisitor,
error_prefix="hrw4u",
output_flag_name="hrw",
output_flag_help="Produce the HRW output (default)")
output_flag_help="Produce the HRW output (default)",
add_args=_add_args,
visitor_kwargs=_visitor_kwargs)


if __name__ == "__main__":
Expand Down
Loading