diff --git a/api/envoy/config/route/v3/route_components.proto b/api/envoy/config/route/v3/route_components.proto index c82592cb5e659..270d1212de840 100644 --- a/api/envoy/config/route/v3/route_components.proto +++ b/api/envoy/config/route/v3/route_components.proto @@ -453,7 +453,7 @@ message WeightedCluster { } } -// [#next-free-field: 14] +// [#next-free-field: 15] message RouteMatch { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RouteMatch"; @@ -518,6 +518,17 @@ message RouteMatch { // Note that CONNECT support is currently considered alpha in Envoy. // [#comment: TODO(htuch): Replace the above comment with an alpha tag.] ConnectMatcher connect_matcher = 12; + + // If specified, the route is a path-separated prefix rule meaning that the + // ``:path`` header (without the query string) must either exactly match the + // ``path_separated_prefix`` or have it as a prefix, followed by ``/`` + // + // For example, ``/api/dev`` would match + // ``/api/dev``, ``/api/dev/``, ``/api/dev/v1``, and ``/api/dev?param=true`` + // but would not match ``/api/developer`` + // + // Expect the value to not contain ``?`` or ``#`` and not to end in ``/`` + string path_separated_prefix = 14 [(validate.rules).string = {pattern: "^[^?#]+[^?#/]$"}]; } // Indicates that prefix/path matching should be case sensitive. The default diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index f1f3f7d90c5db..25a953b2abed3 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -118,6 +118,7 @@ New Features * matching: the matching API can now express a match tree that will always match by omitting a matcher at the top level. * outlier_detection: :ref:`max_ejection_time_jitter` configuration added to allow adding a random value to the ejection time to prevent 'thundering herd' scenarios. Defaults to 0 so as to not break or change the behavior of existing deployments. * redis: support for hostnames returned in ``cluster_slots`` response is now available. +* router: added a path-separated prefix matcher, to make route creation more efficient. :ref:`path_separated_prefix `. * schema_validator_tool: added ``bootstrap`` checking to the :ref:`schema validator check tool `. * schema_validator_tool: added ``--fail-on-deprecated`` and ``--fail-on-wip`` to the diff --git a/envoy/router/router.h b/envoy/router/router.h index a44221f22f4d3..c74fba658e1db 100644 --- a/envoy/router/router.h +++ b/envoy/router/router.h @@ -727,6 +727,7 @@ enum class PathMatchType { Prefix, Exact, Regex, + PathSeparatedPrefix, }; /** diff --git a/source/common/router/config_impl.cc b/source/common/router/config_impl.cc index dffc76bbc3b13..6f00cd1edc5dd 100644 --- a/source/common/router/config_impl.cc +++ b/source/common/router/config_impl.cc @@ -92,6 +92,11 @@ RouteEntryImplBaseConstSharedPtr createAndValidateRoute( route = std::make_shared(vhost, route_config, optional_http_filters, factory_context, validator); break; + case envoy::config::route::v3::RouteMatch::PathSpecifierCase::kPathSeparatedPrefix: { + route = std::make_shared( + vhost, route_config, optional_http_filters, factory_context, validator); + break; + } case envoy::config::route::v3::RouteMatch::PathSpecifierCase::PATH_SPECIFIER_NOT_SET: break; // throw the error below. } @@ -1402,6 +1407,40 @@ RouteConstSharedPtr ConnectRouteEntryImpl::matches(const Http::RequestHeaderMap& return nullptr; } +PathSeparatedPrefixRouteEntryImpl::PathSeparatedPrefixRouteEntryImpl( + const VirtualHostImpl& vhost, const envoy::config::route::v3::Route& route, + const OptionalHttpFilters& optional_http_filters, + Server::Configuration::ServerFactoryContext& factory_context, + ProtobufMessage::ValidationVisitor& validator) + : RouteEntryImplBase(vhost, route, optional_http_filters, factory_context, validator), + prefix_(route.match().path_separated_prefix()), + path_matcher_(Matchers::PathMatcher::createPrefix(prefix_, !case_sensitive_)) {} + +void PathSeparatedPrefixRouteEntryImpl::rewritePathHeader(Http::RequestHeaderMap& headers, + bool insert_envoy_original_path) const { + finalizePathHeader(headers, prefix_, insert_envoy_original_path); +} + +absl::optional PathSeparatedPrefixRouteEntryImpl::currentUrlPathAfterRewrite( + const Http::RequestHeaderMap& headers) const { + return currentUrlPathAfterRewriteWithMatchedPath(headers, prefix_); +} + +RouteConstSharedPtr +PathSeparatedPrefixRouteEntryImpl::matches(const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& stream_info, + uint64_t random_value) const { + if (!RouteEntryImplBase::matchRoute(headers, stream_info, random_value)) { + return nullptr; + } + absl::string_view path = Http::PathUtil::removeQueryAndFragment(headers.getPathValue()); + if (path.size() >= prefix_.size() && path_matcher_->match(path) && + (path.size() == prefix_.size() || path[prefix_.size()] == '/')) { + return clusterEntry(headers, random_value); + } + return nullptr; +} + VirtualHostImpl::VirtualHostImpl( const envoy::config::route::v3::VirtualHost& virtual_host, const OptionalHttpFilters& optional_http_filters, const ConfigImpl& global_route_config, diff --git a/source/common/router/config_impl.h b/source/common/router/config_impl.h index 26f382badc3f8..c226277d261ab 100644 --- a/source/common/router/config_impl.h +++ b/source/common/router/config_impl.h @@ -1103,6 +1103,39 @@ class ConnectRouteEntryImpl : public RouteEntryImplBase { bool supportsPathlessHeaders() const override { return true; } }; +/** + * Route entry implementation for path separated prefix match routing. + */ +class PathSeparatedPrefixRouteEntryImpl : public RouteEntryImplBase { +public: + PathSeparatedPrefixRouteEntryImpl(const VirtualHostImpl& vhost, + const envoy::config::route::v3::Route& route, + const OptionalHttpFilters& optional_http_filters, + Server::Configuration::ServerFactoryContext& factory_context, + ProtobufMessage::ValidationVisitor& validator); + + // Router::PathMatchCriterion + const std::string& matcher() const override { return prefix_; } + PathMatchType matchType() const override { return PathMatchType::PathSeparatedPrefix; } + + // Router::Matchable + RouteConstSharedPtr matches(const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& stream_info, + uint64_t random_value) const override; + + // Router::DirectResponseEntry + void rewritePathHeader(Http::RequestHeaderMap& headers, + bool insert_envoy_original_path) const override; + + // Router::RouteEntry + absl::optional + currentUrlPathAfterRewrite(const Http::RequestHeaderMap& headers) const override; + +private: + const std::string prefix_; + const Matchers::PathMatcherConstSharedPtr path_matcher_; +}; + // Contextual information used to construct the route actions for a match tree. struct RouteActionContext { const VirtualHostImpl& vhost; diff --git a/source/extensions/filters/http/jwt_authn/matcher.cc b/source/extensions/filters/http/jwt_authn/matcher.cc index 964caf9f526bc..0a3e5ef463781 100644 --- a/source/extensions/filters/http/jwt_authn/matcher.cc +++ b/source/extensions/filters/http/jwt_authn/matcher.cc @@ -6,6 +6,7 @@ #include "source/common/common/logger.h" #include "source/common/common/matchers.h" #include "source/common/common/regex.h" +#include "source/common/http/path_utility.h" #include "source/common/router/config_impl.h" #include "absl/strings/match.h" @@ -154,6 +155,31 @@ class ConnectMatcherImpl : public BaseMatcherImpl { return false; } }; + +class PathSeparatedPrefixMatcherImpl : public BaseMatcherImpl { +public: + PathSeparatedPrefixMatcherImpl(const RequirementRule& rule) + : BaseMatcherImpl(rule), prefix_(rule.match().path_separated_prefix()), + path_matcher_(Matchers::PathMatcher::createPrefix(prefix_, !case_sensitive_)) {} + + bool matches(const Http::RequestHeaderMap& headers) const override { + if (!BaseMatcherImpl::matchRoute(headers)) { + return false; + } + absl::string_view path = Http::PathUtil::removeQueryAndFragment(headers.getPathValue()); + if (path.size() >= prefix_.size() && path_matcher_->match(path) && + (path.size() == prefix_.size() || path[prefix_.size()] == '/')) { + ENVOY_LOG(debug, "Path-separated prefix requirement '{}' matched.", prefix_); + return true; + } + return false; + } + +private: + // prefix string + const std::string prefix_; + const Matchers::PathMatcherConstSharedPtr path_matcher_; +}; } // namespace MatcherConstPtr Matcher::create(const RequirementRule& rule) { @@ -166,6 +192,8 @@ MatcherConstPtr Matcher::create(const RequirementRule& rule) { return std::make_unique(rule); case RouteMatch::PathSpecifierCase::kConnectMatcher: return std::make_unique(rule); + case RouteMatch::PathSpecifierCase::kPathSeparatedPrefix: + return std::make_unique(rule); case RouteMatch::PathSpecifierCase::PATH_SPECIFIER_NOT_SET: break; // Fall through to PANIC. } diff --git a/test/common/router/config_impl_test.cc b/test/common/router/config_impl_test.cc index 694cb5c35b0a7..c8280fa725089 100644 --- a/test/common/router/config_impl_test.cc +++ b/test/common/router/config_impl_test.cc @@ -6831,6 +6831,8 @@ name: foo route: { cluster: ww2 } - match: { path: "/exact-path" } route: { cluster: ww2 } + - match: { path_separated_prefix: "/path/separated"} + route: { cluster: ww2 } - match: { prefix: "/"} route: { cluster: www2 } metadata: { filter_metadata: { com.bar.foo: { baz: test_value }, baz: {name: bluh} } } @@ -6844,6 +6846,9 @@ name: foo "/rege[xy]", PathMatchType::Regex); checkPathMatchCriterion(config.route(genHeaders("www.foo.com", "/exact-path", "GET"), 0).get(), "/exact-path", PathMatchType::Exact); + checkPathMatchCriterion( + config.route(genHeaders("www.foo.com", "/path/separated", "GET"), 0).get(), "/path/separated", + PathMatchType::PathSeparatedPrefix); const auto route = config.route(genHeaders("www.foo.com", "/", "GET"), 0); checkPathMatchCriterion(route.get(), "/", PathMatchType::Prefix); @@ -7714,6 +7719,255 @@ TEST_F(RouteMatcherTest, TlsContextMatching) { } } +TEST_F(RouteMatcherTest, PathSeparatedPrefixMatch) { + + const std::string yaml = R"EOF( +virtual_hosts: + - name: path_prefix + domains: ["*"] + routes: + - match: + path_separated_prefix: "/rest/api" + case_sensitive: false + route: { cluster: path-separated-cluster} + - match: + prefix: "/" + route: { cluster: default-cluster} + )EOF"; + + factory_context_.cluster_manager_.initializeClusters( + {"path-separated-cluster", "case-sensitive-cluster", "default-cluster", "rewrite-cluster"}, + {}); + TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true); + + // Exact matches + EXPECT_EQ("path-separated-cluster", + config.route(genHeaders("path.prefix.com", "/rest/api", "GET"), 0) + ->routeEntry() + ->clusterName()); + EXPECT_EQ("path-separated-cluster", + config.route(genHeaders("path.prefix.com", "/rest/api?param=true", "GET"), 0) + ->routeEntry() + ->clusterName()); + EXPECT_EQ("path-separated-cluster", + config.route(genHeaders("path.prefix.com", "/rest/api#fragment", "GET"), 0) + ->routeEntry() + ->clusterName()); + + // Prefix matches + EXPECT_EQ("path-separated-cluster", + config.route(genHeaders("path.prefix.com", "/rest/api/", "GET"), 0) + ->routeEntry() + ->clusterName()); + EXPECT_EQ("path-separated-cluster", + config.route(genHeaders("path.prefix.com", "/rest/api/thing?param=true", "GET"), 0) + ->routeEntry() + ->clusterName()); + EXPECT_EQ("path-separated-cluster", + config.route(genHeaders("path.prefix.com", "/rest/api/thing#fragment", "GET"), 0) + ->routeEntry() + ->clusterName()); + + // Non-matching prefixes + EXPECT_EQ("default-cluster", + config.route(genHeaders("path.prefix.com", "/rest/apithing", "GET"), 0) + ->routeEntry() + ->clusterName()); +} + +TEST_F(RouteMatcherTest, PathSeparatedPrefixMatchRewrite) { + + const std::string yaml = R"EOF( +virtual_hosts: + - name: path_prefix + domains: ["*"] + routes: + - match: + path_separated_prefix: "/rewrite" + route: + prefix_rewrite: "/new/api" + cluster: rewrite-cluster + - match: + prefix: "/" + route: { cluster: default-cluster} + )EOF"; + + factory_context_.cluster_manager_.initializeClusters({"default-cluster", "rewrite-cluster"}, {}); + TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true); + + { // Prefix rewrite exact match + NiceMock stream_info; + Http::TestRequestHeaderMapImpl headers = + genHeaders("path.prefix.com", "/rewrite?param=true#fragment", "GET"); + const RouteEntry* route = config.route(headers, 0)->routeEntry(); + EXPECT_EQ("/new/api?param=true#fragment", route->currentUrlPathAfterRewrite(headers)); + route->finalizeRequestHeaders(headers, stream_info, true); + EXPECT_EQ("rewrite-cluster", route->clusterName()); + EXPECT_EQ("/new/api?param=true#fragment", headers.get_(Http::Headers::get().Path)); + EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); + } + + { // Prefix rewrite long match + NiceMock stream_info; + Http::TestRequestHeaderMapImpl headers = + genHeaders("path.prefix.com", "/rewrite/this?param=true#fragment", "GET"); + const RouteEntry* route = config.route(headers, 0)->routeEntry(); + EXPECT_EQ("/new/api/this?param=true#fragment", route->currentUrlPathAfterRewrite(headers)); + route->finalizeRequestHeaders(headers, stream_info, true); + EXPECT_EQ("rewrite-cluster", route->clusterName()); + EXPECT_EQ("/new/api/this?param=true#fragment", headers.get_(Http::Headers::get().Path)); + EXPECT_EQ("path.prefix.com", headers.get_(Http::Headers::get().Host)); + } +} + +TEST_F(RouteMatcherTest, PathSeparatedPrefixMatchCaseSensitivity) { + + const std::string yaml = R"EOF( +virtual_hosts: + - name: path_prefix + domains: ["*"] + routes: + - match: + path_separated_prefix: "/rest/API" + route: { cluster: case-sensitive} + - match: + path_separated_prefix: "/REST/api" + case_sensitive: true + route: { cluster: case-sensitive-explicit} + - match: + path_separated_prefix: "/rest/api" + case_sensitive: false + route: { cluster: case-insensitive} + - match: + prefix: "/" + route: { cluster: default} + )EOF"; + + factory_context_.cluster_manager_.initializeClusters( + {"case-sensitive", "case-sensitive-explicit", "case-insensitive", "default"}, {}); + TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true); + + EXPECT_EQ("case-sensitive", config.route(genHeaders("path.prefix.com", "/rest/API", "GET"), 0) + ->routeEntry() + ->clusterName()); + EXPECT_EQ("case-sensitive", + config.route(genHeaders("path.prefix.com", "/rest/API?param=true", "GET"), 0) + ->routeEntry() + ->clusterName()); + EXPECT_EQ("case-sensitive", config.route(genHeaders("path.prefix.com", "/rest/API/", "GET"), 0) + ->routeEntry() + ->clusterName()); + EXPECT_EQ("case-sensitive", + config.route(genHeaders("path.prefix.com", "/rest/API/thing?param=true", "GET"), 0) + ->routeEntry() + ->clusterName()); + + EXPECT_EQ("case-sensitive-explicit", + config.route(genHeaders("path.prefix.com", "/REST/api", "GET"), 0) + ->routeEntry() + ->clusterName()); + + EXPECT_EQ("case-insensitive", config.route(genHeaders("path.prefix.com", "/REST/API", "GET"), 0) + ->routeEntry() + ->clusterName()); +} + +TEST_F(RouteMatcherTest, PathSeparatedPrefixMatchTrailingSlash) { + + const std::string yaml = R"EOF( +virtual_hosts: + - name: path_prefix + domains: ["*"] + routes: + - match: + path_separated_prefix: "/rest/api/" + route: { cluster: some-cluster } + )EOF"; + + factory_context_.cluster_manager_.initializeClusters({"some-cluster"}, {}); + EXPECT_THROW(TestConfigImpl(parseRouteConfigurationFromYaml(yaml), factory_context_, true), + EnvoyException); +} + +TEST_F(RouteMatcherTest, PathSeparatedPrefixMatchQueryParam) { + + const std::string yaml = R"EOF( +virtual_hosts: + - name: path_prefix + domains: ["*"] + routes: + - match: + path_separated_prefix: "/rest/api?query=1" + route: { cluster: some-cluster } + )EOF"; + + factory_context_.cluster_manager_.initializeClusters({"some-cluster"}, {}); + EXPECT_THROW(TestConfigImpl(parseRouteConfigurationFromYaml(yaml), factory_context_, true), + EnvoyException); +} + +TEST_F(RouteMatcherTest, PathSeparatedPrefixMatchFragment) { + + const std::string yaml = R"EOF( +virtual_hosts: + - name: path_prefix + domains: ["*"] + routes: + - match: + path_separated_prefix: "/rest/api#fragment" + route: { cluster: some-cluster } + )EOF"; + + factory_context_.cluster_manager_.initializeClusters({"some-cluster"}, {}); + EXPECT_THROW(TestConfigImpl(parseRouteConfigurationFromYaml(yaml), factory_context_, true), + EnvoyException); +} + +TEST_F(RouteMatcherTest, PathSeparatedPrefixMatchBaseCondition) { + + const std::string yaml = R"EOF( +virtual_hosts: + - name: path_prefix + domains: ["*"] + routes: + - match: + path_separated_prefix: "/rest/api" + query_parameters: + - name: param + string_match: + exact: test + headers: + - name: cookies + route: { cluster: some-cluster } + - match: + prefix: "/" + route: { cluster: default } + )EOF"; + + factory_context_.cluster_manager_.initializeClusters({"some-cluster", "default"}, {}); + TestConfigImpl config(parseRouteConfigurationFromYaml(yaml), factory_context_, true); + + { + auto headers = genHeaders("path.prefix.com", "/rest/api?param=test", "GET"); + headers.addCopy("cookies", ""); + + EXPECT_EQ("some-cluster", config.route(headers, 0)->routeEntry()->clusterName()); + } + + { + auto headers = genHeaders("path.prefix.com", "/rest/api?param=test", "GET"); + headers.addCopy("pizza", ""); + + EXPECT_EQ("default", config.route(headers, 0)->routeEntry()->clusterName()); + } + { + auto headers = genHeaders("path.prefix.com", "/rest/api?param=testing", "GET"); + headers.addCopy("cookies", ""); + + EXPECT_EQ("default", config.route(headers, 0)->routeEntry()->clusterName()); + } +} + TEST_F(RouteConfigurationV2, RegexPrefixWithNoRewriteWorksWhenPathChanged) { // Setup regex route entry. the regex is trivial, that's ok as we only want to test that diff --git a/test/extensions/filters/http/jwt_authn/matcher_test.cc b/test/extensions/filters/http/jwt_authn/matcher_test.cc index 3718832e80237..97f063e72242b 100644 --- a/test/extensions/filters/http/jwt_authn/matcher_test.cc +++ b/test/extensions/filters/http/jwt_authn/matcher_test.cc @@ -19,14 +19,17 @@ namespace { class MatcherTest : public testing::Test { public: + MatcherConstPtr createMatcher(const char* config) { + RequirementRule rule; + TestUtility::loadFromYaml(config, rule); + return Matcher::create(rule); + } }; TEST_F(MatcherTest, TestMatchPrefix) { const char config[] = R"(match: prefix: "/match")"; - RequirementRule rule; - TestUtility::loadFromYaml(config, rule); - MatcherConstPtr matcher = Matcher::create(rule); + MatcherConstPtr matcher = createMatcher(config); auto headers = TestRequestHeaderMapImpl{{":path", "/match/this"}}; EXPECT_TRUE(matcher->matches(headers)); headers = TestRequestHeaderMapImpl{{":path", "/MATCH"}}; @@ -46,9 +49,7 @@ TEST_F(MatcherTest, TestMatchSafeRegex) { google_re2: {} regex: "/[^c][au]t")"; - RequirementRule rule; - TestUtility::loadFromYaml(config, rule); - MatcherConstPtr matcher = Matcher::create(rule); + MatcherConstPtr matcher = createMatcher(config); auto headers = TestRequestHeaderMapImpl{{":path", "/but"}}; EXPECT_TRUE(matcher->matches(headers)); headers = TestRequestHeaderMapImpl{{":path", "/mat?ok=bye"}}; @@ -65,9 +66,7 @@ TEST_F(MatcherTest, TestMatchPath) { const char config[] = R"(match: path: "/match" case_sensitive: false)"; - RequirementRule rule; - TestUtility::loadFromYaml(config, rule); - MatcherConstPtr matcher = Matcher::create(rule); + MatcherConstPtr matcher = createMatcher(config); auto headers = TestRequestHeaderMapImpl{{":path", "/match"}}; EXPECT_TRUE(matcher->matches(headers)); headers = TestRequestHeaderMapImpl{{":path", "/MATCH"}}; @@ -89,9 +88,7 @@ TEST_F(MatcherTest, TestMatchQuery) { - name: foo string_match: exact: bar)"; - RequirementRule rule; - TestUtility::loadFromYaml(config, rule); - MatcherConstPtr matcher = Matcher::create(rule); + MatcherConstPtr matcher = createMatcher(config); auto headers = TestRequestHeaderMapImpl{{":path", "/boo?foo=bar"}}; EXPECT_TRUE(matcher->matches(headers)); headers = TestRequestHeaderMapImpl{{":path", "/boo?ok=bye"}}; @@ -109,9 +106,7 @@ TEST_F(MatcherTest, TestMatchHeader) { prefix: "/" headers: - name: a)"; - RequirementRule rule; - TestUtility::loadFromYaml(config, rule); - MatcherConstPtr matcher = Matcher::create(rule); + MatcherConstPtr matcher = createMatcher(config); auto headers = TestRequestHeaderMapImpl{{":path", "/"}, {"a", ""}}; EXPECT_TRUE(matcher->matches(headers)); headers = TestRequestHeaderMapImpl{{":path", "/"}, {"a", "some"}, {"b", ""}}; @@ -131,9 +126,7 @@ TEST_F(MatcherTest, TestMatchPathAndHeader) { - name: foo string_match: exact: bar)"; - RequirementRule rule; - TestUtility::loadFromYaml(config, rule); - MatcherConstPtr matcher = Matcher::create(rule); + MatcherConstPtr matcher = createMatcher(config); auto headers = TestRequestHeaderMapImpl{{":path", "/boo?foo=bar"}}; EXPECT_TRUE(matcher->matches(headers)); headers = TestRequestHeaderMapImpl{{":path", "/boo?ok=bye"}}; @@ -149,9 +142,7 @@ TEST_F(MatcherTest, TestMatchPathAndHeader) { TEST_F(MatcherTest, TestMatchConnect) { const char config[] = R"(match: connect_matcher: {})"; - RequirementRule rule; - TestUtility::loadFromYaml(config, rule); - MatcherConstPtr matcher = Matcher::create(rule); + MatcherConstPtr matcher = createMatcher(config); auto headers = TestRequestHeaderMapImpl{{":method", "CONNECT"}}; EXPECT_TRUE(matcher->matches(headers)); headers = TestRequestHeaderMapImpl{{":method", "GET"}}; @@ -165,9 +156,7 @@ TEST_F(MatcherTest, TestMatchConnectQuery) { - name: foo string_match: exact: "bar")"; - RequirementRule rule; - TestUtility::loadFromYaml(config, rule); - MatcherConstPtr matcher = Matcher::create(rule); + MatcherConstPtr matcher = createMatcher(config); auto headers = TestRequestHeaderMapImpl{{":method", "CONNECT"}, {":path", "/boo?foo=bar"}}; EXPECT_TRUE(matcher->matches(headers)); headers = TestRequestHeaderMapImpl{{":method", "GET"}, {":path", "/boo?foo=bar"}}; @@ -176,6 +165,92 @@ TEST_F(MatcherTest, TestMatchConnectQuery) { EXPECT_FALSE(matcher->matches(headers)); } +TEST_F(MatcherTest, TestMatchPathSeparatedPrefix) { + const char config[] = R"(match: + path_separated_prefix: "/rest/api")"; + MatcherConstPtr matcher = createMatcher(config); + + // Exact matches + auto headers = TestRequestHeaderMapImpl{{":path", "/rest/api"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestRequestHeaderMapImpl{{":path", "/rest/api?param=true"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestRequestHeaderMapImpl{{":path", "/rest/api#fragment"}}; + EXPECT_TRUE(matcher->matches(headers)); + + // Prefix matches + headers = TestRequestHeaderMapImpl{{":path", "/rest/api/"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestRequestHeaderMapImpl{{":path", "/rest/api/thing?param=true"}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestRequestHeaderMapImpl{{":path", "/rest/api/thing#fragment"}}; + EXPECT_TRUE(matcher->matches(headers)); + + // Non-matching prefixes + headers = TestRequestHeaderMapImpl{{":path", "/rest/apithing"}}; + EXPECT_FALSE(matcher->matches(headers)); +} + +TEST_F(MatcherTest, TestMatchPathSeparatedPrefixCaseSensitivity) { + + const char configCaseSensitive[] = R"(match: + path_separated_prefix: "/rest/API")"; + MatcherConstPtr matcherSensitive = createMatcher(configCaseSensitive); + + const char configCaseSensitiveExplicit[] = R"(match: + path_separated_prefix: "/rest/API" + case_sensitive: true)"; + MatcherConstPtr matcherSensitiveExplicit = createMatcher(configCaseSensitiveExplicit); + + const char configCaseInsensitive[] = R"(match: + path_separated_prefix: "/rest/api" + case_sensitive: false)"; + MatcherConstPtr matcherInsensitive = createMatcher(configCaseInsensitive); + + auto headers = TestRequestHeaderMapImpl{{":path", "/rest/API"}}; + EXPECT_TRUE(matcherSensitive->matches(headers)); + EXPECT_TRUE(matcherSensitiveExplicit->matches(headers)); + EXPECT_TRUE(matcherInsensitive->matches(headers)); + + headers = TestRequestHeaderMapImpl{{":path", "/rest/API/"}}; + EXPECT_TRUE(matcherSensitive->matches(headers)); + EXPECT_TRUE(matcherSensitiveExplicit->matches(headers)); + EXPECT_TRUE(matcherInsensitive->matches(headers)); + + headers = TestRequestHeaderMapImpl{{":path", "/rest/API?param=true"}}; + EXPECT_TRUE(matcherSensitive->matches(headers)); + EXPECT_TRUE(matcherSensitiveExplicit->matches(headers)); + EXPECT_TRUE(matcherInsensitive->matches(headers)); + + headers = TestRequestHeaderMapImpl{{":path", "/rest/API/thing?param=true"}}; + EXPECT_TRUE(matcherSensitive->matches(headers)); + EXPECT_TRUE(matcherSensitiveExplicit->matches(headers)); + EXPECT_TRUE(matcherInsensitive->matches(headers)); + + headers = TestRequestHeaderMapImpl{{":path", "/REST/API"}}; + EXPECT_FALSE(matcherSensitive->matches(headers)); + EXPECT_FALSE(matcherSensitiveExplicit->matches(headers)); + EXPECT_TRUE(matcherInsensitive->matches(headers)); +} + +TEST_F(MatcherTest, TestMatchPathSeparatedPrefixBaseCondition) { + const char config[] = R"(match: + path_separated_prefix: "/rest/api" + query_parameters: + - name: param + string_match: + exact: test + headers: + - name: cookies)"; + MatcherConstPtr matcher = createMatcher(config); + auto headers = TestRequestHeaderMapImpl{{":path", "/rest/api?param=test"}, {"cookies", ""}}; + EXPECT_TRUE(matcher->matches(headers)); + headers = TestRequestHeaderMapImpl{{":path", "/rest/api?param=test"}, {"pizza", ""}}; + EXPECT_FALSE(matcher->matches(headers)); + headers = TestRequestHeaderMapImpl{{":path", "/rest/api"}, {"cookies", ""}}; + EXPECT_FALSE(matcher->matches(headers)); +} + } // namespace } // namespace JwtAuthn } // namespace HttpFilters