Allow HTTP/3 requests to have a "host" header, per the RFC Protected by quic_reloadable_flag_quic_allow_host_in_request. PiperOrigin-RevId: 666964686
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h index aef9ee1..146bb11 100755 --- a/quiche/common/quiche_feature_flags_list.h +++ b/quiche/common/quiche_feature_flags_list.h
@@ -12,6 +12,7 @@ QUICHE_FLAG(bool, quiche_reloadable_flag_quic_act_upon_invalid_header, true, true, "If true, reject or send error response code upon receiving invalid request or response headers.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_add_stream_info_to_idle_close_detail, false, true, "If true, include stream information in idle timeout connection close detail.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_allow_client_enabled_bbr_v2, true, true, "If true, allow client to enable BBRv2 on server via connection option 'B2ON'.") +QUICHE_FLAG(bool, quiche_reloadable_flag_quic_allow_host_in_request, false, false, "If true, requests with a host header will be allowed.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_bbr2_enable_bbpd_by_default, true, true, "If true, QUIC BBR2 will use a 0.91 PROBE_DOWN gain by default.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_bbr2_extra_acked_window, false, true, "When true, the BBR4 copt sets the extra_acked window to 20 RTTs and BBR5 sets it to 40 RTTs.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_bbr2_probe_two_rounds, true, true, "When true, the BB2U copt causes BBR2 to wait two rounds with out draining the queue before exiting PROBE_UP and BB2S has the same effect in STARTUP.")
diff --git a/quiche/quic/core/http/quic_spdy_server_stream_base.cc b/quiche/quic/core/http/quic_spdy_server_stream_base.cc index 0302542..e53d8b4 100644 --- a/quiche/quic/core/http/quic_spdy_server_stream_base.cc +++ b/quiche/quic/core/http/quic_spdy_server_stream_base.cc
@@ -4,6 +4,7 @@ #include "quiche/quic/core/http/quic_spdy_server_stream_base.h" +#include <optional> #include <string> #include <utility> @@ -14,6 +15,7 @@ #include "quiche/quic/platform/api/quic_flag_utils.h" #include "quiche/quic/platform/api/quic_flags.h" #include "quiche/quic/platform/api/quic_logging.h" +#include "quiche/common/platform/api/quiche_flag_utils.h" #include "quiche/common/quiche_text_utils.h" namespace quic { @@ -63,7 +65,9 @@ bool saw_path = false; bool saw_scheme = false; bool saw_method = false; - bool saw_authority = false; + std::optional<std::string> authority; + std::optional<std::string> host; + bool is_extended_connect = false; // Check if it is missing any required headers and if there is any disallowed // ones. @@ -86,12 +90,14 @@ } else if (pair.first == ":path") { saw_path = true; } else if (pair.first == ":authority") { - saw_authority = true; + authority = pair.second; } else if (absl::StrContains(pair.first, ":")) { set_invalid_request_details( absl::StrCat("Unexpected ':' in header ", pair.first, ".")); QUIC_DLOG(ERROR) << invalid_request_details(); return false; + } else if (pair.first == "host") { + host = pair.second; } if (is_extended_connect) { if (!spdy_session()->allow_extended_connect()) { @@ -110,8 +116,27 @@ } } + if (GetQuicReloadableFlag(quic_allow_host_in_request)) { + // If the :scheme pseudo-header field identifies a scheme that has a + // mandatory authority component (including "http" and "https"), the + // request MUST contain either an :authority pseudo-header field or a + // Host header field. If these fields are present, they MUST NOT be + // empty. If both fields are present, they MUST contain the same value. + // If the scheme does not have a mandatory authority component and none + // is provided in the request target, the request MUST NOT contain the + // :authority pseudo-header or Host header fields. + // + // https://datatracker.ietf.org/doc/html/rfc9114#section-4.3.1 + QUICHE_RELOADABLE_FLAG_COUNT_N(quic_allow_host_in_request, 2, 3); + if (host && (!authority || authority != host)) { + QUIC_CODE_COUNT(http3_host_header_does_not_match_authority); + set_invalid_request_details("Host header does not match authority"); + return false; + } + } + if (is_extended_connect) { - if (saw_scheme && saw_path && saw_authority) { + if (saw_scheme && saw_path && authority) { // Saw all the required pseudo headers. return true; } @@ -132,10 +157,13 @@ return true; } // Check non-CONNECT request. - if (saw_method && saw_authority && saw_path && saw_scheme) { + if (saw_method && authority && saw_path && saw_scheme) { return true; } - set_invalid_request_details("Missing required pseudo headers."); + set_invalid_request_details( + absl::StrCat("Missing required pseudo headers. saw_method=", saw_method, + ", authority=", authority.has_value(), + ", saw_path=", saw_path, ", saw_scheme=", saw_scheme)); QUIC_DLOG(ERROR) << invalid_request_details(); return false; }
diff --git a/quiche/quic/core/http/quic_spdy_server_stream_base_test.cc b/quiche/quic/core/http/quic_spdy_server_stream_base_test.cc index b136fa7..ae43178 100644 --- a/quiche/quic/core/http/quic_spdy_server_stream_base_test.cc +++ b/quiche/quic/core/http/quic_spdy_server_stream_base_test.cc
@@ -289,6 +289,63 @@ EXPECT_TRUE(stream_->rst_sent()); } +TEST_F(QuicSpdyServerStreamBaseTest, HostHeaderWithoutAuthority) { + SetQuicReloadableFlag(quic_act_upon_invalid_header, true); + SetQuicReloadableFlag(quic_allow_host_in_request, true); + // A request with host but without authority should be rejected. + QuicHeaderList header_list; + header_list.OnHeader("host", "www.google.com:4433"); + header_list.OnHeader(":scheme", "http"); + header_list.OnHeader(":method", "POST"); + header_list.OnHeader(":path", "/path"); + header_list.OnHeaderBlockEnd(128, 128); + + EXPECT_CALL( + session_, + MaybeSendRstStreamFrame( + _, QuicResetStreamError::FromInternal(QUIC_BAD_APPLICATION_PAYLOAD), + _)); + stream_->OnStreamHeaderList(/*fin=*/false, 0, header_list); + EXPECT_TRUE(stream_->rst_sent()); +} + +TEST_F(QuicSpdyServerStreamBaseTest, HostHeaderWitDifferentAuthority) { + SetQuicReloadableFlag(quic_act_upon_invalid_header, true); + SetQuicReloadableFlag(quic_allow_host_in_request, true); + // A request with host that does not match authority should be rejected. + QuicHeaderList header_list; + header_list.OnHeader(":authority", "www.google.com:4433"); + header_list.OnHeader(":scheme", "http"); + header_list.OnHeader(":method", "POST"); + header_list.OnHeader(":path", "/path"); + header_list.OnHeader("host", "mail.google.com:4433"); + header_list.OnHeaderBlockEnd(128, 128); + + EXPECT_CALL( + session_, + MaybeSendRstStreamFrame( + _, QuicResetStreamError::FromInternal(QUIC_BAD_APPLICATION_PAYLOAD), + _)); + stream_->OnStreamHeaderList(/*fin=*/false, 0, header_list); + EXPECT_TRUE(stream_->rst_sent()); +} + +TEST_F(QuicSpdyServerStreamBaseTest, ValidHostHeader) { + SetQuicReloadableFlag(quic_act_upon_invalid_header, true); + SetQuicReloadableFlag(quic_allow_host_in_request, true); + // A request with host that matches authority should be accepted. + QuicHeaderList header_list; + header_list.OnHeader(":authority", "www.google.com:4433"); + header_list.OnHeader(":scheme", "http"); + header_list.OnHeader(":method", "POST"); + header_list.OnHeader(":path", "/path"); + header_list.OnHeader("host", "www.google.com:4433"); + header_list.OnHeaderBlockEnd(128, 128); + + stream_->OnStreamHeaderList(/*fin=*/false, 0, header_list); + EXPECT_FALSE(stream_->rst_sent()); +} + TEST_F(QuicSpdyServerStreamBaseTest, EmptyHeaders) { SetQuicReloadableFlag(quic_act_upon_invalid_header, true); quiche::HttpHeaderBlock empty_header;
diff --git a/quiche/quic/core/http/quic_spdy_stream.cc b/quiche/quic/core/http/quic_spdy_stream.cc index 933cd50..e3e10f2 100644 --- a/quiche/quic/core/http/quic_spdy_stream.cc +++ b/quiche/quic/core/http/quic_spdy_stream.cc
@@ -1787,11 +1787,18 @@ return false; } if (name == ":status") { + // Note: remove this when deprecating quic_allow_host_in_request. is_response = !pair.second.empty(); } - if (is_response && name == "host") { - // Host header is allowed in response. - continue; + if (name == "host") { + if (GetQuicReloadableFlag(quic_allow_host_in_request)) { + QUICHE_RELOADABLE_FLAG_COUNT_N(quic_allow_host_in_request, 1, 3); + continue; + } + if (is_response) { + // Host header is allowed in response. + continue; + } } if (http2::GetInvalidHttp2HeaderSet().contains(name)) { invalid_request_details_ = absl::StrCat(name, " header is not allowed");
diff --git a/quiche/quic/core/http/quic_spdy_stream_test.cc b/quiche/quic/core/http/quic_spdy_stream_test.cc index 5ede717..a7efd70 100644 --- a/quiche/quic/core/http/quic_spdy_stream_test.cc +++ b/quiche/quic/core/http/quic_spdy_stream_test.cc
@@ -3520,6 +3520,22 @@ stream_->invalid_request_details()); } +TEST_P(QuicSpdyStreamTest, HostHeaderInRequest) { + if (!UsesHttp3()) { + return; + } + + Initialize(kShouldProcessData); + + headers_["host"] = "foo"; + if (GetQuicReloadableFlag(quic_allow_host_in_request)) { + EXPECT_TRUE(stream_->ValidateReceivedHeaders(AsHeaderList(headers_))); + } else { + EXPECT_FALSE(stream_->ValidateReceivedHeaders(AsHeaderList(headers_))); + EXPECT_EQ("host header is not allowed", stream_->invalid_request_details()); + } +} + } // namespace } // namespace test } // namespace quic