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

quiche: handle multiple cookies and multiple set-cookie headers #14969

Merged
merged 11 commits into from
Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 4 additions & 1 deletion source/common/http/header_map_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,10 @@ void HeaderMapImpl::appendCopy(const LowerCaseString& key, absl::string_view val
// TODO(#9221): converge on and document a policy for coalescing multiple headers.
auto entry = getExisting(key);
if (!entry.empty()) {
const uint64_t added_size = appendToHeader(entry[0]->value(), value);
const std::string delimiter = (key == Http::Headers::get().Cookie ? "; " : ",");
const uint64_t added_size = header_map_coalesce_cookie_headers_
? appendToHeader(entry[0]->value(), value, delimiter)
: appendToHeader(entry[0]->value(), value);
addSize(added_size);
} else {
addCopy(key, value);
Expand Down
2 changes: 2 additions & 0 deletions source/common/http/header_map_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ class HeaderMapImpl : NonCopyable {
HeaderList headers_;
// This holds the internal byte size of the HeaderMap.
uint64_t cached_byte_size_ = 0;
const bool header_map_coalesce_cookie_headers_ = Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.header_map_coalesce_cookie_headers");
};

/**
Expand Down
1 change: 1 addition & 0 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ constexpr const char* runtime_features[] = {
"envoy.reloadable_features.unify_grpc_handling",
"envoy.reloadable_features.upstream_http2_flood_checks",
"envoy.restart_features.use_apple_api_for_dns_lookups",
"envoy.reloadable_features.header_map_coalesce_cookie_headers",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this will result in cookies coalescing, but in correctly coalescing cookies. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed it!

};

// This is a section for officially sanctioned runtime features which are too
Expand Down
3 changes: 2 additions & 1 deletion source/extensions/quic_listeners/quiche/envoy_quic_utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ spdy::SpdyHeaderBlock envoyHeadersToSpdyHeaderBlock(const Http::HeaderMap& heade
spdy::SpdyHeaderBlock header_block;
headers.iterate([&header_block](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate {
// The key-value pairs are copied.
header_block.insert({header.key().getStringView(), header.value().getStringView()});
header_block.AppendValueOrAddHeader(header.key().getStringView(),
header.value().getStringView());
return Http::HeaderMap::Iterate::Continue;
});
return header_block;
Expand Down
16 changes: 12 additions & 4 deletions source/extensions/quic_listeners/quiche/envoy_quic_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,17 @@ template <class T>
std::unique_ptr<T> quicHeadersToEnvoyHeaders(const quic::QuicHeaderList& header_list) {
auto headers = T::create();
for (const auto& entry : header_list) {
// TODO(danzh): Avoid copy by referencing entry as header_list is already validated by QUIC.
headers->addCopy(Http::LowerCaseString(entry.first), entry.second);
auto key = Http::LowerCaseString(entry.first);
if (!Runtime::runtimeFeatureEnabled(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I don't think you need to runtime guard QUIC code if youdon't want to as it's still alpha

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point! Removed the runtime guard here but still checks it in tests.

"envoy.reloadable_features.header_map_coalesce_cookie_headers") ||
key != Http::Headers::get().Cookie) {
// TODO(danzh): Avoid copy by referencing entry as header_list is already validated by QUIC.
headers->addCopy(key, entry.second);
} else {
// QUICHE breaks "cookie" header into crumbs. Coalesce them by appending current one to
// existing one if there is any.
headers->appendCopy(key, entry.second);
}
}
return headers;
}
Expand All @@ -54,8 +63,7 @@ std::unique_ptr<T> spdyHeaderBlockToEnvoyHeaders(const spdy::SpdyHeaderBlock& he
for (auto entry : header_block) {
// TODO(danzh): Avoid temporary strings and addCopy() with string_view.
std::string key(entry.first);
std::string value(entry.second);
headers->addCopy(Http::LowerCaseString(key), value);
headers->addCopy(Http::LowerCaseString(key), entry.second);
}
return headers;
}
Expand Down
18 changes: 18 additions & 0 deletions test/common/http/header_map_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,24 @@ TEST_P(HeaderMapImplTest, DoubleCookieAdd) {
ASSERT_EQ(set_cookie_value[1]->value().getStringView(), "bar");
}

TEST_P(HeaderMapImplTest, AppendCookieHeadersWithSemicolon) {
if (!Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.header_map_coalesce_cookie_headers")) {
return;
}
TestRequestHeaderMapImpl headers;
const std::string foo("foo=1");
const std::string bar("bar=2");
const LowerCaseString& cookie = Http::Headers::get().Cookie;
headers.addReference(cookie, foo);
headers.appendCopy(cookie, bar);
EXPECT_EQ(1UL, headers.size());

const auto cookie_value = headers.get(LowerCaseString("cookie"));
ASSERT_EQ(cookie_value.size(), 1);
ASSERT_EQ(cookie_value[0]->value().getStringView(), "foo=1; bar=2");
}

TEST_P(HeaderMapImplTest, DoubleInlineSet) {
TestRequestHeaderMapImpl headers;
headers.setReferenceKey(Headers::get().ContentType, "blah");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ TEST_P(EnvoyQuicServerStreamTest, GetRequestAndResponse) {
request_headers.OnHeader(":authority", host_);
request_headers.OnHeader(":method", "GET");
request_headers.OnHeader(":path", "/");
// QUICHE stack doesn't coalesce Cookie headers for header compression optimization.
request_headers.OnHeader("cookie", "a=b");
request_headers.OnHeader("cookie", "c=d");
request_headers.OnHeaderBlockEnd(/*uncompressed_header_bytes=*/0,
/*compressed_header_bytes=*/0);

Expand All @@ -192,13 +195,22 @@ TEST_P(EnvoyQuicServerStreamTest, GetRequestAndResponse) {
EXPECT_EQ(host_, headers->getHostValue());
EXPECT_EQ("/", headers->getPathValue());
EXPECT_EQ(Http::Headers::get().MethodValues.Get, headers->getMethodValue());
if (Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.header_map_coalesce_cookie_headers")) {
// Verify that the duplicated headers are handled correctly before passing to stream
// decoder.
EXPECT_EQ("a=b; c=d",
headers->get(Http::Headers::get().Cookie)[0]->value().getStringView());
}
}));
if (quic::VersionUsesHttp3(quic_version_.transport_version)) {
EXPECT_CALL(stream_decoder_, decodeData(BufferStringEqual(""), /*end_stream=*/true));
spdy::SpdyHeaderBlock spdy_headers;
spdy_headers[":authority"] = host_;
spdy_headers[":method"] = "GET";
spdy_headers[":path"] = "/";
spdy_headers.AppendValueOrAddHeader("cookie", "a=b");
spdy_headers.AppendValueOrAddHeader("cookie", "c=d");
std::string payload = spdyHeaderToHttp3StreamPayload(spdy_headers);
quic::QuicStreamFrame frame(stream_id_, true, 0, payload);
quic_stream_->OnStreamFrame(frame);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,5 +574,38 @@ TEST_P(QuicHttpIntegrationTest, Reset101SwitchProtocolResponse) {
EXPECT_FALSE(response->complete());
}

TEST_P(QuicHttpIntegrationTest, MultipleSetCookieAndCookieHeaders) {
initialize();

codec_client_ = makeHttpConnection(lookupPort("http"));
auto encoder_decoder =
codec_client_->startRequest(Http::TestRequestHeaderMapImpl{{":method", "GET"},
{":path", "/dynamo/url"},
{":scheme", "http"},
{":authority", "host"},
{"cookie", "a=b"},
{"cookie", "c=d"}});
request_encoder_ = &encoder_decoder.first;
auto response = std::move(encoder_decoder.second);
codec_client_->sendData(*request_encoder_, 0, true);
waitForNextUpstreamRequest();
if (Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.header_map_coalesce_cookie_headers")) {
EXPECT_EQ(upstream_request_->headers().get(Http::Headers::get().Cookie)[0]->value(),
"a=b; c=d");
}

upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"},
{"set-cookie", "foo"},
{"set-cookie", "bar"}},
true);
response->waitForEndStream();
EXPECT_TRUE(response->complete());
const auto out = response->headers().get(Http::LowerCaseString("set-cookie"));
ASSERT_EQ(out.size(), 2);
ASSERT_EQ(out[0]->value().getStringView(), "foo");
ASSERT_EQ(out[1]->value().getStringView(), "bar");
}

} // namespace Quic
} // namespace Envoy