Skip to content
Open
14 changes: 13 additions & 1 deletion doc/admin-guide/plugins/header_rewrite.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1137,7 +1137,8 @@ rm-destination
Removes individual components of the remapped destination's address. When
changing the remapped destination, ``<part>`` should be used to indicate the
component that is being modified (see `URL Parts`_). Currently the only valid
parts for rm-destination are QUERY, PATH, and PORT.
parts for rm-destination are QUERY, PATH, and PORT; any other part is ignored,
leaving the destination unchanged.

For the query parameter, this operator takes an optional second argument,
which is a list of query parameters to remove (or keep with ``[INV]`` modifier).
Expand Down Expand Up @@ -1421,6 +1422,17 @@ the operator will effectively be a no-op.
This operator is deprecated, use the `set-http-cntl`_ operator instead,
with the ``SKIP_REMAP`` control.

sort-destination
~~~~~~~~~~~~~~~~
::

sort-destination QUERY

Sorts the query parameters of the remapped destination's URL by parameter
name. Sorting is stable, so parameters that share the same name keep their
relative order. Currently ``QUERY`` is the only valid part for
sort-destination; any other part is ignored, leaving the destination unchanged.

set-cookie
~~~~~~~~~~
::
Expand Down
6 changes: 6 additions & 0 deletions plugins/header_rewrite/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ add_atsplugin(
resources.cc
ruleset.cc
statement.cc
url_query.cc
value.cc
)

Expand Down Expand Up @@ -99,6 +100,11 @@ if(BUILD_TESTING)
endif()
endif()

add_executable(test_url_query unit_tests/test_url_query.cc url_query.cc)
target_include_directories(test_url_query PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
add_catch2_test(NAME test_url_query COMMAND test_url_query)
target_link_libraries(test_url_query PRIVATE Catch2::Catch2WithMain libswoc::libswoc)

# add_executable(test_matcher matcher_tests.cc matcher.cc lulu.cc regex_helper.cc)
# add_catch2_test(NAME test_matcher COMMAND $<TARGET_FILE:test_matcher>)
#
Expand Down
2 changes: 2 additions & 0 deletions plugins/header_rewrite/factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ operator_factory(const std::string &op)
o = new OperatorSetDestination();
} else if (op == "rm-destination") {
o = new OperatorRMDestination();
} else if (op == "sort-destination") {
o = new OperatorSortDestination();
} else if (op == "set-redirect") {
o = new OperatorSetRedirect();
} else if (op == "timeout-out") {
Expand Down
10 changes: 10 additions & 0 deletions plugins/header_rewrite/header_rewrite_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,16 @@ test_parsing()
END_TEST();
}

{
ParserTest p("sort-destination QUERY");

CHECK_EQ(p.getTokens().size(), 2UL);
CHECK_EQ(p.getTokens()[0], "sort-destination");
CHECK_EQ(p.getTokens()[1], "QUERY");

END_TEST();
}

return errors;
}

Expand Down
55 changes: 55 additions & 0 deletions plugins/header_rewrite/operators.cc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "swoc/swoc_file.h"

#include "operators.h"
#include "url_query.h"
#include "ts/apidefs.h"
#include "conditions.h"
#include "factory.h"
Expand Down Expand Up @@ -482,6 +483,60 @@ OperatorRMDestination::exec(const Resources &res) const
return true;
}

// OperatorSortDestination
void
OperatorSortDestination::initialize(Parser &p)
{
Operator::initialize(p);

_url_qual = parse_url_qualifier(p.get_arg());

require_resources(RSRC_CLIENT_REQUEST_HEADERS);
require_resources(RSRC_SERVER_REQUEST_HEADERS);
}

bool
OperatorSortDestination::exec(const Resources &res) const
{
if (res._rri || (res.bufp && res.hdr_loc)) {
TSMBuffer bufp;
TSMLoc url_m_loc;

// Determine which TSMBuffer and TSMLoc to use
if (res._rri) {
bufp = res._rri->requestBufp;
url_m_loc = res._rri->requestUrl;
} else {
bufp = res.bufp;
if (TSHttpHdrUrlGet(res.bufp, res.hdr_loc, &url_m_loc) != TS_SUCCESS) {
Dbg(pi_dbg_ctl, "TSHttpHdrUrlGet was unable to return the url m_loc");
return true;
}
}

switch (_url_qual) {
case URL_QUAL_QUERY: {
int q_len = 0;
const char *query = TSUrlHttpQueryGet(bufp, url_m_loc, &q_len);
std::string sorted = sort_query(q_len > 0 ? std::string_view(query, static_cast<size_t>(q_len)) : std::string_view());

const_cast<Resources &>(res).changed_url = true;
TSUrlHttpQuerySet(bufp, url_m_loc, sorted.c_str(), sorted.size());
res.reset_query_cache();
Dbg(pi_dbg_ctl, "OperatorSortDestination::exec() rewrote QUERY to \"%s\"", sorted.c_str());
break;
}
default:
Dbg(pi_dbg_ctl, "Sort destination %i has no handler", _url_qual);
break;
}
} else {
Dbg(pi_dbg_ctl, "OperatorSortDestination::exec() unable to continue due to missing bufp=%p or hdr_loc=%p, rri=%p!", res.bufp,
res.hdr_loc, res._rri);
}
return true;
}

// OperatorSetRedirect
void
OperatorSetRedirect::initialize(Parser &p)
Expand Down
18 changes: 18 additions & 0 deletions plugins/header_rewrite/operators.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,24 @@ class OperatorRMDestination : public Operator
std::vector<std::string_view> _stop_list;
};

class OperatorSortDestination : public Operator
{
public:
OperatorSortDestination() { Dbg(dbg_ctl, "Calling CTOR for OperatorSortDestination"); }

// noncopyable
OperatorSortDestination(const OperatorSortDestination &) = delete;
void operator=(const OperatorSortDestination &) = delete;

void initialize(Parser &p) override;

protected:
bool exec(const Resources &res) const override;

private:
UrlQualifiers _url_qual = URL_QUAL_NONE;
};

class OperatorSetRedirect : public Operator
{
public:
Expand Down
68 changes: 68 additions & 0 deletions plugins/header_rewrite/unit_tests/test_url_query.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
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 <catch2/catch_test_macros.hpp>

#include "url_query.h"

TEST_CASE("sort_query orders params by name", "[header_rewrite][url_query]")
{
SECTION("out-of-order params are sorted")
{
CHECK(sort_query("b=2&a=1") == "a=1&b=2");
}

SECTION("valueless params sort by their own name")
{
CHECK(sort_query("b&a=1") == "a=1&b");
}

SECTION("params with duplicate names keep their relative order")
{
CHECK(sort_query("x=1&a=2&x=3") == "a=2&x=1&x=3");
}

SECTION("empty query stays empty")
{
CHECK(sort_query("") == "");
}

SECTION("single param is unaffected")
{
CHECK(sort_query("a=1") == "a=1");
}

SECTION("trailing '&' produces a clean drop, not an empty trailing token")
{
CHECK(sort_query("a=1&") == "a=1");
}

SECTION("param value containing '=' is preserved and sorts by its name only")
{
CHECK(sort_query("a=1=2") == "a=1=2");
}

SECTION("leading '&' produces a clean drop, not an empty leading token")
{
CHECK(sort_query("&a=1") == "a=1");
}

SECTION("consecutive '&'s produce a clean drop, not empty middle tokens")
{
CHECK(sort_query("b=2&&a=1&&c=3") == "a=1&b=2&c=3");
}
}
72 changes: 72 additions & 0 deletions plugins/header_rewrite/url_query.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
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 "url_query.h"

#include <algorithm>
#include <vector>

#include "swoc/TextView.h"

namespace
{

std::vector<std::string_view>
split(std::string_view text, char delimiter)
{
std::vector<std::string_view> tokens;
swoc::TextView view(text);

while (view) {
tokens.push_back(view.take_prefix_at(delimiter));
}

return tokens;
}

std::string_view
param_name(std::string_view param)
{
return param.substr(0, param.find('='));
}

} // namespace

std::string
sort_query(std::string_view query)
{
if (query.empty()) {
return {};
}

std::vector<std::string_view> params = split(query, '&');

std::stable_sort(params.begin(), params.end(),
[](std::string_view a, std::string_view b) { return param_name(a) < param_name(b); });

std::string result;
result.reserve(query.size()); // same length as query, capped at 64KB by request_line_max_size

for (const auto &param : params) {
if (!result.empty()) {
result += '&';
}
result.append(param);
}

return result;
}
26 changes: 26 additions & 0 deletions plugins/header_rewrite/url_query.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
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 <string>
#include <string_view>

// Sort the '&'-separated parameters of a URL query string by parameter
// name. Sorting is stable: parameters that share the same name keep their
// relative order. An empty input returns an empty string.
std::string sort_query(std::string_view query);
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ autest:
args:
- "rules/set_status_in_if.conf"

- from: "http://www.example.com/from_19/"
to: "http://backend.ex:{SERVER_HTTP_PORT}/to_19/"
plugins:
- name: "header_rewrite.so"
args:
- "rules/rule_sort_destination.conf"

metric_checks:
- metric: "proxy.process.plugin.header_rewrite.operators"
min: 1
Expand Down Expand Up @@ -1973,3 +1980,28 @@ sessions:

proxy-response:
status: 403

# Test 68: sort-destination QUERY sorts the request-URL query before the
# request reaches the origin.
- transactions:
- client-request:
method: "GET"
version: "1.1"
url: /from_19/?b=2&a=1&c=3
headers:
fields:
- [ Host, www.example.com ]
- [ uuid, 68 ]

proxy-request:
url: /to_19/?a=1&b=2&c=3

server-response:
status: 200
reason: OK
headers:
fields:
- [ Connection, close ]

proxy-response:
status: 200
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#
# 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.

sort-destination QUERY