Skip to content

Commit

Permalink
Implement host_rewrite_path option (#12440)
Browse files Browse the repository at this point in the history
This implements a host_rewrite_path option for rewriting the Host header based on path. See rational in the linked issue.

Note: the regex is executed on the path with query/fragment stripped. This is analogues to what regex_rewrite option is doing.

Risk Level: Low
Testing: added unit tests
Docs Changes: document the new option in proto file
Release Notes: added to current.rst
Fixes #12430

Signed-off-by: Petr Pchelko <ppchelko@wikimedia.org>
  • Loading branch information
Petr Pchelko authored Aug 11, 2020
1 parent 276c978 commit 374dca7
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 9 deletions.
19 changes: 18 additions & 1 deletion api/envoy/config/route/v3/route_components.proto
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ message CorsPolicy {
core.v3.RuntimeFractionalPercent shadow_enabled = 10;
}

// [#next-free-field: 35]
// [#next-free-field: 36]
message RouteAction {
option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RouteAction";

Expand Down Expand Up @@ -890,6 +890,23 @@ message RouteAction {
// must come from trusted source.
string host_rewrite_header = 29
[(validate.rules).string = {well_known_regex: HTTP_HEADER_NAME strict: false}];

// Indicates that during forwarding, the host header will be swapped with
// the result of the regex substitution executed on path value with query and fragment removed.
// This is useful for transitioning variable content between path segment and subdomain.
//
// For example with the following config:
//
// .. code-block:: yaml
//
// host_rewrite_path_regex:
// pattern:
// google_re2: {}
// regex: "^/(.+)/.+$"
// substitution: \1
//
// Would rewrite the host header to `envoyproxy.io` given the path `/envoyproxy.io/some/path`.
type.matcher.v3.RegexMatchAndSubstitute host_rewrite_path_regex = 35;
}

// Specifies the upstream timeout for the route. If not specified, the default is 15s. This
Expand Down
19 changes: 18 additions & 1 deletion api/envoy/config/route/v4alpha/route_components.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ New Features
* router: added new
:ref:`envoy-ratelimited<config_http_filters_router_retry_policy-envoy-ratelimited>`
retry policy, which allows retrying envoy's own rate limited responses.
* router: added new :ref:`host_rewrite_path_regex <envoy_v3_api_field_config.route.v3.RouteAction.host_rewrite_path_regex>`
option, which allows rewriting Host header based on path.
* signal: added support for calling fatal error handlers without envoy's signal handler, via FatalErrorHandler::callFatalErrorHandlers().
* stats: added optional histograms to :ref:`cluster stats <config_cluster_manager_cluster_stats_request_response_sizes>`
that track headers and body sizes of requests and responses.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions source/common/router/config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,14 @@ RouteEntryImplBase::RouteEntryImplBase(const VirtualHostImpl& vhost,
? absl::optional<Http::LowerCaseString>(Http::LowerCaseString(
route.route().host_rewrite_header()))
: absl::nullopt),
host_rewrite_path_regex_(
route.route().has_host_rewrite_path_regex()
? Regex::Utility::parseRegex(route.route().host_rewrite_path_regex().pattern())
: nullptr),
host_rewrite_path_regex_substitution_(
route.route().has_host_rewrite_path_regex()
? route.route().host_rewrite_path_regex().substitution()
: ""),
cluster_name_(route.route().cluster()), cluster_header_name_(route.route().cluster_header()),
cluster_not_found_response_code_(ConfigUtility::parseClusterNotFoundResponseCode(
route.route().cluster_not_found_response_code())),
Expand Down Expand Up @@ -527,6 +535,11 @@ void RouteEntryImplBase::finalizeRequestHeaders(Http::RequestHeaderMap& headers,
headers.setHost(header_value);
}
}
} else if (host_rewrite_path_regex_ != nullptr) {
const std::string path(headers.getPathValue());
absl::string_view just_path(Http::PathUtil::removeQueryAndFragment(path));
headers.setHost(
host_rewrite_path_regex_->replaceAll(just_path, host_rewrite_path_regex_substitution_));
}

// Handle path rewrite
Expand Down
2 changes: 2 additions & 0 deletions source/common/router/config_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,8 @@ class RouteEntryImplBase : public RouteEntry,
// to virtual host is currently safe.
const bool auto_host_rewrite_;
const absl::optional<Http::LowerCaseString> auto_host_rewrite_header_;
const Regex::CompiledMatcherPtr host_rewrite_path_regex_;
const std::string host_rewrite_path_regex_substitution_;
const std::string cluster_name_;
const Http::LowerCaseString cluster_header_name_;
const Http::Code cluster_not_found_response_code_;
Expand Down
37 changes: 32 additions & 5 deletions test/common/router/config_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -705,12 +705,12 @@ TEST_F(RouteMatcherTest, TestRoutes) {
prefix: "/host/rewrite/me"
route:
cluster: ats
host_rewrite: new_host
host_rewrite_literal: new_host
- match:
prefix: "/oldhost/rewrite/me"
route:
cluster: ats
host_rewrite: new_oldhost
host_rewrite_literal: new_oldhost
- match:
path: "/foo"
case_sensitive: true
Expand All @@ -728,7 +728,7 @@ TEST_F(RouteMatcherTest, TestRoutes) {
case_sensitive: false
route:
cluster: ats
host_rewrite: new_host
host_rewrite_literal: new_host
- match:
path: "/FOOD"
case_sensitive: false
Expand All @@ -749,12 +749,21 @@ TEST_F(RouteMatcherTest, TestRoutes) {
value: rewrote
route:
cluster: ats
auto_host_rewrite_header: x-rewrite-host
host_rewrite_header: x-rewrite-host
- match:
path: "/do-not-rewrite-host-with-header-value"
route:
cluster: ats
auto_host_rewrite_header: x-rewrite-host
host_rewrite_header: x-rewrite-host
- match:
path: "/rewrite-host-with-path-regex/envoyproxy.io"
route:
cluster: ats
host_rewrite_path_regex:
pattern:
google_re2: {}
regex: "^/.+/(.+)$"
substitution: \1
- match:
prefix: "/"
route:
Expand Down Expand Up @@ -1022,6 +1031,24 @@ TEST_F(RouteMatcherTest, TestRoutes) {
EXPECT_EQ("api.lyft.com", headers.get_(Http::Headers::get().Host));
}

// Rewrites host using path.
{
Http::TestRequestHeaderMapImpl headers =
genHeaders("api.lyft.com", "/rewrite-host-with-path-regex/envoyproxy.io", "GET");
const RouteEntry* route = config.route(headers, 0)->routeEntry();
route->finalizeRequestHeaders(headers, stream_info, true);
EXPECT_EQ("envoyproxy.io", headers.get_(Http::Headers::get().Host));
}

// Rewrites host using path, removes query parameters
{
Http::TestRequestHeaderMapImpl headers = genHeaders(
"api.lyft.com", "/rewrite-host-with-path-regex/envoyproxy.io?query=query", "GET");
const RouteEntry* route = config.route(headers, 0)->routeEntry();
route->finalizeRequestHeaders(headers, stream_info, true);
EXPECT_EQ("envoyproxy.io", headers.get_(Http::Headers::get().Host));
}

// Case sensitive rewrite matching test.
{
Http::TestRequestHeaderMapImpl headers =
Expand Down

0 comments on commit 374dca7

Please sign in to comment.