Skip to content

Commit

Permalink
http1: allow configuring format of encoded response headers
Browse files Browse the repository at this point in the history
Adds a configuration option that will convert all header keys into
Train-Case. This is useful to allow Envoy to respond with headers
that match the casing of other systems, to ensure that the wire format
of responses is unchanged when migrating to Envoy.

Fixes envoyproxy#8463

Signed-off-by: Snow Pettersen <snowp@squareup.com>
  • Loading branch information
snowp committed Oct 4, 2019
1 parent 0360afe commit 0647655
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 17 deletions.
18 changes: 18 additions & 0 deletions api/envoy/api/v2/core/protocol.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
7 changes: 7 additions & 0 deletions include/envoy/http/codec.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
};

/**
Expand Down
2 changes: 1 addition & 1 deletion include/envoy/http/header_map.h
Original file line number Diff line number Diff line change
Expand Up @@ -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(const HeaderEntry&, void*)>;

/**
* Iterate over a constant header map.
Expand Down
6 changes: 6 additions & 0 deletions source/common/http/http1/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand All @@ -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",
],
)
Expand Down
55 changes: 43 additions & 12 deletions source/common/http/http1/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <string>

#include "envoy/buffer/buffer.h"
#include "envoy/http/codec.h"
#include "envoy/http/header_map.h"
#include "envoy/network/connection.h"

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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.
Expand All @@ -81,8 +84,13 @@ void StreamEncoderImpl::encodeHeaders(const HeaderMap& headers, bool end_stream)
return HeaderMap::Iterate::Continue;
}

static_cast<StreamEncoderImpl*>(context)->encodeHeader(key_to_use,
header.value().getStringView());
if (header_key_formatter_ != nullptr) {
static_cast<StreamEncoderImpl*>(context)->encodeHeader(
header_key_formatter_->format(key_to_use), header.value().getStringView());
} else {
static_cast<StreamEncoderImpl*>(context)->encodeHeader(key_to_use,
header.value().getStringView());
}
return HeaderMap::Iterate::Continue;
},
this);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<TrainCaseHeaderKeyFormatter>();
break;
}
}

void ServerConnectionImpl::onEncodeComplete() {
ASSERT(active_request_);
Expand Down Expand Up @@ -645,7 +676,7 @@ int ServerConnectionImpl::onHeadersComplete(HeaderMapImplPtr&& headers) {
void ServerConnectionImpl::onMessageBegin() {
if (!resetStreamCalled()) {
ASSERT(!active_request_);
active_request_ = std::make_unique<ActiveRequest>(*this);
active_request_ = std::make_unique<ActiveRequest>(*this, header_key_formatter_.get());
active_request_->request_decoder_ = &callbacks_.newStream(active_request_->response_encoder_);
}
}
Expand Down
11 changes: 7 additions & 4 deletions source/common/http/http1/codec_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -100,14 +101,15 @@ 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_;
};

/**
* HTTP/1.1 response encoder.
*/
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_; }

Expand All @@ -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
Expand Down Expand Up @@ -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_{};
Expand Down Expand Up @@ -355,6 +357,7 @@ class ServerConnectionImpl : public ServerConnection, public ConnectionImpl {
ServerConnectionCallbacks& callbacks_;
std::unique_ptr<ActiveRequest> active_request_;
Http1Settings codec_settings_;
HeaderKeyFormatterPtr header_key_formatter_;
};

/**
Expand Down
12 changes: 12 additions & 0 deletions source/common/http/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
25 changes: 25 additions & 0 deletions test/common/http/http1/codec_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<Http::MockStreamDecoder> 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();

Expand Down

0 comments on commit 0647655

Please sign in to comment.