From f8b938cec803878c0295259683e2deb7127ad9a7 Mon Sep 17 00:00:00 2001 From: Matt Klein Date: Sat, 20 Mar 2021 19:32:49 +0000 Subject: [PATCH] http: add HTTP/1.1 case preservation 1) Add new stateful header formatter extension point 2) Add preserve case formatter extension Fixes https://github.com/envoyproxy/envoy/issues/14363 Signed-off-by: Matt Klein --- CODEOWNERS | 2 + api/BUILD | 1 + api/envoy/config/core/v3/protocol.proto | 7 ++ api/envoy/config/core/v4alpha/protocol.proto | 7 ++ .../header_formatters/preserve_case/v3/BUILD | 9 +++ .../preserve_case/v3/preserve_case.proto | 19 +++++ api/versioning/BUILD | 1 + bazel/envoy_library.bzl | 1 + docs/root/api-v3/config/config.rst | 1 + .../api-v3/config/http/header_formatters.rst | 8 ++ .../http_conn_man/_include/preserve-case.yaml | 50 ++++++++++++ .../http/http_conn_man/header_casing.rst | 44 +++++++++-- docs/root/version_history/current.rst | 1 + generated_api_shadow/BUILD | 1 + .../envoy/config/core/v3/protocol.proto | 7 ++ .../envoy/config/core/v4alpha/protocol.proto | 7 ++ .../header_formatters/preserve_case/v3/BUILD | 9 +++ .../preserve_case/v3/preserve_case.proto | 19 +++++ include/envoy/http/BUILD | 9 +++ include/envoy/http/codec.h | 3 +- include/envoy/http/header_formatter.h | 68 ++++++++++++++++ include/envoy/http/header_map.h | 8 ++ source/common/http/header_map_impl.h | 12 +++ source/common/http/http1/BUILD | 16 ++++ source/common/http/http1/codec_impl.cc | 71 +++++++++++------ source/common/http/http1/codec_impl.h | 48 ++++++------ source/common/http/http1/header_formatter.h | 16 +--- source/common/http/http1/settings.cc | 57 ++++++++++++++ source/common/http/http1/settings.h | 24 ++++++ source/common/http/utility.cc | 37 --------- source/common/http/utility.h | 9 --- source/common/upstream/upstream_impl.cc | 11 ++- source/extensions/extensions_build_config.bzl | 9 +++ .../network/http_connection_manager/BUILD | 1 + .../network/http_connection_manager/config.cc | 6 +- .../header_formatters/preserve_case/BUILD | 20 +++++ .../preserve_case/preserve_case_formatter.cc | 61 +++++++++++++++ source/extensions/upstreams/http/BUILD | 1 + source/extensions/upstreams/http/config.cc | 12 ++- source/extensions/upstreams/http/config.h | 9 ++- test/common/http/utility_test.cc | 12 +-- test/common/upstream/upstream_impl_test.cc | 3 +- .../header_formatters/preserve_case/BUILD | 24 ++++++ ...reserve_case_formatter_integration_test.cc | 78 +++++++++++++++++++ test/extensions/upstreams/http/BUILD | 1 + test/extensions/upstreams/http/config_test.cc | 11 ++- test/test_common/utility.h | 4 + 47 files changed, 700 insertions(+), 135 deletions(-) create mode 100644 api/envoy/extensions/http/header_formatters/preserve_case/v3/BUILD create mode 100644 api/envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.proto create mode 100644 docs/root/api-v3/config/http/header_formatters.rst create mode 100644 docs/root/configuration/http/http_conn_man/_include/preserve-case.yaml create mode 100644 generated_api_shadow/envoy/extensions/http/header_formatters/preserve_case/v3/BUILD create mode 100644 generated_api_shadow/envoy/extensions/http/header_formatters/preserve_case/v3/preserve_case.proto create mode 100644 include/envoy/http/header_formatter.h create mode 100644 source/common/http/http1/settings.cc create mode 100644 source/common/http/http1/settings.h create mode 100644 source/extensions/http/header_formatters/preserve_case/BUILD create mode 100644 source/extensions/http/header_formatters/preserve_case/preserve_case_formatter.cc create mode 100644 test/extensions/http/header_formatters/preserve_case/BUILD create mode 100644 test/extensions/http/header_formatters/preserve_case/preserve_case_formatter_integration_test.cc diff --git a/CODEOWNERS b/CODEOWNERS index 2c66321140f7..f888b7128d93 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -174,3 +174,5 @@ 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 diff --git a/api/BUILD b/api/BUILD index bc8c50266298..914eaa892aee 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 8c7693c8ab17..5ee7d5c696ac 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 + // effect the output of encoding headers. E.g., preserving case during proxying. + // [#extension-category: envoy.http.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 1f0af4d12922..1f1c9a1c128f 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 + // effect the output of encoding headers. E.g., preserving case during proxying. + // [#extension-category: envoy.http.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..904ebd9b95f8 --- /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.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 83e85ed89d5a..daadfd39ea67 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 75eb02258235..bc7378d49435 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.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 70a73db70ca8..84bbc17d48a3 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 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..f345f71d4804 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,40 @@ 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 different 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 to non-HTTP/1 to HTTP/1 or when stateful +formatting is not desired due to increases memory requirements. + +Stateful formatters +------------------- + +Stateful formatters are instantiated on decoding, passed every decoded header, attached to the +header map, and 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 0e9b565afbb1..ecf09ede307f 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -138,6 +138,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 bc8c50266298..914eaa892aee 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 8c7693c8ab17..5ee7d5c696ac 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 + // effect the output of encoding headers. E.g., preserving case during proxying. + // [#extension-category: envoy.http.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 131553b755df..d247ef2979f7 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 + // effect the output of encoding headers. E.g., preserving case during proxying. + // [#extension-category: envoy.http.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..904ebd9b95f8 --- /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.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/http/BUILD b/include/envoy/http/BUILD index fb857462c3aa..4c7057627b27 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", ], @@ -141,3 +142,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 651de40d4aba..ab17e0666e8e 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" @@ -436,7 +437,7 @@ struct Http1Settings { }; // How header keys should be formatted when serializing HTTP/1.1 headers. - HeaderKeyFormat header_key_format_{HeaderKeyFormat::Default}; + absl::variant header_key_format_; // Behaviour on invalid HTTP messaging: // - if true, the HTTP/1.1 connection is left open (where possible) diff --git a/include/envoy/http/header_formatter.h b/include/envoy/http/header_formatter.h new file mode 100644 index 000000000000..cc60d42c4e58 --- /dev/null +++ b/include/envoy/http/header_formatter.h @@ -0,0 +1,68 @@ +#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 rememberOriginalHeaderKey(absl::string_view key) PURE; +}; + +using StatefulHeaderKeyFormatterPtr = std::unique_ptr; +using StatefulHeaderKeyFormatterOptRef = OptRef; + +/** + * Interface for creating stateful header key formatters. + */ +class StatefulHeaderKeyFormatterFactory { +public: + virtual ~StatefulHeaderKeyFormatterFactory() = default; + + /** + * Create a new formatter. + */ + virtual StatefulHeaderKeyFormatterPtr create() PURE; +}; + +using StatefulHeaderKeyFormatterFactoryPtr = std::unique_ptr; + +/** + * Extension configuration for stateful header key formatters. + */ +class StatefulHeaderKeyFormatterFactoryConfig : public Config::TypedFactory { +public: + virtual StatefulHeaderKeyFormatterFactoryPtr + createFromProto(const Protobuf::Message& config) PURE; + + std::string category() const override { return "envoy.http.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 795452439f54..771076980c58 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,13 @@ class HeaderMap { headers.dumpState(os); return os; } + + /** + * Return the optional formatter attached to this header map. Filters can use the non-const + * version to remember additional header keys during operation if they wish. + */ + virtual HeaderKeyFormatterOptConstRef 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 1a87b2c4df71..53b2046fa01e 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; + HeaderKeyFormatterOptConstRef formatter() const { + return makeOptRefFromPtr(static_cast(formatter_.get())); + } + StatefulHeaderKeyFormatterOptRef formatter() { return makeOptRefFromPtr(formatter_.get()); } protected: struct HeaderEntryImpl : public HeaderEntry, NonCopyable { @@ -327,6 +332,7 @@ class HeaderMapImpl : NonCopyable { virtual HeaderEntryImpl** inlineHeaders() PURE; HeaderList headers_; + 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 +346,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 +404,8 @@ template class TypedHeaderMapImpl : public HeaderMapImpl, publ void dumpState(std::ostream& os, int indent_level = 0) const override { HeaderMapImpl::dumpState(os, indent_level); } + HeaderKeyFormatterOptConstRef 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 699ab767a9d0..ec54a693e91d 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( @@ -79,3 +82,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 7aa15888c9ed..5daae4cbec95 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" @@ -64,24 +65,33 @@ const StringUtil::CaseUnorderedSet& caseUnorderdSetContainingUpgradeAndHttp2Sett Http::Headers::get().ConnectionValues.Http2Settings); } -HeaderKeyFormatterPtr formatter(const Http::Http1Settings& settings) { - if (settings.header_key_format_ == Http1Settings::HeaderKeyFormat::ProperCase) { +HeaderKeyFormatterConstPtr encodeOnlyFormatterFromSettings(const Http::Http1Settings& settings) { + if (absl::holds_alternative(settings.header_key_format_) && + absl::get(settings.header_key_format_) == + Http1Settings::HeaderKeyFormat::ProperCase) { return std::make_unique(); } return nullptr; } + +StatefulHeaderKeyFormatterPtr statefulFormatterFromSettings(const Http::Http1Settings& settings) { + if (absl::holds_alternative(settings.header_key_format_)) { + return absl::get(settings.header_key_format_)->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 +112,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,8 +129,13 @@ 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(); + } + bool saw_content_length = false; - headers.iterate([this](const HeaderEntry& header) -> HeaderMap::Iterate { + headers.iterate([this, 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. @@ -133,7 +149,7 @@ void StreamEncoderImpl::encodeHeadersBase(const RequestOrResponseHeaderMap& head return HeaderMap::Iterate::Continue; } - encodeFormattedHeader(key_to_use, header.value().getStringView()); + encodeFormattedHeader(key_to_use, header.value().getStringView(), formatter); return HeaderMap::Iterate::Continue; }); @@ -172,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(Headers::get().ContentLength.get(), "0"); + encodeFormattedHeader(Headers::get().ContentLength.get(), "0", formatter); } } chunk_encoding_ = false; @@ -191,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(Headers::get().TransferEncoding.get(), - Headers::get().TransferEncodingValues.Chunked); + Headers::get().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,7 +263,8 @@ void StreamEncoderImpl::encodeTrailersBase(const HeaderMap& trailers) { connection_.buffer().add(LAST_CHUNK); 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; }); @@ -482,11 +499,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( @@ -508,11 +525,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->rememberOriginalHeaderKey(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_)); } @@ -866,7 +891,7 @@ Status ConnectionImpl::onMessageBeginBase() { protocol_ = Protocol::Http11; processing_trailers_ = false; header_parsing_state_ = HeaderParsingState::Field; - allocHeaders(); + allocHeaders(statefulFormatterFromSettings(codec_settings_)); return onMessageBegin(); } @@ -960,7 +985,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); @@ -1117,7 +1142,7 @@ Envoy::StatusOr ServerConnectionImpl::onHeadersC 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"); @@ -1253,7 +1278,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()) { @@ -1275,7 +1300,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 6cb1d994da9d..a47174033dd9 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/common/scope_tracker.h" #include "envoy/config/core/v3/protocol.pb.h" #include "envoy/http/codec.h" @@ -76,7 +77,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); @@ -115,9 +116,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_; }; @@ -126,9 +127,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_; } @@ -152,8 +152,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, - http_parser_type type, uint32_t max_headers_kb, const uint32_t max_headers_count, - HeaderKeyFormatterPtr&& header_key_formatter); + http_parser_type type, uint32_t max_headers_kb, const uint32_t max_headers_count); // The following define special return values for http_parser callbacks. See: // https://github.com/nodejs/http-parser/blob/5c5b3ac62662736de9e71640a8dc16da45b32503/http_parser.h#L72 @@ -280,11 +281,11 @@ class ConnectionImpl : public virtual Connection, Network::Connection& connection_; CodecStats& stats_; - const Http1Settings codec_settings_; + const Http1Settings& codec_settings_; http_parser 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; @@ -314,7 +315,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; /** @@ -496,8 +497,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_; @@ -545,10 +546,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_); @@ -595,9 +598,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_; @@ -628,10 +630,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..a0d221421013 --- /dev/null +++ b/source/common/http/http1/settings.cc @@ -0,0 +1,57 @@ +#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_ = 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 000000000000..8dfebf177cc3 --- /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 e2a39c3aaf51..a19973f80c9b 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 526fb701a4fd..c21eccc779b7 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, 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 4e4a52ecbe47..3749a303495f 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -674,7 +674,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); } @@ -695,7 +696,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( @@ -708,8 +709,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 a703b904bc16..795d1e2b28de 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", # @@ -220,6 +221,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", @@ -227,6 +229,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", @@ -262,6 +265,12 @@ EXTENSIONS = { # "envoy.tls.cert_validator.spiffe": "//source/extensions/transport_sockets/tls/cert_validator/spiffe:config", + + # + # HTTP header formatters + # + + "envoy.http.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 de7d4224163b..0868898bc13c 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 6475de5f7ce1..37c69d72aa55 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 000000000000..5db2d7adfcf9 --- /dev/null +++ b/source/extensions/http/header_formatters/preserve_case/BUILD @@ -0,0 +1,20 @@ +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"], + category = "envoy.http.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 000000000000..89b0fa23da21 --- /dev/null +++ b/source/extensions/http/header_formatters/preserve_case/preserve_case_formatter.cc @@ -0,0 +1,61 @@ +#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/http/header_formatter.h" +#include "envoy/registry/registry.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 { + const auto remembered_key = original_header_keys_.find(key); + if (remembered_key != original_header_keys_.end()) { + return *remembered_key; + } else { + return std::string(key); + } + } + void rememberOriginalHeaderKey(absl::string_view key) override { + original_header_keys_.emplace(key); + } + +private: + StringUtil::CaseUnorderedSet original_header_keys_; +}; + +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::StatefulHeaderKeyFormatterFactoryPtr + createFromProto(const Protobuf::Message&) override { + return std::make_unique(); + } + 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/upstreams/http/BUILD b/source/extensions/upstreams/http/BUILD index 00657164e9d1..198a0b12b4fc 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 ba4a83ef8281..9822271413b3 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 9fd0141d7756..5fcf134c6866 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/http/utility_test.cc b/test/common/http/utility_test.cc index 46b5e8988e66..49cf81f341e2 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/common/upstream/upstream_impl_test.cc b/test/common/upstream/upstream_impl_test.cc index a2ae9c022c3a..f60140dd12f4 100644 --- a/test/common/upstream/upstream_impl_test.cc +++ b/test/common/upstream/upstream_impl_test.cc @@ -358,7 +358,8 @@ TEST_F(StrictDnsClusterImplTest, Basic) { EXPECT_EQ(3U, cluster.info()->maxRequestsPerConnection()); EXPECT_EQ(0U, cluster.info()->http2Options().hpack_table_size().value()); EXPECT_EQ(Http::Http1Settings::HeaderKeyFormat::ProperCase, - cluster.info()->http1Settings().header_key_format_); + absl::get( + cluster.info()->http1Settings().header_key_format_)); cluster.info()->stats().upstream_rq_total_.inc(); EXPECT_EQ(1UL, stats_.counter("cluster.name.upstream_rq_total").value()); 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..8e7c613da222 --- /dev/null +++ b/test/extensions/http/header_formatters/preserve_case/BUILD @@ -0,0 +1,24 @@ +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 = "adaptive_concurrency_integration_test", + srcs = [ + "preserve_case_formatter_integration_test.cc", + ], + extension_name = "envoy.http.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..4a05325c76af --- /dev/null +++ b/test/extensions/http/header_formatters/preserve_case/preserve_case_formatter_integration_test.cc @@ -0,0 +1,78 @@ +#include "test/integration/http_integration.h" + +namespace Envoy { +namespace { + +class PreserveCaseIntegrationTest : public testing::TestWithParam, + public HttpIntegrationTest { +public: + PreserveCaseIntegrationTest() : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, GetParam()) {} + + 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(); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, PreserveCaseIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Verify that we preserve case in both directions. +TEST_P(PreserveCaseIntegrationTest, EndToEnd) { + initialize(); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("http")); + auto request = "GET / HTTP/1.1\r\nHOst: host\r\nMy-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-Header: foo")); + EXPECT_TRUE(absl::StrContains(upstream_request, "HOst: host")); + + // Verify that the downstream response has proper cased headers. + auto response = + "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nResponse-Header: foo\r\n\r\n"; + ASSERT_TRUE(upstream_connection->write(response)); + + // Verify that downstream response has preserved case headers. + std::string downstream_response; + tcp_client->waitForData("Content-Length: 0", false); + tcp_client->waitForData("Response-Header: foo", false); + tcp_client->close(); +} + +} // namespace +} // namespace Envoy \ No newline at end of file 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 fd8f25ce68d9..bf73443f439f 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; } + HeaderKeyFormatterOptConstRef formatter() const override { + return const_cast(header_map_.get())->formatter(); + } + StatefulHeaderKeyFormatterOptRef formatter() override { return header_map_->formatter(); } std::unique_ptr header_map_{Impl::create()}; };