-
Notifications
You must be signed in to change notification settings - Fork 5.3k
quiche: enforce content-length header consistency #18459
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
Changes from all commits
6c484a6
d4ecff6
5129e73
b9286b8
a097a9d
8db7c28
265c74f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,9 +107,13 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, | |
| return Http::HeaderUtility::HeaderValidationResult::REJECT; | ||
| } | ||
| if (header_name == "content-length") { | ||
| return Http::HeaderUtility::validateContentLength( | ||
| header_value, override_stream_error_on_invalid_http_message, | ||
| close_connection_upon_invalid_header_); | ||
| size_t content_length = 0; | ||
| Http::HeaderUtility::HeaderValidationResult result = | ||
| Http::HeaderUtility::validateContentLength( | ||
| header_value, override_stream_error_on_invalid_http_message, | ||
| close_connection_upon_invalid_header_, content_length); | ||
| content_length_ = content_length; | ||
| return result; | ||
| } | ||
| return Http::HeaderUtility::HeaderValidationResult::ACCEPT; | ||
| } | ||
|
|
@@ -122,6 +126,26 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, | |
| // Needed for ENVOY_STREAM_LOG. | ||
| virtual uint32_t streamId() PURE; | ||
| virtual Network::Connection* connection() PURE; | ||
| // Either reset the stream or close the connection according to | ||
| // should_close_connection and configured http3 options. | ||
| virtual void | ||
| onStreamError(absl::optional<bool> should_close_connection, | ||
| quic::QuicRstStreamErrorCode rst = quic::QUIC_BAD_APPLICATION_PAYLOAD) PURE; | ||
|
|
||
| // TODO(danzh) remove this once QUICHE enforces content-length consistency. | ||
| void updateReceivedContentBytes(size_t payload_length, bool end_stream) { | ||
| received_content_bytes_ += payload_length; | ||
| if (!content_length_.has_value()) { | ||
| return; | ||
| } | ||
| if (received_content_bytes_ > content_length_.value() || | ||
| (end_stream && received_content_bytes_ != content_length_.value() && | ||
| !(got_304_response_ && received_content_bytes_ == 0) && !(sent_head_request_))) { | ||
| details_ = Http3ResponseCodeDetailValues::inconsistent_content_length; | ||
| // Reset instead of closing the connection to align with nghttp2. | ||
| onStreamError(false); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we make sure the stream would have informative error details here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added inconsistent_content_length response code detail |
||
| } | ||
| } | ||
|
|
||
| // True once end of stream is propagated to Envoy. Envoy doesn't expect to be | ||
| // notified more than once about end of stream. So once this is true, no need | ||
|
|
@@ -141,6 +165,8 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, | |
| // TODO(kbaichoo): bind the account to the QUIC buffers to enable tracking of | ||
| // memory allocated within QUIC buffers. | ||
| Buffer::BufferMemoryAccountSharedPtr buffer_memory_account_ = nullptr; | ||
| bool got_304_response_{false}; | ||
| bool sent_head_request_{false}; | ||
|
|
||
| private: | ||
| // Keeps track of bytes buffered in the stream send buffer in QUICHE and reacts | ||
|
|
@@ -157,6 +183,8 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, | |
| // state change in its own call stack. And Envoy upstream doesn't like quic stream to be unblocked | ||
| // in its callstack either because the stream will push data right away. | ||
| Event::SchedulableCallbackPtr async_stream_blockage_change_; | ||
| absl::optional<size_t> content_length_; | ||
| size_t received_content_bytes_{0}; | ||
| }; | ||
|
|
||
| } // namespace Quic | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -493,6 +493,27 @@ TEST_P(ProtocolIntegrationTest, 304HeadResponseWithoutContentLengthLegacy) { | |
| EXPECT_TRUE(response->headers().get(Http::LowerCaseString("content-length")).empty()); | ||
| } | ||
|
|
||
| // Tests that the response to a HEAD request can have content-length header but empty body. | ||
| TEST_P(ProtocolIntegrationTest, 200HeadResponseWithContentLength) { | ||
| initialize(); | ||
| codec_client_ = makeHttpConnection(lookupPort("http")); | ||
| auto response = codec_client_->makeHeaderOnlyRequest( | ||
| Http::TestRequestHeaderMapImpl{{":method", "HEAD"}, | ||
| {":path", "/test/long/url"}, | ||
| {":scheme", "http"}, | ||
| {":authority", "host"}, | ||
| {"if-none-match", "\"1234567890\""}}); | ||
| waitForNextUpstreamRequest(); | ||
| upstream_request_->encodeHeaders( | ||
| Http::TestResponseHeaderMapImpl{{":status", "200"}, {"content-length", "123"}}, true); | ||
| ASSERT_TRUE(response->waitForEndStream()); | ||
| EXPECT_TRUE(response->complete()); | ||
| EXPECT_EQ("200", response->headers().getStatusValue()); | ||
| EXPECT_EQ( | ||
| "123", | ||
| response->headers().get(Http::LowerCaseString("content-length"))[0]->value().getStringView()); | ||
| } | ||
|
|
||
| // Tests missing headers needed for H/1 codec first line. | ||
| TEST_P(DownstreamProtocolIntegrationTest, DownstreamRequestWithFaultyFilter) { | ||
| if (upstreamProtocol() == Http::CodecType::HTTP3) { | ||
|
|
@@ -3158,4 +3179,54 @@ TEST_P(ProtocolIntegrationTest, FragmentStrippedFromPathWithOverride) { | |
| EXPECT_EQ("200", response->headers().getStatusValue()); | ||
| } | ||
|
|
||
| TEST_P(DownstreamProtocolIntegrationTest, ContentLengthSmallerThanPayload) { | ||
| initialize(); | ||
| codec_client_ = makeHttpConnection(lookupPort("http")); | ||
| auto response = | ||
| codec_client_->makeRequestWithBody(Http::TestRequestHeaderMapImpl{{":method", "POST"}, | ||
| {":path", "/test/long/url"}, | ||
| {":scheme", "http"}, | ||
| {":authority", "host"}, | ||
| {"content-length", "123"}}, | ||
| 1024); | ||
| if (downstreamProtocol() == Http::CodecType::HTTP1) { | ||
| waitForNextUpstreamRequest(); | ||
| // HTTP/1.x requests get the payload length from Content-Length header. The remaining bytes is | ||
| // parsed as another request. | ||
| EXPECT_EQ(123u, upstream_request_->body().length()); | ||
| upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); | ||
| ASSERT_TRUE(response->waitForEndStream()); | ||
| EXPECT_EQ("200", response->headers().getStatusValue()); | ||
| EXPECT_TRUE(response->complete()); | ||
| } else { | ||
| // Inconsistency in content-length header and the actually body length should be treated as a | ||
| // stream error. | ||
| ASSERT_TRUE(response->waitForReset()); | ||
| EXPECT_EQ(Http::StreamResetReason::RemoteReset, response->resetReason()); | ||
| } | ||
| } | ||
|
|
||
| TEST_P(DownstreamProtocolIntegrationTest, ContentLengthLargerThanPayload) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think of one additional test in multiplexed integration test where we have an incorrect body length and trailers, just to test the accounting you added there?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| if (downstreamProtocol() == Http::CodecType::HTTP1) { | ||
| // HTTP/1.x request rely on Content-Length header to determine payload length. So there is no | ||
| // inconsistency but the request will hang there waiting for the rest bytes. | ||
| return; | ||
| } | ||
|
|
||
| initialize(); | ||
| codec_client_ = makeHttpConnection(lookupPort("http")); | ||
| auto response = | ||
| codec_client_->makeRequestWithBody(Http::TestRequestHeaderMapImpl{{":method", "POST"}, | ||
| {":path", "/test/long/url"}, | ||
| {":scheme", "http"}, | ||
| {":authority", "host"}, | ||
| {"content-length", "1025"}}, | ||
| 1024); | ||
|
|
||
| // Inconsistency in content-length header and the actually body length should be treated as a | ||
| // stream error. | ||
| ASSERT_TRUE(response->waitForReset()); | ||
| EXPECT_EQ(Http::StreamResetReason::RemoteReset, response->resetReason()); | ||
| } | ||
|
|
||
| } // namespace Envoy | ||
Uh oh!
There was an error while loading. Please reload this page.