Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: add StringMatcher in HeaderMatcher and deprecate the old fields (exact, prefix, etc.) #17119

Merged
merged 16 commits into from
Jul 15, 2021
Merged
6 changes: 5 additions & 1 deletion api/envoy/config/route/v3/route_components.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1850,7 +1850,7 @@ message RateLimit {
// value.
//
// [#next-major-version: HeaderMatcher should be refactored to use StringMatcher.]
// [#next-free-field: 13]
// [#next-free-field: 14]
message HeaderMatcher {
option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.HeaderMatcher";

Expand Down Expand Up @@ -1922,6 +1922,10 @@ message HeaderMatcher {
// * The regex ``\d{3}`` does not match the value *1234*, so it will match when inverted.
// * The range [-10,0) will match the value -1, so it will not match when inverted.
bool invert_match = 8;

// If true, indicates the exact_match/prefix_match/suffix_match/contains_match should be case
yangminzhu marked this conversation as resolved.
Show resolved Hide resolved
// insensitive. This has no effect for the safe_regex_match.
yangminzhu marked this conversation as resolved.
Show resolved Hide resolved
bool ignore_case = 13;
yangminzhu marked this conversation as resolved.
Show resolved Hide resolved
}

// Query parameter matching treats the query string of a request's :path header
Expand Down
6 changes: 5 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.

1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ New Features
* http: added a new option to upstream HTTP/2 :ref:`keepalive <envoy_v3_api_field_config.core.v3.Http2ProtocolOptions.connection_keepalive>` to send a PING ahead of a new stream if the connection has been idle for a sufficient duration.
* http: added the ability to :ref:`unescape slash sequences <envoy_v3_api_field_extensions.filters.network.http_connection_manager.v3.HttpConnectionManager.path_with_escaped_slashes_action>` in the path. Requests with unescaped slashes can be proxied, rejected or redirected to the new unescaped path. By default this feature is disabled. The default behavior can be overridden through :ref:`http_connection_manager.path_with_escaped_slashes_action<config_http_conn_man_runtime_path_with_escaped_slashes_action>` runtime variable. This action can be selectively enabled for a portion of requests by setting the :ref:`http_connection_manager.path_with_escaped_slashes_action_sampling<config_http_conn_man_runtime_path_with_escaped_slashes_action_enabled>` runtime variable.
* http: added upstream and downstream alpha HTTP/3 support! See :ref:`quic_options <envoy_v3_api_field_config.listener.v3.UdpListenerConfig.quic_options>` for downstream and the new http3_protocol_options in :ref:`http_protocol_options <envoy_v3_api_msg_extensions.upstreams.http.v3.HttpProtocolOptions>` for upstream HTTP/3.
* http: added :ref:`ignore_case <envoy_v3_api_field_config.route.v3.HeaderMatcher.ignore_case>` for the exact, prefix, suffix and contains match in the header matcher.
* jwt_authn: added support to fetch remote jwks asynchronously specified by :ref:`async_fetch <envoy_v3_api_field_extensions.filters.http.jwt_authn.v3.RemoteJwks.async_fetch>`.
* listener: added ability to change an existing listener's address.
* local_rate_limit_filter: added suppoort for locally rate limiting http requests on a per connection basis. This can be enabled by setting the :ref:`local_rate_limit_per_downstream_connection <envoy_v3_api_field_extensions.filters.http.local_ratelimit.v3.LocalRateLimit.local_rate_limit_per_downstream_connection>` field to true.
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.

26 changes: 19 additions & 7 deletions source/common/http/header_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ using SharedResponseCodeDetails = ConstSingleton<SharedResponseCodeDetailsValues
// f.prefix_match: Match will succeed if header value matches the prefix value specified here.
// g.suffix_match: Match will succeed if header value matches the suffix value specified here.
HeaderUtility::HeaderData::HeaderData(const envoy::config::route::v3::HeaderMatcher& config)
: name_(config.name()), invert_match_(config.invert_match()) {
: name_(config.name()), contains_match_lowercase_(""), invert_match_(config.invert_match()),
ignore_case_(config.ignore_case()) {
switch (config.header_match_specifier_case()) {
case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::kExactMatch:
header_match_type_ = HeaderMatchType::Value;
Expand Down Expand Up @@ -65,6 +66,9 @@ HeaderUtility::HeaderData::HeaderData(const envoy::config::route::v3::HeaderMatc
case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::kContainsMatch:
header_match_type_ = HeaderMatchType::Contains;
value_ = config.contains_match();
if (ignore_case_) {
contains_match_lowercase_ = LowerCaseString(value_);
}
break;
case envoy::config::route::v3::HeaderMatcher::HeaderMatchSpecifierCase::
HEADER_MATCH_SPECIFIER_NOT_SET:
Expand Down Expand Up @@ -138,17 +142,20 @@ bool HeaderUtility::matchHeaders(const HeaderMap& request_headers, const HeaderD
}
}

const auto value = header_value.result().value();
bool match;
switch (header_data.header_match_type_) {
case HeaderMatchType::Value:
match = header_data.value_.empty() || header_value.result().value() == header_data.value_;
match = header_data.value_.empty() ||
(header_data.ignore_case_ ? absl::EqualsIgnoreCase(value, header_data.value_)
: value == header_data.value_);
break;
case HeaderMatchType::Regex:
match = header_data.regex_->match(header_value.result().value());
match = header_data.regex_->match(value);
break;
case HeaderMatchType::Range: {
int64_t header_int_value = 0;
match = absl::SimpleAtoi(header_value.result().value(), &header_int_value) &&
match = absl::SimpleAtoi(value, &header_int_value) &&
header_int_value >= header_data.range_.start() &&
header_int_value < header_data.range_.end();
break;
Expand All @@ -157,13 +164,18 @@ bool HeaderUtility::matchHeaders(const HeaderMap& request_headers, const HeaderD
match = header_data.present_;
break;
case HeaderMatchType::Prefix:
match = absl::StartsWith(header_value.result().value(), header_data.value_);
match = header_data.ignore_case_ ? absl::StartsWithIgnoreCase(value, header_data.value_)
: absl::StartsWith(value, header_data.value_);
break;
case HeaderMatchType::Suffix:
match = absl::EndsWith(header_value.result().value(), header_data.value_);
match = header_data.ignore_case_ ? absl::EndsWithIgnoreCase(value, header_data.value_)
: absl::EndsWith(value, header_data.value_);
break;
case HeaderMatchType::Contains:
match = absl::StrContains(header_value.result().value(), header_data.value_);
match = header_data.ignore_case_
? absl::StrContains(absl::AsciiStrToLower(value),
header_data.contains_match_lowercase_.get())
yangminzhu marked this conversation as resolved.
Show resolved Hide resolved
: absl::StrContains(value, header_data.value_);
break;
default:
NOT_REACHED_GCOVR_EXCL_LINE;
Expand Down
3 changes: 3 additions & 0 deletions source/common/http/header_utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,13 @@ class HeaderUtility {
const LowerCaseString name_;
HeaderMatchType header_match_type_;
std::string value_;
// The contains_match_lowercase_ is populated only for contains match when ignore_case is true.
LowerCaseString contains_match_lowercase_;
Regex::CompiledMatcherPtr regex_;
envoy::type::v3::Int64Range range_;
const bool invert_match_;
bool present_;
bool ignore_case_;

// HeaderMatcher
bool matchesHeaders(const HeaderMap& headers) const override {
Expand Down
102 changes: 93 additions & 9 deletions test/common/http/header_utility_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,9 @@ name: match-header

TEST(MatchHeadersTest, HeaderExactMatch) {
TestRequestHeaderMapImpl matching_headers{{"match-header", "match-value"}};
TestRequestHeaderMapImpl unmatching_headers{{"match-header", "other-value"},
{"other-header", "match-value"}};
TestRequestHeaderMapImpl unmatching_headers_1{{"match-header", "other-value"},
{"other-header", "match-value"}};
TestRequestHeaderMapImpl unmatching_headers_2{{"match-header", "MATCH-VALUE"}};
const std::string yaml = R"EOF(
name: match-header
exact_match: match-value
Expand All @@ -393,7 +394,8 @@ exact_match: match-value
header_data.push_back(
std::make_unique<HeaderUtility::HeaderData>(parseHeaderMatcherFromYaml(yaml)));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers_1, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers_2, header_data));
}

TEST(MatchHeadersTest, HeaderExactMatchInverse) {
Expand All @@ -414,6 +416,25 @@ invert_match: true
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
}

TEST(MatchHeadersTest, HeaderExactMatchIgnoreCase) {
TestRequestHeaderMapImpl matching_headers_1{{"match-header", "match-value"}};
TestRequestHeaderMapImpl matching_headers_2{{"match-header", "MATCH-VALUE"}};
yangminzhu marked this conversation as resolved.
Show resolved Hide resolved
TestRequestHeaderMapImpl unmatching_headers{{"match-header", "other-value"},
{"other-header", "match-value"}};
const std::string yaml = R"EOF(
name: match-header
exact_match: match-value
yangminzhu marked this conversation as resolved.
Show resolved Hide resolved
ignore_case: true
)EOF";

std::vector<HeaderUtility::HeaderDataPtr> header_data;
header_data.push_back(
std::make_unique<HeaderUtility::HeaderData>(parseHeaderMatcherFromYaml(yaml)));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers_1, header_data));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers_2, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
}

TEST(MatchHeadersTest, HeaderSafeRegexMatch) {
TestRequestHeaderMapImpl matching_headers{{"match-header", "123"}};
TestRequestHeaderMapImpl unmatching_headers{{"match-header", "1234"},
Expand Down Expand Up @@ -574,7 +595,8 @@ invert_match: true

TEST(MatchHeadersTest, HeaderPrefixMatch) {
TestRequestHeaderMapImpl matching_headers{{"match-header", "value123"}};
TestRequestHeaderMapImpl unmatching_headers{{"match-header", "123value"}};
TestRequestHeaderMapImpl unmatching_headers_1{{"match-header", "123value"}};
TestRequestHeaderMapImpl unmatching_headers_2{{"match-header", "VALUE123"}};

const std::string yaml = R"EOF(
name: match-header
Expand All @@ -585,7 +607,8 @@ prefix_match: value
header_data.push_back(
std::make_unique<HeaderUtility::HeaderData>(parseHeaderMatcherFromYaml(yaml)));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers_1, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers_2, header_data));
}

TEST(MatchHeadersTest, HeaderPrefixInverseMatch) {
Expand All @@ -605,9 +628,29 @@ invert_match: true
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
}

TEST(MatchHeadersTest, HeaderPrefixMatchIgnoreCase) {
TestRequestHeaderMapImpl matching_headers_1{{"match-header", "value123"}};
TestRequestHeaderMapImpl matching_headers_2{{"match-header", "VALUE123"}};
TestRequestHeaderMapImpl unmatching_headers{{"match-header", "123value"}};

const std::string yaml = R"EOF(
name: match-header
prefix_match: value
ignore_case: true
)EOF";

std::vector<HeaderUtility::HeaderDataPtr> header_data;
header_data.push_back(
std::make_unique<HeaderUtility::HeaderData>(parseHeaderMatcherFromYaml(yaml)));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers_1, header_data));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers_2, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
}

TEST(MatchHeadersTest, HeaderSuffixMatch) {
TestRequestHeaderMapImpl matching_headers{{"match-header", "123value"}};
TestRequestHeaderMapImpl unmatching_headers{{"match-header", "value123"}};
TestRequestHeaderMapImpl unmatching_headers_1{{"match-header", "value123"}};
TestRequestHeaderMapImpl unmatching_headers_2{{"match-header", "123VALUE"}};

const std::string yaml = R"EOF(
name: match-header
Expand All @@ -618,7 +661,8 @@ suffix_match: value
header_data.push_back(
std::make_unique<HeaderUtility::HeaderData>(parseHeaderMatcherFromYaml(yaml)));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers_1, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers_2, header_data));
}

TEST(MatchHeadersTest, HeaderSuffixInverseMatch) {
Expand All @@ -638,9 +682,29 @@ invert_match: true
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
}

TEST(MatchHeadersTest, HeaderSuffixMatchIgnoreCase) {
TestRequestHeaderMapImpl matching_headers_1{{"match-header", "123value"}};
TestRequestHeaderMapImpl matching_headers_2{{"match-header", "123VALUE"}};
TestRequestHeaderMapImpl unmatching_headers{{"match-header", "value123"}};

const std::string yaml = R"EOF(
name: match-header
suffix_match: value
ignore_case: true
)EOF";

std::vector<HeaderUtility::HeaderDataPtr> header_data;
header_data.push_back(
std::make_unique<HeaderUtility::HeaderData>(parseHeaderMatcherFromYaml(yaml)));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers_1, header_data));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers_2, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
}

TEST(MatchHeadersTest, HeaderContainsMatch) {
TestRequestHeaderMapImpl matching_headers{{"match-header", "123onevalue456"}};
TestRequestHeaderMapImpl unmatching_headers{{"match-header", "123anothervalue456"}};
TestRequestHeaderMapImpl unmatching_headers_1{{"match-header", "123anothervalue456"}};
TestRequestHeaderMapImpl unmatching_headers_2{{"match-header", "123ONEVALUE456"}};

const std::string yaml = R"EOF(
name: match-header
Expand All @@ -651,7 +715,8 @@ contains_match: onevalue
header_data.push_back(
std::make_unique<HeaderUtility::HeaderData>(parseHeaderMatcherFromYaml(yaml)));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers_1, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers_2, header_data));
}

TEST(MatchHeadersTest, HeaderContainsInverseMatch) {
Expand All @@ -671,6 +736,25 @@ invert_match: true
EXPECT_FALSE(HeaderUtility::matchHeaders(matching_headers, header_data));
}

TEST(MatchHeadersTest, HeaderContainsMatchIgnoreCase) {
TestRequestHeaderMapImpl matching_headers_1{{"match-header", "123onevalue456"}};
TestRequestHeaderMapImpl matching_headers_2{{"match-header", "123ONEVALUE456"}};
TestRequestHeaderMapImpl unmatching_headers{{"match-header", "123anothervalue456"}};

const std::string yaml = R"EOF(
name: match-header
contains_match: onevalue
ignore_case: true
)EOF";

std::vector<HeaderUtility::HeaderDataPtr> header_data;
header_data.push_back(
std::make_unique<HeaderUtility::HeaderData>(parseHeaderMatcherFromYaml(yaml)));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers_1, header_data));
EXPECT_TRUE(HeaderUtility::matchHeaders(matching_headers_2, header_data));
EXPECT_FALSE(HeaderUtility::matchHeaders(unmatching_headers, header_data));
}

TEST(HeaderIsValidTest, InvalidHeaderValuesAreRejected) {
// ASCII values 1-31 are control characters (with the exception of ASCII
// values 9, 10, and 13 which are a horizontal tab, line feed, and carriage
Expand Down