Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Below are the list of reasons the HttpConnectionManager or Router filter may sen
duration_timeout, The max connection duration was exceeded.
direct_response, A direct response was generated by the router filter.
filter_chain_not_found, The request was rejected due to no matching filter chain.
filter_removed_required_headers, The request was rejected in the filter manager because a configured filter removed required headers.
internal_redirect, The original stream was replaced with an internal redirect.
low_version, The HTTP/1.0 or HTTP/0.9 request was rejected due to HTTP/1.0 support not being configured.
maintenance_mode, The request was rejected by the router filter because the cluster was in maintenance mode.
Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Bug Fixes

* dns: fix a bug where custom resolvers provided in configuration were not preserved after network issues.
* http: fixed URL parsing for HTTP/1.1 fully qualified URLs and connect requests containing IPv6 addresses.
* http: reject requests with missing required headers after filter chain processing.
* http: sending CONNECT_ERROR for HTTP/2 where appropriate during CONNECT requests.
* tls: fix read resumption after triggering buffer high-watermark and all remaining request/response bytes are stored in the SSL connection's internal buffers.

Expand Down
6 changes: 4 additions & 2 deletions include/envoy/http/codec.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,12 @@ class RequestEncoder : public virtual StreamEncoder {
public:
/**
* Encode headers, optionally indicating end of stream.
* @param headers supplies the header map to encode.
* @param headers supplies the header map to encode. Must have required HTTP headers.
* @param end_stream supplies whether this is a header only request.
Comment thread
asraa marked this conversation as resolved.
* @return Status indicating whether encoding succeeded. Encoding will fail if request
* headers are missing required HTTP headers (method, path for non-CONNECT, host for CONNECT).
*/
virtual void encodeHeaders(const RequestHeaderMap& headers, bool end_stream) PURE;
virtual Status encodeHeaders(const RequestHeaderMap& headers, bool end_stream) PURE;

/**
* Encode trailers. This implicitly ends the stream.
Expand Down
4 changes: 3 additions & 1 deletion include/envoy/router/router.h
Original file line number Diff line number Diff line change
Expand Up @@ -1274,8 +1274,10 @@ class GenericUpstream {
* Encode headers, optionally indicating end of stream.
* @param headers supplies the header map to encode.
* @param end_stream supplies whether this is a header only request.
Comment thread
asraa marked this conversation as resolved.
* @return status indicating success. Encoding will fail if headers do not have required HTTP
* headers.
*/
virtual void encodeHeaders(const Http::RequestHeaderMap& headers, bool end_stream) PURE;
virtual Http::Status encodeHeaders(const Http::RequestHeaderMap& headers, bool end_stream) PURE;
/**
* Encode trailers. This implicitly ends the stream.
* @param trailers supplies the trailers to encode.
Expand Down
2 changes: 2 additions & 0 deletions include/envoy/stream_info/stream_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ struct ResponseCodeDetailValues {
const std::string AdminFilterResponse = "admin_filter_response";
// The original stream was replaced with an internal redirect.
const std::string InternalRedirect = "internal_redirect";
// The request was rejected because configured filters erroneously removed required headers.
const std::string FilterRemovedRequiredHeaders = "filter_removed_required_headers";
// Changes or additions to details should be reflected in
// docs/root/configuration/http/http_conn_man/response_code_details_details.rst
};
Expand Down
1 change: 1 addition & 0 deletions source/common/http/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ envoy_cc_library(
],
deps = [
":header_map_lib",
":status_lib",
":utility_lib",
"//include/envoy/common:regex_interface",
"//include/envoy/http:header_map_interface",
Expand Down
5 changes: 3 additions & 2 deletions source/common/http/codec_wrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ class ResponseDecoderWrapper : public ResponseDecoder {
class RequestEncoderWrapper : public RequestEncoder {
public:
// RequestEncoder
void encodeHeaders(const RequestHeaderMap& headers, bool end_stream) override {
inner_.encodeHeaders(headers, end_stream);
Status encodeHeaders(const RequestHeaderMap& headers, bool end_stream) override {
RETURN_IF_ERROR(inner_.encodeHeaders(headers, end_stream));
if (end_stream) {
onEncodeComplete();
}
return okStatus();
}

void encodeData(Buffer::Instance& data, bool end_stream) override {
Expand Down
26 changes: 26 additions & 0 deletions source/common/http/filter_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,32 @@ void ActiveStreamDecoderFilter::onDecoderFilterAboveWriteBufferHighWatermark() {
parent_.filter_manager_callbacks_.onDecoderFilterAboveWriteBufferHighWatermark();
}

FilterHeadersStatus ActiveStreamDecoderFilter::decodeHeaders(RequestHeaderMap& headers,
bool end_stream) {
// Each decoder filter instance checks if the request passed to the filter is gRPC
// so that we can issue gRPC local responses to gRPC requests. Filter's decodeHeaders()
// called here may change the content type, so we must check it before the call.

is_grpc_request_ = Grpc::Common::isGrpcRequestHeaders(headers);
FilterHeadersStatus status = handle_->decodeHeaders(headers, end_stream);

// Validate filters did not erroneously remove required headers. If they do, send a direct
// response.
const Http::Status header_status = HeaderUtility::checkRequiredHeaders(headers);
Comment thread
alyssawilk marked this conversation as resolved.
Outdated
if (!header_status.ok()) {
ENVOY_STREAM_LOG(debug, "filter={} removed required headers", parent_,
static_cast<const void*>(this));
parent_.stream_info_.setResponseFlag(StreamInfo::ResponseFlag::DownstreamProtocolError);
const std::string details =
absl::StrCat(StreamInfo::ResponseCodeDetails::get().FilterRemovedRequiredHeaders, "{",
header_status.message(), "}");
sendLocalReply(Http::Code::ServiceUnavailable, header_status.message(), nullptr, absl::nullopt,
Comment thread
asraa marked this conversation as resolved.
Outdated
details);
return FilterHeadersStatus::StopIteration;
}
return status;
}

void ActiveStreamDecoderFilter::requestDataTooLarge() {
ENVOY_STREAM_LOG(debug, "request data too large watermark exceeded", parent_);
if (parent_.state_.decoder_filters_streaming_) {
Expand Down
9 changes: 1 addition & 8 deletions source/common/http/filter_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,7 @@ struct ActiveStreamDecoderFilter : public ActiveStreamFilterBase,

Network::Socket::OptionsSharedPtr getUpstreamSocketOptions() const override;

// Each decoder filter instance checks if the request passed to the filter is gRPC
// so that we can issue gRPC local responses to gRPC requests. Filter's decodeHeaders()
// called here may change the content type, so we must check it before the call.
FilterHeadersStatus decodeHeaders(RequestHeaderMap& headers, bool end_stream) {
is_grpc_request_ = Grpc::Common::isGrpcRequestHeaders(headers);
FilterHeadersStatus status = handle_->decodeHeaders(headers, end_stream);
return status;
}
FilterHeadersStatus decodeHeaders(RequestHeaderMap& headers, bool end_stream);

void requestDataTooLarge();
void requestDataDrained();
Expand Down
22 changes: 22 additions & 0 deletions source/common/http/header_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -276,5 +276,27 @@ bool HeaderUtility::shouldCloseConnection(Http::Protocol protocol,
return false;
}

Http::Status HeaderUtility::checkRequiredHeaders(const Http::RequestHeaderMap& headers) {
if (!headers.Method()) {
return absl::InvalidArgumentError(
absl::StrCat("missing required header: ", Envoy::Http::Headers::get().Method.get()));
}
bool is_connect = Http::HeaderUtility::isConnect(headers);
if (is_connect) {
if (!headers.Host()) {
// Host header must be present for CONNECT request.
return absl::InvalidArgumentError(
absl::StrCat("missing required header: ", Envoy::Http::Headers::get().Host.get()));
}
} else {
if (!headers.Path()) {
// :path header must be present for non-CONNECT requests.
return absl::InvalidArgumentError(
absl::StrCat("missing required header: ", Envoy::Http::Headers::get().Path.get()));
}
}
return Http::okStatus();
}

} // namespace Http
} // namespace Envoy
9 changes: 9 additions & 0 deletions source/common/http/header_utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "envoy/http/protocol.h"
#include "envoy/type/v3/range.pb.h"

#include "common/http/status.h"
#include "common/protobuf/protobuf.h"

namespace Envoy {
Expand Down Expand Up @@ -174,6 +175,14 @@ class HeaderUtility {
* @brief Remove the port part from host/authority header if it is equal to provided port
*/
static void stripPortFromHost(RequestHeaderMap& headers, uint32_t listener_port);

/* Does a common header check ensuring required headers are present.
* Required request headers include :method header, :path for non-CONNECT requests, and
* host/authority for HTTP/1.1 or CONNECT requests.
* @return Status containing the result. If failed, message includes details on which header was
* missing.
*/
static Http::Status checkRequiredHeaders(const Http::RequestHeaderMap& headers);
};
} // namespace Http
} // namespace Envoy
15 changes: 6 additions & 9 deletions source/common/http/http1/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -366,20 +366,16 @@ void ResponseEncoderImpl::encodeHeaders(const ResponseHeaderMap& headers, bool e

static const char REQUEST_POSTFIX[] = " HTTP/1.1\r\n";

void RequestEncoderImpl::encodeHeaders(const RequestHeaderMap& headers, bool end_stream) {
Status RequestEncoderImpl::encodeHeaders(const RequestHeaderMap& headers, bool end_stream) {
// Required headers must be present. This can only happen by some erroneous processing after the
// downstream codecs decode.
RETURN_IF_ERROR(HeaderUtility::checkRequiredHeaders(headers));
Comment thread
asraa marked this conversation as resolved.

const HeaderEntry* method = headers.Method();
const HeaderEntry* path = headers.Path();
const HeaderEntry* host = headers.Host();
bool is_connect = HeaderUtility::isConnect(headers);

// TODO(#10878): Include missing host header for CONNECT.
// The RELEASE_ASSERT below does not change the existing behavior of `encodeHeaders`.
// The `encodeHeaders` used to throw on errors. Callers of `encodeHeaders()` do not catch
// exceptions and this would cause abnormal process termination in error cases. This change
// replaces abnormal process termination from unhandled exception with the RELEASE_ASSERT. Further
// work will replace this RELEASE_ASSERT with proper error handling.
RELEASE_ASSERT(method && (path || is_connect), ":method and :path must be specified");

if (method->value() == Headers::get().MethodValues.Head) {
head_request_ = true;
} else if (method->value() == Headers::get().MethodValues.Connect) {
Expand All @@ -400,6 +396,7 @@ void RequestEncoderImpl::encodeHeaders(const RequestHeaderMap& headers, bool end
connection_.copyToBuffer(REQUEST_POSTFIX, sizeof(REQUEST_POSTFIX) - 1);

encodeHeadersBase(headers, absl::nullopt, end_stream);
return okStatus();
}

int ConnectionImpl::setAndCheckCallbackStatus(Status&& status) {
Expand Down
2 changes: 1 addition & 1 deletion source/common/http/http1/codec_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ class RequestEncoderImpl : public StreamEncoderImpl, public RequestEncoder {
bool connectRequest() const { return connect_request_; }

// Http::RequestEncoder
void encodeHeaders(const RequestHeaderMap& headers, bool end_stream) override;
Status encodeHeaders(const RequestHeaderMap& headers, bool end_stream) override;
void encodeTrailers(const RequestTrailerMap& trailers) override { encodeTrailersBase(trailers); }

private:
Expand Down
3 changes: 2 additions & 1 deletion source/common/http/http1/codec_impl_legacy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ void ResponseEncoderImpl::encodeHeaders(const ResponseHeaderMap& headers, bool e

static const char REQUEST_POSTFIX[] = " HTTP/1.1\r\n";

void RequestEncoderImpl::encodeHeaders(const RequestHeaderMap& headers, bool end_stream) {
Status RequestEncoderImpl::encodeHeaders(const RequestHeaderMap& headers, bool end_stream) {
const HeaderEntry* method = headers.Method();
const HeaderEntry* path = headers.Path();
const HeaderEntry* host = headers.Host();
Expand Down Expand Up @@ -397,6 +397,7 @@ void RequestEncoderImpl::encodeHeaders(const RequestHeaderMap& headers, bool end
connection_.copyToBuffer(REQUEST_POSTFIX, sizeof(REQUEST_POSTFIX) - 1);

encodeHeadersBase(headers, absl::nullopt, end_stream);
return okStatus();
}

http_parser_settings ConnectionImpl::settings_{
Expand Down
2 changes: 1 addition & 1 deletion source/common/http/http1/codec_impl_legacy.h
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class RequestEncoderImpl : public StreamEncoderImpl, public RequestEncoder {
bool connectRequest() const { return connect_request_; }

// Http::RequestEncoder
void encodeHeaders(const RequestHeaderMap& headers, bool end_stream) override;
Status encodeHeaders(const RequestHeaderMap& headers, bool end_stream) override;
void encodeTrailers(const RequestTrailerMap& trailers) override { encodeTrailersBase(trailers); }

private:
Expand Down
8 changes: 6 additions & 2 deletions source/common/http/http2/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,11 @@ void ConnectionImpl::StreamImpl::encodeHeadersBase(const std::vector<nghttp2_nv>
}
}

void ConnectionImpl::ClientStreamImpl::encodeHeaders(const RequestHeaderMap& headers,
bool end_stream) {
Status ConnectionImpl::ClientStreamImpl::encodeHeaders(const RequestHeaderMap& headers,
bool end_stream) {
// Required headers must be present. This can only happen by some erroneous processing after the
// downstream codecs decode.
RETURN_IF_ERROR(HeaderUtility::checkRequiredHeaders(headers));
// This must exist outside of the scope of isUpgrade as the underlying memory is
// needed until encodeHeadersBase has been called.
std::vector<nghttp2_nv> final_headers;
Expand All @@ -211,6 +214,7 @@ void ConnectionImpl::ClientStreamImpl::encodeHeaders(const RequestHeaderMap& hea
buildHeaders(final_headers, headers);
}
encodeHeadersBase(final_headers, end_stream);
return okStatus();
}

void ConnectionImpl::ServerStreamImpl::encodeHeaders(const ResponseHeaderMap& headers,
Expand Down
2 changes: 1 addition & 1 deletion source/common/http/http2/codec_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable<Log
}

// RequestEncoder
void encodeHeaders(const RequestHeaderMap& headers, bool end_stream) override;
Status encodeHeaders(const RequestHeaderMap& headers, bool end_stream) override;
void encodeTrailers(const RequestTrailerMap& trailers) override {
encodeTrailersBase(trailers);
}
Expand Down
5 changes: 3 additions & 2 deletions source/common/http/http2/codec_impl_legacy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ void ConnectionImpl::StreamImpl::encodeHeadersBase(const std::vector<nghttp2_nv>
parent_.checkProtocolConstraintViolation();
}

void ConnectionImpl::ClientStreamImpl::encodeHeaders(const RequestHeaderMap& headers,
bool end_stream) {
Status ConnectionImpl::ClientStreamImpl::encodeHeaders(const RequestHeaderMap& headers,
bool end_stream) {
// This must exist outside of the scope of isUpgrade as the underlying memory is
// needed until encodeHeadersBase has been called.
std::vector<nghttp2_nv> final_headers;
Expand All @@ -211,6 +211,7 @@ void ConnectionImpl::ClientStreamImpl::encodeHeaders(const RequestHeaderMap& hea
buildHeaders(final_headers, headers);
}
encodeHeadersBase(final_headers, end_stream);
return okStatus();
}

void ConnectionImpl::ServerStreamImpl::encodeHeaders(const ResponseHeaderMap& headers,
Expand Down
2 changes: 1 addition & 1 deletion source/common/http/http2/codec_impl_legacy.h
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable<Log
}

// RequestEncoder
void encodeHeaders(const RequestHeaderMap& headers, bool end_stream) override;
Status encodeHeaders(const RequestHeaderMap& headers, bool end_stream) override;
void encodeTrailers(const RequestTrailerMap& trailers) override {
encodeTrailersBase(trailers);
}
Expand Down
5 changes: 0 additions & 5 deletions source/common/router/router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -329,11 +329,6 @@ void Filter::chargeUpstreamCode(Http::Code code,
}

Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) {
// Do a common header check. We make sure that all outgoing requests have all HTTP/2 headers.
// These get stripped by HTTP/1 codec where applicable.
ASSERT(headers.Method());
ASSERT(headers.Host());

downstream_headers_ = &headers;

// Extract debug configuration from filter state. This is used further along to determine whether
Expand Down
17 changes: 15 additions & 2 deletions source/common/router/upstream_request.cc
Original file line number Diff line number Diff line change
Expand Up @@ -416,10 +416,23 @@ void UpstreamRequest::onPoolReady(
}
}

upstream_->encodeHeaders(*parent_.downstreamHeaders(), shouldSendEndStream());

const Http::Status status =
upstream_->encodeHeaders(*parent_.downstreamHeaders(), shouldSendEndStream());
calling_encode_headers_ = false;

if (!status.ok()) {
Comment thread
asraa marked this conversation as resolved.
// It is possible that encodeHeaders() fails. This can happen if filters or other extensions
// erroneously remove required headers.
stream_info_.setResponseFlag(StreamInfo::ResponseFlag::DownstreamProtocolError);
const std::string details =
absl::StrCat(StreamInfo::ResponseCodeDetails::get().FilterRemovedRequiredHeaders, "{",
status.message(), "}");
ENVOY_LOG_MISC(info, "{}", status.message());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Debug log level? Should this local reply block be shared with the FM code in a small utility?

parent_.callbacks()->sendLocalReply(Http::Code::ServiceUnavailable, status.message(), nullptr,
absl::nullopt, details);
return;
}

if (!paused_for_connect_) {
encodeBodyAndTrailers();
}
Expand Down
4 changes: 3 additions & 1 deletion source/common/tcp_proxy/upstream.cc
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ void HttpUpstream::setRequestEncoder(Http::RequestEncoder& request_encoder, bool
{Http::Headers::get().Scheme, scheme},
{Http::Headers::get().Path, "/"},
{Http::Headers::get().Host, hostname_}});
request_encoder_->encodeHeaders(*headers, false);
const auto status = request_encoder_->encodeHeaders(*headers, false);
// Encoding can only fail on missing required request headers.
ASSERT(status.ok());
}

void HttpUpstream::resetEncoder(Network::ConnectionEvent event, bool inform_downstream) {
Expand Down
8 changes: 6 additions & 2 deletions source/common/upstream/health_checker_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,9 @@ void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onInterval() {
stream_info.setDownstreamRemoteAddress(local_address_);
stream_info.onUpstreamHostSelected(host_);
parent_.request_headers_parser_->evaluateHeaders(*request_headers, stream_info);
request_encoder->encodeHeaders(*request_headers, true);
auto status = request_encoder->encodeHeaders(*request_headers, true);
// Encoding will only fail if required request headers are missing.
ASSERT(status.ok());
}

void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onResetStream(Http::StreamResetReason,
Expand Down Expand Up @@ -691,7 +693,9 @@ void GrpcHealthCheckerImpl::GrpcActiveHealthCheckSession::onInterval() {
Router::FilterUtility::setUpstreamScheme(
headers_message->headers(), host_->transportSocketFactory().implementsSecureTransport());

request_encoder_->encodeHeaders(headers_message->headers(), false);
auto status = request_encoder_->encodeHeaders(headers_message->headers(), false);
// Encoding will only fail if required headers are missing.
ASSERT(status.ok());

grpc::health::v1::HealthCheckRequest request;
if (parent_.service_name_.has_value()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ EnvoyQuicClientStream::EnvoyQuicClientStream(quic::PendingStream* pending,
16 * 1024, [this]() { runLowWatermarkCallbacks(); },
[this]() { runHighWatermarkCallbacks(); }) {}

void EnvoyQuicClientStream::encodeHeaders(const Http::RequestHeaderMap& headers, bool end_stream) {
Http::Status EnvoyQuicClientStream::encodeHeaders(const Http::RequestHeaderMap& headers,
bool end_stream) {
ENVOY_STREAM_LOG(debug, "encodeHeaders: (end_stream={}) {}.", *this, end_stream, headers);
quic::QuicStream* writing_stream =
quic::VersionUsesHttp3(transport_version())
Expand All @@ -60,6 +61,7 @@ void EnvoyQuicClientStream::encodeHeaders(const Http::RequestHeaderMap& headers,
// IETF QUIC sends HEADER frame on current stream. After writing headers, the
// buffer may increase.
maybeCheckWatermark(bytes_to_send_old, bytes_to_send_new, *filterManagerConnection());
return Http::okStatus();
}

void EnvoyQuicClientStream::encodeData(Buffer::Instance& data, bool end_stream) {
Expand Down
Loading