From a3e5002cf8d90af11ff3d8360330adc78d7a7a79 Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Mon, 5 Jan 2026 15:34:33 -0700 Subject: [PATCH 1/3] hrw: Add native support for hrw4u parsing (CPP) This also adds a new tool, and infrastructure, to allow us to compare and verify that an old and new (hrw4u) configuration generates the same structure of Statements and Operators (functional equivalence). That tool could also be used in an automation process migrating existing configurations to hrw4u. u4wrh: adds an option to disable optimizations --- CMakeLists.txt | 6 + doc/admin-guide/configuration/hrw4u.en.rst | 83 +- doc/admin-guide/plugins/header_rewrite.en.rst | 6 + include/hrw4u/Error.h | 145 ++ include/hrw4u/HRW4UVisitor.h | 100 + include/hrw4u/ObjTypes.h | 245 +++ include/hrw4u/Tables.h | 99 + include/hrw4u/Types.h | 124 ++ include/hrw4u/Visitor.h | 139 ++ plugins/header_rewrite/CMakeLists.txt | 33 + plugins/header_rewrite/condition.cc | 68 + plugins/header_rewrite/condition.h | 40 + plugins/header_rewrite/conditions.h | 205 ++ plugins/header_rewrite/factory.cc | 73 + plugins/header_rewrite/factory.h | 2 + plugins/header_rewrite/header_rewrite.cc | 82 +- plugins/header_rewrite/hrw4u.cc | 470 +++++ plugins/header_rewrite/hrw4u.h | 65 + plugins/header_rewrite/matcher.h | 52 + plugins/header_rewrite/objtypes.cc | 479 +++++ plugins/header_rewrite/objtypes.h | 34 + plugins/header_rewrite/operator.cc | 25 + plugins/header_rewrite/operator.h | 21 + plugins/header_rewrite/operators.h | 410 ++++ plugins/header_rewrite/parser.cc | 92 - plugins/header_rewrite/parser.h | 106 +- plugins/header_rewrite/ruleset.h | 12 + plugins/header_rewrite/statement.h | 30 +- plugins/header_rewrite/types.h | 52 + plugins/header_rewrite/value.cc | 28 +- plugins/header_rewrite/value.h | 18 + src/hrw4u/CMakeLists.txt | 92 + src/hrw4u/Error.cc | 203 ++ src/hrw4u/HRW4UVisitorImpl.cc | 1655 +++++++++++++++++ src/hrw4u/HRW4UVisitorImpl.h | 162 ++ src/hrw4u/Tables.cc | 568 ++++++ src/hrw4u/Types.cc | 186 ++ src/hrw4u/Visitor.cc | 92 + tools/hrw4u/scripts/u4wrh | 4 + tools/hrw4u/src/common.py | 18 +- tools/hrw4u/src/hrw_visitor.py | 6 +- tools/hrw4u/src/tables.py | 4 +- tools/hrw4u/tests/data/ops/skip-remap.ast.txt | 2 +- .../hrw4u/tests/data/ops/skip-remap.input.txt | 2 +- .../tests/data/ops/skip-remap.output.txt | 2 +- tools/hrw_confcmp/CMakeLists.txt | 108 ++ tools/hrw_confcmp/comparator.cc | 643 +++++++ tools/hrw_confcmp/comparator.h | 119 ++ tools/hrw_confcmp/main.cc | 328 ++++ tools/hrw_confcmp/rules_factory.cc | 92 + tools/hrw_confcmp/run_tests.sh | 357 ++++ tools/hrw_confcmp/ts_api_stubs.cc | 953 ++++++++++ 52 files changed, 8680 insertions(+), 260 deletions(-) create mode 100644 include/hrw4u/Error.h create mode 100644 include/hrw4u/HRW4UVisitor.h create mode 100644 include/hrw4u/ObjTypes.h create mode 100644 include/hrw4u/Tables.h create mode 100644 include/hrw4u/Types.h create mode 100644 include/hrw4u/Visitor.h create mode 100644 plugins/header_rewrite/hrw4u.cc create mode 100644 plugins/header_rewrite/hrw4u.h create mode 100644 plugins/header_rewrite/objtypes.cc create mode 100644 plugins/header_rewrite/objtypes.h create mode 100644 plugins/header_rewrite/types.h create mode 100644 src/hrw4u/CMakeLists.txt create mode 100644 src/hrw4u/Error.cc create mode 100644 src/hrw4u/HRW4UVisitorImpl.cc create mode 100644 src/hrw4u/HRW4UVisitorImpl.h create mode 100644 src/hrw4u/Tables.cc create mode 100644 src/hrw4u/Types.cc create mode 100644 src/hrw4u/Visitor.cc create mode 100644 tools/hrw_confcmp/CMakeLists.txt create mode 100644 tools/hrw_confcmp/comparator.cc create mode 100644 tools/hrw_confcmp/comparator.h create mode 100644 tools/hrw_confcmp/main.cc create mode 100644 tools/hrw_confcmp/rules_factory.cc create mode 100755 tools/hrw_confcmp/run_tests.sh create mode 100644 tools/hrw_confcmp/ts_api_stubs.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 7df4f8b63ca..58e88921239 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -761,6 +761,9 @@ add_subdirectory(src/traffic_via) if(ENABLE_CRIPTS) add_subdirectory(src/cripts) endif() + +# HRW4U C++ parser library (optional, requires ANTLR4) +add_subdirectory(src/hrw4u) if(ENABLE_AUTEST) add_subdirectory(tests) endif() @@ -800,6 +803,9 @@ if(ENABLE_BENCHMARKS) add_subdirectory(tools/benchmark) endif() +# Add hrw_confcmp comparison tool (requires ANTLR4) +add_subdirectory(tools/hrw_confcmp) + add_custom_target( clang-format-install COMMAND ${CMAKE_SOURCE_DIR}/tools/clang-format.sh --install diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst index 8a3bed3d862..6ef5c54455d 100644 --- a/doc/admin-guide/configuration/hrw4u.en.rst +++ b/doc/admin-guide/configuration/hrw4u.en.rst @@ -29,11 +29,19 @@ Overview ======== HRW4U replaces the free-form text parsing of ``header_rewrite`` with a formally defined -grammar using ANTLR. This makes HRW4U easier to parse, validate, and extend. +grammar using ANTLR. When |TS| is built with ANTLR4 support, the plugin **natively** +parses ``.hrw4u`` files. Simply use files with the ``.hrw4u`` extension: -Rather than repeating ``header_rewrite`` documentation, please refer to: - - :ref:`admin-plugins-header-rewrite` for feature behavior and semantics - - This page focuses on syntax and behavior *differences* in HRW4U +.. code-block:: none + + # In plugin.config for global rules + header_rewrite.so rules.hrw4u + + # In remap.config for per-mapping rules + map http://a http://b @plugin=header_rewrite.so @pparam=rules.hrw4u + +For feature behavior and semantics, refer to :ref:`admin-plugins-header-rewrite`. +This page focuses on syntax differences in HRW4U. Why HRW4U? ---------- @@ -43,52 +51,22 @@ HRW4U aims to improve the following: - Structured grammar and parser - Better error diagnostics (line/col, filename, hints) -- Proper nested condition support using `if (...)` and `{ ... }` blocks +- Proper nested condition support using ``if (...) { ... }`` blocks - Symbol tables for variable declarations and usage - Static validation of operand types and value ranges -- Explicit `VARS` declarations with typed variables (`bool`, `int8`, `int16`) +- Explicit ``VARS`` declarations with typed variables (``bool``, ``int8``, ``int16``) - Optional debug output to trace logic evaluation -Building --------- - -Currently, the HRW4U compiler is not built as part of the ATS build process. You need to -build it separately using Python 3.10+ and pyenv environments. There's a ``bootstrap.sh`` -script in the ``tools/hrw4u`` directory that helps with the setup process. - -Once set up, simply run: - -.. code-block:: none - - make - make package - -This will produce a PIP package in the ``dist`` directory. You can install it in a -virtualenv or system-wide using: - -.. code-block:: none - - pipx install dist/hrw4u-1.4.0-py3-none-any.whl - -Using ------ - -Once installed, you will have a ``hrw4u`` command available. You can run it as -follows to produce the help output: - -.. code-block:: none - - hrw4u --help - -Doing a compile is simply: +Standalone Compiler +------------------- -.. code-block:: none +A standalone Python compiler is available in ``tools/hrw4u`` for development: - hrw4u some_file.hrw4u +- Debug tracing (``--debug``) +- IDE integration via LSP (``hrw4u-lsp``) +- Reverse conversion (``u4wrh``) to convert header_rewrite to hrw4u -in Addition to ``hrw4u``, you also have the reverse tool, converting existing ``header_rewrite`` -configurations to ``hrw4u``. This tool is named ``u4wrh``. For people using IDEs, the package also -provides an LSP for this language, named ``hrw4u-lsp``. +Build with Python 3.10+ using ``./bootstrap.sh && make package``. Syntax Differences ================== @@ -246,11 +224,13 @@ rm-destination QUERY ... [I] keep_query("foo,bar") Keep only specif run-plugin foo.so "args" run-plugin("foo.so", "arg1", ...) Run an external remap plugin set-body "foo" inbound.resp.body = "foo" Set the response body set-body-from "\https://..." set-body-from("\https://...") Set the response body from a URL +set-cc-alg "cubic" set-cc-alg("cubic") Set the TCP congestion control algorithm set-config 12 set-config("name", 17) Set a configuration variable to a value set-conn-dscp 8 inbound.conn.dscp = 8 Set the DSCP value for the connection set-conn-mark 17 inbound.conn.mark = 17 Set the MARK value for the connection set-cookie foo bar {in,out}bound.cookie.foo = "bar" Set a request/response cookie named foo set-destination bar {in,out}bound.url. = "bar" Set a URL component, <:ref:`C`> is path, query etc. +set-effective-address "1.2.3" set-effective-address("1.2.3.4") Set the client's effective address set-header X-Bar foo inbound.{req,resp}.X-Bar = "foo" Assign a client request/origin response header set-plugin-cntl set-plugin-cntl() = Set the plugin control to , see <:ref:`C`> set-redirect set-redirect(302, "\https://...") Set a redirect response @@ -413,20 +393,11 @@ These can be used with both sets and equality checks, using the ``with`` keyword ... } -Running and Debugging -===================== - -To run HRW4U, just install and run the hrw4u compiler: - -.. code-block:: none - - hrw4u /path/to/rules.hrw4u - -Run with `--debug all` to trace: +Debugging +========= -- Lexer, parser, visitor behavior -- Condition evaluations -- State and output emission +Syntax errors are reported with filename, line, and column position. For development, +the standalone compiler's ``--debug all`` option traces lexer, parser, and evaluation. Examples ======== diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index c30af3fd70a..fbed5d0a4af 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -72,6 +72,12 @@ to keep it in the same location as your other proxy configuration files. The paths given to the configuration file(s) may be absolute (leading with a ``/`` character), or they may be relative to the |TS| configuration directory. +.. note:: + An alternative syntax called **HRW4U** is available, offering cleaner syntax + with ``if/else`` blocks and better error messages. Files with the ``.hrw4u`` + extension are automatically parsed using this format. See :ref:`admin-hrw4u` + for details. + There are two methods for enabling this plugin, based on whether you wish it to operate globally on every request that passes through your proxy, or only on some subset of the requests by enabling it only for specific mapping rules. diff --git a/include/hrw4u/Error.h b/include/hrw4u/Error.h new file mode 100644 index 00000000000..d80bb79c2b7 --- /dev/null +++ b/include/hrw4u/Error.h @@ -0,0 +1,145 @@ +/* + 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. +*/ + +#pragma once + +#include +#include +#include +#include + +namespace hrw4u +{ + +struct SourceLocation { + std::string filename; + std::string context; + size_t line = 0; + size_t column = 0; + size_t length = 0; + + [[nodiscard]] std::string format() const; + + [[nodiscard]] bool + is_valid() const + { + return line > 0; + } +}; + +enum class ErrorSeverity { Warning, Error, Fatal }; + +struct ParseError { + std::string message; + std::string code; + ErrorSeverity severity = ErrorSeverity::Error; + SourceLocation location; + + [[nodiscard]] std::string format() const; + [[nodiscard]] std::string_view severity_str() const; +}; + +using ErrorCallback = std::function; + +class ErrorCollector +{ +public: + ErrorCollector() = default; + + explicit ErrorCollector(ErrorCallback callback); + void add_error(ParseError error); + void add_error(ErrorSeverity severity, std::string message, SourceLocation location = {}, std::string code = {}); + + void + warning(std::string message, SourceLocation location = {}) + { + add_error(ErrorSeverity::Warning, std::move(message), std::move(location)); + } + + void + error(std::string message, SourceLocation location = {}) + { + add_error(ErrorSeverity::Error, std::move(message), std::move(location)); + } + + void + fatal(std::string message, SourceLocation location = {}) + { + add_error(ErrorSeverity::Fatal, std::move(message), std::move(location)); + } + + [[nodiscard]] bool has_errors() const; + [[nodiscard]] bool has_fatal() const; + + [[nodiscard]] bool + has_messages() const + { + return !_errors.empty(); + } + + [[nodiscard]] size_t error_count() const; + + [[nodiscard]] const std::vector & + errors() const + { + return _errors; + } + + void clear(); + + [[nodiscard]] std::string format_all() const; + [[nodiscard]] std::string summary() const; + + void + set_filename(std::string filename) + { + _current_filename = std::move(filename); + } + + [[nodiscard]] const std::string & + current_filename() const + { + return _current_filename; + } + +private: + std::vector _errors; + ErrorCallback _callback; + std::string _current_filename; +}; + +class ParseException : public std::exception +{ +public: + explicit ParseException(ParseError error); + explicit ParseException(std::string message, SourceLocation location = {}); + + [[nodiscard]] const char *what() const noexcept override; + + [[nodiscard]] const ParseError & + error() const + { + return _error; + } + +private: + ParseError _error; + std::string _formatted; +}; + +} // namespace hrw4u diff --git a/include/hrw4u/HRW4UVisitor.h b/include/hrw4u/HRW4UVisitor.h new file mode 100644 index 00000000000..f89df5be611 --- /dev/null +++ b/include/hrw4u/HRW4UVisitor.h @@ -0,0 +1,100 @@ +/* + 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. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "hrw4u/Types.h" +#include "hrw4u/Error.h" +#include "hrw4u/Tables.h" +#include "hrw4u/Visitor.h" + +namespace hrw4u +{ + +class HRW4UVisitorImpl; + +class HRW4UVisitor +{ +public: + HRW4UVisitor(const FactoryCallbacks &callbacks, const ParserConfig &config); + ~HRW4UVisitor(); + + HRW4UVisitor(const HRW4UVisitor &) = delete; + HRW4UVisitor &operator=(const HRW4UVisitor &) = delete; + HRW4UVisitor(HRW4UVisitor &&) noexcept; + HRW4UVisitor &operator=(HRW4UVisitor &&) noexcept; + + ParseResult parse(std::string_view input); + ParseResult parse_file(std::string_view filename); + + [[nodiscard]] bool has_errors() const; + [[nodiscard]] const ErrorCollector &errors() const; + +private: + std::unique_ptr _impl; +}; + +enum class ModifierType { CONDITION, OPERATOR, UNKNOWN }; + +struct ModifierInfo { + std::string name; + ModifierType type = ModifierType::UNKNOWN; + + static ModifierInfo parse(std::string_view mod); + static bool is_condition_modifier(std::string_view mod); + static bool is_operator_modifier(std::string_view mod); +}; + +struct CondState { + bool not_modifier = false; + bool or_modifier = false; + bool and_modifier = false; + bool last_modifier = false; + bool nocase_modifier = false; + bool ext_modifier = false; + bool pre_modifier = false; + + void reset(); + void add_modifier(std::string_view mod); + + [[nodiscard]] std::vector to_list() const; + [[nodiscard]] std::string render_suffix() const; + [[nodiscard]] CondState copy() const; +}; + +struct OperatorState { + bool last_modifier = false; + bool qsa_modifier = false; + bool inv_modifier = false; + + void reset(); + void add_modifier(std::string_view mod); + + [[nodiscard]] std::vector to_list() const; + [[nodiscard]] std::string render_suffix() const; +}; + +} // namespace hrw4u diff --git a/include/hrw4u/ObjTypes.h b/include/hrw4u/ObjTypes.h new file mode 100644 index 00000000000..5e574b37617 --- /dev/null +++ b/include/hrw4u/ObjTypes.h @@ -0,0 +1,245 @@ +/* + 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. +*/ +#pragma once + +#include +#include + +namespace hrw +{ + +enum class ConditionType : uint8_t { + NONE = 0, + COND_TRUE, + COND_FALSE, + COND_STATUS, + COND_METHOD, + COND_RANDOM, + COND_ACCESS, + COND_COOKIE, + COND_HEADER, + COND_CLIENT_HEADER, + COND_CLIENT_URL, + COND_URL, + COND_FROM_URL, + COND_TO_URL, + COND_DBM, + COND_INTERNAL_TXN, + COND_IP, + COND_TRANSACT_COUNT, + COND_NOW, + COND_GEO, + COND_ID, + COND_CIDR, + COND_INBOUND, + COND_SESSION_TRANSACT_COUNT, + COND_TCP_INFO, + COND_CACHE, + COND_NEXT_HOP, + COND_HTTP_CNTL, + COND_GROUP, + COND_STATE_FLAG, + COND_STATE_INT8, + COND_STATE_INT16, + COND_LAST_CAPTURE, +}; + +enum class OperatorType : uint8_t { + NONE = 0, + RM_HEADER, + SET_HEADER, + ADD_HEADER, + SET_CONFIG, + SET_STATUS, + SET_STATUS_REASON, + SET_DESTINATION, + RM_DESTINATION, + SET_REDIRECT, + TIMEOUT_OUT, + SKIP_REMAP, + NO_OP, + COUNTER, + RM_COOKIE, + SET_COOKIE, + ADD_COOKIE, + SET_CONN_DSCP, + SET_CONN_MARK, + SET_DEBUG, + SET_BODY, + SET_HTTP_CNTL, + SET_PLUGIN_CNTL, + RUN_PLUGIN, + SET_BODY_FROM, + SET_STATE_FLAG, + SET_STATE_INT8, + SET_STATE_INT16, + SET_EFFECTIVE_ADDRESS, + SET_NEXT_HOP_STRATEGY, + SET_CC_ALG, + IF, +}; + +// Returns the canonical condition name (e.g., "STATUS", "METHOD", "IP") +constexpr std::string_view +condition_type_name(ConditionType type) +{ + switch (type) { + case ConditionType::NONE: + return ""; + case ConditionType::COND_TRUE: + return "TRUE"; + case ConditionType::COND_FALSE: + return "FALSE"; + case ConditionType::COND_STATUS: + return "STATUS"; + case ConditionType::COND_METHOD: + return "METHOD"; + case ConditionType::COND_RANDOM: + return "RANDOM"; + case ConditionType::COND_ACCESS: + return "ACCESS"; + case ConditionType::COND_COOKIE: + return "COOKIE"; + case ConditionType::COND_HEADER: + return "HEADER"; + case ConditionType::COND_CLIENT_HEADER: + return "CLIENT-HEADER"; + case ConditionType::COND_CLIENT_URL: + return "CLIENT-URL"; + case ConditionType::COND_URL: + return "URL"; + case ConditionType::COND_FROM_URL: + return "FROM-URL"; + case ConditionType::COND_TO_URL: + return "TO-URL"; + case ConditionType::COND_DBM: + return "DBM"; + case ConditionType::COND_INTERNAL_TXN: + return "INTERNAL-TRANSACTION"; + case ConditionType::COND_IP: + return "IP"; + case ConditionType::COND_TRANSACT_COUNT: + return "TXN-COUNT"; + case ConditionType::COND_NOW: + return "NOW"; + case ConditionType::COND_GEO: + return "GEO"; + case ConditionType::COND_ID: + return "ID"; + case ConditionType::COND_CIDR: + return "CIDR"; + case ConditionType::COND_INBOUND: + return "INBOUND"; + case ConditionType::COND_SESSION_TRANSACT_COUNT: + return "SSN-TXN-COUNT"; + case ConditionType::COND_TCP_INFO: + return "TCP-INFO"; + case ConditionType::COND_CACHE: + return "CACHE"; + case ConditionType::COND_NEXT_HOP: + return "NEXT-HOP"; + case ConditionType::COND_HTTP_CNTL: + return "HTTP-CNTL"; + case ConditionType::COND_GROUP: + return "GROUP"; + case ConditionType::COND_STATE_FLAG: + return "STATE-FLAG"; + case ConditionType::COND_STATE_INT8: + return "STATE-INT8"; + case ConditionType::COND_STATE_INT16: + return "STATE-INT16"; + case ConditionType::COND_LAST_CAPTURE: + return "LAST-CAPTURE"; + } + return ""; +} + +// Returns the canonical operator name (e.g., "set-header", "rm-cookie") +constexpr std::string_view +operator_type_name(OperatorType type) +{ + switch (type) { + case OperatorType::NONE: + return ""; + case OperatorType::RM_HEADER: + return "rm-header"; + case OperatorType::SET_HEADER: + return "set-header"; + case OperatorType::ADD_HEADER: + return "add-header"; + case OperatorType::SET_CONFIG: + return "set-config"; + case OperatorType::SET_STATUS: + return "set-status"; + case OperatorType::SET_STATUS_REASON: + return "set-status-reason"; + case OperatorType::SET_DESTINATION: + return "set-destination"; + case OperatorType::RM_DESTINATION: + return "rm-destination"; + case OperatorType::SET_REDIRECT: + return "set-redirect"; + case OperatorType::TIMEOUT_OUT: + return "timeout-out"; + case OperatorType::SKIP_REMAP: + return "skip-remap"; + case OperatorType::NO_OP: + return "no-op"; + case OperatorType::COUNTER: + return "counter"; + case OperatorType::RM_COOKIE: + return "rm-cookie"; + case OperatorType::SET_COOKIE: + return "set-cookie"; + case OperatorType::ADD_COOKIE: + return "add-cookie"; + case OperatorType::SET_CONN_DSCP: + return "set-conn-dscp"; + case OperatorType::SET_CONN_MARK: + return "set-conn-mark"; + case OperatorType::SET_DEBUG: + return "set-debug"; + case OperatorType::SET_BODY: + return "set-body"; + case OperatorType::SET_HTTP_CNTL: + return "set-http-cntl"; + case OperatorType::SET_PLUGIN_CNTL: + return "set-plugin-cntl"; + case OperatorType::RUN_PLUGIN: + return "run-plugin"; + case OperatorType::SET_BODY_FROM: + return "set-body-from"; + case OperatorType::SET_STATE_FLAG: + return "set-state-flag"; + case OperatorType::SET_STATE_INT8: + return "set-state-int8"; + case OperatorType::SET_STATE_INT16: + return "set-state-int16"; + case OperatorType::SET_EFFECTIVE_ADDRESS: + return "set-effective-address"; + case OperatorType::SET_NEXT_HOP_STRATEGY: + return "set-next-hop-strategy"; + case OperatorType::SET_CC_ALG: + return "set-cc-alg"; + case OperatorType::IF: + return "if"; + } + return ""; +} + +} // namespace hrw diff --git a/include/hrw4u/Tables.h b/include/hrw4u/Tables.h new file mode 100644 index 00000000000..a5394e62f8e --- /dev/null +++ b/include/hrw4u/Tables.h @@ -0,0 +1,99 @@ +/* + 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. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "hrw4u/Types.h" +#include "hrw4u/ObjTypes.h" + +namespace hrw4u +{ + +enum class OperatorPrefix { NONE, SET_ADD_RM, SET_RM }; + +struct MapParams { + std::string target; + SectionSet sections; + SuffixGroup suffix_group = SuffixGroup::URL_FIELDS; + OperatorPrefix op_prefix = OperatorPrefix::NONE; + hrw::ConditionType cond_type = hrw::ConditionType::NONE; + hrw::OperatorType op_type = hrw::OperatorType::NONE; + bool upper = false; + bool prefix = false; + bool has_suffix_validation = false; + bool bare = false; // Don't wrap generated target in %{} + + [[nodiscard]] bool + valid_for_section(SectionType section) const + { + return sections.empty() || sections.count(section) > 0; + } +}; + +struct ResolveResult { + std::string target; + std::string suffix; + std::string error_message; + OperatorPrefix op_prefix = OperatorPrefix::NONE; + hrw::ConditionType cond_type = hrw::ConditionType::NONE; + hrw::OperatorType op_type = hrw::OperatorType::NONE; + bool success = false; + bool prefix = false; + + [[nodiscard]] explicit + operator bool() const + { + return success; + } + [[nodiscard]] hrw::OperatorType get_operator_type(bool is_append = false, bool is_remove = false) const; +}; + +class SymbolResolver +{ +public: + SymbolResolver(); + + [[nodiscard]] ResolveResult resolve_operator(std::string_view symbol, SectionType section) const; + [[nodiscard]] ResolveResult resolve_condition(std::string_view symbol, SectionType section) const; + [[nodiscard]] ResolveResult resolve_function(std::string_view name, SectionType section) const; + [[nodiscard]] ResolveResult resolve_statement_function(std::string_view name, SectionType section) const; + [[nodiscard]] std::optional resolve_hook(std::string_view name) const; + [[nodiscard]] std::optional resolve_var_type(std::string_view name) const; + [[nodiscard]] const MapParams *get_operator_params(std::string_view prefix) const; + [[nodiscard]] const MapParams *get_condition_params(std::string_view prefix) const; + +private: + [[nodiscard]] ResolveResult resolve_in_table(std::string_view symbol, const std::unordered_map &table, + SectionType section) const; + + std::unordered_map _operator_map; + std::unordered_map _condition_map; + std::unordered_map _function_map; + std::unordered_map _statement_function_map; + std::unordered_map _hook_map; + std::unordered_map _var_type_map; +}; + +[[nodiscard]] const SymbolResolver &symbol_resolver(); + +} // namespace hrw4u diff --git a/include/hrw4u/Types.h b/include/hrw4u/Types.h new file mode 100644 index 00000000000..613d55241ef --- /dev/null +++ b/include/hrw4u/Types.h @@ -0,0 +1,124 @@ +/* + 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. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "hrw4u/ObjTypes.h" +#include + +namespace hrw4u +{ + +enum class SectionType : uint8_t { + UNKNOWN = 0, + READ_REQUEST = 1, + SEND_REQUEST = 2, + READ_RESPONSE = 3, + SEND_RESPONSE = 4, + PRE_REMAP = 5, + POST_REMAP = 6, + REMAP = 7, + TXN_START = 8, + TXN_CLOSE = 9, +}; + +[[nodiscard]] std::string_view section_type_to_string(SectionType type); +[[nodiscard]] SectionType section_type_from_string(std::string_view name); + +enum class VarType : uint8_t { + BOOL = 0, + INT8 = 1, + INT16 = 2, +}; + +struct VarTypeInfo { + std::string_view name; + std::string_view cond_tag; + std::string_view op_tag; + hrw::OperatorType op_type; + int limit; +}; + +[[nodiscard]] const VarTypeInfo &var_type_info(VarType type); +[[nodiscard]] std::string_view var_type_to_string(VarType type); +[[nodiscard]] std::optional var_type_from_string(std::string_view name); + +enum class SuffixGroup : uint8_t { + URL_FIELDS, + HTTP_CNTL_FIELDS, + CONN_FIELDS, + GEO_FIELDS, + ID_FIELDS, + DATE_FIELDS, + CERT_FIELDS, + SAN_FIELDS, + BOOL_FIELDS, + PLUGIN_CNTL_FIELDS, +}; + +[[nodiscard]] bool validate_suffix(SuffixGroup group, std::string_view suffix); +[[nodiscard]] const std::vector &get_valid_suffixes(SuffixGroup group); + +// Utility functions for case conversion +inline std::string +to_lower(std::string_view s) +{ + std::string result; + + result.reserve(s.size()); + for (char c : s) { + result.push_back(static_cast(std::tolower(static_cast(c)))); + } + + return result; +} + +inline std::string +to_upper(std::string_view s) +{ + std::string result; + + result.reserve(s.size()); + for (char c : s) { + result.push_back(static_cast(std::toupper(static_cast(c)))); + } + + return result; +} + +struct Variable { + std::string name; + VarType type = VarType::BOOL; + int slot = -1; + + [[nodiscard]] bool + has_explicit_slot() const + { + return slot >= 0; + } +}; + +using SectionSet = std::set; + +} // namespace hrw4u diff --git a/include/hrw4u/Visitor.h b/include/hrw4u/Visitor.h new file mode 100644 index 00000000000..fb063b363d9 --- /dev/null +++ b/include/hrw4u/Visitor.h @@ -0,0 +1,139 @@ +/* + 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. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "hrw4u/Types.h" +#include "hrw4u/Error.h" +#include "hrw4u/ObjTypes.h" + +namespace antlr4 +{ +class ParserRuleContext; +namespace tree +{ + class ParseTree; +} +} // namespace antlr4 + +namespace hrw4u +{ + +class SymbolResolver; +struct ParseError; + +class ParserContext +{ +public: + ParserContext() = default; + + ParserContext(const ParserContext &) = delete; + ParserContext &operator=(const ParserContext &) = delete; + ParserContext(ParserContext &&) = default; + ParserContext &operator=(ParserContext &&) = default; + + std::string op; + std::string arg; + std::string val; + std::vector mods; + char *from_url = nullptr; + char *to_url = nullptr; + hrw::ConditionType cond_type = hrw::ConditionType::NONE; + hrw::OperatorType op_type = hrw::OperatorType::NONE; + + bool consume_mod(const std::string &m); + bool validate_mods() const; + + const std::vector & + get_mods() const + { + return mods; + } +}; + +enum class CondClause { IF, ELIF, ELSE }; + +using ConditionFactory = std::function; +using OperatorFactory = std::function; +using RuleSetFactory = std::function; +using AddConditionCallback = std::function; +using AddOperatorCallback = std::function; +using AddConditionToIfCallback = std::function; +using AddOperatorToIfCallback = std::function; +using AddConditionToGroupCallback = std::function; +using CreateIfOperatorCallback = std::function; +using NewSectionCallback = std::function; +using NewRuleSetSectionCallback = std::function; +using DestroyCallback = std::function; + +struct FactoryCallbacks { + ConditionFactory create_condition; + OperatorFactory create_operator; + RuleSetFactory create_ruleset; + AddConditionCallback add_condition; + AddOperatorCallback add_operator; + AddConditionToIfCallback add_condition_to_if; + AddOperatorToIfCallback add_operator_to_if; + AddConditionToGroupCallback add_condition_to_group; + CreateIfOperatorCallback create_if_operator; + NewSectionCallback new_section; + NewRuleSetSectionCallback new_ruleset_section; + DestroyCallback destroy; + + [[nodiscard]] bool + valid() const + { + return create_condition && create_operator && create_ruleset && add_condition && add_operator; + } +}; + +struct ParseResult { + bool success = false; + std::vector rulesets; + std::vector sections; + ErrorCollector errors; + + void cleanup(const DestroyCallback &destroy); + + explicit + operator bool() const + { + return success; + } +}; + +struct ParserConfig { + SectionType default_hook = SectionType::READ_RESPONSE; + bool strict_mode = true; + bool allow_break = true; + std::string filename; + char *from_url = nullptr; + char *to_url = nullptr; +}; + +[[nodiscard]] ParseResult parse_hrw4u(std::string_view input, const FactoryCallbacks &callbacks, const ParserConfig &config); +[[nodiscard]] ParseResult parse_hrw4u_file(std::string_view filename, const FactoryCallbacks &callbacks, ParserConfig config); + +} // namespace hrw4u diff --git a/plugins/header_rewrite/CMakeLists.txt b/plugins/header_rewrite/CMakeLists.txt index 4c658b78ba9..4277f3cdc7d 100644 --- a/plugins/header_rewrite/CMakeLists.txt +++ b/plugins/header_rewrite/CMakeLists.txt @@ -15,11 +15,34 @@ # ####################### +# Native hrw4u parsing support +if(TARGET ts::hrw4u) + set(HRW4U_NATIVE_DEFAULT ON) +else() + set(HRW4U_NATIVE_DEFAULT OFF) +endif() + +option(ENABLE_HRW4U_NATIVE "Enable native hrw4u parsing (requires ANTLR4)" ${HRW4U_NATIVE_DEFAULT}) + +if(ENABLE_HRW4U_NATIVE) + if(TARGET ts::hrw4u) + message(STATUS "header_rewrite: Native hrw4u parsing enabled") + set(HRW4U_NATIVE_ENABLED TRUE) + else() + message(WARNING "header_rewrite: Native hrw4u parsing requested but ANTLR4 not available - disabled") + set(HRW4U_NATIVE_ENABLED FALSE) + endif() +else() + message(STATUS "header_rewrite: Native hrw4u parsing disabled") + set(HRW4U_NATIVE_ENABLED FALSE) +endif() + add_atsplugin( header_rewrite condition.cc conditions.cc factory.cc + objtypes.cc header_rewrite.cc lulu.cc matcher.cc @@ -33,6 +56,12 @@ add_atsplugin( value.cc ) +# Add hrw4u integration if enabled +if(HRW4U_NATIVE_ENABLED) + target_sources(header_rewrite PRIVATE hrw4u.cc) + target_compile_definitions(header_rewrite PRIVATE ENABLE_HRW4U_NATIVE=1) +endif() + add_library(header_rewrite_parser STATIC parser.cc) target_link_libraries(header_rewrite_parser PUBLIC libswoc::libswoc) @@ -47,6 +76,10 @@ if(ENABLE_CRIPTS) target_compile_definitions(header_rewrite PRIVATE TS_HAS_CRIPTS=1) endif() +if(HRW4U_NATIVE_ENABLED) + target_link_libraries(header_rewrite PRIVATE ts::hrw4u) +endif() + if(maxminddb_FOUND) target_compile_definitions(header_rewrite PUBLIC TS_USE_HRW_MAXMINDDB=1) target_sources(header_rewrite PRIVATE conditions_geo_maxmind.cc) diff --git a/plugins/header_rewrite/condition.cc b/plugins/header_rewrite/condition.cc index 3d0de684776..7d282706b76 100644 --- a/plugins/header_rewrite/condition.cc +++ b/plugins/header_rewrite/condition.cc @@ -132,3 +132,71 @@ Condition::initialize(Parser &p) _cond_op = parse_matcher_op(p.get_arg()); p.validate_mods(); } + +bool +Condition::equals(const Statement *other) const +{ + if (!Statement::equals(other)) { + return false; + } + + auto *cond = static_cast(other); + + // Compare base Condition state + if (_qualifier != cond->_qualifier || _cond_op != cond->_cond_op || _mods != cond->_mods) { + return false; + } + + // Compare matcher state if both have matchers + if (_matcher && cond->_matcher) { + // For now, we'll compare matcher operator type + // Full matcher comparison requires template specializations + return _matcher->op() == cond->_matcher->op(); + } + + // Both should have or not have matchers + return (_matcher == nullptr) == (cond->_matcher == nullptr); +} + +void +Condition::initialize(const hrw::ConditionSpec &spec) +{ + initialize_hooks(); + + if (need_txn_slot()) { + _txn_slot = acquire_txn_slot(); + } + if (need_txn_private_slot()) { + _txn_private_slot = acquire_txn_private_slot(); + } + + if (spec.mod_or) { + _mods |= CondModifiers::OR; + } else if (spec.mod_and) { + _mods |= CondModifiers::AND; + } + + if (spec.mod_not) { + _mods |= CondModifiers::NOT; + } + + if (spec.mod_nocase) { + _mods |= CondModifiers::MOD_NOCASE; + } + + if (spec.mod_ext) { + _mods |= CondModifiers::MOD_EXT; + } + + if (spec.mod_pre) { + _mods |= CondModifiers::MOD_PRE; + } + + if (spec.mod_last) { + _mods |= CondModifiers::MOD_L; + } + + // Parse matcher operation from match_arg + std::string arg = spec.match_arg; + _cond_op = parse_matcher_op(arg); +} diff --git a/plugins/header_rewrite/condition.h b/plugins/header_rewrite/condition.h index 1404a747b60..3d799917b41 100644 --- a/plugins/header_rewrite/condition.h +++ b/plugins/header_rewrite/condition.h @@ -30,6 +30,7 @@ #include "statement.h" #include "matcher.h" #include "parser.h" +#include "types.h" /////////////////////////////////////////////////////////////////////////////// // Base class for all Conditions (this is also the interface) @@ -45,6 +46,44 @@ class Condition : public Statement Condition(const Condition &) = delete; void operator=(const Condition &) = delete; + // Comparison - subclasses can override for type-specific state + bool equals(const Statement *other) const override; + + // Debug string for comparison error messages + std::string + debug_string() const override + { + std::string result = std::string(type_name()); + if (!_qualifier.empty()) { + result += " " + _qualifier; + } + if (_matcher) { + const char *op_str = "?"; + + switch (_cond_op) { + case MATCH_EQUAL: + op_str = "="; + break; + case MATCH_LESS_THEN: + op_str = "<"; + break; + case MATCH_GREATER_THEN: + op_str = ">"; + break; + case MATCH_REGULAR_EXPRESSION: + op_str = "~"; + break; + case MATCH_IP_RANGES: + op_str = "ip_range"; + break; + default: + break; + } + result += " " + std::string(op_str); + } + return result; + } + // Inline this, it's critical for speed (and only used twice) bool do_eval(const Resources &res) @@ -114,6 +153,7 @@ class Condition : public Statement // Virtual methods, has to be implemented by each conditional; void initialize(Parser &p) override; + virtual void initialize(const hrw::ConditionSpec &spec); virtual void append_value(std::string &s, const Resources &res) = 0; protected: diff --git a/plugins/header_rewrite/conditions.h b/plugins/header_rewrite/conditions.h index 28e888d4ed6..fb6e36b7214 100644 --- a/plugins/header_rewrite/conditions.h +++ b/plugins/header_rewrite/conditions.h @@ -47,6 +47,12 @@ class ConditionTrue : public Condition ConditionTrue(const ConditionTrue &) = delete; void operator=(const ConditionTrue &) = delete; + std::string_view + type_name() const override + { + return "ConditionTrue"; + } + void append_value(std::string &s, const Resources & /* res ATS_UNUSED */) override { @@ -72,6 +78,12 @@ class ConditionFalse : public Condition ConditionFalse(const ConditionFalse &) = delete; void operator=(const ConditionFalse &) = delete; + std::string_view + type_name() const override + { + return "ConditionFalse"; + } + void append_value(std::string &s, const Resources & /* res ATS_UNUSED */) override { @@ -101,6 +113,12 @@ class ConditionStatus : public Condition ConditionStatus(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionStatus"; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -123,6 +141,12 @@ class ConditionMethod : public Condition ConditionMethod(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionMethod"; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -144,6 +168,12 @@ class ConditionRandom : public Condition ConditionRandom(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionRandom"; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -165,6 +195,12 @@ class ConditionAccess : public Condition ConditionAccess(const ConditionAccess &) = delete; void operator=(const ConditionAccess &) = delete; + std::string_view + type_name() const override + { + return "ConditionAccess"; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -190,6 +226,12 @@ class ConditionCookie : public Condition ConditionCookie(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionCookie"; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -264,6 +306,24 @@ class ConditionHeader : public Condition ConditionHeader(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionHeader"; + } + + bool + equals(const Statement *other) const override + { + if (!Condition::equals(other)) { + return false; + } + + auto *cond = static_cast(other); + + return _client == cond->_client; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -290,6 +350,24 @@ class ConditionUrl : public Condition ConditionUrl(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionUrl"; + } + + bool + equals(const Statement *other) const override + { + if (!Condition::equals(other)) { + return false; + } + + auto *cond = static_cast(other); + + return _url_qual == cond->_url_qual && _type == cond->_type; + } + void initialize(Parser &p) override; void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -330,6 +408,12 @@ class ConditionDBM : public Condition ConditionDBM(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionDBM"; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -350,6 +434,12 @@ class ConditionInternalTxn : public Condition using SelfType = ConditionInternalTxn; public: + std::string_view + type_name() const override + { + return "ConditionInternalTxn"; + } + void append_value(std::string & /* s ATS_UNUSED */, const Resources & /* res ATS_UNUSED */) override { @@ -373,6 +463,12 @@ class ConditionIp : public Condition ConditionIp(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionIp"; + } + void initialize(Parser &p) override; void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -404,6 +500,12 @@ class ConditionTransactCount : public Condition ConditionTransactCount(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionTransactCount"; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -425,6 +527,12 @@ class ConditionNow : public Condition ConditionNow(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionNow"; + } + void initialize(Parser &p) override; void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -456,6 +564,12 @@ class ConditionGeo : public Condition ConditionGeo(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionGeo"; + } + void initialize(Parser &p) override; void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -502,6 +616,12 @@ class ConditionId : public Condition ConditionId(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionId"; + } + void initialize(Parser &p) override; void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -530,6 +650,12 @@ class ConditionCidr : public Condition ConditionCidr(SelfType &) = delete; SelfType &operator=(SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionCidr"; + } + void initialize(Parser &p) override; void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -565,6 +691,12 @@ class ConditionInbound : public Condition ConditionInbound(SelfType &) = delete; SelfType &operator=(SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionInbound"; + } + void initialize(Parser &p) override; void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -601,6 +733,12 @@ class ConditionStringLiteral : public Condition ConditionStringLiteral(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionStringLiteral"; + } + void append_value(std::string &s, const Resources & /* res ATS_UNUSED */) override; protected: @@ -624,6 +762,12 @@ class ConditionSessionTransactCount : public Condition ConditionSessionTransactCount(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionSessionTransactCount"; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -645,6 +789,12 @@ class ConditionTcpInfo : public Condition ConditionTcpInfo(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionTcpInfo"; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -667,6 +817,12 @@ class ConditionCache : public Condition ConditionCache(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionCache"; + } + void initialize(Parser &p) override; void append_value(std::string &s, const Resources &res) override; @@ -690,6 +846,12 @@ class ConditionNextHop : public Condition ConditionNextHop(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionNextHop"; + } + void initialize(Parser &p) override; void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -713,6 +875,12 @@ class ConditionHttpCntl : public Condition ConditionHttpCntl(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionHttpCntl"; + } + void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -736,6 +904,12 @@ class ConditionGroup : public Condition delete _cond; } + std::string_view + type_name() const override + { + return "ConditionGroup"; + } + void set_qualifier(const std::string &q) override { @@ -792,6 +966,13 @@ class ConditionGroup : public Condition return _cond != nullptr; } + // For comparison tool access + Condition * + get_conditions() const + { + return _cond; + } + private: Condition *_cond = nullptr; // First pre-condition (linked list) bool _end = false; @@ -814,6 +995,12 @@ class ConditionStateFlag : public Condition ConditionStateFlag(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionStateFlag"; + } + void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -849,6 +1036,12 @@ class ConditionStateInt8 : public Condition ConditionStateInt8(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionStateInt8"; + } + void initialize(Parser &p) override; void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -895,6 +1088,12 @@ class ConditionStateInt16 : public Condition ConditionStateInt16(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionStateInt16"; + } + void initialize(Parser &p) override; void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; @@ -933,6 +1132,12 @@ class ConditionLastCapture : public Condition ConditionLastCapture(const SelfType &) = delete; void operator=(const SelfType &) = delete; + std::string_view + type_name() const override + { + return "ConditionLastCapture"; + } + void set_qualifier(const std::string &q) override; void append_value(std::string &s, const Resources &res) override; diff --git a/plugins/header_rewrite/factory.cc b/plugins/header_rewrite/factory.cc index 6e5cc4bd064..cd9a3a9be45 100644 --- a/plugins/header_rewrite/factory.cc +++ b/plugins/header_rewrite/factory.cc @@ -24,10 +24,83 @@ #include "operators.h" #include "conditions.h" #include "conditions_geo.h" +#include "hrw4u/ObjTypes.h" /////////////////////////////////////////////////////////////////////////////// // "Factory" functions, processing the parsed lines // +Operator * +operator_factory(hrw::OperatorType op_type) +{ + switch (op_type) { + case hrw::OperatorType::RM_HEADER: + return new OperatorRMHeader(); + case hrw::OperatorType::SET_HEADER: + return new OperatorSetHeader(); + case hrw::OperatorType::ADD_HEADER: + return new OperatorAddHeader(); + case hrw::OperatorType::SET_CONFIG: + return new OperatorSetConfig(); + case hrw::OperatorType::SET_STATUS: + return new OperatorSetStatus(); + case hrw::OperatorType::SET_STATUS_REASON: + return new OperatorSetStatusReason(); + case hrw::OperatorType::SET_DESTINATION: + return new OperatorSetDestination(); + case hrw::OperatorType::RM_DESTINATION: + return new OperatorRMDestination(); + case hrw::OperatorType::SET_REDIRECT: + return new OperatorSetRedirect(); + case hrw::OperatorType::TIMEOUT_OUT: + return new OperatorSetTimeoutOut(); + case hrw::OperatorType::SKIP_REMAP: + return new OperatorSkipRemap(); + case hrw::OperatorType::NO_OP: + return new OperatorNoOp(); + case hrw::OperatorType::COUNTER: + return new OperatorCounter(); + case hrw::OperatorType::RM_COOKIE: + return new OperatorRMCookie(); + case hrw::OperatorType::SET_COOKIE: + return new OperatorSetCookie(); + case hrw::OperatorType::ADD_COOKIE: + return new OperatorAddCookie(); + case hrw::OperatorType::SET_CONN_DSCP: + return new OperatorSetConnDSCP(); + case hrw::OperatorType::SET_CONN_MARK: + return new OperatorSetConnMark(); + case hrw::OperatorType::SET_DEBUG: + return new OperatorSetDebug(); + case hrw::OperatorType::SET_BODY: + return new OperatorSetBody(); + case hrw::OperatorType::SET_HTTP_CNTL: + return new OperatorSetHttpCntl(); + case hrw::OperatorType::SET_PLUGIN_CNTL: + return new OperatorSetPluginCntl(); + case hrw::OperatorType::RUN_PLUGIN: + return new OperatorRunPlugin(); + case hrw::OperatorType::SET_BODY_FROM: + return new OperatorSetBodyFrom(); + case hrw::OperatorType::SET_STATE_FLAG: + return new OperatorSetStateFlag(); + case hrw::OperatorType::SET_STATE_INT8: + return new OperatorSetStateInt8(); + case hrw::OperatorType::SET_STATE_INT16: + return new OperatorSetStateInt16(); + case hrw::OperatorType::SET_EFFECTIVE_ADDRESS: + return new OperatorSetEffectiveAddress(); + case hrw::OperatorType::SET_NEXT_HOP_STRATEGY: + return new OperatorSetNextHopStrategy(); + case hrw::OperatorType::SET_CC_ALG: + return new OperatorSetCCAlgorithm(); + case hrw::OperatorType::IF: + case hrw::OperatorType::NONE: + default: + TSError("[%s] Invalid operator type: %d", PLUGIN_NAME, static_cast(op_type)); + return nullptr; + } +} + Operator * operator_factory(const std::string &op) { diff --git a/plugins/header_rewrite/factory.h b/plugins/header_rewrite/factory.h index 5145d187f44..b80649617ff 100644 --- a/plugins/header_rewrite/factory.h +++ b/plugins/header_rewrite/factory.h @@ -25,6 +25,8 @@ #include "operator.h" #include "condition.h" +#include "hrw4u/ObjTypes.h" +Operator *operator_factory(hrw::OperatorType op_type); Operator *operator_factory(const std::string &op); Condition *condition_factory(const std::string &cond); diff --git a/plugins/header_rewrite/header_rewrite.cc b/plugins/header_rewrite/header_rewrite.cc index 61895124f69..48c70cebbe3 100644 --- a/plugins/header_rewrite/header_rewrite.cc +++ b/plugins/header_rewrite/header_rewrite.cc @@ -38,6 +38,10 @@ #include "conditions_geo.h" #include "operators.h" +#ifdef ENABLE_HRW4U_NATIVE +#include "hrw4u.h" +#endif + // Debugs namespace header_rewrite_ns { @@ -114,7 +118,8 @@ class RulesConfig return _inboundIpSource; } - bool parse_config(const std::string &fname, TSHttpHookID default_hook, char *from_url = nullptr, char *to_url = nullptr); + bool parse_config(const std::string &fname, TSHttpHookID default_hook, char *from_url = nullptr, char *to_url = nullptr, + bool force_hrw4u = false); private: void add_rule(std::unique_ptr rule); @@ -185,7 +190,7 @@ validate_rule_completion(RuleSet *rule, const std::string &fname, int lineno) // anyways (or reload for remap.config), so not really in the critical path. // bool -RulesConfig::parse_config(const std::string &fname, TSHttpHookID default_hook, char *from_url, char *to_url) +RulesConfig::parse_config(const std::string &fname, TSHttpHookID default_hook, char *from_url, char *to_url, bool force_hrw4u) { std::unique_ptr rule(nullptr); std::string filename; @@ -208,18 +213,72 @@ RulesConfig::parse_config(const std::string &fname, TSHttpHookID default_hook, c filename = fname; } - auto reader = openConfig(filename); - if (!reader || !reader->stream) { + if (force_hrw4u || filename.ends_with(".hrw4u")) { +#ifdef ENABLE_HRW4U_NATIVE + hrw4u_integration::HRW4UConfig config; + + Dbg(pi_dbg_ctl, "Detected hrw4u file: %s", filename.c_str()); + + config.default_hook = default_hook; + config.from_url = from_url; + config.to_url = to_url; + config.filename = filename; + + auto result = hrw4u_integration::parse_hrw4u_file(filename, config); + + if (!result) { + TSError("[%s] hrw4u parse failed: %s", PLUGIN_NAME, result.error_message.c_str()); + return false; + } + + Dbg(pi_dbg_ctl, "hrw4u parse returned %zu rulesets", result.rulesets.size()); + + // Add parsed rulesets with their associated hooks + for (size_t i = 0; i < result.rulesets.size(); ++i) { + if (result.rulesets[i]) { + TSHttpHookID hook = (i < result.hooks.size()) ? result.hooks[i] : default_hook; + + result.rulesets[i]->set_hook(hook); + + const char *hook_name = (hook == TS_REMAP_PSEUDO_HOOK) ? "REMAP_PSEUDO_HOOK" : TSHttpHookNameLookup(hook); + + Dbg(pi_dbg_ctl, "New RuleSet in %%{%s} at %s", hook_name, filename.c_str()); + add_rule(std::move(result.rulesets[i])); + } else { + Dbg(pi_dbg_ctl, "hrw4u: Skipping null ruleset at index %zu", i); + } + } + + // Collect all resource IDs that we need + for (size_t i = TS_HTTP_READ_REQUEST_HDR_HOOK; i <= TS_HTTP_LAST_HOOK; ++i) { + if (_rules[i]) { + _resids[i] = _rules[i]->get_all_resource_ids(); + Dbg(pi_dbg_ctl, "hrw4u: Hook %s has rules with resids=%d", TSHttpHookNameLookup(static_cast(i)), + static_cast(_resids[i])); + } + } + + Dbg(pi_dbg_ctl, "Successfully parsed hrw4u file: %s", filename.c_str()); + return true; +#else + TSError("[%s] .hrw4u files require ANTLR4 support (ENABLE_HRW4U_NATIVE): %s", PLUGIN_NAME, filename.c_str()); + return false; +#endif + } + + std::ifstream config_file(filename); + + if (!config_file.is_open()) { TSError("[%s] unable to open %s", PLUGIN_NAME, filename.c_str()); return false; } Dbg(dbg_ctl, "Parsing started on file: %s", filename.c_str()); - while (!reader->stream->eof()) { + while (!config_file.eof()) { std::string line; - getline(*reader->stream, line); + getline(config_file, line); ++lineno; Dbg(dbg_ctl, "Reading line: %d: %s", lineno, line.c_str()); @@ -401,15 +460,6 @@ RulesConfig::parse_config(const std::string &fname, TSHttpHookID default_hook, c } } - if (reader->pipebuf) { - reader->pipebuf->close(); - if (reader->pipebuf->exit_status() != 0) { - TSError("[%s] hrw4u preprocessor exited with non-zero status (%d): %s", PLUGIN_NAME, reader->pipebuf->exit_status(), - fname.c_str()); - return false; - } - } - if (!group_stack.empty()) { TSError("[%s] missing final %%{GROUP:END} condition in file: %s, lineno: %d", PLUGIN_NAME, fname.c_str(), lineno); return false; @@ -464,8 +514,8 @@ setPluginControlValues(TSHttpTxn txnp, RulesConfig *conf) static int cont_rewrite_headers(TSCont contp, TSEvent event, void *edata) { - auto txnp = static_cast(edata); TSHttpHookID hook = TS_HTTP_LAST_HOOK; + auto txnp = static_cast(edata); auto *conf = static_cast(TSContDataGet(contp)); switch (event) { diff --git a/plugins/header_rewrite/hrw4u.cc b/plugins/header_rewrite/hrw4u.cc new file mode 100644 index 00000000000..a1bffa881e9 --- /dev/null +++ b/plugins/header_rewrite/hrw4u.cc @@ -0,0 +1,470 @@ +/* + 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. +*/ + +#include "hrw4u.h" + +#include +#include + +#include "ts/ts.h" + +#include "factory.h" +#include "ruleset.h" +#include "conditions.h" +#include "operators.h" + +#ifdef ENABLE_HRW4U_NATIVE +#include "hrw4u/HRW4UVisitor.h" +#include "hrw4u/Visitor.h" +#include "hrw4u/Types.h" +#include "hrw4u/ObjTypes.h" +#endif + +namespace hrw4u_integration +{ +bool +is_hrw4u_file(std::string_view filename) +{ + constexpr std::string_view suffix = ".hrw4u"; + + if (filename.size() < suffix.size()) { + return false; + } + + return filename.substr(filename.size() - suffix.size()) == suffix; +} + +TSHttpHookID +section_to_hook(int section_type) +{ +#ifdef ENABLE_HRW4U_NATIVE + using hrw4u::SectionType; + + switch (static_cast(section_type)) { + case SectionType::READ_REQUEST: + return TS_HTTP_READ_REQUEST_HDR_HOOK; + case SectionType::SEND_REQUEST: + return TS_HTTP_SEND_REQUEST_HDR_HOOK; + case SectionType::READ_RESPONSE: + return TS_HTTP_READ_RESPONSE_HDR_HOOK; + case SectionType::SEND_RESPONSE: + return TS_HTTP_SEND_RESPONSE_HDR_HOOK; + case SectionType::PRE_REMAP: + return TS_HTTP_PRE_REMAP_HOOK; + case SectionType::REMAP: + return TS_REMAP_PSEUDO_HOOK; + case SectionType::POST_REMAP: + return TS_HTTP_POST_REMAP_HOOK; + case SectionType::TXN_START: + return TS_HTTP_TXN_START_HOOK; + case SectionType::TXN_CLOSE: + return TS_HTTP_TXN_CLOSE_HOOK; + default: + return TS_HTTP_READ_RESPONSE_HDR_HOOK; + } +#else + return static_cast(section_type); +#endif +} + +int +hook_to_section(TSHttpHookID hook) +{ +#ifdef ENABLE_HRW4U_NATIVE + using hrw4u::SectionType; + + switch (hook) { + case TS_HTTP_READ_REQUEST_HDR_HOOK: + return static_cast(SectionType::READ_REQUEST); + case TS_HTTP_SEND_REQUEST_HDR_HOOK: + return static_cast(SectionType::SEND_REQUEST); + case TS_HTTP_READ_RESPONSE_HDR_HOOK: + return static_cast(SectionType::READ_RESPONSE); + case TS_HTTP_SEND_RESPONSE_HDR_HOOK: + return static_cast(SectionType::SEND_RESPONSE); + case TS_HTTP_PRE_REMAP_HOOK: + return static_cast(SectionType::PRE_REMAP); + case TS_REMAP_PSEUDO_HOOK: + return static_cast(SectionType::REMAP); + case TS_HTTP_POST_REMAP_HOOK: + return static_cast(SectionType::POST_REMAP); + case TS_HTTP_TXN_START_HOOK: + return static_cast(SectionType::TXN_START); + case TS_HTTP_TXN_CLOSE_HOOK: + return static_cast(SectionType::TXN_CLOSE); + default: + return static_cast(SectionType::READ_RESPONSE); + } +#else + return static_cast(hook); +#endif +} + +#ifdef ENABLE_HRW4U_NATIVE + +namespace +{ + class FactoryBridge + { + public: + static void * + create_condition(const hrw4u::ParserContext &ctx) + { + std::string cond_spec = ctx.op; + + if (cond_spec.size() > 3 && cond_spec.substr(0, 2) == "%{" && cond_spec.back() == '}') { + cond_spec = cond_spec.substr(2, cond_spec.size() - 3); + } + + Condition *cond = condition_factory(cond_spec); + + if (!cond) { + TSError("[header_rewrite:hrw4u] Failed to create condition: %s", cond_spec.c_str()); + return nullptr; + } + + Dbg(pi_dbg_ctl, " Creating condition: %%{%s} with arg: %s", cond_spec.c_str(), ctx.arg.c_str()); + + Parser p; + + p.set_op(cond_spec); + p.set_arg(ctx.arg); + for (const auto &mod : ctx.mods) { + p.add_mod(mod); + } + + try { + cond->initialize(p); + } catch (const std::exception &e) { + TSError("[header_rewrite:hrw4u] Failed to initialize condition %s: %s", cond_spec.c_str(), e.what()); + delete cond; + return nullptr; + } + + return cond; + } + + static void * + create_operator(const hrw4u::ParserContext &ctx) + { + Operator *op = operator_factory(ctx.op_type); + + if (!op) { + TSError("[header_rewrite:hrw4u] Failed to create operator type %d (factory returned nullptr)", + static_cast(ctx.op_type)); + return nullptr; + } + + Dbg(pi_dbg_ctl, " Adding operator: %s, arg=\"%s\", val=\"%s\"", std::string(hrw::operator_type_name(ctx.op_type)).c_str(), + ctx.arg.c_str(), ctx.val.c_str()); + + Parser p(ctx.from_url, ctx.to_url); + + p.set_op(""); + p.set_arg(ctx.arg); + p.set_val(ctx.val); + for (const auto &mod : ctx.mods) { + p.add_mod(mod); + } + + try { + op->initialize(p); + } catch (const std::exception &e) { + TSError("[header_rewrite:hrw4u] Failed to initialize operator type %d: %s", static_cast(ctx.op_type), e.what()); + delete op; + + return nullptr; + } + + return op; + } + + static void * + create_ruleset() + { + return new RuleSet(); + } + + static bool + add_condition(void *rule, void *condition) + { + if (!rule || !condition) { + return false; + } + + auto *ruleset = static_cast(rule); + auto *cond = static_cast(condition); + auto *group = ruleset->get_group(); + + if (group) { + group->add_condition(cond); + + ruleset->require_resources(cond->get_resource_ids()); + return true; + } + + return false; + } + + static bool + add_operator(void *rule, void *op) + { + if (!rule || !op) { + return false; + } + + auto *ruleset = static_cast(rule); + auto *operator_ = static_cast(op); + + return ruleset->add_operator(operator_); + } + + static bool + add_condition_to_if(void *op_if_ptr, void *condition) + { + if (!op_if_ptr || !condition) { + return false; + } + + auto *op_if = static_cast(op_if_ptr); + auto *cond = static_cast(condition); + auto *group = op_if->get_group(); + + if (group) { + group->add_condition(cond); + + op_if->require_resources(cond->get_resource_ids()); + return true; + } + + return false; + } + + static bool + add_condition_to_group(void *group_ptr, void *condition) + { + if (!group_ptr || !condition) { + return false; + } + + auto *group = static_cast(group_ptr); + auto *cond = static_cast(condition); + + group->add_condition(cond); + + return true; + } + + static bool + add_operator_to_if(void *op_if_ptr, void *op) + { + if (!op_if_ptr || !op) { + return false; + } + + auto *op_if = static_cast(op_if_ptr); + auto *operator_ = static_cast(op); + auto *cur_sec = op_if->cur_section(); + + if (!cur_sec) { + return false; + } + + if (!cur_sec->ops.oper) { + cur_sec->ops.oper.reset(operator_); + } else { + cur_sec->ops.oper->append(operator_); + } + + cur_sec->ops.oper_mods = static_cast(cur_sec->ops.oper_mods | cur_sec->ops.oper->get_oper_modifiers()); + op_if->require_resources(operator_->get_resource_ids()); + + return true; + } + + static void * + create_if_operator() + { + return new OperatorIf(); + } + + static Parser::CondClause + to_parser_clause(hrw4u::CondClause clause) + { + switch (clause) { + case hrw4u::CondClause::ELIF: + return Parser::CondClause::ELIF; + case hrw4u::CondClause::ELSE: + return Parser::CondClause::ELSE; + default: + return Parser::CondClause::IF; + } + } + + static void * + new_section(void *op_if_ptr, hrw4u::CondClause clause) + { + if (!op_if_ptr) { + return nullptr; + } + return static_cast(op_if_ptr)->new_section(to_parser_clause(clause)); + } + + static void * + new_ruleset_section(void *ruleset_ptr, hrw4u::CondClause clause) + { + if (!ruleset_ptr) { + return nullptr; + } + return static_cast(ruleset_ptr)->new_section(to_parser_clause(clause)); + } + + static void + destroy(void *ptr, std::string_view type) + { + if (!ptr) { + return; + } + + if (type == "condition") { + delete static_cast(ptr); + } else if (type == "operator" || type == "operator_if") { + delete static_cast(ptr); + } else if (type == "ruleset") { + delete static_cast(ptr); + } + } + }; + + hrw4u::FactoryCallbacks + make_callbacks() + { + hrw4u::FactoryCallbacks callbacks; + callbacks.create_condition = FactoryBridge::create_condition; + callbacks.create_operator = FactoryBridge::create_operator; + callbacks.create_ruleset = FactoryBridge::create_ruleset; + callbacks.add_condition = FactoryBridge::add_condition; + callbacks.add_operator = FactoryBridge::add_operator; + callbacks.add_condition_to_if = FactoryBridge::add_condition_to_if; + callbacks.add_operator_to_if = FactoryBridge::add_operator_to_if; + callbacks.add_condition_to_group = FactoryBridge::add_condition_to_group; + callbacks.create_if_operator = FactoryBridge::create_if_operator; + callbacks.new_section = FactoryBridge::new_section; + callbacks.new_ruleset_section = FactoryBridge::new_ruleset_section; + callbacks.destroy = FactoryBridge::destroy; + + return callbacks; + } + +} // namespace + +HRW4UResult +parse_hrw4u_content(std::string_view content, const HRW4UConfig &config) +{ + HRW4UResult result; + hrw4u::ParserConfig parser_config; + + parser_config.default_hook = static_cast(hook_to_section(config.default_hook)); + parser_config.filename = config.filename; + parser_config.from_url = config.from_url; + parser_config.to_url = config.to_url; + + hrw4u::HRW4UVisitor visitor(make_callbacks(), parser_config); + auto parse_result = visitor.parse(content); + + if (!parse_result.success) { + std::ostringstream oss; + + result.success = false; + for (const auto &err : parse_result.errors.errors()) { + oss << err.format() << "\n"; + } + result.error_message = oss.str(); + + return result; + } + + result.success = true; + for (size_t i = 0; i < parse_result.rulesets.size(); ++i) { + TSHttpHookID hook; + auto *ruleset = static_cast(parse_result.rulesets[i]); + + result.rulesets.emplace_back(ruleset); + if (i < parse_result.sections.size()) { + hook = section_to_hook(static_cast(parse_result.sections[i])); + } else { + hook = config.default_hook; + } + result.hooks.push_back(hook); + } + + Dbg(pi_dbg_ctl, "hrw4u: Parsed %zu rulesets from %s", parse_result.rulesets.size(), + config.filename.empty() ? "" : config.filename.c_str()); + + return result; +} + +HRW4UResult +parse_hrw4u_file(const std::string &filename, const HRW4UConfig &config) +{ + HRW4UResult result; + std::ifstream infile(filename); + + if (!infile.is_open()) { + result.success = false; + result.error_message = "Cannot open file: " + filename; + + return result; + } + + HRW4UConfig updated_config = config; + std::stringstream buffer; + + buffer << infile.rdbuf(); + updated_config.filename = filename; + + return parse_hrw4u_content(buffer.str(), updated_config); +} + +#else // !ENABLE_HRW4U_NATIVE + +HRW4UResult +parse_hrw4u_content(std::string_view, const HRW4UConfig &) +{ + HRW4UResult result; + + result.success = false; + result.error_message = "hrw4u parsing not enabled. Build with ANTLR4 support."; + + return result; +} + +HRW4UResult +parse_hrw4u_file(const std::string &, const HRW4UConfig &) +{ + HRW4UResult result; + + result.success = false; + result.error_message = "hrw4u parsing not enabled. Build with ANTLR4 support."; + + return result; +} + +#endif // ENABLE_HRW4U_NATIVE + +} // namespace hrw4u_integration diff --git a/plugins/header_rewrite/hrw4u.h b/plugins/header_rewrite/hrw4u.h new file mode 100644 index 00000000000..87c9bf6e2c3 --- /dev/null +++ b/plugins/header_rewrite/hrw4u.h @@ -0,0 +1,65 @@ +/* + 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. +*/ + +// Integration layer for native hrw4u parsing in header_rewrite plugin. + +#pragma once + +#include +#include +#include +#include + +#include "ts/ts.h" +#include "parser.h" + +class RuleSet; +class Condition; +class Operator; + +namespace hrw4u_integration +{ + +bool is_hrw4u_file(std::string_view filename); + +struct HRW4UConfig { + TSHttpHookID default_hook = TS_HTTP_READ_RESPONSE_HDR_HOOK; + char *from_url = nullptr; + char *to_url = nullptr; + std::string filename; +}; + +struct HRW4UResult { + bool success = false; + std::vector> rulesets; + std::vector hooks; + std::string error_message; + + explicit + operator bool() const + { + return success; + } +}; + +HRW4UResult parse_hrw4u_file(const std::string &filename, const HRW4UConfig &config); +HRW4UResult parse_hrw4u_content(std::string_view content, const HRW4UConfig &config); + +TSHttpHookID section_to_hook(int section_type); +int hook_to_section(TSHttpHookID hook); +} // namespace hrw4u_integration diff --git a/plugins/header_rewrite/matcher.h b/plugins/header_rewrite/matcher.h index 1f4b52e143f..02342abc7d2 100644 --- a/plugins/header_rewrite/matcher.h +++ b/plugins/header_rewrite/matcher.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include "swoc/swoc_ip.h" @@ -95,6 +96,57 @@ has_modifier(const CondModifiers flags, const CondModifiers bit) return static_cast(flags) & static_cast(bit); } +// Convert CondModifiers bitmask to human-readable string (e.g., "OR,NOT,NOCASE") +inline std::string +cond_modifiers_to_string(CondModifiers mods) +{ + std::vector names; + unsigned bits = static_cast(mods); + + if (bits & static_cast(CondModifiers::OR)) { + names.push_back("OR"); + } + if (bits & static_cast(CondModifiers::AND)) { + names.push_back("AND"); + } + if (bits & static_cast(CondModifiers::NOT)) { + names.push_back("NOT"); + } + if (bits & static_cast(CondModifiers::MOD_NOCASE)) { + names.push_back("NOCASE"); + } + if (bits & static_cast(CondModifiers::MOD_L)) { + names.push_back("LAST"); + } + if (bits & static_cast(CondModifiers::MOD_EXT)) { + names.push_back("EXT"); + } + if (bits & static_cast(CondModifiers::MOD_PRE)) { + names.push_back("PRE"); + } + if (bits & static_cast(CondModifiers::MOD_SUF)) { + names.push_back("SUF"); + } + if (bits & static_cast(CondModifiers::MOD_MID)) { + names.push_back("MID"); + } + + if (names.empty()) { + return "none"; + } + + std::string result; + + for (size_t i = 0; i < names.size(); ++i) { + if (i > 0) { + result += ","; + } + result += names[i]; + } + + return result; +} + /////////////////////////////////////////////////////////////////////////////// // Base class for all Matchers (this is also the interface) // diff --git a/plugins/header_rewrite/objtypes.cc b/plugins/header_rewrite/objtypes.cc new file mode 100644 index 00000000000..4c428469d3a --- /dev/null +++ b/plugins/header_rewrite/objtypes.cc @@ -0,0 +1,479 @@ +/* + 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. +*/ + +#include "objtypes.h" +#include "conditions.h" +#include "conditions_geo.h" +#include "operators.h" + +namespace hrw +{ + +Condition * +create_condition(const ConditionSpec &spec) +{ + Condition *cond = nullptr; + + switch (spec.type) { + case ConditionType::NONE: + return nullptr; + + case ConditionType::COND_TRUE: + cond = new ConditionTrue(); + break; + + case ConditionType::COND_FALSE: + cond = new ConditionFalse(); + break; + + case ConditionType::COND_STATUS: + cond = new ConditionStatus(); + break; + + case ConditionType::COND_METHOD: + cond = new ConditionMethod(); + break; + + case ConditionType::COND_RANDOM: + cond = new ConditionRandom(); + break; + + case ConditionType::COND_ACCESS: + cond = new ConditionAccess(); + break; + + case ConditionType::COND_COOKIE: + cond = new ConditionCookie(); + break; + + case ConditionType::COND_HEADER: + cond = new ConditionHeader(); + break; + + case ConditionType::COND_CLIENT_HEADER: + cond = new ConditionHeader(true); + break; + + case ConditionType::COND_CLIENT_URL: + cond = new ConditionUrl(ConditionUrl::CLIENT); + break; + + case ConditionType::COND_URL: + cond = new ConditionUrl(ConditionUrl::URL); + break; + + case ConditionType::COND_FROM_URL: + cond = new ConditionUrl(ConditionUrl::FROM); + break; + + case ConditionType::COND_TO_URL: + cond = new ConditionUrl(ConditionUrl::TO); + break; + + case ConditionType::COND_DBM: + cond = new ConditionDBM(); + break; + + case ConditionType::COND_INTERNAL_TXN: + cond = new ConditionInternalTxn(); + break; + + case ConditionType::COND_IP: + cond = new ConditionIp(); + break; + + case ConditionType::COND_TRANSACT_COUNT: + cond = new ConditionTransactCount(); + break; + + case ConditionType::COND_NOW: + cond = new ConditionNow(); + break; + + case ConditionType::COND_GEO: +#if TS_USE_HRW_GEOIP + cond = new GeoIPConditionGeo(); +#elif TS_USE_HRW_MAXMINDDB + cond = new MMConditionGeo(); +#else + cond = new ConditionGeo(); +#endif + break; + + case ConditionType::COND_ID: + cond = new ConditionId(); + break; + + case ConditionType::COND_CIDR: + cond = new ConditionCidr(); + break; + + case ConditionType::COND_INBOUND: + cond = new ConditionInbound(); + break; + + case ConditionType::COND_SESSION_TRANSACT_COUNT: + cond = new ConditionSessionTransactCount(); + break; + + case ConditionType::COND_TCP_INFO: + cond = new ConditionTcpInfo(); + break; + + case ConditionType::COND_CACHE: + cond = new ConditionCache(); + break; + + case ConditionType::COND_NEXT_HOP: + cond = new ConditionNextHop(); + break; + + case ConditionType::COND_HTTP_CNTL: + cond = new ConditionHttpCntl(); + break; + + case ConditionType::COND_GROUP: + cond = new ConditionGroup(); + break; + + case ConditionType::COND_STATE_FLAG: + cond = new ConditionStateFlag(); + break; + + case ConditionType::COND_STATE_INT8: + cond = new ConditionStateInt8(); + break; + + case ConditionType::COND_STATE_INT16: + cond = new ConditionStateInt16(); + break; + + case ConditionType::COND_LAST_CAPTURE: + cond = new ConditionLastCapture(); + break; + } + + if (cond) { + cond->initialize(spec); + } + + return cond; +} + +Operator * +create_operator(const OperatorSpec &spec) +{ + Operator *op = nullptr; + + switch (spec.type) { + case OperatorType::NONE: + return nullptr; + + case OperatorType::RM_HEADER: + op = new OperatorRMHeader(); + break; + + case OperatorType::SET_HEADER: + op = new OperatorSetHeader(); + break; + + case OperatorType::ADD_HEADER: + op = new OperatorAddHeader(); + break; + + case OperatorType::SET_CONFIG: + op = new OperatorSetConfig(); + break; + + case OperatorType::SET_STATUS: + op = new OperatorSetStatus(); + break; + + case OperatorType::SET_STATUS_REASON: + op = new OperatorSetStatusReason(); + break; + + case OperatorType::SET_DESTINATION: + op = new OperatorSetDestination(); + break; + + case OperatorType::RM_DESTINATION: + op = new OperatorRMDestination(); + break; + + case OperatorType::SET_REDIRECT: + op = new OperatorSetRedirect(); + break; + + case OperatorType::TIMEOUT_OUT: + op = new OperatorSetTimeoutOut(); + break; + + case OperatorType::SKIP_REMAP: + op = new OperatorSkipRemap(); + break; + + case OperatorType::NO_OP: + op = new OperatorNoOp(); + break; + + case OperatorType::COUNTER: + op = new OperatorCounter(); + break; + + case OperatorType::RM_COOKIE: + op = new OperatorRMCookie(); + break; + + case OperatorType::SET_COOKIE: + op = new OperatorSetCookie(); + break; + + case OperatorType::ADD_COOKIE: + op = new OperatorAddCookie(); + break; + + case OperatorType::SET_CONN_DSCP: + op = new OperatorSetConnDSCP(); + break; + + case OperatorType::SET_CONN_MARK: + op = new OperatorSetConnMark(); + break; + + case OperatorType::SET_DEBUG: + op = new OperatorSetDebug(); + break; + + case OperatorType::SET_BODY: + op = new OperatorSetBody(); + break; + + case OperatorType::SET_HTTP_CNTL: + op = new OperatorSetHttpCntl(); + break; + + case OperatorType::SET_PLUGIN_CNTL: + op = new OperatorSetPluginCntl(); + break; + + case OperatorType::RUN_PLUGIN: + op = new OperatorRunPlugin(); + break; + + case OperatorType::SET_BODY_FROM: + op = new OperatorSetBodyFrom(); + break; + + case OperatorType::SET_STATE_FLAG: + op = new OperatorSetStateFlag(); + break; + + case OperatorType::SET_STATE_INT8: + op = new OperatorSetStateInt8(); + break; + + case OperatorType::SET_STATE_INT16: + op = new OperatorSetStateInt16(); + break; + + case OperatorType::SET_EFFECTIVE_ADDRESS: + op = new OperatorSetEffectiveAddress(); + break; + + case OperatorType::SET_NEXT_HOP_STRATEGY: + op = new OperatorSetNextHopStrategy(); + break; + + case OperatorType::SET_CC_ALG: + op = new OperatorSetCCAlgorithm(); + break; + + case OperatorType::IF: + op = new OperatorIf(); + break; + } + + if (op) { + op->initialize(spec); + } + + return op; +} + +ConditionSpec +parse_condition_string(const std::string &cond_str, const std::string &arg) +{ + ConditionSpec spec; + std::string c_name; + std::string c_qual; + std::string::size_type pos = cond_str.find(':'); + + if (pos != std::string::npos) { + c_name = cond_str.substr(0, pos); + c_qual = cond_str.substr(pos + 1); + } else { + c_name = cond_str; + } + + if (c_name == "TRUE") { + spec.type = ConditionType::COND_TRUE; + } else if (c_name == "FALSE") { + spec.type = ConditionType::COND_FALSE; + } else if (c_name == "STATUS") { + spec.type = ConditionType::COND_STATUS; + } else if (c_name == "METHOD") { + spec.type = ConditionType::COND_METHOD; + } else if (c_name == "RANDOM") { + spec.type = ConditionType::COND_RANDOM; + } else if (c_name == "ACCESS") { + spec.type = ConditionType::COND_ACCESS; + } else if (c_name == "COOKIE") { + spec.type = ConditionType::COND_COOKIE; + } else if (c_name == "HEADER") { + spec.type = ConditionType::COND_HEADER; + } else if (c_name == "CLIENT-HEADER") { + spec.type = ConditionType::COND_CLIENT_HEADER; + } else if (c_name == "CLIENT-URL") { + spec.type = ConditionType::COND_CLIENT_URL; + } else if (c_name == "URL") { + spec.type = ConditionType::COND_URL; + } else if (c_name == "FROM-URL") { + spec.type = ConditionType::COND_FROM_URL; + } else if (c_name == "TO-URL") { + spec.type = ConditionType::COND_TO_URL; + } else if (c_name == "DBM") { + spec.type = ConditionType::COND_DBM; + } else if (c_name == "INTERNAL-TRANSACTION" || c_name == "INTERNAL-TXN") { + spec.type = ConditionType::COND_INTERNAL_TXN; + } else if (c_name == "IP") { + spec.type = ConditionType::COND_IP; + } else if (c_name == "TXN-COUNT") { + spec.type = ConditionType::COND_TRANSACT_COUNT; + } else if (c_name == "NOW") { + spec.type = ConditionType::COND_NOW; + } else if (c_name == "GEO") { + spec.type = ConditionType::COND_GEO; + } else if (c_name == "ID") { + spec.type = ConditionType::COND_ID; + } else if (c_name == "CIDR") { + spec.type = ConditionType::COND_CIDR; + } else if (c_name == "INBOUND") { + spec.type = ConditionType::COND_INBOUND; + } else if (c_name == "SSN-TXN-COUNT") { + spec.type = ConditionType::COND_SESSION_TRANSACT_COUNT; + } else if (c_name == "TCP-INFO") { + spec.type = ConditionType::COND_TCP_INFO; + } else if (c_name == "CACHE") { + spec.type = ConditionType::COND_CACHE; + } else if (c_name == "NEXT-HOP") { + spec.type = ConditionType::COND_NEXT_HOP; + } else if (c_name == "HTTP-CNTL") { + spec.type = ConditionType::COND_HTTP_CNTL; + } else if (c_name == "GROUP") { + spec.type = ConditionType::COND_GROUP; + } else if (c_name == "STATE-FLAG") { + spec.type = ConditionType::COND_STATE_FLAG; + } else if (c_name == "STATE-INT8") { + spec.type = ConditionType::COND_STATE_INT8; + } else if (c_name == "STATE-INT16") { + spec.type = ConditionType::COND_STATE_INT16; + } else if (c_name == "LAST-CAPTURE") { + spec.type = ConditionType::COND_LAST_CAPTURE; + } + + spec.qualifier = c_qual; + spec.match_arg = arg; + + return spec; +} + +OperatorSpec +parse_operator_string(const std::string &op_str, const std::string &arg, const std::string &val) +{ + OperatorSpec spec; + + if (op_str == "rm-header") { + spec.type = OperatorType::RM_HEADER; + } else if (op_str == "set-header") { + spec.type = OperatorType::SET_HEADER; + } else if (op_str == "add-header") { + spec.type = OperatorType::ADD_HEADER; + } else if (op_str == "set-config") { + spec.type = OperatorType::SET_CONFIG; + } else if (op_str == "set-status") { + spec.type = OperatorType::SET_STATUS; + } else if (op_str == "set-status-reason") { + spec.type = OperatorType::SET_STATUS_REASON; + } else if (op_str == "set-destination") { + spec.type = OperatorType::SET_DESTINATION; + } else if (op_str == "rm-destination") { + spec.type = OperatorType::RM_DESTINATION; + } else if (op_str == "set-redirect") { + spec.type = OperatorType::SET_REDIRECT; + } else if (op_str == "timeout-out") { + spec.type = OperatorType::TIMEOUT_OUT; + } else if (op_str == "skip-remap") { + spec.type = OperatorType::SKIP_REMAP; + } else if (op_str == "no-op") { + spec.type = OperatorType::NO_OP; + } else if (op_str == "counter") { + spec.type = OperatorType::COUNTER; + } else if (op_str == "rm-cookie") { + spec.type = OperatorType::RM_COOKIE; + } else if (op_str == "set-cookie") { + spec.type = OperatorType::SET_COOKIE; + } else if (op_str == "add-cookie") { + spec.type = OperatorType::ADD_COOKIE; + } else if (op_str == "set-conn-dscp") { + spec.type = OperatorType::SET_CONN_DSCP; + } else if (op_str == "set-conn-mark") { + spec.type = OperatorType::SET_CONN_MARK; + } else if (op_str == "set-debug") { + spec.type = OperatorType::SET_DEBUG; + } else if (op_str == "set-body") { + spec.type = OperatorType::SET_BODY; + } else if (op_str == "set-http-cntl") { + spec.type = OperatorType::SET_HTTP_CNTL; + } else if (op_str == "set-plugin-cntl") { + spec.type = OperatorType::SET_PLUGIN_CNTL; + } else if (op_str == "run-plugin") { + spec.type = OperatorType::RUN_PLUGIN; + } else if (op_str == "set-body-from") { + spec.type = OperatorType::SET_BODY_FROM; + } else if (op_str == "set-state-flag") { + spec.type = OperatorType::SET_STATE_FLAG; + } else if (op_str == "set-state-int8") { + spec.type = OperatorType::SET_STATE_INT8; + } else if (op_str == "set-state-int16") { + spec.type = OperatorType::SET_STATE_INT16; + } else if (op_str == "set-effective-address") { + spec.type = OperatorType::SET_EFFECTIVE_ADDRESS; + } else if (op_str == "set-next-hop-strategy") { + spec.type = OperatorType::SET_NEXT_HOP_STRATEGY; + } else if (op_str == "set-cc-alg") { + spec.type = OperatorType::SET_CC_ALG; + } + + spec.arg = arg; + spec.value = val; + + return spec; +} + +} // namespace hrw diff --git a/plugins/header_rewrite/objtypes.h b/plugins/header_rewrite/objtypes.h new file mode 100644 index 00000000000..fd71dc09ae7 --- /dev/null +++ b/plugins/header_rewrite/objtypes.h @@ -0,0 +1,34 @@ +/* + 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. +*/ +#pragma once + +#include "types.h" + +class Condition; +class Operator; + +namespace hrw +{ + +Condition *create_condition(const ConditionSpec &spec); +Operator *create_operator(const OperatorSpec &spec); + +ConditionSpec parse_condition_string(const std::string &cond_str, const std::string &arg); +OperatorSpec parse_operator_string(const std::string &op_str, const std::string &arg, const std::string &val); + +} // namespace hrw diff --git a/plugins/header_rewrite/operator.cc b/plugins/header_rewrite/operator.cc index 6643bb8059e..afc8eb9396a 100644 --- a/plugins/header_rewrite/operator.cc +++ b/plugins/header_rewrite/operator.cc @@ -53,6 +53,31 @@ Operator::initialize(Parser &p) p.validate_mods(); } +void +Operator::initialize(const hrw::OperatorSpec &spec) +{ + initialize_hooks(); + + if (need_txn_slot()) { + _txn_slot = acquire_txn_slot(); + } + if (need_txn_private_slot()) { + _txn_private_slot = acquire_txn_private_slot(); + } + + if (spec.mod_last) { + _mods = static_cast(_mods | OPER_LAST); + } + + if (spec.mod_qsa) { + _mods = static_cast(_mods | OPER_QSA); + } + + if (spec.mod_inv) { + _mods = static_cast(_mods | OPER_INV); + } +} + void OperatorHeaders::initialize(Parser &p) { diff --git a/plugins/header_rewrite/operator.h b/plugins/header_rewrite/operator.h index bc23c93aabb..9621fd8bf57 100644 --- a/plugins/header_rewrite/operator.h +++ b/plugins/header_rewrite/operator.h @@ -29,6 +29,7 @@ #include "resources.h" #include "statement.h" #include "parser.h" +#include "types.h" // Operator modifiers enum OperModifiers { @@ -68,8 +69,28 @@ class Operator : public Statement Operator(const Operator &) = delete; void operator=(const Operator &) = delete; + // Comparison - compares type, resources, and modifiers (masking OPER_LAST). + bool + equals(const Statement *other) const override + { + if (!other || strcmp(type_name(), other->type_name()) != 0) { + return false; + } + + if (get_resource_ids() != other->get_resource_ids()) { + return false; + } + + auto *op = static_cast(other); + OperModifiers mods1_masked = static_cast(_mods & ~OPER_LAST); + OperModifiers mods2_masked = static_cast(op->_mods & ~OPER_LAST); + + return mods1_masked == mods2_masked; + } + OperModifiers get_oper_modifiers() const; void initialize(Parser &p) override; + virtual void initialize(const hrw::OperatorSpec &spec); // Returns number of executed operators that need to defer call to TSHttpTxnReenable(). It is a fatal error if this // returns more than 1. If multiple operators need to defer reenable on the same hook, issue 11549 should be diff --git a/plugins/header_rewrite/operators.h b/plugins/header_rewrite/operators.h index eee90c35810..569d6e59ef2 100644 --- a/plugins/header_rewrite/operators.h +++ b/plugins/header_rewrite/operators.h @@ -48,6 +48,22 @@ class OperatorSetConfig : public Operator OperatorSetConfig(const OperatorSetConfig &) = delete; void operator=(const OperatorSetConfig &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetConfig"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _key == op->_key && _type == op->_type && _config == op->_config && _value.equals(&op->_value); + } + void initialize(Parser &p) override; protected: @@ -70,6 +86,23 @@ class OperatorSetStatus : public Operator OperatorSetStatus(const OperatorSetStatus &) = delete; void operator=(const OperatorSetStatus &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetStatus"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _status.equals(&op->_status) && _reason_len == op->_reason_len && + (!_reason || !op->_reason || strncmp(_reason, op->_reason, _reason_len) == 0); + } + void initialize(Parser &p) override; protected: @@ -91,6 +124,22 @@ class OperatorSetStatusReason : public Operator OperatorSetStatusReason(const OperatorSetStatusReason &) = delete; void operator=(const OperatorSetStatusReason &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetStatusReason"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _reason.equals(&op->_reason); + } + void initialize(Parser &p) override; protected: @@ -110,6 +159,28 @@ class OperatorSetDestination : public Operator OperatorSetDestination(const OperatorSetDestination &) = delete; void operator=(const OperatorSetDestination &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetDestination"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _url_qual == op->_url_qual && _value.equals(&op->_value); + } + + std::string + debug_string() const override + { + return std::string(type_name()) + " " + _value.get_value(); + } + void initialize(Parser &p) override; protected: @@ -130,6 +201,12 @@ class OperatorRMDestination : public Operator OperatorRMDestination(const OperatorRMDestination &) = delete; void operator=(const OperatorRMDestination &) = delete; + std::string_view + type_name() const override + { + return "OperatorRMDestination"; + } + void initialize(Parser &p) override; protected: @@ -151,6 +228,28 @@ class OperatorSetRedirect : public Operator OperatorSetRedirect(const OperatorSetRedirect &) = delete; void operator=(const OperatorSetRedirect &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetRedirect"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _status.equals(&op->_status) && _location.equals(&op->_location); + } + + std::string + debug_string() const override + { + return std::string(type_name()) + " " + std::to_string(_status.get_int_value()) + " " + _location.get_value(); + } + void initialize(Parser &p) override; TSHttpStatus @@ -184,6 +283,12 @@ class OperatorNoOp : public Operator OperatorNoOp(const OperatorNoOp &) = delete; void operator=(const OperatorNoOp &) = delete; + std::string_view + type_name() const override + { + return "OperatorNoOp"; + } + protected: bool exec(const Resources & /* res ATS_UNUSED */) const override @@ -201,6 +306,22 @@ class OperatorSetTimeoutOut : public Operator OperatorSetTimeoutOut(const OperatorSetTimeoutOut &) = delete; void operator=(const OperatorSetTimeoutOut &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetTimeoutOut"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _type == op->_type && _timeout.equals(&op->_timeout); + } + void initialize(Parser &p) override; protected: @@ -228,6 +349,12 @@ class OperatorSkipRemap : public Operator OperatorSkipRemap(const OperatorSkipRemap &) = delete; void operator=(const OperatorSkipRemap &) = delete; + std::string_view + type_name() const override + { + return "OperatorSkipRemap"; + } + void initialize(Parser &p) override; protected: @@ -247,6 +374,12 @@ class OperatorRMHeader : public OperatorHeaders OperatorRMHeader(const OperatorRMHeader &) = delete; void operator=(const OperatorRMHeader &) = delete; + std::string_view + type_name() const override + { + return "OperatorRMHeader"; + } + protected: bool exec(const Resources &res) const override; }; @@ -260,6 +393,28 @@ class OperatorAddHeader : public OperatorHeaders OperatorAddHeader(const OperatorAddHeader &) = delete; void operator=(const OperatorAddHeader &) = delete; + std::string_view + type_name() const override + { + return "OperatorAddHeader"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _header == op->_header && _value.equals(&op->_value); + } + + std::string + debug_string() const override + { + return std::string(type_name()) + " " + _header + "=\"" + _value.get_value() + "\""; + } + void initialize(Parser &p) override; protected: @@ -278,6 +433,28 @@ class OperatorSetHeader : public OperatorHeaders OperatorSetHeader(const OperatorSetHeader &) = delete; void operator=(const OperatorSetHeader &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetHeader"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _header == op->_header && _value.equals(&op->_value); + } + + std::string + debug_string() const override + { + return std::string(type_name()) + " " + _header + "=\"" + _value.get_value() + "\""; + } + void initialize(Parser &p) override; protected: @@ -296,6 +473,12 @@ class OperatorCounter : public Operator OperatorCounter(const OperatorCounter &) = delete; void operator=(const OperatorCounter &) = delete; + std::string_view + type_name() const override + { + return "OperatorCounter"; + } + void initialize(Parser &p) override; protected: @@ -315,6 +498,12 @@ class OperatorRMCookie : public OperatorCookies OperatorRMCookie(const OperatorRMCookie &) = delete; void operator=(const OperatorRMCookie &) = delete; + std::string_view + type_name() const override + { + return "OperatorRMCookie"; + } + protected: bool exec(const Resources &res) const override; }; @@ -328,6 +517,28 @@ class OperatorAddCookie : public OperatorCookies OperatorAddCookie(const OperatorAddCookie &) = delete; void operator=(const OperatorAddCookie &) = delete; + std::string_view + type_name() const override + { + return "OperatorAddCookie"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _cookie == op->_cookie && _value.equals(&op->_value); + } + + std::string + debug_string() const override + { + return std::string(type_name()) + " " + _cookie + "=\"" + _value.get_value() + "\""; + } + void initialize(Parser &p) override; protected: @@ -346,6 +557,28 @@ class OperatorSetCookie : public OperatorCookies OperatorSetCookie(const OperatorSetCookie &) = delete; void operator=(const OperatorSetCookie &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetCookie"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _cookie == op->_cookie && _value.equals(&op->_value); + } + + std::string + debug_string() const override + { + return std::string(type_name()) + " " + _cookie + "=\"" + _value.get_value() + "\""; + } + void initialize(Parser &p) override; protected: @@ -376,6 +609,22 @@ class OperatorSetConnDSCP : public Operator OperatorSetConnDSCP(const OperatorSetConnDSCP &) = delete; void operator=(const OperatorSetConnDSCP &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetConnDSCP"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _ds_value.equals(&op->_ds_value); + } + void initialize(Parser &p) override; protected: @@ -395,6 +644,22 @@ class OperatorSetConnMark : public Operator OperatorSetConnMark(const OperatorSetConnMark &) = delete; void operator=(const OperatorSetConnMark &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetConnMark"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _ds_value.equals(&op->_ds_value); + } + void initialize(Parser &p) override; protected: @@ -414,6 +679,12 @@ class OperatorSetDebug : public Operator OperatorSetDebug(const OperatorSetDebug &) = delete; void operator=(const OperatorSetDebug &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetDebug"; + } + void initialize(Parser &p) override; protected: @@ -430,6 +701,22 @@ class OperatorSetBody : public Operator OperatorSetBody(const OperatorSetBody &) = delete; void operator=(const OperatorSetBody &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetBody"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _value.equals(&op->_value); + } + void initialize(Parser &p) override; protected: @@ -449,6 +736,12 @@ class OperatorSetHttpCntl : public Operator OperatorSetHttpCntl(const OperatorSetHttpCntl &) = delete; void operator=(const OperatorSetHttpCntl &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetHttpCntl"; + } + void initialize(Parser &p) override; protected: @@ -469,6 +762,12 @@ class OperatorSetPluginCntl : public Operator OperatorSetPluginCntl(const OperatorSetPluginCntl &) = delete; void operator=(const OperatorSetPluginCntl &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetPluginCntl"; + } + void initialize(Parser &p) override; enum class PluginCtrl { @@ -513,6 +812,12 @@ class OperatorRunPlugin : public Operator OperatorRunPlugin(const OperatorRunPlugin &) = delete; void operator=(const OperatorRunPlugin &) = delete; + std::string_view + type_name() const override + { + return "OperatorRunPlugin"; + } + void initialize(Parser &p) override; protected: @@ -532,6 +837,22 @@ class OperatorSetBodyFrom : public Operator OperatorSetBodyFrom(const OperatorSetBodyFrom &) = delete; void operator=(const OperatorSetBodyFrom &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetBodyFrom"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _value.equals(&op->_value); + } + void initialize(Parser &p) override; enum { TS_EVENT_FETCHSM_SUCCESS = 70000, TS_EVENT_FETCHSM_FAILURE = 70001, TS_EVENT_FETCHSM_TIMEOUT = 70002 }; @@ -557,6 +878,12 @@ class OperatorSetStateFlag : public Operator OperatorSetStateFlag(const OperatorSetStateFlag &) = delete; void operator=(const OperatorSetStateFlag &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetStateFlag"; + } + void initialize(Parser &p) override; protected: @@ -588,6 +915,22 @@ class OperatorSetStateInt8 : public Operator OperatorSetStateInt8(const OperatorSetStateInt8 &) = delete; void operator=(const OperatorSetStateInt8 &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetStateInt8"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _byte_ix == op->_byte_ix && _value.equals(&op->_value); + } + void initialize(Parser &p) override; protected: @@ -618,6 +961,12 @@ class OperatorSetStateInt16 : public Operator OperatorSetStateInt16(const OperatorSetStateInt16 &) = delete; void operator=(const OperatorSetStateInt16 &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetStateInt16"; + } + void initialize(Parser &p) override; protected: @@ -643,6 +992,22 @@ class OperatorSetEffectiveAddress : public Operator OperatorSetEffectiveAddress(const OperatorSetEffectiveAddress &) = delete; void operator=(const OperatorSetEffectiveAddress &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetEffectiveAddress"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _value.equals(&op->_value); + } + void initialize(Parser &p) override; protected: @@ -668,6 +1033,22 @@ class OperatorSetNextHopStrategy : public Operator OperatorSetNextHopStrategy(const OperatorSetNextHopStrategy &) = delete; void operator=(const OperatorSetNextHopStrategy &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetNextHopStrategy"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _value.equals(&op->_value); + } + void initialize(Parser &p) override; protected: @@ -710,6 +1091,12 @@ class OperatorIf : public Operator OperatorIf(const OperatorIf &) = delete; void operator=(const OperatorIf &) = delete; + std::string_view + type_name() const override + { + return "OperatorIf"; + } + ConditionGroup *new_section(Parser::CondClause clause); bool add_operator(Parser &p, const char *filename, int lineno); Condition *make_condition(Parser &p, const char *filename, int lineno); @@ -733,6 +1120,13 @@ class OperatorIf : public Operator return _cur_section; } + // For comparison tool access + const CondOpSection * + get_sections() const + { + return &_sections; + } + OperModifiers exec_and_return_mods(const Resources &res) const; protected: @@ -760,6 +1154,22 @@ class OperatorSetCCAlgorithm : public Operator OperatorSetCCAlgorithm(const OperatorSetCCAlgorithm &) = delete; void operator=(const OperatorSetCCAlgorithm &) = delete; + std::string_view + type_name() const override + { + return "OperatorSetCCAlgorithm"; + } + + bool + equals(const Statement *other) const override + { + if (!Operator::equals(other)) { + return false; + } + auto *op = static_cast(other); + return _cc_alg.equals(&op->_cc_alg); + } + void initialize(Parser &p) override; protected: diff --git a/plugins/header_rewrite/parser.cc b/plugins/header_rewrite/parser.cc index 745f7d0882e..e3557e2b5ec 100644 --- a/plugins/header_rewrite/parser.cc +++ b/plugins/header_rewrite/parser.cc @@ -374,95 +374,3 @@ HRWSimpleTokenizer::HRWSimpleTokenizer(const std::string &line) _tokens.push_back(line.substr(cur_token_start)); } } - -// This is the universal configuration reader, which can read both -// a raw file, as well as executing an external compiler (hrw4u) to parse -// the configuration file. -namespace -{ -void -_log_stderr(int fd) -{ - char buffer[512]; - std::string partial; - - while (ssize_t n = read(fd, buffer, sizeof(buffer))) { - if (n <= 0) { - break; - } - partial.append(buffer, n); - size_t pos = 0; - while ((pos = partial.find('\n')) != std::string::npos) { - std::string line = partial.substr(0, pos); - TSError("[header_rewrite: hrw4u] %s", line.c_str()); - partial.erase(0, pos + 1); - } - } - - if (!partial.empty()) { - TSError("[hrw4u] stderr: %s", partial.c_str()); - } - - close(fd); -} -} // namespace - -std::optional -openConfig(const std::string &filename) -{ - namespace fs = std::filesystem; - const std::string suffix = ".hrw4u"; - std::string hrw4u = Layout::get()->bindir + "/traffic_hrw4u"; - - static const bool has_compiler = [hrw4u]() { - fs::path path(hrw4u); - std::error_code ec; - auto status = fs::status(path, ec); - auto perms = status.permissions(); - return fs::exists(path, ec) && fs::is_regular_file(path, ec) && (perms & fs::perms::owner_exec) != fs::perms::none; - }(); - - if (filename.ends_with(suffix) && has_compiler) { - int pipe_fds[2]; - int stderr_pipe[2]; - - if (pipe(pipe_fds) != 0 || pipe(stderr_pipe) != 0) { - TSError("[header_rewrite] failed to create pipe for hrw4u compiler: %s", strerror(errno)); - return std::nullopt; - } - - pid_t pid = fork(); - if (pid < 0) { - TSError("[header_rewrite] failed to fork for hrw4u compiler: %s", strerror(errno)); - return std::nullopt; - } else if (pid == 0) { - dup2(pipe_fds[1], STDOUT_FILENO); - dup2(stderr_pipe[1], STDERR_FILENO); - close(pipe_fds[0]); - close(stderr_pipe[0]); - - const char *argv[] = {hrw4u.c_str(), filename.c_str(), nullptr}; - execvp(argv[0], const_cast(argv)); - _exit(127); // child exec failed - } - - // Parent - close(pipe_fds[1]); - close(stderr_pipe[1]); - - _log_stderr(stderr_pipe[0]); - - auto pipebuf = std::make_shared(fdopen(pipe_fds[0], "r")); - pipebuf->set_pid(pid); - auto stream = std::make_unique(pipebuf.get()); - - return ConfReader{.stream = std::move(stream), .pipebuf = std::move(pipebuf)}; - } else { - auto file = std::make_unique(filename); - if (!file->is_open()) { - return std::nullopt; - } - - return ConfReader{.stream = std::move(file), .pipebuf = nullptr}; - } -} diff --git a/plugins/header_rewrite/parser.h b/plugins/header_rewrite/parser.h index 854ba452310..e6d04fb7ad1 100644 --- a/plugins/header_rewrite/parser.h +++ b/plugins/header_rewrite/parser.h @@ -33,81 +33,6 @@ #include "ts/ts.h" #include "lulu.h" -/////////////////////////////////////////////////////////////////////////////// -// Simple wrapper, for dealing with raw configurations, and the compiled -// configurations. -class HRW4UPipe : public std::streambuf -{ -public: - explicit HRW4UPipe(FILE *pipe) : _pipe(pipe) { setg(_buffer, _buffer, _buffer); } - - ~HRW4UPipe() override { close(); } - - void - set_pid(pid_t pid) - { - _pid = pid; - } - - int - exit_status() const - { - return _exit_code; - } - - void - close() - { - if (_pipe) { - fclose(_pipe); - _pipe = nullptr; - } - - if (_pid > 0) { - int status = -1; - waitpid(_pid, &status, 0); - if (WIFEXITED(status)) { - _exit_code = WEXITSTATUS(status); - } else if (WIFSIGNALED(status)) { - _exit_code = 128 + WTERMSIG(status); - } else { - _exit_code = -1; - } - _pid = -1; - } - } - -protected: - int - underflow() override - { - if (!_pipe) { - return traits_type::eof(); - } - - size_t n = fread(_buffer, 1, sizeof(_buffer), _pipe); - if (n == 0) { - return traits_type::eof(); - } - - setg(_buffer, _buffer, _buffer + n); - return traits_type::to_int_type(*gptr()); - } - -private: - char _buffer[65536]; - FILE *_pipe = nullptr; - pid_t _pid = -1; - int _exit_code = -1; -}; - -struct ConfReader { - std::unique_ptr stream; - std::shared_ptr pipebuf; -}; - -std::optional openConfig(const std::string &filename); - /////////////////////////////////////////////////////////////////////////////// // class Parser @@ -269,6 +194,37 @@ class Parser } } + // Setters for programmatic construction (used by hrw4u integration) + void + set_op(const std::string &op) + { + _op = op; + } + + void + set_arg(const std::string &arg) + { + _arg = arg; + } + + void + set_val(const std::string &val) + { + _val = val; + } + + void + add_mod(const std::string &mod) + { + _mods.push_back(mod); + } + + void + clear_mods() + { + _mods.clear(); + } + private: bool preprocess(std::vector tokens); diff --git a/plugins/header_rewrite/ruleset.h b/plugins/header_rewrite/ruleset.h index 78680bc9d7e..3749e7cda84 100644 --- a/plugins/header_rewrite/ruleset.h +++ b/plugins/header_rewrite/ruleset.h @@ -107,6 +107,12 @@ class RuleSet return _ids; } + void + require_resources(const ResourceIDs ids) + { + _ids = static_cast(_ids | ids); + } + bool last() const { @@ -115,6 +121,12 @@ class RuleSet OperModifiers exec(const Resources &res) const; + const OperatorIf * + get_operator_if() const + { + return &_op_if; + } + // Linked list of RuleSets std::unique_ptr next; diff --git a/plugins/header_rewrite/statement.h b/plugins/header_rewrite/statement.h index aac03d878a1..f2bb423f875 100644 --- a/plugins/header_rewrite/statement.h +++ b/plugins/header_rewrite/statement.h @@ -23,6 +23,7 @@ #pragma once #include +#include #include #include @@ -141,8 +142,27 @@ class Statement } // noncopyable - Statement(const Statement &) = delete; - void operator=(const Statement &) = delete; + Statement(const Statement &) = delete; + void operator=(const Statement &) = delete; + virtual std::string_view type_name() const = 0; + + // Returns a human-readable representation of this statement + virtual std::string + debug_string() const + { + return std::string(type_name()); + } + + // Comparison interface - compares this statement with another + virtual bool + equals(const Statement *other) const + { + if (!other || type_name() != other->type_name()) { + return false; + } + + return _hook == other->_hook && _rsrc == other->_rsrc; + } // Which hook are we adding this statement to? bool set_hook(TSHttpHookID hook); @@ -164,6 +184,12 @@ class Statement ResourceIDs get_resource_ids() const; + Statement * + next() const + { + return _next; + } + virtual void initialize(Parser &) { diff --git a/plugins/header_rewrite/types.h b/plugins/header_rewrite/types.h new file mode 100644 index 00000000000..5b25d72ab9f --- /dev/null +++ b/plugins/header_rewrite/types.h @@ -0,0 +1,52 @@ +/* + 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. +*/ +#pragma once + +#include + +#include "hrw4u/ObjTypes.h" + +namespace hrw +{ +struct ConditionSpec { + ConditionType type = ConditionType::NONE; + std::string qualifier; + std::string match_arg; + int slot = -1; + + bool mod_not = false; + bool mod_or = false; + bool mod_and = false; + bool mod_nocase = false; + bool mod_last = false; + bool mod_ext = false; + bool mod_pre = false; +}; + +struct OperatorSpec { + OperatorType type = OperatorType::NONE; + std::string arg; + std::string value; + int slot = -1; + + bool mod_last = false; + bool mod_qsa = false; + bool mod_inv = false; +}; + +} // namespace hrw diff --git a/plugins/header_rewrite/value.cc b/plugins/header_rewrite/value.cc index 70fd17893a9..9ea1e9b41ee 100644 --- a/plugins/header_rewrite/value.cc +++ b/plugins/header_rewrite/value.cc @@ -26,7 +26,7 @@ #include "value.h" #include "condition.h" -#include "factory.h" +#include "objtypes.h" #include "parser.h" #include "conditions.h" @@ -48,18 +48,26 @@ Value::set_value(const std::string &val, Statement *owner) Condition *tcond_val = nullptr; if (token.substr(0, 2) == "%{") { + // The cond_token format is "COND:qualifier" or "COND:qualifier arg" std::string cond_token = token.substr(2, token.size() - 3); + std::string cond_name; + std::string cond_arg; + auto space_pos = cond_token.find(' '); + + if (space_pos != std::string::npos) { + cond_name = cond_token.substr(0, space_pos); + cond_arg = cond_token.substr(space_pos + 1); + } else { + cond_name = cond_token; + } - if ((tcond_val = condition_factory(cond_token))) { - Parser parser; + auto spec = hrw::parse_condition_string(cond_name, cond_arg); - if (parser.parse_line(cond_token)) { - tcond_val->initialize(parser); - require_resources(tcond_val->get_resource_ids()); - } else { - // TODO: should we produce error here? - Dbg(dbg_ctl, "Error parsing value '%s'", _value.c_str()); - } + tcond_val = hrw::create_condition(spec); + if (tcond_val) { + require_resources(tcond_val->get_resource_ids()); + } else { + Dbg(dbg_ctl, "Error creating condition for value '%s'", _value.c_str()); } } else { tcond_val = new ConditionStringLiteral(token); diff --git a/plugins/header_rewrite/value.h b/plugins/header_rewrite/value.h index 6b96098e500..987aa01e417 100644 --- a/plugins/header_rewrite/value.h +++ b/plugins/header_rewrite/value.h @@ -48,6 +48,24 @@ class Value : public Statement Value(const Value &) = delete; void operator=(const Value &) = delete; + std::string_view + type_name() const override + { + return "Value"; + } + + bool + equals(const Statement *other) const override + { + if (!Statement::equals(other)) { + return false; + } + + auto *val = static_cast(other); + + return _value == val->_value; + } + void set_value(const std::string &val, Statement *owner = nullptr); void diff --git a/src/hrw4u/CMakeLists.txt b/src/hrw4u/CMakeLists.txt new file mode 100644 index 00000000000..fe3c8274f6e --- /dev/null +++ b/src/hrw4u/CMakeLists.txt @@ -0,0 +1,92 @@ +####################### +# +# 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. +# +####################### + +# Find ANTLR4 runtime +find_package(antlr4-runtime QUIET) + +if(NOT antlr4-runtime_FOUND) + message(STATUS "ANTLR4 runtime not found - hrw4u library will not be built") + return() +endif() + +# Find ANTLR4 generator tool +find_program(ANTLR4_EXECUTABLE antlr4 NAMES antlr4 antlr) + +if(NOT ANTLR4_EXECUTABLE) + message(STATUS "ANTLR4 tool not found - hrw4u library will not be built") + return() +endif() + +message(STATUS "Building hrw4u library with ANTLR4 support") + +# Grammar file location +set(HRW4U_GRAMMAR_FILE "${PROJECT_SOURCE_DIR}/tools/hrw4u/grammar/hrw4u.g4") + +# Generated parser output directory +set(HRW4U_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") +file(MAKE_DIRECTORY ${HRW4U_GENERATED_DIR}) + +# Generate C++ parser from grammar +set(HRW4U_GENERATED_SOURCES ${HRW4U_GENERATED_DIR}/hrw4uLexer.cpp ${HRW4U_GENERATED_DIR}/hrw4uParser.cpp + ${HRW4U_GENERATED_DIR}/hrw4uBaseVisitor.cpp ${HRW4U_GENERATED_DIR}/hrw4uVisitor.cpp +) + +set(HRW4U_GENERATED_HEADERS ${HRW4U_GENERATED_DIR}/hrw4uLexer.h ${HRW4U_GENERATED_DIR}/hrw4uParser.h + ${HRW4U_GENERATED_DIR}/hrw4uBaseVisitor.h ${HRW4U_GENERATED_DIR}/hrw4uVisitor.h +) + +add_custom_command( + OUTPUT ${HRW4U_GENERATED_SOURCES} ${HRW4U_GENERATED_HEADERS} + COMMAND ${ANTLR4_EXECUTABLE} -Dlanguage=Cpp -visitor -no-listener -o ${HRW4U_GENERATED_DIR} ${HRW4U_GRAMMAR_FILE} + DEPENDS ${HRW4U_GRAMMAR_FILE} + COMMENT "Generating ANTLR4 C++ parser for hrw4u" + VERBATIM +) + +add_custom_target(hrw4u_generated DEPENDS ${HRW4U_GENERATED_SOURCES}) + +# Suppress warnings for generated ANTLR4 code +set_source_files_properties(${HRW4U_GENERATED_SOURCES} PROPERTIES COMPILE_FLAGS "-Wno-unused-parameter") + +# Library sources +set(HRW4U_SOURCES Error.cc Types.cc Tables.cc Visitor.cc HRW4UVisitorImpl.cc) + +# Create static library +add_library(hrw4u STATIC ${HRW4U_SOURCES} ${HRW4U_GENERATED_SOURCES}) +add_library(ts::hrw4u ALIAS hrw4u) + +add_dependencies(hrw4u hrw4u_generated) + +target_include_directories( + hrw4u + PUBLIC $ $ + PRIVATE ${HRW4U_GENERATED_DIR} ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_link_libraries(hrw4u PUBLIC antlr4_static) + +set(HRW4U_PUBLIC_HEADERS + ${PROJECT_SOURCE_DIR}/include/hrw4u/Error.h ${PROJECT_SOURCE_DIR}/include/hrw4u/Types.h + ${PROJECT_SOURCE_DIR}/include/hrw4u/Tables.h ${PROJECT_SOURCE_DIR}/include/hrw4u/Visitor.h + ${PROJECT_SOURCE_DIR}/include/hrw4u/HRW4UVisitor.h +) + +set_target_properties(hrw4u PROPERTIES PUBLIC_HEADER "${HRW4U_PUBLIC_HEADERS}") + +install(TARGETS hrw4u PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/hrw4u) + +clang_tidy_check(hrw4u) diff --git a/src/hrw4u/Error.cc b/src/hrw4u/Error.cc new file mode 100644 index 00000000000..bd8f1c4d89e --- /dev/null +++ b/src/hrw4u/Error.cc @@ -0,0 +1,203 @@ +/* + 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. +*/ + +#include "hrw4u/Error.h" +#include +#include + +namespace hrw4u +{ + +std::string +SourceLocation::format() const +{ + std::ostringstream ss; + + if (!filename.empty()) { + ss << filename << ":"; + } + if (line > 0) { + ss << line; + if (column > 0) { + ss << ":" << column; + } + } + return ss.str(); +} + +std::string_view +ParseError::severity_str() const +{ + switch (severity) { + case ErrorSeverity::Warning: + return "warning"; + case ErrorSeverity::Error: + return "error"; + case ErrorSeverity::Fatal: + return "fatal"; + } + return "unknown"; +} + +std::string +ParseError::format() const +{ + std::ostringstream ss; + + if (location.is_valid()) { + ss << location.format() << ": "; + } + + ss << severity_str() << ": " << message; + + if (!code.empty()) { + ss << " [" << code << "]"; + } + + if (!location.context.empty() && location.line > 0) { + ss << "\n"; + ss << std::setw(4) << location.line << " | " << location.context; + + if (location.column > 0) { + ss << "\n | "; + for (size_t i = 0; i < location.column; ++i) { + ss << ' '; + } + ss << '^'; + } + } + + return ss.str(); +} + +ErrorCollector::ErrorCollector(ErrorCallback callback) : _callback(std::move(callback)) {} + +void +ErrorCollector::add_error(ParseError error) +{ + if (!error.location.filename.empty() && _current_filename.empty()) { + _current_filename = error.location.filename; + } else if (error.location.filename.empty() && !_current_filename.empty()) { + error.location.filename = _current_filename; + } + + if (_callback) { + _callback(error); + } + + _errors.push_back(std::move(error)); +} + +void +ErrorCollector::add_error(ErrorSeverity severity, std::string message, SourceLocation location, std::string code) +{ + add_error( + ParseError{.message = std::move(message), .code = std::move(code), .severity = severity, .location = std::move(location)}); +} + +bool +ErrorCollector::has_errors() const +{ + for (const auto &err : _errors) { + if (err.severity != ErrorSeverity::Warning) { + return true; + } + } + + return false; +} + +bool +ErrorCollector::has_fatal() const +{ + for (const auto &err : _errors) { + if (err.severity == ErrorSeverity::Fatal) { + return true; + } + } + + return false; +} + +size_t +ErrorCollector::error_count() const +{ + size_t count = 0; + + for (const auto &err : _errors) { + if (err.severity != ErrorSeverity::Warning) { + ++count; + } + } + + return count; +} + +void +ErrorCollector::clear() +{ + _errors.clear(); +} + +std::string +ErrorCollector::format_all() const +{ + std::ostringstream ss; + + for (const auto &err : _errors) { + ss << err.format() << "\n"; + } + + return ss.str(); +} + +std::string +ErrorCollector::summary() const +{ + size_t warnings = 0; + size_t errors = 0; + std::ostringstream ss; + + for (const auto &err : _errors) { + if (err.severity == ErrorSeverity::Warning) { + ++warnings; + } else { + ++errors; + } + } + + ss << errors << " error(s), " << warnings << " warning(s)"; + + return ss.str(); +} + +ParseException::ParseException(ParseError error) : _error(std::move(error)), _formatted(_error.format()) {} + +ParseException::ParseException(std::string message, SourceLocation location) + : _error{.message = std::move(message), .severity = ErrorSeverity::Fatal, .location = std::move(location)}, + _formatted(_error.format()) +{ +} + +const char * +ParseException::what() const noexcept +{ + return _formatted.c_str(); +} + +} // namespace hrw4u diff --git a/src/hrw4u/HRW4UVisitorImpl.cc b/src/hrw4u/HRW4UVisitorImpl.cc new file mode 100644 index 00000000000..c19a37a07a8 --- /dev/null +++ b/src/hrw4u/HRW4UVisitorImpl.cc @@ -0,0 +1,1655 @@ +/* + 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. +*/ + +#include "HRW4UVisitorImpl.h" + +#include +#include +#include +#include +#include + +#include "hrw4uLexer.h" +#include "hrw4uParser.h" + +#include "atn/ParserATNSimulator.h" +#include "atn/PredictionMode.h" + +namespace hrw4u +{ + +HRW4UVisitorImpl::HRW4UVisitorImpl(const FactoryCallbacks &callbacks, const ParserConfig &config) + : _callbacks(callbacks), _config(config), _resolver(symbol_resolver()) +{ +} + +ParseResult +HRW4UVisitorImpl::parse(std::string_view input) +{ + ParseResult result; + std::string line; + + _source_lines.clear(); + for (char c : input) { + if (c == '\n') { + _source_lines.push_back(line); + line.clear(); + } else { + line += c; + } + } + if (!line.empty()) { + _source_lines.push_back(line); + } + + try { + std::string input_str(input); + antlr4::ANTLRInputStream input_stream(input_str); + hrw4uLexer lexer(&input_stream); + HRW4UErrorListener lexer_error_listener(_errors, _config.filename); + + lexer.removeErrorListeners(); + lexer.addErrorListener(&lexer_error_listener); + + antlr4::CommonTokenStream tokens(&lexer); + hrw4uParser parser(&tokens); + HRW4UErrorListener parser_error_listener(_errors, _config.filename); + + parser.removeErrorListeners(); + parser.addErrorListener(&parser_error_listener); + + // Use SLL prediction mode for faster parsing + parser.getInterpreter()->setPredictionMode(antlr4::atn::PredictionMode::SLL); + + hrw4uParser::ProgramContext *tree = parser.program(); + + if (!_errors.has_errors()) { + visit(tree); + close_section(); + } + + if (_errors.has_errors()) { + cleanup_on_error(); + result.success = false; + } else { + result.success = true; + result.rulesets = std::move(_rulesets); + result.sections = std::move(_sections); + } + result.errors = std::move(_errors); + + } catch (const std::exception &e) { + add_error(std::string("Parse error: ") + e.what()); + cleanup_on_error(); + result.success = false; + result.errors = std::move(_errors); + } + + return result; +} + +void +HRW4UVisitorImpl::add_error(antlr4::ParserRuleContext *ctx, const std::string &message) +{ + _errors.add_error(ParseError{.message = message, .location = get_location(ctx)}); +} + +void +HRW4UVisitorImpl::add_error(const std::string &message) +{ + _errors.add_error(ParseError{.message = message, .location = {.filename = _config.filename}}); +} + +SourceLocation +HRW4UVisitorImpl::get_location(antlr4::ParserRuleContext *ctx) const +{ + SourceLocation loc; + + loc.filename = _config.filename; + if (ctx && ctx->start) { + loc.line = ctx->start->getLine(); + loc.column = ctx->start->getCharPositionInLine(); + loc.context = get_source_line(loc.line); + if (ctx->stop) { + loc.length = ctx->stop->getStopIndex() - ctx->start->getStartIndex() + 1; + } else { + loc.length = ctx->start->getText().size(); + } + } + return loc; +} + +std::string +HRW4UVisitorImpl::get_source_line(size_t line_number) const +{ + if (line_number > 0 && line_number <= _source_lines.size()) { + return _source_lines[line_number - 1]; + } + return ""; +} + +void +HRW4UVisitorImpl::start_section(SectionType type) +{ + close_section(); + _current_section = type; + _current_ruleset = nullptr; + _section_has_ops = false; +} + +void +HRW4UVisitorImpl::close_section() +{ + if (_current_ruleset != nullptr) { + _rulesets.push_back(_current_ruleset); + _sections.push_back(_current_section); + _current_ruleset = nullptr; + } + _current_section = SectionType::UNKNOWN; +} + +void * +HRW4UVisitorImpl::get_or_create_ruleset() +{ + if (_current_ruleset == nullptr && _callbacks.create_ruleset) { + _current_ruleset = _callbacks.create_ruleset(); + track_object(_current_ruleset, "ruleset"); + } + return _current_ruleset; +} + +ParserContext +HRW4UVisitorImpl::build_parser_context(const std::string &op, const std::string &arg, const std::string &val) +{ + ParserContext ctx; + + ctx.op = op; + ctx.arg = arg; + ctx.val = val; + ctx.from_url = _config.from_url; + ctx.to_url = _config.to_url; + + for (const auto &m : _cond_state.to_list()) { + ctx.mods.push_back(m); + } + for (const auto &m : _oper_state.to_list()) { + ctx.mods.push_back(m); + } + + return ctx; +} + +void * +HRW4UVisitorImpl::create_condition(const ParserContext &pctx) +{ + if (!_callbacks.create_condition) { + add_error("No condition factory callback configured"); + return nullptr; + } + void *cond = _callbacks.create_condition(pctx); + if (cond) { + track_object(cond, "condition"); + } + return cond; +} + +void * +HRW4UVisitorImpl::create_operator(const ParserContext &pctx) +{ + if (!_callbacks.create_operator) { + add_error("No operator factory callback configured"); + return nullptr; + } + void *op = _callbacks.create_operator(pctx); + if (op) { + track_object(op, "operator"); + } + return op; +} + +bool +HRW4UVisitorImpl::add_condition_to_current(void *cond) +{ + if (!cond) { + return false; + } + + // If we have an active group stack, use the dedicated group callback + // Groups are ConditionGroup* objects, not RuleSet*, so we need a separate callback + if (!_group_stack.empty()) { + void *group_ptr = _group_stack.top(); + + if (group_ptr && _callbacks.add_condition_to_group) { + return _callbacks.add_condition_to_group(group_ptr, cond); + } + return false; + } + + if (!_if_stack.empty()) { + IfBlockState &state = _if_stack.top(); + + if (state.op_if && _callbacks.add_condition_to_if) { + return _callbacks.add_condition_to_if(state.op_if, cond); + } + // If op_if is nullptr, fall through to add to RuleSet (section-level if/elif/else) + } + + void *ruleset = get_or_create_ruleset(); + + if (!ruleset || !_callbacks.add_condition) { + return false; + } + return _callbacks.add_condition(ruleset, cond); +} + +bool +HRW4UVisitorImpl::add_operator_to_current(void *op) +{ + if (!op) { + return false; + } + + if (!_if_stack.empty()) { + IfBlockState &state = _if_stack.top(); + + if (state.op_if && _callbacks.add_operator_to_if) { + return _callbacks.add_operator_to_if(state.op_if, op); + } + // If op_if is nullptr, fall through to add to RuleSet (section-level if/elif/else) + } + + void *ruleset = get_or_create_ruleset(); + if (!ruleset || !_callbacks.add_operator) { + return false; + } + _section_has_ops = true; + return _callbacks.add_operator(ruleset, op); +} + +void +HRW4UVisitorImpl::cleanup_on_error() +{ + if (_callbacks.destroy) { + for (auto &[obj, type] : _allocated_objects) { + if (obj && type == "ruleset") { + _callbacks.destroy(obj, type); + } + } + } + + _allocated_objects.clear(); + _rulesets.clear(); + _sections.clear(); + _current_ruleset = nullptr; +} + +void +HRW4UVisitorImpl::track_object(void *obj, const std::string &type) +{ + if (obj) { + _allocated_objects.emplace_back(obj, type); + } +} + +ResolveResult +HRW4UVisitorImpl::resolve_identifier(const std::string &ident) +{ + auto it = _variables.find(ident); + + if (it != _variables.end()) { + const VarTypeInfo &info = var_type_info(it->second.type); + ResolveResult result; + + result.target = "STATE-" + std::string(info.cond_tag) + ":" + std::to_string(it->second.slot); + result.success = true; + + return result; + } + + return _resolver.resolve_condition(ident, _current_section); +} + +std::string +HRW4UVisitorImpl::extract_value_string(hrw4uParser::ValueContext *ctx) +{ + if (!ctx) { + return ""; + } + + if (ctx->str) { + std::string text = ctx->str->getText(); + + if (text.size() >= 2 && text.front() == '"' && text.back() == '"') { + return text.substr(1, text.size() - 2); + } + return text; + } + if (ctx->number) { + return ctx->number->getText(); + } + if (ctx->ident) { + return ctx->ident->getText(); + } + if (ctx->TRUE()) { + return "true"; + } + if (ctx->FALSE()) { + return "false"; + } + if (ctx->ip()) { + return ctx->ip()->getText(); + } + if (ctx->iprange()) { + return ctx->iprange()->getText(); + } + + return ctx->getText(); +} + +std::string +HRW4UVisitorImpl::substitute_strings(const std::string &str, antlr4::ParserRuleContext * /* ctx */) +{ + if (str.size() < 2 || str.front() != '"' || str.back() != '"') { + return str; + } + + try { + std::string inner = str.substr(1, str.size() - 2); + std::string result = ""; + static const std::regex pattern(R"(\{([a-zA-Z_][a-zA-Z0-9_.-]*(?:\([^)]*\))?)\})"); + std::smatch match; + std::string remaining = inner; + + while (std::regex_search(remaining, match, pattern)) { + if (match.position() > 0 && remaining[match.position() - 1] == '%') { + result += remaining.substr(0, match.position() + match.length()); + remaining = remaining.substr(match.position() + match.length()); + continue; + } + + result += match.prefix(); + + std::string content = match[1].str(); + size_t paren_pos = content.find('('); + + if (paren_pos != std::string::npos) { + std::string func_name = content.substr(0, paren_pos); + std::string args_str = content.substr(paren_pos + 1); + + if (!args_str.empty() && args_str.back() == ')') { + args_str.pop_back(); + } + + auto func_result = _resolver.resolve_function(func_name, _current_section); + + if (func_result.success) { + std::string replacement = func_result.target; + + if (!args_str.empty()) { + replacement += ":" + args_str; + } + result += "%{" + replacement + "}"; + } else { + result += "{" + content + "}"; + } + } else { + auto var_it = _variables.find(content); + + if (var_it != _variables.end()) { + const VarTypeInfo &info = var_type_info(var_it->second.type); + + result += "%{STATE-" + std::string(info.cond_tag) + ":" + std::to_string(var_it->second.slot) + "}"; + } else { + auto cond_result = _resolver.resolve_condition(content, _current_section); + + if (cond_result.success) { + std::string resolved = cond_result.target; + + if (!cond_result.suffix.empty()) { + resolved += ":" + cond_result.suffix; + } + + if (resolved.size() >= 4 && resolved.substr(0, 2) == "%{" && resolved.back() == '}') { + result += resolved; + } else { + result += "%{" + resolved + "}"; + } + } else { + result += "{" + content + "}"; + } + } + } + + remaining = match.suffix(); + } + + result += remaining; + + return "\"" + result + "\""; + } catch (const std::exception &e) { + add_error("String substitution error: " + std::string(e.what())); + return str; + } +} + +void +HRW4UVisitorImpl::extract_modifiers(hrw4uParser::ModifierContext *ctx) +{ + if (!ctx || !ctx->modifierList()) { + return; + } + + for (auto *token : ctx->modifierList()->mods) { + std::string mod = token->getText(); + + std::transform(mod.begin(), mod.end(), mod.begin(), [](unsigned char c) { return std::toupper(c); }); + if (ModifierInfo::is_condition_modifier(mod)) { + _cond_state.add_modifier(mod); + } else if (ModifierInfo::is_operator_modifier(mod)) { + _oper_state.add_modifier(mod); + } else { + add_error(ctx, "Unknown modifier: " + mod); + } + } +} + +std::string +HRW4UVisitorImpl::get_comparison_op(hrw4uParser::ComparisonContext *ctx) +{ + if (ctx->children.size() > 1) { + std::string op_text = ctx->children[1]->getText(); + + if (op_text == "==" || op_text == "=") { + return "="; + } + if (op_text == "!=" || op_text == "!~") { + return "="; + } + if (op_text == ">" || op_text == "<") { + return op_text; + } + } + return "="; +} + +std::any +HRW4UVisitorImpl::visitProgram(hrw4uParser::ProgramContext *ctx) +{ + for (auto *item : ctx->programItem()) { + if (item->commentLine()) { + continue; + } + if (item->section()) { + visit(item->section()); + } + } + + close_section(); + return {}; +} + +std::any +HRW4UVisitorImpl::visitSection(hrw4uParser::SectionContext *ctx) +{ + if (ctx->varSection()) { + return visit(ctx->varSection()); + } + + if (!ctx->name) { + add_error(ctx, "Missing section name"); + return {}; + } + + std::string section_name = ctx->name->getText(); + auto section_type = _resolver.resolve_hook(section_name); + + if (!section_type) { + add_error(ctx, "Invalid section name: " + section_name); + return {}; + } + + start_section(*section_type); + + bool in_statement_block = false; + + for (size_t idx = 0; idx < ctx->sectionBody().size(); ++idx) { + auto *body = ctx->sectionBody()[idx]; + bool is_conditional = body->conditional() != nullptr; + bool is_comment = body->commentLine() != nullptr; + + if (is_comment) { + continue; + } else if (is_conditional) { + if (idx > 0) { + close_section(); + start_section(*section_type); + } + visit(body->conditional()); + in_statement_block = false; + } else { + if (!in_statement_block) { + if (idx > 0) { + close_section(); + start_section(*section_type); + } + in_statement_block = true; + } + + if (auto *stmt = body->statement()) { + visit(stmt); + } + } + } + + return {}; +} + +std::any +HRW4UVisitorImpl::visitVarSection(hrw4uParser::VarSectionContext *ctx) +{ + if (_current_section != SectionType::UNKNOWN) { + add_error(ctx, "Variable section must appear before any hook section"); + return {}; + } + + if (ctx->variables()) { + for (auto *item : ctx->variables()->variablesItem()) { + if (item->commentLine()) { + continue; + } + if (item->variableDecl()) { + visit(item->variableDecl()); + } + } + } + + return {}; +} + +std::any +HRW4UVisitorImpl::visitVariableDecl(hrw4uParser::VariableDeclContext *ctx) +{ + if (!ctx->name || !ctx->typeName) { + add_error(ctx, "Variable declaration requires name and type"); + return {}; + } + + std::string name = ctx->name->getText(); + std::string type_name = ctx->typeName->getText(); + + if (name.find('.') != std::string::npos || name.find(':') != std::string::npos) { + add_error(ctx, "Variable name cannot contain '.' or ':': " + name); + return {}; + } + + auto var_type = _resolver.resolve_var_type(type_name); + + if (!var_type) { + add_error(ctx, "Invalid variable type: " + type_name); + return {}; + } + + int slot = _next_var_slot++; + + if (ctx->slot) { + slot = std::stoi(ctx->slot->getText()); + } + _variables[name] = Variable{.name = name, .type = *var_type, .slot = slot}; + + return {}; +} + +std::any +HRW4UVisitorImpl::visitStatement(hrw4uParser::StatementContext *ctx) +{ + if (ctx->BREAK()) { + process_break(); + return {}; + } + + if (ctx->functionCall()) { + process_function_statement(ctx->functionCall()); + return {}; + } + + if (ctx->EQUAL() && ctx->lhs && ctx->value()) { + process_assignment(ctx, ctx->lhs->getText(), ctx->value(), false); + return {}; + } + + if (ctx->PLUSEQUAL() && ctx->lhs && ctx->value()) { + process_assignment(ctx, ctx->lhs->getText(), ctx->value(), true); + return {}; + } + + if (ctx->op) { + auto result = _resolver.resolve_statement_function(ctx->op->getText(), _current_section); + ParserContext pctx = build_parser_context(result.success ? result.target : ctx->op->getText()); + pctx.op_type = result.op_type; + void *op = create_operator(pctx); + + if (op) { + add_operator_to_current(op); + } + + return {}; + } + + add_error(ctx, "Unrecognized statement"); + return {}; +} + +void +HRW4UVisitorImpl::process_break() +{ + _oper_state.last_modifier = true; + ParserContext pctx = build_parser_context("no-op"); + pctx.op_type = hrw::OperatorType::NO_OP; + void *op = create_operator(pctx); + + if (op) { + add_operator_to_current(op); + } + _oper_state.reset(); +} + +void +HRW4UVisitorImpl::process_assignment(hrw4uParser::StatementContext *stmt_ctx, const std::string &lhs, + hrw4uParser::ValueContext *value_ctx, bool is_append) +{ + std::string raw_rhs = value_ctx->getText(); + std::string rhs; + + if (raw_rhs.size() >= 2 && raw_rhs.front() == '"' && raw_rhs.back() == '"') { + rhs = substitute_strings(raw_rhs, value_ctx); + if (rhs.size() >= 2 && rhs.front() == '"' && rhs.back() == '"') { + rhs = rhs.substr(1, rhs.size() - 2); + } + } else { + rhs = extract_value_string(value_ctx); + } + + auto var_it = _variables.find(lhs); + + if (var_it != _variables.end()) { + if (is_append) { + add_error(stmt_ctx, "Cannot use += operator with variables"); + return; + } + + const Variable &var = var_it->second; + const VarTypeInfo &info = var_type_info(var.type); + std::string rhs_value = rhs; + + if (value_ctx && value_ctx->ident) { + auto rhs_var_it = _variables.find(rhs); + + if (rhs_var_it != _variables.end()) { + auto resolve_result = resolve_identifier(rhs); + + if (resolve_result.success) { + rhs_value = "%{" + resolve_result.target + "}"; + } + } + } + + ParserContext pctx = build_parser_context(std::string(info.op_tag), std::to_string(var.slot), rhs_value); + pctx.op_type = info.op_type; + void *oper = create_operator(pctx); + + if (oper) { + add_operator_to_current(oper); + } + + return; + } + + auto result = _resolver.resolve_operator(lhs, _current_section); + + if (!result.success) { + add_error(stmt_ctx, "Cannot resolve operator for: " + lhs + " - " + result.error_message); + return; + } + + ParserContext pctx = build_parser_context("", result.suffix.empty() ? rhs : result.suffix, result.suffix.empty() ? "" : rhs); + pctx.op_type = result.get_operator_type(is_append, rhs.empty()); + void *op = create_operator(pctx); + + if (op) { + add_operator_to_current(op); + } +} + +void +HRW4UVisitorImpl::process_function_statement(hrw4uParser::FunctionCallContext *ctx) +{ + if (!ctx->funcName) { + add_error(ctx, "Missing function name"); + return; + } + + std::string func_name = ctx->funcName->getText(); + std::vector args; + + if (ctx->argumentList()) { + for (auto *val : ctx->argumentList()->value()) { + std::string raw_arg = val->getText(); + + if (raw_arg.size() >= 2 && raw_arg.front() == '"' && raw_arg.back() == '"') { + std::string substituted = substitute_strings(raw_arg, val); + + if (substituted.size() >= 2 && substituted.front() == '"' && substituted.back() == '"') { + substituted = substituted.substr(1, substituted.size() - 2); + } + args.push_back(substituted); + } else { + args.push_back(extract_value_string(val)); + } + } + } + + auto result = _resolver.resolve_statement_function(func_name, _current_section); + + if (!result.success) { + add_error(ctx, "Unknown function: " + func_name); + return; + } + + std::string arg = args.empty() ? "" : args[0]; + std::string val; + + for (size_t i = 1; i < args.size(); ++i) { + if (!val.empty()) { + val += " "; + } + val += args[i]; + } + + std::string first_arg; + + if (!result.target.empty()) { + first_arg = result.target; + if (!arg.empty()) { + first_arg += " " + arg; + } + } else { + first_arg = arg; + } + + ParserContext pctx = build_parser_context("", first_arg, val); + + pctx.op_type = result.op_type; + if (func_name == "keep_query") { + pctx.mods.push_back("I"); + } + + void *op = create_operator(pctx); + + if (op) { + add_operator_to_current(op); + } +} + +std::any +HRW4UVisitorImpl::visitConditional(hrw4uParser::ConditionalContext *ctx) +{ + bool is_section_level = _if_stack.empty(); + bool has_elif_else = !ctx->elifClause().empty() || ctx->elseClause(); + + if (is_section_level && !has_elif_else) { + if (ctx->ifStatement()) { + if (ctx->ifStatement()->condition()) { + visit(ctx->ifStatement()->condition()); + } + if (ctx->ifStatement()->block()) { + visit(ctx->ifStatement()->block()); + } + } + } else if (is_section_level && has_elif_else) { + IfBlockState state{.op_if = nullptr, .clause_index = 0}; + + _if_stack.push(state); + if (ctx->ifStatement()) { + if (ctx->ifStatement()->condition()) { + visit(ctx->ifStatement()->condition()); + } + if (ctx->ifStatement()->block()) { + visit(ctx->ifStatement()->block()); + } + } + + for (auto *elif_ctx : ctx->elifClause()) { + if (_callbacks.new_ruleset_section) { + void *ruleset = get_or_create_ruleset(); + void *group = _callbacks.new_ruleset_section(ruleset, CondClause::ELIF); + + if (group) { + _group_stack.push(group); + visit(elif_ctx->condition()); + visit(elif_ctx->block()); + _group_stack.pop(); + } + } + } + + if (ctx->elseClause()) { + if (_callbacks.new_ruleset_section) { + void *ruleset = get_or_create_ruleset(); + void *group = _callbacks.new_ruleset_section(ruleset, CondClause::ELSE); + + if (group) { + _group_stack.push(group); + visit(ctx->elseClause()->block()); + _group_stack.pop(); + } + } + } + + _if_stack.pop(); + } else { + bool has_elif_else = !ctx->elifClause().empty() || ctx->elseClause(); + + if (!has_elif_else) { + visit(ctx->ifStatement()); + } else { + void *op_if = nullptr; + + if (_callbacks.create_if_operator) { + op_if = _callbacks.create_if_operator(); + if (op_if) { + track_object(op_if, "operator_if"); + } + } + + IfBlockState state{.op_if = op_if, .clause_index = 0}; + + _if_stack.push(state); + + if (ctx->ifStatement()) { + if (ctx->ifStatement()->condition()) { + visit(ctx->ifStatement()->condition()); + } + if (ctx->ifStatement()->block()) { + visit(ctx->ifStatement()->block()); + } + } + + for (auto *elif_ctx : ctx->elifClause()) { + if (_callbacks.new_section && op_if) { + void *group = _callbacks.new_section(op_if, CondClause::ELIF); + + if (group) { + _group_stack.push(group); + visit(elif_ctx->condition()); + visit(elif_ctx->block()); + _group_stack.pop(); + } + } + } + + if (ctx->elseClause()) { + if (_callbacks.new_section && op_if) { + void *group = _callbacks.new_section(op_if, CondClause::ELSE); + + if (group) { + _group_stack.push(group); + visit(ctx->elseClause()->block()); + _group_stack.pop(); + } + } + } + + _if_stack.pop(); + + if (op_if) { + add_operator_to_current(op_if); + } + } + } + + return {}; +} + +std::any +HRW4UVisitorImpl::visitIfStatement(hrw4uParser::IfStatementContext *ctx) +{ + void *op_if = nullptr; + + if (_callbacks.create_if_operator) { + op_if = _callbacks.create_if_operator(); + if (op_if) { + track_object(op_if, "operator_if"); + } + } + + IfBlockState state{.op_if = op_if, .clause_index = 0}; + + _if_stack.push(state); + + visit(ctx->condition()); + visit(ctx->block()); + + _if_stack.pop(); + if (op_if) { + add_operator_to_current(op_if); + } + + return {}; +} + +std::any +HRW4UVisitorImpl::visitElifClause(hrw4uParser::ElifClauseContext *ctx) +{ + visit(ctx->condition()); + visit(ctx->block()); + return {}; +} + +std::any +HRW4UVisitorImpl::visitElseClause(hrw4uParser::ElseClauseContext *ctx) +{ + visit(ctx->block()); + return {}; +} + +std::any +HRW4UVisitorImpl::visitBlock(hrw4uParser::BlockContext *ctx) +{ + for (auto *item : ctx->blockItem()) { + if (item->commentLine()) { + continue; + } else if (item->statement()) { + visit(item->statement()); + } else if (item->conditional()) { + visit(item->conditional()); + } + } + return {}; +} + +std::any +HRW4UVisitorImpl::visitCondition(hrw4uParser::ConditionContext *ctx) +{ + _cond_state.reset(); + process_expression(ctx->expression(), true, false); + return {}; +} + +void +HRW4UVisitorImpl::process_expression(hrw4uParser::ExpressionContext *ctx, bool last, bool followed_by_or) +{ + if (ctx->OR()) { + process_expression(ctx->expression(), false, true); + process_term(ctx->term(), last, followed_by_or); + } else { + process_term(ctx->term(), last, followed_by_or); + } +} + +void +HRW4UVisitorImpl::process_term(hrw4uParser::TermContext *ctx, bool last, bool followed_by_or) +{ + if (ctx->AND()) { + process_term(ctx->term(), false, false); + process_factor(ctx->factor(), last, followed_by_or); + } else { + process_factor(ctx->factor(), last, followed_by_or); + } +} + +void +HRW4UVisitorImpl::process_factor(hrw4uParser::FactorContext *ctx, bool last, bool followed_by_or, bool negated) +{ + if (ctx->children.size() == 2 && ctx->children[0]->getText() == "!") { + auto *inner_factor = dynamic_cast(ctx->children[1]); + + if (inner_factor) { + process_factor(inner_factor, last, followed_by_or, !negated); + return; + } + } + + if (ctx->LPAREN()) { + ParserContext pctx_group = build_parser_context("%{GROUP}"); + void *group = create_condition(pctx_group); + + if (!group) { + return; + } + + add_condition_to_current(group); + _cond_state.reset(); + _group_stack.push(group); + process_expression(ctx->expression(), true, false); + _group_stack.pop(); + + return; + } + + if (ctx->comparison()) { + if (!last) { + if (followed_by_or) { + _cond_state.or_modifier = true; + } else { + _cond_state.add_modifier("AND"); + } + } + + void *cond = process_comparison(ctx->comparison(), negated); + + if (cond) { + add_condition_to_current(cond); + } + return; + } + + if (ctx->functionCall()) { + if (!last) { + if (followed_by_or) { + _cond_state.or_modifier = true; + } else { + _cond_state.add_modifier("AND"); + } + } + + void *cond = process_function_condition(ctx->functionCall(), negated); + + if (cond) { + add_condition_to_current(cond); + } + return; + } + + if (ctx->TRUE()) { + _cond_state.not_modifier = negated; + + if (!last) { + if (followed_by_or) { + _cond_state.or_modifier = true; + } else { + _cond_state.add_modifier("AND"); + } + } + + ParserContext pctx = build_parser_context("TRUE"); + void *cond = create_condition(pctx); + + if (cond) { + add_condition_to_current(cond); + } + _cond_state.reset(); + return; + } + + if (ctx->FALSE()) { + _cond_state.not_modifier = negated; + + if (!last) { + if (followed_by_or) { + _cond_state.or_modifier = true; + } else { + _cond_state.add_modifier("AND"); + } + } + + ParserContext pctx = build_parser_context("FALSE"); + void *cond = create_condition(pctx); + + if (cond) { + add_condition_to_current(cond); + } + _cond_state.reset(); + return; + } + + if (ctx->ident) { + if (!last) { + if (followed_by_or) { + _cond_state.or_modifier = true; + } else { + _cond_state.add_modifier("AND"); + } + } + + void *cond = process_identifier_condition(ctx->ident->getText(), negated); + + if (cond) { + add_condition_to_current(cond); + } + return; + } +} + +void * +HRW4UVisitorImpl::process_comparison(hrw4uParser::ComparisonContext *ctx, bool negated) +{ + auto *comp = ctx->comparable(); + + if (!comp) { + add_error(ctx, "Missing comparable in comparison"); + return nullptr; + } + + std::string lhs_resolved; + hrw::ConditionType cond_type = hrw::ConditionType::NONE; + + if (comp->ident) { + auto result = resolve_identifier(comp->ident->getText()); + std::string ident = comp->ident->getText(); + + if (!result.success) { + add_error(ctx, "Unknown condition symbol: " + ident); + return nullptr; + } + lhs_resolved = result.target; + cond_type = result.cond_type; + if (!result.suffix.empty()) { + lhs_resolved += ":" + result.suffix; + } + } else if (comp->functionCall()) { + if (!comp->functionCall()->funcName) { + add_error(ctx, "Missing function name in comparison"); + return nullptr; + } + + std::string func_name = comp->functionCall()->funcName->getText(); + std::vector func_args; + + if (comp->functionCall()->argumentList()) { + for (auto *val : comp->functionCall()->argumentList()->value()) { + func_args.push_back(extract_value_string(val)); + } + } + + auto result = _resolver.resolve_function(func_name, _current_section); + + if (!result.success) { + add_error(ctx, "Unknown function in comparison: " + func_name); + return nullptr; + } + + lhs_resolved = result.target; + cond_type = result.cond_type; + if (!func_args.empty()) { + lhs_resolved += ":" + func_args[0]; + for (size_t i = 1; i < func_args.size(); ++i) { + lhs_resolved += "," + func_args[i]; + } + } + } else { + add_error(ctx, "Invalid comparable"); + return nullptr; + } + + std::string op = lhs_resolved; + std::string arg; + + if (ctx->children.size() < 2) { + add_error(ctx, "Missing operator in comparison"); + return nullptr; + } + + std::string op_text = ctx->children[1]->getText(); + bool is_negated = negated; + + if (op_text == "!=" || op_text == "!~") { + is_negated = !is_negated; + } + + if (ctx->value()) { + std::string rhs = extract_value_string(ctx->value()); + + if (op_text == "==" || op_text == "=" || op_text == "!=") { + arg = "=" + rhs; + } else if (op_text == ">" || op_text == "<") { + arg = op_text + rhs; + } + } else if (ctx->regex()) { + arg = ctx->regex()->getText(); + } else if (ctx->set()) { + std::string set_text = ctx->set()->getText(); + + if (!set_text.empty() && set_text[0] == '[') { + set_text[0] = '('; + set_text[set_text.length() - 1] = ')'; + } + arg = set_text; + } else if (ctx->iprange()) { + arg = ctx->iprange()->getText(); + } + + if (ctx->modifier()) { + extract_modifiers(ctx->modifier()); + } + + _cond_state.not_modifier = is_negated; + + ParserContext pctx = build_parser_context(op, arg); + pctx.cond_type = cond_type; + void *cond = create_condition(pctx); + + _cond_state.reset(); + return cond; +} + +void * +HRW4UVisitorImpl::process_function_condition(hrw4uParser::FunctionCallContext *ctx, bool negated) +{ + if (!ctx->funcName) { + add_error(ctx, "Missing function name"); + return nullptr; + } + + std::string func_name = ctx->funcName->getText(); + std::vector args; + + if (ctx->argumentList()) { + for (auto *val : ctx->argumentList()->value()) { + args.push_back(extract_value_string(val)); + } + } + + auto result = _resolver.resolve_function(func_name, _current_section); + + if (!result.success) { + add_error(ctx, "Unknown function: " + func_name); + return nullptr; + } + + std::string op = result.target; + + if (!args.empty()) { + op += ":" + args[0]; + for (size_t i = 1; i < args.size(); ++i) { + op += "," + args[i]; + } + } + + _cond_state.not_modifier = negated; + + ParserContext pctx = build_parser_context(op); + pctx.cond_type = result.cond_type; + void *cond = create_condition(pctx); + + _cond_state.reset(); + + return cond; +} + +void * +HRW4UVisitorImpl::process_identifier_condition(const std::string &ident, bool negated) +{ + auto result = resolve_identifier(ident); + + if (!result.success) { + add_error("Cannot resolve identifier: " + ident + " - " + result.error_message); + return nullptr; + } + + std::string op = result.target; + std::string arg; + + if (!result.suffix.empty()) { + op += ":" + result.suffix; + } + + bool actual_negation = negated; + + if (result.prefix) { + arg = "=\"\""; + actual_negation = !negated; + } + + _cond_state.not_modifier = actual_negation; + + ParserContext pctx = build_parser_context(op, arg); + pctx.cond_type = result.cond_type; + void *cond = create_condition(pctx); + + _cond_state.reset(); + + return cond; +} + +std::any +HRW4UVisitorImpl::visitExpression(hrw4uParser::ExpressionContext *) +{ + return {}; +} + +std::any +HRW4UVisitorImpl::visitTerm(hrw4uParser::TermContext *) +{ + return {}; +} + +std::any +HRW4UVisitorImpl::visitFactor(hrw4uParser::FactorContext *) +{ + return {}; +} + +std::any +HRW4UVisitorImpl::visitComparison(hrw4uParser::ComparisonContext *) +{ + return {}; +} + +std::any +HRW4UVisitorImpl::visitFunctionCall(hrw4uParser::FunctionCallContext *) +{ + return std::string{}; +} + +std::any +HRW4UVisitorImpl::visitValue(hrw4uParser::ValueContext *ctx) +{ + return extract_value_string(ctx); +} + +std::any +HRW4UVisitorImpl::visitModifier(hrw4uParser::ModifierContext *ctx) +{ + extract_modifiers(ctx); + return {}; +} + +void +HRW4UErrorListener::syntaxError(antlr4::Recognizer *recognizer, antlr4::Token *offendingSymbol, size_t line, + size_t charPositionInLine, const std::string &msg, std::exception_ptr) +{ + ParseError error; + + error.message = msg; + error.location.filename = std::string(_filename); + error.location.line = line; + error.location.column = charPositionInLine; + + // Try to get the source line for context + try { + antlr4::CharStream *input = nullptr; + + if (auto *lexer = dynamic_cast(recognizer)) { + input = lexer->getInputStream(); + } else if (auto *parser = dynamic_cast(recognizer)) { + auto *tokenStream = parser->getTokenStream(); + + if (tokenStream && tokenStream->getTokenSource()) { + input = tokenStream->getTokenSource()->getInputStream(); + } + } + + if (input) { + std::string text = input->toString(); + size_t pos = 0; + size_t current_line = 1; + + // Find the start of the requested line + while (current_line < line && pos < text.size()) { + if (text[pos] == '\n') { + ++current_line; + } + ++pos; + } + + size_t line_start = pos; + + while (pos < text.size() && text[pos] != '\n') { + ++pos; + } + error.location.context = text.substr(line_start, pos - line_start); + } + } catch (...) { + if (offendingSymbol) { + error.location.context = offendingSymbol->getText(); + } + } + + _errors.add_error(error); +} + +HRW4UVisitor::HRW4UVisitor(const FactoryCallbacks &callbacks, const ParserConfig &config) + : _impl(std::make_unique(callbacks, config)) +{ +} + +HRW4UVisitor::~HRW4UVisitor() = default; + +HRW4UVisitor::HRW4UVisitor(HRW4UVisitor &&) noexcept = default; + +HRW4UVisitor &HRW4UVisitor::operator=(HRW4UVisitor &&) noexcept = default; + +ParseResult +HRW4UVisitor::parse(std::string_view input) +{ + return _impl->parse(input); +} + +ParseResult +HRW4UVisitor::parse_file(std::string_view filename) +{ + std::string fname{filename}; + std::ifstream infile{fname}; + + if (!infile.is_open()) { + ParseResult result; + + result.success = false; + result.errors.add_error(ParseError{.message = "Cannot open file: " + fname}); + return result; + } + + std::stringstream buffer; + + buffer << infile.rdbuf(); + return parse(buffer.str()); +} + +bool +HRW4UVisitor::has_errors() const +{ + return _impl->has_errors(); +} + +const ErrorCollector & +HRW4UVisitor::errors() const +{ + return _impl->errors(); +} + +void +CondState::reset() +{ + not_modifier = false; + or_modifier = false; + and_modifier = false; + last_modifier = false; + nocase_modifier = false; + ext_modifier = false; + pre_modifier = false; +} + +void +CondState::add_modifier(std::string_view mod) +{ + if (mod == "NOT" || mod == "N") { + not_modifier = true; + } else if (mod == "OR" || mod == "O") { + or_modifier = true; + } else if (mod == "AND") { + and_modifier = true; + } else if (mod == "L" || mod == "LAST") { + last_modifier = true; + } else if (mod == "NC" || mod == "NOCASE" || mod == "I") { + nocase_modifier = true; + } else if (mod == "EXT") { + ext_modifier = true; + } else if (mod == "PRE") { + pre_modifier = true; + } +} + +std::vector +CondState::to_list() const +{ + std::vector result; + + if (not_modifier) { + result.push_back("NOT"); + } + if (or_modifier) { + result.push_back("OR"); + } + if (and_modifier) { + result.push_back("AND"); + } + if (last_modifier) { + result.push_back("L"); + } + if (nocase_modifier) { + result.push_back("NOCASE"); + } + if (ext_modifier) { + result.push_back("EXT"); + } + if (pre_modifier) { + result.push_back("PRE"); + } + return result; +} + +std::string +CondState::render_suffix() const +{ + auto mods = to_list(); + + if (mods.empty()) { + return ""; + } + + std::string result = " ["; + + for (size_t i = 0; i < mods.size(); ++i) { + if (i > 0) { + result += ","; + } + result += mods[i]; + } + result += "]"; + return result; +} + +CondState +CondState::copy() const +{ + return *this; +} + +void +OperatorState::reset() +{ + last_modifier = false; + qsa_modifier = false; + inv_modifier = false; +} + +void +OperatorState::add_modifier(std::string_view mod) +{ + if (mod == "L" || mod == "LAST") { + last_modifier = true; + } else if (mod == "QSA") { + qsa_modifier = true; + } else if (mod == "INV") { + inv_modifier = true; + } +} + +std::vector +OperatorState::to_list() const +{ + std::vector result; + + if (last_modifier) { + result.push_back("L"); + } + if (qsa_modifier) { + result.push_back("QSA"); + } + if (inv_modifier) { + result.push_back("INV"); + } + + return result; +} + +std::string +OperatorState::render_suffix() const +{ + auto mods = to_list(); + + if (mods.empty()) { + return ""; + } + + std::string result = " ["; + + for (size_t i = 0; i < mods.size(); ++i) { + if (i > 0) { + result += ","; + } + result += mods[i]; + } + result += "]"; + + return result; +} + +ModifierInfo +ModifierInfo::parse(std::string_view mod) +{ + ModifierInfo info; + + info.name = std::string(mod); + std::transform(info.name.begin(), info.name.end(), info.name.begin(), [](unsigned char c) { return std::toupper(c); }); + + if (is_condition_modifier(info.name)) { + info.type = ModifierType::CONDITION; + } else if (is_operator_modifier(info.name)) { + info.type = ModifierType::OPERATOR; + } else { + info.type = ModifierType::UNKNOWN; + } + + return info; +} + +bool +ModifierInfo::is_condition_modifier(std::string_view mod) +{ + return mod == "NOT" || mod == "N" || mod == "OR" || mod == "O" || mod == "AND" || mod == "NC" || mod == "NOCASE" || mod == "I" || + mod == "EXT" || mod == "PRE"; +} + +bool +ModifierInfo::is_operator_modifier(std::string_view mod) +{ + return mod == "L" || mod == "LAST" || mod == "QSA" || mod == "INV"; +} + +} // namespace hrw4u diff --git a/src/hrw4u/HRW4UVisitorImpl.h b/src/hrw4u/HRW4UVisitorImpl.h new file mode 100644 index 00000000000..8fc70a656a0 --- /dev/null +++ b/src/hrw4u/HRW4UVisitorImpl.h @@ -0,0 +1,162 @@ +/* + 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. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hrw4u/Types.h" +#include "hrw4u/Error.h" +#include "hrw4u/Tables.h" +#include "hrw4u/Visitor.h" +#include "hrw4u/HRW4UVisitor.h" +#include "hrw4uBaseVisitor.h" +#include "hrw4uParser.h" +#include "hrw4uLexer.h" + +namespace hrw4u +{ + +class HRW4UVisitorImpl : public hrw4uBaseVisitor +{ +public: + HRW4UVisitorImpl(const FactoryCallbacks &callbacks, const ParserConfig &config); + ~HRW4UVisitorImpl() override = default; + + HRW4UVisitorImpl(const HRW4UVisitorImpl &) = delete; + HRW4UVisitorImpl &operator=(const HRW4UVisitorImpl &) = delete; + + ParseResult parse(std::string_view input); + + [[nodiscard]] bool + has_errors() const + { + return _errors.has_errors(); + } + + [[nodiscard]] const ErrorCollector & + errors() const + { + return _errors; + } + + std::any visitProgram(hrw4uParser::ProgramContext *ctx) override; + std::any visitSection(hrw4uParser::SectionContext *ctx) override; + std::any visitVarSection(hrw4uParser::VarSectionContext *ctx) override; + std::any visitVariableDecl(hrw4uParser::VariableDeclContext *ctx) override; + std::any visitStatement(hrw4uParser::StatementContext *ctx) override; + std::any visitConditional(hrw4uParser::ConditionalContext *ctx) override; + std::any visitIfStatement(hrw4uParser::IfStatementContext *ctx) override; + std::any visitElifClause(hrw4uParser::ElifClauseContext *ctx) override; + std::any visitElseClause(hrw4uParser::ElseClauseContext *ctx) override; + std::any visitBlock(hrw4uParser::BlockContext *ctx) override; + std::any visitCondition(hrw4uParser::ConditionContext *ctx) override; + std::any visitExpression(hrw4uParser::ExpressionContext *ctx) override; + std::any visitTerm(hrw4uParser::TermContext *ctx) override; + std::any visitFactor(hrw4uParser::FactorContext *ctx) override; + std::any visitComparison(hrw4uParser::ComparisonContext *ctx) override; + std::any visitFunctionCall(hrw4uParser::FunctionCallContext *ctx) override; + std::any visitValue(hrw4uParser::ValueContext *ctx) override; + std::any visitModifier(hrw4uParser::ModifierContext *ctx) override; + +private: + struct IfBlockState { + void *op_if = nullptr; + int clause_index = 0; + }; + + void add_error(antlr4::ParserRuleContext *ctx, const std::string &message); + void add_error(const std::string &message); + SourceLocation get_location(antlr4::ParserRuleContext *ctx) const; + + void start_section(SectionType type); + void close_section(); + void *get_or_create_ruleset(); + + ParserContext build_parser_context(const std::string &op, const std::string &arg = "", const std::string &val = ""); + void *create_condition(const ParserContext &pctx); + void *create_operator(const ParserContext &pctx); + bool add_condition_to_current(void *cond); + bool add_operator_to_current(void *op); + + void process_expression(hrw4uParser::ExpressionContext *ctx, bool last, bool followed_by_or); + void process_term(hrw4uParser::TermContext *ctx, bool last, bool followed_by_or); + void process_factor(hrw4uParser::FactorContext *ctx, bool last, bool followed_by_or, bool negated = false); + void *process_comparison(hrw4uParser::ComparisonContext *ctx, bool negated); + void *process_function_condition(hrw4uParser::FunctionCallContext *ctx, bool negated); + void *process_identifier_condition(const std::string &ident, bool negated); + + void process_assignment(hrw4uParser::StatementContext *ctx, const std::string &lhs, hrw4uParser::ValueContext *value_ctx, + bool append); + void process_function_statement(hrw4uParser::FunctionCallContext *ctx); + void process_break(); + + std::string extract_value_string(hrw4uParser::ValueContext *ctx); + std::string substitute_strings(const std::string &str, antlr4::ParserRuleContext *ctx); + void extract_modifiers(hrw4uParser::ModifierContext *ctx); + std::string get_comparison_op(hrw4uParser::ComparisonContext *ctx); + void cleanup_on_error(); + void track_object(void *obj, const std::string &type); + ResolveResult resolve_identifier(const std::string &ident); + std::string get_source_line(size_t line_number) const; + + const FactoryCallbacks &_callbacks; + const ParserConfig &_config; + ErrorCollector _errors; + const SymbolResolver &_resolver; + + std::vector _rulesets; + std::vector _sections; + std::vector> _allocated_objects; + std::stack _if_stack; + std::stack _group_stack; + std::map _variables; + + CondState _cond_state; + OperatorState _oper_state; + SectionType _current_section = SectionType::UNKNOWN; + void *_current_ruleset = nullptr; + int _next_var_slot = 0; + bool _section_has_ops = false; + + std::vector _source_lines; + + static constexpr int MAX_IF_DEPTH = 10; +}; + +class HRW4UErrorListener : public antlr4::BaseErrorListener +{ +public: + HRW4UErrorListener(ErrorCollector &errors, std::string_view filename) : _errors(errors), _filename(filename) {} + + void syntaxError(antlr4::Recognizer *recognizer, antlr4::Token *offendingSymbol, size_t line, size_t charPositionInLine, + const std::string &msg, std::exception_ptr e) override; + +private: + ErrorCollector &_errors; + std::string_view _filename; +}; + +} // namespace hrw4u diff --git a/src/hrw4u/Tables.cc b/src/hrw4u/Tables.cc new file mode 100644 index 00000000000..2aadb52682b --- /dev/null +++ b/src/hrw4u/Tables.cc @@ -0,0 +1,568 @@ +/* + 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. +*/ + +#include "hrw4u/Tables.h" +#include + +namespace hrw4u +{ +hrw::OperatorType +ResolveResult::get_operator_type(bool is_append, bool is_remove) const +{ + if (op_prefix == OperatorPrefix::NONE) { + return op_type; + } + + if (is_remove) { + if (op_type == hrw::OperatorType::SET_HEADER || op_type == hrw::OperatorType::ADD_HEADER) { + return hrw::OperatorType::RM_HEADER; + } + if (op_type == hrw::OperatorType::SET_COOKIE || op_type == hrw::OperatorType::ADD_COOKIE) { + return hrw::OperatorType::RM_COOKIE; + } + if (op_type == hrw::OperatorType::SET_DESTINATION) { + return hrw::OperatorType::RM_DESTINATION; + } + } + + if (is_append) { + if (op_type == hrw::OperatorType::SET_HEADER) { + return hrw::OperatorType::ADD_HEADER; + } + if (op_type == hrw::OperatorType::SET_COOKIE) { + return hrw::OperatorType::ADD_COOKIE; + } + } + + return op_type; +} + +namespace +{ + std::string + generate_condition_target(hrw::ConditionType type, bool wrap_in_braces = true) + { + auto name = hrw::condition_type_name(type); + if (name.empty()) { + return ""; + } + if (wrap_in_braces) { + return "%{" + std::string(name) + "}"; + } + return std::string(name); + } + + // HTTP_SECTIONS: All hooks where HTTP transaction data is available (excludes TXN_START/TXN_CLOSE) + const SectionSet HTTP_SECTIONS = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, + SectionType::SEND_REQUEST, SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}; + +} // namespace + +SymbolResolver::SymbolResolver() +{ + _operator_map = { + {"http.cntl.", + {.sections = HTTP_SECTIONS, + .upper = true, + .suffix_group = SuffixGroup::HTTP_CNTL_FIELDS, + .has_suffix_validation = true, + .op_prefix = OperatorPrefix::NONE, + .op_type = hrw::OperatorType::SET_HTTP_CNTL} }, + {"http.status.reason", + {.sections = HTTP_SECTIONS, .op_prefix = OperatorPrefix::NONE, .op_type = hrw::OperatorType::SET_STATUS_REASON} }, + {"http.status", {.sections = HTTP_SECTIONS, .op_prefix = OperatorPrefix::NONE, .op_type = hrw::OperatorType::SET_STATUS}}, + {"inbound.conn.dscp", + {.sections = HTTP_SECTIONS, .op_prefix = OperatorPrefix::NONE, .op_type = hrw::OperatorType::SET_CONN_DSCP} }, + {"inbound.conn.mark", + {.sections = HTTP_SECTIONS, .op_prefix = OperatorPrefix::NONE, .op_type = hrw::OperatorType::SET_CONN_MARK} }, + {"outbound.conn.dscp", + {.sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST}, + .op_prefix = OperatorPrefix::NONE, + .op_type = hrw::OperatorType::SET_CONN_DSCP} }, + {"outbound.conn.mark", + {.sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST}, + .op_prefix = OperatorPrefix::NONE, + .op_type = hrw::OperatorType::SET_CONN_MARK} }, + {"inbound.cookie.", + {.sections = HTTP_SECTIONS, .op_prefix = OperatorPrefix::SET_ADD_RM, .op_type = hrw::OperatorType::SET_COOKIE} }, + {"inbound.req.", + {.sections = HTTP_SECTIONS, .op_prefix = OperatorPrefix::SET_ADD_RM, .op_type = hrw::OperatorType::SET_HEADER} }, + {"inbound.resp.body", {.sections = HTTP_SECTIONS, .op_prefix = OperatorPrefix::NONE, .op_type = hrw::OperatorType::SET_BODY} }, + {"inbound.resp.", + {.sections = {SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE, SectionType::TXN_CLOSE}, + .op_prefix = OperatorPrefix::SET_ADD_RM, + .op_type = hrw::OperatorType::SET_HEADER} }, + {"inbound.status.reason", + {.sections = HTTP_SECTIONS, .op_prefix = OperatorPrefix::NONE, .op_type = hrw::OperatorType::SET_STATUS_REASON} }, + {"inbound.status", {.sections = HTTP_SECTIONS, .op_prefix = OperatorPrefix::NONE, .op_type = hrw::OperatorType::SET_STATUS}}, + {"inbound.url.", + {.sections = HTTP_SECTIONS, + .upper = true, + .suffix_group = SuffixGroup::URL_FIELDS, + .has_suffix_validation = true, + .op_prefix = OperatorPrefix::SET_RM, + .op_type = hrw::OperatorType::SET_DESTINATION} }, + {"outbound.cookie.", + {.sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .op_prefix = OperatorPrefix::SET_ADD_RM, + .op_type = hrw::OperatorType::SET_COOKIE} }, + {"outbound.req.", + {.sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST}, + .op_prefix = OperatorPrefix::SET_ADD_RM, + .op_type = hrw::OperatorType::SET_HEADER} }, + {"outbound.resp.", + {.sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .op_prefix = OperatorPrefix::SET_ADD_RM, + .op_type = hrw::OperatorType::SET_HEADER} }, + {"outbound.status.reason", + {.sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .op_prefix = OperatorPrefix::NONE, + .op_type = hrw::OperatorType::SET_STATUS_REASON} }, + {"outbound.status", + {.sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .op_prefix = OperatorPrefix::NONE, + .op_type = hrw::OperatorType::SET_STATUS} }, + {"outbound.url.", + {.upper = true, + .suffix_group = SuffixGroup::URL_FIELDS, + .has_suffix_validation = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST}, + .op_prefix = OperatorPrefix::SET_RM, + .op_type = hrw::OperatorType::SET_DESTINATION} }, + }; + + _condition_map = { + {"inbound.ip", {.target = "%{IP:CLIENT}", .cond_type = hrw::ConditionType::COND_IP} }, + {"inbound.method", {.sections = HTTP_SECTIONS, .cond_type = hrw::ConditionType::COND_METHOD} }, + {"inbound.server", {.target = "%{IP:INBOUND}", .cond_type = hrw::ConditionType::COND_IP} }, + {"inbound.status", {.sections = HTTP_SECTIONS, .cond_type = hrw::ConditionType::COND_STATUS} }, + {"now", {.cond_type = hrw::ConditionType::COND_NOW} }, + {"outbound.ip", + {.target = "%{IP:SERVER}", + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_IP} }, + {"outbound.method", + {.sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST}, + .cond_type = hrw::ConditionType::COND_METHOD} }, + {"outbound.server", + {.target = "%{IP:OUTBOUND}", + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_IP} }, + {"outbound.status", + {.sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_STATUS} }, + {"tcp.info", {.cond_type = hrw::ConditionType::COND_TCP_INFO} }, + {"capture.", {.prefix = true, .cond_type = hrw::ConditionType::COND_LAST_CAPTURE} }, + {"from.url.", + {.sections = HTTP_SECTIONS, + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::URL_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_FROM_URL} }, + {"geo.", + {.upper = true, + .prefix = true, + .suffix_group = SuffixGroup::GEO_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_GEO} }, + {"http.cntl.", + {.sections = HTTP_SECTIONS, + .upper = true, + .suffix_group = SuffixGroup::HTTP_CNTL_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_HTTP_CNTL} }, + {"id.", + {.upper = true, + .suffix_group = SuffixGroup::ID_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_ID} }, + {"inbound.conn.client-cert.SAN.", + {.target = "INBOUND:CLIENT-CERT:SAN", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::SAN_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"inbound.conn.server-cert.SAN.", + {.target = "INBOUND:SERVER-CERT:SAN", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::SAN_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"inbound.conn.client-cert.san.", + {.target = "INBOUND:CLIENT-CERT:SAN", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::SAN_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"inbound.conn.server-cert.san.", + {.target = "INBOUND:SERVER-CERT:SAN", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::SAN_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"inbound.conn.client-cert.", + {.target = "INBOUND:CLIENT-CERT", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::CERT_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"inbound.conn.server-cert.", + {.target = "INBOUND:SERVER-CERT", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::CERT_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"inbound.conn.", + {.target = "INBOUND", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::CONN_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"inbound.cookie.", {.sections = HTTP_SECTIONS, .prefix = true, .cond_type = hrw::ConditionType::COND_COOKIE} }, + {"inbound.req.", {.sections = HTTP_SECTIONS, .prefix = true, .cond_type = hrw::ConditionType::COND_CLIENT_HEADER}}, + {"inbound.resp.", + {.sections = {SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE, SectionType::TXN_CLOSE}, + .prefix = true, + .cond_type = hrw::ConditionType::COND_HEADER} }, + {"inbound.url.", + {.sections = HTTP_SECTIONS, + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::URL_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_CLIENT_URL} }, + {"now.", + {.upper = true, + .suffix_group = SuffixGroup::DATE_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_NOW} }, + {"outbound.conn.client-cert.SAN.", + {.target = "OUTBOUND:CLIENT-CERT:SAN", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::SAN_FIELDS, + .has_suffix_validation = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"outbound.conn.server-cert.SAN.", + {.target = "OUTBOUND:SERVER-CERT:SAN", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::SAN_FIELDS, + .has_suffix_validation = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"outbound.conn.client-cert.san.", + {.target = "OUTBOUND:CLIENT-CERT:SAN", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::SAN_FIELDS, + .has_suffix_validation = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"outbound.conn.server-cert.san.", + {.target = "OUTBOUND:SERVER-CERT:SAN", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::SAN_FIELDS, + .has_suffix_validation = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"outbound.conn.client-cert.", + {.target = "OUTBOUND:CLIENT-CERT", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::CERT_FIELDS, + .has_suffix_validation = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"outbound.conn.server-cert.", + {.target = "OUTBOUND:SERVER-CERT", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::CERT_FIELDS, + .has_suffix_validation = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"outbound.conn.", + {.target = "OUTBOUND", + .upper = true, + .prefix = true, + .suffix_group = SuffixGroup::CONN_FIELDS, + .has_suffix_validation = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_INBOUND} }, + {"outbound.cookie.", + {.prefix = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_COOKIE} }, + {"outbound.req.", + {.prefix = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST}, + .cond_type = hrw::ConditionType::COND_HEADER} }, + {"outbound.resp.", + {.prefix = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST, + SectionType::READ_RESPONSE, SectionType::SEND_RESPONSE}, + .cond_type = hrw::ConditionType::COND_HEADER} }, + {"outbound.url.", + {.upper = true, + .prefix = true, + .suffix_group = SuffixGroup::URL_FIELDS, + .has_suffix_validation = true, + .sections = {SectionType::PRE_REMAP, SectionType::REMAP, SectionType::READ_REQUEST, SectionType::SEND_REQUEST}, + .cond_type = hrw::ConditionType::COND_NEXT_HOP} }, + {"to.url.", + {.upper = true, + .prefix = true, + .suffix_group = SuffixGroup::URL_FIELDS, + .has_suffix_validation = true, + .cond_type = hrw::ConditionType::COND_TO_URL} }, + }; + + _function_map = { + {"access", {.bare = true, .cond_type = hrw::ConditionType::COND_ACCESS} }, + {"cache", {.bare = true, .cond_type = hrw::ConditionType::COND_CACHE} }, + {"cidr", {.bare = true, .cond_type = hrw::ConditionType::COND_CIDR} }, + {"internal", {.bare = true, .cond_type = hrw::ConditionType::COND_INTERNAL_TXN} }, + {"random", {.bare = true, .cond_type = hrw::ConditionType::COND_RANDOM} }, + {"ssn-txn-count", {.bare = true, .cond_type = hrw::ConditionType::COND_SESSION_TRANSACT_COUNT}}, + {"txn-count", {.bare = true, .cond_type = hrw::ConditionType::COND_TRANSACT_COUNT} }, + }; + + _statement_function_map = { + {"add-header", {.sections = HTTP_SECTIONS, .op_type = hrw::OperatorType::ADD_HEADER} }, + {"counter", {.op_type = hrw::OperatorType::COUNTER} }, + {"set-debug", {.op_type = hrw::OperatorType::SET_DEBUG} }, + {"no-op", {.op_type = hrw::OperatorType::NO_OP} }, + {"remove_query", {.target = "QUERY", .sections = HTTP_SECTIONS, .op_type = hrw::OperatorType::RM_DESTINATION} }, + {"keep_query", {.target = "QUERY", .sections = HTTP_SECTIONS, .op_type = hrw::OperatorType::RM_DESTINATION} }, + {"run-plugin", {.sections = HTTP_SECTIONS, .op_type = hrw::OperatorType::RUN_PLUGIN} }, + {"set-body-from", {.sections = HTTP_SECTIONS, .op_type = hrw::OperatorType::SET_BODY_FROM} }, + {"set-cc-alg", {.sections = HTTP_SECTIONS, .op_type = hrw::OperatorType::SET_CC_ALG} }, + {"set-config", {.sections = HTTP_SECTIONS, .op_type = hrw::OperatorType::SET_CONFIG} }, + {"set-effective-address", {.sections = HTTP_SECTIONS, .op_type = hrw::OperatorType::SET_EFFECTIVE_ADDRESS} }, + {"set-redirect", {.sections = HTTP_SECTIONS, .op_type = hrw::OperatorType::SET_REDIRECT} }, + {"skip-remap", {.sections = {SectionType::PRE_REMAP, SectionType::READ_REQUEST}, .op_type = hrw::OperatorType::SKIP_REMAP}}, + {"set-plugin-cntl", {.sections = HTTP_SECTIONS, .op_type = hrw::OperatorType::SET_PLUGIN_CNTL} }, + }; + + _hook_map = { + {"read_request", SectionType::READ_REQUEST }, + {"send_request", SectionType::SEND_REQUEST }, + {"read_response", SectionType::READ_RESPONSE}, + {"send_response", SectionType::SEND_RESPONSE}, + {"pre_remap", SectionType::PRE_REMAP }, + {"post_remap", SectionType::POST_REMAP }, + {"remap", SectionType::REMAP }, + {"txn_start", SectionType::TXN_START }, + {"txn_close", SectionType::TXN_CLOSE }, + }; + + _var_type_map = { + {"bool", VarType::BOOL }, + {"boolean", VarType::BOOL }, + {"int8", VarType::INT8 }, + {"int16", VarType::INT16}, + }; +} + +ResolveResult +SymbolResolver::resolve_in_table(std::string_view symbol, const std::unordered_map &table, + SectionType section) const +{ + ResolveResult result; + std::string symbol_str(symbol); + auto it = table.find(symbol_str); + + if (it != table.end()) { + if (!it->second.valid_for_section(section)) { + result.error_message = "symbol '" + symbol_str + "' not valid in section " + std::string(section_type_to_string(section)); + return result; + } + if (it->second.target.empty()) { + if (it->second.cond_type != hrw::ConditionType::NONE) { + result.target = generate_condition_target(it->second.cond_type, !it->second.bare); + } + } else { + result.target = it->second.target; + } + result.op_prefix = it->second.op_prefix; + result.cond_type = it->second.cond_type; + result.op_type = it->second.op_type; + result.success = true; + + return result; + } + + std::string_view longest_prefix; + const MapParams *longest_params = nullptr; + + for (const auto &[prefix, params] : table) { + if (prefix.back() == '.' && symbol.size() >= prefix.size() && symbol.substr(0, prefix.size()) == prefix) { + if (prefix.size() > longest_prefix.size()) { + longest_prefix = prefix; + longest_params = ¶ms; + } + } + } + + if (longest_params) { + if (!longest_params->valid_for_section(section)) { + result.error_message = "symbol '" + symbol_str + "' not valid in section " + std::string(section_type_to_string(section)); + + return result; + } + + std::string suffix(symbol.substr(longest_prefix.size())); + + if (longest_params->has_suffix_validation && !validate_suffix(longest_params->suffix_group, suffix)) { + result.error_message = "invalid suffix '" + suffix + "' for " + std::string(longest_prefix); + + return result; + } + + if (longest_params->upper) { + suffix = to_upper(suffix); + } + + if (longest_params->target.empty()) { + if (longest_params->cond_type != hrw::ConditionType::NONE) { + result.target = generate_condition_target(longest_params->cond_type, false); + } + } else { + result.target = longest_params->target; + } + result.suffix = suffix; + result.op_prefix = longest_params->op_prefix; + result.cond_type = longest_params->cond_type; + result.op_type = longest_params->op_type; + result.prefix = longest_params->prefix; + result.success = true; + return result; + } + result.error_message = "unknown symbol: " + symbol_str; + + return result; +} + +ResolveResult +SymbolResolver::resolve_operator(std::string_view symbol, SectionType section) const +{ + return resolve_in_table(symbol, _operator_map, section); +} + +ResolveResult +SymbolResolver::resolve_condition(std::string_view symbol, SectionType section) const +{ + return resolve_in_table(symbol, _condition_map, section); +} + +ResolveResult +SymbolResolver::resolve_function(std::string_view name, SectionType section) const +{ + return resolve_in_table(name, _function_map, section); +} + +ResolveResult +SymbolResolver::resolve_statement_function(std::string_view name, SectionType section) const +{ + return resolve_in_table(name, _statement_function_map, section); +} + +std::optional +SymbolResolver::resolve_hook(std::string_view name) const +{ + auto it = _hook_map.find(to_lower(name)); + + if (it != _hook_map.end()) { + return it->second; + } + + return std::nullopt; +} + +std::optional +SymbolResolver::resolve_var_type(std::string_view name) const +{ + auto it = _var_type_map.find(to_lower(name)); + + if (it != _var_type_map.end()) { + return it->second; + } + + return std::nullopt; +} + +const MapParams * +SymbolResolver::get_operator_params(std::string_view prefix) const +{ + std::string key(prefix); + auto it = _operator_map.find(key); + + if (it != _operator_map.end()) { + return &it->second; + } + + return nullptr; +} + +const MapParams * +SymbolResolver::get_condition_params(std::string_view prefix) const +{ + std::string key(prefix); + auto it = _condition_map.find(key); + + if (it != _condition_map.end()) { + return &it->second; + } + + return nullptr; +} + +const SymbolResolver & +symbol_resolver() +{ + static SymbolResolver instance; + return instance; +} + +} // namespace hrw4u diff --git a/src/hrw4u/Types.cc b/src/hrw4u/Types.cc new file mode 100644 index 00000000000..be2e86079a9 --- /dev/null +++ b/src/hrw4u/Types.cc @@ -0,0 +1,186 @@ +/* + 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. +*/ + +#include "hrw4u/Types.h" + +#include +#include +#include + +namespace hrw4u +{ + +std::string_view +section_type_to_string(SectionType type) +{ + switch (type) { + case SectionType::UNKNOWN: + return "UNKNOWN"; + case SectionType::READ_REQUEST: + return "read_request"; + case SectionType::SEND_REQUEST: + return "send_request"; + case SectionType::READ_RESPONSE: + return "read_response"; + case SectionType::SEND_RESPONSE: + return "send_response"; + case SectionType::PRE_REMAP: + return "pre_remap"; + case SectionType::POST_REMAP: + return "post_remap"; + case SectionType::REMAP: + return "remap"; + case SectionType::TXN_START: + return "txn_start"; + case SectionType::TXN_CLOSE: + return "txn_close"; + } + return "UNKNOWN"; +} + +SectionType +section_type_from_string(std::string_view name) +{ + static const std::unordered_map map = { + {"read_request", SectionType::READ_REQUEST }, + {"send_request", SectionType::SEND_REQUEST }, + {"read_response", SectionType::READ_RESPONSE}, + {"send_response", SectionType::SEND_RESPONSE}, + {"pre_remap", SectionType::PRE_REMAP }, + {"post_remap", SectionType::POST_REMAP }, + {"remap", SectionType::REMAP }, + {"txn_start", SectionType::TXN_START }, + {"txn_close", SectionType::TXN_CLOSE }, + }; + + auto it = map.find(to_lower(name)); + + if (it != map.end()) { + return it->second; + } + + return SectionType::UNKNOWN; +} + +const VarTypeInfo & +var_type_info(VarType type) +{ + static const VarTypeInfo info_table[] = { + {"bool", "FLAG", "set-state-flag", hrw::OperatorType::SET_STATE_FLAG, 16}, + {"int8", "INT8", "set-state-int8", hrw::OperatorType::SET_STATE_INT8, 4 }, + {"int16", "INT16", "set-state-int16", hrw::OperatorType::SET_STATE_INT16, 1 }, + }; + + return info_table[static_cast(type)]; +} + +std::string_view +var_type_to_string(VarType type) +{ + return var_type_info(type).name; +} + +std::optional +var_type_from_string(std::string_view name) +{ + static const std::unordered_map map = { + {"bool", VarType::BOOL }, + {"boolean", VarType::BOOL }, + {"int8", VarType::INT8 }, + {"int16", VarType::INT16}, + }; + + auto it = map.find(to_lower(name)); + if (it != map.end()) { + return it->second; + } + return std::nullopt; +} + +namespace +{ + + const std::vector URL_FIELDS_VEC = {"host", "port", "path", "query", "scheme", "url"}; + + const std::vector HTTP_CNTL_FIELDS_VEC = { + "logging", "intercept_retry", "resp_cacheable", "req_cacheable", "server_no_store", "txn_debug", "skip_remap"}; + + const std::vector CONN_FIELDS_VEC = {"dscp", "mark", "local-addr", "remote-addr", "local-port", "remote-port", + "tls", "h2", "ipv4", "ipv6", "ip-family", "stack"}; + + const std::vector GEO_FIELDS_VEC = {"country", "country-iso", "asn", "asn-name"}; + + const std::vector ID_FIELDS_VEC = {"unique", "process", "request", "uuid"}; + + const std::vector DATE_FIELDS_VEC = {"year", "month", "day", "hour", "minute", "second", "weekday", "yearday"}; + + const std::vector CERT_FIELDS_VEC = {"subject", "issuer", "serial", "signature", + "notbefore", "notafter", "pem"}; + + const std::vector SAN_FIELDS_VEC = {"dns", "uri", "email", "ip"}; + + const std::vector BOOL_FIELDS_VEC = {"true", "false"}; + + const std::vector PLUGIN_CNTL_FIELDS_VEC = { + "request-enable", "response-enable", "tls-tunnel", "websocket", "h2", "negative-reval", "non-reval", "req-read", "cntl"}; + + const std::vector EMPTY_VEC; + +} // namespace + +bool +validate_suffix(SuffixGroup group, std::string_view suffix) +{ + const auto &valid = get_valid_suffixes(group); + + if (valid.empty()) { + return true; + } + + return std::find(valid.begin(), valid.end(), to_lower(suffix)) != valid.end(); +} + +const std::vector & +get_valid_suffixes(SuffixGroup group) +{ + switch (group) { + case SuffixGroup::URL_FIELDS: + return URL_FIELDS_VEC; + case SuffixGroup::HTTP_CNTL_FIELDS: + return HTTP_CNTL_FIELDS_VEC; + case SuffixGroup::CONN_FIELDS: + return CONN_FIELDS_VEC; + case SuffixGroup::GEO_FIELDS: + return GEO_FIELDS_VEC; + case SuffixGroup::ID_FIELDS: + return ID_FIELDS_VEC; + case SuffixGroup::DATE_FIELDS: + return DATE_FIELDS_VEC; + case SuffixGroup::CERT_FIELDS: + return CERT_FIELDS_VEC; + case SuffixGroup::SAN_FIELDS: + return SAN_FIELDS_VEC; + case SuffixGroup::BOOL_FIELDS: + return BOOL_FIELDS_VEC; + case SuffixGroup::PLUGIN_CNTL_FIELDS: + return PLUGIN_CNTL_FIELDS_VEC; + } + return EMPTY_VEC; +} + +} // namespace hrw4u diff --git a/src/hrw4u/Visitor.cc b/src/hrw4u/Visitor.cc new file mode 100644 index 00000000000..de4771c49e8 --- /dev/null +++ b/src/hrw4u/Visitor.cc @@ -0,0 +1,92 @@ +/* + 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. +*/ + +#include "hrw4u/Visitor.h" +#include "hrw4u/HRW4UVisitor.h" +#include "hrw4u/Tables.h" +#include +#include +#include + +namespace hrw4u +{ + +bool +ParserContext::consume_mod(const std::string &m) +{ + auto it = std::find(mods.begin(), mods.end(), m); + if (it != mods.end()) { + mods.erase(it); + return true; + } + return false; +} + +bool +ParserContext::validate_mods() const +{ + return mods.empty(); +} + +void +ParseResult::cleanup(const DestroyCallback &destroy) +{ + if (!destroy) { + return; + } + for (void *rs : rulesets) { + if (rs) { + destroy(rs, "ruleset"); + } + } + rulesets.clear(); + sections.clear(); +} + +ParseResult +parse_hrw4u(std::string_view input, const FactoryCallbacks &callbacks, const ParserConfig &config) +{ + ParseResult result; + + if (!callbacks.valid()) { + result.errors.error("Invalid factory callbacks provided"); + return result; + } + + if (input.empty()) { + result.errors.error("Empty input"); + return result; + } + + HRW4UVisitor visitor(callbacks, config); + + result.errors.set_filename(config.filename); + + return visitor.parse(input); +} + +ParseResult +parse_hrw4u_file(std::string_view filename, const FactoryCallbacks &callbacks, ParserConfig config) +{ + config.filename = std::string(filename); + + HRW4UVisitor visitor(callbacks, config); + return visitor.parse_file(filename); +} + +} // namespace hrw4u diff --git a/tools/hrw4u/scripts/u4wrh b/tools/hrw4u/scripts/u4wrh index 129552d7e71..1db06ad9599 100755 --- a/tools/hrw4u/scripts/u4wrh +++ b/tools/hrw4u/scripts/u4wrh @@ -32,6 +32,10 @@ def main() -> None: # Argument parsing output_group.add_argument("--hrw4u", action="store_true", help="Produce reconstructed hrw4u output (default)") parser.add_argument("--no-comments", action="store_true", help="Skip comment preservation (ignore comments in output)") + parser.add_argument( + "--no-merge-sections", + action="store_true", + help="Disable section merging (create separate sections for each cond directive)") args = parser.parse_args() # Default to hrw4u output if neither AST nor hrw4u specified diff --git a/tools/hrw4u/src/common.py b/tools/hrw4u/src/common.py index 28478933c0f..24c97693d40 100644 --- a/tools/hrw4u/src/common.py +++ b/tools/hrw4u/src/common.py @@ -209,8 +209,22 @@ def generate_output( else: if tree is not None: preserve_comments = not getattr(args, 'no_comments', False) - visitor = visitor_class( - filename=filename, debug=args.debug, error_collector=error_collector, preserve_comments=preserve_comments) + merge_sections = not getattr(args, 'no_merge_sections', False) + + # Build visitor kwargs based on what the visitor class supports + visitor_kwargs = { + 'filename': filename, + 'debug': args.debug, + 'error_collector': error_collector, + 'preserve_comments': preserve_comments + } + + # Only add merge_sections if the visitor supports it (u4wrh) + import inspect + if 'merge_sections' in inspect.signature(visitor_class.__init__).parameters: + visitor_kwargs['merge_sections'] = merge_sections + + visitor = visitor_class(**visitor_kwargs) try: result = visitor.visit(tree) if result: diff --git a/tools/hrw4u/src/hrw_visitor.py b/tools/hrw4u/src/hrw_visitor.py index 149b139dfe5..9eed914e3b1 100644 --- a/tools/hrw4u/src/hrw_visitor.py +++ b/tools/hrw4u/src/hrw_visitor.py @@ -40,13 +40,15 @@ def __init__( section_label: SectionType = SectionType.REMAP, debug: bool = SystemDefaults.DEFAULT_DEBUG, error_collector=None, - preserve_comments: bool = True) -> None: + preserve_comments: bool = True, + merge_sections: bool = True) -> None: super().__init__(filename=filename, debug=debug, error_collector=error_collector) # HRW inverse-specific state self.section_label = section_label self.preserve_comments = preserve_comments + self.merge_sections = merge_sections self._pending_terms: list[tuple[str, CondState]] = [] self._in_group: bool = False self._group_terms: list[tuple[str, CondState]] = [] @@ -86,7 +88,7 @@ def _reset_condition_state(self) -> None: def _start_new_section(self, section_type: SectionType) -> None: """Start a new section, handling continuation of existing sections.""" with self.debug_context(f"start_section {section_type.value}"): - if self._section_opened and self._section_label == section_type: + if self.merge_sections and self._section_opened and self._section_label == section_type: self.debug(f"continuing existing section") while self._if_depth > 0: self.decrease_indent() diff --git a/tools/hrw4u/src/tables.py b/tools/hrw4u/src/tables.py index 94253fba0a9..a145d40a822 100644 --- a/tools/hrw4u/src/tables.py +++ b/tools/hrw4u/src/tables.py @@ -44,7 +44,7 @@ "inbound.cookie.": MapParams(target=HeaderOperations.COOKIE_OPERATIONS, validate=Validator.http_token(), sections=HTTP_SECTIONS), "inbound.req.": MapParams(target=HeaderOperations.OPERATIONS, add=True, validate=Validator.http_header_name(), sections=HTTP_SECTIONS), "inbound.resp.body": MapParams(target="set-body", validate=Validator.quoted_or_simple(), sections=HTTP_SECTIONS), - "inbound.resp.": MapParams(target=HeaderOperations.OPERATIONS, add=True, validate=Validator.http_header_name(), sections={SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE}), + "inbound.resp.": MapParams(target=HeaderOperations.OPERATIONS, add=True, validate=Validator.http_header_name(), sections={SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE, SectionType.TXN_CLOSE}), "inbound.status.reason": MapParams(target="set-status-reason", validate=Validator.quoted_or_simple(), sections=HTTP_SECTIONS), "inbound.status": MapParams(target="set-status", validate=Validator.range(0, 999), sections=HTTP_SECTIONS), "inbound.url.": MapParams(target=HeaderOperations.DESTINATION_OPERATIONS, upper=True, validate=Validator.suffix_group(SuffixGroup.URL_FIELDS), sections=HTTP_SECTIONS), @@ -109,7 +109,7 @@ "inbound.conn.": MapParams(target="INBOUND", upper=True, prefix=True, validate=Validator.suffix_group(SuffixGroup.CONN_FIELDS)), "inbound.cookie.": MapParams(target="COOKIE", prefix=True, validate=Validator.http_token(), sections=HTTP_SECTIONS, rev={"reverse_fallback": "inbound.cookie."}), "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.resp.": MapParams(target="HEADER", prefix=True, validate=Validator.http_header_name(), sections={SectionType.READ_RESPONSE, SectionType.SEND_RESPONSE, SectionType.TXN_CLOSE}, 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), "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}), diff --git a/tools/hrw4u/tests/data/ops/skip-remap.ast.txt b/tools/hrw4u/tests/data/ops/skip-remap.ast.txt index f817d0995f0..9223a553e19 100644 --- a/tools/hrw4u/tests/data/ops/skip-remap.ast.txt +++ b/tools/hrw4u/tests/data/ops/skip-remap.ast.txt @@ -1 +1 @@ -(program (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.req.path) ~ (regex /foo/)))))) (block { (blockItem (statement (functionCall skip-remap ( (argumentList (value true)) )) ;)) })))) })) ) +(program (programItem (section PRE_REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable inbound.req.path) ~ (regex /foo/)))))) (block { (blockItem (statement (functionCall skip-remap ( (argumentList (value true)) )) ;)) })))) })) ) diff --git a/tools/hrw4u/tests/data/ops/skip-remap.input.txt b/tools/hrw4u/tests/data/ops/skip-remap.input.txt index ac8da25f338..a679ea7c608 100644 --- a/tools/hrw4u/tests/data/ops/skip-remap.input.txt +++ b/tools/hrw4u/tests/data/ops/skip-remap.input.txt @@ -1,4 +1,4 @@ -REMAP { +PRE_REMAP { if inbound.req.path ~ /foo/ { skip-remap(true); } diff --git a/tools/hrw4u/tests/data/ops/skip-remap.output.txt b/tools/hrw4u/tests/data/ops/skip-remap.output.txt index 5f4a5194b20..fdf7a4022f7 100644 --- a/tools/hrw4u/tests/data/ops/skip-remap.output.txt +++ b/tools/hrw4u/tests/data/ops/skip-remap.output.txt @@ -1,3 +1,3 @@ -cond %{REMAP_PSEUDO_HOOK} [AND] +cond %{READ_REQUEST_PRE_REMAP_HOOK} [AND] cond %{CLIENT-HEADER:path} /foo/ skip-remap TRUE diff --git a/tools/hrw_confcmp/CMakeLists.txt b/tools/hrw_confcmp/CMakeLists.txt new file mode 100644 index 00000000000..6fcfb1df83c --- /dev/null +++ b/tools/hrw_confcmp/CMakeLists.txt @@ -0,0 +1,108 @@ +####################### +# +# 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. +# +####################### + +# hrw_confcmp - Configuration comparison tool +# Only build when ANTLR4 is available (for hrw4u support) +if(TARGET ts::hrw4u) + # Create simplified parser that doesn't need Layout/hrw4u + add_library(hrw_confcmp_parser STATIC ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/parser.cc) + target_include_directories( + hrw_confcmp_parser + PUBLIC ${CMAKE_SOURCE_DIR}/plugins/header_rewrite + PUBLIC ${CMAKE_SOURCE_DIR}/include + ) + target_link_libraries(hrw_confcmp_parser PUBLIC libswoc::libswoc ts::tscore) + + # Create a library with necessary header_rewrite components for the tool + add_library( + hrw_confcmp_lib STATIC + ts_api_stubs.cc + rules_factory.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/condition.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/conditions.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/factory.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/objtypes.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/header_rewrite.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/hrw4u.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/lulu.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/matcher.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/operator.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/operators.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/regex_helper.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/resources.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/ruleset.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/statement.cc + ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/value.cc + ) + + target_include_directories( + hrw_confcmp_lib + PUBLIC ${CMAKE_SOURCE_DIR}/plugins/header_rewrite + PUBLIC ${CMAKE_SOURCE_DIR}/include + ) + + target_link_libraries( + hrw_confcmp_lib + PUBLIC libswoc::libswoc + PUBLIC OpenSSL::Crypto + PUBLIC OpenSSL::SSL + PUBLIC PkgConfig::PCRE2 + PUBLIC ts::tscore + PUBLIC ts::tsutil + PUBLIC ts::hrw4u + ) + + # Enable native .hrw4u parsing support in the header_rewrite code + target_compile_definitions(hrw_confcmp_lib PUBLIC ENABLE_HRW4U_NATIVE) + + # Conditionally add maxminddb if available + if(maxminddb_FOUND) + target_compile_definitions(hrw_confcmp_lib PUBLIC TS_USE_HRW_MAXMINDDB=1) + target_sources(hrw_confcmp_lib PRIVATE ${CMAKE_SOURCE_DIR}/plugins/header_rewrite/conditions_geo_maxmind.cc) + target_link_libraries(hrw_confcmp_lib PUBLIC maxminddb::maxminddb) + endif() + + add_executable(hrw_confcmp main.cc comparator.cc) + + target_link_libraries( + hrw_confcmp + PRIVATE hrw_confcmp_parser + PRIVATE hrw_confcmp_lib + PRIVATE ts::inkevent + PRIVATE ts::records + ) + + target_include_directories( + hrw_confcmp + PRIVATE ${CMAKE_SOURCE_DIR}/plugins/header_rewrite + PRIVATE ${CMAKE_SOURCE_DIR}/include + ) + + # Note: This tool needs significant refactoring to properly link against + # header_rewrite parsing infrastructure. The header_rewrite plugin code + # needs to be split into a library that can be reused by this tool. + + message(STATUS "hrw_confcmp tool will be built (hrw4u support available)") + + # Copy test runner script to build directory for convenience + configure_file(${CMAKE_CURRENT_SOURCE_DIR}/run_tests.sh ${CMAKE_CURRENT_BINARY_DIR}/run_tests.sh COPYONLY) + + install(TARGETS hrw_confcmp RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + install(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/run_tests.sh DESTINATION ${CMAKE_INSTALL_BINDIR}) +else() + message(STATUS "hrw_confcmp tool skipped (requires ANTLR4)") +endif() diff --git a/tools/hrw_confcmp/comparator.cc b/tools/hrw_confcmp/comparator.cc new file mode 100644 index 00000000000..84e5a5d60b9 --- /dev/null +++ b/tools/hrw_confcmp/comparator.cc @@ -0,0 +1,643 @@ +/* + 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. +*/ + +#include "comparator.h" +#include "ruleset.h" +#include "condition.h" +#include "operator.h" +#include "operators.h" +#include "conditions.h" +#include "matcher.h" + +#include +#include + +#include "hrw4u.h" +#include "hrw4u/Types.h" + +namespace ConfigComparison +{ + +namespace +{ + // Helper to format hook ID as hrw4u section name for display + const char * + get_hrw4u_section_name(TSHttpHookID hook) + { + static char buf[64]; + int section_type = hrw4u_integration::hook_to_section(hook); + auto section = static_cast(section_type); + auto sv = hrw4u::section_type_to_string(section); + size_t len = sv.size(); + + if (len >= sizeof(buf)) { + len = sizeof(buf) - 1; + } + for (size_t i = 0; i < len; ++i) { + buf[i] = static_cast(std::toupper(static_cast(sv[i]))); + } + buf[len] = '\0'; + return buf; + } + + template + int + count_chain(T *head, NextFn next_fn) + { + int count = 0; + + for (auto *p = head; p; p = next_fn(p)) { + count++; + } + + return count; + } + + inline CondModifiers + mask_cond_modifier(CondModifiers mods, CondModifiers to_remove) + { + return static_cast(static_cast(mods) & ~static_cast(to_remove)); + } + + inline OperModifiers + mask_oper_modifier(OperModifiers mods, OperModifiers to_remove) + { + return static_cast(static_cast(mods) & ~static_cast(to_remove)); + } +} // namespace + +void +ComparisonResult::add_diff(const std::string &context, const std::string &msg) +{ + success = false; + std::string full_msg = context + ": " + msg; + differences.push_back(full_msg); + + std::cout << " ERROR: " << full_msg << "\n"; + total_comparisons++; +} + +void +ComparisonResult::add_diff(const std::string &context, const std::string &msg, const std::string &expected, const std::string &got) +{ + success = false; + std::string full_msg = context + ": " + msg; + differences.push_back(full_msg); + + std::cout << " ERROR: " << full_msg << "\n"; + std::cout << " Expected (hrw): " << expected << "\n"; + std::cout << " Got (hrw4u): " << got << "\n"; + total_comparisons++; +} + +void +ComparisonResult::add_success(const std::string & /* context */) +{ + successful_compares++; + total_comparisons++; +} + +bool +ConfigComparator::compare_rulesets_for_hook(RuleSet *rs1, RuleSet *rs2, TSHttpHookID hook) +{ + std::string context = std::string("Hook[") + TSHttpHookNameLookup(hook) + "]"; + + if (!rs1 && !rs2) { + return true; + } + + if (!rs1 || !rs2) { + if (!rs1) { + _result.add_diff(context, "hrw config has NO rules for this hook, but hrw4u config DOES have rules"); + } else { + _result.add_diff(context, "hrw config HAS rules for this hook, but hrw4u config DOES NOT"); + } + return false; + } + + return compare_ruleset_chain(rs1, rs2, context); +} + +bool +ConfigComparator::compare_ruleset_chain(RuleSet *rs1, RuleSet *rs2, const std::string &context) +{ + int index = 0; + bool all_match = true; + + int count1 = count_chain(rs1, [](RuleSet *r) { return r->next.get(); }); + int count2 = count_chain(rs2, [](RuleSet *r) { return r->next.get(); }); + + while (rs1 || rs2) { + std::string ctx = context + ".RuleSet[" + std::to_string(index) + "]"; + + if (!rs1 || !rs2) { + std::ostringstream oss; + oss << "RuleSet chain length mismatch: expected " << count1 << " rulesets, got " << count2; + _result.add_diff(context, oss.str()); + return false; + } + + if (!compare_single_ruleset(rs1, rs2, ctx)) { + all_match = false; + } + + rs1 = rs1->next.get(); + rs2 = rs2->next.get(); + index++; + } + + return all_match; +} + +bool +ConfigComparator::compare_single_ruleset(RuleSet *rs1, RuleSet *rs2, const std::string &context) +{ + bool all_match = true; + + if (_debug) { + debug_print_ruleset(rs1, context + " OLD"); + debug_print_ruleset(rs2, context + " NEW"); + } + + if (rs1->get_hook() != rs2->get_hook()) { + std::ostringstream oss; + oss << "Hook mismatch: expected " << TSHttpHookNameLookup(rs1->get_hook()) << ", got " << TSHttpHookNameLookup(rs2->get_hook()); + _result.add_diff(context, oss.str()); + all_match = false; + } + + if (rs1->get_resource_ids() != rs2->get_resource_ids()) { + std::ostringstream oss; + oss << "Resource IDs differ: expected 0x" << std::hex << static_cast(rs1->get_resource_ids()) << ", got 0x" + << static_cast(rs2->get_resource_ids()) << std::dec; + _result.add_diff(context, oss.str()); + all_match = false; + } + + ConditionGroup *g1 = rs1->get_group(); + ConditionGroup *g2 = rs2->get_group(); + bool has_cond1 = g1 && g1->has_conditions(); + bool has_cond2 = g2 && g2->has_conditions(); + bool conditions_deferred = false; + + if (has_cond1 && has_cond2) { + if (!compare_statement_chains(g1->get_conditions(), g2->get_conditions(), context + ".conditions")) { + all_match = false; + } + } else if (has_cond1 || has_cond2) { + conditions_deferred = true; + Dbg(dbg_ctl, "Top-level condition mismatch, will check in OperatorIf sections"); + } + + const OperatorIf *op_if1 = rs1->get_operator_if(); + const OperatorIf *op_if2 = rs2->get_operator_if(); + + if (op_if1 && op_if2) { + if (!compare_operator_if_sections(op_if1, op_if2, context + ".OperatorIf")) { + all_match = false; + } + } else if (op_if1 || op_if2) { + _result.add_diff(context, "One RuleSet has OperatorIf structure but the other does not"); + all_match = false; + } else if (conditions_deferred) { + if (has_cond1) { + _result.add_diff(context, "hrw has top-level conditions but hrw4u has none"); + } else { + _result.add_diff(context, "hrw4u has top-level conditions but hrw has none"); + } + all_match = false; + } + + if (all_match) { + _result.add_success(context); + } + + return all_match; +} + +bool +ConfigComparator::compare_operator_if_sections(const OperatorIf *op1, const OperatorIf *op2, const std::string &context) +{ + const auto *sec1 = op1->get_sections(); + const auto *sec2 = op2->get_sections(); + int sec_index = 0; + bool all_match = true; + int count1 = 0, count2 = 0; + + for (auto *s = sec1; s; s = s->next.get()) { + count1++; + } + for (auto *s = sec2; s; s = s->next.get()) { + count2++; + } + + while (sec1 || sec2) { + std::string ctx = context + ".Section[" + std::to_string(sec_index) + "]"; + + if (!sec1 || !sec2) { + std::ostringstream oss; + + oss << "OperatorIf section count mismatch: expected " << count1 << " sections, got " << count2; + _result.add_diff(context, oss.str()); + + return false; + } + + Statement *cond1 = sec1->group.get_conditions(); + Statement *cond2 = sec2->group.get_conditions(); + Operator *oper1 = sec1->ops.oper.get(); + Operator *oper2 = sec2->ops.oper.get(); + + if (!cond1 && oper1 && oper1->type_name() == "OperatorIf" && !oper1->next()) { + auto *nested_op = static_cast(oper1); + const auto *nested_sec = nested_op->get_sections(); + + if (nested_sec && !nested_sec->next.get()) { + cond1 = nested_sec->group.get_conditions(); + oper1 = nested_sec->ops.oper.get(); + } + } + + if (!cond2 && oper2 && oper2->type_name() == "OperatorIf" && !oper2->next()) { + auto *nested_op = static_cast(oper2); + const auto *nested_sec = nested_op->get_sections(); + + if (nested_sec && !nested_sec->next.get()) { + cond2 = nested_sec->group.get_conditions(); + oper2 = nested_sec->ops.oper.get(); + } + } + + if (!compare_statement_chains(cond1, cond2, ctx + ".conditions")) { + all_match = false; + } + + if (oper1 && oper2) { + if (!compare_statement_chains(oper1, oper2, ctx + ".operators")) { + all_match = false; + } + } else if (oper1 || oper2) { + if (!oper1) { + _result.add_diff(ctx, "Expected operators present, but new config section has none"); + } else { + _result.add_diff(ctx, "Expected no operators, but new config section has some"); + } + all_match = false; + } + + sec1 = sec1->next.get(); + sec2 = sec2->next.get(); + sec_index++; + } + + return all_match; +} + +bool +ConfigComparator::compare_statement_chains(Statement *s1, Statement *s2, const std::string &context) +{ + int index = 0; + bool all_match = true; + int count1 = 0, count2 = 0; + std::vector types1, types2; + + for (Statement *s = s1; s; s = s->next()) { + count1++; + types1.push_back(std::string(s->type_name())); + } + for (Statement *s = s2; s; s = s->next()) { + count2++; + types2.push_back(std::string(s->type_name())); + } + + while (s1 || s2) { + std::string ctx = context + "[" + std::to_string(index) + "]"; + + if (!s1 || !s2) { + std::ostringstream oss; + + oss << "Statement chain length mismatch: expected " << count1 << " statements, got " << count2; + _result.add_diff(context, oss.str()); + + std::cout << " Expected chain: "; + for (size_t i = 0; i < types1.size(); i++) { + std::cout << types1[i] << (i < types1.size() - 1 ? " -> " : ""); + } + std::cout << "\n"; + + std::cout << " Got chain: "; + for (size_t i = 0; i < types2.size(); i++) { + std::cout << types2[i] << (i < types2.size() - 1 ? " -> " : ""); + } + std::cout << "\n"; + + return false; + } + + if (!compare_single_statement(s1, s2, ctx)) { + all_match = false; + } + + s1 = s1->next(); + s2 = s2->next(); + index++; + } + + return all_match; +} + +bool +ConfigComparator::compare_single_statement(Statement *s1, Statement *s2, const std::string &context) +{ + if (s1->type_name() != s2->type_name()) { + std::ostringstream oss; + + oss << "Statement type mismatch: expected '" << s1->type_name() << "', got '" << s2->type_name() << "'"; + _result.add_diff(context, oss.str()); + + return false; + } + + if (s1->type_name() == "OperatorIf") { + auto *op_if1 = static_cast(s1); + auto *op_if2 = static_cast(s2); + + return compare_operator_if_sections(op_if1, op_if2, context + ".OperatorIf"); + } + + bool semantic_match = true; + auto *cond1 = dynamic_cast(s1); + auto *cond2 = dynamic_cast(s2); + + if (cond1 && cond2) { + CondModifiers mods1_masked = mask_cond_modifier(cond1->mods(), CondModifiers::AND); + CondModifiers mods2_masked = mask_cond_modifier(cond2->mods(), CondModifiers::AND); + + if (cond1->get_qualifier() != cond2->get_qualifier() || cond1->get_cond_op() != cond2->get_cond_op() || + mods1_masked != mods2_masked) { + semantic_match = false; + } + if (cond1->get_matcher() && cond2->get_matcher()) { + if (cond1->get_matcher()->op() != cond2->get_matcher()->op()) { + semantic_match = false; + } + } else if ((cond1->get_matcher() == nullptr) != (cond2->get_matcher() == nullptr)) { + semantic_match = false; + } + } else { + auto *op1 = dynamic_cast(s1); + auto *op2 = dynamic_cast(s2); + + if (op1 && op2) { + OperModifiers mods1_masked = mask_oper_modifier(op1->get_oper_modifiers(), OPER_LAST); + OperModifiers mods2_masked = mask_oper_modifier(op2->get_oper_modifiers(), OPER_LAST); + auto *redirect1 = dynamic_cast(s1); + auto *redirect2 = dynamic_cast(s2); + + if (redirect1 && redirect2) { + const std::string query_suffix = "?%{CLIENT-URL:QUERY}"; + bool op1_has_qsa = (mods1_masked & OPER_QSA) != 0; + bool op2_has_qsa = (mods2_masked & OPER_QSA) != 0; + std::string loc1 = redirect1->get_location(); + std::string loc2 = redirect2->get_location(); + + if (op1_has_qsa && !op2_has_qsa) { + if (loc2.size() >= query_suffix.size() && loc2.substr(loc2.size() - query_suffix.size()) == query_suffix) { + mods2_masked = static_cast(mods2_masked | OPER_QSA); + loc2 = loc2.substr(0, loc2.size() - query_suffix.size()); + } + } else if (!op1_has_qsa && op2_has_qsa) { + if (loc1.size() >= query_suffix.size() && loc1.substr(loc1.size() - query_suffix.size()) == query_suffix) { + mods1_masked = static_cast(mods1_masked | OPER_QSA); + loc1 = loc1.substr(0, loc1.size() - query_suffix.size()); + } + } + + if (mods1_masked != mods2_masked || redirect1->get_status() != redirect2->get_status() || loc1 != loc2) { + semantic_match = false; + } + } else { + if (mods1_masked != mods2_masked) { + semantic_match = false; + } + if (!op1->equals(op2)) { + semantic_match = false; + } + } + } else { + semantic_match = s1->equals(s2); + } + } + + if (semantic_match) { + _result.add_success(context + "." + std::string(s1->type_name())); + return true; + } else { + if (_debug) { + std::cerr << "DEBUG: Statement comparison failed for " << s1->type_name() << "\n"; + std::cerr << " Statement 1: hook=" << TSHttpHookNameLookup(s1->get_hook()) << ", rsrc=0x" << std::hex + << static_cast(s1->get_resource_ids()) << std::dec << "\n"; + std::cerr << " Statement 2: hook=" << TSHttpHookNameLookup(s2->get_hook()) << ", rsrc=0x" << std::hex + << static_cast(s2->get_resource_ids()) << std::dec << "\n"; + + if (cond1 && cond2) { + unsigned mods1 = static_cast(cond1->mods()); + unsigned mods2 = static_cast(cond2->mods()); + + std::cerr << " Condition 1: op=" << static_cast(cond1->get_cond_op()) << ", qualifier='" << cond1->get_qualifier() + << "', mods=" << mods1 << " (" << cond_modifiers_to_string(cond1->mods()) << ")\n"; + std::cerr << " Condition 2: op=" << static_cast(cond2->get_cond_op()) << ", qualifier='" << cond2->get_qualifier() + << "', mods=" << mods2 << " (" << cond_modifiers_to_string(cond2->mods()) << ")\n"; + + if (cond1->get_matcher() && cond2->get_matcher()) { + std::cerr << " Matcher 1: op=" << static_cast(cond1->get_matcher()->op()) << "\n"; + std::cerr << " Matcher 2: op=" << static_cast(cond2->get_matcher()->op()) << "\n"; + } else { + std::cerr << " Matcher 1: " << (cond1->get_matcher() ? "present" : "nullptr") << "\n"; + std::cerr << " Matcher 2: " << (cond2->get_matcher() ? "present" : "nullptr") << "\n"; + } + } + + auto *op1 = dynamic_cast(s1); + auto *op2 = dynamic_cast(s2); + + if (op1 && op2) { + std::cerr << " Operator 1: mods=" << static_cast(op1->get_oper_modifiers()) << "\n"; + std::cerr << " Operator 2: mods=" << static_cast(op2->get_oper_modifiers()) << "\n"; + } + } + + std::string msg = std::string(s1->type_name()) + " value mismatch"; + _result.add_diff(context, msg, s1->debug_string(), s2->debug_string()); + return false; + } +} + +void +ConfigComparator::debug_print_ruleset(RuleSet *rs, const std::string &label) +{ + if (!rs) { + std::cerr << "DEBUG: " << label << ": nullptr\n"; + return; + } + + std::cerr << "DEBUG: " << label << " RuleSet:\n"; + std::cerr << " Hook: " << TSHttpHookNameLookup(rs->get_hook()) << "\n"; + std::cerr << " Resource IDs: 0x" << std::hex << static_cast(rs->get_resource_ids()) << std::dec << "\n"; + + ConditionGroup *g = rs->get_group(); + if (g) { + std::cerr << " Condition Group: present\n"; + Statement *cond = g->get_conditions(); + int count = 0; + std::cerr << " Conditions: "; + while (cond) { + std::cerr << cond->type_name() << " "; + count++; + cond = cond->next(); + } + std::cerr << "(" << count << " total)\n"; + } else { + std::cerr << " Condition Group: nullptr\n"; + } + + const OperatorIf *op_if = rs->get_operator_if(); + if (op_if) { + const auto *sections = op_if->get_sections(); + int sec_num = 0; + + std::cerr << " OperatorIf sections:\n"; + for (const auto *sec = sections; sec; sec = sec->next.get(), sec_num++) { + Statement *sec_cond = sec->group.get_conditions(); + int cond_count = 0; + + std::cerr << " Section[" << sec_num << "]:\n"; + std::cerr << " Conditions: "; + while (sec_cond) { + std::cerr << sec_cond->type_name() << " "; + cond_count++; + sec_cond = sec_cond->next(); + } + std::cerr << "(" << cond_count << " total)\n"; + + if (sec->ops.oper) { + int op_count = 0; + + std::cerr << " Operators: "; + for (Operator *op = sec->ops.oper.get(); op; op = static_cast(op->next())) { + std::cerr << op->type_name() << " "; + op_count++; + } + std::cerr << "(" << op_count << " total)\n"; + } else { + std::cerr << " Operators: (0 total)\n"; + } + } + } +} + +void +ConfigComparator::set_debug(bool debug) +{ + _debug = debug; +} + +std::string +ParseStats::format_hooks() const +{ + std::string result; + + for (auto hook : hooks) { + if (!result.empty()) { + result += ", "; + } + result += is_hrw4u ? get_hrw4u_section_name(hook) : TSHttpHookNameLookup(hook); + } + + return result.empty() ? "(none)" : result; +} + +ParseStats +ConfigComparator::collect_stats(RuleSet *config) +{ + ParseStats stats; + + count_ruleset_stats(config, stats); + return stats; +} + +void +ConfigComparator::count_ruleset_stats(RuleSet *rs, ParseStats &stats) +{ + while (rs) { + ConditionGroup *group = rs->get_group(); + + stats.rulesets++; + stats.hooks.insert(rs->get_hook()); + + if (group && group->has_conditions()) { + Statement *cond = group->get_conditions(); + + count_statement_stats(cond, stats); + } + + const OperatorIf *op_if = rs->get_operator_if(); + + if (op_if) { + count_operator_if_stats(op_if, stats); + } + + rs = rs->next.get(); + } +} + +void +ConfigComparator::count_statement_stats(Statement *stmt, ParseStats &stats) +{ + while (stmt) { + if (dynamic_cast(stmt)) { + stats.conditions++; + } else if (auto *op = dynamic_cast(stmt)) { + stats.operators++; + if (op->type_name() == "OperatorIf") { + auto *op_if = static_cast(op); + + count_operator_if_stats(op_if, stats); + } + } + + stmt = stmt->next(); + } +} + +void +ConfigComparator::count_operator_if_stats(const OperatorIf *op_if, ParseStats &stats) +{ + const auto *section = op_if->get_sections(); + + while (section) { + Statement *cond = section->group.get_conditions(); + Operator *oper = section->ops.oper.get(); + + count_statement_stats(cond, stats); + count_statement_stats(oper, stats); + section = section->next.get(); + } +} + +} // namespace ConfigComparison diff --git a/tools/hrw_confcmp/comparator.h b/tools/hrw_confcmp/comparator.h new file mode 100644 index 00000000000..154cb48949d --- /dev/null +++ b/tools/hrw_confcmp/comparator.h @@ -0,0 +1,119 @@ +/* + 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. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "ts/ts.h" + +// Forward declarations +class RuleSet; +class Statement; +class Condition; +class Operator; +class OperatorIf; + +namespace ConfigComparison +{ + +struct ParseStats { + int rulesets = 0; + int conditions = 0; + int operators = 0; + std::set hooks; + bool is_hrw4u = false; + + std::string format_hooks() const; +}; + +struct ComparisonResult { + bool success = true; + std::vector differences; + int total_comparisons = 0; + int successful_compares = 0; + + void add_diff(const std::string &context, const std::string &msg); + void add_diff(const std::string &context, const std::string &msg, const std::string &expected, const std::string &got); + void add_success(const std::string &context); + + std::string + summary() const + { + if (success) { + return "✓ All comparisons passed (" + std::to_string(successful_compares) + "/" + std::to_string(total_comparisons) + ")"; + } else { + return "✗ " + std::to_string(differences.size()) + " difference(s) found"; + } + } +}; + +class ConfigComparator +{ +public: + ConfigComparator() = default; + + bool compare_rulesets_for_hook(RuleSet *rs1, RuleSet *rs2, TSHttpHookID hook); + + void set_debug(bool debug); + + const ComparisonResult & + get_result() const + { + return _result; + } + + // Statistics collection + ParseStats collect_stats(RuleSet *config); + + const ParseStats & + get_old_stats() const + { + return _old_stats; + } + + const ParseStats & + get_new_stats() const + { + return _new_stats; + } + +private: + bool compare_ruleset_chain(RuleSet *rs1, RuleSet *rs2, const std::string &context); + bool compare_single_ruleset(RuleSet *rs1, RuleSet *rs2, const std::string &context); + bool compare_statement_chains(Statement *s1, Statement *s2, const std::string &context); + bool compare_single_statement(Statement *s1, Statement *s2, const std::string &context); + bool compare_operator_if_sections(const OperatorIf *op1, const OperatorIf *op2, const std::string &context); + + void debug_print_ruleset(RuleSet *rs, const std::string &label); + + // Helper functions for statistics + void count_ruleset_stats(RuleSet *rs, ParseStats &stats); + void count_statement_stats(Statement *stmt, ParseStats &stats); + void count_operator_if_stats(const OperatorIf *op_if, ParseStats &stats); + + ComparisonResult _result; + bool _debug = false; + ParseStats _old_stats; + ParseStats _new_stats; +}; + +} // namespace ConfigComparison diff --git a/tools/hrw_confcmp/main.cc b/tools/hrw_confcmp/main.cc new file mode 100644 index 00000000000..c76a5d3441d --- /dev/null +++ b/tools/hrw_confcmp/main.cc @@ -0,0 +1,328 @@ +/* + 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. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ts/ts.h" +#include "tscore/Layout.h" +#include "iocore/eventsystem/RecProcess.h" + +class RulesConfig; +class RuleSet; +enum ResourceIDs : unsigned; + +#include "comparator.h" + +static std::once_flag init_flag; + +static void +initialize_hrw_subsystems() +{ + Layout::create(); + RecProcessInit(); +} + +// Factory functions implemented in header_rewrite.cc +extern "C" { +RulesConfig *create_rules_config(int timezone, int inboundIpSource); +void destroy_rules_config(RulesConfig *conf); +bool rules_config_parse(RulesConfig *conf, const char *fname, int default_hook, char *from_url, char *to_url, bool force_hrw4u); +RuleSet *rules_config_get_rule(RulesConfig *conf, int hook); +} + +static ConfigComparison::ParseStats +collect_all_stats(RulesConfig *config, ConfigComparison::ConfigComparator &comparator, bool is_hrw4u = false) +{ + ConfigComparison::ParseStats stats; + stats.is_hrw4u = is_hrw4u; + + for (int hook = TS_HTTP_READ_REQUEST_HDR_HOOK; hook <= TS_HTTP_LAST_HOOK; ++hook) { + RuleSet *rs = rules_config_get_rule(config, hook); + + if (rs) { + ConfigComparison::ParseStats hook_stats = comparator.collect_stats(rs); + + stats.rulesets += hook_stats.rulesets; + stats.conditions += hook_stats.conditions; + stats.operators += hook_stats.operators; + stats.hooks.insert(hook_stats.hooks.begin(), hook_stats.hooks.end()); + } + } + + return stats; +} + +// Result codes for compare_pair +enum CompareResult { COMPARE_MATCH = 0, COMPARE_DIFFER = 1, COMPARE_ERROR = 2 }; + +static CompareResult +compare_pair(const char *hrw_file, const char *hrw4u_file, bool debug, bool quiet, bool profile) +{ + using Clock = std::chrono::high_resolution_clock; + auto t_start = Clock::now(); + + if (!quiet) { + std::cout << "Header Rewrite Configuration Comparison Tool\n"; + std::cout << "============================================\n\n"; + std::cout << "Parsing hrw config: " << hrw_file << " (using text parser)\n"; + } + + auto t_hrw_start = Clock::now(); + RulesConfig *hrw_config = create_rules_config(0, 0); + + if (!rules_config_parse(hrw_config, hrw_file, TS_HTTP_READ_RESPONSE_HDR_HOOK, nullptr, nullptr, false)) { + std::cerr << "ERROR: Failed to parse hrw config file: " << hrw_file << "\n"; + destroy_rules_config(hrw_config); + + return COMPARE_ERROR; + } + + auto t_hrw_end = Clock::now(); + auto t_hrw4u_start = Clock::now(); + RulesConfig *hrw4u_config = create_rules_config(0, 0); + + if (!quiet) { + std::cout << "Parsing hrw4u config: " << hrw4u_file << " (using native hrw4u parser)\n"; + } + + if (!rules_config_parse(hrw4u_config, hrw4u_file, TS_HTTP_READ_RESPONSE_HDR_HOOK, nullptr, nullptr, true)) { + std::cerr << "ERROR: Failed to parse hrw4u config file: " << hrw4u_file << "\n"; + destroy_rules_config(hrw_config); + destroy_rules_config(hrw4u_config); + return COMPARE_ERROR; + } + + auto t_hrw4u_end = Clock::now(); + + if (!quiet) { + std::cout << "\n"; + } + + auto t_compare_start = Clock::now(); + ConfigComparison::ConfigComparator comparator; + bool all_hooks_match = true; + int hooks_compared = 0; + + comparator.set_debug(debug); + + if (debug) { + std::cout << "DEBUG: Scanning all hooks for rules...\n"; + } + + for (int hook = TS_HTTP_READ_REQUEST_HDR_HOOK; hook <= TS_HTTP_LAST_HOOK; ++hook) { + auto hook_id = static_cast(hook); + RuleSet *rs1 = rules_config_get_rule(hrw_config, hook); + RuleSet *rs2 = rules_config_get_rule(hrw4u_config, hook); + const char *name = TSHttpHookNameLookup(hook_id); + + if (debug) { + if (rs1 || rs2) { + std::cout << "DEBUG: Hook " << name << " (" << hook << "): hrw=" << (rs1 ? "HAS_RULES" : "empty") + << ", hrw4u=" << (rs2 ? "HAS_RULES" : "empty") << "\n"; + } + } + + if (!rs1 && !rs2) { + continue; + } + + hooks_compared++; + + if (!quiet) { + std::cout << "Comparing hook: " << name << "\n"; + } + + if (!comparator.compare_rulesets_for_hook(rs1, rs2, hook_id)) { + all_hooks_match = false; + } else if (!quiet) { + std::cout << " ✓ PASSED\n"; + } + } + + auto t_compare_end = Clock::now(); + + if (!quiet) { + std::cout << "\n"; + std::cout << "Collecting parse statistics...\n"; + + ConfigComparison::ParseStats hrw_stats = collect_all_stats(hrw_config, comparator, false); + ConfigComparison::ParseStats hrw4u_stats = collect_all_stats(hrw4u_config, comparator, true); + + std::cout << "\n"; + std::cout << "============================================\n"; + std::cout << "Comparison Summary\n"; + std::cout << "============================================\n"; + std::cout << "Files compared:\n"; + std::cout << " hrw (legacy): " << hrw_file << "\n"; + std::cout << " hrw4u (new): " << hrw4u_file << "\n"; + std::cout << "\n"; + std::cout << "Parse Statistics:\n"; + std::cout << " hrw config:\n"; + std::cout << " Rulesets: " << hrw_stats.rulesets << "\n"; + std::cout << " Total conditions: " << hrw_stats.conditions << " (includes nested)\n"; + std::cout << " Total operators: " << hrw_stats.operators << "\n"; + std::cout << " Hooks: " << hrw_stats.format_hooks() << "\n"; + std::cout << " hrw4u config:\n"; + std::cout << " Rulesets: " << hrw4u_stats.rulesets << "\n"; + std::cout << " Total conditions: " << hrw4u_stats.conditions << " (includes nested)\n"; + std::cout << " Total operators: " << hrw4u_stats.operators << "\n"; + std::cout << " Sections: " << hrw4u_stats.format_hooks() << "\n"; + std::cout << "\n"; + std::cout << "Hooks compared: " << hooks_compared << "\n"; + + const auto &result = comparator.get_result(); + + if (all_hooks_match) { + std::cout << "\n✓ SUCCESS: Configurations are equivalent\n"; + } else { + std::cout << "\n✗ FAILURE: Configurations differ\n"; + std::cout << "\nTotal differences: " << result.differences.size() << "\n"; + } + } + + destroy_rules_config(hrw_config); + destroy_rules_config(hrw4u_config); + + auto t_end = Clock::now(); + + if (profile) { + auto hrw_us = std::chrono::duration_cast(t_hrw_end - t_hrw_start).count(); + auto hrw4u_us = std::chrono::duration_cast(t_hrw4u_end - t_hrw4u_start).count(); + auto compare_us = std::chrono::duration_cast(t_compare_end - t_compare_start).count(); + auto total_us = std::chrono::duration_cast(t_end - t_start).count(); + + std::cerr << "PROFILE: hrw_parse=" << hrw_us << "us hrw4u_parse=" << hrw4u_us << "us compare=" << compare_us + << "us total=" << total_us << "us\n"; + } + + return all_hooks_match ? COMPARE_MATCH : COMPARE_DIFFER; +} + +static void +usage(const char *progname) +{ + std::cerr << "Usage: " << progname << " [--debug] [--quiet] [--profile] \n"; + std::cerr << " " << progname << " --batch [--quiet] [--profile] < pairs.txt\n"; + std::cerr << "\n"; + std::cerr << "Compare header_rewrite configurations in hrw (.config) and hrw4u (.hrw4u) formats.\n"; + std::cerr << "Both files should produce equivalent runtime behavior.\n"; + std::cerr << "\n"; + std::cerr << "Options:\n"; + std::cerr << " --debug Show detailed parsing and comparison information\n"; + std::cerr << " --quiet Minimal output (for batch mode), only show failures\n"; + std::cerr << " --batch Read file pairs from stdin (one pair per line: hrw_file hrw4u_file)\n"; + std::cerr << " --profile Show timing breakdown for each comparison\n"; + std::cerr << "\n"; + std::cerr << "Exit codes:\n"; + std::cerr << " 0 - Configurations are equivalent (all pairs in batch mode)\n"; + std::cerr << " 1 - Configurations differ (any pair in batch mode)\n"; + std::cerr << " 2 - Error (parse failure, file not found, etc.)\n"; +} + +int +main(int argc, char *argv[]) +{ + std::call_once(init_flag, initialize_hrw_subsystems); + + bool debug = false; + bool quiet = false; + bool batch = false; + bool profile = false; + std::vector files; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--debug") == 0) { + debug = true; + } else if (strcmp(argv[i], "--quiet") == 0) { + quiet = true; + } else if (strcmp(argv[i], "--batch") == 0) { + batch = true; + } else if (strcmp(argv[i], "--profile") == 0) { + profile = true; + } else if (argv[i][0] == '-') { + std::cerr << "Unknown option: " << argv[i] << "\n"; + usage(argv[0]); + return 2; + } else { + files.push_back(argv[i]); + } + } + + if (batch) { + std::string line; + int total = 0, passed = 0, failed = 0, errors = 0; + + while (std::getline(std::cin, line)) { + if (line.empty() || line[0] == '#') { + continue; + } + + std::istringstream iss(line); + std::string hrw_file, hrw4u_file; + + if (!(iss >> hrw_file >> hrw4u_file)) { + std::cerr << "ERROR: Invalid line format (expected: hrw_file hrw4u_file): " << line << "\n"; + errors++; + continue; + } + total++; + + CompareResult result = compare_pair(hrw_file.c_str(), hrw4u_file.c_str(), debug, quiet, profile); + + switch (result) { + case COMPARE_MATCH: + passed++; + if (!quiet) { + std::cout << "PASS: " << hrw_file << " <-> " << hrw4u_file << "\n"; + } + break; + case COMPARE_DIFFER: + failed++; + std::cout << "FAIL: " << hrw_file << " <-> " << hrw4u_file << "\n"; + break; + case COMPARE_ERROR: + errors++; + break; + } + } + + if (!quiet || failed > 0 || errors > 0) { + std::cout << "\nBatch Summary: " << total << " total, " << passed << " passed, " << failed << " failed, " << errors + << " errors\n"; + } + + if (errors > 0) { + return 2; + } + return (failed > 0) ? 1 : 0; + } + + if (files.size() != 2) { + usage(argv[0]); + return 2; + } + + return compare_pair(files[0], files[1], debug, quiet, profile); +} diff --git a/tools/hrw_confcmp/rules_factory.cc b/tools/hrw_confcmp/rules_factory.cc new file mode 100644 index 00000000000..bbbb9aa2d3b --- /dev/null +++ b/tools/hrw_confcmp/rules_factory.cc @@ -0,0 +1,92 @@ +/* + 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. +*/ + +// Factory functions to create/destroy RulesConfig without exposing the full definition. +// This avoids ODR violations between main.cc and the library. + +#include "parser.h" +#include "ruleset.h" +#include "resources.h" + +// Forward declare RulesConfig from header_rewrite.cc +class RulesConfig; + +#include +#include +#include +#include +#include +#include + +#include "ts/ts.h" +#include "hrw4u.h" + +// RulesConfig definition must match header_rewrite.cc exactly +class RulesConfig +{ +public: + RulesConfig(int timezone, int inboundIpSource); + ~RulesConfig(); + + bool parse_config(const std::string &fname, TSHttpHookID default_hook, char *from_url = nullptr, char *to_url = nullptr, + bool force_hrw4u = false); + + ResourceIDs resid(int hook) const; + RuleSet *rule(int hook) const; + int timezone() const; + int inboundIpSource() const; + +private: + void add_rule(std::unique_ptr rule); + + TSCont _cont; + std::array, TS_HTTP_LAST_HOOK + 1> _rules{}; + std::array _resids{}; + + int _timezone = 0; + int _inboundIpSource = 0; +}; + +// Factory function implementations +extern "C" { + +RulesConfig * +create_rules_config(int timezone, int inboundIpSource) +{ + return new RulesConfig(timezone, inboundIpSource); +} + +void +destroy_rules_config(RulesConfig *conf) +{ + delete conf; +} + +bool +rules_config_parse(RulesConfig *conf, const char *fname, int default_hook, char *from_url, char *to_url, bool force_hrw4u) +{ + return conf->parse_config(fname, static_cast(default_hook), from_url, to_url, force_hrw4u); +} + +RuleSet * +rules_config_get_rule(RulesConfig *conf, int hook) +{ + return conf->rule(hook); +} + +} // extern "C" diff --git a/tools/hrw_confcmp/run_tests.sh b/tools/hrw_confcmp/run_tests.sh new file mode 100755 index 00000000000..ca08fc35c80 --- /dev/null +++ b/tools/hrw_confcmp/run_tests.sh @@ -0,0 +1,357 @@ +#!/bin/bash +# +# 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. +# +# HRW4U Test Runner +# Runs hrw_confcmp on pairs of .input.txt and .output.txt files + +set -euo pipefail + +# Try to find hrw_confcmp binary +# 1. Look in current directory (if running from build dir) +# 2. Look relative to script location (if running from source dir) +if [[ -x "./tools/hrw_confcmp/hrw_confcmp" ]]; then + CONFCMP="$(pwd)/tools/hrw_confcmp/hrw_confcmp" +elif [[ -x "hrw_confcmp" ]]; then + CONFCMP="$(pwd)/hrw_confcmp" +else + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + CONFCMP="${SCRIPT_DIR}/hrw_confcmp" +fi + +if [[ ! -x "$CONFCMP" ]]; then + echo "Error: hrw_confcmp not found or not executable" + echo "Tried:" + echo " - ./tools/hrw_confcmp/hrw_confcmp (build directory)" + echo " - ./hrw_confcmp (current directory)" + echo " - ${SCRIPT_DIR}/hrw_confcmp (script directory)" + echo "" + echo "Please build it first with: cmake --build build --target hrw_confcmp" + exit 1 +fi + +USE_BATCH=false +SHOW_HELP=false +TEST_DIRS=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --batch) + USE_BATCH=true + shift + ;; + --help|-h) + SHOW_HELP=true + shift + ;; + *) + TEST_DIRS+=("$1") + shift + ;; + esac +done + +if [[ "$SHOW_HELP" == true ]] || [[ ${#TEST_DIRS[@]} -eq 0 ]]; then + echo "Usage: $0 [--batch] [test_directory...]" + echo "" + echo "Options:" + echo " --batch Use batch mode for faster execution (single process)" + echo "" + echo "Example:" + echo " $0 tools/hrw4u/tests/data/vars" + echo " $0 --batch tools/hrw4u/tests/data/{hooks,conds,ops,vars}" + exit 1 +fi + +# Get time in milliseconds (portable across macOS/Linux) +get_time_ms() { + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS: use perl for millisecond precision + perl -MTime::HiRes=time -e 'printf "%.0f\n", time * 1000' + else + # Linux: use date with nanoseconds + echo $(($(date +%s%N) / 1000000)) + fi +} + +# Check if a test should be skipped based on exceptions.txt +is_test_excepted() { + local test_dir="$1" + local test_name="$2" + + # Only except examples/all-nonsense + if [[ "$test_dir" =~ /examples$ ]] && [[ "$test_name" == "all-nonsense" ]]; then + return 0 # Test is excepted + fi + + return 1 # Test is not excepted +} + +# Collect all test pairs from directories +collect_test_pairs() { + for test_dir in "${TEST_DIRS[@]}"; do + if [[ ! -d "$test_dir" ]]; then + echo "Warning: Directory not found: $test_dir" >&2 + continue + fi + + local abs_test_dir="$(cd "$test_dir" && pwd)" + + while IFS= read -r input_file; do + local base_name="${input_file%.input.txt}" + local test_name="${base_name##*/}" + local output_file="${base_name}.output.txt" + + # Skip excepted tests + if is_test_excepted "$test_dir" "$test_name"; then + echo " Skipping excepted test: ${test_name}" >&2 + continue + fi + + if [[ -f "$output_file" ]]; then + local abs_output="$(cd "$(dirname "$output_file")" && pwd)/$(basename "$output_file")" + local abs_input="$(cd "$(dirname "$input_file")" && pwd)/$(basename "$input_file")" + echo "$abs_output $abs_input" + fi + done < <(find "$test_dir" -maxdepth 1 -name "*.input.txt" | sort) + done +} + +# Run in batch mode (single process, much faster) +run_batch_mode() { + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Running tests in BATCH mode (single process)" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + local start_ms=$(get_time_ms) + + # Collect pairs and feed to batch mode + local pairs + pairs=$(collect_test_pairs) + local total_tests=$(echo "$pairs" | grep -c . || echo 0) + + if [[ $total_tests -eq 0 ]]; then + echo " No test pairs found" + return 0 + fi + + echo " Found $total_tests test pairs" + echo "" + + # Run in batch mode (without --quiet so we can count results) + local result + set +e + result=$(echo "$pairs" | "$CONFCMP" --batch 2>&1) + local exit_code=$? + set -e + + local end_ms=$(get_time_ms) + local elapsed=$((end_ms - start_ms)) + + # Parse batch output - use the summary line which is always present + local summary_line + summary_line=$(echo "$result" | grep "^Batch Summary:" || echo "") + local passed=0 + local failed=0 + + if [[ -n "$summary_line" ]]; then + # Extract numbers: "Batch Summary: 47 total, 47 passed, 0 failed, 0 errors" + passed=$(echo "$summary_line" | awk -F', ' '{for(i=1;i<=NF;i++) if($i ~ /passed/) {gsub(/[^0-9]/,"",$i); print $i}}') + failed=$(echo "$summary_line" | awk -F', ' '{for(i=1;i<=NF;i++) if($i ~ /failed/) {gsub(/[^0-9]/,"",$i); print $i}}') + fi + passed=${passed:-0} + failed=${failed:-0} + + # Show failures if any + if [[ $failed -gt 0 ]]; then + echo "Failed tests:" + echo "$result" | grep "^FAIL:" | while read -r line; do + echo " ✗ $line" + done + echo "" + fi + + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Overall Summary (batch mode)" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Total tests: $total_tests" + echo " Passed: $passed" + echo " Failed: $failed" + echo "" + echo " Total time: ${elapsed} ms" + if [[ $total_tests -gt 0 ]]; then + local avg=$((elapsed / total_tests)) + echo " Avg per test: ${avg} ms" + fi + + if [[ $exit_code -eq 0 ]]; then + echo "" + echo "✓ All tests passed!" + return 0 + else + echo "" + echo "✗ Some tests failed" + return 1 + fi +} + +# Run in serial mode (one process per test, slower but more detailed output) +run_serial_mode() { + local total_tests=0 + local total_passed=0 + local total_failed=0 + local failed_tests=() + local total_time_ms=0 + + process_directory() { + local test_dir="$1" + local dir_name="${test_dir##*/}" + + if [[ ! -d "$test_dir" ]]; then + echo "Warning: Directory not found: $test_dir" + return + fi + + local abs_test_dir="$(cd "$test_dir" && pwd)" + + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Testing directory: $dir_name" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + local dir_tests=0 + local dir_passed=0 + local dir_failed=0 + + while IFS= read -r input_file; do + local base_name="${input_file%.input.txt}" + local test_name="${base_name##*/}" + local output_file="${base_name}.output.txt" + + if [[ ! -f "$output_file" ]]; then + continue + fi + + # Skip excepted tests + if is_test_excepted "$test_dir" "$test_name"; then + printf " ⊘ %-30s (excepted)\n" "${dir_name}/${test_name}:" + continue + fi + + dir_tests=$((dir_tests + 1)) + + local abs_output="$(cd "$(dirname "$output_file")" && pwd)/$(basename "$output_file")" + local abs_input="$(cd "$(dirname "$input_file")" && pwd)/$(basename "$input_file")" + + local start_ms=$(get_time_ms) + if "$CONFCMP" "$abs_output" "$abs_input" >/dev/null 2>&1; then + local end_ms=$(get_time_ms) + local elapsed=$((end_ms - start_ms)) + total_time_ms=$((total_time_ms + elapsed)) + printf " ✓ %-30s %4d ms\n" "${dir_name}/${test_name}:" "$elapsed" + dir_passed=$((dir_passed + 1)) + else + local end_ms=$(get_time_ms) + local elapsed=$((end_ms - start_ms)) + total_time_ms=$((total_time_ms + elapsed)) + printf " ✗ %-30s %4d ms (FAILED)\n" "${dir_name}/${test_name}:" "$elapsed" + dir_failed=$((dir_failed + 1)) + failed_tests+=("${dir_name}/${test_name}|$CONFCMP \"$abs_output\" \"$abs_input\"") + fi + done < <(find "$test_dir" -maxdepth 1 -name "*.input.txt" | sort) + + if [[ $dir_tests -eq 0 ]]; then + echo " No test pairs found (*.input.txt + *.output.txt)" + else + echo "" + echo " Directory Summary: $dir_passed passed, $dir_failed failed (total: $dir_tests)" + fi + + total_tests=$((total_tests + dir_tests)) + total_passed=$((total_passed + dir_passed)) + total_failed=$((total_failed + dir_failed)) + } + + for test_dir in "${TEST_DIRS[@]}"; do + process_directory "$test_dir" + done + + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Overall Summary" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Total tests: $total_tests" + echo " Passed: $total_passed" + echo " Failed: $total_failed" + echo "" + if [[ $total_tests -gt 0 ]]; then + avg_time=$((total_time_ms / total_tests)) + echo " Total time: ${total_time_ms} ms" + echo " Avg per test: ${avg_time} ms" + fi + + if [[ $total_failed -gt 0 ]]; then + echo "" + echo "Failed tests:" + for failed in "${failed_tests[@]}"; do + IFS='|' read -r test_name cmd <<< "$failed" + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " Failed Test: $test_name" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + + # Extract file paths from command + output_file=$(echo "$cmd" | grep -o '"[^"]*\.output\.txt"' | tr -d '"') + input_file=$(echo "$cmd" | grep -o '"[^"]*\.input\.txt"' | tr -d '"') + + echo "" + echo "To re-run this test:" + echo " $cmd" + echo "" + + echo "Expected Output (.output.txt):" + echo "────────────────────────────────────────────────────────────" + cat "$output_file" 2>/dev/null || echo "Error: Could not read $output_file" + echo "" + + echo "HRW4U Input (.input.txt):" + echo "────────────────────────────────────────────────────────────" + cat "$input_file" 2>/dev/null || echo "Error: Could not read $input_file" + echo "" + + echo "Comparison Details (--debug):" + echo "────────────────────────────────────────────────────────────" + $CONFCMP --debug "$output_file" "$input_file" 2>&1 + echo "" + done + echo "" + exit 1 + else + echo "" + echo "✓ All tests passed!" + exit 0 + fi +} + +# Main execution +if [[ "$USE_BATCH" == true ]]; then + run_batch_mode +else + run_serial_mode +fi diff --git a/tools/hrw_confcmp/ts_api_stubs.cc b/tools/hrw_confcmp/ts_api_stubs.cc new file mode 100644 index 00000000000..eb02a428814 --- /dev/null +++ b/tools/hrw_confcmp/ts_api_stubs.cc @@ -0,0 +1,953 @@ +/* + 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. +*/ + +// Stub implementations of TS API functions for hrw_confcmp tool. + +#include "ts/ts.h" +#include "ts/remap.h" +#include +#include +#include +#include + +#ifndef TS_REMAP_PSEUDO_HOOK +constexpr int TS_REMAP_PSEUDO_HOOK = 30; +#endif + +const char *TS_MIME_FIELD_COOKIE = "Cookie"; +int TS_MIME_LEN_COOKIE = 6; + +const char *TS_HTTP_METHOD_CONNECT = "CONNECT"; +const char *TS_HTTP_METHOD_DELETE = "DELETE"; +const char *TS_HTTP_METHOD_GET = "GET"; +const char *TS_HTTP_METHOD_HEAD = "HEAD"; +const char *TS_HTTP_METHOD_OPTIONS = "OPTIONS"; +const char *TS_HTTP_METHOD_POST = "POST"; +const char *TS_HTTP_METHOD_PURGE = "PURGE"; +const char *TS_HTTP_METHOD_PUT = "PUT"; +const char *TS_HTTP_METHOD_TRACE = "TRACE"; +const char *TS_HTTP_METHOD_PUSH = "PUSH"; + +int TS_HTTP_LEN_CONNECT = 7; +int TS_HTTP_LEN_DELETE = 6; +int TS_HTTP_LEN_GET = 3; +int TS_HTTP_LEN_HEAD = 4; +int TS_HTTP_LEN_OPTIONS = 7; +int TS_HTTP_LEN_POST = 4; +int TS_HTTP_LEN_PURGE = 5; +int TS_HTTP_LEN_PUT = 3; +int TS_HTTP_LEN_TRACE = 5; +int TS_HTTP_LEN_PUSH = 4; + +// Stub implementations - these won't actually work for runtime, +// but allow the code to compile and link for static analysis/comparison + +TSReturnCode +TSPluginRegister(TSPluginRegistrationInfo const *) +{ + return TS_SUCCESS; +} +void +TSHttpHookAdd(TSHttpHookID, TSCont) +{ +} +const char * +TSConfigDirGet() +{ + return "/tmp"; +} +const char * +TSHttpHookNameLookup(TSHttpHookID hook) +{ + // Return abbreviated hook names as used in header_rewrite configs + // These match the names in Parser::cond_is_hook() + switch (hook) { + case TS_HTTP_READ_REQUEST_HDR_HOOK: + return "READ_REQUEST_HDR_HOOK"; + case TS_HTTP_SEND_REQUEST_HDR_HOOK: + return "SEND_REQUEST_HDR_HOOK"; + case TS_HTTP_READ_RESPONSE_HDR_HOOK: + return "READ_RESPONSE_HDR_HOOK"; + case TS_HTTP_SEND_RESPONSE_HDR_HOOK: + return "SEND_RESPONSE_HDR_HOOK"; + case TS_HTTP_TXN_START_HOOK: + return "TXN_START_HOOK"; + case TS_HTTP_TXN_CLOSE_HOOK: + return "TXN_CLOSE_HOOK"; + case TS_HTTP_PRE_REMAP_HOOK: + return "READ_REQUEST_PRE_REMAP_HOOK"; + case TS_HTTP_POST_REMAP_HOOK: + return "POST_REMAP_HOOK"; + case TS_REMAP_PSEUDO_HOOK: + return "REMAP_PSEUDO_HOOK"; + default: { + static char buf[64]; + + snprintf(buf, sizeof(buf), "HOOK_%d", static_cast(hook)); + return buf; + } + } +} + +TSCont +TSContCreate(TSEventFunc, TSMutex) +{ + return nullptr; +} +void +TSContDestroy(TSCont) +{ +} +void +TSContDataSet(TSCont, void *) +{ +} +void * +TSContDataGet(TSCont) +{ + return nullptr; +} + +TSMutex +TSMutexCreate() +{ + return nullptr; +} + +void +TSError(const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + va_end(args); +} + +void +TSWarning(const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + va_end(args); +} + +int +_TSAssert(const char *, const char *, int) +{ + return 0; +} +void +_TSReleaseAssert(const char *, const char *, int) +{ + std::exit(1); +} +char * +_TSstrdup(const char *str, int64_t, const char *) +{ + return strdup(str); +} +void +TSfree(void *ptr) +{ + free(ptr); +} + +TSReturnCode +TSUserArgIndexReserve(TSUserArgType, const char *, const char *, int *idx) +{ + static int counter = 0; + *idx = counter++; + return TS_SUCCESS; +} + +void +TSUserArgSet(void *, int, void *) +{ +} + +void * +TSUserArgGet(void *, int) +{ + return nullptr; +} + +TSMBuffer +TSMBufferCreate() +{ + return nullptr; +} + +TSReturnCode +TSMBufferDestroy(TSMBuffer) +{ + return TS_SUCCESS; +} + +TSMLoc +TSHttpHdrCreate(TSMBuffer) +{ + return nullptr; +} + +TSReturnCode +TSHttpHdrTypeSet(TSMBuffer, TSMLoc, TSHttpType) +{ + return TS_SUCCESS; +} + +TSReturnCode +TSHttpHdrUrlGet(TSMBuffer, TSMLoc, TSMLoc *) +{ + return TS_ERROR; +} + +TSReturnCode +TSHttpHdrUrlSet(TSMBuffer, TSMLoc, TSMLoc) +{ + return TS_SUCCESS; +} + +const char * +TSHttpHdrMethodGet(TSMBuffer, TSMLoc, int *length) +{ + *length = 3; + return "GET"; +} + +TSHttpStatus +TSHttpHdrStatusGet(TSMBuffer, TSMLoc) +{ + return TS_HTTP_STATUS_OK; +} + +TSReturnCode +TSHttpHdrStatusSet(TSMBuffer, TSMLoc, TSHttpStatus, TSHttpTxn, std::string_view) +{ + return TS_SUCCESS; +} + +TSReturnCode +TSHttpHdrReasonSet(TSMBuffer, TSMLoc, const char *, int) +{ + return TS_SUCCESS; +} + +const char * +TSHttpHdrReasonLookup(TSHttpStatus) +{ + return "OK"; +} + +TSParseResult +TSHttpHdrParseResp(TSHttpParser, TSMBuffer, TSMLoc, const char **, const char *) +{ + return TS_PARSE_ERROR; +} + +TSHttpParser +TSHttpParserCreate() +{ + return nullptr; +} + +void +TSHttpParserDestroy(TSHttpParser) +{ +} + +TSReturnCode +TSUrlCreate(TSMBuffer, TSMLoc *) +{ + return TS_ERROR; +} + +TSParseResult +TSUrlParse(TSMBuffer, TSMLoc, const char **, const char *) +{ + return TS_PARSE_ERROR; +} + +const char * +TSUrlSchemeGet(TSMBuffer, TSMLoc, int *length) +{ + *length = 4; + return "http"; +} + +TSReturnCode +TSUrlSchemeSet(TSMBuffer, TSMLoc, const char *, int) +{ + return TS_SUCCESS; +} + +const char * +TSUrlHostGet(TSMBuffer, TSMLoc, int *length) +{ + *length = 9; + return "localhost"; +} + +TSReturnCode +TSUrlHostSet(TSMBuffer, TSMLoc, const char *, int) +{ + return TS_SUCCESS; +} + +int +TSUrlPortGet(TSMBuffer, TSMLoc) +{ + return 80; +} + +TSReturnCode +TSUrlPortSet(TSMBuffer, TSMLoc, int) +{ + return TS_SUCCESS; +} + +const char * +TSUrlPathGet(TSMBuffer, TSMLoc, int *length) +{ + *length = 1; + return "/"; +} + +TSReturnCode +TSUrlPathSet(TSMBuffer, TSMLoc, const char *, int) +{ + return TS_SUCCESS; +} + +const char * +TSUrlHttpQueryGet(TSMBuffer, TSMLoc, int *length) +{ + *length = 0; + return ""; +} + +TSReturnCode +TSUrlHttpQuerySet(TSMBuffer, TSMLoc, const char *, int) +{ + return TS_SUCCESS; +} + +char * +TSUrlStringGet(TSMBuffer, TSMLoc, int *length) +{ + static char url[] = "http://localhost/"; + *length = strlen(url); + return strdup(url); +} + +TSMLoc +TSMimeHdrFieldFind(TSMBuffer, TSMLoc, const char *, int) +{ + return nullptr; +} + +TSMLoc +TSMimeHdrFieldNextDup(TSMBuffer, TSMLoc, TSMLoc) +{ + return nullptr; +} + +TSReturnCode +TSMimeHdrFieldCreateNamed(TSMBuffer, TSMLoc, const char *, int, TSMLoc *) +{ + return TS_ERROR; +} + +TSReturnCode +TSMimeHdrFieldAppend(TSMBuffer, TSMLoc, TSMLoc) +{ + return TS_SUCCESS; +} + +TSReturnCode +TSMimeHdrFieldDestroy(TSMBuffer, TSMLoc, TSMLoc) +{ + return TS_SUCCESS; +} + +const char * +TSMimeHdrFieldValueStringGet(TSMBuffer, TSMLoc, TSMLoc, int, int *length) +{ + *length = 0; + return ""; +} + +TSReturnCode +TSMimeHdrFieldValueStringSet(TSMBuffer, TSMLoc, TSMLoc, int, const char *, int) +{ + return TS_SUCCESS; +} + +const char * +TSMimeHdrStringToWKS(const char *, int) +{ + return nullptr; +} + +TSReturnCode +TSHandleMLocRelease(TSMBuffer, TSMLoc, TSMLoc) +{ + return TS_SUCCESS; +} + +TSHttpSsn +TSHttpTxnSsnGet(TSHttpTxn) +{ + return nullptr; +} + +TSVConn +TSHttpSsnClientVConnGet(TSHttpSsn) +{ + return nullptr; +} + +int +TSHttpSsnTransactionCount(TSHttpSsn) +{ + return 1; +} + +int +TSHttpTxnServerSsnTransactionCount(TSHttpTxn) +{ + return 1; +} + +TSReturnCode +TSHttpTxnClientReqGet(TSHttpTxn, TSMBuffer *, TSMLoc *) +{ + return TS_ERROR; +} + +TSReturnCode +TSHttpTxnClientRespGet(TSHttpTxn, TSMBuffer *, TSMLoc *) +{ + return TS_ERROR; +} + +TSReturnCode +TSHttpTxnServerReqGet(TSHttpTxn, TSMBuffer *, TSMLoc *) +{ + return TS_ERROR; +} + +TSReturnCode +TSHttpTxnServerRespGet(TSHttpTxn, TSMBuffer *, TSMLoc *) +{ + return TS_ERROR; +} + +TSReturnCode +TSHttpTxnPristineUrlGet(TSHttpTxn, TSMBuffer *, TSMLoc *) +{ + return TS_ERROR; +} + +void +TSHttpTxnReenable(TSHttpTxn, TSEvent) +{ +} + +void +TSHttpTxnHookAdd(TSHttpTxn, TSHttpHookID, TSCont) +{ +} + +uint64_t +TSHttpTxnIdGet(TSHttpTxn) +{ + return 12345; +} + +int +TSHttpTxnIsInternal(TSHttpTxn) +{ + return 0; +} + +TSReturnCode +TSHttpTxnCacheLookupStatusGet(TSHttpTxn, int *status) +{ + *status = 0; + return TS_SUCCESS; +} + +void +TSHttpTxnStatusSet(TSHttpTxn, TSHttpStatus, std::string_view) +{ +} + +void +TSHttpTxnErrorBodySet(TSHttpTxn, char *, size_t, char *) +{ +} + +const sockaddr * +TSHttpTxnClientAddrGet(TSHttpTxn) +{ + return nullptr; +} + +const sockaddr * +TSHttpTxnIncomingAddrGet(TSHttpTxn) +{ + return nullptr; +} + +const sockaddr * +TSHttpTxnOutgoingAddrGet(TSHttpTxn) +{ + return nullptr; +} + +const sockaddr * +TSHttpTxnServerAddrGet(TSHttpTxn) +{ + return nullptr; +} + +TSReturnCode +TSHttpTxnVerifiedAddrGet(TSHttpTxn, const sockaddr **) +{ + return TS_ERROR; +} + +TSReturnCode +TSHttpTxnVerifiedAddrSet(TSHttpTxn, const sockaddr *) +{ + return TS_SUCCESS; +} + +const char * +TSHttpTxnNextHopNameGet(TSHttpTxn) +{ + return "nexthop"; +} + +int +TSHttpTxnNextHopPortGet(TSHttpTxn) +{ + return 8080; +} + +const char * +TSHttpNextHopStrategyNameGet(const void *) +{ + return "default"; +} + +const void * +TSHttpTxnNextHopNamedStrategyGet(TSHttpTxn, const char *) +{ + return nullptr; +} + +void +TSHttpTxnNextHopStrategySet(TSHttpTxn, const void *) +{ +} + +bool +TSHttpTxnCntlGet(TSHttpTxn, TSHttpCntlType) +{ + return false; +} + +TSReturnCode +TSHttpTxnCntlSet(TSHttpTxn, TSHttpCntlType, bool) +{ + return TS_SUCCESS; +} + +TSReturnCode +TSHttpTxnConfigFind(const char *, int, TSOverridableConfigKey *, TSRecordDataType *) +{ + return TS_ERROR; +} + +TSReturnCode +TSHttpTxnConfigIntSet(TSHttpTxn, TSOverridableConfigKey, int64_t) +{ + return TS_SUCCESS; +} + +TSReturnCode +TSHttpTxnConfigFloatSet(TSHttpTxn, TSOverridableConfigKey, float) +{ + return TS_SUCCESS; +} + +TSReturnCode +TSHttpTxnConfigStringSet(TSHttpTxn, TSOverridableConfigKey, const char *, int) +{ + return TS_SUCCESS; +} + +void +TSHttpTxnActiveTimeoutSet(TSHttpTxn, int) +{ +} + +void +TSHttpTxnNoActivityTimeoutSet(TSHttpTxn, int) +{ +} + +void +TSHttpTxnConnectTimeoutSet(TSHttpTxn, int) +{ +} + +void +TSHttpTxnDNSTimeoutSet(TSHttpTxn, int) +{ +} + +TSReturnCode +TSHttpTxnClientPacketDscpSet(TSHttpTxn, int) +{ + return TS_SUCCESS; +} + +TSReturnCode +TSHttpTxnClientPacketMarkSet(TSHttpTxn, int) +{ + return TS_SUCCESS; +} + +TSReturnCode +TSHttpTxnClientFdGet(TSHttpTxn, int *fd) +{ + *fd = -1; + return TS_ERROR; +} + +TSReturnCode +TSHttpTxnClientProtocolStackGet(TSHttpTxn, int, const char **, int *count) +{ + *count = 0; + return TS_SUCCESS; +} + +const char * +TSHttpTxnClientProtocolStackContains(TSHttpTxn, const char *) +{ + return nullptr; +} + +TSReturnCode +TSClientRequestUuidGet(TSHttpTxn, char *buf) +{ + strcpy(buf, "uuid-1234"); + return TS_SUCCESS; +} + +TSUuid +TSProcessUuidGet() +{ + return nullptr; +} + +const char * +TSUuidStringGet(TSUuid) +{ + return "process-uuid"; +} + +TSReturnCode +TSVConnPPInfoGet(TSVConn, unsigned short, const char **, int *) +{ + return TS_ERROR; +} + +TSSslConnection +TSVConnSslConnectionGet(TSVConn) +{ + return nullptr; +} + +TSFetchSM +TSFetchUrl(const char *, int, const sockaddr *, TSCont, TSFetchWakeUpOptions, TSFetchEvent) +{ + return nullptr; +} + +char * +TSFetchRespGet(TSHttpTxn, int *length) +{ + *length = 0; + return nullptr; +} + +int +TSStatCreate(const char *, TSRecordDataType, TSStatPersistence, TSStatSync) +{ + return -1; +} + +TSReturnCode +TSStatFindName(const char *, int *id) +{ + *id = -1; + return TS_ERROR; +} + +void +TSStatIntIncrement(int, int64_t) +{ +} + +bool +isPluginDynamicReloadEnabled() +{ + return false; +} + +int cmd_disable_pfreelist = 0; + +#include "proxy/http/remap/PluginFactory.h" + +void +RemapPluginInst::done() +{ +} + +TSRemapStatus +RemapPluginInst::doRemap(TSHttpTxn, TSRemapRequestInfo *) +{ + return TSREMAP_NO_REMAP; +} + +PluginFactory::PluginFactory() {} + +PluginFactory::~PluginFactory() {} + +PluginFactory & +PluginFactory::addSearchDir(const fs::path &) +{ + return *this; +} + +PluginFactory & +PluginFactory::setRuntimeDir(const fs::path &) +{ + return *this; +} + +RemapPluginInst * +PluginFactory::getRemapPlugin(const fs::path &, int, char **, std::string &, bool) +{ + return nullptr; +} + +const char * +PluginFactory::getUuid() +{ + return "stub-uuid"; +} + +#include "tscore/ink_config.h" + +#if TS_HAS_CRIPTS +#include +#include + +namespace detail +{ +class CertBase +{ +public: + class X509Value + { + public: + virtual ~X509Value(); + void _load_long(long (*)(const X509 *)) const; + void _load_name(X509_NAME *(*)(const X509 *)) const; + void _load_time(ASN1_STRING *(*)(const X509 *)) const; + void _load_integer(ASN1_STRING *(*)(X509 *)) const; + }; + + class Version : public X509Value + { + public: + void _load() const; + }; + + class Subject : public X509Value + { + public: + void _load() const; + }; + + class Issuer : public X509Value + { + public: + void _load() const; + }; + + class SerialNumber : public X509Value + { + public: + void _load() const; + }; + + class NotBefore : public X509Value + { + public: + void _load() const; + }; + + class NotAfter : public X509Value + { + public: + void _load() const; + }; + + class Certificate : public X509Value + { + public: + Certificate(CertBase *); + virtual ~Certificate(); + }; + + class Signature : public X509Value + { + public: + Signature(CertBase *); + virtual ~Signature(); + }; + + class SAN + { + public: + class SANBase + { + public: + std::string Join(const char *) const; + }; + SANBase dns; + SANBase ipadd; + SANBase email; + SANBase uri; + }; +}; + +CertBase::X509Value::~X509Value() {} + +void +CertBase::X509Value::_load_long(long (*)(const X509 *)) const +{ +} + +void +CertBase::X509Value::_load_name(X509_NAME *(*)(const X509 *)) const +{ +} + +void +CertBase::X509Value::_load_time(ASN1_STRING *(*)(const X509 *)) const +{ +} + +void +CertBase::X509Value::_load_integer(ASN1_STRING *(*)(X509 *)) const +{ +} + +void +CertBase::Version::_load() const +{ +} + +void +CertBase::Subject::_load() const +{ +} + +void +CertBase::Issuer::_load() const +{ +} + +void +CertBase::SerialNumber::_load() const +{ +} + +void +CertBase::NotBefore::_load() const +{ +} + +void +CertBase::NotAfter::_load() const +{ +} + +CertBase::Certificate::Certificate(CertBase *) {} +CertBase::Certificate::~Certificate() {} +CertBase::Signature::Signature(CertBase *) {} +CertBase::Signature::~Signature() {} + +std::string +CertBase::SAN::SANBase::Join(const char *) const +{ + return ""; +} + +} // namespace detail + +namespace cripts +{ +namespace Client +{ + class Connection + { + public: + Connection(); + virtual ~Connection(); + }; + + Connection::Connection() {} + Connection::~Connection() {} +} // namespace Client + +namespace Server +{ + class Connection + { + public: + Connection(); + virtual ~Connection(); + }; + + Connection::Connection() {} + Connection::~Connection() {} +} // namespace Server +} // namespace cripts +#endif From c88fedbf34fecfe6d1d288ef217811ff434a358f Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Thu, 22 Jan 2026 14:16:42 -0700 Subject: [PATCH 2/3] Adds more operators to hrw4u python Fixes parser problems in u4wrh with implicit hooks. --- tools/hrw4u/pyproject.toml | 2 +- tools/hrw4u/src/hrw_visitor.py | 20 ++++++++++++++++++- tools/hrw4u/src/tables.py | 2 ++ tools/hrw4u/src/types.py | 2 ++ tools/hrw4u/tests/data/ops/set-cc-alg.ast.txt | 1 + .../hrw4u/tests/data/ops/set-cc-alg.input.txt | 3 +++ .../tests/data/ops/set-cc-alg.output.txt | 2 ++ .../data/ops/set-effective-address.ast.txt | 1 + .../data/ops/set-effective-address.input.txt | 3 +++ .../data/ops/set-effective-address.output.txt | 2 ++ 10 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tools/hrw4u/tests/data/ops/set-cc-alg.ast.txt create mode 100644 tools/hrw4u/tests/data/ops/set-cc-alg.input.txt create mode 100644 tools/hrw4u/tests/data/ops/set-cc-alg.output.txt create mode 100644 tools/hrw4u/tests/data/ops/set-effective-address.ast.txt create mode 100644 tools/hrw4u/tests/data/ops/set-effective-address.input.txt create mode 100644 tools/hrw4u/tests/data/ops/set-effective-address.output.txt diff --git a/tools/hrw4u/pyproject.toml b/tools/hrw4u/pyproject.toml index ce607fd5b69..c8fc0e1c44c 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.2" 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/hrw_visitor.py b/tools/hrw4u/src/hrw_visitor.py index 9eed914e3b1..62d2a8c4033 100644 --- a/tools/hrw4u/src/hrw_visitor.py +++ b/tools/hrw4u/src/hrw_visitor.py @@ -46,7 +46,7 @@ def __init__( super().__init__(filename=filename, debug=debug, error_collector=error_collector) # HRW inverse-specific state - self.section_label = section_label + self._section_label = section_label self.preserve_comments = preserve_comments self.merge_sections = merge_sections self._pending_terms: list[tuple[str, CondState]] = [] @@ -59,6 +59,7 @@ def __init__( self._if_depth = 0 # Track nesting depth of if blocks self._in_elif_mode = False self._just_closed_nested = False + self._expecting_if_cond = False # True after 'if' operator, before its condition @lru_cache(maxsize=128) def _cached_percent_parsing(self, pct_text: str) -> tuple[str, str | None]: @@ -84,6 +85,16 @@ def _reset_condition_state(self) -> None: self._in_elif_mode = False self._in_group = False self._group_terms.clear() + self._expecting_if_cond = False + + def _close_if_chain_for_new_rule(self) -> None: + """Close if-else chain when a new rule starts without elif/else.""" + expecting_nested_if = self._expecting_if_cond + self._expecting_if_cond = False + + if (self._if_depth > 0 and not self._in_elif_mode and not self._pending_terms and not expecting_nested_if): + self.debug("new rule detected - closing if chain") + self._start_new_section(SectionType.REMAP) def _start_new_section(self, section_type: SectionType) -> None: """Start a new section, handling continuation of existing sections.""" @@ -179,6 +190,7 @@ def visitIfLine(self, ctx: u4wrhParser.IfLineContext) -> None: with self.debug_context("visitIfLine"): self._flush_pending_condition() self._just_closed_nested = False + self._expecting_if_cond = True return None def visitEndifLine(self, ctx: u4wrhParser.EndifLineContext) -> None: @@ -231,6 +243,9 @@ def visitCondLine(self, ctx: u4wrhParser.CondLineContext) -> None: except ValueError: pass + # Not a hook - check if we need to close existing if-else chain + self._close_if_chain_for_new_rule() + match tag: case "GROUP": if payload is None: @@ -266,6 +281,9 @@ def visitCondLine(self, ctx: u4wrhParser.CondLineContext) -> None: return None case _: + # No percent block - check if we need to close existing if-else chain + self._close_if_chain_for_new_rule() + if body.comparison(): comparison_expr = self._build_comparison_expression(body.comparison()) if comparison_expr != "ERROR": # Skip if error occurred diff --git a/tools/hrw4u/src/tables.py b/tools/hrw4u/src/tables.py index a145d40a822..db08bfe4023 100644 --- a/tools/hrw4u/src/tables.py +++ b/tools/hrw4u/src/tables.py @@ -65,7 +65,9 @@ "keep_query": MapParams(target="rm-destination QUERY", validate=Validator.arg_count(1).quoted_or_simple(), sections=HTTP_SECTIONS), "run-plugin": MapParams(target="run-plugin", validate=Validator.min_args(1).quoted_or_simple(), sections=HTTP_SECTIONS), "set-body-from": MapParams(target="set-body-from", validate=Validator.arg_count(1).quoted_or_simple(), sections=HTTP_SECTIONS), + "set-cc-alg": MapParams(target="set-cc-alg", validate=Validator.arg_count(1).quoted_or_simple(), sections=HTTP_SECTIONS), "set-config": MapParams(target="set-config", validate=Validator.arg_count(2).quoted_or_simple(), sections=HTTP_SECTIONS), + "set-effective-address": MapParams(target="set-effective-address", validate=Validator.arg_count(1).quoted_or_simple(), sections=HTTP_SECTIONS), "set-redirect": MapParams(target="set-redirect", validate=Validator.arg_count(2).arg_at(0, Validator.range(300, 399)).arg_at(1, Validator.quoted_or_simple()), sections=HTTP_SECTIONS), "skip-remap": MapParams(target="skip-remap", validate=Validator.arg_count(1).suffix_group(SuffixGroup.BOOL_FIELDS)._add(Validator.normalize_arg_at(0)), sections={SectionType.PRE_REMAP, SectionType.REMAP, SectionType.READ_REQUEST}), "set-plugin-cntl": MapParams(target="set-plugin-cntl", validate=Validator.arg_count(2)._add(Validator.normalize_arg_at(0)).arg_at(0, Validator.suffix_group(SuffixGroup.PLUGIN_CNTL_FIELDS))._add(Validator.normalize_arg_at(1))._add(Validator.conditional_arg_validation(SuffixGroup.PLUGIN_CNTL_MAPPING.value)), sections=HTTP_SECTIONS), diff --git a/tools/hrw4u/src/types.py b/tools/hrw4u/src/types.py index 0cacafe673b..cd1aae1dac6 100644 --- a/tools/hrw4u/src/types.py +++ b/tools/hrw4u/src/types.py @@ -39,7 +39,9 @@ class MagicStrings(str, Enum): SET_BODY_FROM = "set-body-from" NO_OP = "no-op" SET_DEBUG = "set-debug" + SET_CC_ALG = "set-cc-alg" SET_CONFIG = "set-config" + SET_EFFECTIVE_ADDRESS = "set-effective-address" SET_REDIRECT = "set-redirect" SKIP_REMAP = "skip-remap" RUN_PLUGIN = "run-plugin" diff --git a/tools/hrw4u/tests/data/ops/set-cc-alg.ast.txt b/tools/hrw4u/tests/data/ops/set-cc-alg.ast.txt new file mode 100644 index 00000000000..7d723bb3bb7 --- /dev/null +++ b/tools/hrw4u/tests/data/ops/set-cc-alg.ast.txt @@ -0,0 +1 @@ +(program (programItem (section REMAP { (sectionBody (statement (functionCall set-cc-alg ( (argumentList (value "cubic")) )) ;)) })) ) diff --git a/tools/hrw4u/tests/data/ops/set-cc-alg.input.txt b/tools/hrw4u/tests/data/ops/set-cc-alg.input.txt new file mode 100644 index 00000000000..642ff2f64bf --- /dev/null +++ b/tools/hrw4u/tests/data/ops/set-cc-alg.input.txt @@ -0,0 +1,3 @@ +REMAP { + set-cc-alg("cubic"); +} diff --git a/tools/hrw4u/tests/data/ops/set-cc-alg.output.txt b/tools/hrw4u/tests/data/ops/set-cc-alg.output.txt new file mode 100644 index 00000000000..446f639664a --- /dev/null +++ b/tools/hrw4u/tests/data/ops/set-cc-alg.output.txt @@ -0,0 +1,2 @@ +cond %{REMAP_PSEUDO_HOOK} [AND] + set-cc-alg "cubic" diff --git a/tools/hrw4u/tests/data/ops/set-effective-address.ast.txt b/tools/hrw4u/tests/data/ops/set-effective-address.ast.txt new file mode 100644 index 00000000000..91379a7f997 --- /dev/null +++ b/tools/hrw4u/tests/data/ops/set-effective-address.ast.txt @@ -0,0 +1 @@ +(program (programItem (section REMAP { (sectionBody (statement (functionCall set-effective-address ( (argumentList (value "{inbound.req.Real-IP}")) )) ;)) })) ) diff --git a/tools/hrw4u/tests/data/ops/set-effective-address.input.txt b/tools/hrw4u/tests/data/ops/set-effective-address.input.txt new file mode 100644 index 00000000000..ef958d8a5ee --- /dev/null +++ b/tools/hrw4u/tests/data/ops/set-effective-address.input.txt @@ -0,0 +1,3 @@ +REMAP { + set-effective-address("{inbound.req.Real-IP}"); +} diff --git a/tools/hrw4u/tests/data/ops/set-effective-address.output.txt b/tools/hrw4u/tests/data/ops/set-effective-address.output.txt new file mode 100644 index 00000000000..625d503a2b9 --- /dev/null +++ b/tools/hrw4u/tests/data/ops/set-effective-address.output.txt @@ -0,0 +1,2 @@ +cond %{REMAP_PSEUDO_HOOK} [AND] + set-effective-address "%{CLIENT-HEADER:Real-IP}" From 537f745e2c600a435d14838d433d59bab33177f1 Mon Sep 17 00:00:00 2001 From: Leif Hedstrom Date: Thu, 22 Jan 2026 15:57:12 -0700 Subject: [PATCH 3/3] Adds autest testing This also fixes a few issues in the tool chains --- include/hrw4u/HRW4UVisitor.h | 2 + include/hrw4u/Visitor.h | 2 + plugins/header_rewrite/condition.cc | 4 + plugins/header_rewrite/conditions.cc | 18 +- plugins/header_rewrite/conditions.h | 1 + plugins/header_rewrite/hrw4u.cc | 32 +- plugins/header_rewrite/ruleset.cc | 9 + src/hrw4u/HRW4UVisitorImpl.cc | 29 +- .../header_rewrite_hrw4u.replay.yaml | 1021 +++++++++++++++++ .../header_rewrite_hrw4u.test.py | 24 + .../rules/glob_set_redirect.hrw4u | 27 + .../header_rewrite/rules/implicit_hook.hrw4u | 35 + .../header_rewrite/rules/nested_ifs.hrw4u | 40 + .../header_rewrite/rules/regex_tests.hrw4u | 35 + .../header_rewrite/rules/rule.hrw4u | 23 + .../rules/rule_add_cache_result_header.conf | 2 +- .../rules/rule_add_cache_result_header.hrw4u | 19 + .../header_rewrite/rules/rule_client.hrw4u | 45 + .../rules/rule_cond_method.hrw4u | 27 + .../rules/rule_effective_address.conf | 2 +- .../rules/rule_effective_address.hrw4u | 23 + .../rules/rule_empty_body.hrw4u | 23 + .../header_rewrite/rules/rule_l_value.hrw4u | 25 + .../rules/rule_set_body_from_plugin.hrw4u | 29 + .../rules/rule_set_body_from_remap.hrw4u | 27 + .../rules/rule_set_body_status.hrw4u | 23 + .../rule_set_header_after_ssn_txn_count.conf | 2 +- .../rule_set_header_after_ssn_txn_count.hrw4u | 21 + .../header_rewrite/rules/set_redirect.hrw4u | 19 + tools/hrw4u/src/hrw_symbols.py | 33 +- tools/hrw4u/src/hrw_visitor.py | 5 + tools/hrw_confcmp/comparator.cc | 32 + tools/hrw_confcmp/main.cc | 4 +- tools/hrw_confcmp/run_tests.sh | 172 ++- 34 files changed, 1800 insertions(+), 35 deletions(-) create mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_hrw4u.replay.yaml create mode 100644 tests/gold_tests/pluginTest/header_rewrite/header_rewrite_hrw4u.test.py create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/glob_set_redirect.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/regex_tests.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_client.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_cond_method.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_effective_address.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_empty_body.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_l_value.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_status.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.hrw4u create mode 100644 tests/gold_tests/pluginTest/header_rewrite/rules/set_redirect.hrw4u diff --git a/include/hrw4u/HRW4UVisitor.h b/include/hrw4u/HRW4UVisitor.h index f89df5be611..855d60da9ab 100644 --- a/include/hrw4u/HRW4UVisitor.h +++ b/include/hrw4u/HRW4UVisitor.h @@ -76,6 +76,8 @@ struct CondState { bool nocase_modifier = false; bool ext_modifier = false; bool pre_modifier = false; + bool mid_modifier = false; + bool suf_modifier = false; void reset(); void add_modifier(std::string_view mod); diff --git a/include/hrw4u/Visitor.h b/include/hrw4u/Visitor.h index fb063b363d9..56c2592e83e 100644 --- a/include/hrw4u/Visitor.h +++ b/include/hrw4u/Visitor.h @@ -86,6 +86,7 @@ using AddConditionToGroupCallback = std::function; using NewSectionCallback = std::function; using NewRuleSetSectionCallback = std::function; +using SetRuleSetHookCallback = std::function; using DestroyCallback = std::function; struct FactoryCallbacks { @@ -100,6 +101,7 @@ struct FactoryCallbacks { CreateIfOperatorCallback create_if_operator; NewSectionCallback new_section; NewRuleSetSectionCallback new_ruleset_section; + SetRuleSetHookCallback set_ruleset_hook; DestroyCallback destroy; [[nodiscard]] bool diff --git a/plugins/header_rewrite/condition.cc b/plugins/header_rewrite/condition.cc index 7d282706b76..dcb3d5913f7 100644 --- a/plugins/header_rewrite/condition.cc +++ b/plugins/header_rewrite/condition.cc @@ -196,6 +196,10 @@ Condition::initialize(const hrw::ConditionSpec &spec) _mods |= CondModifiers::MOD_L; } + if (!spec.qualifier.empty()) { + set_qualifier(spec.qualifier); + } + // Parse matcher operation from match_arg std::string arg = spec.match_arg; _cond_op = parse_matcher_op(arg); diff --git a/plugins/header_rewrite/conditions.cc b/plugins/header_rewrite/conditions.cc index 19c7f3dbdf3..1743df8c5ea 100644 --- a/plugins/header_rewrite/conditions.cc +++ b/plugins/header_rewrite/conditions.cc @@ -67,6 +67,9 @@ ConditionStatus::initialize_hooks() { add_allowed_hook(TS_HTTP_READ_RESPONSE_HDR_HOOK); add_allowed_hook(TS_HTTP_SEND_RESPONSE_HDR_HOOK); + require_resources(RSRC_SERVER_RESPONSE_HEADERS); + require_resources(RSRC_CLIENT_RESPONSE_HEADERS); + require_resources(RSRC_RESPONSE_STATUS); } bool @@ -200,6 +203,16 @@ ConditionAccess::eval(const Resources & /* res ATS_UNUSED */) } // ConditionHeader: request or response header +void +ConditionHeader::initialize_hooks() +{ + Condition::initialize_hooks(); + require_resources(RSRC_CLIENT_REQUEST_HEADERS); + require_resources(RSRC_CLIENT_RESPONSE_HEADERS); + require_resources(RSRC_SERVER_REQUEST_HEADERS); + require_resources(RSRC_SERVER_RESPONSE_HEADERS); +} + void ConditionHeader::initialize(Parser &p) { @@ -208,11 +221,6 @@ ConditionHeader::initialize(Parser &p) match->set(p.get_arg(), mods()); _matcher = std::move(match); - - require_resources(RSRC_CLIENT_REQUEST_HEADERS); - require_resources(RSRC_CLIENT_RESPONSE_HEADERS); - require_resources(RSRC_SERVER_REQUEST_HEADERS); - require_resources(RSRC_SERVER_RESPONSE_HEADERS); } void diff --git a/plugins/header_rewrite/conditions.h b/plugins/header_rewrite/conditions.h index fb6e36b7214..0a293451f49 100644 --- a/plugins/header_rewrite/conditions.h +++ b/plugins/header_rewrite/conditions.h @@ -328,6 +328,7 @@ class ConditionHeader : public Condition void append_value(std::string &s, const Resources &res) override; protected: + void initialize_hooks() override; bool eval(const Resources &res) override; private: diff --git a/plugins/header_rewrite/hrw4u.cc b/plugins/header_rewrite/hrw4u.cc index a1bffa881e9..035f7f9e41b 100644 --- a/plugins/header_rewrite/hrw4u.cc +++ b/plugins/header_rewrite/hrw4u.cc @@ -209,7 +209,13 @@ namespace auto *ruleset = static_cast(rule); auto *cond = static_cast(condition); - auto *group = ruleset->get_group(); + + if (!cond->set_hook(ruleset->get_hook())) { + TSError("[header_rewrite:hrw4u] can't use this condition in hook=%s", TSHttpHookNameLookup(ruleset->get_hook())); + return false; + } + + auto *group = ruleset->get_group(); if (group) { group->add_condition(cond); @@ -243,6 +249,10 @@ namespace auto *op_if = static_cast(op_if_ptr); auto *cond = static_cast(condition); + + // Note: We don't set the hook here because the OperatorIf's hook isn't set yet. + // The hook will be set when the OperatorIf is added to the RuleSet. + auto *group = op_if->get_group(); if (group) { @@ -279,7 +289,11 @@ namespace auto *op_if = static_cast(op_if_ptr); auto *operator_ = static_cast(op); - auto *cur_sec = op_if->cur_section(); + + // Note: We don't set the hook here because the OperatorIf's hook isn't set yet. + // The hook will be set when the OperatorIf is added to the RuleSet. + + auto *cur_sec = op_if->cur_section(); if (!cur_sec) { return false; @@ -349,6 +363,19 @@ namespace delete static_cast(ptr); } } + + static void + set_ruleset_hook(void *ruleset_ptr, int section_type) + { + if (!ruleset_ptr) { + return; + } + + auto *ruleset = static_cast(ruleset_ptr); + TSHttpHookID hook = section_to_hook(section_type); + + ruleset->set_hook(hook); + } }; hrw4u::FactoryCallbacks @@ -366,6 +393,7 @@ namespace callbacks.create_if_operator = FactoryBridge::create_if_operator; callbacks.new_section = FactoryBridge::new_section; callbacks.new_ruleset_section = FactoryBridge::new_ruleset_section; + callbacks.set_ruleset_hook = FactoryBridge::set_ruleset_hook; callbacks.destroy = FactoryBridge::destroy; return callbacks; diff --git a/plugins/header_rewrite/ruleset.cc b/plugins/header_rewrite/ruleset.cc index 28fda37a511..ba120a210f9 100644 --- a/plugins/header_rewrite/ruleset.cc +++ b/plugins/header_rewrite/ruleset.cc @@ -136,6 +136,15 @@ RuleSet::get_all_resource_ids() const bool RuleSet::add_operator(Operator *op) { + // OperatorIf is a pseudo-operator container - it doesn't need hook validation itself. + if (op->type_name() != "OperatorIf") { + if (!op->set_hook(_hook)) { + Dbg(pi_dbg_ctl, "can't use this operator in hook=%s", TSHttpHookNameLookup(_hook)); + TSError("[%s] can't use this operator in hook=%s", PLUGIN_NAME, TSHttpHookNameLookup(_hook)); + return false; + } + } + auto *cur_sec = _op_if.cur_section(); if (!cur_sec->ops.oper) { diff --git a/src/hrw4u/HRW4UVisitorImpl.cc b/src/hrw4u/HRW4UVisitorImpl.cc index c19a37a07a8..6ec2b9aff30 100644 --- a/src/hrw4u/HRW4UVisitorImpl.cc +++ b/src/hrw4u/HRW4UVisitorImpl.cc @@ -169,6 +169,13 @@ HRW4UVisitorImpl::get_or_create_ruleset() if (_current_ruleset == nullptr && _callbacks.create_ruleset) { _current_ruleset = _callbacks.create_ruleset(); track_object(_current_ruleset, "ruleset"); + + // Set the hook on the ruleset immediately so operators added to it get the correct hook + if (_current_ruleset && _callbacks.set_ruleset_hook) { + // Use current section if set, otherwise use the default hook from config + SectionType effective_section = (_current_section != SectionType::UNKNOWN) ? _current_section : _config.default_hook; + _callbacks.set_ruleset_hook(_current_ruleset, static_cast(effective_section)); + } } return _current_ruleset; } @@ -816,9 +823,15 @@ HRW4UVisitorImpl::visitConditional(hrw4uParser::ConditionalContext *ctx) if (ctx->ifStatement()->condition()) { visit(ctx->ifStatement()->condition()); } + + // Push marker so nested conditionals know they're not at section level + IfBlockState state{.op_if = nullptr, .clause_index = 0}; + + _if_stack.push(state); if (ctx->ifStatement()->block()) { visit(ctx->ifStatement()->block()); } + _if_stack.pop(); } } else if (is_section_level && has_elif_else) { IfBlockState state{.op_if = nullptr, .clause_index = 0}; @@ -1311,7 +1324,7 @@ HRW4UVisitorImpl::process_identifier_condition(const std::string &ident, bool ne bool actual_negation = negated; if (result.prefix) { - arg = "=\"\""; + arg = "="; actual_negation = !negated; } @@ -1482,6 +1495,8 @@ CondState::reset() nocase_modifier = false; ext_modifier = false; pre_modifier = false; + mid_modifier = false; + suf_modifier = false; } void @@ -1501,6 +1516,10 @@ CondState::add_modifier(std::string_view mod) ext_modifier = true; } else if (mod == "PRE") { pre_modifier = true; + } else if (mod == "MID") { + mid_modifier = true; + } else if (mod == "SUF") { + suf_modifier = true; } } @@ -1530,6 +1549,12 @@ CondState::to_list() const if (pre_modifier) { result.push_back("PRE"); } + if (mid_modifier) { + result.push_back("MID"); + } + if (suf_modifier) { + result.push_back("SUF"); + } return result; } @@ -1643,7 +1668,7 @@ bool ModifierInfo::is_condition_modifier(std::string_view mod) { return mod == "NOT" || mod == "N" || mod == "OR" || mod == "O" || mod == "AND" || mod == "NC" || mod == "NOCASE" || mod == "I" || - mod == "EXT" || mod == "PRE"; + mod == "EXT" || mod == "PRE" || mod == "MID" || mod == "SUF"; } bool diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_hrw4u.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_hrw4u.replay.yaml new file mode 100644 index 00000000000..5ecd4d33ccc --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_hrw4u.replay.yaml @@ -0,0 +1,1021 @@ +# 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. + +meta: + version: "1.0" + +# Configuration section for autest integration +autest: + description: 'Test header_rewrite with hrw4u configuration format' + + dns: + name: 'dns' + + server: + name: 'server' + + client: + name: 'client' + process_config: + other_args: '--thread-limit 1' + + ats: + name: 'ts' + + copy_to_config_dir: + - 'rules' + + records_config: + proxy.config.http.insert_response_via_str: 0 + proxy.config.http.auth_server_session_private: 1 + proxy.config.http.server_session_sharing.pool: 'global' + proxy.config.http.server_session_sharing.match: 'both' + proxy.config.diags.debug.enabled: 1 + proxy.config.diags.debug.tags: 'header_rewrite' + + remap_config: + - from: "http://no_path.com/" + to: "http://no_path.com?name=brian/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/set_redirect.hrw4u" + + - from: "http://www.example.com/from_1/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_1/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_client.hrw4u" + + - from: "http://www.example.com/from_2/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_2/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_cond_method.hrw4u" + + - from: "http://www.example.com/from_3/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_3/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_l_value.hrw4u" + + - from: "http://www.example.com/from_4/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_4/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_header_after_ssn_txn_count.hrw4u" + + - from: "http://www.example.com/from_5/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_5/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_add_cache_result_header.hrw4u" + + - from: "http://www.example.com/from_6/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_6/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_effective_address.hrw4u" + + - from: "http://www.example.com/from_7/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_7/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule.hrw4u" + + - from: "http://www.example.com/from_8/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_8/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/implicit_hook.hrw4u" + + - from: "http://www.example.com/from_9/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_9/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/regex_tests.hrw4u" + + - from: "http://www.example.com/from_10/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_10/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_empty_body.hrw4u" + + - from: "http://www.example.com/from_11/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_11/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/rule_set_body_status.hrw4u" + + - from: "http://www.example.com/from_12/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_12/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/nested_ifs.hrw4u" + + + +# Proxy verifier sessions +sessions: + # Test 1: Setup cache hit for tests later +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_5/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 1 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + - [ Cache-Control, "max-age=5,public" ] + content: + size: 6 + data: "CACHED" + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Result, { value: "miss", as: equal } ] + + # Test 2: TO-URL redirect test +- transactions: + - client-request: + method: "HEAD" + version: "1.1" + url: / + headers: + fields: + - [ Host, no_path.com ] + - [ uuid, 2 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 301 + reason: Redirect + headers: + fields: + - [ Location, { value: "http://no_path.com?name=brian/", as: equal } ] + + # Test 3: CLIENT-URL test +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_1/hello?=foo=bar + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 3 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 304 + reason: "Not Modified" + headers: + fields: + - [ Cache-Control, { value: "no-store", as: equal } ] + - [ X-Pre-Else, { value: "Yes", as: equal } ] + - [ X-Testing, { value: "No", as: equal } ] + - [ X-Extension, { as: absent } ] + + # Test 4: sets matching +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_1/hrw-sets.png + headers: + fields: + - [ Host, www.example.com ] + - [ X-Testing, "foo,bar" ] + - [ uuid, 4 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Extension, { value: "Yes", as: equal } ] + - [ X-Testing, { value: "Yes", as: equal } ] + - [ X-Pre-Else, { as: absent } ] + + # Test 5: elif condition +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_1/hrw-sets.png + headers: + fields: + - [ Host, www.example.com ] + - [ X-Testing, "elif" ] + - [ uuid, 5 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Extension, { value: "Yes", as: equal } ] + - [ X-Testing, { value: "elif", as: equal } ] + - [ X-Pre-Else, { as: absent } ] + + # Test 6: cond method GET +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_2/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 6 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ Via, { as: present } ] + + # Test 7: cond method DELETE +- transactions: + - client-request: + method: "DELETE" + version: "1.1" + url: /from_2/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 7 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ Via, { as: present } ] + + # Test 8: End [L] #5423 +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_3/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 8 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-First, { value: "First", as: equal } ] + - [ X-Last, { value: "Last", as: equal } ] + - [ X-Not-Here, { as: absent } ] + + # Test 9: SSN-TXN-COUNT condition - multiple transactions in same session +- transactions: + # First transaction + - client-request: + method: "GET" + version: "1.1" + url: /from_4/hello + headers: + fields: + - [ Host, www.example.com ] + - [ Connection, keep-alive ] + - [ Content-Length, "0" ] + - [ uuid, 9 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Server, microserver ] + - [ Content-Length, "0" ] + + proxy-response: + status: 200 + + # Second transaction + - client-request: + method: "GET" + version: "1.1" + url: /from_4/hello + headers: + fields: + - [ Host, www.example.com ] + - [ Connection, keep-alive ] + - [ Content-Length, "0" ] + - [ uuid, 10 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Server, microserver ] + - [ Content-Length, "0" ] + + proxy-response: + status: 200 + + # Third transaction + - client-request: + method: "GET" + version: "1.1" + url: /from_4/hello + headers: + fields: + - [ Host, www.example.com ] + - [ Connection, keep-alive ] + - [ Content-Length, "0" ] + - [ uuid, 11 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Server, microserver ] + - [ Content-Length, "0" ] + + proxy-response: + status: 200 + + # Fourth transaction + - client-request: + method: "GET" + version: "1.1" + url: /from_4/hello + headers: + fields: + - [ Host, www.example.com ] + - [ Connection, keep-alive ] + - [ Content-Length, "0" ] + - [ uuid, 12 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Server, microserver ] + - [ Content-Length, "0" ] + + proxy-response: + status: 200 + headers: + fields: + - [ Connection, { value: "close", as: equal } ] + + # Fifth transaction (with Connection: close) + - client-request: + method: "GET" + version: "1.1" + url: /from_4/world + headers: + fields: + - [ Host, www.example.com ] + - [ Connection, close ] + - [ Content-Length, "0" ] + - [ uuid, 13 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Server, microserver ] + - [ Connection, close ] + - [ Content-Length, "0" ] + + proxy-response: + status: 200 + headers: + fields: + - [ Connection, { value: "close", as: equal } ] + +# Test 10: Cache condition test - multiple requests +# Note: This test involves sleep delays and cache state changes +- transactions: + # First request - cache hit-fresh + - client-request: + # Make sure I/O for writing the cache is complete. + delay: 100ms + + method: "GET" + version: "1.1" + url: /from_5/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 14 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + - [ Cache-Control, "max-age=5,public" ] + content: + size: 6 + data: "CACHED" + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Result, { value: "hit-fresh", as: equal } ] + + # Note: sleep 8 seconds here - subsequent requests will show stale behavior + # Second request - after cache expires (hit-stale) + - client-request: + method: "GET" + version: "1.1" + url: /from_5/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 15 ] + delay: 8s + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + - [ Cache-Control, "max-age=5,public" ] + content: + size: 6 + data: "CACHED" + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Result, { value: "hit-stale", as: equal } ] + + # Third request - should hit fresh cache + - client-request: + method: "GET" + version: "1.1" + url: /from_5/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 16 ] + + # The server-response should not be served. + server-response: + status: 500 + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Result, { value: "hit-fresh", as: equal } ] + +# Test 11: Effective address test +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_6/ + headers: + fields: + - [ Host, www.example.com ] + - [ Real-IP, "1.2.3.4" ] + - [ uuid, 17 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ Effective-IP, { value: "1.2.3.4", as: equal } ] + +# Test 12: Status change test (200 to 303) +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_7/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 18 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 303 + +# Test 13: Implicit hook test - no X-Fie header +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_8/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 19 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Response-Foo, { value: "No", as: equal } ] + +# Test 14: Implicit hook test - X-Fie: Fie +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_8/ + headers: + fields: + - [ Host, www.example.com ] + - [ X-Fie, "Fie" ] + - [ uuid, 20 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Response-Foo, { value: "Yes", as: equal } ] + +# Test 15: Implicit hook test - X-Client-Foo: fOoBar +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_8/ + headers: + fields: + - [ Host, www.example.com ] + - [ X-Client-Foo, "fOoBar" ] + - [ uuid, 21 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Response-Foo, { value: "Prefix", as: equal } ] + +# Test 16: Regex test - no additional headers +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_9/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 22 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Match-2, { value: yes, as: equal } ] + - [ X-Match, { as: absent } ] + +# Test 17: Regex test - X-Test1: Foobar +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_9/ + headers: + fields: + - [ Host, www.example.com ] + - [ X-Test1, "Foobar" ] + - [ uuid, 23 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Match, { value: yes, as: equal } ] + - [ X-Match-2, { value: yes, as: equal } ] + +# Test 18: Regex test - X-Test1: none +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_9/ + headers: + fields: + - [ Host, www.example.com ] + - [ X-Test1, "none" ] + - [ uuid, 24 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Match-2, { value: yes, as: equal } ] + - [ X-Match, { as: absent } ] + +# Test 19: Regex test - query parameter capture +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_9/?uid=123 + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 25 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Match-3, { value: "123", as: equal } ] + - [ X-Match-2, { as: absent } ] + +# Test 20: set-body with empty string +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_10/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 26 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + content: + size: 31 + data: "ATS should not serve this body" + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "no-store", as: equal } ] + - [ Content-Length, { value: "0", as: equal } ] + content: + size: 0 + +# Test 21: set-body with STATUS variable +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_11/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 27 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + content: + size: 31 + data: "ATS should not serve this body" + + proxy-response: + status: 200 + headers: + fields: + - [ Cache-Control, { value: "no-store", as: equal } ] + - [ Content-Length, { value: "3", as: equal } ] + content: + data: "200" + +# Test 22: Nested if/elif/else - X-Foo=foo + X-Bar=bar path +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_12/ + headers: + fields: + - [ Host, www.example.com ] + - [ X-Foo, "foo" ] + - [ X-Bar, "bar" ] + - [ uuid, 28 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-When-200-Before, { value: "Yes", as: equal } ] + - [ X-Foo, { value: "Yes", as: equal } ] + - [ X-Foo-And-Bar, { value: "Yes", as: equal } ] + - [ X-Foo-And-Fie, { as: absent } ] + - [ X-Fie-Anywhere, { as: absent } ] + - [ X-When-200-After, { value: "Yes", as: equal } ] + +# Test 23: Nested if/elif/else - X-Foo=foo + X-Fie=fie path +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_12/ + headers: + fields: + - [ Host, www.example.com ] + - [ X-Foo, "foo" ] + - [ X-Fie, "fie" ] + - [ uuid, 29 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-When-200-Before, { value: "Yes", as: equal } ] + - [ X-Foo, { value: "Yes", as: equal } ] + - [ X-Foo-And-Bar, { as: absent } ] + - [ X-Foo-And-Fie, { value: "Yes", as: equal } ] + - [ X-Fie-Anywhere, { value: "Yes", as: equal } ] + - [ X-When-200-After, { value: "Yes", as: equal } ] + +# Test 24: Nested if/elif/else - X-Foo=maybe path +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_12/ + headers: + fields: + - [ Host, www.example.com ] + - [ X-Foo, "maybe" ] + - [ uuid, 30 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-When-200-Before, { value: "Yes", as: equal } ] + - [ X-Foo, { value: "Maybe", as: equal } ] + - [ X-Foo-And-Bar, { as: absent } ] + - [ X-Foo-And-Fie, { as: absent } ] + - [ X-Fie-Anywhere, { as: absent } ] + - [ X-When-200-After, { value: "Yes", as: equal } ] + +# Test 25: Nested if/elif/else - X-Foo=definitely path +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_12/ + headers: + fields: + - [ Host, www.example.com ] + - [ X-Foo, "definitely" ] + - [ uuid, 31 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-When-200-Before, { value: "Yes", as: equal } ] + - [ X-Foo, { value: "Definitely", as: equal } ] + - [ X-Foo-And-Bar, { as: absent } ] + - [ X-Foo-And-Fie, { as: absent } ] + - [ X-Fie-Anywhere, { as: absent } ] + - [ X-When-200-After, { value: "Yes", as: equal } ] + +# Test 26: Nested if/elif/else - else path (no X-Foo) +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_12/ + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 32 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-When-200-Before, { value: "Yes", as: equal } ] + - [ X-Foo, { value: "Nothing", as: equal } ] + - [ X-Foo-And-Bar, { as: absent } ] + - [ X-Foo-And-Fie, { as: absent } ] + - [ X-Fie-Anywhere, { as: absent } ] + - [ X-When-200-After, { value: "Yes", as: equal } ] + +# Test 27: Nested if/elif/else - else path with X-Fie +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_12/ + headers: + fields: + - [ Host, www.example.com ] + - [ X-Fie, "fie" ] + - [ uuid, 33 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-When-200-Before, { value: "Yes", as: equal } ] + - [ X-Foo, { value: "Nothing", as: equal } ] + - [ X-Foo-And-Bar, { as: absent } ] + - [ X-Foo-And-Fie, { as: absent } ] + - [ X-Fie-Anywhere, { value: "Yes", as: equal } ] + - [ X-When-200-After, { value: "Yes", as: equal } ] diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_hrw4u.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_hrw4u.test.py new file mode 100644 index 00000000000..8be67cb64d4 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_hrw4u.test.py @@ -0,0 +1,24 @@ +''' +Test header_rewrite with hrw4u configuration format. +''' +# 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.Summary = ''' +A set of remap rules and tests for header_rewrite using hrw4u configuration format. +''' + +Test.ATSReplayTest(replay_file="header_rewrite_hrw4u.replay.yaml",) diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/glob_set_redirect.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/glob_set_redirect.hrw4u new file mode 100644 index 00000000000..ae70990fe29 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/glob_set_redirect.hrw4u @@ -0,0 +1,27 @@ +# +# 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. +READ_RESPONSE { + if id.REQUEST == 0 { + set-redirect(301, "http://redirect.com/here"); + } +} + +SEND_RESPONSE { + if id.REQUEST == 1 { + set-redirect(301, "http://redirect.com/here"); + } +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.hrw4u new file mode 100644 index 00000000000..2522960c71f --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/implicit_hook.hrw4u @@ -0,0 +1,35 @@ +# +# 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. +SEND_RESPONSE { + if inbound.req.X-Client-Foo == "foo" with NOCASE,PRE { + inbound.resp.X-Response-Foo = "Prefix"; + } elif inbound.req.X-Client-Foo == "bar" { + inbound.resp.X-Response-Foo = "Never"; + } elif inbound.req.X-Client-Foo { + inbound.resp.X-Response-Foo = "Yes"; + } else { + inbound.resp.X-Response-Foo = "No"; + } +} + +REMAP { + if inbound.req.X-Fie == "fie" with NOCASE { + inbound.req.X-Client-Foo += "Yes"; + } elif inbound.req.X-Fie == "nope" { + inbound.req.X-client-Foo += "Yes"; + } +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.hrw4u new file mode 100644 index 00000000000..666b2e9cba9 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/nested_ifs.hrw4u @@ -0,0 +1,40 @@ +# +# 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. +# +SEND_RESPONSE { + if inbound.status == 200 { + inbound.resp.X-When-200-Before = "Yes"; + if inbound.req.X-Foo == "foo" { + inbound.resp.X-Foo = "Yes"; + if inbound.req.X-Bar == "bar" with NOCASE { + inbound.resp.X-Foo-And-Bar = "Yes"; + } elif inbound.req.X-Fie == "fie" with NOCASE { + inbound.resp.X-Foo-And-Fie = "Yes"; + } + } elif inbound.req.X-Foo == "maybe" { + inbound.resp.X-Foo = "Maybe"; + } elif inbound.req.X-Foo == "definitely" { + inbound.resp.X-Foo = "Definitely"; + } else { + inbound.resp.X-Foo = "Nothing"; + } + if inbound.req.X-Fie == "fie" with NOCASE { + inbound.resp.X-Fie-Anywhere = "Yes"; + } + inbound.resp.X-When-200-After = "Yes"; + } +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/regex_tests.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/regex_tests.hrw4u new file mode 100644 index 00000000000..79767422e8e --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/regex_tests.hrw4u @@ -0,0 +1,35 @@ +# +# 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. +SEND_RESPONSE { + if inbound.req.X-Test1 ~ /^[Ff]o+[bB]ar/ { + inbound.resp.X-Match = "yes"; + } +} + +SEND_RESPONSE { + if inbound.url.query ~ /uid=([0-9]+)/ { + inbound.resp.X-Match-3 = "{capture.1}"; + } elif inbound.req.X-We-Set ~ /^fie[0-9]+fum$/ with NOCASE { + inbound.resp.X-Match-2 = "yes"; + } +} + +REMAP { + if inbound.url.path ~ /^from/ { + inbound.req.X-We-Set = "FIe123fuM"; + } +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule.hrw4u new file mode 100644 index 00000000000..1b97beb0361 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule.hrw4u @@ -0,0 +1,23 @@ +# +# 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. +READ_RESPONSE { + if outbound.status == 200 { + outbound.status = 303; + } elif outbound.status == 503 { + outbound.status = 502; + } +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.conf index 25b4f7e6107..e4f9fb24347 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.conf +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.conf @@ -16,4 +16,4 @@ # limitations under the License. cond %{SEND_RESPONSE_HDR_HOOK} - set-header Cache-Result %{CACHE} \ No newline at end of file + set-header Cache-Result %{CACHE} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.hrw4u new file mode 100644 index 00000000000..62ca3a1f6d2 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_add_cache_result_header.hrw4u @@ -0,0 +1,19 @@ +# +# 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. +SEND_RESPONSE { + inbound.resp.Cache-Result = "{cache()}"; +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_client.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_client.hrw4u new file mode 100644 index 00000000000..15c4da28776 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_client.hrw4u @@ -0,0 +1,45 @@ +# +# 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. +REMAP { + if inbound.url.path ~ /^from_1\// && inbound.url.scheme == http && inbound.url.host == www.example.com && inbound.url.query ~ /foo=bar/ { + inbound.status = 304; + } +} + +SEND_RESPONSE { + if inbound.url.path in [png, gif, jpeg] with EXT,NOCASE { + inbound.resp.X-Extension = "Yes"; + } +} + +SEND_RESPONSE { + if inbound.url.path in [hrw, foo] with NOCASE,MID { + no-op(); + } else { + inbound.resp.X-Pre-Else = "Yes"; + } +} + +SEND_RESPONSE { + if inbound.req.X-Testing in [foo, bar, "foo,bar"] { + inbound.resp.X-Testing = "Yes"; + } elif inbound.req.X-Testing == "elif" { + inbound.resp.X-Testing = "elif"; + } else { + inbound.resp.X-Testing = "No"; + } +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_cond_method.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_cond_method.hrw4u new file mode 100644 index 00000000000..60e8a66a7b7 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_cond_method.hrw4u @@ -0,0 +1,27 @@ +# +# 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. +REMAP { + if inbound.method in [GET, PUSH] { + set-config("proxy.config.http.insert_response_via_str", 1); + } +} + +SEND_REQUEST { + if outbound.method == DELETE { + set-config("proxy.config.http.insert_response_via_str", 1); + } +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_effective_address.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_effective_address.conf index f7c84b18a5c..4be16a30bd2 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_effective_address.conf +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_effective_address.conf @@ -16,7 +16,7 @@ # limitations under the License. cond %{REMAP_PSEUDO_HOOK} - set-effective-address %{HEADER:Real-IP} + set-effective-address %{CLIENT-HEADER:Real-IP} cond %{SEND_RESPONSE_HDR_HOOK} set-header Effective-IP %{INBOUND:REMOTE-ADDR} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_effective_address.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_effective_address.hrw4u new file mode 100644 index 00000000000..a04c8c459ef --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_effective_address.hrw4u @@ -0,0 +1,23 @@ +# +# 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. +REMAP { + set-effective-address("{inbound.req.Real-IP}"); +} + +SEND_RESPONSE { + inbound.resp.Effective-IP = "{inbound.conn.REMOTE-ADDR}"; +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_empty_body.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_empty_body.hrw4u new file mode 100644 index 00000000000..bd59c145797 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_empty_body.hrw4u @@ -0,0 +1,23 @@ +# +# 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. +REMAP { + inbound.status = 200; +} + +SEND_RESPONSE { + inbound.resp.body = ""; +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_l_value.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_l_value.hrw4u new file mode 100644 index 00000000000..311e211bdb7 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_l_value.hrw4u @@ -0,0 +1,25 @@ +# +# 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. +SEND_RESPONSE { + inbound.resp.X-First = "First"; + inbound.resp.X-Last = "Last"; + break; +} + +SEND_RESPONSE { + inbound.resp.X-Not-Here = "Stop"; +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.hrw4u new file mode 100644 index 00000000000..4d57b082536 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_plugin.hrw4u @@ -0,0 +1,29 @@ +# +# 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 uses cond %{CLIENT-URL:PATH} to differentiate tests +# It is not needed to make set-body-from work +READ_RESPONSE { + if inbound.url.path == "plugin_success" { + set-body-from("http://www.example.com/404.html"); + } +} + +READ_RESPONSE { + if inbound.url.path == "plugin_fail" { + set-body-from("http://www.example.com/plugin_no_server"); + } +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.hrw4u new file mode 100644 index 00000000000..31c3a6f6c95 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_from_remap.hrw4u @@ -0,0 +1,27 @@ +# +# 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. +READ_RESPONSE { + if inbound.url.path == "remap_success" || inbound.url.path == "200" { + set-body-from("http://www.example.com/404.html"); + } +} + +READ_RESPONSE { + if inbound.url.path == "remap_fail" { + set-body-from("http://www.example.com/fail"); + } +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_status.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_status.hrw4u new file mode 100644 index 00000000000..5afa3fae975 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_body_status.hrw4u @@ -0,0 +1,23 @@ +# +# 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. +REMAP { + inbound.status = 200; +} + +SEND_RESPONSE { + inbound.resp.body = "{inbound.status}"; +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.conf index ab3fdd36536..7565763bf41 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.conf +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.conf @@ -16,4 +16,4 @@ # limitations under the License. cond %{SEND_RESPONSE_HDR_HOOK} [AND] cond %{SSN-TXN-COUNT} >2 - set-header Connection close \ No newline at end of file + set-header Connection close diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.hrw4u new file mode 100644 index 00000000000..5e890bcd57b --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_set_header_after_ssn_txn_count.hrw4u @@ -0,0 +1,21 @@ +# +# 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. +SEND_RESPONSE { + if ssn-txn-count() > 2 { + inbound.resp.Connection = "close"; + } +} diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/set_redirect.hrw4u b/tests/gold_tests/pluginTest/header_rewrite/rules/set_redirect.hrw4u new file mode 100644 index 00000000000..2ec22d0c0e3 --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/set_redirect.hrw4u @@ -0,0 +1,19 @@ +# +# 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. +REMAP { + set-redirect(301, "{to.url.url}"); +} diff --git a/tools/hrw4u/src/hrw_symbols.py b/tools/hrw4u/src/hrw_symbols.py index 34870279775..557fcd0136f 100644 --- a/tools/hrw4u/src/hrw_symbols.py +++ b/tools/hrw4u/src/hrw_symbols.py @@ -341,14 +341,43 @@ def convert_set_to_brackets(self, set_text: str) -> str: if set_text.startswith('(') and set_text.endswith(')'): content = set_text[1:-1] - content = ', '.join(item.strip() for item in content.split(',')) + items = self._split_set_items(content) + content = ', '.join(item.strip() for item in items) return '[' + content + ']' elif set_text.startswith('[') and set_text.endswith(']'): content = set_text[1:-1] - content = ', '.join(item.strip() for item in content.split(',')) + items = self._split_set_items(content) + content = ', '.join(item.strip() for item in items) return '[' + content + ']' return set_text + def _split_set_items(self, content: str) -> list[str]: + """Split set items on commas, respecting quoted strings.""" + items = [] + current = [] + in_quotes = False + quote_char = None + + for char in content: + if char in '"\'': + if not in_quotes: + in_quotes = True + quote_char = char + elif char == quote_char: + in_quotes = False + quote_char = None + current.append(char) + elif char == ',' and not in_quotes: + items.append(''.join(current)) + current = [] + else: + current.append(char) + + if current: + items.append(''.join(current)) + + return items + def format_iprange(self, iprange_text: str) -> str: """Format IP range with proper spacing.""" try: diff --git a/tools/hrw4u/src/hrw_visitor.py b/tools/hrw4u/src/hrw_visitor.py index 62d2a8c4033..29b3b6f8027 100644 --- a/tools/hrw4u/src/hrw_visitor.py +++ b/tools/hrw4u/src/hrw_visitor.py @@ -387,6 +387,11 @@ def visitOpLine(self, ctx: u4wrhParser.OpLineContext) -> None: stmt = self.symbol_resolver.op_to_hrw4u(cmd, args, self._section_label, op_state) self.emit(stmt + ";") + # If [L] modifier was on a non-no-op operator, emit break; after the statement + # (no-op [L] is already converted to "break" by op_to_hrw4u) + if op_state.last and cmd != "no-op": + self.emit("break;") + return None # Condition block lifecycle methods - specific to inverse visitor diff --git a/tools/hrw_confcmp/comparator.cc b/tools/hrw_confcmp/comparator.cc index 84e5a5d60b9..b488bca9fa8 100644 --- a/tools/hrw_confcmp/comparator.cc +++ b/tools/hrw_confcmp/comparator.cc @@ -24,6 +24,7 @@ #include "conditions.h" #include "matcher.h" +#include #include #include @@ -315,14 +316,45 @@ ConfigComparator::compare_statement_chains(Statement *s1, Statement *s2, const s bool all_match = true; int count1 = 0, count2 = 0; std::vector types1, types2; + std::vector stmts1, stmts2; for (Statement *s = s1; s; s = s->next()) { count1++; types1.push_back(std::string(s->type_name())); + stmts1.push_back(s); } for (Statement *s = s2; s; s = s->next()) { count2++; types2.push_back(std::string(s->type_name())); + stmts2.push_back(s); + } + + // Handle semantic equivalence: [Operator[L]] == [Operator, OperatorNoOp[L]] + // When one chain has an operator with [L], and the other has the same operator + // followed by a trailing OperatorNoOp with [L], they are semantically equivalent. + if (count1 != count2 && std::abs(count1 - count2) == 1) { + std::vector &shorter = (count1 < count2) ? stmts1 : stmts2; + std::vector &longer = (count1 < count2) ? stmts2 : stmts1; + Statement *last_longer = longer.back(); + auto *last_op = dynamic_cast(last_longer); + + if (last_op && last_op->type_name() == "OperatorNoOp" && (last_op->get_oper_modifiers() & OPER_LAST)) { + Statement *last_shorter = shorter.back(); + auto *last_shorter_op = dynamic_cast(last_shorter); + + if (last_shorter_op && (last_shorter_op->get_oper_modifiers() & OPER_LAST)) { + longer.pop_back(); + for (size_t i = 0; i < shorter.size(); i++) { + std::string ctx = context + "[" + std::to_string(i) + "]"; + + if (!compare_single_statement(shorter[i], longer[i], ctx)) { + all_match = false; + } + } + + return all_match; + } + } } while (s1 || s2) { diff --git a/tools/hrw_confcmp/main.cc b/tools/hrw_confcmp/main.cc index c76a5d3441d..52bba0ff34f 100644 --- a/tools/hrw_confcmp/main.cc +++ b/tools/hrw_confcmp/main.cc @@ -92,7 +92,7 @@ compare_pair(const char *hrw_file, const char *hrw4u_file, bool debug, bool quie auto t_hrw_start = Clock::now(); RulesConfig *hrw_config = create_rules_config(0, 0); - if (!rules_config_parse(hrw_config, hrw_file, TS_HTTP_READ_RESPONSE_HDR_HOOK, nullptr, nullptr, false)) { + if (!rules_config_parse(hrw_config, hrw_file, TS_HTTP_LAST_HOOK, nullptr, nullptr, false)) { std::cerr << "ERROR: Failed to parse hrw config file: " << hrw_file << "\n"; destroy_rules_config(hrw_config); @@ -107,7 +107,7 @@ compare_pair(const char *hrw_file, const char *hrw4u_file, bool debug, bool quie std::cout << "Parsing hrw4u config: " << hrw4u_file << " (using native hrw4u parser)\n"; } - if (!rules_config_parse(hrw4u_config, hrw4u_file, TS_HTTP_READ_RESPONSE_HDR_HOOK, nullptr, nullptr, true)) { + if (!rules_config_parse(hrw4u_config, hrw4u_file, TS_HTTP_LAST_HOOK, nullptr, nullptr, true)) { std::cerr << "ERROR: Failed to parse hrw4u config file: " << hrw4u_file << "\n"; destroy_rules_config(hrw_config); destroy_rules_config(hrw4u_config); diff --git a/tools/hrw_confcmp/run_tests.sh b/tools/hrw_confcmp/run_tests.sh index ca08fc35c80..e7db2ad4852 100755 --- a/tools/hrw_confcmp/run_tests.sh +++ b/tools/hrw_confcmp/run_tests.sh @@ -46,6 +46,7 @@ fi USE_BATCH=false SHOW_HELP=false +DEBUG_MODE=false TEST_DIRS=() # Parse arguments @@ -55,6 +56,10 @@ while [[ $# -gt 0 ]]; do USE_BATCH=true shift ;; + --debug) + DEBUG_MODE=true + shift + ;; --help|-h) SHOW_HELP=true shift @@ -67,17 +72,29 @@ while [[ $# -gt 0 ]]; do done if [[ "$SHOW_HELP" == true ]] || [[ ${#TEST_DIRS[@]} -eq 0 ]]; then - echo "Usage: $0 [--batch] [test_directory...]" + echo "Usage: $0 [--batch] [--debug] [test_directory...]" echo "" echo "Options:" echo " --batch Use batch mode for faster execution (single process)" + echo " --debug Stop after first failure and show detailed comparison" + echo "" + echo "The script will automatically test:" + echo " 1. Original config files (.conf, .config, .hrw) vs their .hrw4u conversions" + echo " 2. Standard test pairs (*.input.txt + *.output.txt)" echo "" - echo "Example:" + echo "Examples:" echo " $0 tools/hrw4u/tests/data/vars" echo " $0 --batch tools/hrw4u/tests/data/{hooks,conds,ops,vars}" + echo " $0 --debug tests/gold_tests/pluginTest/header_rewrite/rules" exit 1 fi +# Debug mode forces serial mode (can't stop on first failure in batch mode) +if [[ "$DEBUG_MODE" == true ]] && [[ "$USE_BATCH" == true ]]; then + echo "Note: --debug mode forces serial execution (incompatible with --batch)" + USE_BATCH=false +fi + # Get time in milliseconds (portable across macOS/Linux) get_time_ms() { if [[ "$OSTYPE" == "darwin"* ]]; then @@ -89,16 +106,21 @@ get_time_ms() { fi } -# Check if a test should be skipped based on exceptions.txt +# Check if a test should be skipped based on exceptions is_test_excepted() { local test_dir="$1" local test_name="$2" - # Only except examples/all-nonsense + # examples/all-nonsense: Intentionally invalid syntax for testing error handling if [[ "$test_dir" =~ /examples$ ]] && [[ "$test_name" == "all-nonsense" ]]; then return 0 # Test is excepted fi + # ops/skip-remap: Uses READ_REQUEST_PRE_REMAP_HOOK which cannot be used in remap rules + if [[ "$test_dir" =~ /ops$ ]] && [[ "$test_name" == "skip-remap" ]]; then + return 0 # Test is excepted + fi + return 1 # Test is not excepted } @@ -112,6 +134,20 @@ collect_test_pairs() { local abs_test_dir="$(cd "$test_dir" && pwd)" + # First, find original config files (.conf, .config, .hrw) with matching .hrw4u files + while IFS= read -r orig_file; do + local base_name="${orig_file%.*}" + local hrw4u_file="${base_name}.hrw4u" + + if [[ -f "$hrw4u_file" ]]; then + local abs_orig="$(cd "$(dirname "$orig_file")" && pwd)/$(basename "$orig_file")" + local abs_hrw4u="$(cd "$(dirname "$hrw4u_file")" && pwd)/$(basename "$hrw4u_file")" + # Output: expected (original) then input (hrw4u) + echo "$abs_orig $abs_hrw4u" + fi + done < <(find "$test_dir" -maxdepth 1 \( -name "*.conf" -o -name "*.config" -o -name "*.hrw" \) | sort) + + # Then, find .input.txt/.output.txt pairs while IFS= read -r input_file; do local base_name="${input_file%.input.txt}" local test_name="${base_name##*/}" @@ -239,6 +275,64 @@ run_serial_mode() { local dir_passed=0 local dir_failed=0 + # First, process original config files (.conf, .config, .hrw) with matching .hrw4u files + while IFS= read -r orig_file; do + local base_name="${orig_file%.*}" + local test_name="${base_name##*/}" + local hrw4u_file="${base_name}.hrw4u" + + if [[ ! -f "$hrw4u_file" ]]; then + continue + fi + + dir_tests=$((dir_tests + 1)) + + local abs_orig="$(cd "$(dirname "$orig_file")" && pwd)/$(basename "$orig_file")" + local abs_hrw4u="$(cd "$(dirname "$hrw4u_file")" && pwd)/$(basename "$hrw4u_file")" + + local start_ms=$(get_time_ms) + if "$CONFCMP" "$abs_orig" "$abs_hrw4u" >/dev/null 2>&1; then + local end_ms=$(get_time_ms) + local elapsed=$((end_ms - start_ms)) + total_time_ms=$((total_time_ms + elapsed)) + printf " ✓ %-30s %4d ms\n" "${dir_name}/${test_name}:" "$elapsed" + dir_passed=$((dir_passed + 1)) + else + local end_ms=$(get_time_ms) + local elapsed=$((end_ms - start_ms)) + total_time_ms=$((total_time_ms + elapsed)) + printf " ✗ %-30s %4d ms (FAILED)\n" "${dir_name}/${test_name}:" "$elapsed" + dir_failed=$((dir_failed + 1)) + failed_tests+=("${dir_name}/${test_name}|$CONFCMP \"$abs_orig\" \"$abs_hrw4u\"") + + # In debug mode, stop immediately and show details + if [[ "$DEBUG_MODE" == true ]]; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " FIRST FAILURE (debug mode): ${dir_name}/${test_name}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "To re-run this test:" + echo " $CONFCMP \"$abs_orig\" \"$abs_hrw4u\"" + echo "" + echo "Original Config File (authority):" + echo "────────────────────────────────────────────────────────────" + cat "$abs_orig" 2>/dev/null || echo "Error: Could not read $abs_orig" + echo "" + echo "Generated HRW4U File:" + echo "────────────────────────────────────────────────────────────" + cat "$abs_hrw4u" 2>/dev/null || echo "Error: Could not read $abs_hrw4u" + echo "" + echo "Comparison Details (--debug):" + echo "────────────────────────────────────────────────────────────" + $CONFCMP --debug "$abs_orig" "$abs_hrw4u" 2>&1 + echo "" + exit 1 + fi + fi + done < <(find "$test_dir" -maxdepth 1 \( -name "*.conf" -o -name "*.config" -o -name "*.hrw" \) | sort) + + # Then, process .input.txt/.output.txt pairs while IFS= read -r input_file; do local base_name="${input_file%.input.txt}" local test_name="${base_name##*/}" @@ -273,11 +367,36 @@ run_serial_mode() { printf " ✗ %-30s %4d ms (FAILED)\n" "${dir_name}/${test_name}:" "$elapsed" dir_failed=$((dir_failed + 1)) failed_tests+=("${dir_name}/${test_name}|$CONFCMP \"$abs_output\" \"$abs_input\"") + + # In debug mode, stop immediately and show details + if [[ "$DEBUG_MODE" == true ]]; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " FIRST FAILURE (debug mode): ${dir_name}/${test_name}" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "To re-run this test:" + echo " $CONFCMP \"$abs_output\" \"$abs_input\"" + echo "" + echo "Expected Output (.output.txt):" + echo "────────────────────────────────────────────────────────────" + cat "$abs_output" 2>/dev/null || echo "Error: Could not read $abs_output" + echo "" + echo "HRW4U Input (.input.txt):" + echo "────────────────────────────────────────────────────────────" + cat "$abs_input" 2>/dev/null || echo "Error: Could not read $abs_input" + echo "" + echo "Comparison Details (--debug):" + echo "────────────────────────────────────────────────────────────" + $CONFCMP --debug "$abs_output" "$abs_input" 2>&1 + echo "" + exit 1 + fi fi done < <(find "$test_dir" -maxdepth 1 -name "*.input.txt" | sort) if [[ $dir_tests -eq 0 ]]; then - echo " No test pairs found (*.input.txt + *.output.txt)" + echo " No test pairs found" else echo "" echo " Directory Summary: $dir_passed passed, $dir_failed failed (total: $dir_tests)" @@ -316,28 +435,43 @@ run_serial_mode() { echo " Failed Test: $test_name" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - # Extract file paths from command - output_file=$(echo "$cmd" | grep -o '"[^"]*\.output\.txt"' | tr -d '"') - input_file=$(echo "$cmd" | grep -o '"[^"]*\.input\.txt"' | tr -d '"') - echo "" echo "To re-run this test:" echo " $cmd" echo "" - echo "Expected Output (.output.txt):" - echo "────────────────────────────────────────────────────────────" - cat "$output_file" 2>/dev/null || echo "Error: Could not read $output_file" - echo "" - - echo "HRW4U Input (.input.txt):" - echo "────────────────────────────────────────────────────────────" - cat "$input_file" 2>/dev/null || echo "Error: Could not read $input_file" - echo "" + # Extract file paths from command - try both types + local first_file=$(echo "$cmd" | grep -o '"[^"]*"' | head -1 | tr -d '"') + local second_file=$(echo "$cmd" | grep -o '"[^"]*"' | tail -1 | tr -d '"') + + # Determine test type based on file extensions + if [[ "$first_file" =~ \.(conf|config|hrw)$ ]]; then + # Config file test: original vs .hrw4u + echo "Original Config File (authority):" + echo "────────────────────────────────────────────────────────────" + cat "$first_file" 2>/dev/null || echo "Error: Could not read $first_file" + echo "" + + echo "Generated HRW4U File:" + echo "────────────────────────────────────────────────────────────" + cat "$second_file" 2>/dev/null || echo "Error: Could not read $second_file" + echo "" + else + # Standard test: .output.txt vs .input.txt + echo "Expected Output (.output.txt):" + echo "────────────────────────────────────────────────────────────" + cat "$first_file" 2>/dev/null || echo "Error: Could not read $first_file" + echo "" + + echo "HRW4U Input (.input.txt):" + echo "────────────────────────────────────────────────────────────" + cat "$second_file" 2>/dev/null || echo "Error: Could not read $second_file" + echo "" + fi echo "Comparison Details (--debug):" echo "────────────────────────────────────────────────────────────" - $CONFCMP --debug "$output_file" "$input_file" 2>&1 + $CONFCMP --debug "$first_file" "$second_file" 2>&1 echo "" done echo ""