diff --git a/api/envoy/config/bootstrap/v3/bootstrap.proto b/api/envoy/config/bootstrap/v3/bootstrap.proto index b29e8e9c24e13..0e8de36633354 100644 --- a/api/envoy/config/bootstrap/v3/bootstrap.proto +++ b/api/envoy/config/bootstrap/v3/bootstrap.proto @@ -40,7 +40,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // ` for more detail. // Bootstrap :ref:`configuration overview `. -// [#next-free-field: 32] +// [#next-free-field: 33] message Bootstrap { option (udpa.annotations.versioning).previous_message_type = "envoy.config.bootstrap.v2.Bootstrap"; @@ -322,6 +322,13 @@ message Bootstrap { // field. // [#not-implemented-hide:] map certificate_provider_instances = 25; + + // Specifies a set of headers that need to be registered as inline header. This configuration + // allows users to customize the inline headers on-demand at Envoy startup without modifying + // Envoy's source code. + // + // Note that the 'set-cookie' header cannot be registered as inline header. + repeated CustomInlineHeader inline_headers = 32; } // Administration interface :ref:`operations documentation @@ -595,3 +602,43 @@ message LayeredRuntime { // such that later layers in the list overlay earlier entries. repeated RuntimeLayer layers = 1; } + +// Used to specify the header that needs to be registered as an inline header. +// +// If request or response contain multiple headers with the same name and the header +// name is registered as an inline header. Then multiple headers will be folded +// into one, and multiple header values will be concatenated by a suitable delimiter. +// The delimiter is generally a comma. +// +// For example, if 'foo' is registered as an inline header, and the headers contains +// the following two headers: +// +// .. code-block:: text +// +// foo: bar +// foo: eep +// +// Then they will eventually be folded into: +// +// .. code-block:: text +// +// foo: bar, eep +// +// Inline headers provide O(1) search performance, but each inline header imposes +// an additional memory overhead on all instances of the corresponding type of +// HeaderMap or TrailerMap. +message CustomInlineHeader { + enum InlineHeaderType { + REQUEST_HEADER = 0; + REQUEST_TRAILER = 1; + RESPONSE_HEADER = 2; + RESPONSE_TRAILER = 3; + } + + // The name of the header that is expected to be set as the inline header. + string inline_header_name = 1 + [(validate.rules).string = {min_len: 1 well_known_regex: HTTP_HEADER_NAME strict: false}]; + + // The type of the header that is expected to be set as the inline header. + InlineHeaderType inline_header_type = 2 [(validate.rules).enum = {defined_only: true}]; +} diff --git a/api/envoy/config/bootstrap/v4alpha/bootstrap.proto b/api/envoy/config/bootstrap/v4alpha/bootstrap.proto index 1bcc2ff78e9e1..5c45b8f7dbce9 100644 --- a/api/envoy/config/bootstrap/v4alpha/bootstrap.proto +++ b/api/envoy/config/bootstrap/v4alpha/bootstrap.proto @@ -37,7 +37,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // ` for more detail. // Bootstrap :ref:`configuration overview `. -// [#next-free-field: 32] +// [#next-free-field: 33] message Bootstrap { option (udpa.annotations.versioning).previous_message_type = "envoy.config.bootstrap.v3.Bootstrap"; @@ -289,6 +289,13 @@ message Bootstrap { // field. // [#not-implemented-hide:] map certificate_provider_instances = 25; + + // Specifies a set of headers that need to be registered as inline header. This configuration + // allows users to customize the inline headers on-demand at Envoy startup without modifying + // Envoy's source code. + // + // Note that the 'set-cookie' header cannot be registered as inline header. + repeated CustomInlineHeader inline_headers = 32; } // Administration interface :ref:`operations documentation @@ -568,3 +575,46 @@ message LayeredRuntime { // such that later layers in the list overlay earlier entries. repeated RuntimeLayer layers = 1; } + +// Used to specify the header that needs to be registered as an inline header. +// +// If request or response contain multiple headers with the same name and the header +// name is registered as an inline header. Then multiple headers will be folded +// into one, and multiple header values will be concatenated by a suitable delimiter. +// The delimiter is generally a comma. +// +// For example, if 'foo' is registered as an inline header, and the headers contains +// the following two headers: +// +// .. code-block:: text +// +// foo: bar +// foo: eep +// +// Then they will eventually be folded into: +// +// .. code-block:: text +// +// foo: bar, eep +// +// Inline headers provide O(1) search performance, but each inline header imposes +// an additional memory overhead on all instances of the corresponding type of +// HeaderMap or TrailerMap. +message CustomInlineHeader { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.bootstrap.v3.CustomInlineHeader"; + + enum InlineHeaderType { + REQUEST_HEADER = 0; + REQUEST_TRAILER = 1; + RESPONSE_HEADER = 2; + RESPONSE_TRAILER = 3; + } + + // The name of the header that is expected to be set as the inline header. + string inline_header_name = 1 + [(validate.rules).string = {min_len: 1 well_known_regex: HTTP_HEADER_NAME strict: false}]; + + // The type of the header that is expected to be set as the inline header. + InlineHeaderType inline_header_type = 2 [(validate.rules).enum = {defined_only: true}]; +} diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index b5dc10eafef2e..14895a9b95d7c 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -28,6 +28,7 @@ Removed Config or Runtime New Features ------------ +* bootstrap: added :ref:`inline_headers ` in the bootstrap to make custom inline headers bootstrap configurable. * http: added :ref:`string_match ` in the header matcher. Deprecated diff --git a/generated_api_shadow/envoy/config/bootstrap/v3/bootstrap.proto b/generated_api_shadow/envoy/config/bootstrap/v3/bootstrap.proto index 813cfa9e2a595..9171d066a4302 100644 --- a/generated_api_shadow/envoy/config/bootstrap/v3/bootstrap.proto +++ b/generated_api_shadow/envoy/config/bootstrap/v3/bootstrap.proto @@ -40,7 +40,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // ` for more detail. // Bootstrap :ref:`configuration overview `. -// [#next-free-field: 32] +// [#next-free-field: 33] message Bootstrap { option (udpa.annotations.versioning).previous_message_type = "envoy.config.bootstrap.v2.Bootstrap"; @@ -321,6 +321,13 @@ message Bootstrap { // [#not-implemented-hide:] map certificate_provider_instances = 25; + // Specifies a set of headers that need to be registered as inline header. This configuration + // allows users to customize the inline headers on-demand at Envoy startup without modifying + // Envoy's source code. + // + // Note that the 'set-cookie' header cannot be registered as inline header. + repeated CustomInlineHeader inline_headers = 32; + Runtime hidden_envoy_deprecated_runtime = 11 [ deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0", @@ -599,3 +606,43 @@ message LayeredRuntime { // such that later layers in the list overlay earlier entries. repeated RuntimeLayer layers = 1; } + +// Used to specify the header that needs to be registered as an inline header. +// +// If request or response contain multiple headers with the same name and the header +// name is registered as an inline header. Then multiple headers will be folded +// into one, and multiple header values will be concatenated by a suitable delimiter. +// The delimiter is generally a comma. +// +// For example, if 'foo' is registered as an inline header, and the headers contains +// the following two headers: +// +// .. code-block:: text +// +// foo: bar +// foo: eep +// +// Then they will eventually be folded into: +// +// .. code-block:: text +// +// foo: bar, eep +// +// Inline headers provide O(1) search performance, but each inline header imposes +// an additional memory overhead on all instances of the corresponding type of +// HeaderMap or TrailerMap. +message CustomInlineHeader { + enum InlineHeaderType { + REQUEST_HEADER = 0; + REQUEST_TRAILER = 1; + RESPONSE_HEADER = 2; + RESPONSE_TRAILER = 3; + } + + // The name of the header that is expected to be set as the inline header. + string inline_header_name = 1 + [(validate.rules).string = {min_len: 1 well_known_regex: HTTP_HEADER_NAME strict: false}]; + + // The type of the header that is expected to be set as the inline header. + InlineHeaderType inline_header_type = 2 [(validate.rules).enum = {defined_only: true}]; +} diff --git a/generated_api_shadow/envoy/config/bootstrap/v4alpha/bootstrap.proto b/generated_api_shadow/envoy/config/bootstrap/v4alpha/bootstrap.proto index 6e4fc1a4d8ff6..b21acabe686fc 100644 --- a/generated_api_shadow/envoy/config/bootstrap/v4alpha/bootstrap.proto +++ b/generated_api_shadow/envoy/config/bootstrap/v4alpha/bootstrap.proto @@ -39,7 +39,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // ` for more detail. // Bootstrap :ref:`configuration overview `. -// [#next-free-field: 32] +// [#next-free-field: 33] message Bootstrap { option (udpa.annotations.versioning).previous_message_type = "envoy.config.bootstrap.v3.Bootstrap"; @@ -318,6 +318,13 @@ message Bootstrap { // field. // [#not-implemented-hide:] map certificate_provider_instances = 25; + + // Specifies a set of headers that need to be registered as inline header. This configuration + // allows users to customize the inline headers on-demand at Envoy startup without modifying + // Envoy's source code. + // + // Note that the 'set-cookie' header cannot be registered as inline header. + repeated CustomInlineHeader inline_headers = 32; } // Administration interface :ref:`operations documentation @@ -600,3 +607,46 @@ message LayeredRuntime { // such that later layers in the list overlay earlier entries. repeated RuntimeLayer layers = 1; } + +// Used to specify the header that needs to be registered as an inline header. +// +// If request or response contain multiple headers with the same name and the header +// name is registered as an inline header. Then multiple headers will be folded +// into one, and multiple header values will be concatenated by a suitable delimiter. +// The delimiter is generally a comma. +// +// For example, if 'foo' is registered as an inline header, and the headers contains +// the following two headers: +// +// .. code-block:: text +// +// foo: bar +// foo: eep +// +// Then they will eventually be folded into: +// +// .. code-block:: text +// +// foo: bar, eep +// +// Inline headers provide O(1) search performance, but each inline header imposes +// an additional memory overhead on all instances of the corresponding type of +// HeaderMap or TrailerMap. +message CustomInlineHeader { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.bootstrap.v3.CustomInlineHeader"; + + enum InlineHeaderType { + REQUEST_HEADER = 0; + REQUEST_TRAILER = 1; + RESPONSE_HEADER = 2; + RESPONSE_TRAILER = 3; + } + + // The name of the header that is expected to be set as the inline header. + string inline_header_name = 1 + [(validate.rules).string = {min_len: 1 well_known_regex: HTTP_HEADER_NAME strict: false}]; + + // The type of the header that is expected to be set as the inline header. + InlineHeaderType inline_header_type = 2 [(validate.rules).enum = {defined_only: true}]; +} diff --git a/source/server/BUILD b/source/server/BUILD index 9c893e5444288..ebde406556b5e 100644 --- a/source/server/BUILD +++ b/source/server/BUILD @@ -515,6 +515,7 @@ envoy_cc_library( "//source/common/grpc:context_lib", "//source/common/http:codes_lib", "//source/common/http:context_lib", + "//source/common/http:headers_lib", "//source/common/init:manager_lib", "//source/common/local_info:local_info_lib", "//source/common/memory:heap_shrinker_lib", diff --git a/source/server/server.cc b/source/server/server.cc index 81b06d0fcff84..f2ffe578f48cd 100644 --- a/source/server/server.cc +++ b/source/server/server.cc @@ -33,6 +33,7 @@ #include "source/common/config/version_converter.h" #include "source/common/config/xds_resource.h" #include "source/common/http/codes.h" +#include "source/common/http/headers.h" #include "source/common/local_info/local_info_impl.h" #include "source/common/memory/stats.h" #include "source/common/network/address_impl.h" @@ -292,6 +293,46 @@ void loadBootstrap(absl::optional bootstrap_version, throw EnvoyException(fmt::format("Unknown bootstrap version {}.", *bootstrap_version)); } } + +bool canBeRegisteredAsInlineHeader(const Http::LowerCaseString& header_name) { + // 'set-cookie' cannot currently be registered as an inline header. + if (header_name == Http::Headers::get().SetCookie) { + return false; + } + return true; +} + +void registerCustomInlineHeadersFromBootstrap( + const envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + for (const auto& inline_header : bootstrap.inline_headers()) { + const Http::LowerCaseString lower_case_name(inline_header.inline_header_name()); + if (!canBeRegisteredAsInlineHeader(lower_case_name)) { + throw EnvoyException(fmt::format("Header {} cannot be registered as an inline header.", + inline_header.inline_header_name())); + } + switch (inline_header.inline_header_type()) { + case envoy::config::bootstrap::v3::CustomInlineHeader::REQUEST_HEADER: + Http::CustomInlineHeaderRegistry::registerInlineHeader< + Http::RequestHeaderMap::header_map_type>(lower_case_name); + break; + case envoy::config::bootstrap::v3::CustomInlineHeader::REQUEST_TRAILER: + Http::CustomInlineHeaderRegistry::registerInlineHeader< + Http::RequestTrailerMap::header_map_type>(lower_case_name); + break; + case envoy::config::bootstrap::v3::CustomInlineHeader::RESPONSE_HEADER: + Http::CustomInlineHeaderRegistry::registerInlineHeader< + Http::ResponseHeaderMap::header_map_type>(lower_case_name); + break; + case envoy::config::bootstrap::v3::CustomInlineHeader::RESPONSE_TRAILER: + Http::CustomInlineHeaderRegistry::registerInlineHeader< + Http::ResponseTrailerMap::header_map_type>(lower_case_name); + break; + default: + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + } +} + } // namespace void InstanceUtil::loadBootstrapConfig(envoy::config::bootstrap::v3::Bootstrap& bootstrap, @@ -353,8 +394,10 @@ void InstanceImpl::initialize(const Options& options, // setPrefix has a release assert verifying that setPrefix() is not called after prefix() ThreadSafeSingleton::get().setPrefix(bootstrap_.header_prefix().c_str()); } - // TODO(mattklein123): Custom O(1) headers can be registered at this point for creating/finalizing - // any header maps. + + // Register Custom O(1) headers from bootstrap. + registerCustomInlineHeadersFromBootstrap(bootstrap_); + ENVOY_LOG(info, "HTTP header map info:"); for (const auto& info : Http::HeaderMapImplUtility::getAllHeaderMapImplInfo()) { ENVOY_LOG(info, " {}: {} bytes: {}", info.name_, info.size_, diff --git a/test/server/server_test.cc b/test/server/server_test.cc index f2c005356bcdf..20218f9cc6f44 100644 --- a/test/server/server_test.cc +++ b/test/server/server_test.cc @@ -334,6 +334,98 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, ServerInstanceImplTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); +// The finalize() of Http::CustomInlineHeaderRegistry will be called When InstanceImpl is +// instantiated. For this reason, the test case for inline headers must precede all other test +// cases. + +// Test the case where inline headers contain names that cannot be registered as inline headers. +TEST_P(ServerInstanceImplTest, WithErrorCustomInlineHeaders) { + EXPECT_THROW_WITH_MESSAGE( + initialize("test/server/test_data/server/bootstrap_inline_headers_error.yaml"), + EnvoyException, "Header set-cookie cannot be registered as an inline header."); +} + +TEST_P(ServerInstanceImplTest, WithDeathCustomInlineHeaders) { +#if !defined(NDEBUG) + // The finalize() of Http::CustomInlineHeaderRegistry will be called after the first successful + // instantiation of InstanceImpl. If InstanceImpl is instantiated again and the inline header is + // registered again, the assertion will be triggered. + EXPECT_DEATH( + { + initialize("test/server/test_data/server/bootstrap_inline_headers.yaml"); + initialize("test/server/test_data/server/bootstrap_inline_headers.yaml"); + }, + ""); +#endif // !defined(NDEBUG) +} + +// Test whether the custom inline headers can be registered correctly. +TEST_P(ServerInstanceImplTest, WithCustomInlineHeaders) { + static bool is_registered = false; + + if (!is_registered) { + // Avoid repeated registration of custom inline headers in the current process after the + // finalize() of Http::CustomInlineHeaderRegistry has already been called. + EXPECT_NO_THROW(initialize("test/server/test_data/server/bootstrap_inline_headers.yaml")); + is_registered = true; + } + + EXPECT_TRUE( + Http::CustomInlineHeaderRegistry::getInlineHeader( + Http::LowerCaseString("test1")) + .has_value()); + EXPECT_TRUE( + Http::CustomInlineHeaderRegistry::getInlineHeader( + Http::LowerCaseString("test2")) + .has_value()); + EXPECT_TRUE( + Http::CustomInlineHeaderRegistry::getInlineHeader( + Http::LowerCaseString("test3")) + .has_value()); + EXPECT_TRUE( + Http::CustomInlineHeaderRegistry::getInlineHeader( + Http::LowerCaseString("test4")) + .has_value()); + + { + Http::TestRequestHeaderMapImpl headers{ + {"test1", "test1_value1"}, + {"test1", "test1_value2"}, + {"test3", "test3_value1"}, + {"test3", "test3_value2"}, + }; + + // 'test1' is registered as the inline request header. + auto test1_headers = headers.get(Http::LowerCaseString("test1")); + EXPECT_EQ(1, test1_headers.size()); + EXPECT_EQ("test1_value1,test1_value2", headers.get_("test1")); + + // 'test3' is not registered as an inline request header. + auto test3_headers = headers.get(Http::LowerCaseString("test3")); + EXPECT_EQ(2, test3_headers.size()); + EXPECT_EQ("test3_value1", headers.get_("test3")); + } + + { + Http::TestResponseHeaderMapImpl headers{ + {"test1", "test1_value1"}, + {"test1", "test1_value2"}, + {"test3", "test3_value1"}, + {"test3", "test3_value2"}, + }; + + // 'test1' is not registered as the inline response header. + auto test1_headers = headers.get(Http::LowerCaseString("test1")); + EXPECT_EQ(2, test1_headers.size()); + EXPECT_EQ("test1_value1", headers.get_("test1")); + + // 'test3' is registered as an inline response header. + auto test3_headers = headers.get(Http::LowerCaseString("test3")); + EXPECT_EQ(1, test3_headers.size()); + EXPECT_EQ("test3_value1,test3_value2", headers.get_("test3")); + } +} + // Validates that server stats are flushed even when server is stuck with initialization. TEST_P(ServerInstanceImplTest, StatsFlushWhenServerIsStillInitializing) { CustomStatsSinkFactory factory; diff --git a/test/server/test_data/server/bootstrap_inline_headers.yaml b/test/server/test_data/server/bootstrap_inline_headers.yaml new file mode 100644 index 0000000000000..7d5e05c47f9b4 --- /dev/null +++ b/test/server/test_data/server/bootstrap_inline_headers.yaml @@ -0,0 +1,9 @@ +inline_headers: +- inline_header_name: test1 + inline_header_type: REQUEST_HEADER +- inline_header_name: test2 + inline_header_type: REQUEST_TRAILER +- inline_header_name: test3 + inline_header_type: RESPONSE_HEADER +- inline_header_name: test4 + inline_header_type: RESPONSE_TRAILER diff --git a/test/server/test_data/server/bootstrap_inline_headers_error.yaml b/test/server/test_data/server/bootstrap_inline_headers_error.yaml new file mode 100644 index 0000000000000..d3d884fd3e8e6 --- /dev/null +++ b/test/server/test_data/server/bootstrap_inline_headers_error.yaml @@ -0,0 +1,3 @@ +inline_headers: +- inline_header_name: set-cookie + inline_header_type: RESPONSE_HEADER