diff --git a/api/envoy/api/v2/core/protocol.proto b/api/envoy/api/v2/core/protocol.proto index c45bb7adf7db..65d859f26ae9 100644 --- a/api/envoy/api/v2/core/protocol.proto +++ b/api/envoy/api/v2/core/protocol.proto @@ -42,6 +42,24 @@ message Http1ProtocolOptions { // Envoy does not otherwise support HTTP/1.0 without a Host header. // This is a no-op if *accept_http_10* is not true. string default_host_for_http_10 = 3; + + message HeaderKeyFormat { + enum HeaderFormat { + // By default, all headers are lower cased. + DEFAULT = 0; + + // Formats the header using Train-Case: the first character and any character following a + // special character will be capitalized if it's an alpha character. For example, + // "content-type" becomes "Content-Type", and "foo$b#$ar" becomes "Foo$B#$Ar". + TRAIN_CASE = 1; + } + + HeaderFormat header_format = 1; + } + + // Describes how the keys for response headers should be formatted. By default, all header keys + // are lower cased. + HeaderKeyFormat response_header_key_format = 4; } // [#comment:next free field: 13] diff --git a/include/envoy/http/codec.h b/include/envoy/http/codec.h index eb5951d3b9b6..9f6d3264de1b 100644 --- a/include/envoy/http/codec.h +++ b/include/envoy/http/codec.h @@ -222,6 +222,13 @@ struct Http1Settings { bool accept_http_10_{false}; // Set a default host if no Host: header is present for HTTP/1.0 requests.` std::string default_host_for_http_10_; + + enum class HeaderKeyFormat { + Default, + TrainCase, + }; + + HeaderKeyFormat header_key_format_; }; /** diff --git a/include/envoy/http/header_map.h b/include/envoy/http/header_map.h index def6d597461e..f785f8aed8b7 100644 --- a/include/envoy/http/header_map.h +++ b/include/envoy/http/header_map.h @@ -478,7 +478,7 @@ class HeaderMap { * @param context supplies the context passed to iterate(). * @return Iterate::Continue to continue iteration. */ - using ConstIterateCb = Iterate (*)(const HeaderEntry&, void*); + using ConstIterateCb = std::function; /** * Iterate over a constant header map. diff --git a/source/common/http/http1/BUILD b/source/common/http/http1/BUILD index 9756586598c1..df67bf12e952 100644 --- a/source/common/http/http1/BUILD +++ b/source/common/http/http1/BUILD @@ -8,6 +8,11 @@ load( envoy_package() +envoy_cc_library( + name = "header_formatter_lib", + hdrs = ["header_formatter.h"], +) + envoy_cc_library( name = "codec_lib", srcs = ["codec_impl.cc"], @@ -32,6 +37,7 @@ envoy_cc_library( "//source/common/http:header_utility_lib", "//source/common/http:headers_lib", "//source/common/http:utility_lib", + "//source/common/http/http1:header_formatter_lib", "//source/common/runtime:runtime_lib", ], ) diff --git a/source/common/http/http1/codec_impl.cc b/source/common/http/http1/codec_impl.cc index afadd309c192..7d3f96187f73 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/http/codec.h" #include "envoy/http/header_map.h" #include "envoy/network/connection.h" @@ -34,7 +35,9 @@ const StringUtil::CaseUnorderedSet& caseUnorderdSetContainingUpgradeAndHttp2Sett const std::string StreamEncoderImpl::CRLF = "\r\n"; const std::string StreamEncoderImpl::LAST_CHUNK = "0\r\n\r\n"; -StreamEncoderImpl::StreamEncoderImpl(ConnectionImpl& connection) : connection_(connection) { +StreamEncoderImpl::StreamEncoderImpl(ConnectionImpl& connection, + HeaderKeyFormatter* header_key_formatter) + : connection_(connection), header_key_formatter_(header_key_formatter) { if (connection_.connection().aboveHighWatermark()) { runHighWatermarkCallbacks(); } @@ -67,7 +70,7 @@ void StreamEncoderImpl::encode100ContinueHeaders(const HeaderMap& headers) { void StreamEncoderImpl::encodeHeaders(const HeaderMap& headers, bool end_stream) { bool saw_content_length = false; headers.iterate( - [](const HeaderEntry& header, void* context) -> HeaderMap::Iterate { + [this](const HeaderEntry& header, void* context) -> 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. @@ -81,8 +84,13 @@ void StreamEncoderImpl::encodeHeaders(const HeaderMap& headers, bool end_stream) return HeaderMap::Iterate::Continue; } - static_cast(context)->encodeHeader(key_to_use, - header.value().getStringView()); + if (header_key_formatter_ != nullptr) { + static_cast(context)->encodeHeader( + header_key_formatter_->format(key_to_use), header.value().getStringView()); + } else { + static_cast(context)->encodeHeader(key_to_use, + header.value().getStringView()); + } return HeaderMap::Iterate::Continue; }, this); @@ -116,17 +124,32 @@ void StreamEncoderImpl::encodeHeaders(const HeaderMap& headers, bool end_stream) // For 204s and 1xx where content length is disallowed, don't append the content length but // also don't chunk encode. if (is_content_length_allowed_) { - encodeHeader(Headers::get().ContentLength.get().c_str(), - Headers::get().ContentLength.get().size(), "0", 1); + if (header_key_formatter_ != nullptr) { + const auto formatted_header = + header_key_formatter_->format(Headers::get().ContentLength.get().c_str()); + encodeHeader(formatted_header.c_str(), formatted_header.size(), "0", 1); + } else { + encodeHeader(Headers::get().ContentLength.get().c_str(), + Headers::get().ContentLength.get().size(), "0", 1); + } } chunk_encoding_ = false; } else if (connection_.protocol() == Protocol::Http10) { chunk_encoding_ = false; } else { - encodeHeader(Headers::get().TransferEncoding.get().c_str(), - Headers::get().TransferEncoding.get().size(), - Headers::get().TransferEncodingValues.Chunked.c_str(), - Headers::get().TransferEncodingValues.Chunked.size()); + if (header_key_formatter_ != nullptr) { + const auto formatted_header = + header_key_formatter_->format(Headers::get().TransferEncoding.get().c_str()); + encodeHeader(formatted_header.c_str(), formatted_header.size(), + Headers::get().TransferEncodingValues.Chunked.c_str(), + Headers::get().TransferEncodingValues.Chunked.size()); + } else { + encodeHeader(Headers::get().TransferEncoding.get().c_str(), + Headers::get().TransferEncoding.get().size(), + Headers::get().TransferEncodingValues.Chunked.c_str(), + Headers::get().TransferEncodingValues.Chunked.size()); + } + // We do not apply chunk encoding for HTTP upgrades. // If there is a body in a WebSocket Upgrade response, the chunks will be // passed through via maybeDirectDispatch so we need to avoid appending @@ -545,7 +568,15 @@ ServerConnectionImpl::ServerConnectionImpl(Network::Connection& connection, Stat ServerConnectionCallbacks& callbacks, Http1Settings settings, uint32_t max_request_headers_kb) : ConnectionImpl(connection, stats, HTTP_REQUEST, max_request_headers_kb), - callbacks_(callbacks), codec_settings_(settings) {} + callbacks_(callbacks), codec_settings_(settings) { + switch (codec_settings_.header_key_format_) { + case Http1Settings::HeaderKeyFormat::Default: + break; + case Http1Settings::HeaderKeyFormat::TrainCase: + header_key_formatter_ = std::make_unique(); + break; + } +} void ServerConnectionImpl::onEncodeComplete() { ASSERT(active_request_); @@ -645,7 +676,7 @@ int ServerConnectionImpl::onHeadersComplete(HeaderMapImplPtr&& headers) { void ServerConnectionImpl::onMessageBegin() { if (!resetStreamCalled()) { ASSERT(!active_request_); - active_request_ = std::make_unique(*this); + active_request_ = std::make_unique(*this, header_key_formatter_.get()); active_request_->request_decoder_ = &callbacks_.newStream(active_request_->response_encoder_); } } diff --git a/source/common/http/http1/codec_impl.h b/source/common/http/http1/codec_impl.h index e6dd04016888..7a6c854c0f38 100644 --- a/source/common/http/http1/codec_impl.h +++ b/source/common/http/http1/codec_impl.h @@ -18,6 +18,7 @@ #include "common/http/codec_helper.h" #include "common/http/codes.h" #include "common/http/header_map_impl.h" +#include "common/http/http1/header_formatter.h" namespace Envoy { namespace Http { @@ -66,7 +67,7 @@ class StreamEncoderImpl : public StreamEncoder, void isResponseToHeadRequest(bool value) { is_response_to_head_request_ = value; } protected: - StreamEncoderImpl(ConnectionImpl& connection); + StreamEncoderImpl(ConnectionImpl& connection, HeaderKeyFormatter* header_key_formatter); static const std::string CRLF; static const std::string LAST_CHUNK; @@ -100,6 +101,7 @@ class StreamEncoderImpl : public StreamEncoder, bool processing_100_continue_{false}; bool is_response_to_head_request_{false}; bool is_content_length_allowed_{true}; + HeaderKeyFormatter* header_key_formatter_; }; /** @@ -107,7 +109,7 @@ class StreamEncoderImpl : public StreamEncoder, */ class ResponseStreamEncoderImpl : public StreamEncoderImpl { public: - ResponseStreamEncoderImpl(ConnectionImpl& connection) : StreamEncoderImpl(connection) {} + ResponseStreamEncoderImpl(ConnectionImpl& connection, HeaderKeyFormatter* header_key_formatter) : StreamEncoderImpl(connection, header_key_formatter) {} bool startedResponse() { return started_response_; } @@ -123,7 +125,7 @@ class ResponseStreamEncoderImpl : public StreamEncoderImpl { */ class RequestStreamEncoderImpl : public StreamEncoderImpl { public: - RequestStreamEncoderImpl(ConnectionImpl& connection) : StreamEncoderImpl(connection) {} + RequestStreamEncoderImpl(ConnectionImpl& connection) : StreamEncoderImpl(connection, nullptr) {} bool headRequest() { return head_request_; } // Http::StreamEncoder @@ -321,7 +323,7 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl { * An active HTTP/1.1 request. */ struct ActiveRequest { - ActiveRequest(ConnectionImpl& connection) : response_encoder_(connection) {} + ActiveRequest(ConnectionImpl& connection, HeaderKeyFormatter* header_key_formatter) : response_encoder_(connection, header_key_formatter) {} HeaderString request_url_; StreamDecoder* request_decoder_{}; @@ -355,6 +357,7 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl { ServerConnectionCallbacks& callbacks_; std::unique_ptr active_request_; Http1Settings codec_settings_; + HeaderKeyFormatterPtr header_key_formatter_; }; /** diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index 65a708903745..858a1c35177c 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -276,6 +276,18 @@ Utility::parseHttp1Settings(const envoy::api::v2::core::Http1ProtocolOptions& co 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(); + + switch (config.response_header_key_format().header_format()) { + case envoy::api::v2::core::Http1ProtocolOptions_HeaderKeyFormat::DEFAULT: + ret.header_key_format_ = Http1Settings::HeaderKeyFormat::Default; + break; + case envoy::api::v2::core::Http1ProtocolOptions_HeaderKeyFormat::TRAIN_CASE: + ret.header_key_format_ = Http1Settings::HeaderKeyFormat::TrainCase; + break; + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } + return ret; } diff --git a/test/common/http/http1/codec_impl_test.cc b/test/common/http/http1/codec_impl_test.cc index 0303cfee5be1..7d8d092da8ab 100644 --- a/test/common/http/http1/codec_impl_test.cc +++ b/test/common/http/http1/codec_impl_test.cc @@ -504,6 +504,31 @@ TEST_F(Http1ServerConnectionImplTest, HeaderOnlyResponse) { EXPECT_EQ("HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n", output); } +TEST_F(Http1ServerConnectionImplTest, HeaderOnlyResponseTrainCaseHeaders) { + codec_settings_.header_key_format_ = Http1Settings::HeaderKeyFormat::TrainCase; + initialize(); + + NiceMock decoder; + Http::StreamEncoder* response_encoder = nullptr; + EXPECT_CALL(callbacks_, newStream(_, _)) + .WillOnce(Invoke([&](Http::StreamEncoder& encoder, bool) -> Http::StreamDecoder& { + response_encoder = &encoder; + return decoder; + })); + + Buffer::OwnedImpl buffer("GET / HTTP/1.1\r\n\r\n"); + codec_->dispatch(buffer); + EXPECT_EQ(0U, buffer.length()); + + std::string output; + ON_CALL(connection_, write(_, _)).WillByDefault(AddBufferToString(&output)); + + TestHeaderMapImpl headers{{":status", "200"}, {"some-header", "foo"}, {"some#header", "baz"}}; + response_encoder->encodeHeaders(headers, true); + EXPECT_EQ("HTTP/1.1 200 OK\r\nSome-Header: foo\r\nSome#Header: baz\r\nContent-Length: 0\r\n\r\n", + output); +} + TEST_F(Http1ServerConnectionImplTest, HeaderOnlyResponseWith204) { initialize();