Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: support appending to predefined inline headers. #4211

Merged
merged 4 commits into from
Aug 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ Version history
* http: added generic :ref:`Upgrade support
<envoy_api_field_config.filter.network.http_connection_manager.v2.HttpConnectionManager.upgrade_configs>`.
* http: better handling of HEAD requests. Now sending transfer-encoding: chunked rather than content-length: 0.
* http: fixed missing support for appending to predefined inline headers, e.g.
*authorization*, in features that interact with request and response headers,
e.g. :ref:`request_headers_to_add
<envoy_api_field_route.Route.request_headers_to_add>`. For example, a
request header *authorization: token1* will appear as *authorization:
token1,token2*, after having :ref:`request_headers_to_add
<envoy_api_field_route.Route.request_headers_to_add>` with *authorization:
token2* applied.
* http: response filters not applied to early error paths such as http_parser generated 400s.
* http: :ref:`hpack_table_size <envoy_api_field_core.Http2ProtocolOptions.hpack_table_size>` now controls
dynamic table size of both: encoder and decoder.
Expand Down
28 changes: 17 additions & 11 deletions include/envoy/http/header_map.h
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,12 @@ class HeaderMap {
/**
* Add a reference header to the map. Both key and value MUST point to data that will live beyond
* the lifetime of any request/response using the string (since a codec may optimize for zero
* copy). Nothing will be copied.
* copy). The key will not be copied and a best effort will be made not to
* copy the value (but this may happen when comma concatenating, see below).
*
* Calling addReference multiple times for the same header will result in multiple headers being
* present in the HeaderMap.
* Calling addReference multiple times for the same header will result in:
* - Comma concatenation for predefined inline headers.
* - Multiple headers being present in the HeaderMap for other headers.
*
* @param key specifies the name of the header to add; it WILL NOT be copied.
* @param value specifies the value of the header to add; it WILL NOT be copied.
Expand All @@ -336,8 +338,9 @@ class HeaderMap {
* the lifetime of any request/response using the string (since a codec may optimize for zero
* copy). The value will be copied.
*
* Calling addReferenceKey multiple times for the same header will result in multiple headers
* being present in the HeaderMap.
* Calling addReference multiple times for the same header will result in:
* - Comma concatenation for predefined inline headers.
* - Multiple headers being present in the HeaderMap for other headers.
*
* @param key specifies the name of the header to add; it WILL NOT be copied.
* @param value specifies the value of the header to add; it WILL be copied.
Expand All @@ -349,8 +352,9 @@ class HeaderMap {
* live beyond the lifetime of any request/response using the string (since a codec may optimize
* for zero copy). The value will be copied.
*
* Calling addReferenceKey multiple times for the same header will result in multiple headers
* being present in the HeaderMap.
* Calling addReference multiple times for the same header will result in:
* - Comma concatenation for predefined inline headers.
* - Multiple headers being present in the HeaderMap for other headers.
*
* @param key specifies the name of the header to add; it WILL NOT be copied.
* @param value specifies the value of the header to add; it WILL be copied.
Expand All @@ -360,8 +364,9 @@ class HeaderMap {
/**
* Add a header by copying both the header key and the value.
*
* Calling addCopy multiple times for the same header will result in multiple headers being
* present in the HeaderMap.
* Calling addCopy multiple times for the same header will result in:
* - Comma concatenation for predefined inline headers.
* - Multiple headers being present in the HeaderMap for other headers.
*
* @param key specifies the name of the header to add; it WILL be copied.
* @param value specifies the value of the header to add; it WILL be copied.
Expand All @@ -371,8 +376,9 @@ class HeaderMap {
/**
* Add a header by copying both the header key and the value.
*
* Calling addCopy multiple times for the same header will result in multiple headers being
* present in the HeaderMap.
* Calling addCopy multiple times for the same header will result in:
* - Comma concatenation for predefined inline headers.
* - Multiple headers being present in the HeaderMap for other headers.
*
* @param key specifies the name of the header to add; it WILL be copied.
* @param value specifies the value of the header to add; it WILL be copied.
Expand Down
26 changes: 17 additions & 9 deletions source/common/http/header_map_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ void HeaderString::freeDynamic() {
void HeaderString::append(const char* data, uint32_t size) {
switch (type_) {
case Type::Reference: {
// Switch back to inline and fall through. We do not actually append to the static string
// currently which would require a copy.
type_ = Type::Inline;
buffer_.dynamic_ = inline_buffer_;
string_length_ = 0;

FALLTHRU;
// Rather than be too clever and optimize this uncommon case, we dynamically
// allocate and copy.
type_ = Type::Dynamic;
dynamic_capacity_ = (string_length_ + size) * 2;
char* buf = static_cast<char*>(malloc(dynamic_capacity_));
RELEASE_ASSERT(buf != nullptr, "");
memcpy(buf, buffer_.ref_, string_length_);
buffer_.dynamic_ = buf;
break;
}

case Type::Inline: {
Expand All @@ -94,13 +96,15 @@ void HeaderString::append(const char* data, uint32_t size) {
RELEASE_ASSERT(new_capacity <= std::numeric_limits<uint32_t>::max(), "");
buffer_.dynamic_ = static_cast<char*>(malloc(new_capacity));
memcpy(buffer_.dynamic_, inline_buffer_, string_length_);
RELEASE_ASSERT(buffer_.dynamic_ != nullptr, "");
dynamic_capacity_ = new_capacity;
type_ = Type::Dynamic;
} else {
if (size + 1 + string_length_ > dynamic_capacity_) {
// Need to reallocate.
dynamic_capacity_ = (string_length_ + size) * 2;
buffer_.dynamic_ = static_cast<char*>(realloc(buffer_.dynamic_, dynamic_capacity_));
RELEASE_ASSERT(buffer_.dynamic_ != nullptr, "");
}
}
}
Expand Down Expand Up @@ -323,8 +327,12 @@ void HeaderMapImpl::insertByKey(HeaderString&& key, HeaderString&& value) {
if (cb) {
key.clear();
StaticLookupResponse ref_lookup_response = cb(*this);
ASSERT(*ref_lookup_response.entry_ == nullptr); // This function doesn't handle append.
maybeCreateInline(ref_lookup_response.entry_, *ref_lookup_response.key_, std::move(value));
if (*ref_lookup_response.entry_ == nullptr) {
maybeCreateInline(ref_lookup_response.entry_, *ref_lookup_response.key_, std::move(value));
} else {
appendToHeader((*ref_lookup_response.entry_)->value(), value.c_str());
value.clear();
}
} else {
std::list<HeaderEntryImpl>::iterator i = headers_.insert(std::move(key), std::move(value));
i->entry_ = i;
Expand Down
38 changes: 32 additions & 6 deletions test/common/http/header_map_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ TEST(HeaderStringTest, All) {
HeaderString string(static_string);
EXPECT_EQ(HeaderString::Type::Reference, string.type());
string.append("a", 1);
EXPECT_STREQ("a", string.c_str());
EXPECT_STREQ("HELLOa", string.c_str());
}

// Copy inline
Expand Down Expand Up @@ -425,11 +425,37 @@ TEST(HeaderMapImplTest, SetRemovesAllValues) {
}

TEST(HeaderMapImplTest, DoubleInlineAdd) {
HeaderMapImpl headers;
headers.addReferenceKey(Headers::get().ContentLength, 5);
EXPECT_DEBUG_DEATH(headers.addReferenceKey(Headers::get().ContentLength, 6), "");
EXPECT_STREQ("5", headers.ContentLength()->value().c_str());
EXPECT_EQ(1UL, headers.size());
{
HeaderMapImpl headers;
const std::string foo("foo");
const std::string bar("bar");
headers.addReference(Headers::get().ContentLength, foo);
headers.addReference(Headers::get().ContentLength, bar);
EXPECT_STREQ("foo,bar", headers.ContentLength()->value().c_str());
EXPECT_EQ(1UL, headers.size());
}
{
HeaderMapImpl headers;
headers.addReferenceKey(Headers::get().ContentLength, "foo");
headers.addReferenceKey(Headers::get().ContentLength, "bar");
EXPECT_STREQ("foo,bar", headers.ContentLength()->value().c_str());
EXPECT_EQ(1UL, headers.size());
}
{
HeaderMapImpl headers;
headers.addReferenceKey(Headers::get().ContentLength, 5);
headers.addReferenceKey(Headers::get().ContentLength, 6);
EXPECT_STREQ("5,6", headers.ContentLength()->value().c_str());
EXPECT_EQ(1UL, headers.size());
}
{
HeaderMapImpl headers;
const std::string foo("foo");
headers.addReference(Headers::get().ContentLength, foo);
headers.addReferenceKey(Headers::get().ContentLength, 6);
EXPECT_STREQ("foo,6", headers.ContentLength()->value().c_str());
EXPECT_EQ(1UL, headers.size());
}
}

TEST(HeaderMapImplTest, DoubleInlineSet) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
headers_to_add {
}
headers_to_add {
header {
key: "te"
value: "l"
}
}
headers_to_add {
header {
key: "te"
value: "@"
}
}
55 changes: 55 additions & 0 deletions test/integration/header_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,26 @@ stat_prefix: header_test
- header:
key: "x-real-ip"
value: "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%"
- name: append-same-headers
domains: ["append-same-headers.com"]
request_headers_to_add:
- header:
key: "x-foo"
value: "value1"
- header:
key: "authorization"
value: "token1"
routes:
- match: { prefix: "/test" }
route:
cluster: cluster_0
request_headers_to_add:
- header:
key: "x-foo"
value: "value2"
- header:
key: "authorization"
value: "token2"
)EOF";

} // namespace
Expand Down Expand Up @@ -915,4 +935,39 @@ TEST_P(HeaderIntegrationTest, TestXFFParsing) {
});
}

// Validates behavior around same header appending (both predefined headers and
// other).
TEST_P(HeaderIntegrationTest, TestAppendSameHeaders) {
initializeFilter(HeaderMode::Append, false);
performRequest(
Http::TestHeaderMapImpl{
{":method", "GET"},
{":path", "/test"},
{":scheme", "http"},
{":authority", "append-same-headers.com"},
{"authorization", "token3"},
{"x-foo", "value3"},
},
Http::TestHeaderMapImpl{
{":authority", "append-same-headers.com"},
{":path", "/test"},
{":method", "GET"},
{"authorization", "token3,token2,token1"},
{"x-foo", "value3"},
{"x-foo", "value2"},
{"x-foo", "value1"},
},
Http::TestHeaderMapImpl{
{"server", "envoy"},
{"content-length", "0"},
{":status", "200"},
{"x-unmodified", "response"},
},
Http::TestHeaderMapImpl{
{"server", "envoy"},
{"x-unmodified", "response"},
{":status", "200"},
});
}

} // namespace Envoy