diff --git a/api/BUILD b/api/BUILD index 6b8fc64edab1..6b32d7600ced 100644 --- a/api/BUILD +++ b/api/BUILD @@ -235,6 +235,7 @@ proto_library( "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", "//envoy/extensions/filters/udp/dns_filter/v3alpha:pkg", "//envoy/extensions/filters/udp/udp_proxy/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 c294370366df..79b1a60f6bb8 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"; @@ -111,6 +112,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"; @@ -129,6 +131,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 e33d83442afc..4f7ca2d8e5f2 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"; @@ -111,6 +112,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"; @@ -129,6 +131,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 000000000000..ee92fb652582 --- /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 000000000000..64bdd497ecab --- /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 6f6762722148..bc4f8ad044b8 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -118,6 +118,7 @@ proto_library( "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", "//envoy/extensions/filters/udp/dns_filter/v3alpha:pkg", "//envoy/extensions/filters/udp/udp_proxy/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/docs/root/api-v3/config/config.rst b/docs/root/api-v3/config/config.rst index c1240c53af72..88110dd3a55b 100644 --- a/docs/root/api-v3/config/config.rst +++ b/docs/root/api-v3/config/config.rst @@ -25,3 +25,4 @@ Extensions wasm/wasm watchdog/watchdog descriptors/descriptors + 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 000000000000..1518defddb38 --- /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 000000000000..cf637b1eeb7a --- /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 7795829b66b8..ad56ec71b268 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 e1a8982658e1..442c1f4622d7 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -33,6 +33,7 @@ Removed Config or Runtime New Features ------------ +* http: added the ability to preserve HTTP/1 header case across the proxy. See the :ref:`header casing ` documentation for more information. * listener: added an option when balancing across active listeners and wildcard matching is used to return the listener that matches the IP family type associated with the listener's socket address. It is off by default, but is turned on by default in v1.19. To set change the runtime guard `envoy.reloadable_features.listener_wildcard_match_ip_family` to true. * upstream: added support for :ref:`slow start mode `, which allows to progresively increase traffic for new endpoints. * upstream: extended :ref:`Round Robin load balancer configuration ` with :ref:`slow start ` support. diff --git a/generated_api_shadow/BUILD b/generated_api_shadow/BUILD index c93cdaf5db22..113216bc613f 100644 --- a/generated_api_shadow/BUILD +++ b/generated_api_shadow/BUILD @@ -180,6 +180,7 @@ proto_library( "//envoy/extensions/filters/network/thrift_proxy/filters/ratelimit/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/v3:pkg", "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", + "//envoy/extensions/http/header_formatters/preserve_case/v3:pkg", "//envoy/extensions/retry/host/omit_host_metadata/v3:pkg", "//envoy/extensions/retry/priority/previous_priorities/v3:pkg", "//envoy/extensions/transport_sockets/alts/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 c294370366df..79b1a60f6bb8 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"; @@ -111,6 +112,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"; @@ -129,6 +131,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 2063d6d6793e..ed05c8162a49 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"; @@ -111,6 +112,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"; @@ -129,6 +131,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 000000000000..ee92fb652582 --- /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 000000000000..64bdd497ecab --- /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 f96654f5938a..10d4cbacbfbb 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 a350d27f3e61..866992d03a37 100644 --- a/include/envoy/http/BUILD +++ b/include/envoy/http/BUILD @@ -103,6 +103,7 @@ envoy_cc_library( "abseil_inlined_vector", ], deps = [ + ":header_formatter_interface", "//source/common/common:assert_lib", "//source/common/common:hash_lib", ], @@ -140,3 +141,11 @@ envoy_cc_library( ":header_map_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 9ffa360bd72e..e44428aebed3 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" @@ -396,11 +397,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 000000000000..c30f4c459ee4 --- /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 e3262e3ecfb0..bd3e187c9538 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" @@ -610,6 +611,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 375abeca3658..18964720c4de 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" @@ -96,6 +97,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 { @@ -323,6 +328,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; }; @@ -334,6 +344,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); } @@ -388,6 +402,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 8c864eaca715..b0d345eb72c0 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( @@ -77,3 +80,16 @@ envoy_cc_library( "//source/common/upstream:upstream_lib", ], ) + +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", + ], +) diff --git a/source/common/http/http1/codec_impl.cc b/source/common/http/http1/codec_impl.cc index f0ce0cfb5f88..d5945b7a9fab 100644 --- a/source/common/http/http1/codec_impl.cc +++ b/source/common/http/http1/codec_impl.cc @@ -5,6 +5,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" @@ -60,7 +61,7 @@ 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(); } @@ -68,17 +69,23 @@ HeaderKeyFormatterPtr formatter(const Http::Http1Settings& settings) { 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_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(); } @@ -99,9 +106,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); } @@ -114,25 +122,32 @@ void ResponseEncoderImpl::encode100ContinueHeaders(const ResponseHeaderMap& head void StreamEncoderImpl::encodeHeadersBase(const RequestOrResponseHeaderMap& headers, absl::optional status, bool end_stream) { + 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](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(Headers::get().HostLegacy.get()); - key_size_to_use = Headers::get().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; @@ -163,7 +178,7 @@ void StreamEncoderImpl::encodeHeadersBase(const RequestOrResponseHeaderMap& head // For 204s and 1xx where content length is disallowed, don't append the content length but // also don't chunk encode. if (!status || (*status >= 200 && *status != 204)) { - encodeFormattedHeader(Headers::get().ContentLength.get(), "0"); + encodeFormattedHeader(header_values.ContentLength.get(), "0", formatter); } chunk_encoding_ = false; } else if (connection_.protocol() == Protocol::Http10) { @@ -180,8 +195,8 @@ void StreamEncoderImpl::encodeHeadersBase(const RequestOrResponseHeaderMap& head // For responses to connect requests, do not send the chunked encoding header: // https://tools.ietf.org/html/rfc7231#section-4.3.6. if (!is_response_to_connect_request_) { - encodeFormattedHeader(Headers::get().TransferEncoding.get(), - Headers::get().TransferEncodingValues.Chunked); + encodeFormattedHeader(header_values.TransferEncoding.get(), + 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 @@ -236,8 +251,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; }); @@ -470,11 +487,11 @@ http_parser_settings ConnectionImpl::settings_{ ConnectionImpl::ConnectionImpl(Network::Connection& connection, CodecStats& stats, const Http1Settings& settings, http_parser_type 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( @@ -496,11 +513,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_)); } @@ -851,7 +876,7 @@ Status ConnectionImpl::onMessageBeginBase() { protocol_ = Protocol::Http11; processing_trailers_ = false; header_parsing_state_ = HeaderParsingState::Field; - allocHeaders(); + allocHeaders(statefulFormatterFromSettings(codec_settings_)); return onMessageBegin(); } @@ -868,7 +893,7 @@ ServerConnectionImpl::ServerConnectionImpl( envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction headers_with_underscores_action) : ConnectionImpl(connection, stats, settings, HTTP_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); @@ -1009,7 +1034,7 @@ Envoy::StatusOr ServerConnectionImpl::onHeadersComplete() { Status ServerConnectionImpl::onMessageBegin() { 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"); @@ -1167,7 +1192,7 @@ ClientConnectionImpl::ClientConnectionImpl(Network::Connection& connection, Code ConnectionCallbacks&, const Http1Settings& settings, const uint32_t max_response_headers_count) : ConnectionImpl(connection, stats, settings, HTTP_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()) { @@ -1189,7 +1214,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 deb08526a3ad..bfdfcb80c17b 100644 --- a/source/common/http/http1/codec_impl.h +++ b/source/common/http/http1/codec_impl.h @@ -8,6 +8,7 @@ #include #include +#include "envoy/common/optref.h" #include "envoy/config/core/v3/protocol.pb.h" #include "envoy/http/codec.h" #include "envoy/network/connection.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); void encodeTrailersBase(const HeaderMap& headers); @@ -113,9 +114,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_; }; @@ -124,9 +125,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_; } @@ -150,8 +150,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_; } @@ -212,6 +211,9 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable(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_); @@ -573,9 +576,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_; @@ -606,10 +608,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 d99dc79cc741..f99026148408 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 000000000000..1d2ad0a5457b --- /dev/null +++ b/source/common/http/http1/settings.cc @@ -0,0 +1,56 @@ +#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) { + Http1Settings ret = parseHttp1Settings(config, validation_visitor); + + 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 diff --git a/source/common/http/http1/settings.h b/source/common/http/http1/settings.h new file mode 100644 index 000000000000..42b0506ddad0 --- /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); + +} // namespace Http1 +} // namespace Http +} // namespace Envoy diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index 1c11e101303d..8ab01ad2b446 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -467,42 +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) { - Http1Settings ret = parseHttp1Settings(config); - - 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 492b7e7d47db..ea5e84686fa7 100644 --- a/source/common/http/utility.h +++ b/source/common/http/utility.h @@ -279,15 +279,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); - 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 2bc898e45e87..80ac768df79c 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -670,7 +670,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); } @@ -691,7 +692,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( @@ -702,8 +703,10 @@ ClusterInfoImpl::ClusterInfoImpl( : runtime_(runtime), name_(config.name()), 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 1fa9f6293ea7..2ef4362a6006 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -34,6 +34,7 @@ EXTENSIONS = { # # WASM # + "envoy.bootstrap.wasm": "//source/extensions/bootstrap/wasm:config", # @@ -205,6 +206,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", @@ -212,6 +214,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", @@ -235,6 +238,12 @@ EXTENSIONS = { # "envoy.rate_limit_descriptors.expr": "//source/extensions/rate_limit_descriptors/expr: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 db02c5750db8..cfae2dba0057 100644 --- a/source/extensions/filters/network/http_connection_manager/BUILD +++ b/source/extensions/filters/network/http_connection_manager/BUILD @@ -39,6 +39,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 4c2cedfbe8c3..9a5dda4fcb3b 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -23,6 +23,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" @@ -251,8 +252,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())), max_request_headers_kb_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( config, max_request_headers_kb, Http::DEFAULT_MAX_REQUEST_HEADERS_KB)), max_request_headers_count_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( 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 000000000000..ec4c59cc85ab --- /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"], + security_posture = "robust_to_untrusted_downstream_and_upstream", + deps = [ + "//include/envoy/registry", + "//source/common/http/http1:header_formatter_lib", + "@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 000000000000..2205d9449d25 --- /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 proper_case_header_key_formatter_.format(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 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 000000000000..bc8e89618b9e --- /dev/null +++ b/source/extensions/http/header_formatters/preserve_case/preserve_case_formatter.h @@ -0,0 +1,29 @@ +#pragma once + +#include "envoy/http/header_formatter.h" + +#include "common/common/utility.h" +#include "common/http/http1/header_formatter.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_; + Envoy::Http::Http1::ProperCaseHeaderKeyFormatter proper_case_header_key_formatter_; +}; + +} // namespace PreserveCase +} // namespace HeaderFormatters +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/upstreams/http/BUILD b/source/extensions/upstreams/http/BUILD index cfcf0b407f97..3139cf5bc2a0 100644 --- a/source/extensions/upstreams/http/BUILD +++ b/source/extensions/upstreams/http/BUILD @@ -20,6 +20,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 6d8a8090c738..3e156adff74f 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" @@ -74,8 +75,10 @@ ProtocolOptionsConfigImpl::parseFeatures(const envoy::config::cluster::v3::Clust } 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))), common_http_protocol_options_(options.common_http_protocol_options()), upstream_http_protocol_options_( @@ -104,8 +107,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 cb4a1cb0dca9..ca5a0c4008e4 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. @@ -58,7 +60,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 343d4506bba0..e1d441fc362e 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 846da59adc13..3c1a5417e3f3 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,34 +424,35 @@ 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).stream_error_on_invalid_http_message_); + EXPECT_FALSE(Http1::parseHttp1Settings(http1_options, validation_visitor, hcm_value) + .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).stream_error_on_invalid_http_message_); + EXPECT_TRUE(Http1::parseHttp1Settings(http1_options, validation_visitor, hcm_value) + .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).stream_error_on_invalid_http_message_); + EXPECT_FALSE(Http1::parseHttp1Settings(http1_options, validation_visitor, hcm_value) + .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).stream_error_on_invalid_http_message_); + EXPECT_TRUE(Http1::parseHttp1Settings(http1_options, validation_visitor, hcm_value) + .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).stream_error_on_invalid_http_message_); + EXPECT_FALSE(Http1::parseHttp1Settings(http1_options, validation_visitor, hcm_value) + .stream_error_on_invalid_http_message_); } TEST(HttpUtility, getLastAddressFromXFF) { 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 000000000000..bc19822cacd0 --- /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 000000000000..314a33c7ae06 --- /dev/null +++ b/test/extensions/http/header_formatters/preserve_case/preserve_case_formatter_integration_test.cc @@ -0,0 +1,111 @@ +#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"); + headers.addCopy(Http::LowerCaseString("x-forwarded-for"), "x-forwarded-for-value"); + 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"); + headers.addCopy(Http::LowerCaseString("server"), "server-value"); + 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")); + EXPECT_TRUE(absl::StrContains(upstream_request, "X-Forwarded-For: 1.2.3.4")); + + // 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->waitForData("Server: server-value", false); + tcp_client->close(); +} + +} // namespace +} // namespace Envoy 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 000000000000..e88384336e88 --- /dev/null +++ b/test/extensions/http/header_formatters/preserve_case/preserve_case_formatter_test.cc @@ -0,0 +1,31 @@ +#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")); + EXPECT_EQ("HeLLO", formatter.format("heLLO")); + EXPECT_EQ("Hello-World", formatter.format("hello-world")); +} + +} // namespace PreserveCase +} // namespace HeaderFormatters +} // namespace Http +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/upstreams/http/BUILD b/test/extensions/upstreams/http/BUILD index 8b0781c2696e..e8836ee76640 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 3cdafaf73d71..ee7e05595331 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 7580a576e56d..bbe39d406c1b 100644 --- a/test/test_common/utility.h +++ b/test/test_common/utility.h @@ -990,6 +990,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()}; };