diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index c30af3fd70a..e0aef1f838c 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -1529,6 +1529,15 @@ The URL part names which may be used for these conditions and actions are: parameters, until the end of the URL. Empty string if there were no query parameters. ``Value`` = `p=hrw&v=1` + A specific query parameter value can be extracted using the sub-key + syntax ``QUERY:``. For example, ``%{CLIENT-URL:QUERY:p}`` + would return ``hrw`` for the URL above. + + .. note:: + Query parameter names and values are matched as-is without URL + decoding. For example, ``%{CLIENT-URL:QUERY:my%20param}`` matches + the literal parameter name ``my%20param``, not ``my param``. + URL The complete URL. ``Value`` = `https://docs.trafficserver.apache.org/...` ========== ====================================================================== @@ -2000,3 +2009,17 @@ Those will pick the address provided by PROXY protocol, instead of the peer's ad cond %{SEND_RESPONSE_HDR_HOOK} set-header real-ip %{INBOUND:REMOTE-ADDR} + +Route Based on Query Parameter Value +------------------------------------ + +This rule extracts a specific query parameter value and uses it to set a custom +header or make routing decisions. The ``QUERY:`` sub-key syntax +allows extracting individual query parameter values:: + + cond %{REMAP_PSEUDO_HOOK} [AND] + cond %{CLIENT-URL:QUERY:version} ="v2" + set-destination HOST api-v2.example.com + + cond %{SEND_RESPONSE_HDR_HOOK} + set-header X-API-Version %{CLIENT-URL:QUERY:version} diff --git a/plugins/header_rewrite/conditions.cc b/plugins/header_rewrite/conditions.cc index 19c7f3dbdf3..7f9e7c18029 100644 --- a/plugins/header_rewrite/conditions.cc +++ b/plugins/header_rewrite/conditions.cc @@ -30,6 +30,8 @@ #include #include +#include "swoc/TextView.h" + #include "ts/ts.h" #include "conditions.h" @@ -280,7 +282,27 @@ ConditionUrl::set_qualifier(const std::string &q) Condition::set_qualifier(q); Dbg(pi_dbg_ctl, "\tParsing %%{URL:%s}", q.c_str()); - _url_qual = parse_url_qualifier(q); + + std::string::size_type pos = q.find(':'); + + if (pos != std::string::npos) { + std::string qual_part = q.substr(0, pos); + std::string sub_qual = q.substr(pos + 1); + + _url_qual = parse_url_qualifier(qual_part); + + if (_url_qual == URL_QUAL_QUERY) { + if (!sub_qual.empty()) { + _query_param = sub_qual; + Dbg(pi_dbg_ctl, "\tQuery parameter sub-key: %s", _query_param.c_str()); + } + } else { + TSError("[%s] Sub-qualifier syntax (component:subkey) is only supported for QUERY component, got: %s", PLUGIN_NAME, + qual_part.c_str()); + } + } else { + _url_qual = parse_url_qualifier(q); + } } void @@ -347,8 +369,19 @@ ConditionUrl::append_value(std::string &s, const Resources &res) break; case URL_QUAL_QUERY: q_str = TSUrlHttpQueryGet(bufp, url, &i); - s.append(q_str, i); - Dbg(pi_dbg_ctl, " Query parameters to match is: %.*s", i, q_str); + if (_query_param.empty()) { + s.append(q_str, i); + Dbg(pi_dbg_ctl, " Query parameters to match is: %.*s", i, q_str); + } else { + swoc::TextView value = res.get_query_param(_query_param, q_str, i); + + if (value.data() != nullptr && value.size() > 0) { + s.append(value.data(), value.size()); + Dbg(pi_dbg_ctl, " Query parameter %s value is: %.*s", _query_param.c_str(), static_cast(value.size()), value.data()); + } else { + Dbg(pi_dbg_ctl, " Query parameter %s is empty or not present", _query_param.c_str()); + } + } break; case URL_QUAL_SCHEME: q_str = TSUrlSchemeGet(bufp, url, &i); diff --git a/plugins/header_rewrite/conditions.h b/plugins/header_rewrite/conditions.h index 28e888d4ed6..6893709d78a 100644 --- a/plugins/header_rewrite/conditions.h +++ b/plugins/header_rewrite/conditions.h @@ -300,6 +300,7 @@ class ConditionUrl : public Condition private: UrlQualifiers _url_qual = URL_QUAL_NONE; UrlType _type; + std::string _query_param; // Optional: specific query parameter name for QUERY sub-key }; // DBM lookups diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc index deadfd44708..9a3238d3af7 100644 --- a/plugins/header_rewrite/operators.cc +++ b/plugins/header_rewrite/operators.cc @@ -324,6 +324,7 @@ OperatorSetDestination::exec(const Resources &res) const const_cast(res).changed_url = true; TSUrlHttpQuerySet(bufp, url_m_loc, value.c_str(), value.size()); + res.reset_query_cache(); Dbg(pi_dbg_ctl, "OperatorSetDestination::exec() invoked with QUERY: %s", value.c_str()); } break; @@ -348,6 +349,7 @@ OperatorSetDestination::exec(const Resources &res) const if (TSUrlCreate(bufp, &new_url_loc) == TS_SUCCESS && TSUrlParse(bufp, new_url_loc, &start, end) == TS_PARSE_DONE && TSHttpHdrUrlSet(bufp, res.hdr_loc, new_url_loc) == TS_SUCCESS) { const_cast(res).changed_url = true; + res.reset_query_cache(); Dbg(pi_dbg_ctl, "Set destination URL to %s", value.c_str()); } else { Dbg(pi_dbg_ctl, "Failed to set URL %s", value.c_str()); @@ -461,6 +463,7 @@ OperatorRMDestination::exec(const Resources &res) const } const_cast(res).changed_url = true; TSUrlHttpQuerySet(bufp, url_m_loc, value.c_str(), value.size()); + res.reset_query_cache(); break; case URL_QUAL_PORT: const_cast(res).changed_url = true; diff --git a/plugins/header_rewrite/resources.cc b/plugins/header_rewrite/resources.cc index 025fe390e49..0d4d6e44330 100644 --- a/plugins/header_rewrite/resources.cc +++ b/plugins/header_rewrite/resources.cc @@ -181,3 +181,35 @@ Resources::destroy() _ready = false; } + +swoc::TextView +Resources::get_query_param(const std::string &name, const char *query_str, int query_len) const +{ + // Note: Query parameter names and values are matched as-is without URL decoding. + // For example, searching for "my%20param" matches the literal string, not "my param". + if (!_extended_info.query_parsed) { + if (query_str && query_len > 0) { + swoc::TextView query_view(query_str, query_len); + + while (!query_view.empty()) { + swoc::TextView param = query_view.take_prefix_at('&'); + swoc::TextView param_name = param.take_prefix_at('='); + swoc::TextView param_value = param; + + if (!param_name.empty()) { + // We only allow caching / using the first instance of a query param + _extended_info.query_params[param_name] = param_value; + } + } + } + _extended_info.query_parsed = true; + } + + auto it = _extended_info.query_params.find(swoc::TextView(name)); + + if (it != _extended_info.query_params.end()) { + return it->second; + } + + return swoc::TextView(); +} diff --git a/plugins/header_rewrite/resources.h b/plugins/header_rewrite/resources.h index ab59540f97e..81705956455 100644 --- a/plugins/header_rewrite/resources.h +++ b/plugins/header_rewrite/resources.h @@ -22,6 +22,9 @@ #pragma once #include +#include + +#include "swoc/TextView.h" #include "regex_helper.h" #include "ts/ts.h" @@ -96,6 +99,16 @@ class Resources return _extended_info.matches; } + // Get a query parameter value by name, with caching + swoc::TextView get_query_param(const std::string &name, const char *query_str, int query_len) const; + + void + reset_query_cache() const + { + _extended_info.query_params.clear(); + _extended_info.query_parsed = false; + } + TSCont contp = nullptr; TSRemapRequestInfo *_rri = nullptr; TSMBuffer bufp = nullptr; @@ -114,8 +127,10 @@ class Resources TSHttpStatus resp_status = TS_HTTP_STATUS_NONE; struct LifetimeExtension { - std::string subject_storage; - RegexMatches matches; + std::string subject_storage; + RegexMatches matches; + std::unordered_map query_params; + bool query_parsed = false; }; bool changed_url = false; mutable LifetimeExtension _extended_info; diff --git a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml index 5577ef86c71..0c9a6ac2363 100644 --- a/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml +++ b/tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml @@ -138,6 +138,13 @@ autest: args: - "rules/nested_ifs.conf" + - from: "http://www.example.com/from_13/" + to: "http://backend.ex:{SERVER_HTTP_PORT}/to_13/" + plugins: + - name: "header_rewrite.so" + args: + - "rules/query_sub_key.conf" + # Proxy verifier sessions @@ -1019,3 +1026,51 @@ sessions: - [ X-Foo-And-Fie, { as: absent } ] - [ X-Fie-Anywhere, { value: "Yes", as: equal } ] - [ X-When-200-After, { value: "Yes", as: equal } ] + +# Test 28: Query parameter sub-key extraction +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_13/?sub=Hello + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 34 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Query-Sub, { value: "Hello", as: equal } ] + +# Test 29: Query parameter sub-key - parameter not present +- transactions: + - client-request: + method: "GET" + version: "1.1" + url: /from_13/?other=value + headers: + fields: + - [ Host, www.example.com ] + - [ uuid, 35 ] + + server-response: + status: 200 + reason: OK + headers: + fields: + - [ Connection, close ] + + proxy-response: + status: 200 + headers: + fields: + - [ X-Query-Sub, { as: absent } ] diff --git a/tests/gold_tests/pluginTest/header_rewrite/rules/query_sub_key.conf b/tests/gold_tests/pluginTest/header_rewrite/rules/query_sub_key.conf new file mode 100644 index 00000000000..a885c65283d --- /dev/null +++ b/tests/gold_tests/pluginTest/header_rewrite/rules/query_sub_key.conf @@ -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. + +# Test query parameter sub-key extraction +cond %{SEND_RESPONSE_HDR_HOOK} [AND] +cond %{CLIENT-URL:QUERY:sub} ="" [NOT] + set-header X-Query-Sub "%{CLIENT-URL:QUERY:sub}"