diff --git a/docs/configuration/http_conn_man/route_config/route.rst b/docs/configuration/http_conn_man/route_config/route.rst index e27eacaab5135..f162237b464ec 100644 --- a/docs/configuration/http_conn_man/route_config/route.rst +++ b/docs/configuration/http_conn_man/route_config/route.rst @@ -198,7 +198,7 @@ The router can match a request to a route based on headers specified in the rout .. code-block:: json [ - {"name": "...", "value": "..."} + {"name": "...", "value": "...", "regex": "..."} ] name @@ -208,6 +208,11 @@ value *(optional, string)* Specifies the value of the header. If the value is absent a request that has the *name* header will match, regardless of the header's value. +regex + *(optional, boolean)* Specifies whether the header value is a regular + expression or not. Defaults to false. The regex grammar used in the value field + is defined `here `_. + The router will check the request's headers against all the specified headers in the route config. A match will happen if all the headers in the route are present in the request with the same values (or based on presence if the ``value`` field is not in the config). diff --git a/docs/configuration/http_filters/fault_filter.rst b/docs/configuration/http_filters/fault_filter.rst index 9749d0f67525e..ceb2463784462 100644 --- a/docs/configuration/http_filters/fault_filter.rst +++ b/docs/configuration/http_filters/fault_filter.rst @@ -119,7 +119,7 @@ actual fault injection further depend on the values of *abort_percent* and .. code-block:: json [ - {"name": "...", "value": "..."} + {"name": "...", "value": "...", "regex": "..."} ] name @@ -130,6 +130,11 @@ value absent a request that has the *name* header will match, regardless of the header's value. +regex + *(optional, boolean)* Specifies whether the header value is a regular expression + or not. Defaults to false. The regex grammar used in the value field + is defined `here `_. + The filter will check the request's headers against all the specified headers in the filter config. A match will happen if all the headers in the config are present in the request with the same values (or based on diff --git a/source/common/http/filter/fault_filter.cc b/source/common/http/filter/fault_filter.cc index 36ae4ab7f9cc2..3ddba4244c6ff 100644 --- a/source/common/http/filter/fault_filter.cc +++ b/source/common/http/filter/fault_filter.cc @@ -60,7 +60,8 @@ FaultFilterConfig::FaultFilterConfig(const Json::Object& json_config, Runtime::L for (const Json::ObjectPtr& header_map : config_headers) { // allow header value to be empty, allows matching to be only based on header presence. fault_filter_headers_.emplace_back(Http::LowerCaseString(header_map->getString("name")), - header_map->getString("value", EMPTY_STRING)); + header_map->getString("value", EMPTY_STRING), + header_map->getBoolean("regex", false)); } } } diff --git a/source/common/router/config_impl.cc b/source/common/router/config_impl.cc index e34738d408d14..398bc8fe1f66c 100644 --- a/source/common/router/config_impl.cc +++ b/source/common/router/config_impl.cc @@ -48,17 +48,20 @@ Upstream::ResourcePriority ConfigUtility::parsePriority(const Json::Object& conf } } -bool ConfigUtility::matchHeaders(const Http::HeaderMap& headers, - const std::vector request_headers) { +bool ConfigUtility::matchHeaders(const Http::HeaderMap& request_headers, + const std::vector config_headers) { bool matches = true; - if (!request_headers.empty()) { - for (const HeaderData& header_data : request_headers) { - const Http::HeaderEntry* header = headers.get(header_data.name_); - if (header_data.value_ == EMPTY_STRING) { + if (!config_headers.empty()) { + for (const HeaderData& cfg_header_data : config_headers) { + const Http::HeaderEntry* header = request_headers.get(cfg_header_data.name_); + if (cfg_header_data.value_.empty()) { matches &= (header != nullptr); + } else if (!cfg_header_data.is_regex_) { + matches &= (header != nullptr) && (header->value() == cfg_header_data.value_.c_str()); } else { - matches &= (header != nullptr) && (header->value() == header_data.value_.c_str()); + matches &= (header != nullptr) && + std::regex_match(header->value().c_str(), cfg_header_data.regex_pattern_); } if (!matches) { break; @@ -91,8 +94,11 @@ RouteEntryImplBase::RouteEntryImplBase(const VirtualHostImpl& vhost, const Json: std::vector config_headers = route.getObjectArray("headers"); for (const Json::ObjectPtr& header_map : config_headers) { // allow header value to be empty, allows matching to be only based on header presence. + // Regex is an opt-in. Unless explicitly mentioned, we will use header values for exact string + // matches. config_headers_.emplace_back(Http::LowerCaseString(header_map->getString("name")), - header_map->getString("value", EMPTY_STRING)); + header_map->getString("value", EMPTY_STRING), + header_map->getBoolean("regex", false)); } } } diff --git a/source/common/router/config_impl.h b/source/common/router/config_impl.h index baac21ff39982..33c14887e5746 100644 --- a/source/common/router/config_impl.h +++ b/source/common/router/config_impl.h @@ -46,11 +46,14 @@ class SslRedirector : public RedirectEntry { class ConfigUtility { public: struct HeaderData { - HeaderData(const Http::LowerCaseString& name, const std::string& value) - : name_(name), value_(value) {} + HeaderData(const Http::LowerCaseString& name, const std::string& value, const bool is_regex) + : name_(name), value_(value), regex_pattern_(value_, std::regex::optimize), + is_regex_(is_regex) {} const Http::LowerCaseString name_; const std::string value_; + const std::regex regex_pattern_; + const bool is_regex_; }; /** diff --git a/test/common/router/config_impl_test.cc b/test/common/router/config_impl_test.cc index aca68929f94f5..8d525f27498f0 100644 --- a/test/common/router/config_impl_test.cc +++ b/test/common/router/config_impl_test.cc @@ -3,8 +3,8 @@ #include "common/json/json_loader.h" #include "common/router/config_impl.h" -#include "test/mocks/upstream/mocks.h" #include "test/mocks/runtime/mocks.h" +#include "test/mocks/upstream/mocks.h" #include "test/test_common/utility.h" using testing::_; @@ -436,6 +436,20 @@ TEST(RouteMatcherTest, HeaderMatchedRouting) { {"name": "test_header_presence"} ] }, + { + "prefix": "/", + "cluster": "local_service_with_header_pattern_set_regex", + "headers" : [ + {"name": "test_header_pattern", "value": "^user=test-\\d+$", "regex": true} + ] + }, + { + "prefix": "/", + "cluster": "local_service_with_header_pattern_unset_regex", + "headers" : [ + {"name": "test_header_pattern", "value": "^customer=test-\\d+$"} + ] + }, { "prefix": "/", "cluster": "local_service_without_headers" @@ -484,6 +498,19 @@ TEST(RouteMatcherTest, HeaderMatchedRouting) { EXPECT_EQ("local_service_with_empty_headers", config.routeForRequest(headers, 0)->clusterName()); } + + { + Http::TestHeaderMapImpl headers = genHeaders("www.lyft.com", "/", "GET"); + headers.addViaCopy("test_header_pattern", "user=test-1223"); + EXPECT_EQ("local_service_with_header_pattern_set_regex", + config.routeForRequest(headers, 0)->clusterName()); + } + + { + Http::TestHeaderMapImpl headers = genHeaders("www.lyft.com", "/", "GET"); + headers.addViaCopy("test_header_pattern", "customer=test-1223"); + EXPECT_EQ("local_service_without_headers", config.routeForRequest(headers, 0)->clusterName()); + } } TEST(RouteMatcherTest, ContentType) {