diff --git a/CODEOWNERS b/CODEOWNERS index a0c6e40bc469f..b1d70cf7e3e88 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -171,6 +171,8 @@ extensions/filters/http/oauth2 @rgs1 @derekargueta @snowp /*/extensions/io_socket/user_space @lambdai @antoniovicente # Default UUID4 request ID extension /*/extensions/request_id/uuid @mattklein123 @alyssawilk +# HTTP header formatters +/*/extensions/http/header_formatters/preserve_case @mattklein123 @jmarantz # External Rate Limit /*/extensions/filters/common/ratelimit @esmet @mattklein123 /*/extensions/filters/http/ratelimit @esmet @mattklein123 diff --git a/api/BUILD b/api/BUILD index bc8c502662985..914eaa892aee4 100644 --- a/api/BUILD +++ b/api/BUILD @@ -243,6 +243,7 @@ proto_library( "//envoy/extensions/filters/udp/dns_filter/v3alpha:pkg", "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", "//envoy/extensions/health_checkers/redis/v3:pkg", + "//envoy/extensions/http/header_formatters/preserve_case/v3:pkg", "//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg", "//envoy/extensions/internal_redirect/previous_routes/v3:pkg", "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index 8c7693c8ab17e..ac2a4907b2efb 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.config.core.v3; +import "envoy/config/core/v3/extension.proto"; import "envoy/type/v3/percent.proto"; import "google/protobuf/duration.proto"; @@ -118,6 +119,7 @@ message Http1ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.Http1ProtocolOptions"; + // [#next-free-field: 9] message HeaderKeyFormat { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.Http1ProtocolOptions.HeaderKeyFormat"; @@ -136,6 +138,11 @@ message Http1ProtocolOptions { // Note that while this results in most headers following conventional casing, certain headers // are not covered. For example, the "TE" header will be formatted as "Te". ProperCaseWords proper_case_words = 1; + + // Configuration for stateful formatter extensions that allow using received headers to + // affect the output of encoding headers. E.g., preserving case during proxying. + // [#extension-category: envoy.http.stateful_header_formatters] + TypedExtensionConfig stateful_formatter = 8; } } diff --git a/api/envoy/config/core/v4alpha/protocol.proto b/api/envoy/config/core/v4alpha/protocol.proto index 1f0af4d12922c..7cc09a5fbadbd 100644 --- a/api/envoy/config/core/v4alpha/protocol.proto +++ b/api/envoy/config/core/v4alpha/protocol.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.config.core.v4alpha; +import "envoy/config/core/v4alpha/extension.proto"; import "envoy/type/v3/percent.proto"; import "google/protobuf/duration.proto"; @@ -121,6 +122,7 @@ message Http1ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.config.core.v3.Http1ProtocolOptions"; + // [#next-free-field: 9] message HeaderKeyFormat { option (udpa.annotations.versioning).previous_message_type = "envoy.config.core.v3.Http1ProtocolOptions.HeaderKeyFormat"; @@ -139,6 +141,11 @@ message Http1ProtocolOptions { // Note that while this results in most headers following conventional casing, certain headers // are not covered. For example, the "TE" header will be formatted as "Te". ProperCaseWords proper_case_words = 1; + + // Configuration for stateful formatter extensions that allow using received headers to + // affect the output of encoding headers. E.g., preserving case during proxying. + // [#extension-category: envoy.http.stateful_header_formatters] + TypedExtensionConfig stateful_formatter = 8; } } diff --git a/api/envoy/extensions/http/header_formatters/preserve_case/v3/BUILD b/api/envoy/extensions/http/header_formatters/preserve_case/v3/BUILD new file mode 100644 index 0000000000000..ee92fb652582e --- /dev/null +++ b/api/envoy/extensions/http/header_formatters/preserve_case/v3/BUILD @@ -0,0 +1,9 @@ +# 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 = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/api/envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.proto b/api/envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.proto new file mode 100644 index 0000000000000..64bdd497ecab0 --- /dev/null +++ b/api/envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package envoy.extensions.http.header_formatters.preserve_case.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.http.header_formatters.preserve_case.v3"; +option java_outer_classname = "PreserveCaseProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Preserve case header formatter] +// [#extension: envoy.http.stateful_header_formatters.preserve_case] + +// Configuration for the preserve case header formatter. +// See the :ref:`header casing ` configuration guide for more +// information. +message PreserveCaseFormatterConfig { +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 83e85ed89d5af..daadfd39ea67d 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -126,6 +126,7 @@ proto_library( "//envoy/extensions/filters/udp/dns_filter/v3alpha:pkg", "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", "//envoy/extensions/health_checkers/redis/v3:pkg", + "//envoy/extensions/http/header_formatters/preserve_case/v3:pkg", "//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg", "//envoy/extensions/internal_redirect/previous_routes/v3:pkg", "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", diff --git a/bazel/envoy_library.bzl b/bazel/envoy_library.bzl index 75eb02258235d..e0c27f3bd9715 100644 --- a/bazel/envoy_library.bzl +++ b/bazel/envoy_library.bzl @@ -80,6 +80,7 @@ EXTENSION_CATEGORIES = [ "envoy.grpc_credentials", "envoy.guarddog_actions", "envoy.health_checkers", + "envoy.http.stateful_header_formatters", "envoy.internal_redirect_predicates", "envoy.io_socket", "envoy.matching.input_matchers", diff --git a/docs/root/api-v3/config/config.rst b/docs/root/api-v3/config/config.rst index 70a73db70ca83..84bbc17d48a33 100644 --- a/docs/root/api-v3/config/config.rst +++ b/docs/root/api-v3/config/config.rst @@ -26,3 +26,4 @@ Extensions watchdog/watchdog descriptors/descriptors request_id/request_id + http/header_formatters diff --git a/docs/root/api-v3/config/http/header_formatters.rst b/docs/root/api-v3/config/http/header_formatters.rst new file mode 100644 index 0000000000000..1518defddb384 --- /dev/null +++ b/docs/root/api-v3/config/http/header_formatters.rst @@ -0,0 +1,8 @@ +HTTP header formatters +====================== + +.. toctree:: + :glob: + :maxdepth: 2 + + ../../extensions/http/header_formatters/*/v3/* \ No newline at end of file diff --git a/docs/root/configuration/http/http_conn_man/_include/preserve-case.yaml b/docs/root/configuration/http/http_conn_man/_include/preserve-case.yaml new file mode 100644 index 0000000000000..cf637b1eeb7a1 --- /dev/null +++ b/docs/root/configuration/http/http_conn_man/_include/preserve-case.yaml @@ -0,0 +1,50 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 443 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + http_protocol_options: + header_key_format: + stateful_formatter: + name: preserve_case + typed_config: + "@type": type.googleapis.com/envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig + http_filters: + - name: envoy.filters.http.router + route_config: + virtual_hosts: + - name: default + domains: ["*"] + routes: + - match: { prefix: "/" } + route: + cluster: service_foo + clusters: + - name: service_foo + connect_timeout: 15s + typed_extension_protocol_options: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicit_http_config: + http_protocol_options: + header_key_format: + stateful_formatter: + name: preserve_case + typed_config: + "@type": type.googleapis.com/envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig + load_assignment: + cluster_name: some_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 8080 diff --git a/docs/root/configuration/http/http_conn_man/header_casing.rst b/docs/root/configuration/http/http_conn_man/header_casing.rst index 7795829b66b83..ad56ec71b2684 100644 --- a/docs/root/configuration/http/http_conn_man/header_casing.rst +++ b/docs/root/configuration/http/http_conn_man/header_casing.rst @@ -1,3 +1,5 @@ +.. _config_http_conn_man_header_casing: + HTTP/1.1 Header Casing ====================== @@ -5,10 +7,41 @@ When handling HTTP/1.1, Envoy will normalize the header keys to be all lowercase compliant with the HTTP/1.1 spec, in practice this can result in issues when migrating existing systems that might rely on specific header casing. -To support these use cases, Envoy allows configuring a formatting scheme for the headers, which -will have Envoy transform the header keys during serialization. To configure this formatting on -response headers, specify the format in the :ref:`http_protocol_options `. -To configure this for upstream request headers, specify the formatting in :ref:`http_protocol_options ` in the Cluster's :ref:`extension_protocol_options`. +To support these use cases, Envoy allows :ref:`configuring a formatting scheme for the headers +`, which will have Envoy +transform the header keys during serialization. + +To configure this formatting on response headers, specify the format in the +:ref:`http_protocol_options +`. +To configure this for upstream request headers, specify the formatting in +:ref:`http_protocol_options ` in +the cluster's +:ref:`extension_protocol_options`. + +Currently Envoy supports two mutually exclusive types of header key formatters: + +Stateless formatters +-------------------- + +Stateless formatters are run on encoding and do not depend on any previous knowledge of the headers. +An example of this type of formatter is the :ref:`proper case words +` +formatter. These formatters are useful when converting from non-HTTP/1 to HTTP/1 (within a single +proxy or across multiple hops) or when stateful formatting is not desired due to increased memory +requirements. + +Stateful formatters +------------------- + +Stateful formatters are instantiated on decoding, called for every decoded header, attached to the +header map, and are then available during encoding to format the headers prior to writing. Thus, they +traverse the entire proxy stack. An example of this type of formatter is the :ref:`preserve case +formatter +` +configured via the :ref:`stateful_formatter +` field. +The following is an example configuration which will preserve HTTP/1 header case across the proxy. -See :ref:`below ` for other connection timeouts. -on the :ref:`Cluster `. FIXME +.. literalinclude:: _include/preserve-case.yaml + :language: yaml diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 163fefe768c6e..6c55788a3f161 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -140,6 +140,7 @@ New Features * http: added support for `Envoy::ScopeTrackedObject` for HTTP/1 and HTTP/2 dispatching. Crashes while inside the dispatching loop should dump debug information. Furthermore, HTTP/1 and HTTP/2 clients now dumps the originating request whose response from the upstream caused Envoy to crash. * http: added support for :ref:`preconnecting `. Preconnecting is off by default, but recommended for clusters serving latency-sensitive traffic, especially if using HTTP/1.1. * http: added new runtime config `envoy.reloadable_features.check_unsupported_typed_per_filter_config`, the default value is true. When the value is true, envoy will reject virtual host-specific typed per filter config when the filter doesn't support it. +* http: added the ability to preserve HTTP/1 header case across the proxy. See the :ref:`header casing ` documentation for more information. * http: change frame flood and abuse checks to the upstream HTTP/2 codec to ON by default. It can be disabled by setting the `envoy.reloadable_features.upstream_http2_flood_checks` runtime key to false. * json: introduced new JSON parser (https://github.com/nlohmann/json) to replace RapidJSON. The new parser is disabled by default. To test the new RapidJSON parser, enable the runtime feature `envoy.reloadable_features.remove_legacy_json`. * kill_request: :ref:`Kill Request ` Now supports bidirection killing. diff --git a/generated_api_shadow/BUILD b/generated_api_shadow/BUILD index bc8c502662985..914eaa892aee4 100644 --- a/generated_api_shadow/BUILD +++ b/generated_api_shadow/BUILD @@ -243,6 +243,7 @@ proto_library( "//envoy/extensions/filters/udp/dns_filter/v3alpha:pkg", "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", "//envoy/extensions/health_checkers/redis/v3:pkg", + "//envoy/extensions/http/header_formatters/preserve_case/v3:pkg", "//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg", "//envoy/extensions/internal_redirect/previous_routes/v3:pkg", "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", diff --git a/generated_api_shadow/envoy/config/core/v3/protocol.proto b/generated_api_shadow/envoy/config/core/v3/protocol.proto index 8c7693c8ab17e..ac2a4907b2efb 100644 --- a/generated_api_shadow/envoy/config/core/v3/protocol.proto +++ b/generated_api_shadow/envoy/config/core/v3/protocol.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.config.core.v3; +import "envoy/config/core/v3/extension.proto"; import "envoy/type/v3/percent.proto"; import "google/protobuf/duration.proto"; @@ -118,6 +119,7 @@ message Http1ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.Http1ProtocolOptions"; + // [#next-free-field: 9] message HeaderKeyFormat { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.Http1ProtocolOptions.HeaderKeyFormat"; @@ -136,6 +138,11 @@ message Http1ProtocolOptions { // Note that while this results in most headers following conventional casing, certain headers // are not covered. For example, the "TE" header will be formatted as "Te". ProperCaseWords proper_case_words = 1; + + // Configuration for stateful formatter extensions that allow using received headers to + // affect the output of encoding headers. E.g., preserving case during proxying. + // [#extension-category: envoy.http.stateful_header_formatters] + TypedExtensionConfig stateful_formatter = 8; } } diff --git a/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto b/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto index 131553b755dfb..0fa01f030a86c 100644 --- a/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto +++ b/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package envoy.config.core.v4alpha; +import "envoy/config/core/v4alpha/extension.proto"; import "envoy/type/v3/percent.proto"; import "google/protobuf/duration.proto"; @@ -121,6 +122,7 @@ message Http1ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.config.core.v3.Http1ProtocolOptions"; + // [#next-free-field: 9] message HeaderKeyFormat { option (udpa.annotations.versioning).previous_message_type = "envoy.config.core.v3.Http1ProtocolOptions.HeaderKeyFormat"; @@ -139,6 +141,11 @@ message Http1ProtocolOptions { // Note that while this results in most headers following conventional casing, certain headers // are not covered. For example, the "TE" header will be formatted as "Te". ProperCaseWords proper_case_words = 1; + + // Configuration for stateful formatter extensions that allow using received headers to + // affect the output of encoding headers. E.g., preserving case during proxying. + // [#extension-category: envoy.http.stateful_header_formatters] + TypedExtensionConfig stateful_formatter = 8; } } diff --git a/generated_api_shadow/envoy/extensions/http/header_formatters/preserve_case/v3/BUILD b/generated_api_shadow/envoy/extensions/http/header_formatters/preserve_case/v3/BUILD new file mode 100644 index 0000000000000..ee92fb652582e --- /dev/null +++ b/generated_api_shadow/envoy/extensions/http/header_formatters/preserve_case/v3/BUILD @@ -0,0 +1,9 @@ +# 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 = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/generated_api_shadow/envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.proto b/generated_api_shadow/envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.proto new file mode 100644 index 0000000000000..64bdd497ecab0 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package envoy.extensions.http.header_formatters.preserve_case.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.http.header_formatters.preserve_case.v3"; +option java_outer_classname = "PreserveCaseProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Preserve case header formatter] +// [#extension: envoy.http.stateful_header_formatters.preserve_case] + +// Configuration for the preserve case header formatter. +// See the :ref:`header casing ` configuration guide for more +// information. +message PreserveCaseFormatterConfig { +} diff --git a/include/envoy/common/optref.h b/include/envoy/common/optref.h index f96654f5938af..10d4cbacbfbb0 100644 --- a/include/envoy/common/optref.h +++ b/include/envoy/common/optref.h @@ -17,6 +17,26 @@ template struct OptRef : public absl::optional>(t) {} OptRef() = default; + /** + * Copy constructor that allows conversion. + */ + template explicit OptRef(OptRef rhs) { + if (rhs.has_value()) { + *this = rhs.ref(); + } + } + + /** + * Assignment that allows conversion. + */ + template OptRef& operator=(OptRef rhs) { + this->reset(); + if (rhs.has_value()) { + *this = rhs.ref(); + } + return *this; + } + /** * Helper to call a method on T. The caller is responsible for ensuring * has_value() is true. diff --git a/include/envoy/http/BUILD b/include/envoy/http/BUILD index 42503c61d0e83..0e41b02ffd76f 100644 --- a/include/envoy/http/BUILD +++ b/include/envoy/http/BUILD @@ -104,6 +104,7 @@ envoy_cc_library( "abseil_inlined_vector", ], deps = [ + ":header_formatter_interface", "//source/common/common:assert_lib", "//source/common/common:hash_lib", ], @@ -142,3 +143,11 @@ envoy_cc_library( "//include/envoy/tracing:trace_reason_interface", ], ) + +envoy_cc_library( + name = "header_formatter_interface", + hdrs = ["header_formatter.h"], + deps = [ + "//include/envoy/config:typed_config_interface", + ], +) diff --git a/include/envoy/http/codec.h b/include/envoy/http/codec.h index 4ccdf2e3b3574..cd003751430c9 100644 --- a/include/envoy/http/codec.h +++ b/include/envoy/http/codec.h @@ -7,6 +7,7 @@ #include "envoy/buffer/buffer.h" #include "envoy/common/pure.h" #include "envoy/grpc/status.h" +#include "envoy/http/header_formatter.h" #include "envoy/http/header_map.h" #include "envoy/http/metadata_interface.h" #include "envoy/http/protocol.h" @@ -439,11 +440,16 @@ struct Http1Settings { // Performs proper casing of header keys: the first and all alpha characters following a // non-alphanumeric character is capitalized. ProperCase, + // A stateful formatter extension has been configured. + StatefulFormatter, }; // How header keys should be formatted when serializing HTTP/1.1 headers. HeaderKeyFormat header_key_format_{HeaderKeyFormat::Default}; + // Non-null IFF header_key_format_ is configured to StatefulFormatter. + StatefulHeaderKeyFormatterFactorySharedPtr stateful_header_key_formatter_; + // Behaviour on invalid HTTP messaging: // - if true, the HTTP/1.1 connection is left open (where possible) // - if false, the HTTP/1.1 connection is terminated diff --git a/include/envoy/http/header_formatter.h b/include/envoy/http/header_formatter.h new file mode 100644 index 0000000000000..c30f4c459ee40 --- /dev/null +++ b/include/envoy/http/header_formatter.h @@ -0,0 +1,70 @@ +#pragma once + +#include "envoy/common/optref.h" +#include "envoy/config/typed_config.h" + +namespace Envoy { +namespace Http { + +/** + * Interface for generic header key formatting. + */ +class HeaderKeyFormatter { +public: + virtual ~HeaderKeyFormatter() = default; + + /** + * Given an input key return the formatted key to encode. + */ + virtual std::string format(absl::string_view key) const PURE; +}; + +using HeaderKeyFormatterConstPtr = std::unique_ptr; +using HeaderKeyFormatterOptConstRef = OptRef; + +/** + * Interface for header key formatters that are stateful. A formatter is created during decoding + * headers, attached to the header map, and can then be used during encoding for reverse + * translations if applicable. + */ +class StatefulHeaderKeyFormatter : public HeaderKeyFormatter { +public: + /** + * Called for each header key received by the codec. + */ + virtual void processKey(absl::string_view key) PURE; +}; + +using StatefulHeaderKeyFormatterPtr = std::unique_ptr; +using StatefulHeaderKeyFormatterOptRef = OptRef; +using StatefulHeaderKeyFormatterOptConstRef = OptRef; + +/** + * Interface for creating stateful header key formatters. + */ +class StatefulHeaderKeyFormatterFactory { +public: + virtual ~StatefulHeaderKeyFormatterFactory() = default; + + /** + * Create a new formatter. + */ + virtual StatefulHeaderKeyFormatterPtr create() PURE; +}; + +using StatefulHeaderKeyFormatterFactorySharedPtr = + std::shared_ptr; + +/** + * Extension configuration for stateful header key formatters. + */ +class StatefulHeaderKeyFormatterFactoryConfig : public Config::TypedFactory { +public: + virtual StatefulHeaderKeyFormatterFactorySharedPtr + createFromProto(const Protobuf::Message& config) PURE; + + std::string category() const override { return "envoy.http.stateful_header_formatters"; } +}; + +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/include/envoy/http/header_map.h b/include/envoy/http/header_map.h index 795452439f544..2567a95b6935f 100644 --- a/include/envoy/http/header_map.h +++ b/include/envoy/http/header_map.h @@ -10,6 +10,7 @@ #include "envoy/common/optref.h" #include "envoy/common/pure.h" +#include "envoy/http/header_formatter.h" #include "common/common/assert.h" #include "common/common/hash.h" @@ -659,6 +660,19 @@ class HeaderMap { headers.dumpState(os); return os; } + + /** + * Return the optional stateful formatter attached to this header map. + * + * Filters can use the non-const version to process additional header keys during operation if + * they wish. The sequence of events would be to first add/modify the header map, and then call + * processKey(), similar to what is done when headers are received by the codec. + * + * TODO(mattklein123): The above sequence will not work for headers added via route (headers to + * add, etc.). We can potentially add direct processKey() calls in these places as a follow up. + */ + virtual StatefulHeaderKeyFormatterOptConstRef formatter() const PURE; + virtual StatefulHeaderKeyFormatterOptRef formatter() PURE; }; using HeaderMapPtr = std::unique_ptr; diff --git a/source/common/http/header_map_impl.h b/source/common/http/header_map_impl.h index 1a87b2c4df711..2f52e70c66691 100644 --- a/source/common/http/header_map_impl.h +++ b/source/common/http/header_map_impl.h @@ -7,6 +7,7 @@ #include #include +#include "envoy/common/optref.h" #include "envoy/http/header_map.h" #include "common/common/non_copyable.h" @@ -100,6 +101,10 @@ class HeaderMapImpl : NonCopyable { size_t size() const { return headers_.size(); } bool empty() const { return headers_.empty(); } void dumpState(std::ostream& os, int indent_level = 0) const; + StatefulHeaderKeyFormatterOptConstRef formatter() const { + return StatefulHeaderKeyFormatterOptConstRef(makeOptRefFromPtr(formatter_.get())); + } + StatefulHeaderKeyFormatterOptRef formatter() { return makeOptRefFromPtr(formatter_.get()); } protected: struct HeaderEntryImpl : public HeaderEntry, NonCopyable { @@ -327,6 +332,11 @@ class HeaderMapImpl : NonCopyable { virtual HeaderEntryImpl** inlineHeaders() PURE; HeaderList headers_; + // TODO(mattklein123): The formatter does not currently get copied when a header map gets + // copied. This may be problematic in certain cases like request shadowing. This is omitted + // on purpose until someone asks for it, at which point a clone() method can be created to + // avoid using extra space/processing for a shared_ptr. + StatefulHeaderKeyFormatterPtr formatter_; // This holds the internal byte size of the HeaderMap. uint64_t cached_byte_size_ = 0; const bool header_map_correctly_coalesce_cookies_ = Runtime::runtimeFeatureEnabled( @@ -340,6 +350,10 @@ class HeaderMapImpl : NonCopyable { */ template class TypedHeaderMapImpl : public HeaderMapImpl, public Interface { public: + void setFormatter(StatefulHeaderKeyFormatterPtr&& formatter) { + formatter_ = std::move(formatter); + } + // Implementation of Http::HeaderMap that passes through to HeaderMapImpl. bool operator==(const HeaderMap& rhs) const override { return HeaderMapImpl::operator==(rhs); } bool operator!=(const HeaderMap& rhs) const override { return HeaderMapImpl::operator!=(rhs); } @@ -394,6 +408,10 @@ template class TypedHeaderMapImpl : public HeaderMapImpl, publ void dumpState(std::ostream& os, int indent_level = 0) const override { HeaderMapImpl::dumpState(os, indent_level); } + StatefulHeaderKeyFormatterOptConstRef formatter() const override { + return HeaderMapImpl::formatter(); + } + StatefulHeaderKeyFormatterOptRef formatter() override { return HeaderMapImpl::formatter(); } // Generic custom header functions for each fully typed interface. To avoid accidental issues, // the Handle type is different for each interface, which is why these functions live here vs. diff --git a/source/common/http/http1/BUILD b/source/common/http/http1/BUILD index 8242b63181be7..495eef9ee4fdf 100644 --- a/source/common/http/http1/BUILD +++ b/source/common/http/http1/BUILD @@ -12,6 +12,9 @@ envoy_cc_library( name = "header_formatter_lib", srcs = ["header_formatter.cc"], hdrs = ["header_formatter.h"], + deps = [ + "//include/envoy/http:header_formatter_interface", + ], ) envoy_cc_library( @@ -81,6 +84,19 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "settings_lib", + srcs = ["settings.cc"], + hdrs = ["settings.h"], + external_deps = ["abseil_optional"], + deps = [ + "//include/envoy/http:codec_interface", + "//include/envoy/protobuf:message_validator_interface", + "//source/common/config:utility_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) + envoy_cc_library( name = "parser_interface", hdrs = ["parser.h"], diff --git a/source/common/http/http1/codec_impl.cc b/source/common/http/http1/codec_impl.cc index d86b03a2949c7..0d96e483ff8a3 100644 --- a/source/common/http/http1/codec_impl.cc +++ b/source/common/http/http1/codec_impl.cc @@ -4,6 +4,7 @@ #include #include "envoy/buffer/buffer.h" +#include "envoy/common/optref.h" #include "envoy/http/codec.h" #include "envoy/http/header_map.h" #include "envoy/network/connection.h" @@ -64,24 +65,31 @@ const StringUtil::CaseUnorderedSet& caseUnorderdSetContainingUpgradeAndHttp2Sett Http::Headers::get().ConnectionValues.Http2Settings); } -HeaderKeyFormatterPtr formatter(const Http::Http1Settings& settings) { +HeaderKeyFormatterConstPtr encodeOnlyFormatterFromSettings(const Http::Http1Settings& settings) { if (settings.header_key_format_ == Http1Settings::HeaderKeyFormat::ProperCase) { return std::make_unique(); } return nullptr; } + +StatefulHeaderKeyFormatterPtr statefulFormatterFromSettings(const Http::Http1Settings& settings) { + if (settings.header_key_format_ == Http1Settings::HeaderKeyFormat::StatefulFormatter) { + return settings.stateful_header_key_formatter_->create(); + } + return nullptr; +} + } // namespace const std::string StreamEncoderImpl::CRLF = "\r\n"; // Last chunk as defined here https://tools.ietf.org/html/rfc7230#section-4.1 const std::string StreamEncoderImpl::LAST_CHUNK = "0\r\n"; -StreamEncoderImpl::StreamEncoderImpl(ConnectionImpl& connection, - HeaderKeyFormatter* header_key_formatter) +StreamEncoderImpl::StreamEncoderImpl(ConnectionImpl& connection) : connection_(connection), disable_chunk_encoding_(false), chunk_encoding_(true), connect_request_(false), is_tcp_tunneling_(false), is_response_to_head_request_(false), - is_response_to_connect_request_(false), header_key_formatter_(header_key_formatter) { + is_response_to_connect_request_(false) { if (connection_.connection().aboveHighWatermark()) { runHighWatermarkCallbacks(); } @@ -102,9 +110,10 @@ void StreamEncoderImpl::encodeHeader(absl::string_view key, absl::string_view va this->encodeHeader(key.data(), key.size(), value.data(), value.size()); } -void StreamEncoderImpl::encodeFormattedHeader(absl::string_view key, absl::string_view value) { - if (header_key_formatter_ != nullptr) { - encodeHeader(header_key_formatter_->format(key), value); +void StreamEncoderImpl::encodeFormattedHeader(absl::string_view key, absl::string_view value, + HeaderKeyFormatterOptConstRef formatter) { + if (formatter.has_value()) { + encodeHeader(formatter->format(key), value); } else { encodeHeader(key, value); } @@ -118,26 +127,32 @@ void ResponseEncoderImpl::encode100ContinueHeaders(const ResponseHeaderMap& head void StreamEncoderImpl::encodeHeadersBase(const RequestOrResponseHeaderMap& headers, absl::optional status, bool end_stream, bool bodiless_request) { + HeaderKeyFormatterOptConstRef formatter(headers.formatter()); + if (!formatter.has_value()) { + formatter = connection_.formatter(); + } + const Http::HeaderValues& header_values = Http::Headers::get(); bool saw_content_length = false; - headers.iterate([this, &header_values](const HeaderEntry& header) -> HeaderMap::Iterate { - absl::string_view key_to_use = header.key().getStringView(); - uint32_t key_size_to_use = header.key().size(); - // Translate :authority -> host so that upper layers do not need to deal with this. - if (key_size_to_use > 1 && key_to_use[0] == ':' && key_to_use[1] == 'a') { - key_to_use = absl::string_view(header_values.HostLegacy.get()); - key_size_to_use = header_values.HostLegacy.get().size(); - } + headers.iterate( + [this, &header_values, formatter](const HeaderEntry& header) -> HeaderMap::Iterate { + absl::string_view key_to_use = header.key().getStringView(); + uint32_t key_size_to_use = header.key().size(); + // Translate :authority -> host so that upper layers do not need to deal with this. + if (key_size_to_use > 1 && key_to_use[0] == ':' && key_to_use[1] == 'a') { + key_to_use = absl::string_view(header_values.HostLegacy.get()); + key_size_to_use = header_values.HostLegacy.get().size(); + } - // Skip all headers starting with ':' that make it here. - if (key_to_use[0] == ':') { - return HeaderMap::Iterate::Continue; - } + // Skip all headers starting with ':' that make it here. + if (key_to_use[0] == ':') { + return HeaderMap::Iterate::Continue; + } - encodeFormattedHeader(key_to_use, header.value().getStringView()); + encodeFormattedHeader(key_to_use, header.value().getStringView(), formatter); - return HeaderMap::Iterate::Continue; - }); + return HeaderMap::Iterate::Continue; + }); if (headers.ContentLength()) { saw_content_length = true; @@ -173,7 +188,7 @@ void StreamEncoderImpl::encodeHeadersBase(const RequestOrResponseHeaderMap& head if (!bodiless_request || !Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.dont_add_content_length_for_bodiless_requests")) { - encodeFormattedHeader(header_values.ContentLength.get(), "0"); + encodeFormattedHeader(header_values.ContentLength.get(), "0", formatter); } } chunk_encoding_ = false; @@ -192,7 +207,7 @@ void StreamEncoderImpl::encodeHeadersBase(const RequestOrResponseHeaderMap& head // https://tools.ietf.org/html/rfc7231#section-4.3.6. if (!is_response_to_connect_request_) { encodeFormattedHeader(header_values.TransferEncoding.get(), - header_values.TransferEncodingValues.Chunked); + header_values.TransferEncodingValues.Chunked, formatter); } // We do not apply chunk encoding for HTTP upgrades, including CONNECT style upgrades. // If there is a body in a response on the upgrade path, the chunks will be @@ -247,8 +262,10 @@ void StreamEncoderImpl::encodeTrailersBase(const HeaderMap& trailers) { // Finalize the body connection_.buffer().add(LAST_CHUNK); + // TODO(mattklein123): Wire up the formatter if someone actually asks for this (very unlikely). trailers.iterate([this](const HeaderEntry& header) -> HeaderMap::Iterate { - encodeFormattedHeader(header.key().getStringView(), header.value().getStringView()); + encodeFormattedHeader(header.key().getStringView(), header.value().getStringView(), + HeaderKeyFormatterOptConstRef()); return HeaderMap::Iterate::Continue; }); @@ -436,11 +453,11 @@ int ConnectionImpl::setAndCheckCallbackStatusOr(Envoy::StatusOr&& ConnectionImpl::ConnectionImpl(Network::Connection& connection, CodecStats& stats, const Http1Settings& settings, MessageType type, - uint32_t max_headers_kb, const uint32_t max_headers_count, - HeaderKeyFormatterPtr&& header_key_formatter) + uint32_t max_headers_kb, const uint32_t max_headers_count) : connection_(connection), stats_(stats), codec_settings_(settings), - header_key_formatter_(std::move(header_key_formatter)), processing_trailers_(false), - handling_upgrade_(false), reset_stream_called_(false), deferred_end_stream_headers_(false), + encode_only_header_key_formatter_(encodeOnlyFormatterFromSettings(settings)), + processing_trailers_(false), handling_upgrade_(false), reset_stream_called_(false), + deferred_end_stream_headers_(false), strict_1xx_and_204_headers_(Runtime::runtimeFeatureEnabled( "envoy.reloadable_features.strict_1xx_and_204_response_headers")), dispatching_(false), output_buffer_(connection.dispatcher().getWatermarkFactory().create( @@ -460,11 +477,19 @@ Status ConnectionImpl::completeLastHeader() { RETURN_IF_ERROR(checkHeaderNameForUnderscores()); auto& headers_or_trailers = headersOrTrailers(); if (!current_header_field_.empty()) { - current_header_field_.inlineTransform([](char c) { return absl::ascii_tolower(c); }); // Strip trailing whitespace of the current header value if any. Leading whitespace was trimmed // in ConnectionImpl::onHeaderValue. http_parser does not strip leading or trailing whitespace // as the spec requires: https://tools.ietf.org/html/rfc7230#section-3.2.4 current_header_value_.rtrim(); + + // If there is a stateful formatter installed, remember the original header key before + // converting to lower case. + auto formatter = headers_or_trailers.formatter(); + if (formatter.has_value()) { + formatter->processKey(current_header_field_.getStringView()); + } + current_header_field_.inlineTransform([](char c) { return absl::ascii_tolower(c); }); + headers_or_trailers.addViaMove(std::move(current_header_field_), std::move(current_header_value_)); } @@ -820,7 +845,7 @@ Status ConnectionImpl::onMessageBegin() { protocol_ = Protocol::Http11; processing_trailers_ = false; header_parsing_state_ = HeaderParsingState::Field; - allocHeaders(); + allocHeaders(statefulFormatterFromSettings(codec_settings_)); return onMessageBeginBase(); } @@ -914,7 +939,7 @@ ServerConnectionImpl::ServerConnectionImpl( envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action) : ConnectionImpl(connection, stats, settings, MessageType::Request, max_request_headers_kb, - max_request_headers_count, formatter(settings)), + max_request_headers_count), callbacks_(callbacks), response_buffer_releasor_([this](const Buffer::OwnedBufferFragmentImpl* fragment) { releaseOutboundResponse(fragment); @@ -1076,7 +1101,7 @@ Envoy::StatusOr ServerConnectionImpl::onHeadersCompleteBase() { Status ServerConnectionImpl::onMessageBeginBase() { if (!resetStreamCalled()) { ASSERT(!active_request_.has_value()); - active_request_.emplace(*this, header_key_formatter_.get()); + active_request_.emplace(*this); auto& active_request = active_request_.value(); if (resetStreamCalled()) { return codecClientError("cannot create new streams after calling reset"); @@ -1213,7 +1238,7 @@ ClientConnectionImpl::ClientConnectionImpl(Network::Connection& connection, Code ConnectionCallbacks&, const Http1Settings& settings, const uint32_t max_response_headers_count) : ConnectionImpl(connection, stats, settings, MessageType::Response, MAX_RESPONSE_HEADERS_KB, - max_response_headers_count, formatter(settings)) {} + max_response_headers_count) {} bool ClientConnectionImpl::cannotHaveBody() { if (pending_response_.has_value() && pending_response_.value().encoder_.headRequest()) { @@ -1236,7 +1261,7 @@ RequestEncoder& ClientConnectionImpl::newStream(ResponseDecoder& response_decode ASSERT(!pending_response_.has_value()); ASSERT(pending_response_done_); - pending_response_.emplace(*this, header_key_formatter_.get(), &response_decoder); + pending_response_.emplace(*this, &response_decoder); pending_response_done_ = false; return pending_response_.value().encoder_; } diff --git a/source/common/http/http1/codec_impl.h b/source/common/http/http1/codec_impl.h index 067e475339c99..fff5b3be66477 100644 --- a/source/common/http/http1/codec_impl.h +++ b/source/common/http/http1/codec_impl.h @@ -6,6 +6,7 @@ #include #include +#include "envoy/common/optref.h" #include "envoy/common/scope_tracker.h" #include "envoy/config/core/v3/protocol.pb.h" #include "envoy/http/codec.h" @@ -75,7 +76,7 @@ class StreamEncoderImpl : public virtual StreamEncoder, void clearReadDisableCallsForTests() { read_disable_calls_ = 0; } protected: - StreamEncoderImpl(ConnectionImpl& connection, HeaderKeyFormatter* header_key_formatter); + StreamEncoderImpl(ConnectionImpl& connection); void encodeHeadersBase(const RequestOrResponseHeaderMap& headers, absl::optional status, bool end_stream, bool bodiless_request); void encodeTrailersBase(const HeaderMap& headers); @@ -114,9 +115,9 @@ class StreamEncoderImpl : public virtual StreamEncoder, */ void endEncode(); - void encodeFormattedHeader(absl::string_view key, absl::string_view value); + void encodeFormattedHeader(absl::string_view key, absl::string_view value, + HeaderKeyFormatterOptConstRef formatter); - const HeaderKeyFormatter* const header_key_formatter_; absl::string_view details_; }; @@ -125,9 +126,8 @@ class StreamEncoderImpl : public virtual StreamEncoder, */ class ResponseEncoderImpl : public StreamEncoderImpl, public ResponseEncoder { public: - ResponseEncoderImpl(ConnectionImpl& connection, HeaderKeyFormatter* header_key_formatter, - bool stream_error_on_invalid_http_message) - : StreamEncoderImpl(connection, header_key_formatter), + ResponseEncoderImpl(ConnectionImpl& connection, bool stream_error_on_invalid_http_message) + : StreamEncoderImpl(connection), stream_error_on_invalid_http_message_(stream_error_on_invalid_http_message) {} bool startedResponse() { return started_response_; } @@ -151,8 +151,7 @@ class ResponseEncoderImpl : public StreamEncoderImpl, public ResponseEncoder { */ class RequestEncoderImpl : public StreamEncoderImpl, public RequestEncoder { public: - RequestEncoderImpl(ConnectionImpl& connection, HeaderKeyFormatter* header_key_formatter) - : StreamEncoderImpl(connection, header_key_formatter) {} + RequestEncoderImpl(ConnectionImpl& connection) : StreamEncoderImpl(connection) {} bool upgradeRequest() const { return upgrade_request_; } bool headRequest() const { return head_request_; } bool connectRequest() const { return connect_request_; } @@ -217,6 +216,9 @@ class ConnectionImpl : public virtual Connection, virtual void maybeAddSentinelBufferFragment(Buffer::Instance&) {} CodecStats& stats() { return stats_; } bool enableTrailers() const { return codec_settings_.enable_trailers_; } + HeaderKeyFormatterOptConstRef formatter() const { + return makeOptRefFromPtr(encode_only_header_key_formatter_.get()); + } // Http::Connection Http::Status dispatch(Buffer::Instance& data) override; @@ -238,8 +240,7 @@ class ConnectionImpl : public virtual Connection, protected: ConnectionImpl(Network::Connection& connection, CodecStats& stats, const Http1Settings& settings, - MessageType type, uint32_t max_headers_kb, const uint32_t max_headers_count, - HeaderKeyFormatterPtr&& header_key_formatter); + MessageType type, uint32_t max_headers_kb, const uint32_t max_headers_count); bool resetStreamCalled() { return reset_stream_called_; } @@ -269,7 +270,7 @@ class ConnectionImpl : public virtual Connection, std::unique_ptr parser_; Buffer::Instance* current_dispatching_buffer_{}; Http::Code error_code_{Http::Code::BadRequest}; - const HeaderKeyFormatterPtr header_key_formatter_; + const HeaderKeyFormatterConstPtr encode_only_header_key_formatter_; HeaderString current_header_field_; HeaderString current_header_value_; bool processing_trailers_ : 1; @@ -299,7 +300,7 @@ class ConnectionImpl : public virtual Connection, virtual HeaderMap& headersOrTrailers() PURE; virtual RequestOrResponseHeaderMap& requestOrResponseHeaders() PURE; - virtual void allocHeaders() PURE; + virtual void allocHeaders(StatefulHeaderKeyFormatterPtr&& formatter) PURE; virtual void allocTrailers() PURE; /** @@ -436,8 +437,8 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl { * An active HTTP/1.1 request. */ struct ActiveRequest { - ActiveRequest(ServerConnectionImpl& connection, HeaderKeyFormatter* header_key_formatter) - : response_encoder_(connection, header_key_formatter, + ActiveRequest(ServerConnectionImpl& connection) + : response_encoder_(connection, connection.codec_settings_.stream_error_on_invalid_http_message_) {} HeaderString request_url_; @@ -486,10 +487,12 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl { RequestOrResponseHeaderMap& requestOrResponseHeaders() override { return *absl::get(headers_or_trailers_); } - void allocHeaders() override { + void allocHeaders(StatefulHeaderKeyFormatterPtr&& formatter) override { ASSERT(nullptr == absl::get(headers_or_trailers_)); ASSERT(!processing_trailers_); - headers_or_trailers_.emplace(RequestHeaderMapImpl::create()); + auto headers = RequestHeaderMapImpl::create(); + headers->setFormatter(std::move(formatter)); + headers_or_trailers_.emplace(std::move(headers)); } void allocTrailers() override { ASSERT(processing_trailers_); @@ -536,9 +539,8 @@ class ClientConnectionImpl : public ClientConnection, public ConnectionImpl { private: struct PendingResponse { - PendingResponse(ConnectionImpl& connection, HeaderKeyFormatter* header_key_formatter, - ResponseDecoder* decoder) - : encoder_(connection, header_key_formatter), decoder_(decoder) {} + PendingResponse(ConnectionImpl& connection, ResponseDecoder* decoder) + : encoder_(connection), decoder_(decoder) {} RequestEncoderImpl encoder_; ResponseDecoder* decoder_; @@ -570,10 +572,12 @@ class ClientConnectionImpl : public ClientConnection, public ConnectionImpl { RequestOrResponseHeaderMap& requestOrResponseHeaders() override { return *absl::get(headers_or_trailers_); } - void allocHeaders() override { + void allocHeaders(StatefulHeaderKeyFormatterPtr&& formatter) override { ASSERT(nullptr == absl::get(headers_or_trailers_)); ASSERT(!processing_trailers_); - headers_or_trailers_.emplace(ResponseHeaderMapImpl::create()); + auto headers = ResponseHeaderMapImpl::create(); + headers->setFormatter(std::move(formatter)); + headers_or_trailers_.emplace(std::move(headers)); } void allocTrailers() override { ASSERT(processing_trailers_); diff --git a/source/common/http/http1/header_formatter.h b/source/common/http/http1/header_formatter.h index d99dc79cc741a..f99026148408b 100644 --- a/source/common/http/http1/header_formatter.h +++ b/source/common/http/http1/header_formatter.h @@ -1,25 +1,11 @@ #pragma once -#include -#include - -#include "envoy/common/pure.h" - -#include "absl/strings/string_view.h" +#include "envoy/http/header_formatter.h" namespace Envoy { namespace Http { namespace Http1 { -class HeaderKeyFormatter { -public: - virtual ~HeaderKeyFormatter() = default; - - virtual std::string format(absl::string_view key) const PURE; -}; - -using HeaderKeyFormatterPtr = std::unique_ptr; - /** * A HeaderKeyFormatter that upper cases the first character in each word: The * first character as well as any alpha character following a special diff --git a/source/common/http/http1/settings.cc b/source/common/http/http1/settings.cc new file mode 100644 index 0000000000000..c9a33381a68f8 --- /dev/null +++ b/source/common/http/http1/settings.cc @@ -0,0 +1,58 @@ +#include "common/http/http1/settings.h" + +#include "envoy/http/header_formatter.h" + +#include "common/config/utility.h" + +namespace Envoy { +namespace Http { +namespace Http1 { + +Http1Settings parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config, + ProtobufMessage::ValidationVisitor& validation_visitor) { + Http1Settings ret; + ret.allow_absolute_url_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, allow_absolute_url, true); + ret.accept_http_10_ = config.accept_http_10(); + ret.default_host_for_http_10_ = config.default_host_for_http_10(); + ret.enable_trailers_ = config.enable_trailers(); + ret.allow_chunked_length_ = config.allow_chunked_length(); + + if (config.header_key_format().has_proper_case_words()) { + ret.header_key_format_ = Http1Settings::HeaderKeyFormat::ProperCase; + } else if (config.header_key_format().has_stateful_formatter()) { + auto& factory = + Config::Utility::getAndCheckFactory( + config.header_key_format().stateful_formatter()); + auto header_formatter_config = Envoy::Config::Utility::translateAnyToFactoryConfig( + config.header_key_format().stateful_formatter().typed_config(), validation_visitor, + factory); + ret.header_key_format_ = Http1Settings::HeaderKeyFormat::StatefulFormatter; + ret.stateful_header_key_formatter_ = factory.createFromProto(*header_formatter_config); + } + + return ret; +} + +Http1Settings parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config, + ProtobufMessage::ValidationVisitor& validation_visitor, + const Protobuf::BoolValue& hcm_stream_error, + bool validate_scheme) { + Http1Settings ret = parseHttp1Settings(config, validation_visitor); + ret.validate_scheme_ = validate_scheme; + + if (config.has_override_stream_error_on_invalid_http_message()) { + // override_stream_error_on_invalid_http_message, if set, takes precedence over any HCM + // stream_error_on_invalid_http_message + ret.stream_error_on_invalid_http_message_ = + config.override_stream_error_on_invalid_http_message().value(); + } else { + // fallback to HCM value + ret.stream_error_on_invalid_http_message_ = hcm_stream_error.value(); + } + + return ret; +} + +} // namespace Http1 +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/source/common/http/http1/settings.h b/source/common/http/http1/settings.h new file mode 100644 index 0000000000000..8dfebf177cc36 --- /dev/null +++ b/source/common/http/http1/settings.h @@ -0,0 +1,24 @@ +#pragma once + +#include "envoy/config/core/v3/protocol.pb.h" +#include "envoy/http/codec.h" +#include "envoy/protobuf/message_validator.h" + +namespace Envoy { +namespace Http { +namespace Http1 { + +/** + * @return Http1Settings An Http1Settings populated from the + * envoy::config::core::v3::Http1ProtocolOptions config. + */ +Http1Settings parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config, + ProtobufMessage::ValidationVisitor& validation_visitor); + +Http1Settings parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config, + ProtobufMessage::ValidationVisitor& validation_visitor, + const Protobuf::BoolValue& hcm_stream_error, bool validate_scheme); + +} // namespace Http1 +} // namespace Http +} // namespace Envoy \ No newline at end of file diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index e2a39c3aaf51a..a19973f80c9b0 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -467,43 +467,6 @@ bool Utility::isWebSocketUpgradeRequest(const RequestHeaderMap& headers) { Http::Headers::get().UpgradeValues.WebSocket)); } -Http1Settings -Utility::parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config) { - Http1Settings ret; - ret.allow_absolute_url_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, allow_absolute_url, true); - ret.accept_http_10_ = config.accept_http_10(); - ret.default_host_for_http_10_ = config.default_host_for_http_10(); - ret.enable_trailers_ = config.enable_trailers(); - ret.allow_chunked_length_ = config.allow_chunked_length(); - - if (config.header_key_format().has_proper_case_words()) { - ret.header_key_format_ = Http1Settings::HeaderKeyFormat::ProperCase; - } else { - ret.header_key_format_ = Http1Settings::HeaderKeyFormat::Default; - } - - return ret; -} - -Http1Settings -Utility::parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config, - const Protobuf::BoolValue& hcm_stream_error, bool validate_scheme) { - Http1Settings ret = parseHttp1Settings(config); - ret.validate_scheme_ = validate_scheme; - - if (config.has_override_stream_error_on_invalid_http_message()) { - // override_stream_error_on_invalid_http_message, if set, takes precedence over any HCM - // stream_error_on_invalid_http_message - ret.stream_error_on_invalid_http_message_ = - config.override_stream_error_on_invalid_http_message().value(); - } else { - // fallback to HCM value - ret.stream_error_on_invalid_http_message_ = hcm_stream_error.value(); - } - - return ret; -} - void Utility::sendLocalReply(const bool& is_reset, StreamDecoderFilterCallbacks& callbacks, const LocalReplyData& local_reply_data) { absl::string_view details; diff --git a/source/common/http/utility.h b/source/common/http/utility.h index 7d2a065bcb372..2af7e49e0a4b7 100644 --- a/source/common/http/utility.h +++ b/source/common/http/utility.h @@ -280,15 +280,6 @@ bool isH2UpgradeRequest(const RequestHeaderMap& headers); */ bool isWebSocketUpgradeRequest(const RequestHeaderMap& headers); -/** - * @return Http1Settings An Http1Settings populated from the - * envoy::config::core::v3::Http1ProtocolOptions config. - */ -Http1Settings parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config); - -Http1Settings parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config, - const Protobuf::BoolValue& hcm_stream_error, bool validate_scheme); - struct EncodeFunctions { // Function to modify locally generated response headers. std::function modify_headers_; diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index e46391a6fb43a..8d80b69346abd 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -696,7 +696,8 @@ class FactoryContextImpl : public Server::Configuration::CommonFactoryContext { std::shared_ptr createOptions(const envoy::config::cluster::v3::Cluster& config, - std::shared_ptr&& options) { + std::shared_ptr&& options, + ProtobufMessage::ValidationVisitor& validation_visitor) { if (options) { return std::move(options); } @@ -717,7 +718,7 @@ createOptions(const envoy::config::cluster::v3::Cluster& config, config.upstream_http_protocol_options()) : absl::nullopt), config.protocol_selection() == envoy::config::cluster::v3::Cluster::USE_DOWNSTREAM_PROTOCOL, - config.has_http2_protocol_options()); + config.has_http2_protocol_options(), validation_visitor); } ClusterInfoImpl::ClusterInfoImpl( @@ -730,8 +731,10 @@ ClusterInfoImpl::ClusterInfoImpl( type_(config.type()), extension_protocol_options_(parseExtensionProtocolOptions(config, factory_context)), http_protocol_options_( - createOptions(config, extensionProtocolOptionsTyped( - "envoy.extensions.upstreams.http.v3.HttpProtocolOptions"))), + createOptions(config, + extensionProtocolOptionsTyped( + "envoy.extensions.upstreams.http.v3.HttpProtocolOptions"), + factory_context.messageValidationVisitor())), max_requests_per_connection_( PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, max_requests_per_connection, 0)), max_response_headers_count_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index dcc15451a5abc..a56bb94927e5e 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -39,6 +39,7 @@ EXTENSIONS = { # # WASM # + "envoy.bootstrap.wasm": "//source/extensions/bootstrap/wasm:config", # @@ -213,6 +214,7 @@ EXTENSIONS = { # # Internal redirect predicates # + "envoy.internal_redirect_predicates.allow_listed_routes": "//source/extensions/internal_redirect/allow_listed_routes:config", "envoy.internal_redirect_predicates.previous_routes": "//source/extensions/internal_redirect/previous_routes:config", "envoy.internal_redirect_predicates.safe_cross_scheme": "//source/extensions/internal_redirect/safe_cross_scheme:config", @@ -220,6 +222,7 @@ EXTENSIONS = { # # Http Upstreams (excepting envoy.upstreams.http.generic which is hard-coded into the build so not registered here) # + "envoy.upstreams.http.http": "//source/extensions/upstreams/http/http:config", "envoy.upstreams.http.tcp": "//source/extensions/upstreams/http/tcp:config", @@ -255,6 +258,12 @@ EXTENSIONS = { # "envoy.tls.cert_validator.spiffe": "//source/extensions/transport_sockets/tls/cert_validator/spiffe:config", + + # + # HTTP header formatters + # + + "envoy.http.stateful_header_formatters.preserve_case": "//source/extensions/http/header_formatters/preserve_case:preserve_case_formatter" } # These can be changed to ["//visibility:public"], for downstream builds which diff --git a/source/extensions/filters/network/http_connection_manager/BUILD b/source/extensions/filters/network/http_connection_manager/BUILD index de7d4224163b9..0868898bc13cc 100644 --- a/source/extensions/filters/network/http_connection_manager/BUILD +++ b/source/extensions/filters/network/http_connection_manager/BUILD @@ -41,6 +41,7 @@ envoy_cc_extension( "//source/common/http:request_id_extension_lib", "//source/common/http:utility_lib", "//source/common/http/http1:codec_lib", + "//source/common/http/http1:settings_lib", "//source/common/http/http2:codec_lib", "//source/common/json:json_loader_lib", "//source/common/local_reply:local_reply_lib", diff --git a/source/extensions/filters/network/http_connection_manager/config.cc b/source/extensions/filters/network/http_connection_manager/config.cc index 6475de5f7ce12..37c69d72aa55c 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -24,6 +24,7 @@ #include "common/http/conn_manager_utility.h" #include "common/http/default_server_string.h" #include "common/http/http1/codec_impl.h" +#include "common/http/http1/settings.h" #include "common/http/http2/codec_impl.h" #include "common/http/http3/quic_codec_factory.h" #include "common/http/http3/well_known_names.h" @@ -207,8 +208,9 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( http2_options_(Http2::Utility::initializeAndValidateOptions( config.http2_protocol_options(), config.has_stream_error_on_invalid_http_message(), config.stream_error_on_invalid_http_message())), - http1_settings_(Http::Utility::parseHttp1Settings( - config.http_protocol_options(), config.stream_error_on_invalid_http_message(), + http1_settings_(Http::Http1::parseHttp1Settings( + config.http_protocol_options(), context.messageValidationVisitor(), + config.stream_error_on_invalid_http_message(), xff_num_trusted_hops_ == 0 && use_remote_address_)), max_request_headers_kb_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( config, max_request_headers_kb, Http::DEFAULT_MAX_REQUEST_HEADERS_KB)), diff --git a/source/extensions/http/header_formatters/preserve_case/BUILD b/source/extensions/http/header_formatters/preserve_case/BUILD new file mode 100644 index 0000000000000..6fde9d6725f30 --- /dev/null +++ b/source/extensions/http/header_formatters/preserve_case/BUILD @@ -0,0 +1,21 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_extension( + name = "preserve_case_formatter", + srcs = ["preserve_case_formatter.cc"], + hdrs = ["preserve_case_formatter.h"], + category = "envoy.http.stateful_header_formatters", + security_posture = "robust_to_untrusted_downstream_and_upstream", + deps = [ + "//include/envoy/registry", + "@envoy_api//envoy/extensions/http/header_formatters/preserve_case/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/http/header_formatters/preserve_case/preserve_case_formatter.cc b/source/extensions/http/header_formatters/preserve_case/preserve_case_formatter.cc new file mode 100644 index 0000000000000..11b1f8ed84e2d --- /dev/null +++ b/source/extensions/http/header_formatters/preserve_case/preserve_case_formatter.cc @@ -0,0 +1,67 @@ +#include "extensions/http/header_formatters/preserve_case/preserve_case_formatter.h" + +#include "envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.pb.h" +#include "envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.pb.validate.h" +#include "envoy/registry/registry.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace HeaderFormatters { +namespace PreserveCase { + +std::string PreserveCaseHeaderFormatter::format(absl::string_view key) const { + const auto remembered_key_itr = original_header_keys_.find(key); + // TODO(mattklein123): We can avoid string copies here if the formatter interface allowed us + // to return something like GetAllOfHeaderAsStringResult with both a string_view and an + // optional backing string. We can do this in a follow up if there is interest. + // TODO(mattklein123): This implementation does not cover headers added by Envoy that may need + // do be in a different case. We can handle this in the future by extending this formatter to + // have an "inner formatter" that would allow performing proper case (for example) on unknown + // headers. + if (remembered_key_itr != original_header_keys_.end()) { + return *remembered_key_itr; + } else { + return std::string(key); + } +} + +void PreserveCaseHeaderFormatter::processKey(absl::string_view key) { + // Note: This implementation will only remember the first instance of a particular header key. + // So for example "Foo" followed by "foo" will both be serialized as "Foo" on the way out. We + // could do better here but it's unlikely it's worth it and we can see if anyone complains about + // the implementation. + original_header_keys_.emplace(key); +} + +class PreserveCaseFormatterFactory : public Envoy::Http::StatefulHeaderKeyFormatterFactory { +public: + // Envoy::Http::StatefulHeaderKeyFormatterFactory + Envoy::Http::StatefulHeaderKeyFormatterPtr create() override { + return std::make_unique(); + } +}; + +class PreserveCaseFormatterFactoryConfig + : public Envoy::Http::StatefulHeaderKeyFormatterFactoryConfig { +public: + // Envoy::Http::StatefulHeaderKeyFormatterFactoryConfig + std::string name() const override { return "preserve_case"; } + Envoy::Http::StatefulHeaderKeyFormatterFactorySharedPtr + createFromProto(const Protobuf::Message&) override { + return std::make_shared(); + } + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } +}; + +REGISTER_FACTORY(PreserveCaseFormatterFactoryConfig, + Envoy::Http::StatefulHeaderKeyFormatterFactoryConfig); + +} // namespace PreserveCase +} // namespace HeaderFormatters +} // namespace Http +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/http/header_formatters/preserve_case/preserve_case_formatter.h b/source/extensions/http/header_formatters/preserve_case/preserve_case_formatter.h new file mode 100644 index 0000000000000..a9fa89df64266 --- /dev/null +++ b/source/extensions/http/header_formatters/preserve_case/preserve_case_formatter.h @@ -0,0 +1,27 @@ +#pragma once + +#include "envoy/http/header_formatter.h" + +#include "common/common/utility.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace HeaderFormatters { +namespace PreserveCase { + +class PreserveCaseHeaderFormatter : public Envoy::Http::StatefulHeaderKeyFormatter { +public: + // Envoy::Http::StatefulHeaderKeyFormatter + std::string format(absl::string_view key) const override; + void processKey(absl::string_view key) override; + +private: + StringUtil::CaseUnorderedSet original_header_keys_; +}; + +} // namespace PreserveCase +} // namespace HeaderFormatters +} // namespace Http +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/upstreams/http/BUILD b/source/extensions/upstreams/http/BUILD index 00657164e9d1e..198a0b12b4fc1 100644 --- a/source/extensions/upstreams/http/BUILD +++ b/source/extensions/upstreams/http/BUILD @@ -21,6 +21,7 @@ envoy_cc_extension( "//source/common/common:minimal_logger_lib", "//source/common/config:utility_lib", "//source/common/http:utility_lib", + "//source/common/http/http1:settings_lib", "//source/common/protobuf:utility_lib", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", diff --git a/source/extensions/upstreams/http/config.cc b/source/extensions/upstreams/http/config.cc index ba4a83ef82812..9822271413b30 100644 --- a/source/extensions/upstreams/http/config.cc +++ b/source/extensions/upstreams/http/config.cc @@ -10,6 +10,7 @@ #include "envoy/upstream/upstream.h" #include "common/config/utility.h" +#include "common/http/http1/settings.h" #include "common/http/utility.h" #include "common/protobuf/utility.h" @@ -75,8 +76,10 @@ uint64_t ProtocolOptionsConfigImpl::parseFeatures(const envoy::config::cluster:: } ProtocolOptionsConfigImpl::ProtocolOptionsConfigImpl( - const envoy::extensions::upstreams::http::v3::HttpProtocolOptions& options) - : http1_settings_(Envoy::Http::Utility::parseHttp1Settings(getHttpOptions(options))), + const envoy::extensions::upstreams::http::v3::HttpProtocolOptions& options, + ProtobufMessage::ValidationVisitor& validation_visitor) + : http1_settings_( + Envoy::Http::Http1::parseHttp1Settings(getHttpOptions(options), validation_visitor)), http2_options_(Http2::Utility::initializeAndValidateOptions(getHttp2Options(options))), http3_options_(getHttp3Options(options)), common_http_protocol_options_(options.common_http_protocol_options()), @@ -112,8 +115,9 @@ ProtocolOptionsConfigImpl::ProtocolOptionsConfigImpl( const envoy::config::core::v3::Http2ProtocolOptions& http2_options, const envoy::config::core::v3::HttpProtocolOptions& common_options, const absl::optional upstream_options, - bool use_downstream_protocol, bool use_http2) - : http1_settings_(Envoy::Http::Utility::parseHttp1Settings(http1_settings)), + bool use_downstream_protocol, bool use_http2, + ProtobufMessage::ValidationVisitor& validation_visitor) + : http1_settings_(Envoy::Http::Http1::parseHttp1Settings(http1_settings, validation_visitor)), http2_options_(Http2::Utility::initializeAndValidateOptions(http2_options)), common_http_protocol_options_(common_options), upstream_http_protocol_options_(upstream_options), diff --git a/source/extensions/upstreams/http/config.h b/source/extensions/upstreams/http/config.h index 9fd0141d7756e..5fcf134c68664 100644 --- a/source/extensions/upstreams/http/config.h +++ b/source/extensions/upstreams/http/config.h @@ -25,14 +25,16 @@ namespace Http { class ProtocolOptionsConfigImpl : public Upstream::ProtocolOptionsConfig { public: ProtocolOptionsConfigImpl( - const envoy::extensions::upstreams::http::v3::HttpProtocolOptions& options); + const envoy::extensions::upstreams::http::v3::HttpProtocolOptions& options, + ProtobufMessage::ValidationVisitor& validation_visitor); // Constructor for legacy (deprecated) config. ProtocolOptionsConfigImpl( const envoy::config::core::v3::Http1ProtocolOptions& http1_settings, const envoy::config::core::v3::Http2ProtocolOptions& http2_options, const envoy::config::core::v3::HttpProtocolOptions& common_options, const absl::optional upstream_options, - bool use_downstream_protocol, bool use_http2); + bool use_downstream_protocol, bool use_http2, + ProtobufMessage::ValidationVisitor& validation_visitor); // Given the supplied cluster config, and protocol options configuration, // returns a unit64_t representing the enabled Upstream::ClusterInfo::Features. @@ -60,7 +62,8 @@ class ProtocolOptionsConfigFactory : public Server::Configuration::ProtocolOptio const auto& typed_config = MessageUtil::downcastAndValidate< const envoy::extensions::upstreams::http::v3::HttpProtocolOptions&>( config, context.messageValidationVisitor()); - return std::make_shared(typed_config); + return std::make_shared(typed_config, + context.messageValidationVisitor()); } std::string category() const override { return "envoy.upstream_options"; } std::string name() const override { diff --git a/test/common/common/optref_test.cc b/test/common/common/optref_test.cc index 343d4506bba06..e1d441fc362e9 100644 --- a/test/common/common/optref_test.cc +++ b/test/common/common/optref_test.cc @@ -34,4 +34,22 @@ TEST(OptRefTest, Const) { EXPECT_EQ(5, optref->size()); } +class Foo {}; +class Bar : public Foo {}; + +TEST(OptRefTest, Conversion) { + Foo foo; + Bar bar; + OptRef foo_ref(foo); + OptRef bar_ref(bar); + + // Copy construct conversion. + OptRef converted_ref(bar); + OptRef converted_optref(bar_ref); + + // Assignment conversion. + foo_ref = bar; + foo_ref = bar_ref; +} + } // namespace Envoy diff --git a/test/common/http/utility_test.cc b/test/common/http/utility_test.cc index 46b5e8988e667..49cf81f341e27 100644 --- a/test/common/http/utility_test.cc +++ b/test/common/http/utility_test.cc @@ -9,6 +9,7 @@ #include "common/common/fmt.h" #include "common/http/exception.h" #include "common/http/header_map_impl.h" +#include "common/http/http1/settings.h" #include "common/http/utility.h" #include "common/network/address_impl.h" @@ -423,33 +424,34 @@ TEST(HttpUtility, ValidateStreamErrorsWithHcm) { TEST(HttpUtility, ValidateStreamErrorConfigurationForHttp1) { envoy::config::core::v3::Http1ProtocolOptions http1_options; Protobuf::BoolValue hcm_value; + NiceMock validation_visitor; // nothing explicitly configured, default to false (i.e. default stream error behavior for HCM) - EXPECT_FALSE(Utility::parseHttp1Settings(http1_options, hcm_value, false) + EXPECT_FALSE(Http1::parseHttp1Settings(http1_options, validation_visitor, hcm_value, false) .stream_error_on_invalid_http_message_); // http1_options.stream_error overrides HCM.stream_error http1_options.mutable_override_stream_error_on_invalid_http_message()->set_value(true); hcm_value.set_value(false); - EXPECT_TRUE(Utility::parseHttp1Settings(http1_options, hcm_value, false) + EXPECT_TRUE(Http1::parseHttp1Settings(http1_options, validation_visitor, hcm_value, false) .stream_error_on_invalid_http_message_); // http1_options.stream_error overrides HCM.stream_error (flip boolean value) http1_options.mutable_override_stream_error_on_invalid_http_message()->set_value(false); hcm_value.set_value(true); - EXPECT_FALSE(Utility::parseHttp1Settings(http1_options, hcm_value, false) + EXPECT_FALSE(Http1::parseHttp1Settings(http1_options, validation_visitor, hcm_value, false) .stream_error_on_invalid_http_message_); http1_options.clear_override_stream_error_on_invalid_http_message(); // fallback to HCM.stream_error hcm_value.set_value(true); - EXPECT_TRUE(Utility::parseHttp1Settings(http1_options, hcm_value, false) + EXPECT_TRUE(Http1::parseHttp1Settings(http1_options, validation_visitor, hcm_value, false) .stream_error_on_invalid_http_message_); // fallback to HCM.stream_error (flip boolean value) hcm_value.set_value(false); - EXPECT_FALSE(Utility::parseHttp1Settings(http1_options, hcm_value, false) + EXPECT_FALSE(Http1::parseHttp1Settings(http1_options, validation_visitor, hcm_value, false) .stream_error_on_invalid_http_message_); } diff --git a/test/extensions/http/header_formatters/preserve_case/BUILD b/test/extensions/http/header_formatters/preserve_case/BUILD new file mode 100644 index 0000000000000..bc19822cacd0a --- /dev/null +++ b/test/extensions/http/header_formatters/preserve_case/BUILD @@ -0,0 +1,35 @@ +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 = "preserve_case_formatter_test", + srcs = [ + "preserve_case_formatter_test.cc", + ], + extension_name = "envoy.http.stateful_header_formatters.preserve_case", + deps = [ + "//source/extensions/http/header_formatters/preserve_case:preserve_case_formatter", + ], +) + +envoy_extension_cc_test( + name = "preserve_case_formatter_integration_test", + srcs = [ + "preserve_case_formatter_integration_test.cc", + ], + extension_name = "envoy.http.stateful_header_formatters.preserve_case", + deps = [ + "//source/extensions/http/header_formatters/preserve_case:preserve_case_formatter", + "//test/integration:http_integration_lib", + ], +) diff --git a/test/extensions/http/header_formatters/preserve_case/preserve_case_formatter_integration_test.cc b/test/extensions/http/header_formatters/preserve_case/preserve_case_formatter_integration_test.cc new file mode 100644 index 0000000000000..4eb7b077795e4 --- /dev/null +++ b/test/extensions/http/header_formatters/preserve_case/preserve_case_formatter_integration_test.cc @@ -0,0 +1,107 @@ +#include "test/integration/filters/common.h" +#include "test/integration/http_integration.h" +#include "test/test_common/registry.h" + +namespace Envoy { +namespace { + +// Demonstrate using a filter to affect the case. +class PreserveCaseFilter : public Http::PassThroughFilter { +public: + constexpr static char name[] = "preserve-case-filter"; + + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, bool) override { + headers.addCopy(Http::LowerCaseString("request-header"), "request-header-value"); + headers.formatter()->processKey("Request-Header"); + return Http::FilterHeadersStatus::Continue; + } + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, bool) override { + headers.addCopy(Http::LowerCaseString("response-header"), "response-header-value"); + headers.formatter()->processKey("Response-Header"); + return Http::FilterHeadersStatus::Continue; + } +}; + +constexpr char PreserveCaseFilter::name[]; + +class PreserveCaseIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { +public: + PreserveCaseIntegrationTest() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam()), registration_(factory_) {} + + void initialize() override { + config_helper_.addConfigModifier([](envoy::extensions::filters::network:: + http_connection_manager::v3::HttpConnectionManager& + hcm) { + auto typed_extension_config = hcm.mutable_http_protocol_options() + ->mutable_header_key_format() + ->mutable_stateful_formatter(); + typed_extension_config->set_name("preserve_case"); + typed_extension_config->mutable_typed_config()->set_type_url( + "type.googleapis.com/" + "envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig"); + }); + + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + ConfigHelper::HttpProtocolOptions protocol_options; + auto typed_extension_config = protocol_options.mutable_explicit_http_config() + ->mutable_http_protocol_options() + ->mutable_header_key_format() + ->mutable_stateful_formatter(); + typed_extension_config->set_name("preserve_case"); + typed_extension_config->mutable_typed_config()->set_type_url( + "type.googleapis.com/" + "envoy.extensions.http.header_formatters.preserve_case.v3.PreserveCaseFormatterConfig"); + ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), + protocol_options); + }); + + HttpIntegrationTest::initialize(); + } + + SimpleFilterConfig factory_; + Registry::InjectFactory registration_; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, PreserveCaseIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Verify that we preserve case in both directions. +TEST_P(PreserveCaseIntegrationTest, EndToEnd) { + config_helper_.addFilter(R"EOF( + name: preserve-case-filter + )EOF"); + initialize(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("http")); + auto request = "GET / HTTP/1.1\r\nHOst: host\r\nMy-Request-Header: foo\r\n\r\n"; + ASSERT_TRUE(tcp_client->write(request, false)); + + Envoy::FakeRawConnectionPtr upstream_connection; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(upstream_connection)); + + // Verify that the upstream request has preserved cased headers. + std::string upstream_request; + EXPECT_TRUE(upstream_connection->waitForData(FakeRawConnection::waitForInexactMatch("GET /"), + &upstream_request)); + + EXPECT_TRUE(absl::StrContains(upstream_request, "My-Request-Header: foo")); + EXPECT_TRUE(absl::StrContains(upstream_request, "HOst: host")); + EXPECT_TRUE(absl::StrContains(upstream_request, "Request-Header: request-header-value")); + + // Verify that the downstream response has preserved cased headers. + auto response = + "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nMy-Response-Header: foo\r\n\r\n"; + ASSERT_TRUE(upstream_connection->write(response)); + + // Verify that downstream response has preserved case headers. + tcp_client->waitForData("Content-Length: 0", false); + tcp_client->waitForData("My-Response-Header: foo", false); + tcp_client->waitForData("Response-Header: response-header-value", false); + tcp_client->close(); +} + +} // namespace +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/http/header_formatters/preserve_case/preserve_case_formatter_test.cc b/test/extensions/http/header_formatters/preserve_case/preserve_case_formatter_test.cc new file mode 100644 index 0000000000000..997feb43ecb80 --- /dev/null +++ b/test/extensions/http/header_formatters/preserve_case/preserve_case_formatter_test.cc @@ -0,0 +1,29 @@ +#include "extensions/http/header_formatters/preserve_case/preserve_case_formatter.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Http { +namespace HeaderFormatters { +namespace PreserveCase { + +TEST(PreserveCaseFormatterTest, All) { + PreserveCaseHeaderFormatter formatter; + formatter.processKey("Foo"); + formatter.processKey("Bar"); + formatter.processKey("BAR"); + + EXPECT_EQ("Foo", formatter.format("foo")); + EXPECT_EQ("Foo", formatter.format("Foo")); + EXPECT_EQ("Bar", formatter.format("bar")); + EXPECT_EQ("Bar", formatter.format("Bar")); + EXPECT_EQ("Bar", formatter.format("BAR")); + EXPECT_EQ("baz", formatter.format("baz")); +} + +} // namespace PreserveCase +} // namespace HeaderFormatters +} // namespace Http +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/upstreams/http/BUILD b/test/extensions/upstreams/http/BUILD index 8b0781c2696e3..e8836ee766405 100644 --- a/test/extensions/upstreams/http/BUILD +++ b/test/extensions/upstreams/http/BUILD @@ -15,6 +15,7 @@ envoy_cc_test( "//source/common/upstream:upstream_includes", "//source/common/upstream:upstream_lib", "//source/extensions/upstreams/http:config", + "//test/mocks/protobuf:protobuf_mocks", "//test/test_common:utility_lib", ], ) diff --git a/test/extensions/upstreams/http/config_test.cc b/test/extensions/upstreams/http/config_test.cc index 3cdafaf73d719..ee7e055953313 100644 --- a/test/extensions/upstreams/http/config_test.cc +++ b/test/extensions/upstreams/http/config_test.cc @@ -1,8 +1,12 @@ #include "extensions/upstreams/http/config.h" +#include "test/mocks/protobuf/mocks.h" + #include "gmock/gmock.h" #include "gtest/gtest.h" +using testing::NiceMock; + namespace Envoy { namespace Extensions { namespace Upstreams { @@ -11,10 +15,11 @@ namespace Http { class ConfigTest : public ::testing::Test { public: envoy::extensions::upstreams::http::v3::HttpProtocolOptions options_; + NiceMock validation_visitor_; }; TEST_F(ConfigTest, Basic) { - ProtocolOptionsConfigImpl config(options_); + ProtocolOptionsConfigImpl config(options_, validation_visitor_); EXPECT_FALSE(config.use_downstream_protocol_); EXPECT_FALSE(config.use_http2_); } @@ -22,14 +27,14 @@ TEST_F(ConfigTest, Basic) { TEST_F(ConfigTest, Downstream) { options_.mutable_use_downstream_protocol_config(); { - ProtocolOptionsConfigImpl config(options_); + ProtocolOptionsConfigImpl config(options_, validation_visitor_); EXPECT_TRUE(config.use_downstream_protocol_); EXPECT_FALSE(config.use_http2_); } options_.mutable_use_downstream_protocol_config()->mutable_http2_protocol_options(); { - ProtocolOptionsConfigImpl config(options_); + ProtocolOptionsConfigImpl config(options_, validation_visitor_); EXPECT_TRUE(config.use_downstream_protocol_); EXPECT_TRUE(config.use_http2_); } diff --git a/test/test_common/utility.h b/test/test_common/utility.h index fd8f25ce68d90..9954298a9d267 100644 --- a/test/test_common/utility.h +++ b/test/test_common/utility.h @@ -1013,6 +1013,10 @@ template class TestHeaderMapImplBase : public Inte header_map_->verifyByteSizeInternalForTest(); return rc; } + StatefulHeaderKeyFormatterOptConstRef formatter() const override { + return StatefulHeaderKeyFormatterOptConstRef(header_map_->formatter()); + } + StatefulHeaderKeyFormatterOptRef formatter() override { return header_map_->formatter(); } std::unique_ptr header_map_{Impl::create()}; };