This is an automated email from the ASF dual-hosted git repository. cmcfarlen pushed a commit to branch 10.2.x in repository https://gitbox.apache.org/repos/asf/trafficserver.git
commit f6662dba41ac3359d58a3cebaf025a10a220ccb4 Author: Leif Hedstrom <[email protected]> AuthorDate: Mon Mar 16 10:59:35 2026 -0700 HRW/HRW4U: Adds SERVER-HEADER & SERVER-URL (#12840) - Address CoPilot review comments (cherry picked from commit cd4b79363ec56e35b6c6d5f45ee66fc0b998a6d3) --- doc/admin-guide/configuration/hrw4u.en.rst | 25 ++++++++++++--- 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 | 29 +++++++++++++---- plugins/header_rewrite/resources.h | 2 ++ .../header_rewrite_bundle.replay.yaml | 36 +++++++++++++++++++++- .../rules/rule_server_conditions.conf | 32 +++++++++++++++++++ tools/hrw4u/src/hrw_symbols.py | 2 +- tools/hrw4u/src/tables.py | 21 +++++++------ tools/hrw4u/src/types.py | 1 + tools/hrw4u/tests/data/conds/nexthop.ast.txt | 1 + tools/hrw4u/tests/data/conds/nexthop.input.txt | 13 ++++++++ tools/hrw4u/tests/data/conds/nexthop.output.txt | 9 ++++++ tools/hrw4u/tests/data/conds/outbound.output.txt | 4 +-- .../hrw4u/tests/data/conds/query-param.output.txt | 2 +- .../hrw4u/tests/data/examples/all-nonsense.ast.txt | 2 +- .../tests/data/examples/all-nonsense.input.txt | 2 +- .../tests/data/examples/all-nonsense.output.txt | 8 ++--- .../hrw4u/tests/data/hooks/send_request.output.txt | 2 +- tools/hrw4u/tests/test_lsp.py | 9 ++++-- 22 files changed, 236 insertions(+), 42 deletions(-) diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst index c401dafc4e..2d850faae9 100644 --- a/doc/admin-guide/configuration/hrw4u.en.rst +++ b/doc/admin-guide/configuration/hrw4u.en.rst @@ -236,7 +236,7 @@ Conditions Below is a partial mapping of `header_rewrite` condition symbols to their HRW4U equivalents: ================================= ================================== ================================================ -Header Rewrite HRW4U Description +Header Rewrite HRW4U Description ================================= ================================== ================================================ cond %{ACCESS:/path} access("/path") File exists at "/path" and is accessible by ATS cond %{CACHE} =hit-fresh cache() == "hit-fresh" Cache lookup result status @@ -258,12 +258,13 @@ cond %{IP:SERVER} ="..." outbound.ip == "..." Upstream (n 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:<C>} ="bar" outbound.url.<C> == "bar" Next-hop URL component match, <:ref:`C<admin-plugins-header-rewrite-url-parts>`> is ``host`` etc. -cond %{NEXT-HOP:QUERY:<P>} =bar outbound.url.query.<P> == "bar" Extract specific query parameter ``P`` from next-hop URL +cond %{NEXT-HOP:<C>} ="bar" nexthop.<C> == "bar" Next-hop destination, ``<C>`` is ``host``, ``port``, or ``strategy`` cond %{NOW:<U>} ="..." now.<U> == "..." Current date/time in format, <:ref:`U<admin-plugins-header-rewrite-geo>`> selects time unit cond %{OUTBOUND:CLIENT-CERT:<X>} outbound.client-cert.<X> Access the mTLS / client certificate details, on the outbound (upstream) connection cond %{OUTbOUND:SERVER-CERT:<X>} outbound.client-cert.<X> Access the server (handshake) certificate details, on the outbound connection cond %{RANDOM:500} >250 random(500) > 250 Random number between 0 and the specified range +cond %{SERVER-HEADER:X} =foo outbound.req.X == "foo" Server request header (sent to origin) +cond %{SERVER-URL:<C>} =bar outbound.url.<C> == "bar" Server request URL component (sent to origin) cond %{SSN-TXN-COUNT} >10 ssn-txn-count() > 10 Number of transactions on server connection cond %{TO-URL:<C>} =bar to.url.<C> == "bar" Remap ``To URL`` component match, <:ref:`C<admin-plugins-header-rewrite-url-parts>`> is ``host`` etc. cond %{TO-URL:QUERY:<P>} =bar to.url.query.<P> == "bar" Extract specific query parameter ``P`` from remap ``To URL`` @@ -276,6 +277,20 @@ cond %{HTTP-CNTL:<C>} http.cntl.<C> Check the s cond %{INBOUND:<C>} {in,out}bound.conn.<c> inbound (:ref:`client, user agent<admin-plugins-header-rewrite-inbound>`) connection to ATS ================================= ================================== ================================================ +.. note:: + **Header and URL prefix summary:** + + - ``inbound.req.<header>`` → ``CLIENT-HEADER`` - Headers from the client request + - ``outbound.req.<header>`` → ``SERVER-HEADER`` - Headers in the request sent to origin + - ``inbound.url.<part>`` → ``CLIENT-URL`` - URL from the original client request + - ``outbound.url.<part>`` → ``SERVER-URL`` - URL in the request sent to origin (after remapping) + - ``nexthop.<field>`` → ``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 @@ -336,9 +351,9 @@ HRW4U provides a special ``+=`` operator for adding headers:: The ``+=`` operator only works with the following pre-defined symbols: -- ``inbound.req.<header>`` - Client request headers +- ``inbound.req.<header>`` - Client request headers (maps to ``CLIENT-HEADER``) - ``inbound.resp.<header>`` - Origin response headers -- ``outbound.req.<header>`` - Outbound request headers (context-restricted) +- ``outbound.req.<header>`` - Server request headers (maps to ``SERVER-HEADER``) - ``outbound.resp.<header>`` - 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 3dafdae924..58ac015f79 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:<name>} <operand> + +Value of the header ``<name>`` 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:<part>} <operand> + +The URL of the request being sent to the origin server. This is the URL after +any remapping and modifications have been applied. The ``<part>`` 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 4a4ddc60a4..faf4a19a52 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<MatcherType>(_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 6893709d78..4393ecbab0 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<int>(type)); } // noncopyable @@ -271,7 +273,7 @@ protected: 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 6e5cc4bd06..ffc999f3e0 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 0d4d6e4433..78450147b9 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 @@ -63,12 +70,16 @@ Resources::gather(const ResourceIDs ids, TSHttpHookID hook) case TS_HTTP_SEND_REQUEST_HDR_HOOK: Dbg(pi_dbg_ctl, "Processing TS_HTTP_SEND_REQUEST_HDR_HOOK"); - // Read request headers to server if (ids & RSRC_SERVER_REQUEST_HEADERS) { - Dbg(pi_dbg_ctl, "\tAdding TXN server request header buffers"); - if (TSHttpTxnServerReqGet(state.txnp, &bufp, &hdr_loc) != TS_SUCCESS) { - Dbg(pi_dbg_ctl, "could not gather bufp/hdr_loc for request"); - return; + if (server_bufp && server_hdr_loc) { + bufp = server_bufp; + hdr_loc = server_hdr_loc; + } else { + Dbg(pi_dbg_ctl, "\tAdding TXN server request header buffers"); + if (TSHttpTxnServerReqGet(state.txnp, &bufp, &hdr_loc) != TS_SUCCESS) { + Dbg(pi_dbg_ctl, "could not gather bufp/hdr_loc for request"); + return; + } } } break; @@ -172,6 +183,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 8170595645..2fb45b08c4 100644 --- a/plugins/header_rewrite/resources.h +++ b/plugins/header_rewrite/resources.h @@ -115,6 +115,8 @@ public: 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 8e6e9a995c..81245d5b0d 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 @@ -162,6 +162,13 @@ autest: args: - "rules/complex_logics.conf" + - from: "http://www.example.com/from_16/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_16/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_server_conditions.conf" + log_validation: traffic_out: excludes: @@ -1123,7 +1130,7 @@ sessions: status: 200 # ========================================================================== -# Tests 31-52: Complex GROUP logic tests (rules/complex_logics.conf) +# Tests 31-56: Complex GROUP logic tests (rules/complex_logics.conf) # ========================================================================== # Test 31: GROUP [OR] - only A header present (should match via group) @@ -1783,3 +1790,30 @@ sessions: headers: fields: - [ X-Group-First-Result, { as: absent } ] + +# Test 63: SERVER-HEADER and SERVER-URL conditions +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_16/test + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 63 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Server-Path, { value: "to_16/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 0000000000..e46e2d39da --- /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_16\// + set-header X-Path-Match "Yes" diff --git a/tools/hrw4u/src/hrw_symbols.py b/tools/hrw4u/src/hrw_symbols.py index 558827d6db..f1b04c175d 100644 --- a/tools/hrw4u/src/hrw_symbols.py +++ b/tools/hrw4u/src/hrw_symbols.py @@ -181,7 +181,7 @@ class InverseSymbolResolver(SymbolResolverBase): if tag == "HEADER": return f"{self.get_prefix_for_context('header_condition', section)}{suffix}", False else: - return f"{lhs_prefix}{suffix}", False + return f"{lhs_prefix}{suffix.replace(':', '.')}", False return None def _should_lowercase_suffix(self, tag_match: str, lhs_prefix: str) -> bool: diff --git a/tools/hrw4u/src/tables.py b/tools/hrw4u/src/tables.py index 3b80c5e503..86d2de67a7 100644 --- a/tools/hrw4u/src/tables.py +++ b/tools/hrw4u/src/tables.py @@ -113,6 +113,7 @@ CONDITION_MAP: dict[str, MapParams] = { "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.query.": MapParams(target="CLIENT-URL:QUERY", prefix=True, validate=Validator.http_token(), sections=HTTP_SECTIONS), "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}), @@ -122,29 +123,31 @@ CONDITION_MAP: dict[str, MapParams] = { "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.query.": MapParams(target="NEXT-HOP:QUERY", prefix=True, validate=Validator.http_token(), sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST, SectionType.SEND_REQUEST, SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}), - "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.query.": MapParams(target="SERVER-URL:QUERY", prefix=True, validate=Validator.http_token(), sections={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.query.": MapParams(target="TO-URL:QUERY", prefix=True, validate=Validator.http_token(), sections=HTTP_SECTIONS), "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), + "CLIENT-URL:QUERY": ("inbound.url.query.", False), "COOKIE": ("inbound.cookie.", False), + "FROM-URL:QUERY": ("from.url.query.", False), + "HEADER": ("header_condition", True), "INBOUND:CLIENT-CERT": ("inbound.conn.client-cert.", False), - "INBOUND:SERVER-CERT": ("inbound.conn.server-cert.", False), "INBOUND:CLIENT-CERT:SAN": ("inbound.conn.client-cert.SAN.", False), + "INBOUND:SERVER-CERT": ("inbound.conn.server-cert.", False), "INBOUND:SERVER-CERT:SAN": ("inbound.conn.server-cert.SAN.", False), + "NEXT-HOP": ("nexthop.", False), "OUTBOUND:CLIENT-CERT": ("outbound.conn.client-cert.", False), - "OUTBOUND:SERVER-CERT": ("outbound.conn.server-cert.", False), "OUTBOUND:CLIENT-CERT:SAN": ("outbound.conn.client-cert.SAN.", False), + "OUTBOUND:SERVER-CERT": ("outbound.conn.server-cert.", False), "OUTBOUND:SERVER-CERT:SAN": ("outbound.conn.server-cert.SAN.", False), - "CLIENT-URL:QUERY": ("inbound.url.query.", False), - "NEXT-HOP:QUERY": ("outbound.url.query.", False), - "FROM-URL:QUERY": ("from.url.query.", False), + "SERVER-HEADER": ("outbound.req.", False), + "SERVER-URL": ("outbound.url.", False), "TO-URL:QUERY": ("to.url.query.", False) } diff --git a/tools/hrw4u/src/types.py b/tools/hrw4u/src/types.py index 0cacafe673..3db3e1bfab 100644 --- a/tools/hrw4u/src/types.py +++ b/tools/hrw4u/src/types.py @@ -80,6 +80,7 @@ class LanguageKeyword(Enum): 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 0000000000..ebf488cbd9 --- /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 { (block [...] 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 0000000000..a36effce81 --- /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 0000000000..e4df4bcc25 --- /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 db30ea0f49..fa2638c23a 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/conds/query-param.output.txt b/tools/hrw4u/tests/data/conds/query-param.output.txt index 9a771b1b13..0e612b5e9a 100644 --- a/tools/hrw4u/tests/data/conds/query-param.output.txt +++ b/tools/hrw4u/tests/data/conds/query-param.output.txt @@ -11,7 +11,7 @@ cond %{TO-URL:QUERY:target} ="" [NOT] set-header X-Target "set" cond %{SEND_REQUEST_HDR_HOOK} [AND] -cond %{NEXT-HOP:QUERY:backend} ="fast" +cond %{SERVER-URL:QUERY:backend} ="fast" set-header X-Priority "high" cond %{SEND_RESPONSE_HDR_HOOK} [AND] diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt b/tools/hrw4u/tests/data/examples/all-nonsense.ast.txt index bc4bfcfed1..f404d038eb 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) ;)) (sectionBod [...] +(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) ;)) (sectionBod [...] diff --git a/tools/hrw4u/tests/data/examples/all-nonsense.input.txt b/tools/hrw4u/tests/data/examples/all-nonsense.input.txt index e9d62ea221..cefa165556 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 a51a6fbd1e..bb15eb068e 100644 --- a/tools/hrw4u/tests/data/examples/all-nonsense.output.txt +++ b/tools/hrw4u/tests/data/examples/all-nonsense.output.txt @@ -137,14 +137,14 @@ cond %{GROUP:END} # assign int16 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 @@ -200,7 +200,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 65ee8a3f13..43bd0bba95 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 5f2896bd9e..50453aaa0b 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:
