Skip to content

Commit

Permalink
[http1] fix H/1 response pipelining (envoyproxy#13983)
Browse files Browse the repository at this point in the history
* Fix HTTP/1 response pipelining. Extraneous data after a response complete when the connection is still open should not be processed.

Signed-off-by: Asra Ali <[email protected]>
  • Loading branch information
asraa authored and andreyprezotto committed Nov 24, 2020
1 parent 81b02b6 commit 854b797
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 0 deletions.
4 changes: 4 additions & 0 deletions source/common/http/codec_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ void CodecClient::onData(Buffer::Instance& data) {
host_->cluster().stats().upstream_cx_protocol_error_.inc();
}
}

// All data should be consumed at this point if the connection remains open.
ASSERT(data.length() == 0 || connection_->state() != Network::Connection::State::Open,
absl::StrCat("extraneous bytes after response complete: ", data.length()));
}

CodecClientProd::CodecClientProd(Type type, Network::ClientConnectionPtr&& connection,
Expand Down
13 changes: 13 additions & 0 deletions source/common/http/http1/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,16 @@ Http::Status ConnectionImpl::dispatch(Buffer::Instance& data) {
[&](Buffer::Instance& data) -> Http::Status { return innerDispatch(data); }, data);
}

Http::Status ClientConnectionImpl::dispatch(Buffer::Instance& data) {
Http::Status status = ConnectionImpl::dispatch(data);
if (status.ok() && data.length() > 0) {
// The HTTP/1.1 codec pauses dispatch after a single response is complete. Extraneous data
// after a response is complete indicates an error.
return codecProtocolError("http/1.1 protocol error: extraneous data after response complete");
}
return status;
}

Http::Status ConnectionImpl::innerDispatch(Buffer::Instance& data) {
ENVOY_CONN_LOG(trace, "parsing {} bytes", connection_, data.length());
// Make sure that dispatching_ is set to false after dispatching, even when
Expand Down Expand Up @@ -1285,6 +1295,9 @@ void ClientConnectionImpl::onMessageComplete() {
pending_response_.reset();
headers_or_trailers_.emplace<ResponseHeaderMapPtr>(nullptr);
}

// Pause the parser after a response is complete. Any remaining data indicates an error.
http_parser_pause(&parser_, 1);
}

void ClientConnectionImpl::onResetStream(StreamResetReason reason) {
Expand Down
1 change: 1 addition & 0 deletions source/common/http/http1/codec_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,7 @@ class ClientConnectionImpl : public ClientConnection, public ConnectionImpl {
bool cannotHaveBody();

// ConnectionImpl
Http::Status dispatch(Buffer::Instance& data) override;
void onEncodeComplete() override {}
Status onMessageBegin() override { return okStatus(); }
Status onUrl(const char*, size_t) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; }
Expand Down
10 changes: 10 additions & 0 deletions source/common/http/http1/codec_impl_legacy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,16 @@ Http::Status ConnectionImpl::dispatch(Buffer::Instance& data) {
[&](Buffer::Instance& data) -> Http::Status { return innerDispatch(data); }, data);
}

Http::Status ClientConnectionImpl::dispatch(Buffer::Instance& data) {
Http::Status status = ConnectionImpl::dispatch(data);
if (status.ok() && data.length() > 0) {
// The HTTP/1.1 codec pauses dispatch after a single response is complete. Extraneous data
// after a response is complete indicates an error.
return codecProtocolError("http/1.1 protocol error: extraneous data after response complete");
}
return status;
}

Http::Status ConnectionImpl::innerDispatch(Buffer::Instance& data) {
ENVOY_CONN_LOG(trace, "parsing {} bytes", connection_, data.length());
ASSERT(buffered_body_.length() == 0);
Expand Down
1 change: 1 addition & 0 deletions source/common/http/http1/codec_impl_legacy.h
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ class ClientConnectionImpl : public ClientConnection, public ConnectionImpl {
bool cannotHaveBody();

// ConnectionImpl
Http::Status dispatch(Buffer::Instance& data) override;
void onEncodeComplete() override {}
void onMessageBegin() override {}
void onUrl(const char*, size_t) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; }
Expand Down
4 changes: 4 additions & 0 deletions test/common/http/codec_client_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ TEST_P(CodecNetworkTest, SendData) {
upstream_connection_->write(data, false);
EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance& data) -> Http::Status {
EXPECT_EQ(full_data, data.toString());
data.drain(data.length());
dispatcher_->exit();
return Http::okStatus();
}));
Expand All @@ -397,10 +398,12 @@ TEST_P(CodecNetworkTest, SendHeadersAndClose) {
.Times(2)
.WillOnce(Invoke([&](Buffer::Instance& data) -> Http::Status {
EXPECT_EQ(full_data, data.toString());
data.drain(data.length());
return Http::okStatus();
}))
.WillOnce(Invoke([&](Buffer::Instance& data) -> Http::Status {
EXPECT_EQ("", data.toString());
data.drain(data.length());
return Http::okStatus();
}));
// Because the headers are not complete, the disconnect will reset the stream.
Expand Down Expand Up @@ -435,6 +438,7 @@ TEST_P(CodecNetworkTest, SendHeadersAndCloseUnderReadDisable) {
.Times(2)
.WillOnce(Invoke([&](Buffer::Instance& data) -> Http::Status {
EXPECT_EQ(full_data, data.toString());
data.drain(data.length());
return Http::okStatus();
}))
.WillOnce(Invoke([&](Buffer::Instance& data) -> Http::Status {
Expand Down
12 changes: 12 additions & 0 deletions test/integration/h1_corpus/alloc_headers

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions test/integration/integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,38 @@ TEST_P(IntegrationTest, TestSmuggling) {
}
}

TEST_P(IntegrationTest, TestPipelinedResponses) {
initialize();
auto tcp_client = makeTcpConnection(lookupPort("http"));

ASSERT_TRUE(tcp_client->write(
"POST /test/long/url HTTP/1.1\r\nHost: host\r\ntransfer-encoding: chunked\r\n\r\n"));

FakeRawConnectionPtr fake_upstream_connection;
ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream_connection));
std::string data;
ASSERT_TRUE(fake_upstream_connection->waitForData(
FakeRawConnection::waitForInexactMatch("\r\n\r\n"), &data));
ASSERT_THAT(data, HasSubstr("POST"));

ASSERT_TRUE(fake_upstream_connection->write(
"HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n0\r\n\r\n"
"HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n0\r\n\r\n"
"HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n0\r\n\r\n"));

tcp_client->waitForData("0\r\n\r\n", false);
std::string response = tcp_client->data();

EXPECT_THAT(response, HasSubstr("HTTP/1.1 200 OK\r\n"));
EXPECT_THAT(response, HasSubstr("transfer-encoding: chunked\r\n"));
EXPECT_THAT(response, EndsWith("0\r\n\r\n"));

ASSERT_TRUE(fake_upstream_connection->close());
ASSERT_TRUE(fake_upstream_connection->waitForDisconnect());
tcp_client->close();
EXPECT_EQ(test_server_->counter("cluster.cluster_0.upstream_cx_protocol_error")->value(), 1);
}

TEST_P(IntegrationTest, TestServerAllowChunkedLength) {
config_helper_.addConfigModifier(
[&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
Expand Down

0 comments on commit 854b797

Please sign in to comment.