From 19bd2fac9c554d2f2d68246dbf230adb1a597114 Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Thu, 29 Jan 2026 07:52:26 -0800 Subject: [PATCH] HRW/HRW4U: Adds SERVER-HEADER & SERVER-URL --- doc/admin-guide/configuration/hrw4u.en.rst | 24 +++++++++++--- doc/admin-guide/plugins/header_rewrite.en.rst | 33 +++++++++++++++++++ plugins/header_rewrite/conditions.cc | 29 ++++++++++++++-- plugins/header_rewrite/conditions.h | 10 +++--- plugins/header_rewrite/factory.cc | 6 +++- plugins/header_rewrite/resources.cc | 15 ++++++++- plugins/header_rewrite/resources.h | 2 ++ .../header_rewrite_bundle.replay.yaml | 33 +++++++++++++++++++ .../rules/rule_server_conditions.conf | 32 ++++++++++++++++++ tools/hrw4u/pyproject.toml | 2 +- tools/hrw4u/src/tables.py | 8 +++-- tools/hrw4u/src/types.py | 1 + tools/hrw4u/tests/data/conds/nexthop.ast.txt | 1 + .../hrw4u/tests/data/conds/nexthop.input.txt | 13 ++++++++ .../hrw4u/tests/data/conds/nexthop.output.txt | 9 +++++ .../tests/data/conds/outbound.output.txt | 4 +-- .../tests/data/examples/all-nonsense.ast.txt | 2 +- .../data/examples/all-nonsense.input.txt | 2 +- .../data/examples/all-nonsense.output.txt | 8 ++--- .../tests/data/hooks/send_request.output.txt | 2 +- tools/hrw4u/tests/test_lsp.py | 9 +++-- 21 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf create mode 100644 tools/hrw4u/tests/data/conds/nexthop.ast.txt create mode 100644 tools/hrw4u/tests/data/conds/nexthop.input.txt create mode 100644 tools/hrw4u/tests/data/conds/nexthop.output.txt diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst index 8a3bed3d862..c93a425c1e9 100644 --- a/doc/admin-guide/configuration/hrw4u.en.rst +++ b/doc/admin-guide/configuration/hrw4u.en.rst @@ -181,7 +181,9 @@ cond %{ACCESS:/path} access("/path") File exists 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 %{SERVER-HEADER:X} =foo outbound.req.X == "foo" Server request header (sent to origin) cond %{CLIENT-URL:} =bar inbound.url. == "bar" URL component match, <:ref:`C`> is ``host``, ``path`` etc. +cond %{SERVER-URL:} =bar outbound.url. == "bar" Server request URL component (sent to origin) 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 @@ -195,7 +197,7 @@ cond %{IP:SERVER} ="..." outbound.ip == "..." Upstream (ne 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:} ="bar" nexthop. == "bar" Next-hop destination, ```` is ``host``, ``port``, or ``strategy`` 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 @@ -203,7 +205,7 @@ cond %{RANDOM:500} >250 random(500) > 250 Random numbe 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 %{URL: =bar inbound.url. == "bar" Context aware URL component match (use ``inbound.url`` or ``outbound.url``) 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 @@ -211,6 +213,20 @@ cond %{HTTP-CNTL:} http.cntl. Check the st cond %{INBOUND:} {in,out}bound.conn. inbound (:ref:`client, user agent`) connection to ATS ================================ ================================== ================================================ +.. note:: + **Header and URL prefix summary:** + + - ``inbound.req.
`` → ``CLIENT-HEADER`` - Headers from the client request + - ``outbound.req.
`` → ``SERVER-HEADER`` - Headers in the request sent to origin + - ``inbound.url.`` → ``CLIENT-URL`` - URL from the original client request + - ``outbound.url.`` → ``SERVER-URL`` - URL in the request sent to origin (after remapping) + - ``nexthop.`` → ``NEXT-HOP`` - Network destination info (host, port, strategy) + + The distinction between ``outbound.url`` and ``nexthop`` is important: + + - ``outbound.url`` is the HTTP request URL (what's in the request line/Host header) + - ``nexthop`` is the network destination (where ATS connects, may be a parent proxy) + The conditions operating on headers and URLs are also available as operators. E.g.: .. code-block:: none @@ -271,9 +287,9 @@ HRW4U provides a special ``+=`` operator for adding headers:: The ``+=`` operator only works with the following pre-defined symbols: -- ``inbound.req.
`` - Client request headers +- ``inbound.req.
`` - Client request headers (maps to ``CLIENT-HEADER``) - ``inbound.resp.
`` - Origin response headers -- ``outbound.req.
`` - Outbound request headers (context-restricted) +- ``outbound.req.
`` - Server request headers (maps to ``SERVER-HEADER``) - ``outbound.resp.
`` - Outbound response headers (context-restricted) .. note:: diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index 3dafdae9241..58ac015f793 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -369,6 +369,25 @@ header operated on by this condition will be a comma separated string of the values from every occurrence of the header. More details are provided in `Repeated Headers`_ below. +SERVER-HEADER +~~~~~~~~~~~~~ +:: + + cond %{SERVER-HEADER:} + +Value of the header ```` from the request sent to the origin server +(regardless of the hook context in which the rule is being evaluated). This is +useful when you need to check headers that have been modified or added during +the request processing before being sent to the origin. Note that some headers +may appear in an HTTP message more than once. In these cases, the value of the +header operated on by this condition will be a comma separated string of the +values from every occurrence of the header. More details are provided in +`Repeated Headers`_ below. + +Note that the server request headers are only available after the +``SEND_REQUEST_HDR_HOOK`` has been reached. Using this condition in earlier +hooks will result in an empty value. + CLIENT-URL ~~~~~~~~~~ :: @@ -385,6 +404,20 @@ phase of the transaction. This happens when there is no host in the incoming UR and only set as a host header. During the remap phase the host header is copied to the CLIENT-URL. Use CLIENT-HEADER:Host if you are going to match the host. +SERVER-URL +~~~~~~~~~~ +:: + + cond %{SERVER-URL:} + +The URL of the request being sent to the origin server. This is the URL after +any remapping and modifications have been applied. The ```` may be +specified according to the options documented in `URL Parts`_. + +Note that the server request URL is only available after the +``SEND_REQUEST_HDR_HOOK`` has been reached. Using this condition in earlier +hooks will result in an empty value. + CIDR ~~~~ :: diff --git a/plugins/header_rewrite/conditions.cc b/plugins/header_rewrite/conditions.cc index 7f9e7c18029..37cfeeeceaf 100644 --- a/plugins/header_rewrite/conditions.cc +++ b/plugins/header_rewrite/conditions.cc @@ -224,12 +224,20 @@ ConditionHeader::append_value(std::string &s, const Resources &res) TSMLoc hdr_loc; int len; - if (_client) { + switch (_type) { + case CLIENT: bufp = res.client_bufp; hdr_loc = res.client_hdr_loc; - } else { + break; + case SERVER: + bufp = res.server_bufp; + hdr_loc = res.server_hdr_loc; + break; + case HEADER: + default: bufp = res.bufp; hdr_loc = res.hdr_loc; + break; } if (bufp && hdr_loc) { @@ -272,8 +280,13 @@ ConditionUrl::initialize(Parser &p) Condition::initialize(p); auto match = std::make_unique(_cond_op); + match->set(p.get_arg(), mods()); _matcher = std::move(match); + + if (_type == SERVER) { + require_resources(RSRC_SERVER_REQUEST_HEADERS); + } } void @@ -318,6 +331,18 @@ ConditionUrl::append_value(std::string &s, const Resources &res) TSError("[%s] Error getting the pristine URL", PLUGIN_NAME); return; } + } else if (_type == SERVER) { + Dbg(pi_dbg_ctl, " Using the server request url"); + bufp = res.server_bufp; + if (bufp && res.server_hdr_loc) { + if (TSHttpHdrUrlGet(bufp, res.server_hdr_loc, &url) != TS_SUCCESS) { + TSError("[%s] Error getting the server request URL", PLUGIN_NAME); + return; + } + } else { + Dbg(pi_dbg_ctl, " Server request not available"); + return; + } } else if (res._rri != nullptr) { // called at the remap hook bufp = res._rri->requestBufp; diff --git a/plugins/header_rewrite/conditions.h b/plugins/header_rewrite/conditions.h index 6893709d78a..4393ecbab09 100644 --- a/plugins/header_rewrite/conditions.h +++ b/plugins/header_rewrite/conditions.h @@ -255,9 +255,11 @@ class ConditionHeader : public Condition using SelfType = ConditionHeader; public: - explicit ConditionHeader(bool client = false) : _client(client) + enum HeaderType { HEADER, CLIENT, SERVER }; + + explicit ConditionHeader(HeaderType type = HEADER) : _type(type) { - Dbg(dbg_ctl, "Calling CTOR for ConditionHeader, client %d", client); + Dbg(dbg_ctl, "Calling CTOR for ConditionHeader, type %d", static_cast(type)); } // noncopyable @@ -271,7 +273,7 @@ class ConditionHeader : public Condition bool eval(const Resources &res) override; private: - bool _client; + HeaderType _type; }; // url @@ -282,7 +284,7 @@ class ConditionUrl : public Condition using SelfType = ConditionUrl; public: - enum UrlType { CLIENT, URL, FROM, TO }; + enum UrlType { CLIENT, URL, FROM, TO, SERVER }; explicit ConditionUrl(const UrlType type) : _type(type) { Dbg(dbg_ctl, "Calling CTOR for ConditionUrl"); } diff --git a/plugins/header_rewrite/factory.cc b/plugins/header_rewrite/factory.cc index 6e5cc4bd064..ffc999f3e02 100644 --- a/plugins/header_rewrite/factory.cc +++ b/plugins/header_rewrite/factory.cc @@ -132,9 +132,13 @@ condition_factory(const std::string &cond) } else if (c_name == "HEADER") { // This condition adapts to the hook c = new ConditionHeader(); } else if (c_name == "CLIENT-HEADER") { - c = new ConditionHeader(true); + c = new ConditionHeader(ConditionHeader::CLIENT); + } else if (c_name == "SERVER-HEADER") { + c = new ConditionHeader(ConditionHeader::SERVER); } else if (c_name == "CLIENT-URL") { // This condition adapts to the hook c = new ConditionUrl(ConditionUrl::CLIENT); + } else if (c_name == "SERVER-URL") { + c = new ConditionUrl(ConditionUrl::SERVER); } else if (c_name == "URL") { c = new ConditionUrl(ConditionUrl::URL); } else if (c_name == "FROM-URL") { diff --git a/plugins/header_rewrite/resources.cc b/plugins/header_rewrite/resources.cc index 0d4d6e44330..8f479991ede 100644 --- a/plugins/header_rewrite/resources.cc +++ b/plugins/header_rewrite/resources.cc @@ -33,7 +33,6 @@ void Resources::gather(const ResourceIDs ids, TSHttpHookID hook) { Dbg(pi_dbg_ctl, "Building resources, hook=%s", TSHttpHookNameLookup(hook)); - Dbg(pi_dbg_ctl, "Gathering resources for hook %s with IDs %d", TSHttpHookNameLookup(hook), ids); // If we need the client request headers, make sure it's also available in the client vars. @@ -45,6 +44,14 @@ Resources::gather(const ResourceIDs ids, TSHttpHookID hook) } } + if (ids & RSRC_SERVER_REQUEST_HEADERS) { + Dbg(pi_dbg_ctl, "\tAdding TXN server request header buffers"); + if (TSHttpTxnServerReqGet(state.txnp, &server_bufp, &server_hdr_loc) != TS_SUCCESS) { + Dbg(pi_dbg_ctl, "could not gather bufp/hdr_loc for server request"); + // Not a fatal error - server request may not be available in all hooks + } + } + switch (hook) { case TS_HTTP_READ_RESPONSE_HDR_HOOK: // Read response headers from server @@ -172,6 +179,12 @@ Resources::destroy() } } + if (server_bufp && (server_bufp != bufp) && (server_bufp != client_bufp)) { + if (server_hdr_loc && (server_hdr_loc != hdr_loc) && (server_hdr_loc != client_hdr_loc)) { + TSHandleMLocRelease(server_bufp, TS_NULL_MLOC, server_hdr_loc); + } + } + #if TS_HAS_CRIPTS delete client_conn; delete server_conn; diff --git a/plugins/header_rewrite/resources.h b/plugins/header_rewrite/resources.h index 81705956455..2fb45b08c44 100644 --- a/plugins/header_rewrite/resources.h +++ b/plugins/header_rewrite/resources.h @@ -115,6 +115,8 @@ class Resources TSMLoc hdr_loc = nullptr; TSMBuffer client_bufp = nullptr; TSMLoc client_hdr_loc = nullptr; + TSMBuffer server_bufp = nullptr; + TSMLoc server_hdr_loc = nullptr; #if TS_HAS_CRIPTS cripts::Transaction state; // This now holds txpn / ssnp cripts::Client::Connection *client_conn = nullptr; diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml index 0c9a6ac2363..ff7b94dab6a 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml @@ -145,6 +145,12 @@ autest: args: - "rules/query_sub_key.conf" + - from: "http://www.example.com/from_14/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_14/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_server_conditions.conf" # Proxy verifier sessions @@ -1074,3 +1080,30 @@ sessions: headers: fields: - [ X-Query-Sub, { as: absent } ] + +# Test 30: SERVER-HEADER and SERVER-URL conditions +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_14/test + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 36 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Server-Path, { value: "to_14/test", as: equal } ] + - [ X-Marker-Found, { value: "Yes", as: equal } ] + - [ X-Server-Host-Header, { value: "backend.ex", as: contains } ] + - [ X-Path-Match, { value: "Yes", as: equal } ] diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf new file mode 100644 index 00000000000..8bfa5854f30 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_server_conditions.conf @@ -0,0 +1,32 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Test SERVER-HEADER and SERVER-URL conditions +cond %{SEND_REQUEST_HDR_HOOK} + set-header X-Server-Marker "ATS-Processed" + +cond %{SEND_RESPONSE_HDR_HOOK} + set-header X-Server-Path "%{SERVER-URL:PATH}" + set-header X-Server-Host-Header "%{SERVER-HEADER:Host}" + +cond %{SEND_RESPONSE_HDR_HOOK} [AND] +cond %{SERVER-HEADER:X-Server-Marker} ="ATS-Processed" + set-header X-Marker-Found "Yes" + +cond %{SEND_RESPONSE_HDR_HOOK} [AND] +cond %{SERVER-URL:PATH} /^to_14\// + set-header X-Path-Match "Yes" diff --git a/tools/hrw4u/pyproject.toml b/tools/hrw4u/pyproject.toml index ce607fd5b69..ac34e0ca914 100644 --- a/tools/hrw4u/pyproject.toml +++ b/tools/hrw4u/pyproject.toml @@ -20,7 +20,7 @@ build-backend = "setuptools.build_meta" [project] name = "hrw4u" -version = "1.4.1" +version = "1.4.4" description = "HRW4U CLI tool for Apache Traffic Server header rewrite rules" authors = [ {name = "Leif Hedstrom", email = "leif@apache.org"} diff --git a/tools/hrw4u/src/tables.py b/tools/hrw4u/src/tables.py index 94253fba0a9..80812cc41a6 100644 --- a/tools/hrw4u/src/tables.py +++ b/tools/hrw4u/src/tables.py @@ -111,6 +111,7 @@ "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.": MapParams(target="CLIENT-URL", upper=True, prefix=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), sections=HTTP_SECTIONS), + "nexthop.": MapParams(target="NEXT-HOP", upper=True, prefix=True, validate=Validator.suffix_group(SuffixGroup.NEXTHOP_FIELDS), sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}, rev={"reverse_fallback": "nexthop."}), "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}), "outbound.conn.server-cert.SAN.": MapParams(target="OUTBOUND:SERVER-CERT:SAN", upper=True, prefix=True, validate=Validator.suffix_group(SuffixGroup.SAN_FIELDS), sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}), @@ -120,15 +121,18 @@ "outbound.conn.server-cert.": MapParams(target="OUTBOUND:SERVER-CERT", upper=True, prefix=True, validate=Validator.suffix_group(SuffixGroup.CERT_FIELDS), sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}), "outbound.conn.": MapParams(target="OUTBOUND", upper=True, prefix=True, validate=Validator.suffix_group(SuffixGroup.CONN_FIELDS), sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}), "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.req.": MapParams(target="SERVER-HEADER", prefix=True, validate=Validator.http_header_name(), sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}, rev={"reverse_fallback": "outbound.req."}), "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.": 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}), + "outbound.url.": MapParams(target="SERVER-URL", upper=True, prefix=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), sections={SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}, rev={"reverse_fallback": "outbound.url."}), "to.url.": MapParams(target="TO-URL", upper=True, prefix=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), sections=HTTP_SECTIONS), } FALLBACK_TAG_MAP: dict[str, tuple[str, bool]] = { "HEADER": ("header_condition", True), "CLIENT-HEADER": ("inbound.req.", False), + "SERVER-HEADER": ("outbound.req.", False), + "SERVER-URL": ("outbound.url.", False), + "NEXT-HOP": ("nexthop.", False), "COOKIE": ("inbound.cookie.", False), "INBOUND:CLIENT-CERT": ("inbound.conn.client-cert.", False), "INBOUND:SERVER-CERT": ("inbound.conn.server-cert.", False), diff --git a/tools/hrw4u/src/types.py b/tools/hrw4u/src/types.py index 0cacafe673b..3db3e1bfabf 100644 --- a/tools/hrw4u/src/types.py +++ b/tools/hrw4u/src/types.py @@ -80,6 +80,7 @@ def get_keywords_with_descriptions(cls) -> dict[str, str]: class SuffixGroup(Enum): URL_FIELDS = frozenset({"SCHEME", "HOST", "PORT", "PATH", "QUERY", "URL"}) + NEXTHOP_FIELDS = frozenset({"HOST", "PORT", "STRATEGY"}) GEO_FIELDS = frozenset({"COUNTRY", "COUNTRY-ISO", "ASN", "ASN-NAME"}) CONN_FIELDS = frozenset( { diff --git a/tools/hrw4u/tests/data/conds/nexthop.ast.txt b/tools/hrw4u/tests/data/conds/nexthop.ast.txt new file mode 100644 index 00000000000..ebf488cbd92 --- /dev/null +++ b/tools/hrw4u/tests/data/conds/nexthop.ast.txt @@ -0,0 +1 @@ +(program (programItem (section SEND_REQUEST { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable nexthop.host) == (value "parent.example.com")))))) (block { (blockItem (statement outbound.req.X-Via-Parent = (value "yes") ;)) })))) })) (programItem (section SEND_RESPONSE { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable outbound.req.X-Via-Parent) == (value "yes")))))) (block { (blockItem (statement inbound.resp.X-Next-Host = (value "{nexthop.host}") ;)) (blockItem (statement inbound.resp.X-Next-Port = (value "{nexthop.port}") ;)) (blockItem (statement inbound.resp.X-Next-Strategy = (value "{nexthop.strategy}") ;)) })))) })) ) diff --git a/tools/hrw4u/tests/data/conds/nexthop.input.txt b/tools/hrw4u/tests/data/conds/nexthop.input.txt new file mode 100644 index 00000000000..a36effce81c --- /dev/null +++ b/tools/hrw4u/tests/data/conds/nexthop.input.txt @@ -0,0 +1,13 @@ +SEND_REQUEST { + if nexthop.host == "parent.example.com" { + outbound.req.X-Via-Parent = "yes"; + } +} + +SEND_RESPONSE { + if outbound.req.X-Via-Parent == "yes" { + inbound.resp.X-Next-Host = "{nexthop.host}"; + inbound.resp.X-Next-Port = "{nexthop.port}"; + inbound.resp.X-Next-Strategy = "{nexthop.strategy}"; + } +} diff --git a/tools/hrw4u/tests/data/conds/nexthop.output.txt b/tools/hrw4u/tests/data/conds/nexthop.output.txt new file mode 100644 index 00000000000..e4df4bcc25d --- /dev/null +++ b/tools/hrw4u/tests/data/conds/nexthop.output.txt @@ -0,0 +1,9 @@ +cond %{SEND_REQUEST_HDR_HOOK} [AND] +cond %{NEXT-HOP:HOST} ="parent.example.com" + set-header X-Via-Parent "yes" + +cond %{SEND_RESPONSE_HDR_HOOK} [AND] +cond %{SERVER-HEADER:X-Via-Parent} ="yes" + set-header X-Next-Host "%{NEXT-HOP:HOST}" + set-header X-Next-Port "%{NEXT-HOP:PORT}" + set-header X-Next-Strategy "%{NEXT-HOP:STRATEGY}" diff --git a/tools/hrw4u/tests/data/conds/outbound.output.txt b/tools/hrw4u/tests/data/conds/outbound.output.txt index db30ea0f498..fa2638c23a2 100644 --- a/tools/hrw4u/tests/data/conds/outbound.output.txt +++ b/tools/hrw4u/tests/data/conds/outbound.output.txt @@ -1,3 +1,3 @@ cond %{SEND_REQUEST_HDR_HOOK} [AND] -cond %{NEXT-HOP:HOST} /foo|bar/ - set-header X-Valid "%{NEXT-HOP:PORT}" +cond %{SERVER-URL:HOST} /foo|bar/ + set-header X-Valid "%{SERVER-URL:PORT}" diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt b/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt index bc4bfcfed1d..f404d038eb4 100644 --- a/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt +++ b/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt @@ -1 +1 @@ -(program (programItem (section (varSection VARS { (variables (variablesItem (commentLine # Boolean and integer state you can flip/use across sections)) (variablesItem (variableDecl FlagA : bool ;)) (variablesItem (variableDecl FlagB : bool ;)) (variablesItem (variableDecl Cnt8 : int8 ;)) (variablesItem (variableDecl Big16 : int16 ;))) }))) (programItem (section REMAP { (sectionBody (commentLine # Plugin controls)) (sectionBody (statement http.cntl.TXN_DEBUG = (value true) ;)) (sectionBody (statement http.cntl.LOGGING = (value true) ;)) (sectionBody (statement http.cntl.REQ_CACHEABLE = (value true) ;)) (sectionBody (statement http.cntl.RESP_CACHEABLE = (value true) ;)) (sectionBody (statement http.cntl.SERVER_NO_STORE = (value false) ;)) (sectionBody (statement http.cntl.SKIP_REMAP = (value false) ;)) (sectionBody (statement http.cntl.INTERCEPT_RETRY = (value false) ;)) (sectionBody (commentLine # allow intercept retry)) (sectionBody (commentLine # Plugin-level knobs)) (sectionBody (statement (functionCall set-plugin-cntl ( (argumentList (value "TIMEZONE") , (value "GMT")) )) ;)) (sectionBody (statement (functionCall set-plugin-cntl ( (argumentList (value INBOUND_IP_SOURCE) , (value "PEER")) )) ;)) (sectionBody (statement (functionCall set-config ( (argumentList (value "proxy.config.http.allow_multi_range") , (value 1)) )) ;)) (sectionBody (commentLine # Connection marks (client side))) (sectionBody (statement inbound.conn.dscp = (value 8) ;)) (sectionBody (statement inbound.conn.mark = (value 1734) ;)) (sectionBody (commentLine # Initialize and twiddle user vars)) (sectionBody (statement FlagA = (value true) ;)) (sectionBody (statement FlagB = (value false) ;)) (sectionBody (statement Cnt8 = (value 7) ;)) (sectionBody (statement Big16 = (value 1024) ;)) (sectionBody (commentLine # Simple demo: count every transaction seen)) (sectionBody (statement (functionCall counter ( (argumentList (value "txn_start_count")) )) ;)) })) (programItem (section PRE_REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (term (factor (comparison (comparable inbound.method) == (value "GET") (modifier with (modifierList NOCASE))))) && (factor ( (expression (expression (term (factor (comparison (comparable inbound.url.path) in (set [ (value "mp3") , (value "m3u") , (value "m3u8") ]) (modifier with (modifierList EXT , NOCASE)))))) || (term (factor (comparison (comparable inbound.url.host) ~ (regex /(?i)^api\./))))) )))) ))))) (block { (blockItem (statement inbound.req.X-Prefilter = (value "static-or-api") ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (expression (term (factor (functionCall access ( (argumentList (value "/var/developertesting")) ))))) || (term (factor (functionCall internal ( ))))) ))))) (block { (blockItem (statement inbound.req.X-Prefilter = (value "dev-or-internal") ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (term (factor (comparison (comparable (functionCall random ( (argumentList (value 100)) ))) > (value 50))))) ))))) (block { (blockItem (statement inbound.req.X-Prefilter = (value "coinflip") ;)) })) (elseClause else (block { (blockItem (statement (functionCall no-op ( )) ;)) })))) (sectionBody (commentLine # Show CIDR and ID usage; also demonstrate list membership on method)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (expression (term (factor (comparison (comparable (functionCall cidr ( (argumentList (value 16) , (value 48)) ))) == (value "10.0.0.0"))))) || (term (factor (comparison (comparable (functionCall cidr ( (argumentList (value 8) , (value 8)) ))) == (value "fd00::"))))) ))))) (block { (blockItem (statement inbound.req.X-Network = (value "private") ;)) })))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.method) in (set [ (value "POST") , (value "PUT") ]))))) ))))) (block { (blockItem (statement FlagB = (value true) ;)) })))) })) (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable from.url.host) == (value "old.example.com") (modifier with (modifierList NOCASE)))))) ))))) (block { (blockItem (statement inbound.url.host = (value "new.example.com") ;)) (blockItem (statement (functionCall keep_query ( (argumentList (value "id,utm_campaign")) )) ;)) })))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable to.url.path) ~ (regex /(?i)^\/legacy\//))))) ))))) (block { (blockItem (statement inbound.url.path = (value "/v2/") ;)) (blockItem (statement (functionCall remove_query ( (argumentList (value "debug,trace")) )) ;)) })))) (sectionBody (statement inbound.req.X-foo = (value inbound.conn.client-cert.SAN) ;)) (sectionBody (commentLine # Run a remap plugin conditionally (args are pass-through))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.url.host) == (value "plugins.example"))))) ))))) (block { (blockItem (statement (functionCall run-plugin ( (argumentList (value "regex_remap.so") , (value "in:^/foo/(.*)$") , (value "out:/bar/$1")) )) ;)) })))) (sectionBody (commentLine # Demonstrate last-rule behavior)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.req.X-Bypass) == (value "1"))))) ))))) (block { (blockItem (statement (functionCall skip-remap ( (argumentList (value true)) )) ;)) (blockItem (statement break ;)) (blockItem (commentLine # like [L])) })))) })) (programItem (section READ_REQUEST { (sectionBody (commentLine # Header presence / equality and capture groups)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor inbound.req.Strict-Transport-Security))) ))))) (block { (blockItem (statement inbound.req.X-HSTS-Present = (value "1") ;)) })))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.req.User-Agent) ~ (regex /(?i)foo-(\d+)/))))) ))))) (block { (blockItem (statement inbound.req.X-UA-Capture = (value "{capture.1}") ;)) })))) (sectionBody (commentLine # Cookies: read, set, and remove)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.cookie.session) == (value "admin") (modifier with (modifierList NOCASE)))))) ))))) (block { (blockItem (statement inbound.cookie.role = (value "super") ;)) })) (elseClause else (block { (blockItem (statement inbound.cookie.role = (value "guest") ;)) })))) (sectionBody (statement inbound.cookie.obsolete = (value "") ;)) (sectionBody (commentLine # IP and URL string expansions in values)) (sectionBody (statement inbound.req.X-Client = (value "{inbound.ip} : {inbound.url.port}") ;)) (sectionBody (statement inbound.req.X-Req-Id = (value "{id.UNIQUE}") ;)) (sectionBody (commentLine # Geo lookups as values)) (sectionBody (statement inbound.req.X-Geo = (value "{geo.country}-{geo.asn}") ;)) (sectionBody (commentLine # Example of counters + vars interplay)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor FlagB))) ))))) (block { (blockItem (statement (functionCall counter ( (argumentList (value "write_methods_seen")) )) ;)) (blockItem (statement Cnt8 = (value 42) ;)) (blockItem (commentLine # assign int8)) (blockItem (statement Big16 = (value 6553) ;)) (blockItem (commentLine # assign int16)) })))) })) (programItem (section SEND_REQUEST { (sectionBody (commentLine # Use NEXT-HOP information to adjust Host header)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable outbound.url.host) == (value "www.firstparent.com"))))) ))))) (block { (blockItem (statement outbound.req.Host = (value "vhost.firstparent.com") ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (term (factor (comparison (comparable outbound.url.host) == (value "www.secondparent.com"))))) ))))) (block { (blockItem (statement outbound.req.Host = (value "vhost.secondparent.com") ;)) })))) (sectionBody (commentLine # Demonstrate HTTP control read and write)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor ! (factor http.cntl.LOGGING)))) ))))) (block { (blockItem (statement http.cntl.LOGGING = (value true) ;)) })))) })) (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.req.X-Bar) == (value "fie") (modifier with (modifierList MID))))))) (block { })))) })) (programItem (section READ_RESPONSE { (sectionBody (commentLine # Cache lookup status checks)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable (functionCall cache ( ))) == (value "hit-stale"))))) ))))) (block { (blockItem (statement outbound.resp.X-Cache = (value "stale") ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (term (factor (comparison (comparable (functionCall cache ( ))) == (value "hit-fresh"))))) ))))) (block { (blockItem (statement outbound.resp.X-Cache = (value "fresh") ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (term (factor (comparison (comparable (functionCall cache ( ))) in (set [ (value "miss") , (value "skipped") ]))))) ))))) (block { (blockItem (statement outbound.resp.X-Cache = (value "{cache()}") ;)) (blockItem (commentLine # echo the value)) })))) (sectionBody (commentLine # Status transforms + reason)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (term (factor (comparison (comparable inbound.status) > (value 399)))) && (factor (comparison (comparable inbound.status) < (value 500))))) ))))) (block { (blockItem (statement http.status = (value 404) ;)) (blockItem (statement http.status.reason = (value "Not Here But Somewhere") ;)) })))) (sectionBody (commentLine # Example body overrides (allowed at READ_RESPONSE only))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable outbound.status) > (value 499)))))) (block { (blockItem (statement (functionCall set-body-from ( (argumentList (value "http://errors.example.com/500?rid={id.REQUEST}")) )) ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.status) == (value 418))))) ))))) (block { (blockItem (statement outbound.resp.Content-Type = (value "text/plain") ;)) (blockItem (statement outbound.resp.Server = (value "ATS-HRW4U") ;)) (blockItem (statement inbound.resp.body = (value "I am a teapot, rewritten") ;)) })))) })) (programItem (section SEND_RESPONSE { (sectionBody (statement outbound.resp.Cache-Control = (value "public, max-age=60") ;)) (sectionBody (statement outbound.resp.X-Now = (value "{now.YEAR}-{now.MONTH}-{now.DAY}T{now.HOUR}:{now.MINUTE}") ;)) (sectionBody (statement outbound.resp.X-Ports = (value "in={inbound.url.port};out={outbound.url.port}") ;)) (sectionBody (statement outbound.resp.X-IPs = (value "client={inbound.ip};server={outbound.ip}") ;)) (sectionBody (statement outbound.resp.X-ID = (value "{id.UNIQUE}") ;)) (sectionBody (statement outbound.resp.ATS-Geo-Country = (value "{geo.country}") ;)) (sectionBody (statement outbound.resp.ATS-Geo-ASN = (value "{geo.asn}") ;)) (sectionBody (statement outbound.resp.ATS-Geo-ASN-NAME = (value "{geo.ASN-NAME}") ;)) (sectionBody (statement outbound.cookie.debug = (value "on") ;)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.req.X-Redirect) == (value "1")))))) (block { (blockItem (statement (functionCall set-redirect ( (argumentList (value 302) , (value "https://redirect.example/x?{inbound.url.query}")) )) ;)) })))) (sectionBody (statement (functionCall counter ( (argumentList (value "send_response_count")) )) ;)) })) (programItem (section TXN_CLOSE { (sectionBody (statement (functionCall counter ( (argumentList (value "txn_close_count")) )) ;)) (sectionBody (statement (functionCall no-op ( )) ;)) })) (programItem (section SEND_RESPONSE { (sectionBody (conditional (ifStatement if (condition (expression (term (factor inbound.conn.TLS)))) (block { (blockItem (statement inbound.resp.X-Client-Cert = (value "{inbound.conn.client-cert.PEM}") ;)) (blockItem (statement inbound.resp.X-Client-Cert-Subject = (value "{inbound.conn.client-cert.SUBJECT}") ;)) (blockItem (statement inbound.resp.X-Client-Cert-Issuer = (value "{inbound.conn.client-cert.ISSUER}") ;)) (blockItem (statement inbound.resp.X-Server-Cert-Subject = (value "{inbound.conn.server-cert.SUBJECT}") ;)) (blockItem (statement inbound.resp.X-Server-Cert-Serial = (value "{inbound.conn.server-cert.SERIAL}") ;)) (blockItem (statement inbound.resp.X-Client-SAN-DNS = (value "{inbound.conn.client-cert.SAN.DNS}") ;)) (blockItem (statement inbound.resp.X-Client-SAN-IP = (value "{inbound.conn.client-cert.SAN.IP}") ;)) (blockItem (statement inbound.resp.X-Server-SAN-Email = (value "{inbound.conn.server-cert.SAN.EMAIL}") ;)) (blockItem (statement inbound.resp.X-Server-SAN-URI = (value "{inbound.conn.server-cert.SAN.URI}") ;)) })))) })) (programItem (section SEND_REQUEST { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.conn.client-cert.SAN.DNS) ~ (regex /example\.com/)))))) (block { (blockItem (statement outbound.req.X-Matched-Domain = (value "true") ;)) })))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.conn.client-cert.SUBJECT) ~ (regex /CN=testcert/)))))) (block { (blockItem (statement outbound.req.X-Test-Client = (value "verified") ;)) })))) })) (programItem (section READ_RESPONSE { (sectionBody (statement outbound.resp.X-Outbound-Client-Cert = (value "{outbound.conn.client-cert.PEM}") ;)) (sectionBody (statement outbound.resp.X-Outbound-Server-Subject = (value "{outbound.conn.server-cert.SUBJECT}") ;)) })) ) +(program (programItem (section (varSection VARS { (variables (variablesItem (commentLine # Boolean and integer state you can flip/use across sections)) (variablesItem (variableDecl FlagA : bool ;)) (variablesItem (variableDecl FlagB : bool ;)) (variablesItem (variableDecl Cnt8 : int8 ;)) (variablesItem (variableDecl Big16 : int16 ;))) }))) (programItem (section REMAP { (sectionBody (commentLine # Plugin controls)) (sectionBody (statement http.cntl.TXN_DEBUG = (value true) ;)) (sectionBody (statement http.cntl.LOGGING = (value true) ;)) (sectionBody (statement http.cntl.REQ_CACHEABLE = (value true) ;)) (sectionBody (statement http.cntl.RESP_CACHEABLE = (value true) ;)) (sectionBody (statement http.cntl.SERVER_NO_STORE = (value false) ;)) (sectionBody (statement http.cntl.SKIP_REMAP = (value false) ;)) (sectionBody (statement http.cntl.INTERCEPT_RETRY = (value false) ;)) (sectionBody (commentLine # allow intercept retry)) (sectionBody (commentLine # Plugin-level knobs)) (sectionBody (statement (functionCall set-plugin-cntl ( (argumentList (value "TIMEZONE") , (value "GMT")) )) ;)) (sectionBody (statement (functionCall set-plugin-cntl ( (argumentList (value INBOUND_IP_SOURCE) , (value "PEER")) )) ;)) (sectionBody (statement (functionCall set-config ( (argumentList (value "proxy.config.http.allow_multi_range") , (value 1)) )) ;)) (sectionBody (commentLine # Connection marks (client side))) (sectionBody (statement inbound.conn.dscp = (value 8) ;)) (sectionBody (statement inbound.conn.mark = (value 1734) ;)) (sectionBody (commentLine # Initialize and twiddle user vars)) (sectionBody (statement FlagA = (value true) ;)) (sectionBody (statement FlagB = (value false) ;)) (sectionBody (statement Cnt8 = (value 7) ;)) (sectionBody (statement Big16 = (value 1024) ;)) (sectionBody (commentLine # Simple demo: count every transaction seen)) (sectionBody (statement (functionCall counter ( (argumentList (value "txn_start_count")) )) ;)) })) (programItem (section PRE_REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (term (factor (comparison (comparable inbound.method) == (value "GET") (modifier with (modifierList NOCASE))))) && (factor ( (expression (expression (term (factor (comparison (comparable inbound.url.path) in (set [ (value "mp3") , (value "m3u") , (value "m3u8") ]) (modifier with (modifierList EXT , NOCASE)))))) || (term (factor (comparison (comparable inbound.url.host) ~ (regex /(?i)^api\./))))) )))) ))))) (block { (blockItem (statement inbound.req.X-Prefilter = (value "static-or-api") ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (expression (term (factor (functionCall access ( (argumentList (value "/var/developertesting")) ))))) || (term (factor (functionCall internal ( ))))) ))))) (block { (blockItem (statement inbound.req.X-Prefilter = (value "dev-or-internal") ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (term (factor (comparison (comparable (functionCall random ( (argumentList (value 100)) ))) > (value 50))))) ))))) (block { (blockItem (statement inbound.req.X-Prefilter = (value "coinflip") ;)) })) (elseClause else (block { (blockItem (statement (functionCall no-op ( )) ;)) })))) (sectionBody (commentLine # Show CIDR and ID usage; also demonstrate list membership on method)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (expression (term (factor (comparison (comparable (functionCall cidr ( (argumentList (value 16) , (value 48)) ))) == (value "10.0.0.0"))))) || (term (factor (comparison (comparable (functionCall cidr ( (argumentList (value 8) , (value 8)) ))) == (value "fd00::"))))) ))))) (block { (blockItem (statement inbound.req.X-Network = (value "private") ;)) })))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.method) in (set [ (value "POST") , (value "PUT") ]))))) ))))) (block { (blockItem (statement FlagB = (value true) ;)) })))) })) (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable from.url.host) == (value "old.example.com") (modifier with (modifierList NOCASE)))))) ))))) (block { (blockItem (statement inbound.url.host = (value "new.example.com") ;)) (blockItem (statement (functionCall keep_query ( (argumentList (value "id,utm_campaign")) )) ;)) })))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable to.url.path) ~ (regex /(?i)^\/legacy\//))))) ))))) (block { (blockItem (statement inbound.url.path = (value "/v2/") ;)) (blockItem (statement (functionCall remove_query ( (argumentList (value "debug,trace")) )) ;)) })))) (sectionBody (statement inbound.req.X-foo = (value inbound.conn.client-cert.SAN) ;)) (sectionBody (commentLine # Run a remap plugin conditionally (args are pass-through))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.url.host) == (value "plugins.example"))))) ))))) (block { (blockItem (statement (functionCall run-plugin ( (argumentList (value "regex_remap.so") , (value "in:^/foo/(.*)$") , (value "out:/bar/$1")) )) ;)) })))) (sectionBody (commentLine # Demonstrate last-rule behavior)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.req.X-Bypass) == (value "1"))))) ))))) (block { (blockItem (statement (functionCall skip-remap ( (argumentList (value true)) )) ;)) (blockItem (statement break ;)) (blockItem (commentLine # like [L])) })))) })) (programItem (section READ_REQUEST { (sectionBody (commentLine # Header presence / equality and capture groups)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor inbound.req.Strict-Transport-Security))) ))))) (block { (blockItem (statement inbound.req.X-HSTS-Present = (value "1") ;)) })))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.req.User-Agent) ~ (regex /(?i)foo-(\d+)/))))) ))))) (block { (blockItem (statement inbound.req.X-UA-Capture = (value "{capture.1}") ;)) })))) (sectionBody (commentLine # Cookies: read, set, and remove)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.cookie.session) == (value "admin") (modifier with (modifierList NOCASE)))))) ))))) (block { (blockItem (statement inbound.cookie.role = (value "super") ;)) })) (elseClause else (block { (blockItem (statement inbound.cookie.role = (value "guest") ;)) })))) (sectionBody (statement inbound.cookie.obsolete = (value "") ;)) (sectionBody (commentLine # IP and URL string expansions in values)) (sectionBody (statement inbound.req.X-Client = (value "{inbound.ip} : {inbound.url.port}") ;)) (sectionBody (statement inbound.req.X-Req-Id = (value "{id.UNIQUE}") ;)) (sectionBody (commentLine # Geo lookups as values)) (sectionBody (statement inbound.req.X-Geo = (value "{geo.country}-{geo.asn}") ;)) (sectionBody (commentLine # Example of counters + vars interplay)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor FlagB))) ))))) (block { (blockItem (statement (functionCall counter ( (argumentList (value "write_methods_seen")) )) ;)) (blockItem (statement Cnt8 = (value 42) ;)) (blockItem (commentLine # assign int8)) (blockItem (statement Big16 = (value 6553) ;)) (blockItem (commentLine # assign int16)) })))) })) (programItem (section SEND_REQUEST { (sectionBody (commentLine # Use server URL information to adjust Host header)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable outbound.url.host) == (value "www.firstparent.com"))))) ))))) (block { (blockItem (statement outbound.req.Host = (value "vhost.firstparent.com") ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (term (factor (comparison (comparable outbound.url.host) == (value "www.secondparent.com"))))) ))))) (block { (blockItem (statement outbound.req.Host = (value "vhost.secondparent.com") ;)) })))) (sectionBody (commentLine # Demonstrate HTTP control read and write)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor ! (factor http.cntl.LOGGING)))) ))))) (block { (blockItem (statement http.cntl.LOGGING = (value true) ;)) })))) })) (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.req.X-Bar) == (value "fie") (modifier with (modifierList MID))))))) (block { })))) })) (programItem (section READ_RESPONSE { (sectionBody (commentLine # Cache lookup status checks)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (factor (comparison (comparable (functionCall cache ( ))) == (value "hit-stale"))))) ))))) (block { (blockItem (statement outbound.resp.X-Cache = (value "stale") ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (term (factor (comparison (comparable (functionCall cache ( ))) == (value "hit-fresh"))))) ))))) (block { (blockItem (statement outbound.resp.X-Cache = (value "fresh") ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (term (factor (comparison (comparable (functionCall cache ( ))) in (set [ (value "miss") , (value "skipped") ]))))) ))))) (block { (blockItem (statement outbound.resp.X-Cache = (value "{cache()}") ;)) (blockItem (commentLine # echo the value)) })))) (sectionBody (commentLine # Status transforms + reason)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor ( (expression (term (term (factor (comparison (comparable inbound.status) > (value 399)))) && (factor (comparison (comparable inbound.status) < (value 500))))) ))))) (block { (blockItem (statement http.status = (value 404) ;)) (blockItem (statement http.status.reason = (value "Not Here But Somewhere") ;)) })))) (sectionBody (commentLine # Example body overrides (allowed at READ_RESPONSE only))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable outbound.status) > (value 499)))))) (block { (blockItem (statement (functionCall set-body-from ( (argumentList (value "http://errors.example.com/500?rid={id.REQUEST}")) )) ;)) })) (elifClause elif (condition (expression (term (factor ( (expression (term (factor (comparison (comparable inbound.status) == (value 418))))) ))))) (block { (blockItem (statement outbound.resp.Content-Type = (value "text/plain") ;)) (blockItem (statement outbound.resp.Server = (value "ATS-HRW4U") ;)) (blockItem (statement inbound.resp.body = (value "I am a teapot, rewritten") ;)) })))) })) (programItem (section SEND_RESPONSE { (sectionBody (statement outbound.resp.Cache-Control = (value "public, max-age=60") ;)) (sectionBody (statement outbound.resp.X-Now = (value "{now.YEAR}-{now.MONTH}-{now.DAY}T{now.HOUR}:{now.MINUTE}") ;)) (sectionBody (statement outbound.resp.X-Ports = (value "in={inbound.url.port};out={outbound.url.port}") ;)) (sectionBody (statement outbound.resp.X-IPs = (value "client={inbound.ip};server={outbound.ip}") ;)) (sectionBody (statement outbound.resp.X-ID = (value "{id.UNIQUE}") ;)) (sectionBody (statement outbound.resp.ATS-Geo-Country = (value "{geo.country}") ;)) (sectionBody (statement outbound.resp.ATS-Geo-ASN = (value "{geo.asn}") ;)) (sectionBody (statement outbound.resp.ATS-Geo-ASN-NAME = (value "{geo.ASN-NAME}") ;)) (sectionBody (statement outbound.cookie.debug = (value "on") ;)) (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.req.X-Redirect) == (value "1")))))) (block { (blockItem (statement (functionCall set-redirect ( (argumentList (value 302) , (value "https://redirect.example/x?{inbound.url.query}")) )) ;)) })))) (sectionBody (statement (functionCall counter ( (argumentList (value "send_response_count")) )) ;)) })) (programItem (section TXN_CLOSE { (sectionBody (statement (functionCall counter ( (argumentList (value "txn_close_count")) )) ;)) (sectionBody (statement (functionCall no-op ( )) ;)) })) (programItem (section SEND_RESPONSE { (sectionBody (conditional (ifStatement if (condition (expression (term (factor inbound.conn.TLS)))) (block { (blockItem (statement inbound.resp.X-Client-Cert = (value "{inbound.conn.client-cert.PEM}") ;)) (blockItem (statement inbound.resp.X-Client-Cert-Subject = (value "{inbound.conn.client-cert.SUBJECT}") ;)) (blockItem (statement inbound.resp.X-Client-Cert-Issuer = (value "{inbound.conn.client-cert.ISSUER}") ;)) (blockItem (statement inbound.resp.X-Server-Cert-Subject = (value "{inbound.conn.server-cert.SUBJECT}") ;)) (blockItem (statement inbound.resp.X-Server-Cert-Serial = (value "{inbound.conn.server-cert.SERIAL}") ;)) (blockItem (statement inbound.resp.X-Client-SAN-DNS = (value "{inbound.conn.client-cert.SAN.DNS}") ;)) (blockItem (statement inbound.resp.X-Client-SAN-IP = (value "{inbound.conn.client-cert.SAN.IP}") ;)) (blockItem (statement inbound.resp.X-Server-SAN-Email = (value "{inbound.conn.server-cert.SAN.EMAIL}") ;)) (blockItem (statement inbound.resp.X-Server-SAN-URI = (value "{inbound.conn.server-cert.SAN.URI}") ;)) })))) })) (programItem (section SEND_REQUEST { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.conn.client-cert.SAN.DNS) ~ (regex /example\.com/)))))) (block { (blockItem (statement outbound.req.X-Matched-Domain = (value "true") ;)) })))) (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.conn.client-cert.SUBJECT) ~ (regex /CN=testcert/)))))) (block { (blockItem (statement outbound.req.X-Test-Client = (value "verified") ;)) })))) })) (programItem (section READ_RESPONSE { (sectionBody (statement outbound.resp.X-Outbound-Client-Cert = (value "{outbound.conn.client-cert.PEM}") ;)) (sectionBody (statement outbound.resp.X-Outbound-Server-Subject = (value "{outbound.conn.server-cert.SUBJECT}") ;)) })) ) diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.input.txt b/tools/hrw4u/tests/data/examples/all-nonsense.input.txt index e9d62ea221b..cefa1655561 100644 --- a/tools/hrw4u/tests/data/examples/all-nonsense.input.txt +++ b/tools/hrw4u/tests/data/examples/all-nonsense.input.txt @@ -116,7 +116,7 @@ READ_REQUEST { } SEND_REQUEST { - # Use NEXT-HOP information to adjust Host header + # Use server URL information to adjust Host header if (outbound.url.host == "www.firstparent.com") { outbound.req.Host = "vhost.firstparent.com"; } elif (outbound.url.host == "www.secondparent.com") { diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.output.txt b/tools/hrw4u/tests/data/examples/all-nonsense.output.txt index 0134d0aa969..fa608e90639 100644 --- a/tools/hrw4u/tests/data/examples/all-nonsense.output.txt +++ b/tools/hrw4u/tests/data/examples/all-nonsense.output.txt @@ -146,14 +146,14 @@ cond %{GROUP:END} cond %{SEND_REQUEST_HDR_HOOK} [AND] -# Use NEXT-HOP information to adjust Host header +# Use server URL information to adjust Host header cond %{GROUP} - cond %{NEXT-HOP:HOST} ="www.firstparent.com" + cond %{SERVER-URL:HOST} ="www.firstparent.com" cond %{GROUP:END} set-header Host "vhost.firstparent.com" elif cond %{GROUP} - cond %{NEXT-HOP:HOST} ="www.secondparent.com" + cond %{SERVER-URL:HOST} ="www.secondparent.com" cond %{GROUP:END} set-header Host "vhost.secondparent.com" # Demonstrate HTTP control read and write @@ -210,7 +210,7 @@ elif cond %{SEND_RESPONSE_HDR_HOOK} [AND] set-header Cache-Control "public, max-age=60" set-header X-Now "%{NOW:YEAR}-%{NOW:MONTH}-%{NOW:DAY}T%{NOW:HOUR}:%{NOW:MINUTE}" - set-header X-Ports "in=%{CLIENT-URL:PORT};out=%{NEXT-HOP:PORT}" + set-header X-Ports "in=%{CLIENT-URL:PORT};out=%{SERVER-URL:PORT}" set-header X-IPs "client=%{IP:CLIENT};server=%{IP:SERVER}" set-header X-ID "%{ID:UNIQUE}" set-header ATS-Geo-Country "%{GEO:COUNTRY}" diff --git a/tools/hrw4u/tests/data/hooks/send_request.output.txt b/tools/hrw4u/tests/data/hooks/send_request.output.txt index 65ee8a3f131..43bd0bba95c 100644 --- a/tools/hrw4u/tests/data/hooks/send_request.output.txt +++ b/tools/hrw4u/tests/data/hooks/send_request.output.txt @@ -1,3 +1,3 @@ cond %{SEND_REQUEST_HDR_HOOK} [AND] -cond %{HEADER:X-Send-Request} ="yes" +cond %{SERVER-HEADER:X-Send-Request} ="yes" rm-header X-Send-Request diff --git a/tools/hrw4u/tests/test_lsp.py b/tools/hrw4u/tests/test_lsp.py index 5f2896bd9e4..50453aaa0bd 100644 --- a/tools/hrw4u/tests/test_lsp.py +++ b/tools/hrw4u/tests/test_lsp.py @@ -409,7 +409,7 @@ def test_multi_section_inbound_always_allowed(shared_lsp_client) -> None: def test_outbound_restrictions_batch(shared_lsp_client) -> None: """Batch test outbound restrictions - outbound features have section-specific availability.""" - # outbound.url. is available in PRE_REMAP through SEND_REQUEST, plus READ_RESPONSE, SEND_RESPONSE + # outbound.url. (SERVER-URL) is only available from SEND_REQUEST onwards (server request must exist) # outbound.cookie. is only available from SEND_REQUEST onwards http_sections = ["PRE_REMAP", "REMAP", "READ_REQUEST", "SEND_REQUEST", "READ_RESPONSE"] @@ -430,8 +430,11 @@ def test_outbound_restrictions_batch(shared_lsp_client) -> None: # outbound.cookie. is only available from SEND_REQUEST onwards if section in ["SEND_REQUEST", "READ_RESPONSE"]: assert len(outbound_cookie_items) > 0, f"outbound.cookie. should be in {section}" - # outbound.url. is available in all these sections - assert len(outbound_url_items) > 0, f"outbound.url. should be in {section}" + # outbound.url. (SERVER-URL) is only available from SEND_REQUEST onwards + if section in ["SEND_REQUEST", "READ_RESPONSE"]: + assert len(outbound_url_items) > 0, f"outbound.url. should be in {section}" + else: + assert len(outbound_url_items) == 0, f"outbound.url. should NOT be in {section}" def test_specific_outbound_conn_completions(shared_lsp_client) -> None: