Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions doc/admin-guide/plugins/header_rewrite.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:<param_name>``. 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``.
Comment on lines +1536 to +1539
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a note that if there are duplicate names, the first value listed wins.


URL The complete URL. ``Value`` = `https://docs.trafficserver.apache.org/...`
========== ======================================================================

Expand Down Expand Up @@ -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:<param_name>`` 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}
39 changes: 36 additions & 3 deletions plugins/header_rewrite/conditions.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
#include <array>
#include <atomic>

#include "swoc/TextView.h"

#include "ts/ts.h"

#include "conditions.h"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<int>(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);
Expand Down
1 change: 1 addition & 0 deletions plugins/header_rewrite/conditions.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions plugins/header_rewrite/operators.cc
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ OperatorSetDestination::exec(const Resources &res) const

const_cast<Resources &>(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;
Expand All @@ -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<Resources &>(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());
Expand Down Expand Up @@ -461,6 +463,7 @@ OperatorRMDestination::exec(const Resources &res) const
}
const_cast<Resources &>(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<Resources &>(res).changed_url = true;
Expand Down
32 changes: 32 additions & 0 deletions plugins/header_rewrite/resources.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
19 changes: 17 additions & 2 deletions plugins/header_rewrite/resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
#pragma once

#include <string>
#include <unordered_map>

#include "swoc/TextView.h"

#include "regex_helper.h"
#include "ts/ts.h"
Expand Down Expand Up @@ -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;
Expand All @@ -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<swoc::TextView, swoc::TextView> query_params;
bool query_parsed = false;
};
bool changed_url = false;
mutable LifetimeExtension _extended_info;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 } ]
Original file line number Diff line number Diff line change
@@ -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}"