diff --git a/api/envoy/type/matcher/v3/http_inputs.proto b/api/envoy/type/matcher/v3/http_inputs.proto new file mode 100644 index 0000000000000..48eb4c43154a8 --- /dev/null +++ b/api/envoy/type/matcher/v3/http_inputs.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package envoy.type.matcher.v3; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type.matcher.v3"; +option java_outer_classname = "HttpInputsProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Common HTTP Inputs] + +// Match input indicates that matching should be done on a specific request header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the request contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpRequestHeaderMatchInput { + // The request header to match on. + string header_name = 1; +} + +// Match input indicating that matching should be done on a specific response header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the response contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpResponseHeaderMatchInput { + // The response header to match on. + string header_name = 1; +} diff --git a/api/envoy/type/matcher/v4alpha/http_inputs.proto b/api/envoy/type/matcher/v4alpha/http_inputs.proto new file mode 100644 index 0000000000000..f15e9ceb70e2f --- /dev/null +++ b/api/envoy/type/matcher/v4alpha/http_inputs.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package envoy.type.matcher.v4alpha; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type.matcher.v4alpha"; +option java_outer_classname = "HttpInputsProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: Common HTTP Inputs] + +// Match input indicates that matching should be done on a specific request header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the request contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpRequestHeaderMatchInput { + option (udpa.annotations.versioning).previous_message_type = + "envoy.type.matcher.v3.HttpRequestHeaderMatchInput"; + + // The request header to match on. + string header_name = 1; +} + +// Match input indicating that matching should be done on a specific response header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the response contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpResponseHeaderMatchInput { + option (udpa.annotations.versioning).previous_message_type = + "envoy.type.matcher.v3.HttpResponseHeaderMatchInput"; + + // The response header to match on. + string header_name = 1; +} diff --git a/docs/root/api-v3/types/types.rst b/docs/root/api-v3/types/types.rst index 3e6af53865bde..4321708bdb82a 100644 --- a/docs/root/api-v3/types/types.rst +++ b/docs/root/api-v3/types/types.rst @@ -21,5 +21,6 @@ Types ../type/matcher/v3/string.proto ../type/matcher/v3/struct.proto ../type/matcher/v3/value.proto + ../type/matcher/v3/http_inputs.proto ../type/metadata/v3/metadata.proto ../type/tracing/v3/custom_tag.proto diff --git a/generated_api_shadow/envoy/type/matcher/v3/http_inputs.proto b/generated_api_shadow/envoy/type/matcher/v3/http_inputs.proto new file mode 100644 index 0000000000000..48eb4c43154a8 --- /dev/null +++ b/generated_api_shadow/envoy/type/matcher/v3/http_inputs.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package envoy.type.matcher.v3; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type.matcher.v3"; +option java_outer_classname = "HttpInputsProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Common HTTP Inputs] + +// Match input indicates that matching should be done on a specific request header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the request contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpRequestHeaderMatchInput { + // The request header to match on. + string header_name = 1; +} + +// Match input indicating that matching should be done on a specific response header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the response contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpResponseHeaderMatchInput { + // The response header to match on. + string header_name = 1; +} diff --git a/generated_api_shadow/envoy/type/matcher/v4alpha/http_inputs.proto b/generated_api_shadow/envoy/type/matcher/v4alpha/http_inputs.proto new file mode 100644 index 0000000000000..f15e9ceb70e2f --- /dev/null +++ b/generated_api_shadow/envoy/type/matcher/v4alpha/http_inputs.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package envoy.type.matcher.v4alpha; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type.matcher.v4alpha"; +option java_outer_classname = "HttpInputsProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: Common HTTP Inputs] + +// Match input indicates that matching should be done on a specific request header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the request contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpRequestHeaderMatchInput { + option (udpa.annotations.versioning).previous_message_type = + "envoy.type.matcher.v3.HttpRequestHeaderMatchInput"; + + // The request header to match on. + string header_name = 1; +} + +// Match input indicating that matching should be done on a specific response header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the response contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpResponseHeaderMatchInput { + option (udpa.annotations.versioning).previous_message_type = + "envoy.type.matcher.v3.HttpResponseHeaderMatchInput"; + + // The response header to match on. + string header_name = 1; +} diff --git a/include/envoy/http/filter.h b/include/envoy/http/filter.h index 3fb1210bb110f..ecb39f22dcb0e 100644 --- a/include/envoy/http/filter.h +++ b/include/envoy/http/filter.h @@ -885,6 +885,8 @@ using StreamFilterSharedPtr = std::shared_ptr; class HttpMatchingData { public: + static absl::string_view name() { return "http"; } + virtual ~HttpMatchingData() = default; virtual RequestHeaderMapOptConstRef requestHeaders() const PURE; diff --git a/include/envoy/matcher/BUILD b/include/envoy/matcher/BUILD index 838e7fd5e0c1c..fadfc109afb0c 100644 --- a/include/envoy/matcher/BUILD +++ b/include/envoy/matcher/BUILD @@ -17,6 +17,7 @@ envoy_cc_library( ], deps = [ "//include/envoy/config:typed_config_interface", + "//include/envoy/protobuf:message_validator_interface", "@envoy_api//envoy/config/common/matcher/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], diff --git a/include/envoy/matcher/matcher.h b/include/envoy/matcher/matcher.h index 613274ce389a2..678eed64023bc 100644 --- a/include/envoy/matcher/matcher.h +++ b/include/envoy/matcher/matcher.h @@ -7,6 +7,7 @@ #include "envoy/config/common/matcher/v3/matcher.pb.h" #include "envoy/config/core/v3/extension.pb.h" #include "envoy/config/typed_config.h" +#include "envoy/protobuf/message_validator.h" #include "absl/strings/string_view.h" #include "absl/types/optional.h" @@ -209,7 +210,9 @@ template class DataInputFactory : public Config::TypedFactory { /** * Creates a DataInput from the provided config. */ - virtual DataInputPtr createDataInput(const Protobuf::Message& config) PURE; + virtual DataInputPtr + createDataInput(const Protobuf::Message& config, + ProtobufMessage::ValidationVisitor& validation_visitor) PURE; /** * The category of this factory depends on the DataType, so we require a name() function to exist diff --git a/source/common/http/BUILD b/source/common/http/BUILD index e19d26c058f47..a1917b42809c8 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -184,6 +184,8 @@ envoy_cc_library( "//source/common/local_reply:local_reply_lib", "//source/common/matcher:matcher_lib", "@envoy_api//envoy/extensions/filters/common/matcher/action/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + "@envoy_api//envoy/type/matcher/v3:pkg_cc_proto", ], ) @@ -247,6 +249,7 @@ envoy_cc_library( "//source/common/http/http2:codec_lib", "//source/common/http/http3:quic_codec_factory_lib", "//source/common/http/http3:well_known_names", + "//source/common/http/match_wrapper:config", "//source/common/network:utility_lib", "//source/common/router:config_lib", "//source/common/router:scoped_rds_lib", diff --git a/source/common/http/filter_manager.cc b/source/common/http/filter_manager.cc index 4a4e355da3f64..bde33e6d45207 100644 --- a/source/common/http/filter_manager.cc +++ b/source/common/http/filter_manager.cc @@ -15,6 +15,8 @@ namespace Envoy { namespace Http { namespace { +REGISTER_FACTORY(HttpRequestHeadersDataInputFactory, Matcher::DataInputFactory); +REGISTER_FACTORY(SkipActionFactory, Matcher::ActionFactory); template using FilterList = std::list>; diff --git a/source/common/http/filter_manager.h b/source/common/http/filter_manager.h index a7b1329bb78f1..f6e91fd4618c0 100644 --- a/source/common/http/filter_manager.h +++ b/source/common/http/filter_manager.h @@ -4,10 +4,14 @@ #include "envoy/common/optref.h" #include "envoy/extensions/filters/common/matcher/action/v3/skip_action.pb.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.validate.h" #include "envoy/http/filter.h" #include "envoy/http/header_map.h" #include "envoy/matcher/matcher.h" #include "envoy/network/socket.h" +#include "envoy/protobuf/message_validator.h" +#include "envoy/type/matcher/v3/http_inputs.pb.validate.h" #include "common/buffer/watermark_buffer.h" #include "common/common/dump_state_utils.h" @@ -18,6 +22,7 @@ #include "common/http/headers.h" #include "common/local_reply/local_reply.h" #include "common/matcher/matcher.h" +#include "common/protobuf/utility.h" namespace Envoy { namespace Http { @@ -97,6 +102,36 @@ class HttpRequestHeadersDataInput : public HttpHeadersDataInputBase +class HttpHeadersDataInputFactoryBase : public Matcher::DataInputFactory { +public: + explicit HttpHeadersDataInputFactoryBase(const std::string& name) : name_(name) {} + + std::string name() const override { return name_; } + + Matcher::DataInputPtr + createDataInput(const Protobuf::Message& config, + ProtobufMessage::ValidationVisitor& validation_visitor) override { + const auto& typed_config = + MessageUtil::downcastAndValidate(config, validation_visitor); + + return std::make_unique(typed_config.header_name()); + }; + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + +private: + const std::string name_; +}; + +class HttpRequestHeadersDataInputFactory + : public HttpHeadersDataInputFactoryBase< + HttpRequestHeadersDataInput, envoy::type::matcher::v3::HttpRequestHeaderMatchInput> { +public: + HttpRequestHeadersDataInputFactory() : HttpHeadersDataInputFactoryBase("request-headers") {} +}; + class HttpResponseHeadersDataInput : public HttpHeadersDataInputBase { public: explicit HttpResponseHeadersDataInput(const std::string& name) : HttpHeadersDataInputBase(name) {} @@ -107,9 +142,26 @@ class HttpResponseHeadersDataInput : public HttpHeadersDataInputBase { +public: + HttpResponseHeadersDataInputFactory() : HttpHeadersDataInputFactoryBase("response-headers") {} +}; + class SkipAction : public Matcher::ActionBase< envoy::extensions::filters::common::matcher::action::v3::SkipFilter> {}; +class SkipActionFactory : public Matcher::ActionFactory { +public: + std::string name() const override { return "skip"; } + Matcher::ActionFactoryCb createActionFactoryCb(const Protobuf::Message&) override { + return []() { return std::make_unique(); }; + } + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } +}; /** * Base class wrapper for both stream encoder and decoder filters. * diff --git a/source/common/http/match_wrapper/BUILD b/source/common/http/match_wrapper/BUILD new file mode 100644 index 0000000000000..1350d3db39e0b --- /dev/null +++ b/source/common/http/match_wrapper/BUILD @@ -0,0 +1,23 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_library( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + "//include/envoy/registry", + "//include/envoy/server:filter_config_interface", + "//source/common/matcher:matcher_lib", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:factory_base_lib", + "@envoy_api//envoy/extensions/common/matching/v3:pkg_cc_proto", + ], +) diff --git a/source/common/http/match_wrapper/config.cc b/source/common/http/match_wrapper/config.cc new file mode 100644 index 0000000000000..7d5b75f9f47ad --- /dev/null +++ b/source/common/http/match_wrapper/config.cc @@ -0,0 +1,90 @@ +#include "common/http/match_wrapper/config.h" + +#include "envoy/http/filter.h" +#include "envoy/matcher/matcher.h" +#include "envoy/registry/registry.h" + +#include "common/config/utility.h" +#include "common/matcher/matcher.h" + +namespace Envoy { +namespace Common { +namespace Http { +namespace MatchWrapper { + +namespace { +struct DelegatingFactoryCallbacks : public Envoy::Http::FilterChainFactoryCallbacks { + DelegatingFactoryCallbacks(Envoy::Http::FilterChainFactoryCallbacks& delegated_callbacks, + Matcher::MatchTreeSharedPtr match_tree) + : delegated_callbacks_(delegated_callbacks), match_tree_(std::move(match_tree)) {} + + void addStreamDecoderFilter(Envoy::Http::StreamDecoderFilterSharedPtr filter) override { + delegated_callbacks_.addStreamDecoderFilter(std::move(filter), match_tree_); + } + void addStreamDecoderFilter( + Envoy::Http::StreamDecoderFilterSharedPtr filter, + Matcher::MatchTreeSharedPtr match_tree) override { + delegated_callbacks_.addStreamDecoderFilter(std::move(filter), std::move(match_tree)); + } + void addStreamEncoderFilter(Envoy::Http::StreamEncoderFilterSharedPtr filter) override { + delegated_callbacks_.addStreamEncoderFilter(std::move(filter), match_tree_); + } + void addStreamEncoderFilter( + Envoy::Http::StreamEncoderFilterSharedPtr filter, + Matcher::MatchTreeSharedPtr match_tree) override { + delegated_callbacks_.addStreamEncoderFilter(std::move(filter), std::move(match_tree)); + } + void addStreamFilter(Envoy::Http::StreamFilterSharedPtr filter) override { + delegated_callbacks_.addStreamFilter(std::move(filter), match_tree_); + } + void + addStreamFilter(Envoy::Http::StreamFilterSharedPtr filter, + Matcher::MatchTreeSharedPtr match_tree) override { + delegated_callbacks_.addStreamFilter(std::move(filter), std::move(match_tree)); + } + void addAccessLogHandler(AccessLog::InstanceSharedPtr handler) override { + delegated_callbacks_.addAccessLogHandler(std::move(handler)); + } + + Envoy::Http::FilterChainFactoryCallbacks& delegated_callbacks_; + Matcher::MatchTreeSharedPtr match_tree_; +}; +} // namespace + +Envoy::Http::FilterFactoryCb MatchWrapperConfig::createFilterFactoryFromProtoTyped( + const envoy::extensions::common::matching::v3::ExtensionWithMatcher& proto_config, + const std::string& prefix, Server::Configuration::FactoryContext& context) { + + ASSERT(proto_config.has_extension_config()); + auto& factory = + Config::Utility::getAndCheckFactory( + proto_config.extension_config()); + + auto message = Config::Utility::translateAnyToFactoryConfig( + proto_config.extension_config().typed_config(), context.messageValidationVisitor(), factory); + auto filter_factory = factory.createFilterFactoryFromProto(*message, prefix, context); + + auto match_tree = + Matcher::MatchTreeFactory(context.messageValidationVisitor()) + .create(proto_config.matcher()); + + return [filter_factory, match_tree](Envoy::Http::FilterChainFactoryCallbacks& callbacks) -> void { + DelegatingFactoryCallbacks delegated_callbacks(callbacks, match_tree); + + return filter_factory(delegated_callbacks); + }; +} + +/** + * Static registration for the match wrapper filter. @see RegisterFactory. + * Note that we register this as a filter in order to serve as a drop in wrapper for other HTTP + * filters. While not a real filter, by being registered as one all the code paths that look up HTTP + * filters will look up this filter factory instead, which does the work to create and associate a + * match tree with the underlying filter. + */ +REGISTER_FACTORY(MatchWrapperConfig, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace MatchWrapper +} // namespace Http +} // namespace Common +} // namespace Envoy diff --git a/source/common/http/match_wrapper/config.h b/source/common/http/match_wrapper/config.h new file mode 100644 index 0000000000000..c1417aaa6f247 --- /dev/null +++ b/source/common/http/match_wrapper/config.h @@ -0,0 +1,28 @@ +#pragma once + +#include "envoy/extensions/common/matching/v3/extension_matcher.pb.validate.h" +#include "envoy/server/filter_config.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Common { +namespace Http { +namespace MatchWrapper { + +class MatchWrapperConfig : public Extensions::HttpFilters::Common::FactoryBase< + envoy::extensions::common::matching::v3::ExtensionWithMatcher> { +public: + MatchWrapperConfig() : FactoryBase("match-wrapper") {} + +private: + Envoy::Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::common::matching::v3::ExtensionWithMatcher& proto_config, + const std::string&, Server::Configuration::FactoryContext& context) override; +}; + +} // namespace MatchWrapper +} // namespace Http +} // namespace Common +} // namespace Envoy diff --git a/source/common/matcher/matcher.h b/source/common/matcher/matcher.h index 4e21e17ea463b..44ae0a26e787d 100644 --- a/source/common/matcher/matcher.h +++ b/source/common/matcher/matcher.h @@ -160,7 +160,7 @@ template class MatchTreeFactory { auto& factory = Config::Utility::getAndCheckFactory>(config); ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig( config.typed_config(), validation_visitor_, factory); - return factory.createDataInput(*message); + return factory.createDataInput(*message, validation_visitor_); } InputMatcherPtr createInputMatcher( diff --git a/test/common/http/match_wrapper/BUILD b/test/common/http/match_wrapper/BUILD new file mode 100644 index 0000000000000..52425ab72baa7 --- /dev/null +++ b/test/common/http/match_wrapper/BUILD @@ -0,0 +1,19 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + deps = [ + "//source/common/http/match_wrapper:config", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:registry_lib", + ], +) diff --git a/test/common/http/match_wrapper/config_test.cc b/test/common/http/match_wrapper/config_test.cc new file mode 100644 index 0000000000000..d29207ff3c6e3 --- /dev/null +++ b/test/common/http/match_wrapper/config_test.cc @@ -0,0 +1,91 @@ +#include "envoy/http/filter.h" +#include "envoy/server/factory_context.h" +#include "envoy/server/filter_config.h" + +#include "common/http/match_wrapper/config.h" + +#include "test/mocks/server/factory_context.h" +#include "test/test_common/registry.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Common { +namespace Http { +namespace MatchWrapper { +namespace { + +struct TestFactory : public Envoy::Server::Configuration::NamedHttpFilterConfigFactory { + std::string name() const override { return "test"; } + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + Envoy::Http::FilterFactoryCb + createFilterFactoryFromProto(const Protobuf::Message&, const std::string&, + Server::Configuration::FactoryContext&) override { + return [](auto& callbacks) { + callbacks.addStreamDecoderFilter(nullptr); + callbacks.addStreamEncoderFilter(nullptr); + callbacks.addStreamFilter(nullptr); + + callbacks.addStreamDecoderFilter(nullptr, nullptr); + callbacks.addStreamEncoderFilter(nullptr, nullptr); + callbacks.addStreamFilter(nullptr, nullptr); + + callbacks.addAccessLogHandler(nullptr); + }; + } +}; + +TEST(MatchWrapper, WithMatcher) { + TestFactory test_factory; + Envoy::Registry::InjectFactory + inject_factory(test_factory); + + NiceMock factory_context; + + const auto config = + TestUtility::parseYaml(R"EOF( +extension_config: + name: test + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue +matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: default-matcher-header + exact_match_map: + map: + match: + action: + name: skip + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.common.matcher.action.v3.SkipFilter +)EOF"); + + MatchWrapperConfig match_wrapper_config; + auto cb = match_wrapper_config.createFilterFactoryFromProto(config, "", factory_context); + + Envoy::Http::MockFilterChainFactoryCallbacks factory_callbacks; + testing::InSequence s; + + // This matches the sequence of calls in the filter factory above: the ones that call the overload + // without a match tree has a match tree added, the other one does not. + EXPECT_CALL(factory_callbacks, addStreamDecoderFilter(_, testing::NotNull())); + EXPECT_CALL(factory_callbacks, addStreamEncoderFilter(_, testing::NotNull())); + EXPECT_CALL(factory_callbacks, addStreamFilter(_, testing::NotNull())); + EXPECT_CALL(factory_callbacks, addStreamDecoderFilter(_, testing::IsNull())); + EXPECT_CALL(factory_callbacks, addStreamEncoderFilter(_, testing::IsNull())); + EXPECT_CALL(factory_callbacks, addStreamFilter(_, testing::IsNull())); + EXPECT_CALL(factory_callbacks, addAccessLogHandler(_)); + cb(factory_callbacks); +} + +} // namespace +} // namespace MatchWrapper +} // namespace Http +} // namespace Common +} // namespace Envoy \ No newline at end of file diff --git a/test/common/matcher/test_utility.h b/test/common/matcher/test_utility.h index 623d8158c55ff..1348d9cd80d4c 100644 --- a/test/common/matcher/test_utility.h +++ b/test/common/matcher/test_utility.h @@ -1,6 +1,7 @@ #pragma once #include "envoy/matcher/matcher.h" +#include "envoy/protobuf/message_validator.h" #include "common/matcher/matcher.h" @@ -29,7 +30,8 @@ class TestDataInputFactory : public DataInputFactory { TestDataInputFactory(absl::string_view factory_name, absl::string_view data) : factory_name_(std::string(factory_name)), value_(std::string(data)), injection_(*this) {} - DataInputPtr createDataInput(const Protobuf::Message&) override { + DataInputPtr createDataInput(const Protobuf::Message&, + ProtobufMessage::ValidationVisitor&) override { return std::make_unique( DataInputGetResult{DataInputGetResult::DataAvailability::AllDataAvailable, value_}); } diff --git a/test/integration/BUILD b/test/integration/BUILD index dc921e3a137ed..605d63e4f2120 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -877,6 +877,8 @@ envoy_cc_test( "//test/integration/filters:encoder_decoder_buffer_filter_lib", "//test/integration/filters:invalid_header_filter_lib", "//test/integration/filters:process_context_lib", + "//test/integration/filters:set_response_code_filter_config_proto_cc_proto", + "//test/integration/filters:set_response_code_filter_lib", "//test/integration/filters:stop_iteration_and_continue", "//test/mocks/http:http_mocks", "//test/test_common:utility_lib", @@ -1076,6 +1078,7 @@ envoy_cc_test( "//test/integration/filters:set_response_code_filter_config_proto_cc_proto", "//test/integration/filters:set_response_code_filter_lib", "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/common/matching/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", "@envoy_api//envoy/service/extension/v3:pkg_cc_proto", ], diff --git a/test/integration/extension_discovery_integration_test.cc b/test/integration/extension_discovery_integration_test.cc index b671e6f94bd98..28f49421dacd3 100644 --- a/test/integration/extension_discovery_integration_test.cc +++ b/test/integration/extension_discovery_integration_test.cc @@ -1,3 +1,4 @@ +#include "envoy/extensions/common/matching/v3/extension_matcher.pb.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" #include "envoy/service/extension/v3/config_discovery.pb.h" @@ -18,6 +19,32 @@ std::string denyPrivateConfig() { )EOF"; } +std::string denyPrivateConfigWithMatcher() { + return R"EOF( + "@type": type.googleapis.com/envoy.extensions.common.matching.v3.ExtensionWithMatcher + extension_config: + name: response-filter-config + typed_config: + "@type": type.googleapis.com/test.integration.filters.SetResponseCodeFilterConfig + prefix: "/private" + code: 403 + matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: some-header + exact_match_map: + map: + match: + action: + name: skip + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.common.matcher.action.v3.SkipFilter + )EOF"; +} + std::string allowAllConfig() { return "code: 200"; } std::string invalidConfig() { return "code: 90"; } @@ -29,9 +56,10 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, ipVersion()) {} void addDynamicFilter(const std::string& name, bool apply_without_warming, - bool set_default_config = true, bool rate_limit = false) { + bool set_default_config = true, bool rate_limit = false, + bool use_default_matcher = false) { config_helper_.addConfigModifier( - [this, name, apply_without_warming, set_default_config, rate_limit]( + [this, name, apply_without_warming, set_default_config, rate_limit, use_default_matcher]( envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& http_connection_manager) { auto* filter = http_connection_manager.mutable_http_filters()->Add(); @@ -39,11 +67,41 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara auto* discovery = filter->mutable_config_discovery(); discovery->add_type_urls( "type.googleapis.com/test.integration.filters.SetResponseCodeFilterConfig"); + discovery->add_type_urls( + "type.googleapis.com/envoy.extensions.common.matching.v3.ExtensionWithMatcher"); if (set_default_config) { - const auto default_configuration = - TestUtility::parseYaml( - "code: 403"); - discovery->mutable_default_config()->PackFrom(default_configuration); + if (use_default_matcher) { + const auto default_configuration = TestUtility::parseYaml< + envoy::extensions::common::matching::v3::ExtensionWithMatcher>( + R"EOF( + extension_config: + name: set-response-code + typed_config: + "@type": type.googleapis.com/test.integration.filters.SetResponseCodeFilterConfig + code: 403 + matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: default-matcher-header + exact_match_map: + map: + match: + action: + name: skip + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.common.matcher.action.v3.SkipFilter + )EOF"); + + discovery->mutable_default_config()->PackFrom(default_configuration); + } else { + const auto default_configuration = + TestUtility::parseYaml( + "code: 403"); + discovery->mutable_default_config()->PackFrom(default_configuration); + } } discovery->set_apply_default_config_without_warming(apply_without_warming); discovery->mutable_config_source()->set_resource_api_version( @@ -125,6 +183,19 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara ecds_stream_->sendGrpcMessage(response); } + void sendXdsResponseWithFullYaml(const std::string& name, const std::string& version, + const std::string& full_yaml) { + envoy::service::discovery::v3::DiscoveryResponse response; + response.set_version_info(version); + response.set_type_url("type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig"); + const auto configuration = TestUtility::parseYaml(full_yaml); + envoy::config::core::v3::TypedExtensionConfig typed_config; + typed_config.set_name(name); + typed_config.mutable_typed_config()->MergeFrom(configuration); + response.add_resources()->PackFrom(typed_config); + ecds_stream_->sendGrpcMessage(response); + } + FakeUpstream& getEcdsFakeUpstream() const { return *fake_upstreams_[1]; } FakeHttpConnectionPtr ecds_connection_{nullptr}; @@ -176,6 +247,81 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccess) { } } +TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccessWithMatcher) { + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("foo", false); + initialize(); + test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); + registerTestServerPorts({"http"}); + sendXdsResponseWithFullYaml("foo", "1", denyPrivateConfigWithMatcher()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.foo.config_reload", + 1); + test_server_->waitUntilListenersReady(); + test_server_->waitForGaugeGe("listener_manager.workers_started", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initialized); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + } + Http::TestRequestHeaderMapImpl banned_request_headers{ + {":method", "GET"}, {":path", "/private/key"}, {":scheme", "http"}, {":authority", "host"}}; + { + auto response = codec_client_->makeHeaderOnlyRequest(banned_request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("403", response->headers().getStatusValue()); + } + Http::TestRequestHeaderMapImpl banned_request_headers_skipped{{":method", "GET"}, + {":path", "/private/key"}, + {"some-header", "match"}, + {":scheme", "http"}, + {":authority", "host"}}; + { + auto response = codec_client_->makeHeaderOnlyRequest(banned_request_headers_skipped); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + } +} + +TEST_P(ExtensionDiscoveryIntegrationTest, BasicDefaultMatcher) { + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("foo", false, true, false, true); + initialize(); + test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); + registerTestServerPorts({"http"}); + sendXdsResponse("foo", "1", invalidConfig()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.foo.config_fail", 1); + test_server_->waitUntilListenersReady(); + test_server_->waitForGaugeGe("listener_manager.workers_started", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initialized); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("403", response->headers().getStatusValue()); + } + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {"default-matcher-header", "match"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); +} + TEST_P(ExtensionDiscoveryIntegrationTest, BasicFailWithDefault) { on_server_init_function_ = [&]() { waitXdsStream(); }; addDynamicFilter("foo", false); diff --git a/test/integration/integration_test.cc b/test/integration/integration_test.cc index f662744274f94..dc6e3f0b214e2 100644 --- a/test/integration/integration_test.cc +++ b/test/integration/integration_test.cc @@ -364,6 +364,58 @@ TEST_P(IntegrationTest, EnvoyProxying100ContinueWithDecodeDataPause) { testEnvoyProxying1xx(true); } +// Verifies that we can construct a match tree with a filter, and that we are able to skip +// filter invocation through the match tree. +TEST_P(IntegrationTest, MatchingHttpFilterConstruction) { + config_helper_.addFilter(R"EOF( +name: matcher +typed_config: + "@type": type.googleapis.com/envoy.extensions.common.matching.v3.ExtensionWithMatcher + extension_config: + name: set-response-code + typed_config: + "@type": type.googleapis.com/test.integration.filters.SetResponseCodeFilterConfig + code: 403 + matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: match-header + exact_match_map: + map: + match: + action: + name: skip + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.common.matcher.action.v3.SkipFilter +)EOF"); + + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + { + auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); + response->waitForEndStream(); + EXPECT_THAT(response->headers(), HttpStatusIs("403")); + } + + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "POST"}, {":path", "/test/long/url"}, {":scheme", "http"}, + {":authority", "host"}, {"match-header", "match"}, {"content-type", "application/grpc"}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 1024); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, true); + + response->waitForEndStream(); + EXPECT_THAT(response->headers(), HttpStatusIs("200")); + + codec_client_->close(); +} + // This is a regression for https://github.com/envoyproxy/envoy/issues/2715 and validates that a // pending request is not sent on a connection that has been half-closed. TEST_P(IntegrationTest, UpstreamDisconnectWithTwoRequests) {