This is an automated email from the ASF dual-hosted git repository. zwoop pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push: new 196ac8cbe9 HRW: Allow sets to have quoted strings (#12256) 196ac8cbe9 is described below commit 196ac8cbe9ee6bddd78d9e8c20d9c0a9a9747c62 Author: Leif Hedstrom <zw...@apache.org> AuthorDate: Fri Jun 6 14:53:13 2025 -0500 HRW: Allow sets to have quoted strings (#12256) --- doc/admin-guide/plugins/header_rewrite.en.rst | 26 +++++++++++----- plugins/header_rewrite/matcher.h | 36 +++++++++++++++++++--- plugins/header_rewrite/parser.cc | 13 ++++++-- plugins/header_rewrite/parser.h | 1 + .../pluginTest/header_rewrite/gold/ext-sets.gold | 1 + .../header_rewrite/gold/header_rewrite-client.gold | 1 + .../header_rewrite/header_rewrite_url.test.py | 3 +- .../header_rewrite/rules/rule_client.conf | 6 ++++ 8 files changed, 71 insertions(+), 16 deletions(-) diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index de9f87226d..cc8c6d62a0 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -739,6 +739,11 @@ The absence of an operand for conditions which accept them simply requires that a value exists (e.g. the content of the header is not an empty string) for the condition to be considered true. +.. note:: + Strings within a set can be quoted, but the quotes are not necessary. This + can be important if a matching string can include spaces or commas, + e.g. ``(foo,"bar,fie",baz)``. + Condition Flags --------------- @@ -764,9 +769,10 @@ EXT The substring match only applies to the file extension following a dot. This is generally mostly useful for the ``URL:PATH`` part. ====== ======================================================================== -**Note**: At most, one of ``[PRE]``, ``[SUF]``, ``[MID]``, or ``[EXT]`` may be -used at any time. They can however be used together with ``[NOCASE]`` and the -other flags. +.. note:: + At most, one of ``[PRE]``, ``[SUF]``, ``[MID]``, or ``[EXT]`` may be + used at any time. They can however be used together with ``[NOCASE]`` and the + other flags. Operators --------- @@ -898,7 +904,9 @@ Will call ``<URL>`` (see URL in `URL Parts`_) to retrieve a custom error respons and set the body with the result. Triggering this rule on an OK transaction will send a 500 status code to the client with the desired response. If this is triggered on any error status code, that original status code will be sent to the client. -**Note**: This config should only be set using READ_RESPONSE_HDR_HOOK + +.. note:: + This config should only be set using READ_RESPONSE_HDR_HOOK An example config would look like:: @@ -958,8 +966,9 @@ the appropriate logs even when the debug tag has not been enabled. For additional information on |TS| debugging statements, refer to :ref:`developer-debug-tags` in the developer's documentation. -**Note**: This operator is deprecated, use the `set-http-cntl`_ operator instead, -with the ``TXN_DEBUG`` control. +.. note:: + This operator is deprecated, use the `set-http-cntl`_ operator instead, + with the ``TXN_DEBUG`` control. set-destination ~~~~~~~~~~~~~~~ @@ -1074,8 +1083,9 @@ When invoked, and when ``<value>`` is any of ``1``, ``true``, or ``TRUE``, this operator causes |TS| to abort further request remapping. Any other value and the operator will effectively be a no-op. -**Note**: This operator is deprecated, use the `set-http-cntl`_ operator instead, -with the ``SKIP_REMAP`` control. +.. note:: + This operator is deprecated, use the `set-http-cntl`_ operator instead, + with the ``SKIP_REMAP`` control. set-cookie ~~~~~~~~~~ diff --git a/plugins/header_rewrite/matcher.h b/plugins/header_rewrite/matcher.h index 9f1a36a15b..3099b300f8 100644 --- a/plugins/header_rewrite/matcher.h +++ b/plugins/header_rewrite/matcher.h @@ -194,13 +194,39 @@ public: // MATCH_SET (allowed for any T) if (_op == MATCH_SET) { _data.template emplace<std::set<T>>(); + auto &values = std::get<std::set<T>>(_data); + swoc::TextView src{s}; + bool in_quotes = false; + size_t start = 0, cur = 0, skip_quotes = 0; + + while (cur < src.size()) { + if (src[cur] == '"') { + skip_quotes = 1; + in_quotes = !in_quotes; + ++cur; + } else if (src[cur] == ',' && !in_quotes) { + swoc::TextView field = src.substr(start + skip_quotes, cur - start - skip_quotes * 2); + + field.ltrim_if(&isspace).rtrim_if(&isspace); + values.insert(convert(std::string(field))); + start = ++cur + skip_quotes; + skip_quotes = 0; + } else { + ++cur; + } + } + + if (in_quotes) { + Dbg(pi_dbg_ctl, "Invalid set: unmatched quotes in: %s", s.c_str()); + throw std::runtime_error("Malformed set, unmatched quotes"); + } - auto &values = std::get<std::set<T>>(_data); - std::istringstream stream(s); - std::string part; + // Last field (if any) + if (start < src.size()) { + swoc::TextView field = src.substr(start + skip_quotes, src.size() - start - skip_quotes * 2); - while (std::getline(stream, part, ',')) { - values.insert(convert(part)); + field.ltrim_if(&isspace).rtrim_if(&isspace); + values.insert(convert(std::string(field))); } if (!values.empty()) { diff --git a/plugins/header_rewrite/parser.cc b/plugins/header_rewrite/parser.cc index 3dbd63de4a..fb73b5aea7 100644 --- a/plugins/header_rewrite/parser.cc +++ b/plugins/header_rewrite/parser.cc @@ -28,7 +28,7 @@ #include "parser.h" -enum ParserState { PARSER_DEFAULT, PARSER_IN_QUOTE, PARSER_IN_REGEX, PARSER_IN_EXPANSION, PARSER_IN_BRACE }; +enum ParserState { PARSER_DEFAULT, PARSER_IN_QUOTE, PARSER_IN_REGEX, PARSER_IN_EXPANSION, PARSER_IN_BRACE, PARSER_IN_PAREN }; bool Parser::parse_line(const std::string &original_line) @@ -71,7 +71,7 @@ Parser::parse_line(const std::string &original_line) cur_token_start = i; } line.erase(i, 1); - } else if ((state != PARSER_IN_REGEX) && (line[i] == '"')) { + } else if ((state != PARSER_IN_REGEX) && (state != PARSER_IN_PAREN) && (line[i] == '"')) { if ((state != PARSER_IN_QUOTE) && !extracting_token) { state = PARSER_IN_QUOTE; extracting_token = true; @@ -97,6 +97,15 @@ Parser::parse_line(const std::string &original_line) _tokens.push_back(line.substr(cur_token_start, cur_token_length)); state = PARSER_DEFAULT; extracting_token = false; + } else if ((state == PARSER_DEFAULT) && (line[i] == '(')) { + state = PARSER_IN_PAREN; + extracting_token = true; + cur_token_start = i; + } else if ((state == PARSER_IN_PAREN) && (line[i] == ')')) { + cur_token_length = i - cur_token_start + 1; + _tokens.push_back(line.substr(cur_token_start, cur_token_length)); + state = PARSER_DEFAULT; + extracting_token = false; } else if (!extracting_token) { if (_tokens.empty() && line[i] == '#') { // this is a comment line (it may have had leading whitespace before the #) diff --git a/plugins/header_rewrite/parser.h b/plugins/header_rewrite/parser.h index 35f056936d..cc0f3d985a 100644 --- a/plugins/header_rewrite/parser.h +++ b/plugins/header_rewrite/parser.h @@ -109,6 +109,7 @@ public: bool parse_line(const std::string &original_line); + // We chose to have this take a std::string, since some of these conversions can not take a TextView easily template <typename NumericT> static NumericT parseNumeric(const std::string &s) diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/ext-sets.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/ext-sets.gold index c71cee3ac7..9636d50f95 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/gold/ext-sets.gold +++ b/tests/gold_tests/pluginTest/header_rewrite/gold/ext-sets.gold @@ -7,4 +7,5 @@ < Date: `` `` < X-Extension: Yes +< X-Testing: Yes `` diff --git a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-client.gold b/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-client.gold index 8d5d99ba35..510f7b3dbc 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-client.gold +++ b/tests/gold_tests/pluginTest/header_rewrite/gold/header_rewrite-client.gold @@ -10,4 +10,5 @@ < Server: ATS/`` < Cache-Control: no-store < X-Pre-Else: Yes +< X-Testing: No `` diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_url.test.py b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_url.test.py index c86ea42e6c..e77b5ae2a7 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_url.test.py +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_url.test.py @@ -90,7 +90,8 @@ ts.Disk.traffic_out.Content = "gold/header_rewrite-tag.gold" tr = Test.AddTestRun() tr.MakeCurlCommand( '--proxy 127.0.0.1:{0} "http://www.example.com/from_path/hrw-sets.png" ' - '-H "Proxy-Connection: keep-alive" --verbose'.format(ts.Variables.port)) + '-H "Proxy-Connection: keep-alive" -H "X-Testing: foo,bar" ' + '--verbose'.format(ts.Variables.port)) tr.Processes.Default.ReturnCode = 0 tr.Processes.Default.Streams.stderr = "gold/ext-sets.gold" tr.StillRunningAfter = server diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_client.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_client.conf index 1b6d845084..1796078a53 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/rules/rule_client.conf +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/rule_client.conf @@ -30,3 +30,9 @@ cond %{CLIENT-URL:PATH} (hrw,foo) [MID,NOCASE] no-op else set-header X-Pre-Else "Yes" + +cond %{SEND_RESPONSE_HDR_HOOK} +cond %{CLIENT-HEADER:X-Testing} (foo,bar,"foo,bar") + set-header X-Testing "Yes" +else + set-header X-Testing "No"