diff --git a/api/envoy/extensions/filters/http/header_to_metadata/v3/BUILD b/api/envoy/extensions/filters/http/header_to_metadata/v3/BUILD index a8dda77ddfc31..8253ea6dff83b 100644 --- a/api/envoy/extensions/filters/http/header_to_metadata/v3/BUILD +++ b/api/envoy/extensions/filters/http/header_to_metadata/v3/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ "//envoy/config/filter/http/header_to_metadata/v2:pkg", + "//envoy/type/matcher/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", ], ) diff --git a/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto b/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto index 8e7c490f01b66..07fbba4089f76 100644 --- a/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto +++ b/api/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto @@ -2,6 +2,9 @@ syntax = "proto3"; package envoy.extensions.filters.http.header_to_metadata.v3; +import "envoy/type/matcher/v3/regex.proto"; + +import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; import "validate/validate.proto"; @@ -44,7 +47,7 @@ message Config { BASE64 = 1; } - // [#next-free-field: 6] + // [#next-free-field: 7] message KeyValuePair { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.header_to_metadata.v2.Config.KeyValuePair"; @@ -57,12 +60,22 @@ message Config { // The value to pair with the given key. // - // When used for a `on_header_present` case, if value is non-empty it'll be used - // instead of the header value. If both are empty, no metadata is added. + // When used for a + // :ref:`on_header_present ` + // case, if value is non-empty it'll be used instead of the header value. If both are empty, no metadata is added. + // + // When used for a :ref:`on_header_missing ` + // case, a non-empty value must be provided otherwise no metadata is added. + string value = 3 [(udpa.annotations.field_migrate).oneof_promotion = "value_type"]; + + // If present, the header's value will be matched and substituted with this. If there is no match or substitution, the header value + // is used as-is. + // + // This is only used for :ref:`on_header_present `. // - // When used for a `on_header_missing` case, a non-empty value must be provided - // otherwise no metadata is added. - string value = 3; + // Note: if the `value` field is non-empty this field should be empty. + type.matcher.v3.RegexMatchAndSubstitute regex_value_rewrite = 6 + [(udpa.annotations.field_migrate).oneof_promotion = "value_type"]; // The value's type — defaults to string. ValueType type = 4; diff --git a/api/envoy/extensions/filters/http/header_to_metadata/v4alpha/BUILD b/api/envoy/extensions/filters/http/header_to_metadata/v4alpha/BUILD new file mode 100644 index 0000000000000..285e2346e0ff7 --- /dev/null +++ b/api/envoy/extensions/filters/http/header_to_metadata/v4alpha/BUILD @@ -0,0 +1,13 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/extensions/filters/http/header_to_metadata/v3:pkg", + "//envoy/type/matcher/v4alpha:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/http/header_to_metadata/v4alpha/header_to_metadata.proto b/api/envoy/extensions/filters/http/header_to_metadata/v4alpha/header_to_metadata.proto new file mode 100644 index 0000000000000..c7df11e3fcb6b --- /dev/null +++ b/api/envoy/extensions/filters/http/header_to_metadata/v4alpha/header_to_metadata.proto @@ -0,0 +1,120 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.header_to_metadata.v4alpha; + +import "envoy/type/matcher/v4alpha/regex.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.header_to_metadata.v4alpha"; +option java_outer_classname = "HeaderToMetadataProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: Header-To-Metadata Filter] +// +// The configuration for transforming headers into metadata. This is useful +// for matching load balancer subsets, logging, etc. +// +// Header to Metadata :ref:`configuration overview `. +// [#extension: envoy.filters.http.header_to_metadata] + +message Config { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.header_to_metadata.v3.Config"; + + enum ValueType { + STRING = 0; + + NUMBER = 1; + + // The value is a serialized `protobuf.Value + // `_. + PROTOBUF_VALUE = 2; + } + + // ValueEncode defines the encoding algorithm. + enum ValueEncode { + // The value is not encoded. + NONE = 0; + + // The value is encoded in `Base64 `_. + // Note: this is mostly used for STRING and PROTOBUF_VALUE to escape the + // non-ASCII characters in the header. + BASE64 = 1; + } + + // [#next-free-field: 7] + message KeyValuePair { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.header_to_metadata.v3.Config.KeyValuePair"; + + // The namespace — if this is empty, the filter's namespace will be used. + string metadata_namespace = 1; + + // The key to use within the namespace. + string key = 2 [(validate.rules).string = {min_bytes: 1}]; + + oneof value_type { + // The value to pair with the given key. + // + // When used for a + // :ref:`on_header_present ` + // case, if value is non-empty it'll be used instead of the header value. If both are empty, no metadata is added. + // + // When used for a :ref:`on_header_missing ` + // case, a non-empty value must be provided otherwise no metadata is added. + string value = 3; + + // If present, the header's value will be matched and substituted with this. If there is no match or substitution, the header value + // is used as-is. + // + // This is only used for :ref:`on_header_present `. + // + // Note: if the `value` field is non-empty this field should be empty. + type.matcher.v4alpha.RegexMatchAndSubstitute regex_value_rewrite = 6; + } + + // The value's type — defaults to string. + ValueType type = 4; + + // How is the value encoded, default is NONE (not encoded). + // The value will be decoded accordingly before storing to metadata. + ValueEncode encode = 5; + } + + // A Rule defines what metadata to apply when a header is present or missing. + message Rule { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.header_to_metadata.v3.Config.Rule"; + + // The header that triggers this rule — required. + string header = 1 + [(validate.rules).string = {min_bytes: 1 well_known_regex: HTTP_HEADER_NAME strict: false}]; + + // If the header is present, apply this metadata KeyValuePair. + // + // If the value in the KeyValuePair is non-empty, it'll be used instead + // of the header value. + KeyValuePair on_header_present = 2; + + // If the header is not present, apply this metadata KeyValuePair. + // + // The value in the KeyValuePair must be set, since it'll be used in lieu + // of the missing header value. + KeyValuePair on_header_missing = 3; + + // Whether or not to remove the header after a rule is applied. + // + // This prevents headers from leaking. + bool remove = 4; + } + + // The list of rules to apply to requests. + repeated Rule request_rules = 1; + + // The list of rules to apply to responses. + repeated Rule response_rules = 2; +} diff --git a/docs/root/configuration/http/http_filters/header_to_metadata_filter.rst b/docs/root/configuration/http/http_filters/header_to_metadata_filter.rst index e482545a481f4..f169fbe939890 100644 --- a/docs/root/configuration/http/http_filters/header_to_metadata_filter.rst +++ b/docs/root/configuration/http/http_filters/header_to_metadata_filter.rst @@ -60,6 +60,26 @@ This would then allow requests with the `x-version` header set to be matched aga endpoints with the corresponding version. Whereas requests with that header missing would be matched with the default endpoints. +If the header's value needs to be transformed before it's added to the request as +dynamic metadata, this filter supports regex matching and substitution: + +.. code-block:: yaml + + http_filters: + - name: envoy.filters.http.header_to_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config + request_rules: + - header: ":path" + on_header_present: + metadata_namespace: envoy.lb + key: cluster + regex_value_rewrite: + pattern: + google_re2: {} + regex: "^/(cluster[\\d\\w-]+)/?.*$" + substitution: "\\1" + Note that this filter also supports per route configuration: .. code-block:: yaml diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 95bd4c5bf88b2..485bf31511de9 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -14,6 +14,7 @@ Minor Behavior Changes * access loggers: applied existing buffer limits to access logs, as well as :ref:`stats ` for logged / dropped logs. This can be reverted temporarily by setting runtime feature `envoy.reloadable_features.disallow_unbounded_access_logs` to false. * build: run as non-root inside Docker containers. Existing behaviour can be restored by setting the environment variable `ENVOY_UID` to `0`. `ENVOY_UID` and `ENVOY_GID` can be used to set the envoy user's `uid` and `gid` respectively. +* header to metadata: on_header_missing rules with empty values are now rejected (they were skipped before). * health check: in the health check filter the :ref:`percentage of healthy servers in upstream clusters ` is now interpreted as an integer. * hot restart: added the option :option:`--use-dynamic-base-id` to select an unused base ID at startup and the option :option:`--base-id-path` to write the base id to a file (for reuse with later hot restarts). * http: changed early error path for HTTP/1.1 so that responses consistently flow through the http connection manager, and the http filter chains. This behavior may be temporarily reverted by setting runtime feature `envoy.reloadable_features.early_errors_via_hcm` to false. @@ -86,6 +87,7 @@ New Features * grpc-json: send a `x-envoy-original-method` header to grpc services. * gzip filter: added option to set zlib's next output buffer size. * hds: updated to allow to explicitly set the API version of gRPC service endpoint and message to be used. +* header to metadata: added support for regex substitutions on header values. * health checks: allow configuring health check transport sockets by specifying :ref:`transport socket match criteria `. * http: added :ref:`local_reply config ` to http_connection_manager to customize :ref:`local reply `. * http: added :ref:`stripping port from host header ` support. diff --git a/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v3/BUILD index a8dda77ddfc31..8253ea6dff83b 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v3/BUILD +++ b/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v3/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ "//envoy/config/filter/http/header_to_metadata/v2:pkg", + "//envoy/type/matcher/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", ], ) diff --git a/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto b/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto index 8e7c490f01b66..07fbba4089f76 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.proto @@ -2,6 +2,9 @@ syntax = "proto3"; package envoy.extensions.filters.http.header_to_metadata.v3; +import "envoy/type/matcher/v3/regex.proto"; + +import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; import "validate/validate.proto"; @@ -44,7 +47,7 @@ message Config { BASE64 = 1; } - // [#next-free-field: 6] + // [#next-free-field: 7] message KeyValuePair { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.header_to_metadata.v2.Config.KeyValuePair"; @@ -57,12 +60,22 @@ message Config { // The value to pair with the given key. // - // When used for a `on_header_present` case, if value is non-empty it'll be used - // instead of the header value. If both are empty, no metadata is added. + // When used for a + // :ref:`on_header_present ` + // case, if value is non-empty it'll be used instead of the header value. If both are empty, no metadata is added. + // + // When used for a :ref:`on_header_missing ` + // case, a non-empty value must be provided otherwise no metadata is added. + string value = 3 [(udpa.annotations.field_migrate).oneof_promotion = "value_type"]; + + // If present, the header's value will be matched and substituted with this. If there is no match or substitution, the header value + // is used as-is. + // + // This is only used for :ref:`on_header_present `. // - // When used for a `on_header_missing` case, a non-empty value must be provided - // otherwise no metadata is added. - string value = 3; + // Note: if the `value` field is non-empty this field should be empty. + type.matcher.v3.RegexMatchAndSubstitute regex_value_rewrite = 6 + [(udpa.annotations.field_migrate).oneof_promotion = "value_type"]; // The value's type — defaults to string. ValueType type = 4; diff --git a/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v4alpha/BUILD b/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v4alpha/BUILD new file mode 100644 index 0000000000000..285e2346e0ff7 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v4alpha/BUILD @@ -0,0 +1,13 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/extensions/filters/http/header_to_metadata/v3:pkg", + "//envoy/type/matcher/v4alpha:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v4alpha/header_to_metadata.proto b/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v4alpha/header_to_metadata.proto new file mode 100644 index 0000000000000..c7df11e3fcb6b --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/header_to_metadata/v4alpha/header_to_metadata.proto @@ -0,0 +1,120 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.header_to_metadata.v4alpha; + +import "envoy/type/matcher/v4alpha/regex.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.header_to_metadata.v4alpha"; +option java_outer_classname = "HeaderToMetadataProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: Header-To-Metadata Filter] +// +// The configuration for transforming headers into metadata. This is useful +// for matching load balancer subsets, logging, etc. +// +// Header to Metadata :ref:`configuration overview `. +// [#extension: envoy.filters.http.header_to_metadata] + +message Config { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.header_to_metadata.v3.Config"; + + enum ValueType { + STRING = 0; + + NUMBER = 1; + + // The value is a serialized `protobuf.Value + // `_. + PROTOBUF_VALUE = 2; + } + + // ValueEncode defines the encoding algorithm. + enum ValueEncode { + // The value is not encoded. + NONE = 0; + + // The value is encoded in `Base64 `_. + // Note: this is mostly used for STRING and PROTOBUF_VALUE to escape the + // non-ASCII characters in the header. + BASE64 = 1; + } + + // [#next-free-field: 7] + message KeyValuePair { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.header_to_metadata.v3.Config.KeyValuePair"; + + // The namespace — if this is empty, the filter's namespace will be used. + string metadata_namespace = 1; + + // The key to use within the namespace. + string key = 2 [(validate.rules).string = {min_bytes: 1}]; + + oneof value_type { + // The value to pair with the given key. + // + // When used for a + // :ref:`on_header_present ` + // case, if value is non-empty it'll be used instead of the header value. If both are empty, no metadata is added. + // + // When used for a :ref:`on_header_missing ` + // case, a non-empty value must be provided otherwise no metadata is added. + string value = 3; + + // If present, the header's value will be matched and substituted with this. If there is no match or substitution, the header value + // is used as-is. + // + // This is only used for :ref:`on_header_present `. + // + // Note: if the `value` field is non-empty this field should be empty. + type.matcher.v4alpha.RegexMatchAndSubstitute regex_value_rewrite = 6; + } + + // The value's type — defaults to string. + ValueType type = 4; + + // How is the value encoded, default is NONE (not encoded). + // The value will be decoded accordingly before storing to metadata. + ValueEncode encode = 5; + } + + // A Rule defines what metadata to apply when a header is present or missing. + message Rule { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.header_to_metadata.v3.Config.Rule"; + + // The header that triggers this rule — required. + string header = 1 + [(validate.rules).string = {min_bytes: 1 well_known_regex: HTTP_HEADER_NAME strict: false}]; + + // If the header is present, apply this metadata KeyValuePair. + // + // If the value in the KeyValuePair is non-empty, it'll be used instead + // of the header value. + KeyValuePair on_header_present = 2; + + // If the header is not present, apply this metadata KeyValuePair. + // + // The value in the KeyValuePair must be set, since it'll be used in lieu + // of the missing header value. + KeyValuePair on_header_missing = 3; + + // Whether or not to remove the header after a rule is applied. + // + // This prevents headers from leaking. + bool remove = 4; + } + + // The list of rules to apply to requests. + repeated Rule request_rules = 1; + + // The list of rules to apply to responses. + repeated Rule response_rules = 2; +} diff --git a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc index 92f7728114d8b..e96cbb55a53cb 100644 --- a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc +++ b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.cc @@ -3,6 +3,7 @@ #include "envoy/extensions/filters/http/header_to_metadata/v3/header_to_metadata.pb.h" #include "common/common/base64.h" +#include "common/common/regex.h" #include "common/config/well_known_names.h" #include "common/http/utility.h" #include "common/protobuf/protobuf.h" @@ -17,6 +18,14 @@ namespace Extensions { namespace HttpFilters { namespace HeaderToMetadataFilter { +Rule::Rule(const std::string& header, const ProtoRule& rule) : header_(header), rule_(rule) { + if (rule.on_header_present().has_regex_value_rewrite()) { + const auto& rewrite_spec = rule.on_header_present().regex_value_rewrite(); + regex_rewrite_ = Regex::Utility::parseRegex(rewrite_spec.pattern()); + regex_rewrite_substitution_ = rewrite_spec.substitution(); + } +} + Config::Config(const envoy::extensions::filters::http::header_to_metadata::v3::Config config, const bool per_route) { request_set_ = Config::configToVector(config.request_rules(), request_rules_); @@ -40,8 +49,6 @@ bool Config::configToVector(const ProtobufRepeatedRule& proto_rules, } for (const auto& entry : proto_rules) { - std::pair rule = {Http::LowerCaseString(entry.header()), entry}; - // Rule must have at least one of the `on_header_*` fields set. if (!entry.has_on_header_present() && !entry.has_on_header_missing()) { const auto& error = fmt::format("header to metadata filter: rule for header '{}' has neither " @@ -50,7 +57,18 @@ bool Config::configToVector(const ProtobufRepeatedRule& proto_rules, throw EnvoyException(error); } - vector.push_back(rule); + // Ensure value and regex_value_rewrite are not mixed. + // TODO(rgs1): remove this once we are on v4 and these fields are folded into a oneof. + if (!entry.on_header_present().value().empty() && + entry.on_header_present().has_regex_value_rewrite()) { + throw EnvoyException("Cannot specify both value and regex_value_rewrite"); + } + + if (entry.has_on_header_missing() && entry.on_header_missing().value().empty()) { + throw EnvoyException("Cannot specify on_header_missing rule with an empty value"); + } + + vector.emplace_back(entry.header(), entry); } return true; @@ -94,11 +112,7 @@ bool HeaderToMetadataFilter::addMetadata(StructMap& map, const std::string& meta ValueType type, ValueEncode encode) const { ProtobufWkt::Value val; - if (value.empty()) { - // No value, skip. we could allow this though. - ENVOY_LOG(debug, "no metadata value provided"); - return false; - } + ASSERT(!value.empty()); if (value.size() >= MAX_HEADER_VALUE_LEN) { // Too long, go away. @@ -138,8 +152,7 @@ bool HeaderToMetadataFilter::addMetadata(StructMap& map, const std::string& meta break; } default: - ENVOY_LOG(debug, "unknown value type"); - return false; + NOT_REACHED_GCOVR_EXCL_LINE; } // Have we seen this namespace before? @@ -164,15 +177,27 @@ void HeaderToMetadataFilter::writeHeaderToMetadata(Http::HeaderMap& headers, Http::StreamFilterCallbacks& callbacks) { StructMap structs_by_namespace; - for (const auto& rulePair : rules) { - const auto& header = rulePair.first; - const auto& rule = rulePair.second; + for (const auto& rule : rules) { + const auto& header = rule.header(); + const auto& proto_rule = rule.rule(); const Http::HeaderEntry* header_entry = headers.get(header); - if (header_entry != nullptr && rule.has_on_header_present()) { - const auto& keyval = rule.on_header_present(); - absl::string_view value = keyval.value().empty() ? header_entry->value().getStringView() - : absl::string_view(keyval.value()); + if (header_entry != nullptr && proto_rule.has_on_header_present()) { + const auto& keyval = proto_rule.on_header_present(); + absl::string_view value = header_entry->value().getStringView(); + // This is used to hold the rewritten header value, so that it can + // be bound to value without going out of scope. + std::string rewritten_value; + + if (!keyval.value().empty()) { + value = absl::string_view(keyval.value()); + } else { + const auto& matcher = rule.regexRewrite(); + if (matcher != nullptr) { + rewritten_value = matcher->replaceAll(value, rule.regexSubstitution()); + value = rewritten_value; + } + } if (!value.empty()) { const auto& nspace = decideNamespace(keyval.metadata_namespace()); @@ -182,20 +207,17 @@ void HeaderToMetadataFilter::writeHeaderToMetadata(Http::HeaderMap& headers, ENVOY_LOG(debug, "value is empty, not adding metadata"); } - if (rule.remove()) { + if (proto_rule.remove()) { headers.remove(header); } - } else if (rule.has_on_header_missing()) { + } else if (proto_rule.has_on_header_missing()) { // Add metadata for the header missing case. - const auto& keyval = rule.on_header_missing(); + const auto& keyval = proto_rule.on_header_missing(); - if (!keyval.value().empty()) { - const auto& nspace = decideNamespace(keyval.metadata_namespace()); - addMetadata(structs_by_namespace, nspace, keyval.key(), keyval.value(), keyval.type(), - keyval.encode()); - } else { - ENVOY_LOG(debug, "value is empty, not adding metadata"); - } + ASSERT(!keyval.value().empty()); + const auto& nspace = decideNamespace(keyval.metadata_namespace()); + addMetadata(structs_by_namespace, nspace, keyval.key(), keyval.value(), keyval.type(), + keyval.encode()); } } diff --git a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h index 29614e6704d89..4cc3e117c4ff2 100644 --- a/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h +++ b/source/extensions/filters/http/header_to_metadata/header_to_metadata_filter.h @@ -8,6 +8,7 @@ #include "envoy/server/filter_config.h" #include "common/common/logger.h" +#include "common/common/matchers.h" #include "absl/strings/string_view.h" @@ -16,10 +17,26 @@ namespace Extensions { namespace HttpFilters { namespace HeaderToMetadataFilter { -using Rule = envoy::extensions::filters::http::header_to_metadata::v3::Config::Rule; +using ProtoRule = envoy::extensions::filters::http::header_to_metadata::v3::Config::Rule; using ValueType = envoy::extensions::filters::http::header_to_metadata::v3::Config::ValueType; using ValueEncode = envoy::extensions::filters::http::header_to_metadata::v3::Config::ValueEncode; -using HeaderToMetadataRules = std::vector>; + +class Rule { +public: + Rule(const std::string& header, const ProtoRule& rule); + const ProtoRule& rule() const { return rule_; } + const Regex::CompiledMatcherPtr& regexRewrite() const { return regex_rewrite_; } + const std::string& regexSubstitution() const { return regex_rewrite_substitution_; } + const Http::LowerCaseString& header() const { return header_; } + +private: + const Http::LowerCaseString header_; + const ProtoRule rule_; + Regex::CompiledMatcherPtr regex_rewrite_{}; + std::string regex_rewrite_substitution_{}; +}; + +using HeaderToMetadataRules = std::vector; // TODO(yangminzhu): Make MAX_HEADER_VALUE_LEN configurable. const uint32_t MAX_HEADER_VALUE_LEN = 8 * 1024; @@ -34,18 +51,13 @@ class Config : public ::Envoy::Router::RouteSpecificFilterConfig, Config(const envoy::extensions::filters::http::header_to_metadata::v3::Config config, bool per_route = false); - HeaderToMetadataRules requestRules() const { return request_rules_; } - HeaderToMetadataRules responseRules() const { return response_rules_; } + const HeaderToMetadataRules& requestRules() const { return request_rules_; } + const HeaderToMetadataRules& responseRules() const { return response_rules_; } bool doResponse() const { return response_set_; } bool doRequest() const { return request_set_; } private: - using ProtobufRepeatedRule = Protobuf::RepeatedPtrField; - - HeaderToMetadataRules request_rules_; - HeaderToMetadataRules response_rules_; - bool response_set_; - bool request_set_; + using ProtobufRepeatedRule = Protobuf::RepeatedPtrField; /** * configToVector is a helper function for converting from configuration (protobuf types) into @@ -60,6 +72,11 @@ class Config : public ::Envoy::Router::RouteSpecificFilterConfig, static bool configToVector(const ProtobufRepeatedRule&, HeaderToMetadataRules&); const std::string& decideNamespace(const std::string& nspace) const; + + HeaderToMetadataRules request_rules_; + HeaderToMetadataRules response_rules_; + bool response_set_; + bool request_set_; }; using ConfigSharedPtr = std::shared_ptr; diff --git a/test/extensions/filters/http/header_to_metadata/config_test.cc b/test/extensions/filters/http/header_to_metadata/config_test.cc index 861e4ee545a76..a9e108371dfbc 100644 --- a/test/extensions/filters/http/header_to_metadata/config_test.cc +++ b/test/extensions/filters/http/header_to_metadata/config_test.cc @@ -20,6 +20,18 @@ namespace HeaderToMetadataFilter { using HeaderToMetadataProtoConfig = envoy::extensions::filters::http::header_to_metadata::v3::Config; +void testForbiddenConfig(const std::string& yaml) { + HeaderToMetadataProtoConfig proto_config; + TestUtility::loadFromYamlAndValidate(yaml, proto_config); + + testing::NiceMock context; + HeaderToMetadataConfig factory; + + EXPECT_THROW(factory.createFilterFactoryFromProto(proto_config, "stats", context), + EnvoyException); +} + +// Tests that an empty header is rejected. TEST(HeaderToMetadataFilterConfigTest, InvalidEmptyHeader) { const std::string yaml = R"EOF( request_rules: @@ -30,6 +42,7 @@ TEST(HeaderToMetadataFilterConfigTest, InvalidEmptyHeader) { EXPECT_THROW(TestUtility::loadFromYamlAndValidate(yaml, proto_config), ProtoValidationException); } +// Tests that empty (metadata) keys are rejected. TEST(HeaderToMetadataFilterConfigTest, InvalidEmptyKey) { const std::string yaml = R"EOF( request_rules: @@ -44,6 +57,7 @@ TEST(HeaderToMetadataFilterConfigTest, InvalidEmptyKey) { EXPECT_THROW(TestUtility::loadFromYamlAndValidate(yaml, proto_config), ProtoValidationException); } +// Tests that a valid config is properly consumed. TEST(HeaderToMetadataFilterConfigTest, SimpleConfig) { const std::string yaml = R"EOF( request_rules: @@ -71,6 +85,7 @@ TEST(HeaderToMetadataFilterConfigTest, SimpleConfig) { cb(filter_callbacks); } +// Tests that per route config properly overrides the global config. TEST(HeaderToMetadataFilterConfigTest, PerRouteConfig) { const std::string yaml = R"EOF( request_rules: @@ -99,6 +114,39 @@ TEST(HeaderToMetadataFilterConfigTest, PerRouteConfig) { EXPECT_FALSE(config->doResponse()); } +// Tests that configuration does not allow value and regex_value_rewrite in the same rule. +TEST(HeaderToMetadataFilterConfigTest, ValueAndRegex) { + const std::string yaml = R"EOF( +request_rules: + - header: x-version + on_header_present: + metadata_namespace: envoy.lb + key: cluster + value: foo + regex_value_rewrite: + pattern: + google_re2: {} + regex: "^/(cluster[\\d\\w-]+)/?.*$" + substitution: "\\1" + )EOF"; + + testForbiddenConfig(yaml); +} + +// Tests that on_header_missing rules don't allow an empty value. +TEST(HeaderToMetadataFilterConfigTest, OnHeaderMissingEmptyValue) { + const std::string yaml = R"EOF( +request_rules: + - header: x-version + on_header_missing: + metadata_namespace: envoy.lb + key: "foo" + type: STRING + )EOF"; + + testForbiddenConfig(yaml); +} + } // namespace HeaderToMetadataFilter } // namespace HttpFilters } // namespace Extensions diff --git a/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc b/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc index 493bdd465dca1..906475013f933 100644 --- a/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc +++ b/test/extensions/filters/http/header_to_metadata/header_to_metadata_filter_test.cc @@ -435,7 +435,7 @@ TEST_F(HeaderToMetadataTest, RejectInvalidRule) { )EOF"; auto expected = "header to metadata filter: rule for header 'x-something' has neither " "`on_header_present` nor `on_header_missing` set"; - EXPECT_THROW_WITH_MESSAGE(initializeFilter(config), Envoy::EnvoyException, expected); + EXPECT_THROW_WITH_MESSAGE(initializeFilter(config), EnvoyException, expected); } TEST_F(HeaderToMetadataTest, PerRouteEmtpyRules) { @@ -463,6 +463,65 @@ TEST_F(HeaderToMetadataTest, NoEmptyValues) { EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); } +/** + * Regex substitution on header value. + */ +TEST_F(HeaderToMetadataTest, RegexSubstitution) { + const std::string config = R"EOF( +request_rules: + - header: :path + on_header_present: + metadata_namespace: envoy.lb + key: cluster + regex_value_rewrite: + pattern: + google_re2: {} + regex: "^/(cluster[\\d\\w-]+)/?.*$" + substitution: "\\1" +)EOF"; + initializeFilter(config); + + // Match with additional path elements. + { + Http::TestRequestHeaderMapImpl headers{{":path", "/cluster-prod-001/x/y"}}; + std::map expected = {{"cluster", "cluster-prod-001"}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(req_info_)); + EXPECT_CALL(req_info_, setDynamicMetadata("envoy.lb", MapEq(expected))); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + } + + // Match with no additional path elements. + { + Http::TestRequestHeaderMapImpl headers{{":path", "/cluster-prod-001"}}; + std::map expected = {{"cluster", "cluster-prod-001"}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(req_info_)); + EXPECT_CALL(req_info_, setDynamicMetadata("envoy.lb", MapEq(expected))); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + } + + // No match. + { + Http::TestRequestHeaderMapImpl headers{{":path", "/foo"}}; + std::map expected = {{"cluster", "/foo"}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(req_info_)); + EXPECT_CALL(req_info_, setDynamicMetadata("envoy.lb", MapEq(expected))); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + } + + // No match with additional path elements. + { + Http::TestRequestHeaderMapImpl headers{{":path", "/foo/bar?x=2"}}; + std::map expected = {{"cluster", "/foo/bar?x=2"}}; + + EXPECT_CALL(decoder_callbacks_, streamInfo()).WillRepeatedly(ReturnRef(req_info_)); + EXPECT_CALL(req_info_, setDynamicMetadata("envoy.lb", MapEq(expected))); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + } +} + } // namespace HeaderToMetadataFilter } // namespace HttpFilters } // namespace Extensions