diff --git a/api/bazel/repository_locations.bzl b/api/bazel/repository_locations.bzl index 98fa8906b1dec..b79a09d1ea618 100644 --- a/api/bazel/repository_locations.bzl +++ b/api/bazel/repository_locations.bzl @@ -33,9 +33,9 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_desc = "xDS API Working Group (xDS-WG)", project_url = "https://github.com/cncf/xds", # During the UDPA -> xDS migration, we aren't working with releases. - version = "7f1daf1720fc185f3b63f70d25aefaeef83d88d7", - sha256 = "62c0daaff43fd9a62c280bf2b0c2b670372b24377ea5e9ea4302cf748dd53cba", - release_date = "2022-03-14", + version = "eded343319d09f30032952beda9840bbd3dcf7ac", + sha256 = "79f15d6d3e9b2948174f583f6ad435a63e84b1e84acfc9ae2587be6798e44590", + release_date = "2022-03-30", strip_prefix = "xds-{version}", urls = ["https://github.com/cncf/xds/archive/{version}.tar.gz"], use_category = ["api"], diff --git a/docs/root/api-v3/common_messages/common_messages.rst b/docs/root/api-v3/common_messages/common_messages.rst index e4e126e6912cd..1196cd4d105fe 100644 --- a/docs/root/api-v3/common_messages/common_messages.rst +++ b/docs/root/api-v3/common_messages/common_messages.rst @@ -35,6 +35,7 @@ Common messages ../extensions/matching/common_inputs/network/v3/network_inputs.proto ../../xds/type/v3/range.proto ../../xds/type/v3/typed_struct.proto + ../../xds/type/matcher/v3/domain.proto ../../xds/type/matcher/v3/ip.proto ../../xds/type/matcher/v3/matcher.proto ../../xds/type/matcher/v3/range.proto diff --git a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst index a178ba64925e2..efe0d24f28d14 100644 --- a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst +++ b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst @@ -66,11 +66,16 @@ Custom Matching Algorithms ************************** In addition to the built-in exact and prefix matchers, these custom matchers -are available in some contexts: +are available in any context: .. _extension_envoy.matching.custom_matchers.trie_matcher: -* :ref:`Trie-based IP matcher ` applies to network inputs. +* :ref:`Trie-based IP matcher ` for matching IP addresses. + +.. _extension_envoy.matching.custom_matchers.domain_matcher: + +* :ref:`Server name matcher ` for matching domain names (e.g. + `SNI `). Filter Integration ################## diff --git a/source/extensions/common/matcher/BUILD b/source/extensions/common/matcher/BUILD index a4b96c3f92bfa..73a145776c98d 100644 --- a/source/extensions/common/matcher/BUILD +++ b/source/extensions/common/matcher/BUILD @@ -26,6 +26,7 @@ envoy_cc_extension( srcs = ["trie_matcher.cc"], hdrs = ["trie_matcher.h"], deps = [ + "//envoy/http:filter_interface", "//envoy/matcher:matcher_interface", "//envoy/network:filter_interface", "//envoy/registry", @@ -36,3 +37,17 @@ envoy_cc_extension( "@com_github_cncf_udpa//xds/type/matcher/v3:pkg_cc_proto", ], ) + +envoy_cc_extension( + name = "domain_matcher_lib", + srcs = ["domain_matcher.cc"], + hdrs = ["domain_matcher.h"], + deps = [ + "//envoy/http:filter_interface", + "//envoy/matcher:matcher_interface", + "//envoy/network:filter_interface", + "//envoy/registry", + "//source/common/matcher:matcher_lib", + "@com_github_cncf_udpa//xds/type/matcher/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/common/matcher/domain_matcher.cc b/source/extensions/common/matcher/domain_matcher.cc new file mode 100644 index 0000000000000..7cbc8ec0c11f9 --- /dev/null +++ b/source/extensions/common/matcher/domain_matcher.cc @@ -0,0 +1,48 @@ +#include "source/extensions/common/matcher/domain_matcher.h" + +#include "envoy/http/filter.h" +#include "envoy/network/filter.h" +#include "envoy/registry/registry.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Matcher { + +void DomainMatcherUtility::validateServerName(const std::string& server_name) { + auto pos = server_name.rfind('*'); + if (pos != std::string::npos) { + if (pos != 0) { + throw EnvoyException(fmt::format("wildcard only allowed in the prefix: {}", server_name)); + } + if (server_name != "*" && !absl::StartsWith(server_name, "*.")) { + throw EnvoyException(fmt::format("wildcard must be the first domain part: {}", server_name)); + } + } + // Reject internationalized domains to avoid ambiguity with case sensitivity. + for (char c : server_name) { + if (!absl::ascii_isascii(c)) { + throw EnvoyException(fmt::format("non-ASCII domains are not supported: {}", server_name)); + } + } +} + +void DomainMatcherUtility::duplicateServerNameError(const std::string& server_name) { + throw EnvoyException(fmt::format("duplicate domain: {}", server_name)); +} + +class NetworkDomainMatcherFactory : public DomainMatcherFactoryBase {}; +class UdpNetworkDomainMatcherFactory : public DomainMatcherFactoryBase {}; +class HttpDomainMatcherFactory : public DomainMatcherFactoryBase {}; + +REGISTER_FACTORY(NetworkDomainMatcherFactory, + ::Envoy::Matcher::CustomMatcherFactory); +REGISTER_FACTORY(UdpNetworkDomainMatcherFactory, + ::Envoy::Matcher::CustomMatcherFactory); +REGISTER_FACTORY(HttpDomainMatcherFactory, + ::Envoy::Matcher::CustomMatcherFactory); + +} // namespace Matcher +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/matcher/domain_matcher.h b/source/extensions/common/matcher/domain_matcher.h new file mode 100644 index 0000000000000..28b121649387c --- /dev/null +++ b/source/extensions/common/matcher/domain_matcher.h @@ -0,0 +1,209 @@ +#pragma once + +#include "envoy/common/optref.h" +#include "envoy/matcher/matcher.h" +#include "envoy/server/factory_context.h" + +#include "source/common/matcher/matcher.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" +#include "absl/strings/str_split.h" +#include "absl/types/optional.h" +#include "absl/types/span.h" +#include "xds/type/matcher/v3/domain.pb.h" +#include "xds/type/matcher/v3/domain.pb.validate.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Matcher { + +using ::Envoy::Matcher::DataInputGetResult; +using ::Envoy::Matcher::DataInputPtr; +using ::Envoy::Matcher::evaluateMatch; +using ::Envoy::Matcher::MatchState; +using ::Envoy::Matcher::MatchTree; +using ::Envoy::Matcher::OnMatch; + +/** + * A "compressed" trie node with individual domain parts as edge values. Example: + * + * (P) + * `com` `org` + * (P) () + * `envoy` + * (E) + * + * with `P` signifying the prefix match and `E` signifying the exact match, + * represents the following patterns: + * - `*` that matches any domain; + * - `*.com` that matches any domain ending with `.com` but not `com`; + * - `envoy.org` that matches the domain `envoy.org` exactly. + */ +template struct DomainNode { + absl::flat_hash_map> children_; + // Matches the exact path from the root. + OptRef> exact_on_match_; + // Matches the prefix for the path from the root. + OptRef> prefix_on_match_; + // Closest parent node with a prefix match. + OptRef> parent_prefix_; + + // Insert an on-match for the parts of the domain by handling the last optional + // wildcard symbol as a special non-part. + void insert(absl::Span parts, OnMatch& on_match) { + if (parts.empty()) { + ASSERT(!exact_on_match_); + exact_on_match_ = makeOptRef(on_match); + } else if (parts[0] == "*") { + // Prefix wildcards are unique and wildcard must be the last part. + ASSERT(!prefix_on_match_ && parts.size() == 1); + prefix_on_match_ = makeOptRef(on_match); + } else { + DomainNode& child = children_[parts[0]]; + return child.insert(parts.subspan(1), on_match); + } + } + + // Link parent prefix from child nodes. + void link(OptRef> parent_prefix) { + parent_prefix_ = parent_prefix; + if (prefix_on_match_) { + parent_prefix = makeOptRef(*this); + } + for (auto& [_, child] : children_) { + child.link(parent_prefix); + } + } + + // Find the deepest node matching the reversed parts of the domain. + // Returns a node and a bool indicating whether the match is exact for this node. + std::pair find(absl::Span parts) const { + if (parts.empty()) { + return std::make_pair(this, true); + } + auto it = children_.find(parts[0]); + if (it != children_.end()) { + return it->second.find(parts.subspan(1)); + } + return std::make_pair(this, false); + } +}; + +template struct DomainTree { + DomainNode root_; + std::vector> on_matches_; +}; + +/** + * General utilities for domain name matching. + */ +class DomainMatcherUtility { +public: + static void validateServerName(const std::string& server_name); + static void duplicateServerNameError(const std::string& server_name); +}; + +/** + * Implementation of a `sublinear` domain matcher using a trie. + **/ +template class DomainMatcher : public MatchTree { +public: + DomainMatcher(DataInputPtr&& data_input, absl::optional> on_no_match, + const std::shared_ptr>& domain_tree) + : data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)), + domain_tree_(domain_tree) {} + + typename MatchTree::MatchResult match(const DataType& data) override { + const auto input = data_input_->get(data); + if (input.data_availability_ != DataInputGetResult::DataAvailability::AllDataAvailable) { + return {MatchState::UnableToMatch, absl::nullopt}; + } + if (!input.data_) { + return {MatchState::MatchComplete, on_no_match_}; + } + std::vector parts = absl::StrSplit(*input.data_, "."); + std::reverse(parts.begin(), parts.end()); + // Traverse from the most specific match to the root and check for prefix matches. + auto [node, exact] = domain_tree_->root_.find(parts); + while (node) { + OptRef> on_match; + if (exact) { + on_match = node->exact_on_match_; + exact = false; + } else { + on_match = node->prefix_on_match_; + } + if (on_match) { + if (on_match->action_cb_) { + return {MatchState::MatchComplete, OnMatch{on_match->action_cb_, nullptr}}; + } + auto matched = evaluateMatch(*on_match->matcher_, data); + if (matched.match_state_ == MatchState::UnableToMatch) { + return {MatchState::UnableToMatch, absl::nullopt}; + } + if (matched.match_state_ == MatchState::MatchComplete && matched.result_) { + return {MatchState::MatchComplete, OnMatch{matched.result_, nullptr}}; + } + } + node = node->parent_prefix_.ptr(); + } + return {MatchState::MatchComplete, on_no_match_}; + } + +private: + const DataInputPtr data_input_; + const absl::optional> on_no_match_; + const std::shared_ptr> domain_tree_; +}; + +template +class DomainMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory { +public: + ::Envoy::Matcher::MatchTreeFactoryCb createCustomMatcherFactoryCb( + const Protobuf::Message& config, Server::Configuration::ServerFactoryContext& factory_context, + ::Envoy::Matcher::DataInputFactoryCb data_input, + absl::optional<::Envoy::Matcher::OnMatchFactoryCb> on_no_match, + ::Envoy::Matcher::OnMatchFactory& on_match_factory) override { + const auto& typed_config = + MessageUtil::downcastAndValidate( + config, factory_context.messageValidationVisitor()); + auto domain_tree = std::make_shared>(); + // Ensures pointer stability when populating the vector. + domain_tree->on_matches_.reserve(typed_config.domain_matchers().size()); + absl::flat_hash_map>> server_names; + for (const auto& domain_matcher : typed_config.domain_matchers()) { + auto& on_match = domain_tree->on_matches_.emplace_back( + on_match_factory.createOnMatch(domain_matcher.on_match()).value()()); + for (const auto& server_name : domain_matcher.domains()) { + DomainMatcherUtility::validateServerName(server_name); + auto [_, inserted] = server_names.try_emplace(absl::AsciiStrToLower(server_name), on_match); + if (!inserted) { + DomainMatcherUtility::duplicateServerNameError(server_name); + } + } + } + for (const auto& [server_name, on_match] : server_names) { + std::vector parts = absl::StrSplit(server_name, "."); + std::reverse(parts.begin(), parts.end()); + domain_tree->root_.insert(parts, on_match); + } + domain_tree->root_.link(OptRef>()); + return [data_input, on_no_match, domain_tree]() { + return std::make_unique>( + data_input(), on_no_match ? absl::make_optional(on_no_match.value()()) : absl::nullopt, + domain_tree); + }; + } + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + std::string name() const override { return "envoy.matching.custom_matchers.domain_matcher"; } +}; + +} // namespace Matcher +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/matcher/trie_matcher.cc b/source/extensions/common/matcher/trie_matcher.cc index 99cfcabd30566..bab889286c2ff 100644 --- a/source/extensions/common/matcher/trie_matcher.cc +++ b/source/extensions/common/matcher/trie_matcher.cc @@ -1,5 +1,7 @@ #include "source/extensions/common/matcher/trie_matcher.h" +#include "envoy/http/filter.h" +#include "envoy/network/filter.h" #include "envoy/registry/registry.h" namespace Envoy { @@ -7,10 +9,16 @@ namespace Extensions { namespace Common { namespace Matcher { +class NetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; +class UdpNetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; +class HttpTrieMatcherFactory : public TrieMatcherFactoryBase {}; + REGISTER_FACTORY(NetworkTrieMatcherFactory, ::Envoy::Matcher::CustomMatcherFactory); REGISTER_FACTORY(UdpNetworkTrieMatcherFactory, ::Envoy::Matcher::CustomMatcherFactory); +REGISTER_FACTORY(HttpTrieMatcherFactory, + ::Envoy::Matcher::CustomMatcherFactory); } // namespace Matcher } // namespace Common diff --git a/source/extensions/common/matcher/trie_matcher.h b/source/extensions/common/matcher/trie_matcher.h index 028795142b8c4..ac113de893124 100644 --- a/source/extensions/common/matcher/trie_matcher.h +++ b/source/extensions/common/matcher/trie_matcher.h @@ -1,7 +1,6 @@ #pragma once #include "envoy/matcher/matcher.h" -#include "envoy/network/filter.h" #include "envoy/server/factory_context.h" #include "source/common/matcher/matcher.h" @@ -153,9 +152,6 @@ class TrieMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory {}; -class UdpNetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; - } // namespace Matcher } // namespace Common } // namespace Extensions diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 173e99f39a904..e7a300468d24d 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -336,6 +336,7 @@ EXTENSIONS = { # "envoy.matching.custom_matchers.trie_matcher": "//source/extensions/common/matcher:trie_matcher_lib", + "envoy.matching.custom_matchers.domain_matcher": "//source/extensions/common/matcher:domain_matcher_lib", } # These can be changed to ["//visibility:public"], for downstream builds which diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index af43c81ed081b..7b521de6abe11 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -823,5 +823,12 @@ envoy.matching.inputs.application_protocol: envoy.matching.custom_matchers.trie_matcher: categories: - envoy.matching.network.custom_matchers + - envoy.matching.http.custom_matchers + security_posture: unknown + status: alpha +envoy.matching.custom_matchers.domain_matcher: + categories: + - envoy.matching.network.custom_matchers + - envoy.matching.http.custom_matchers security_posture: unknown status: alpha diff --git a/test/extensions/common/matcher/BUILD b/test/extensions/common/matcher/BUILD index a6a0bf0d0a5ad..f45da22db52bd 100644 --- a/test/extensions/common/matcher/BUILD +++ b/test/extensions/common/matcher/BUILD @@ -1,6 +1,7 @@ load( "//bazel:envoy_build_system.bzl", "envoy_cc_test", + "envoy_cc_test_library", "envoy_package", ) @@ -22,18 +23,38 @@ envoy_cc_test( name = "trie_matcher_test", srcs = ["trie_matcher_test.cc"], deps = [ - "//source/common/matcher:matcher_lib", + ":custom_matcher_test", "//source/common/network:address_lib", "//source/common/network/matching:data_impl_lib", "//source/common/network/matching:inputs_lib", "//source/extensions/common/matcher:trie_matcher_lib", - "//test/common/matcher:test_utility_lib", "//test/mocks/http:http_mocks", - "//test/mocks/matcher:matcher_mocks", "//test/mocks/network:network_mocks", - "//test/mocks/server:factory_context_mocks", "//test/mocks/stream_info:stream_info_mocks", + ], +) + +envoy_cc_test_library( + name = "custom_matcher_test", + hdrs = ["custom_matcher_test.h"], + deps = [ + "//envoy/matcher:matcher_interface", + "//envoy/registry", + "//source/common/matcher:matcher_lib", + "//source/common/protobuf:utility_lib", + "//test/common/matcher:test_utility_lib", + "//test/mocks/matcher:matcher_mocks", + "//test/mocks/server:factory_context_mocks", "//test/test_common:registry_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) + +envoy_cc_test( + name = "domain_matcher_test", + srcs = ["domain_matcher_test.cc"], + deps = [ + ":custom_matcher_test", + "//source/extensions/common/matcher:domain_matcher_lib", + ], +) diff --git a/test/extensions/common/matcher/custom_matcher_test.h b/test/extensions/common/matcher/custom_matcher_test.h new file mode 100644 index 0000000000000..0614121646047 --- /dev/null +++ b/test/extensions/common/matcher/custom_matcher_test.h @@ -0,0 +1,80 @@ +#include "envoy/config/core/v3/extension.pb.h" +#include "envoy/matcher/matcher.h" +#include "envoy/registry/registry.h" + +#include "source/common/matcher/matcher.h" +#include "source/common/protobuf/utility.h" + +#include "test/common/matcher/test_utility.h" +#include "test/mocks/matcher/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/test_common/registry.h" + +#include "gtest/gtest.h" +#include "xds/type/matcher/v3/matcher.pb.h" +#include "xds/type/matcher/v3/matcher.pb.validate.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Matcher { + +using ::Envoy::Matcher::ActionFactory; +using ::Envoy::Matcher::CustomMatcherFactory; +using ::Envoy::Matcher::MatchState; +using ::Envoy::Matcher::MatchTreeFactory; +using ::Envoy::Matcher::MockMatchTreeValidationVisitor; +using ::Envoy::Matcher::StringAction; +using ::Envoy::Matcher::StringActionFactory; +using ::Envoy::Matcher::TestData; + +template class CustomMatcherTest : public ::testing::Test { +public: + CustomMatcherTest() + : inject_action_(action_factory_), inject_matcher_(matcher_factory_), + factory_(context_, factory_context_, validation_visitor_) { + EXPECT_CALL(validation_visitor_, performDataInputValidation(_, _)).Times(testing::AnyNumber()); + } + + void loadConfig(const std::string& config) { + MessageUtil::loadFromYaml(config, matcher_, ProtobufMessage::getStrictValidationVisitor()); + TestUtility::validate(matcher_); + } + void validateMatch(const std::string& output) { + auto match_tree = factory_.create(matcher_); + const auto result = match_tree()->match(TestData()); + EXPECT_EQ(result.match_state_, MatchState::MatchComplete); + EXPECT_TRUE(result.on_match_.has_value()); + EXPECT_NE(result.on_match_->action_cb_, nullptr); + auto action = result.on_match_->action_cb_(); + const auto value = action->template getTyped(); + EXPECT_EQ(value.string_, output); + } + void validateNoMatch() { + auto match_tree = factory_.create(matcher_); + const auto result = match_tree()->match(TestData()); + EXPECT_EQ(result.match_state_, MatchState::MatchComplete); + EXPECT_FALSE(result.on_match_.has_value()); + } + void validateUnableToMatch() { + auto match_tree = factory_.create(matcher_); + const auto result = match_tree()->match(TestData()); + EXPECT_EQ(result.match_state_, MatchState::UnableToMatch); + } + + StringActionFactory action_factory_; + Registry::InjectFactory> inject_action_; + CustomMatcherFactoryBase matcher_factory_; + Registry::InjectFactory> inject_matcher_; + MockMatchTreeValidationVisitor validation_visitor_; + + absl::string_view context_ = ""; + NiceMock factory_context_; + MatchTreeFactory factory_; + xds::type::matcher::v3::Matcher matcher_; +}; + +} // namespace Matcher +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/common/matcher/domain_matcher_test.cc b/test/extensions/common/matcher/domain_matcher_test.cc new file mode 100644 index 0000000000000..ba4df910d77c1 --- /dev/null +++ b/test/extensions/common/matcher/domain_matcher_test.cc @@ -0,0 +1,379 @@ +#include + +#include "source/extensions/common/matcher/domain_matcher.h" + +#include "test/extensions/common/matcher/custom_matcher_test.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Matcher { +namespace { + +using ::Envoy::Matcher::TestDataInputFactory; + +class DomainMatcherTest + : public CustomMatcherTest> {}; + +TEST_F(DomainMatcherTest, TestMatcher) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "*.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo + - domains: + - example.com + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: bar + )EOF"; + loadConfig(yaml); + + { + auto input = TestDataInputFactory("input", "example.com"); + validateMatch("bar"); + } + { + auto input = TestDataInputFactory("input", "envoy.com"); + validateMatch("foo"); + } + { + auto input = TestDataInputFactory("input", "envoy.org"); + validateNoMatch(); + } + { + auto input = TestDataInputFactory("input", "xxx"); + validateNoMatch(); + } +} + +TEST_F(DomainMatcherTest, TestMatcherInvalidWildcard) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "*.com.*" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo + )EOF"; + loadConfig(yaml); + auto input = TestDataInputFactory("input", "example.com"); + EXPECT_THROW_WITH_MESSAGE(validateMatch("foo"), EnvoyException, + "wildcard only allowed in the prefix: *.com.*"); +} + +TEST_F(DomainMatcherTest, TestMatcherPartialWildcard) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "*nvoy.org" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo + )EOF"; + loadConfig(yaml); + auto input = TestDataInputFactory("input", "example.com"); + EXPECT_THROW_WITH_MESSAGE(validateMatch("foo"), EnvoyException, + "wildcard must be the first domain part: *nvoy.org"); +} + +TEST_F(DomainMatcherTest, TestMatcherUnicodeDomain) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "®.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo + )EOF"; + loadConfig(yaml); + auto input = TestDataInputFactory("input", "example.com"); + EXPECT_THROW_WITH_MESSAGE(validateMatch("foo"), EnvoyException, + "non-ASCII domains are not supported: ®.com"); +} + +TEST_F(DomainMatcherTest, TestMatcherDuplicateDomain) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "example.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo + - domains: + - "EXAMPLE.COM" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: bar + )EOF"; + loadConfig(yaml); + auto input = TestDataInputFactory("input", "example.com"); + EXPECT_THROW_WITH_MESSAGE(validateMatch("foo"), EnvoyException, "duplicate domain: EXAMPLE.COM"); +} + +TEST_F(DomainMatcherTest, TestMatcherOnNoMatch) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "example.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo +on_no_match: + action: + name: bar + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: bar + )EOF"; + loadConfig(yaml); + + { + auto input = TestDataInputFactory("input", "example.com"); + validateMatch("foo"); + } + { + auto input = TestDataInputFactory("input", "envoy.com"); + validateMatch("bar"); + } + { + auto input = TestDataInputFactory("input", "xxx"); + validateMatch("bar"); + } + { + auto input = TestDataInputFactory( + "input", {DataInputGetResult::DataAvailability::AllDataAvailable, absl::nullopt}); + validateMatch("bar"); + } + { + auto input = TestDataInputFactory( + "input", {DataInputGetResult::DataAvailability::MoreDataMightBeAvailable, "example.com"}); + validateUnableToMatch(); + } +} + +TEST_F(DomainMatcherTest, TestMatcherComplex) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "*" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m1 + - domains: + - "*.com" + - "*.org" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m2 + - domains: + - "com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m3 + - domains: + - "example.com" + - "*.example.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m4 + - domains: + - "sub.example.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m5 + - domains: + - "*.envoy.org" + on_match: + matcher: + matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "*.sub.envoy.org" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m6 + - domains: + - "*" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m7 + )EOF"; + loadConfig(yaml); + + // The trie above represents the following decision tree: + // + // *(m1 prefix) + // com(m2 prefix, m3 exact) org(m2 prefix) + // example.com (m4) + // sub.example.com (m5 exact) envoy.org (m7 prefix) + // sub.envoy.org (m6 prefix) + { + auto input = TestDataInputFactory("input", "envoy"); + validateMatch("m1"); + } + { + auto input = TestDataInputFactory("input", "com"); + validateMatch("m3"); + } + { + auto input = TestDataInputFactory("input", "org"); + validateMatch("m1"); + } + { + auto input = TestDataInputFactory("input", "envoy.com"); + validateMatch("m2"); + } + { + auto input = TestDataInputFactory("input", "example.com"); + validateMatch("m4"); + } + { + auto input = TestDataInputFactory("input", "host.example.com"); + validateMatch("m4"); + } + { + auto input = TestDataInputFactory("input", "sub.example.com"); + validateMatch("m5"); + } + { + auto input = TestDataInputFactory("input", "host.sub.example.com"); + validateMatch("m4"); + } + { + auto input = TestDataInputFactory("input", "envoy.org"); + validateMatch("m2"); + } + { + auto input = TestDataInputFactory("input", "sub.envoy.org"); + validateMatch("m7"); + } + { + auto input = TestDataInputFactory("input", "host.sub.envoy.org"); + validateMatch("m6"); + } +} + +} // namespace +} // namespace Matcher +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/common/matcher/trie_matcher_test.cc b/test/extensions/common/matcher/trie_matcher_test.cc index a3ee5b0584841..e81187577a884 100644 --- a/test/extensions/common/matcher/trie_matcher_test.cc +++ b/test/extensions/common/matcher/trie_matcher_test.cc @@ -1,25 +1,11 @@ #include -#include "envoy/config/core/v3/extension.pb.h" -#include "envoy/matcher/matcher.h" -#include "envoy/registry/registry.h" - -#include "source/common/matcher/matcher.h" #include "source/common/network/address_impl.h" #include "source/common/network/matching/data_impl.h" -#include "source/common/protobuf/utility.h" #include "source/extensions/common/matcher/trie_matcher.h" -#include "test/common/matcher/test_utility.h" -#include "test/mocks/matcher/mocks.h" +#include "test/extensions/common/matcher/custom_matcher_test.h" #include "test/mocks/network/mocks.h" -#include "test/mocks/server/factory_context.h" -#include "test/test_common/registry.h" -#include "test/test_common/utility.h" - -#include "gtest/gtest.h" -#include "xds/type/matcher/v3/matcher.pb.h" -#include "xds/type/matcher/v3/matcher.pb.validate.h" namespace Envoy { namespace Extensions { @@ -28,7 +14,6 @@ namespace Matcher { namespace { using ::Envoy::Matcher::ActionFactory; -using ::Envoy::Matcher::CustomMatcherFactory; using ::Envoy::Matcher::DataInputGetResult; using ::Envoy::Matcher::MatchTreeFactory; using ::Envoy::Matcher::MockMatchTreeValidationVisitor; @@ -37,51 +22,7 @@ using ::Envoy::Matcher::StringActionFactory; using ::Envoy::Matcher::TestData; using ::Envoy::Matcher::TestDataInputFactory; -class TrieMatcherTest : public ::testing::Test { -public: - TrieMatcherTest() - : inject_action_(action_factory_), inject_matcher_(trie_matcher_factory_), - factory_(context_, factory_context_, validation_visitor_) { - EXPECT_CALL(validation_visitor_, performDataInputValidation(_, _)).Times(testing::AnyNumber()); - } - - void loadConfig(const std::string& config) { - MessageUtil::loadFromYaml(config, matcher_, ProtobufMessage::getStrictValidationVisitor()); - TestUtility::validate(matcher_); - } - void validateMatch(const std::string& output) { - auto match_tree = factory_.create(matcher_); - const auto result = match_tree()->match(TestData()); - EXPECT_EQ(result.match_state_, MatchState::MatchComplete); - EXPECT_TRUE(result.on_match_.has_value()); - EXPECT_NE(result.on_match_->action_cb_, nullptr); - auto action = result.on_match_->action_cb_(); - const auto value = action->getTyped(); - EXPECT_EQ(value.string_, output); - } - void validateNoMatch() { - auto match_tree = factory_.create(matcher_); - const auto result = match_tree()->match(TestData()); - EXPECT_EQ(result.match_state_, MatchState::MatchComplete); - EXPECT_FALSE(result.on_match_.has_value()); - } - void validateUnableToMatch() { - auto match_tree = factory_.create(matcher_); - const auto result = match_tree()->match(TestData()); - EXPECT_EQ(result.match_state_, MatchState::UnableToMatch); - } - - StringActionFactory action_factory_; - Registry::InjectFactory> inject_action_; - TrieMatcherFactoryBase trie_matcher_factory_; - Registry::InjectFactory> inject_matcher_; - MockMatchTreeValidationVisitor validation_visitor_; - - absl::string_view context_ = ""; - NiceMock factory_context_; - MatchTreeFactory factory_; - xds::type::matcher::v3::Matcher matcher_; -}; +class TrieMatcherTest : public CustomMatcherTest> {}; TEST_F(TrieMatcherTest, TestMatcher) { const std::string yaml = R"EOF( diff --git a/tools/extensions/extensions_check.py b/tools/extensions/extensions_check.py index de667a4eb6b7a..b8e3c1eb0a0c0 100644 --- a/tools/extensions/extensions_check.py +++ b/tools/extensions/extensions_check.py @@ -62,7 +62,7 @@ "envoy.wasm.runtime", "envoy.common.key_value", "envoy.network.dns_resolver", "envoy.rbac.matchers", "envoy.access_loggers.extension_filters", "envoy.http.stateful_session", "envoy.matching.http.input", "envoy.matching.network.input", - "envoy.matching.network.custom_matchers") + "envoy.matching.network.custom_matchers", "envoy.matching.http.custom_matchers") EXTENSION_STATUS_VALUES = ( # This extension is stable and is expected to be production usable.