From d448b848c0db5498ad0abec687eebdc3b0b6b900 Mon Sep 17 00:00:00 2001 From: code Date: Thu, 9 Mar 2023 13:30:27 +0800 Subject: [PATCH] http: added a new dual header mutation filter that could be used as downstream/upstream filter (#25658) Check #24100 for more detailed context. Risk Level: low. new L7 extension. Testing: unit. Docs Changes: added. Release Notes: added. Signed-off-by: wbpcode --- CODEOWNERS | 2 + api/BUILD | 1 + .../filters/http/header_mutation/v3/BUILD | 12 + .../header_mutation/v3/header_mutation.proto | 36 +++ api/versioning/BUILD | 1 + changelogs/current.yaml | 3 + .../http_filters/header_mutation_filter.rst | 25 ++ .../http/http_filters/http_filters.rst | 1 + source/common/http/BUILD | 13 + source/common/http/header_mutation.cc | 91 ++++++ source/common/http/header_mutation.h | 29 ++ source/extensions/extensions_build_config.bzl | 1 + source/extensions/extensions_metadata.yaml | 9 + .../filters/http/header_mutation/BUILD | 38 +++ .../filters/http/header_mutation/config.cc | 37 +++ .../filters/http/header_mutation/config.h | 36 +++ .../http/header_mutation/header_mutation.cc | 79 +++++ .../http/header_mutation/header_mutation.h | 83 +++++ .../header_mutation/BUILD | 2 +- .../header_mutation/header_mutation.cc | 78 +---- .../header_mutation/header_mutation.h | 17 +- test/common/http/BUILD | 10 + test/common/http/header_mutation_test.cc | 297 ++++++++++++++++++ .../filters/http/header_mutation/BUILD | 54 ++++ .../http/header_mutation/config_test.cc | 76 +++++ .../header_mutation_integration_test.cc | 170 ++++++++++ .../header_mutation/header_mutation_test.cc | 217 +++++++++++++ .../header_mutation/header_mutation_test.cc | 156 +++------ 28 files changed, 1369 insertions(+), 205 deletions(-) create mode 100644 api/envoy/extensions/filters/http/header_mutation/v3/BUILD create mode 100644 api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto create mode 100644 docs/root/configuration/http/http_filters/header_mutation_filter.rst create mode 100644 source/common/http/header_mutation.cc create mode 100644 source/common/http/header_mutation.h create mode 100644 source/extensions/filters/http/header_mutation/BUILD create mode 100644 source/extensions/filters/http/header_mutation/config.cc create mode 100644 source/extensions/filters/http/header_mutation/config.h create mode 100644 source/extensions/filters/http/header_mutation/header_mutation.cc create mode 100644 source/extensions/filters/http/header_mutation/header_mutation.h create mode 100644 test/common/http/header_mutation_test.cc create mode 100644 test/extensions/filters/http/header_mutation/BUILD create mode 100644 test/extensions/filters/http/header_mutation/config_test.cc create mode 100644 test/extensions/filters/http/header_mutation/header_mutation_integration_test.cc create mode 100644 test/extensions/filters/http/header_mutation/header_mutation_test.cc diff --git a/CODEOWNERS b/CODEOWNERS index e01084adfbbd..b46e3e08d7f0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -295,6 +295,8 @@ extensions/filters/http/oauth2 @derekargueta @snowp /*/extensions/load_balancing_policies/maglev @wbpcode @UNOWNED # Early header mutation /*/extensions/http/early_header_mutation/header_mutation @wbpcode @UNOWNED +# Header mutation +/*/extensions/filters/http/header_mutation @wbpcode @htuch @soulxu # Intentionally exempt (treated as core code) /*/extensions/filters/common @UNOWNED @UNOWNED diff --git a/api/BUILD b/api/BUILD index c1bde726ce56..8786b5d735f4 100644 --- a/api/BUILD +++ b/api/BUILD @@ -180,6 +180,7 @@ proto_library( "//envoy/extensions/filters/http/grpc_stats/v3:pkg", "//envoy/extensions/filters/http/grpc_web/v3:pkg", "//envoy/extensions/filters/http/gzip/v3:pkg", + "//envoy/extensions/filters/http/header_mutation/v3:pkg", "//envoy/extensions/filters/http/header_to_metadata/v3:pkg", "//envoy/extensions/filters/http/health_check/v3:pkg", "//envoy/extensions/filters/http/ip_tagging/v3:pkg", diff --git a/api/envoy/extensions/filters/http/header_mutation/v3/BUILD b/api/envoy/extensions/filters/http/header_mutation/v3/BUILD new file mode 100644 index 000000000000..7af7ae042311 --- /dev/null +++ b/api/envoy/extensions/filters/http/header_mutation/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/common/mutation_rules/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto b/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto new file mode 100644 index 000000000000..d7b5cbc5797a --- /dev/null +++ b/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.header_mutation.v3; + +import "envoy/config/common/mutation_rules/v3/mutation_rules.proto"; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.header_mutation.v3"; +option java_outer_classname = "HeaderMutationProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/header_mutation/v3;header_mutationv3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Header mutation filter configuration] +// [#extension: envoy.filters.http.header_mutation] + +message Mutations { + // The request mutations are applied before the request is forwarded to the upstream cluster. + repeated config.common.mutation_rules.v3.HeaderMutation request_mutations = 1; + + // The response mutations are applied before the response is sent to the downstream client. + repeated config.common.mutation_rules.v3.HeaderMutation response_mutations = 2; +} + +// Per route configuration for the header mutation filter. If this is configured at multiple levels +// (route level, virtual host level, and route table level), only the most specific one will be used. +message HeaderMutationPerRoute { + Mutations mutations = 1; +} + +// Configuration for the header mutation filter. The mutation rules in the filter configuration will +// always be applied first and then the per-route mutation rules, if both are specified. +message HeaderMutation { + Mutations mutations = 1; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 7412d028dc17..309506ce5dfe 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -118,6 +118,7 @@ proto_library( "//envoy/extensions/filters/http/grpc_stats/v3:pkg", "//envoy/extensions/filters/http/grpc_web/v3:pkg", "//envoy/extensions/filters/http/gzip/v3:pkg", + "//envoy/extensions/filters/http/header_mutation/v3:pkg", "//envoy/extensions/filters/http/header_to_metadata/v3:pkg", "//envoy/extensions/filters/http/health_check/v3:pkg", "//envoy/extensions/filters/http/ip_tagging/v3:pkg", diff --git a/changelogs/current.yaml b/changelogs/current.yaml index a6571bb21b3a..a313809108d4 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -157,6 +157,9 @@ new_features: change: | added :ref:`failed_status_in_metadata ` to support setting the JWT authentication failure status code and message in dynamic metadata. +- area: http filter + change: | + added :ref:`header mutation http filter ` which adds the ability to modify request and response headers in any position of HTTP filter chain. - area: matching change: | added :ref:`Filter State Input ` for matching based on filter state objects. diff --git a/docs/root/configuration/http/http_filters/header_mutation_filter.rst b/docs/root/configuration/http/http_filters/header_mutation_filter.rst new file mode 100644 index 000000000000..215a555096de --- /dev/null +++ b/docs/root/configuration/http/http_filters/header_mutation_filter.rst @@ -0,0 +1,25 @@ +.. _config_http_filters_header_mutation: + +Header Mutation +=============== + +* This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation``. +* :ref:`v3 API reference ` + +This is a filter that can be used to add, remove, append, or update HTTP headers. It can be added in any position in the filter chain +and used as downstream or upstream HTTP filter. The filter can be configured to apply the header mutations to the request, response, or both. + + +In most cases, this filter would be a more flexible alternative to the ``request_headers_to_add``, ``request_headers_to_remove``, +``response_headers_to_add``, and ``response_headers_to_remove`` fields in the :ref:`route configuration `. + + +The filter provides complete control over the position and order of the header mutations. It may be used to influence later route picks if +the route cache is cleared by a filter executing after the header mutation filter. + + +In addition, this filter can be used as upstream filter and mutate the request headers after load balancing and host selection. + + +Please note that as an encoder filter, this filter follows the standard rules of when it will execute in situations such as local replies - response +headers will not be unconditionally added in cases where the filter would be bypassed. diff --git a/docs/root/configuration/http/http_filters/http_filters.rst b/docs/root/configuration/http/http_filters/http_filters.rst index f0b6f4479ec2..5c7a6f27cfd8 100644 --- a/docs/root/configuration/http/http_filters/http_filters.rst +++ b/docs/root/configuration/http/http_filters/http_filters.rst @@ -34,6 +34,7 @@ HTTP filters grpc_json_transcoder_filter grpc_stats_filter grpc_web_filter + header_mutation_filter health_check_filter header_to_metadata_filter ip_tagging_filter diff --git a/source/common/http/BUILD b/source/common/http/BUILD index e180bdc7ed9d..540aaac04ec2 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -576,3 +576,16 @@ envoy_cc_library( "//source/common/common:assert_lib", ], ) + +envoy_cc_library( + name = "header_mutation_lib", + srcs = ["header_mutation.cc"], + hdrs = ["header_mutation.h"], + deps = [ + ":header_map_lib", + ":utility_lib", + "//envoy/http:header_evaluator", + "//source/common/router:header_parser_lib", + "@envoy_api//envoy/config/common/mutation_rules/v3:pkg_cc_proto", + ], +) diff --git a/source/common/http/header_mutation.cc b/source/common/http/header_mutation.cc new file mode 100644 index 000000000000..ba74469037fe --- /dev/null +++ b/source/common/http/header_mutation.cc @@ -0,0 +1,91 @@ +#include "source/common/http/header_mutation.h" + +#include "source/common/router/header_parser.h" + +namespace Envoy { +namespace Http { + +namespace { + +using HeaderAppendAction = envoy::config::core::v3::HeaderValueOption::HeaderAppendAction; +using HeaderValueOption = envoy::config::core::v3::HeaderValueOption; + +// TODO(wbpcode): Inherit from Envoy::Router::HeadersToAddEntry to make sure the formatter +// has the same behavior as the router's formatter. We should try to find a more clean way +// to reuse the formatter after the router's formatter is completely removed. +class AppendMutation : public HeaderEvaluator, public Envoy::Router::HeadersToAddEntry { +public: + AppendMutation(const HeaderValueOption& header_value_option) + : HeadersToAddEntry(header_value_option), header_name_(header_value_option.header().key()) {} + + void evaluateHeaders(Http::HeaderMap& headers, const Http::RequestHeaderMap& request_headers, + const Http::ResponseHeaderMap& response_headers, + const StreamInfo::StreamInfo& stream_info) const override { + std::string value = formatter_->format(request_headers, response_headers, stream_info); + + if (!value.empty() || add_if_empty_) { + switch (append_action_) { + PANIC_ON_PROTO_ENUM_SENTINEL_VALUES; + case HeaderValueOption::APPEND_IF_EXISTS_OR_ADD: + headers.addReferenceKey(header_name_, value); + return; + case HeaderValueOption::ADD_IF_ABSENT: { + auto header = headers.get(header_name_); + if (!header.empty()) { + return; + } + headers.addReferenceKey(header_name_, value); + break; + } + case HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD: + headers.setReferenceKey(header_name_, value); + break; + } + } + } + +private: + Envoy::Http::LowerCaseString header_name_; +}; + +class RemoveMutation : public HeaderEvaluator { +public: + RemoveMutation(const std::string& header_name) : header_name_(header_name) {} + + void evaluateHeaders(Http::HeaderMap& headers, const Http::RequestHeaderMap&, + const Http::ResponseHeaderMap&, + const StreamInfo::StreamInfo&) const override { + headers.remove(header_name_); + } + +private: + const Envoy::Http::LowerCaseString header_name_; +}; +} // namespace + +HeaderMutations::HeaderMutations(const ProtoHeaderMutatons& header_mutations) { + for (const auto& mutation : header_mutations) { + switch (mutation.action_case()) { + case envoy::config::common::mutation_rules::v3::HeaderMutation::ActionCase::kAppend: + header_mutations_.emplace_back(std::make_unique(mutation.append())); + break; + case envoy::config::common::mutation_rules::v3::HeaderMutation::ActionCase::kRemove: + header_mutations_.emplace_back(std::make_unique(mutation.remove())); + break; + default: + PANIC_DUE_TO_PROTO_UNSET; + } + } +} + +void HeaderMutations::evaluateHeaders(Http::HeaderMap& headers, + const Http::RequestHeaderMap& request_headers, + const Http::ResponseHeaderMap& response_headers, + const StreamInfo::StreamInfo& stream_info) const { + for (const auto& mutation : header_mutations_) { + mutation->evaluateHeaders(headers, request_headers, response_headers, stream_info); + } +} + +} // namespace Http +} // namespace Envoy diff --git a/source/common/http/header_mutation.h b/source/common/http/header_mutation.h new file mode 100644 index 000000000000..f1cad1e08c47 --- /dev/null +++ b/source/common/http/header_mutation.h @@ -0,0 +1,29 @@ +#pragma once + +#include "envoy/config/common/mutation_rules/v3/mutation_rules.pb.h" +#include "envoy/http/header_evaluator.h" + +#include "source/common/protobuf/protobuf.h" + +namespace Envoy { +namespace Http { + +using ProtoHeaderMutatons = + Protobuf::RepeatedPtrField; +using ProtoHeaderValueOption = envoy::config::core::v3::HeaderValueOption; + +class HeaderMutations : public HeaderEvaluator { +public: + HeaderMutations(const ProtoHeaderMutatons& header_mutations); + + // Http::HeaderEvaluator + void evaluateHeaders(Http::HeaderMap& headers, const Http::RequestHeaderMap& request_headers, + const Http::ResponseHeaderMap& response_headers, + const StreamInfo::StreamInfo& stream_info) const override; + +private: + std::vector> header_mutations_; +}; + +} // namespace Http +} // namespace Envoy diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index ce472788ce03..2a3cdc9d9c8f 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -132,6 +132,7 @@ EXTENSIONS = { "envoy.filters.http.tap": "//source/extensions/filters/http/tap:config", "envoy.filters.http.wasm": "//source/extensions/filters/http/wasm:config", "envoy.filters.http.stateful_session": "//source/extensions/filters/http/stateful_session:config", + "envoy.filters.http.header_mutation": "//source/extensions/filters/http/header_mutation:config", # # Listener filters diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index e5f245ce745d..9e883d6bcf99 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -504,6 +504,15 @@ envoy.filters.http.stateful_session: type_urls: - envoy.extensions.filters.http.stateful_session.v3.StatefulSession - envoy.extensions.filters.http.stateful_session.v3.StatefulSessionPerRoute +envoy.filters.http.header_mutation: + categories: + - envoy.filters.http + - envoy.filters.http.upstream + security_posture: unknown + status: alpha + type_urls: + - envoy.extensions.filters.http.header_mutation.v3.HeaderMutation + - envoy.extensions.filters.http.header_mutation.v3.HeaderMutationPerRoute envoy.filters.listener.http_inspector: categories: - envoy.filters.listener diff --git a/source/extensions/filters/http/header_mutation/BUILD b/source/extensions/filters/http/header_mutation/BUILD new file mode 100644 index 000000000000..48f12a052502 --- /dev/null +++ b/source/extensions/filters/http/header_mutation/BUILD @@ -0,0 +1,38 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "header_mutation_lib", + srcs = ["header_mutation.cc"], + hdrs = ["header_mutation.h"], + deps = [ + "//envoy/server:filter_config_interface", + "//source/common/config:utility_lib", + "//source/common/http:header_map_lib", + "//source/common/http:header_mutation_lib", + "//source/common/protobuf:utility_lib", + "//source/extensions/filters/http/common:pass_through_filter_lib", + "@envoy_api//envoy/extensions/filters/http/header_mutation/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":header_mutation_lib", + "//envoy/registry", + "//source/common/protobuf:utility_lib", + "//source/extensions/filters/http/common:factory_base_lib", + "@envoy_api//envoy/extensions/filters/http/header_mutation/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/header_mutation/config.cc b/source/extensions/filters/http/header_mutation/config.cc new file mode 100644 index 000000000000..9705e149c747 --- /dev/null +++ b/source/extensions/filters/http/header_mutation/config.cc @@ -0,0 +1,37 @@ +#include "source/extensions/filters/http/header_mutation/config.h" + +#include + +#include "envoy/registry/registry.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace HeaderMutation { + +Http::FilterFactoryCb HeaderMutationFactoryConfig::createFilterFactoryFromProtoTyped( + const ProtoConfig& config, const std::string&, DualInfo, + Server::Configuration::ServerFactoryContext&) { + auto filter_config = std::make_shared(config); + return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter(std::make_shared(filter_config)); + }; +} + +Router::RouteSpecificFilterConfigConstSharedPtr +HeaderMutationFactoryConfig::createRouteSpecificFilterConfigTyped( + const PerRouteProtoConfig& proto_config, Server::Configuration::ServerFactoryContext&, + ProtobufMessage::ValidationVisitor&) { + return std::make_shared(proto_config); +} + +using UpstreamHeaderMutationFactoryConfig = HeaderMutationFactoryConfig; + +REGISTER_FACTORY(HeaderMutationFactoryConfig, Server::Configuration::NamedHttpFilterConfigFactory); +REGISTER_FACTORY(UpstreamHeaderMutationFactoryConfig, + Server::Configuration::UpstreamHttpFilterConfigFactory); + +} // namespace HeaderMutation +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/header_mutation/config.h b/source/extensions/filters/http/header_mutation/config.h new file mode 100644 index 000000000000..d7348241c703 --- /dev/null +++ b/source/extensions/filters/http/header_mutation/config.h @@ -0,0 +1,36 @@ +#pragma once + +#include "envoy/extensions/filters/http/header_mutation/v3/header_mutation.pb.h" +#include "envoy/extensions/filters/http/header_mutation/v3/header_mutation.pb.validate.h" + +#include "source/extensions/filters/http/common/factory_base.h" +#include "source/extensions/filters/http/header_mutation/header_mutation.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace HeaderMutation { + +/** + * Config registration for the stateful session filter. @see NamedHttpFilterConfigFactory. + */ +class HeaderMutationFactoryConfig + : public Common::DualFactoryBase { +public: + HeaderMutationFactoryConfig() : DualFactoryBase("envoy.filters.http.header_mutation") {} + +private: + Http::FilterFactoryCb + createFilterFactoryFromProtoTyped(const ProtoConfig& proto_config, + const std::string& stats_prefix, DualInfo info, + Server::Configuration::ServerFactoryContext& context) override; + Router::RouteSpecificFilterConfigConstSharedPtr + createRouteSpecificFilterConfigTyped(const PerRouteProtoConfig& proto_config, + Server::Configuration::ServerFactoryContext&, + ProtobufMessage::ValidationVisitor&) override; +}; + +} // namespace HeaderMutation +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/header_mutation/header_mutation.cc b/source/extensions/filters/http/header_mutation/header_mutation.cc new file mode 100644 index 000000000000..1cb26682e2e6 --- /dev/null +++ b/source/extensions/filters/http/header_mutation/header_mutation.cc @@ -0,0 +1,79 @@ +#include "source/extensions/filters/http/header_mutation/header_mutation.h" + +#include +#include + +#include "source/common/config/utility.h" +#include "source/common/http/header_map_impl.h" +#include "source/common/http/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace HeaderMutation { + +void Mutations::mutateRequestHeaders(Http::RequestHeaderMap& request_headers, + const StreamInfo::StreamInfo& stream_info) const { + request_mutations_.evaluateHeaders(request_headers, request_headers, + *Http::StaticEmptyHeaders::get().response_headers, + stream_info); +} + +void Mutations::mutateResponseHeaders(const Http::RequestHeaderMap& request_headers, + Http::ResponseHeaderMap& response_headers, + const StreamInfo::StreamInfo& stream_info) const { + response_mutations_.evaluateHeaders(response_headers, request_headers, response_headers, + stream_info); +} + +PerRouteHeaderMutation::PerRouteHeaderMutation(const PerRouteProtoConfig& config) + : mutations_(config.mutations()) {} + +HeaderMutationConfig::HeaderMutationConfig(const ProtoConfig& config) + : mutations_(config.mutations()) {} + +Http::FilterHeadersStatus HeaderMutation::decodeHeaders(Http::RequestHeaderMap& headers, bool) { + config_->mutations().mutateRequestHeaders(headers, decoder_callbacks_->streamInfo()); + + // Only the most specific route config is used. + // TODO(wbpcode): It's possible to traverse all the route configs to merge the header mutations + // in the future. + route_config_ = + Http::Utility::resolveMostSpecificPerFilterConfig(decoder_callbacks_); + + if (route_config_ != nullptr) { + route_config_->mutations().mutateRequestHeaders(headers, decoder_callbacks_->streamInfo()); + } + + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterHeadersStatus HeaderMutation::encodeHeaders(Http::ResponseHeaderMap& headers, bool) { + // The request headers will be set to the downstream stream info before the filter chain is + // started. And in the current implementation, the upstream filter chain will reuse the downstream + // stream info. So the getRequestHeaders() will never return nullptr no matter the filter is + // is used as a downstream or upstream filter. + ASSERT(encoder_callbacks_->streamInfo().getRequestHeaders() != nullptr); + const auto& request_headers = *encoder_callbacks_->streamInfo().getRequestHeaders(); + + config_->mutations().mutateResponseHeaders(request_headers, headers, + encoder_callbacks_->streamInfo()); + + if (route_config_ == nullptr) { + // If we haven't already resolved the route config, do so now. + route_config_ = Http::Utility::resolveMostSpecificPerFilterConfig( + encoder_callbacks_); + } + + if (route_config_ != nullptr) { + route_config_->mutations().mutateResponseHeaders(request_headers, headers, + encoder_callbacks_->streamInfo()); + } + + return Http::FilterHeadersStatus::Continue; +} + +} // namespace HeaderMutation +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/header_mutation/header_mutation.h b/source/extensions/filters/http/header_mutation/header_mutation.h new file mode 100644 index 000000000000..4bb2c23eca41 --- /dev/null +++ b/source/extensions/filters/http/header_mutation/header_mutation.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/extensions/filters/http/header_mutation/v3/header_mutation.pb.h" + +#include "source/common/common/logger.h" +#include "source/common/http/header_mutation.h" +#include "source/extensions/filters/http/common/pass_through_filter.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace HeaderMutation { + +using ProtoConfig = envoy::extensions::filters::http::header_mutation::v3::HeaderMutation; +using PerRouteProtoConfig = + envoy::extensions::filters::http::header_mutation::v3::HeaderMutationPerRoute; +using MutationsProto = envoy::extensions::filters::http::header_mutation::v3::Mutations; + +class Mutations { +public: + Mutations(const MutationsProto& config) + : request_mutations_(config.request_mutations()), + response_mutations_(config.response_mutations()) {} + + void mutateRequestHeaders(Http::RequestHeaderMap& request_headers, + const StreamInfo::StreamInfo& stream_info) const; + void mutateResponseHeaders(const Http::RequestHeaderMap& request_headers, + Http::ResponseHeaderMap& response_headers, + const StreamInfo::StreamInfo& stream_info) const; + +private: + Http::HeaderMutations request_mutations_; + Http::HeaderMutations response_mutations_; +}; + +class PerRouteHeaderMutation : public Router::RouteSpecificFilterConfig { +public: + PerRouteHeaderMutation(const PerRouteProtoConfig& config); + + const Mutations& mutations() const { return mutations_; } + +private: + Mutations mutations_; +}; +using PerRouteHeaderMutationSharedPtr = std::shared_ptr; + +class HeaderMutationConfig { +public: + HeaderMutationConfig(const ProtoConfig& config); + + const Mutations& mutations() const { return mutations_; } + +private: + Mutations mutations_; +}; +using HeaderMutationConfigSharedPtr = std::shared_ptr; + +class HeaderMutation : public Http::PassThroughFilter, public Logger::Loggable { +public: + HeaderMutation(HeaderMutationConfigSharedPtr config) : config_(std::move(config)) {} + + // Http::StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, bool) override; + + // Http::StreamEncoderFilter + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, bool) override; + +private: + HeaderMutationConfigSharedPtr config_{}; + const PerRouteHeaderMutation* route_config_{}; +}; + +} // namespace HeaderMutation +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/http/early_header_mutation/header_mutation/BUILD b/source/extensions/http/early_header_mutation/header_mutation/BUILD index 774fffd7b4f0..3cb66447d187 100644 --- a/source/extensions/http/early_header_mutation/header_mutation/BUILD +++ b/source/extensions/http/early_header_mutation/header_mutation/BUILD @@ -15,7 +15,7 @@ envoy_cc_library( hdrs = ["header_mutation.h"], deps = [ "//envoy/http:early_header_mutation_interface", - "//source/common/router:header_parser_lib", + "//source/common/http:header_mutation_lib", "@envoy_api//envoy/config/common/mutation_rules/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/http/early_header_mutation/header_mutation/v3:pkg_cc_proto", ], diff --git a/source/extensions/http/early_header_mutation/header_mutation/header_mutation.cc b/source/extensions/http/early_header_mutation/header_mutation/header_mutation.cc index c415fa4d5311..082dfbc51a2e 100644 --- a/source/extensions/http/early_header_mutation/header_mutation/header_mutation.cc +++ b/source/extensions/http/early_header_mutation/header_mutation/header_mutation.cc @@ -2,87 +2,21 @@ #include "envoy/config/common/mutation_rules/v3/mutation_rules.pb.h" +#include "source/common/http/header_map_impl.h" + namespace Envoy { namespace Extensions { namespace Http { namespace EarlyHeaderMutation { namespace HeaderMutation { -namespace { - -// TODO(wbpcode): Inherit from Envoy::Router::HeadersToAddEntry to make sure the formatter -// has the same behavior as the router's formatter. We should try to find a more clean way -// to reuse the formatter after the router's formatter is completely removed. -class AppendMutation : public Mutation, public Envoy::Router::HeadersToAddEntry { -public: - AppendMutation(const HeaderValueOption& header_value_option) - : HeadersToAddEntry(header_value_option), header_name_(header_value_option.header().key()) {} - - void mutate(Envoy::Http::RequestHeaderMap& headers, - const StreamInfo::StreamInfo& stream_info) const override { - std::string value = formatter_->format( - headers, *Envoy::Http::StaticEmptyHeaders::get().response_headers, stream_info); - - if (!value.empty() || add_if_empty_) { - switch (append_action_) { - PANIC_ON_PROTO_ENUM_SENTINEL_VALUES; - case HeaderValueOption::APPEND_IF_EXISTS_OR_ADD: - headers.addReferenceKey(header_name_, value); - return; - case HeaderValueOption::ADD_IF_ABSENT: { - auto header = headers.get(header_name_); - if (!header.empty()) { - return; - } - headers.addReferenceKey(header_name_, value); - break; - } - case HeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD: - headers.setReferenceKey(header_name_, value); - break; - } - } - } - -private: - Envoy::Http::LowerCaseString header_name_; -}; - -class RemoveMutation : public Mutation { -public: - RemoveMutation(const std::string& header_name) : header_name_(header_name) {} - - void mutate(Envoy::Http::RequestHeaderMap& headers, - const StreamInfo::StreamInfo&) const override { - headers.remove(header_name_); - } - -private: - const Envoy::Http::LowerCaseString header_name_; -}; - -} // namespace - -HeaderMutation::HeaderMutation(const ProtoHeaderMutation& mutations) { - for (const auto& mutation : mutations.mutations()) { - switch (mutation.action_case()) { - case envoy::config::common::mutation_rules::v3::HeaderMutation::ActionCase::kAppend: - mutations_.emplace_back(std::make_unique(mutation.append())); - break; - case envoy::config::common::mutation_rules::v3::HeaderMutation::ActionCase::kRemove: - mutations_.emplace_back(std::make_unique(mutation.remove())); - break; - default: - PANIC_DUE_TO_PROTO_UNSET; - } - } -} +HeaderMutation::HeaderMutation(const ProtoHeaderMutation& mutations) + : mutations_(mutations.mutations()) {} bool HeaderMutation::mutate(Envoy::Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& stream_info) const { - for (const auto& mutation : mutations_) { - mutation->mutate(headers, stream_info); - } + mutations_.evaluateHeaders(headers, headers, + *Envoy::Http::StaticEmptyHeaders::get().response_headers, stream_info); return true; } diff --git a/source/extensions/http/early_header_mutation/header_mutation/header_mutation.h b/source/extensions/http/early_header_mutation/header_mutation/header_mutation.h index cba418aa09c0..025270b0dddc 100644 --- a/source/extensions/http/early_header_mutation/header_mutation/header_mutation.h +++ b/source/extensions/http/early_header_mutation/header_mutation/header_mutation.h @@ -1,14 +1,10 @@ #pragma once -#include - -#include "envoy/common/regex.h" #include "envoy/extensions/http/early_header_mutation/header_mutation/v3/header_mutation.pb.h" #include "envoy/extensions/http/early_header_mutation/header_mutation/v3/header_mutation.pb.validate.h" #include "envoy/http/early_header_mutation.h" -#include "source/common/common/regex.h" -#include "source/common/router/header_parser.h" +#include "source/common/http/header_mutation.h" namespace Envoy { namespace Extensions { @@ -21,15 +17,6 @@ using HeaderValueOption = envoy::config::core::v3::HeaderValueOption; using ProtoHeaderMutation = envoy::extensions::http::early_header_mutation::header_mutation::v3::HeaderMutation; -class Mutation { -public: - virtual ~Mutation() = default; - - virtual void mutate(Envoy::Http::RequestHeaderMap& headers, - const StreamInfo::StreamInfo& stream_info) const PURE; -}; -using MutationPtr = std::unique_ptr; - class HeaderMutation : public Envoy::Http::EarlyHeaderMutation { public: HeaderMutation(const ProtoHeaderMutation& mutations); @@ -38,7 +25,7 @@ class HeaderMutation : public Envoy::Http::EarlyHeaderMutation { const StreamInfo::StreamInfo& stream_info) const override; private: - std::vector mutations_; + Envoy::Http::HeaderMutations mutations_; }; } // namespace HeaderMutation diff --git a/test/common/http/BUILD b/test/common/http/BUILD index 00c1c1a6b9e8..b85f7245627b 100644 --- a/test/common/http/BUILD +++ b/test/common/http/BUILD @@ -561,3 +561,13 @@ envoy_cc_test( "//source/common/http:dependency_manager", ], ) + +envoy_cc_test( + name = "header_mutation_test", + srcs = ["header_mutation_test.cc"], + deps = [ + "//source/common/http:header_mutation_lib", + "//test/mocks/stream_info:stream_info_mocks", + "//test/test_common:utility_lib", + ], +) diff --git a/test/common/http/header_mutation_test.cc b/test/common/http/header_mutation_test.cc new file mode 100644 index 000000000000..74ce706bd0e7 --- /dev/null +++ b/test/common/http/header_mutation_test.cc @@ -0,0 +1,297 @@ +#include "source/common/http/header_mutation.h" + +#include "test/mocks/stream_info/mocks.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Http { +namespace { + +TEST(HeaderMutationsTest, BasicRemove) { + ProtoHeaderMutatons proto_mutations; + proto_mutations.Add()->set_remove("flag-header"); + proto_mutations.Add()->set_remove("another-flag-header"); + + HeaderMutations mutations(proto_mutations); + NiceMock stream_info; + + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {"not-flag-header", "not-flag-header-value"}, + {":method", "GET"}, + {":path", "/"}, + {":authority", "host"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + EXPECT_EQ("", headers.get_("flag-header")); + EXPECT_EQ("", headers.get_("another-flag-header")); + EXPECT_EQ("not-flag-header-value", headers.get_("not-flag-header")); + } +} + +TEST(HeaderMutationsTest, AllOperations) { + ProtoHeaderMutatons proto_mutations; + // Step 1: Remove 'flag-header' header. + proto_mutations.Add()->set_remove("flag-header"); + + // Step 2: Append 'flag-header' header. + auto append = proto_mutations.Add()->mutable_append(); + append->mutable_header()->set_key("flag-header"); + append->mutable_header()->set_value("%REQ(ANOTHER-FLAG-HEADER)%"); + append->set_append_action(ProtoHeaderValueOption::APPEND_IF_EXISTS_OR_ADD); + + // Step 3: Append 'flag-header-2' header. + auto append2 = proto_mutations.Add()->mutable_append(); + append2->mutable_header()->set_key("flag-header-2"); + append2->mutable_header()->set_value("flag-header-2-value"); + append2->set_append_action(ProtoHeaderValueOption::APPEND_IF_EXISTS_OR_ADD); + + // Step 4: Append 'flag-header-3' header if not exist. + auto append3 = proto_mutations.Add()->mutable_append(); + append3->mutable_header()->set_key("flag-header-3"); + append3->mutable_header()->set_value("flag-header-3-value"); + append3->set_append_action(ProtoHeaderValueOption::ADD_IF_ABSENT); + + // Step 4: Overwrite 'flag-header-4' header if exist. + auto append4 = proto_mutations.Add()->mutable_append(); + append4->mutable_header()->set_key("flag-header-4"); + append4->mutable_header()->set_value("flag-header-4-value"); + append4->set_append_action(ProtoHeaderValueOption::OVERWRITE_IF_EXISTS_OR_ADD); + + HeaderMutations mutations(proto_mutations); + NiceMock stream_info; + + // Remove 'flag-header' and try to append 'flag-header' with value 'another-flag-header-value'. + // But 'another-flag-header' is not found, so 'flag-header' is not appended. + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + + // Original 'flag-header' is removed and no new value is appended because there is no + // 'another-flag-header'. + EXPECT_EQ(0, headers.get(Http::LowerCaseString("flag-header")).size()); + } + // Remove 'flag-header' and try to append 'flag-header' with value 'another-flag-header-value'. + // 'another-flag-header' is found, so 'flag-header' is appended. + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + + // Original 'flag-header' is removed and the new value is appended. + EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); + } + + // Simple append 'flag-header-2' and a old value is exist. + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header-2", "flag-header-2-value-old"}, + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + + EXPECT_EQ(2, headers.get(Http::LowerCaseString("flag-header-2")).size()); + } + // Simple append 'flag-header-2' and a old value is not exist. + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + + EXPECT_EQ(1, headers.get(Http::LowerCaseString("flag-header-2")).size()); + } + + // Add header 'flag-header-3' if it is not exist. + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + + EXPECT_EQ(1, headers.get(Http::LowerCaseString("flag-header-3")).size()); + } + // Skip add header 'flag-header-3' if it is exist. + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header-3", "flag-header-3-value-old"}, + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + + EXPECT_EQ(1, headers.get(Http::LowerCaseString("flag-header-3")).size()); + EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); + } + + // Overwrite header 'flag-header-4' if it is exist. + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header-4", "flag-header-4-value-old"}, + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + + EXPECT_EQ(1, headers.get(Http::LowerCaseString("flag-header-4")).size()); + EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); + } + // Add header 'flag-header-4' if it is not exist. + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + + EXPECT_EQ(1, headers.get(Http::LowerCaseString("flag-header-4")).size()); + EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); + } + + // Hybrid case. + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {"flag-header-2", "flag-header-2-value-old"}, + {"flag-header-3", "flag-header-3-value-old"}, + {"flag-header-4", "flag-header-4-value-old"}, + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + + // 'flag-header' is removed and new 'flag-header' is added. + EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); + // 'flag-header-2' is appended. + EXPECT_EQ(2, headers.get(Http::LowerCaseString("flag-header-2")).size()); + // 'flag-header-3' is not appended and keep the old value. + EXPECT_EQ(1, headers.get(Http::LowerCaseString("flag-header-3")).size()); + EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); + // 'flag-header-4' is overwritten. + EXPECT_EQ(1, headers.get(Http::LowerCaseString("flag-header-4")).size()); + EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); + } +} + +TEST(HeaderMutationsTest, KeepEmptyValue) { + ProtoHeaderMutatons proto_mutations; + // Step 1: Remove the header. + proto_mutations.Add()->set_remove("flag-header"); + + // Step 2: Append the header and keep empty value if the source header is not found. + auto append = proto_mutations.Add()->mutable_append(); + append->mutable_header()->set_key("flag-header"); + append->mutable_header()->set_value("%REQ(ANOTHER-FLAG-HEADER)%"); + append->set_append_action(ProtoHeaderValueOption::APPEND_IF_EXISTS_OR_ADD); + append->set_keep_empty_value(true); + + HeaderMutations mutations(proto_mutations); + NiceMock stream_info; + + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + + // Original 'flag-header' is removed and empty value is appended. + EXPECT_EQ(2, headers.size()); + EXPECT_EQ(1, headers.get(Http::LowerCaseString("flag-header")).size()); + EXPECT_EQ("", headers.get_("flag-header")); + } +} + +TEST(HeaderMutationsTest, BasicOrder) { + { + ProtoHeaderMutatons proto_mutations; + + // Step 1: Append the header. + auto append = proto_mutations.Add()->mutable_append(); + append->mutable_header()->set_key("flag-header"); + append->mutable_header()->set_value("%REQ(ANOTHER-FLAG-HEADER)%"); + append->set_append_action(ProtoHeaderValueOption::APPEND_IF_EXISTS_OR_ADD); + + // Step 2: Remove the header. + proto_mutations.Add()->set_remove("flag-header"); + + HeaderMutations mutations(proto_mutations); + NiceMock stream_info; + + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + EXPECT_EQ("", headers.get_("flag-header")); + EXPECT_EQ(0, headers.get(Http::LowerCaseString("flag-header")).size()); + } + + { + ProtoHeaderMutatons proto_mutations; + // Step 1: Remove the header. + proto_mutations.Add()->set_remove("flag-header"); + + // Step 2: Append the header. + auto append = proto_mutations.Add()->mutable_append(); + append->mutable_header()->set_key("flag-header"); + append->mutable_header()->set_value("%REQ(ANOTHER-FLAG-HEADER)%"); + append->set_append_action(ProtoHeaderValueOption::APPEND_IF_EXISTS_OR_ADD); + + HeaderMutations mutations(proto_mutations); + NiceMock stream_info; + + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {":method", "GET"}, + }; + + mutations.evaluateHeaders(headers, headers, *Http::StaticEmptyHeaders::get().response_headers, + stream_info); + EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); + } +} + +TEST(HeaderMutationTest, Death) { + ProtoHeaderMutatons proto_mutations; + proto_mutations.Add(); + + EXPECT_DEATH(HeaderMutations{proto_mutations}, "unset oneof"); +} + +} // namespace +} // namespace Http +} // namespace Envoy diff --git a/test/extensions/filters/http/header_mutation/BUILD b/test/extensions/filters/http/header_mutation/BUILD new file mode 100644 index 000000000000..1e458242f2ff --- /dev/null +++ b/test/extensions/filters/http/header_mutation/BUILD @@ -0,0 +1,54 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "header_mutation_test", + srcs = [ + "header_mutation_test.cc", + ], + extension_names = ["envoy.filters.http.header_mutation"], + deps = [ + "//source/extensions/filters/http/header_mutation:config", + "//test/mocks/api:api_mocks", + "//test/mocks/http:http_mocks", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:registry_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_names = ["envoy.filters.http.header_mutation"], + deps = [ + "//source/extensions/filters/http/header_mutation:config", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/server:instance_mocks", + "//test/test_common:registry_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "header_mutation_integration_test", + srcs = ["header_mutation_integration_test.cc"], + extension_names = ["envoy.filters.http.header_mutation"], + deps = [ + "//source/extensions/filters/http/header_mutation:config", + "//test/integration:http_integration_lib", + "//test/mocks/server:instance_mocks", + "//test/test_common:registry_lib", + "//test/test_common:utility_lib", + ], +) diff --git a/test/extensions/filters/http/header_mutation/config_test.cc b/test/extensions/filters/http/header_mutation/config_test.cc new file mode 100644 index 000000000000..330d132d3cd3 --- /dev/null +++ b/test/extensions/filters/http/header_mutation/config_test.cc @@ -0,0 +1,76 @@ +#include "envoy/registry/registry.h" + +#include "source/common/config/utility.h" +#include "source/extensions/filters/http/header_mutation/config.h" + +#include "test/mocks/http/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace HeaderMutation { +namespace { + +TEST(FactoryTest, FactoryTest) { + + testing::NiceMock context; + + auto* factory = + Registry::FactoryRegistry::getFactory( + "envoy.filters.http.header_mutation"); + ASSERT_NE(factory, nullptr); + + const std::string config = R"EOF( + mutations: + request_mutations: + - remove: "flag-header" + - append: + header: + key: "flag-header" + value: "%REQ(ANOTHER-FLAG-HEADER)%" + append_action: APPEND_IF_EXISTS_OR_ADD + response_mutations: + - remove: "flag-header" + - append: + header: + key: "flag-header" + value: "%REQ(ANOTHER-FLAG-HEADER)%" + append_action: APPEND_IF_EXISTS_OR_ADD + )EOF"; + + PerRouteProtoConfig per_route_proto_config; + TestUtility::loadFromYaml(config, per_route_proto_config); + ProtoConfig proto_config; + TestUtility::loadFromYaml(config, proto_config); + + testing::NiceMock mock_factory_context; + + auto cb = factory->createFilterFactoryFromProto(proto_config, "test", mock_factory_context); + Http::MockFilterChainFactoryCallbacks filter_callbacks; + EXPECT_CALL(filter_callbacks, addStreamFilter(_)); + cb(filter_callbacks); + + EXPECT_NE(nullptr, factory->createRouteSpecificFilterConfig( + per_route_proto_config, mock_factory_context.server_factory_context_, + mock_factory_context.messageValidationVisitor())); +} + +TEST(FactoryTest, UpstreamFactoryTest) { + + testing::NiceMock context; + + auto* factory = + Registry::FactoryRegistry::getFactory( + "envoy.filters.http.header_mutation"); + ASSERT_NE(factory, nullptr); +} + +} // namespace +} // namespace HeaderMutation +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/header_mutation/header_mutation_integration_test.cc b/test/extensions/filters/http/header_mutation/header_mutation_integration_test.cc new file mode 100644 index 000000000000..e0da5c540783 --- /dev/null +++ b/test/extensions/filters/http/header_mutation/header_mutation_integration_test.cc @@ -0,0 +1,170 @@ +#include "source/extensions/filters/http/header_mutation/config.h" + +#include "test/integration/http_integration.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace HeaderMutation { +namespace { + +class HeaderMutationIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { +public: + HeaderMutationIntegrationTest() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam()) {} + + void initializeFilter() { + setUpstreamProtocol(FakeHttpConnection::Type::HTTP1); + + config_helper_.prependFilter(R"EOF( +name: donwstream-header-mutation +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation + mutations: + request_mutations: + - append: + header: + key: "downstream-request-global-flag-header" + value: "downstream-request-global-flag-header-value" + append_action: APPEND_IF_EXISTS_OR_ADD + response_mutations: + - append: + header: + key: "downstream-global-flag-header" + value: "downstream-global-flag-header-value" + append_action: APPEND_IF_EXISTS_OR_ADD +)EOF", + true); + + config_helper_.prependFilter(R"EOF( +name: upstream-header-mutation +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation + mutations: + request_mutations: + - append: + header: + key: "upstream-request-global-flag-header" + value: "upstream-request-global-flag-header-value" + append_action: APPEND_IF_EXISTS_OR_ADD + response_mutations: + - append: + header: + key: "upstream-global-flag-header" + value: "upstream-global-flag-header-value" + append_action: APPEND_IF_EXISTS_OR_ADD + - append: + header: + key: "request-method-in-upstream-filter" + value: "%REQ(:METHOD)%" + append_action: APPEND_IF_EXISTS_OR_ADD +)EOF", + false); + + config_helper_.addConfigModifier( + [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& + hcm) { + auto* route = hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0); + + // Per route header mutation for downstream. + PerRouteProtoConfig header_mutation_1; + auto response_mutation = + header_mutation_1.mutable_mutations()->mutable_response_mutations()->Add(); + response_mutation->mutable_append()->mutable_header()->set_key( + "downstream-per-route-flag-header"); + response_mutation->mutable_append()->mutable_header()->set_value( + "downstream-per-route-flag-header-value"); + response_mutation->mutable_append()->set_append_action( + envoy::config::core::v3::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD); + + ProtobufWkt::Any per_route_config_1; + per_route_config_1.PackFrom(header_mutation_1); + + route->mutable_typed_per_filter_config()->insert( + {"donwstream-header-mutation", per_route_config_1}); + + // Per route header mutation for upstream. + PerRouteProtoConfig header_mutation_2; + response_mutation = + header_mutation_2.mutable_mutations()->mutable_response_mutations()->Add(); + response_mutation->mutable_append()->mutable_header()->set_key( + "upstream-per-route-flag-header"); + response_mutation->mutable_append()->mutable_header()->set_value( + "upstream-per-route-flag-header-value"); + response_mutation->mutable_append()->set_append_action( + envoy::config::core::v3::HeaderValueOption::APPEND_IF_EXISTS_OR_ADD); + + ProtobufWkt::Any per_route_config_2; + per_route_config_2.PackFrom(header_mutation_2); + + route->mutable_typed_per_filter_config()->insert( + {"upstream-header-mutation", per_route_config_2}); + }); + HttpIntegrationTest::initialize(); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, HeaderMutationIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +TEST_P(HeaderMutationIntegrationTest, TestHeaderMutation) { + initializeFilter(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + waitForNextUpstreamRequest(); + + EXPECT_EQ("downstream-request-global-flag-header-value", + upstream_request_->headers() + .get(Http::LowerCaseString("downstream-request-global-flag-header"))[0] + ->value() + .getStringView()); + EXPECT_EQ("upstream-request-global-flag-header-value", + upstream_request_->headers() + .get(Http::LowerCaseString("upstream-request-global-flag-header"))[0] + ->value() + .getStringView()); + + upstream_request_->encodeHeaders(default_response_headers_, true); + + ASSERT_TRUE(response->waitForEndStream()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + EXPECT_EQ("downstream-global-flag-header-value", + response->headers() + .get(Http::LowerCaseString("downstream-global-flag-header"))[0] + ->value() + .getStringView()); + EXPECT_EQ("downstream-per-route-flag-header-value", + response->headers() + .get(Http::LowerCaseString("downstream-per-route-flag-header"))[0] + ->value() + .getStringView()); + EXPECT_EQ("upstream-global-flag-header-value", + response->headers() + .get(Http::LowerCaseString("upstream-global-flag-header"))[0] + ->value() + .getStringView()); + EXPECT_EQ("upstream-per-route-flag-header-value", + response->headers() + .get(Http::LowerCaseString("upstream-per-route-flag-header"))[0] + ->value() + .getStringView()); + EXPECT_EQ("GET", response->headers() + .get(Http::LowerCaseString("request-method-in-upstream-filter"))[0] + ->value() + .getStringView()); + codec_client_->close(); +} + +} // namespace +} // namespace HeaderMutation +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/header_mutation/header_mutation_test.cc b/test/extensions/filters/http/header_mutation/header_mutation_test.cc new file mode 100644 index 000000000000..231858431b95 --- /dev/null +++ b/test/extensions/filters/http/header_mutation/header_mutation_test.cc @@ -0,0 +1,217 @@ +#include "source/extensions/filters/http/header_mutation/header_mutation.h" + +#include "test/mocks/http/mocks.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace HeaderMutation { +namespace { + +using testing::NiceMock; + +TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { + const std::string route_config_yaml = R"EOF( + mutations: + request_mutations: + - remove: "flag-header" + - append: + header: + key: "flag-header" + value: "%REQ(ANOTHER-FLAG-HEADER)%" + append_action: "APPEND_IF_EXISTS_OR_ADD" + - append: + header: + key: "flag-header-2" + value: "flag-header-2-value" + append_action: "APPEND_IF_EXISTS_OR_ADD" + - append: + header: + key: "flag-header-3" + value: "flag-header-3-value" + append_action: "ADD_IF_ABSENT" + - append: + header: + key: "flag-header-4" + value: "flag-header-4-value" + append_action: "OVERWRITE_IF_EXISTS_OR_ADD" + response_mutations: + - remove: "flag-header" + - append: + header: + key: "flag-header" + value: "%RESP(ANOTHER-FLAG-HEADER)%" + append_action: "APPEND_IF_EXISTS_OR_ADD" + - append: + header: + key: "flag-header-2" + value: "flag-header-2-value" + append_action: "APPEND_IF_EXISTS_OR_ADD" + - append: + header: + key: "flag-header-3" + value: "flag-header-3-value" + append_action: "ADD_IF_ABSENT" + - append: + header: + key: "flag-header-4" + value: "flag-header-4-value" + append_action: "OVERWRITE_IF_EXISTS_OR_ADD" + )EOF"; + + const std::string config_yaml = R"EOF( + mutations: + request_mutations: + - append: + header: + key: "global-flag-header" + value: "global-flag-header-value" + append_action: "ADD_IF_ABSENT" + response_mutations: + - remove: "global-flag-header" + )EOF"; + + PerRouteProtoConfig per_route_proto_config; + TestUtility::loadFromYaml(route_config_yaml, per_route_proto_config); + + PerRouteHeaderMutationSharedPtr config = + std::make_shared(per_route_proto_config); + + ProtoConfig proto_config; + TestUtility::loadFromYaml(config_yaml, proto_config); + HeaderMutationConfigSharedPtr global_config = + std::make_shared(proto_config); + + NiceMock decoder_callbacks; + NiceMock encoder_callbacks; + + const Http::RequestHeaderMap* request_headers_pointer = + Http::StaticEmptyHeaders::get().request_headers.get(); + ON_CALL(encoder_callbacks.stream_info_, getRequestHeaders()) + .WillByDefault(testing::Return(request_headers_pointer)); + + { + HeaderMutation filter{global_config}; + filter.setDecoderFilterCallbacks(decoder_callbacks); + filter.setEncoderFilterCallbacks(encoder_callbacks); + + { + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {"flag-header-2", "flag-header-2-value-old"}, + {"flag-header-3", "flag-header-3-value-old"}, + {"flag-header-4", "flag-header-4-value-old"}, + {":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}}; + + EXPECT_CALL(decoder_callbacks, mostSpecificPerFilterConfig()) + .WillRepeatedly(testing::Return(config.get())); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.decodeHeaders(headers, true)); + + // 'flag-header' is removed and new 'flag-header' is added. + EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); + // 'flag-header-2' is appended. + EXPECT_EQ(2, headers.get(Envoy::Http::LowerCaseString("flag-header-2")).size()); + // 'flag-header-3' is not appended and keep the old value. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-3")).size()); + EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); + // 'flag-header-4' is overwritten. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-4")).size()); + EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); + } + + { + Envoy::Http::TestResponseHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {"flag-header-2", "flag-header-2-value-old"}, + {"flag-header-3", "flag-header-3-value-old"}, + {"flag-header-4", "flag-header-4-value-old"}, + {":status", "200"}, + }; + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.encodeHeaders(headers, true)); + + // 'flag-header' is removed and new 'flag-header' is added. + EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); + // 'flag-header-2' is appended. + EXPECT_EQ(2, headers.get(Envoy::Http::LowerCaseString("flag-header-2")).size()); + // 'flag-header-3' is not appended and keep the old value. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-3")).size()); + EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); + // 'flag-header-4' is overwritten. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-4")).size()); + EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); + } + } + + { + HeaderMutation filter{global_config}; + filter.setDecoderFilterCallbacks(decoder_callbacks); + filter.setEncoderFilterCallbacks(encoder_callbacks); + + Envoy::Http::TestResponseHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {"flag-header-2", "flag-header-2-value-old"}, + {"flag-header-3", "flag-header-3-value-old"}, + {"flag-header-4", "flag-header-4-value-old"}, + {":status", "200"}, + }; + + // If the decoding phase is not performed then try to get the config from the encoding phase. + EXPECT_CALL(encoder_callbacks, mostSpecificPerFilterConfig()) + .WillRepeatedly(testing::Return(config.get())); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.encodeHeaders(headers, true)); + + // 'flag-header' is removed and new 'flag-header' is added. + EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); + // 'flag-header-2' is appended. + EXPECT_EQ(2, headers.get(Envoy::Http::LowerCaseString("flag-header-2")).size()); + // 'flag-header-3' is not appended and keep the old value. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-3")).size()); + EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); + // 'flag-header-4' is overwritten. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-4")).size()); + EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); + } + + { + HeaderMutation filter{global_config}; + filter.setDecoderFilterCallbacks(decoder_callbacks); + filter.setEncoderFilterCallbacks(encoder_callbacks); + + Envoy::Http::TestRequestHeaderMapImpl request_headers = { + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + + Envoy::Http::TestResponseHeaderMapImpl response_headers = { + {"global-flag-header", "global-flag-header-value"}, + {":status", "200"}, + }; + + EXPECT_CALL(decoder_callbacks, mostSpecificPerFilterConfig()) + .WillRepeatedly(testing::Return(nullptr)); + EXPECT_CALL(encoder_callbacks, mostSpecificPerFilterConfig()) + .WillRepeatedly(testing::Return(nullptr)); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.decodeHeaders(request_headers, true)); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter.encodeHeaders(response_headers, true)); + + EXPECT_EQ("global-flag-header-value", request_headers.get_("global-flag-header")); + EXPECT_EQ("", response_headers.get_("global-flag-header")); + } +} + +} // namespace +} // namespace HeaderMutation +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/http/early_header_mutation/header_mutation/header_mutation_test.cc b/test/extensions/http/early_header_mutation/header_mutation/header_mutation_test.cc index f1719572898a..0705d8c58019 100644 --- a/test/extensions/http/early_header_mutation/header_mutation/header_mutation_test.cc +++ b/test/extensions/http/early_header_mutation/header_mutation/header_mutation_test.cc @@ -1,3 +1,4 @@ +#include "source/common/http/header_map_impl.h" #include "source/extensions/http/early_header_mutation/header_mutation/header_mutation.h" #include "test/mocks/stream_info/mocks.h" @@ -15,34 +16,7 @@ namespace { using ProtoHeaderMutation = envoy::extensions::http::early_header_mutation::header_mutation::v3::HeaderMutation; -TEST(HeaderMutationTest, BasicRemove) { - ScopedInjectableLoader engine{std::make_unique()}; - - const std::string config = R"EOF( - mutations: - - remove: "flag-header" - )EOF"; - - ProtoHeaderMutation proto_mutation; - TestUtility::loadFromYaml(config, proto_mutation); - - HeaderMutation mutation(proto_mutation); - NiceMock stream_info; - - { - Envoy::Http::TestRequestHeaderMapImpl headers = { - {"flag-header", "flag-header-value"}, - {":method", "GET"}, - }; - - EXPECT_TRUE(mutation.mutate(headers, stream_info)); - EXPECT_EQ("", headers.get_("flag-header")); - } -} - -TEST(HeaderMutationTest, Basic) { - ScopedInjectableLoader engine{std::make_unique()}; - +TEST(HeaderMutationTest, TestAll) { const std::string config = R"EOF( mutations: - remove: "flag-header" @@ -50,7 +24,22 @@ TEST(HeaderMutationTest, Basic) { header: key: "flag-header" value: "%REQ(ANOTHER-FLAG-HEADER)%" - append_action: APPEND_IF_EXISTS_OR_ADD + append_action: "APPEND_IF_EXISTS_OR_ADD" + - append: + header: + key: "flag-header-2" + value: "flag-header-2-value" + append_action: "APPEND_IF_EXISTS_OR_ADD" + - append: + header: + key: "flag-header-3" + value: "flag-header-3-value" + append_action: "ADD_IF_ABSENT" + - append: + header: + key: "flag-header-4" + value: "flag-header-4-value" + append_action: "OVERWRITE_IF_EXISTS_OR_ADD" )EOF"; ProtoHeaderMutation proto_mutation; @@ -59,94 +48,27 @@ TEST(HeaderMutationTest, Basic) { HeaderMutation mutation(proto_mutation); NiceMock stream_info; - { - Envoy::Http::TestRequestHeaderMapImpl headers = { - {":method", "GET"}, - }; - - EXPECT_TRUE(mutation.mutate(headers, stream_info)); - - EXPECT_EQ(1, headers.size()); - } - - { - Envoy::Http::TestRequestHeaderMapImpl headers = { - {"flag-header", "flag-header-value"}, - {"another-flag-header", "another-flag-header-value"}, - {":method", "GET"}, - }; - - EXPECT_TRUE(mutation.mutate(headers, stream_info)); - - EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); - } -} - -TEST(HeaderMutationTest, BasicOrder) { - ScopedInjectableLoader engine{std::make_unique()}; - - { - const std::string config = R"EOF( - mutations: - - append: - header: - key: "flag-header" - value: "%REQ(ANOTHER-FLAG-HEADER)%" - append_action: ADD_IF_ABSENT - - remove: "flag-header" - )EOF"; - - ProtoHeaderMutation proto_mutation; - TestUtility::loadFromYaml(config, proto_mutation); - - HeaderMutation mutation(proto_mutation); - NiceMock stream_info; - - Envoy::Http::TestRequestHeaderMapImpl headers = { - {"flag-header", "flag-header-value"}, - {"another-flag-header", "another-flag-header-value"}, - {":method", "GET"}, - }; - - EXPECT_TRUE(mutation.mutate(headers, stream_info)); - EXPECT_EQ("", headers.get_("flag-header")); - } - - { - const std::string config = R"EOF( - mutations: - - remove: "flag-header" - - append: - header: - key: "flag-header" - value: "%REQ(ANOTHER-FLAG-HEADER)%" - append_action: ADD_IF_ABSENT - )EOF"; - - ProtoHeaderMutation proto_mutation; - TestUtility::loadFromYaml(config, proto_mutation); - - HeaderMutation mutation(proto_mutation); - NiceMock stream_info; - - Envoy::Http::TestRequestHeaderMapImpl headers = { - {"flag-header", "flag-header-value"}, - {"another-flag-header", "another-flag-header-value"}, - {":method", "GET"}, - }; - - EXPECT_TRUE(mutation.mutate(headers, stream_info)); - EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); - } -} - -TEST(HeaderMutationTest, Death) { - ScopedInjectableLoader engine{std::make_unique()}; - - ProtoHeaderMutation proto_mutation; - proto_mutation.mutable_mutations()->Add(); - - EXPECT_DEATH(HeaderMutation{proto_mutation}, "unset oneof"); + Envoy::Http::TestRequestHeaderMapImpl headers = { + {"flag-header", "flag-header-value"}, + {"another-flag-header", "another-flag-header-value"}, + {"flag-header-2", "flag-header-2-value-old"}, + {"flag-header-3", "flag-header-3-value-old"}, + {"flag-header-4", "flag-header-4-value-old"}, + {":method", "GET"}, + }; + + mutation.mutate(headers, stream_info); + + // 'flag-header' is removed and new 'flag-header' is added. + EXPECT_EQ("another-flag-header-value", headers.get_("flag-header")); + // 'flag-header-2' is appended. + EXPECT_EQ(2, headers.get(Envoy::Http::LowerCaseString("flag-header-2")).size()); + // 'flag-header-3' is not appended and keep the old value. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-3")).size()); + EXPECT_EQ("flag-header-3-value-old", headers.get_("flag-header-3")); + // 'flag-header-4' is overwritten. + EXPECT_EQ(1, headers.get(Envoy::Http::LowerCaseString("flag-header-4")).size()); + EXPECT_EQ("flag-header-4-value", headers.get_("flag-header-4")); } } // namespace