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
12 changes: 12 additions & 0 deletions api/envoy/config/common/mutation_rules/v3/mutation_rules.proto
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ message HeaderMutation {
type.matcher.v3.StringMatcher key_matcher = 1 [(validate.rules).message = {required: true}];
}

message RegexCopy {
string source_header = 1
[(validate.rules).string = {well_known_regex: HTTP_HEADER_VALUE strict: false}];

string target_header = 2
[(validate.rules).string = {well_known_regex: HTTP_HEADER_VALUE strict: false}];

type.matcher.v3.RegexMatchAndSubstitute expression = 3;
}

Comment on lines +100 to +109
Copy link
Member

Choose a reason for hiding this comment

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

We may needn't this. We have provided regex support in the CEL, you can use CEL substitution formatter to extract something from specific attributes (like request header) as new header value. For simple example:

mutations
- append:
    key: something
    value: '%CEL(re.extract(request.host, '(.+?)\\\\:(\\\\d+)', '\\\\2'))%'

If the cel's support is not enough for you, you may could create a custom early mutation extension for this if possible.

oneof action {
option (validate.required) = true;

Expand All @@ -109,5 +119,7 @@ message HeaderMutation {

// Remove the header if the key matches the specified string matcher.
RemoveOnMatch remove_on_match = 3;

RegexCopy regex_copy = 4;
}
}
9 changes: 8 additions & 1 deletion api/envoy/config/route/v3/route.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// * Routing :ref:`architecture overview <arch_overview_http_routing>`
// * HTTP :ref:`router filter <config_http_filters_router>`

// [#next-free-field: 18]
// [#next-free-field: 19]
message RouteConfiguration {
option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.RouteConfiguration";

Expand Down Expand Up @@ -133,6 +133,13 @@ message RouteConfiguration {
// :ref:`envoy_v3_api_msg_config.route.v3.VirtualHost`.domains field.
bool ignore_port_in_host_matching = 14;

// Normally, virtual host matching is done using the :authority (or
// Host: in HTTP < 2) HTTP header. Setting this will instead, use a
// different HTTP header for this purpose. This is intended to be
// combined with an "early_header_mutation" extension to allow
// alternate or simplified host values to be used for host matching.
string alternate_header_for_host_matching = 18;
Comment on lines +136 to +141
Copy link
Member

Choose a reason for hiding this comment

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

vhost_header is enough. Because most users could get it is header name that will be used for vhost searching from the simple vhost_header.

The comments also could be simplified and needn't to refer the early header mutation. The new feature self should be a general/common feature and needn't be bound to early header mutation


// Ignore path-parameters in path-matching.
// Before RFC3986, URI were like(RFC1808): <scheme>://<net_loc>/<path>;<params>?<query>#<fragment>
// Envoy by default takes ":path" as "<path>;<params>".
Expand Down
36 changes: 36 additions & 0 deletions source/common/http/header_mutation.cc
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,34 @@ class RemoveOnMatchMutation : public HeaderEvaluator {
const Matchers::StringMatcherImpl key_matcher_;
};

class RegexCopyMutation : public HeaderEvaluator {
public:
RegexCopyMutation(const std::string& source_header_name, const std::string& target_header_name,
Regex::CompiledMatcherPtr& matcher, const std::string substitution)
: source_header_name_(source_header_name), target_header_name_(target_header_name),
substitution_(substitution) {
matcher_ = std::move(matcher);
}

void evaluateHeaders(Http::HeaderMap& headers, const Formatter::HttpFormatterContext&,
const StreamInfo::StreamInfo&) const override {
auto result = headers.get(source_header_name_);
if (result.empty()) {
return;
}
auto header_value = result[0]->value().getStringView();
auto new_value = matcher_->replaceAll(header_value, substitution_);

headers.setCopy(target_header_name_, new_value);
}

private:
Envoy::Http::LowerCaseString source_header_name_;
Envoy::Http::LowerCaseString target_header_name_;
Regex::CompiledMatcherPtr matcher_;
std::string substitution_;
};

} // namespace

absl::StatusOr<std::unique_ptr<HeaderMutations>>
Expand Down Expand Up @@ -115,6 +143,14 @@ HeaderMutations::HeaderMutations(const ProtoHeaderMutatons& header_mutations,
header_mutations_.emplace_back(std::make_unique<RemoveOnMatchMutation>(
mutation.remove_on_match().key_matcher(), context));
break;
case envoy::config::common::mutation_rules::v3::HeaderMutation::ActionCase::kRegexCopy: {
auto result = Regex::Utility::parseRegex(mutation.regex_copy().expression().pattern(),
context.regexEngine());
SET_AND_RETURN_IF_NOT_OK(result.status(), creation_status);
header_mutations_.emplace_back(std::make_unique<RegexCopyMutation>(
mutation.regex_copy().source_header(), mutation.regex_copy().target_header(), (*result),
mutation.regex_copy().expression().substitution()));
}; break;
default:
PANIC_DUE_TO_PROTO_UNSET;
}
Expand Down
21 changes: 16 additions & 5 deletions source/common/router/config_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1763,7 +1763,8 @@ RouteMatcher::RouteMatcher(const envoy::config::route::v3::RouteConfiguration& r
absl::Status& creation_status)
: vhost_scope_(factory_context.scope().scopeFromStatName(
factory_context.routerContext().virtualClusterStatNames().vhost_)),
ignore_port_in_host_matching_(route_config.ignore_port_in_host_matching()) {
ignore_port_in_host_matching_(route_config.ignore_port_in_host_matching()),
alternate_header_for_host_matching_(route_config.alternate_header_for_host_matching()) {
for (const auto& virtual_host_config : route_config.virtual_hosts()) {
VirtualHostImplSharedPtr virtual_host = std::make_shared<VirtualHostImpl>(
virtual_host_config, global_route_config, factory_context, *vhost_scope_, validator,
Expand Down Expand Up @@ -1809,13 +1810,23 @@ const VirtualHostImpl* RouteMatcher::findVirtualHost(const Http::RequestHeaderMa
return default_virtual_host_.get();
}

// There may be no authority in early reply paths in the HTTP connection manager.
if (headers.Host() == nullptr) {
return nullptr;
absl::string_view host_header_value;
if (alternate_header_for_host_matching_ != "") {
auto result = headers.get(Http::LowerCaseString(alternate_header_for_host_matching_));
// If using an alternate header, it must not be empty.
if (result.empty()) {
return nullptr;
}
host_header_value = result[0]->value().getStringView();
} else {
// There may be no authority in early reply paths in the HTTP connection manager.
if (headers.Host() == nullptr) {
return nullptr;
}
host_header_value = headers.getHostValue();
}

// If 'ignore_port_in_host_matching' is set, ignore the port number in the host header(if any).
absl::string_view host_header_value = headers.getHostValue();
if (ignorePortInHostMatching()) {
if (const absl::string_view::size_type port_start =
Http::HeaderUtility::getPortStart(host_header_value);
Expand Down
1 change: 1 addition & 0 deletions source/common/router/config_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,7 @@ class RouteMatcher {

VirtualHostImplSharedPtr default_virtual_host_;
const bool ignore_port_in_host_matching_{false};
const std::string alternate_header_for_host_matching_;
};

/**
Expand Down
54 changes: 43 additions & 11 deletions test/common/router/config_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2877,6 +2877,40 @@ TEST_F(RouteMatcherTest, QueryParamMatchedRouting) {
}
}

TEST_F(RouteMatcherTest, AlternateHostHeaderMatching) {
const std::string yaml = R"EOF(
alternate_header_for_host_matching: "alternate"
virtual_hosts:
- name: default_service
domains:
- "*"
routes:
- match:
prefix: "/"
route:
cluster: default_service
- name: local_service
domains:
- "foo.example.org"
routes:
- match:
prefix: "/"
route:
cluster: local_service
)EOF";

factory_context_.cluster_manager_.initializeClusters({"local_service", "default_service"}, {});
TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true,
creation_status_);

std::pair<std::string, std::string> alternate_host = {"alternate", "foo.example.org"};
OptionalGenHeadersArg optional_arg;
optional_arg.random_value_pair = alternate_host;

Http::TestRequestHeaderMapImpl headers = genHeaders("example.com", "/", "GET", optional_arg);
EXPECT_EQ("local_service", config.route(headers, 0)->routeEntry()->clusterName());
}

TEST_F(RouteMatcherTest, DynamicMetadataMatchedRouting) {
const std::string yaml = R"EOF(
virtual_hosts:
Expand Down Expand Up @@ -3677,8 +3711,8 @@ TEST_F(RouteMatcherTest, WeightedClusterHeader) {
creation_status_);

Http::TestRequestHeaderMapImpl headers = genHeaders("www1.lyft.com", "/foo", "GET");
// The configured cluster header isn't present in the request headers, therefore cluster selection
// fails and we get the empty string
// The configured cluster header isn't present in the request headers, therefore cluster
// selection fails and we get the empty string
EXPECT_EQ("", config.route(headers, 115)->routeEntry()->clusterName());
// Modify the header mapping.
headers.addCopy("some_header", "some_cluster");
Expand Down Expand Up @@ -3714,8 +3748,8 @@ TEST_F(RouteMatcherTest, WeightedClusterWithProvidedRandomValue) {
OptionalGenHeadersArg optional_arg;
optional_arg.random_value_pair = random_value_pair;
Http::TestRequestHeaderMapImpl headers = genHeaders("www1.lyft.com", "/foo", "GET", optional_arg);
// Here we expect `cluster1` is selected even though random value passed to `route()` function is
// 60 because the overridden weight specified in `random_value_pair` is 10.
// Here we expect `cluster1` is selected even though random value passed to `route()` function
// is 60 because the overridden weight specified in `random_value_pair` is 10.
EXPECT_EQ("cluster1", config.route(headers, 60)->routeEntry()->clusterName());

headers = genHeaders("www1.lyft.com", "/foo", "GET");
Expand Down Expand Up @@ -5487,10 +5521,9 @@ name: foo
factory_context_.cluster_manager_.initializeClusters({"www2", "www2_staging"}, {});
TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true,
creation_status_);
EXPECT_EQ(
creation_status_.message(),
"Only unique values for domains are permitted. Duplicate entry of domain *.lyft.com in route "
"foo");
EXPECT_EQ(creation_status_.message(), "Only unique values for domains are permitted. Duplicate "
"entry of domain *.lyft.com in route "
"foo");
}

TEST_F(RouteMatcherTest, TestDuplicatePrefixWildcardDomainConfig) {
Expand All @@ -5512,9 +5545,8 @@ name: foo
factory_context_.cluster_manager_.initializeClusters({"www2", "www2_staging"}, {});
TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true,
creation_status_);
EXPECT_EQ(
creation_status_.message(),
"Only unique values for domains are permitted. Duplicate entry of domain bar.* in route foo");
EXPECT_EQ(creation_status_.message(), "Only unique values for domains are permitted. Duplicate "
"entry of domain bar.* in route foo");
}

TEST_F(RouteMatcherTest, TestInvalidCharactersInPrefixRewrites) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ TEST(HeaderMutationTest, TestAll) {
key: "flag-header-4"
value: "flag-header-4-value"
append_action: "OVERWRITE_IF_EXISTS_OR_ADD"
- regex_copy:
source_header: "flag-header-5"
target_header: "flag-header-5-target"
expression:
pattern:
regex: "^(.*)-old$"
substitution: \1-new

)EOF";

Server::Configuration::MockServerFactoryContext context;
Expand All @@ -57,6 +65,7 @@ TEST(HeaderMutationTest, TestAll) {
{"flag-header-2", "flag-header-2-value-old"},
{"flag-header-3", "flag-header-3-value-old"},
{"flag-header-4", "flag-header-4-value-old"},
{"flag-header-5", "flag-header-5-value-old"},
{":method", "GET"},
};

Expand All @@ -72,6 +81,10 @@ TEST(HeaderMutationTest, TestAll) {
// 'flag-header-4' is overwritten.
EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-4")).size());
EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4"));
// flag-header-5 is copied to flag-header-5-target, and -old is changed to -new:
EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-5")).size());
EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-5-target")).size());
EXPECT_EQ("flag-header-5-value-new", headers.get_("flag-header-5-target"));
}

} // namespace
Expand Down
Loading