Implement the lower-level part of WebTransport subprotocol negotiation.
This is relatively easy, since due to the way our implementation is structured, the headers are entirely up to the API user; we just need to store the actual selected value so that we can pass it to the application.
PiperOrigin-RevId: 746142355
diff --git a/quiche/quic/core/http/quic_spdy_stream.cc b/quiche/quic/core/http/quic_spdy_stream.cc
index 8b5894e..70a6b2e 100644
--- a/quiche/quic/core/http/quic_spdy_stream.cc
+++ b/quiche/quic/core/http/quic_spdy_stream.cc
@@ -9,8 +9,10 @@
#include <optional>
#include <string>
#include <utility>
+#include <vector>
#include "absl/base/macros.h"
+#include "absl/status/statusor.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
@@ -39,10 +41,12 @@
#include "quiche/quic/platform/api/quic_logging.h"
#include "quiche/quic/platform/api/quic_testvalue.h"
#include "quiche/common/capsule.h"
+#include "quiche/common/http/http_header_block.h"
#include "quiche/common/platform/api/quiche_flag_utils.h"
#include "quiche/common/platform/api/quiche_logging.h"
#include "quiche/common/quiche_mem_slice_storage.h"
#include "quiche/common/quiche_text_utils.h"
+#include "quiche/web_transport/web_transport_headers.h"
using ::quiche::Capsule;
using ::quiche::CapsuleType;
@@ -1426,6 +1430,9 @@
return;
}
if (session()->perspective() != Perspective::IS_CLIENT) {
+ if (web_transport_ != nullptr) {
+ web_transport_->MaybeSetSubprotocolFromResponseHeaders(headers);
+ }
return;
}
QUICHE_DCHECK(IsValidWebTransportSessionId(id(), version()));
@@ -1446,6 +1453,23 @@
web_transport_ =
std::make_unique<WebTransportHttp3>(spdy_session_, this, id());
+
+ // Store the offered subprotocols so that we can later validate the
+ // server-selected one against those.
+ const auto subprotocol_offer_it =
+ headers.find(webtransport::kSubprotocolRequestHeader);
+ if (subprotocol_offer_it != headers.end()) {
+ absl::StatusOr<std::vector<std::string>> subprotocols_offered =
+ webtransport::ParseSubprotocolRequestHeader(
+ subprotocol_offer_it->second);
+ if (subprotocols_offered.ok()) {
+ web_transport_->set_subprotocols_offered(
+ *std::move(subprotocols_offered));
+ } else {
+ QUIC_DLOG(WARNING) << "Attempting to send WebTransport subprotocols that "
+ "cannot be parsed.";
+ }
+ }
}
void QuicSpdyStream::OnCanWriteNewData() {
diff --git a/quiche/quic/core/http/quic_spdy_stream_test.cc b/quiche/quic/core/http/quic_spdy_stream_test.cc
index be64b2c..1536a57 100644
--- a/quiche/quic/core/http/quic_spdy_stream_test.cc
+++ b/quiche/quic/core/http/quic_spdy_stream_test.cc
@@ -26,6 +26,7 @@
#include "quiche/quic/core/http/web_transport_http3.h"
#include "quiche/quic/core/qpack/value_splitting_header_list.h"
#include "quiche/quic/core/quic_connection.h"
+#include "quiche/quic/core/quic_stream_priority.h"
#include "quiche/quic/core/quic_stream_sequencer_buffer.h"
#include "quiche/quic/core/quic_utils.h"
#include "quiche/quic/core/quic_versions.h"
@@ -44,6 +45,7 @@
#include "quiche/quic/test_tools/quic_stream_peer.h"
#include "quiche/quic/test_tools/quic_test_utils.h"
#include "quiche/common/capsule.h"
+#include "quiche/common/http/http_header_block.h"
#include "quiche/common/quiche_ip_address.h"
#include "quiche/common/quiche_mem_slice_storage.h"
#include "quiche/common/simple_buffer_allocator.h"
@@ -3287,7 +3289,7 @@
EXPECT_FALSE(stream_->write_side_closed());
}
-TEST_P(QuicSpdyStreamTest, ProcessOutgoingWebTransportHeaders) {
+TEST_P(QuicSpdyStreamTest, ProcessWebTransportHeadersAsClient) {
if (!UsesHttp3()) {
return;
}
@@ -3304,20 +3306,97 @@
EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _))
.Times(AnyNumber());
- quiche::HttpHeaderBlock headers;
- headers[":method"] = "CONNECT";
- headers[":protocol"] = "webtransport";
- stream_->WriteHeaders(std::move(headers), /*fin=*/false, nullptr);
+ quiche::HttpHeaderBlock request_headers;
+ request_headers[":method"] = "CONNECT";
+ request_headers[":protocol"] = "webtransport";
+ request_headers["wt-available-protocols"] = "moqt-00, moqt-01; foo=bar";
+ stream_->WriteHeaders(std::move(request_headers), /*fin=*/false, nullptr);
ASSERT_TRUE(stream_->web_transport() != nullptr);
EXPECT_EQ(stream_->id(), stream_->web_transport()->id());
+ EXPECT_THAT(stream_->web_transport()->subprotocols_offered(),
+ ElementsAre("moqt-00", "moqt-01"));
+
+ quiche::HttpHeaderBlock response_headers;
+ response_headers[":status"] = "200";
+ response_headers["wt-protocol"] = "moqt-01";
+ stream_->web_transport()->HeadersReceived(response_headers);
+ EXPECT_EQ(stream_->web_transport()->rejection_reason(),
+ WebTransportHttp3RejectionReason::kNone);
+ EXPECT_EQ(stream_->web_transport()->GetNegotiatedSubprotocol(), "moqt-01");
}
-TEST_P(QuicSpdyStreamTest, ProcessIncomingWebTransportHeaders) {
+TEST_P(QuicSpdyStreamTest, WebTransportRejectSubprotocolsThatWereNotOffered) {
if (!UsesHttp3()) {
return;
}
- Initialize(kShouldProcessData);
+ InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
+ session_->set_local_http_datagram_support(HttpDatagramSupport::kRfc);
+ session_->EnableWebTransport();
+ session_->OnSetting(SETTINGS_ENABLE_CONNECT_PROTOCOL, 1);
+ QuicSpdySessionPeer::EnableWebTransport(session_.get());
+ QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+ HttpDatagramSupport::kRfc);
+
+ EXPECT_CALL(*stream_, WriteHeadersMock(false));
+ EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _))
+ .Times(AnyNumber());
+
+ quiche::HttpHeaderBlock request_headers;
+ request_headers[":method"] = "CONNECT";
+ request_headers[":protocol"] = "webtransport";
+ request_headers["wt-available-protocols"] = "moqt-00, moqt-01; foo=bar";
+ stream_->WriteHeaders(std::move(request_headers), /*fin=*/false, nullptr);
+ ASSERT_TRUE(stream_->web_transport() != nullptr);
+
+ quiche::HttpHeaderBlock response_headers;
+ response_headers[":status"] = "200";
+ response_headers["wt-protocol"] = "moqt-02";
+ stream_->web_transport()->HeadersReceived(response_headers);
+ EXPECT_EQ(stream_->web_transport()->rejection_reason(),
+ WebTransportHttp3RejectionReason::kSubprotocolMismatch);
+ EXPECT_EQ(stream_->web_transport()->GetNegotiatedSubprotocol(), std::nullopt);
+}
+
+TEST_P(QuicSpdyStreamTest, WebTransportInvalidSubprotocolResponse) {
+ if (!UsesHttp3()) {
+ return;
+ }
+
+ InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT);
+ session_->set_local_http_datagram_support(HttpDatagramSupport::kRfc);
+ session_->EnableWebTransport();
+ session_->OnSetting(SETTINGS_ENABLE_CONNECT_PROTOCOL, 1);
+ QuicSpdySessionPeer::EnableWebTransport(session_.get());
+ QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(),
+ HttpDatagramSupport::kRfc);
+
+ EXPECT_CALL(*stream_, WriteHeadersMock(false));
+ EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _))
+ .Times(AnyNumber());
+
+ quiche::HttpHeaderBlock request_headers;
+ request_headers[":method"] = "CONNECT";
+ request_headers[":protocol"] = "webtransport";
+ request_headers["wt-available-protocols"] = "moqt-00, moqt-01; foo=bar";
+ stream_->WriteHeaders(std::move(request_headers), /*fin=*/false, nullptr);
+ ASSERT_TRUE(stream_->web_transport() != nullptr);
+
+ quiche::HttpHeaderBlock response_headers;
+ response_headers[":status"] = "200";
+ response_headers["wt-protocol"] = "12345.67";
+ stream_->web_transport()->HeadersReceived(response_headers);
+ EXPECT_EQ(stream_->web_transport()->rejection_reason(),
+ WebTransportHttp3RejectionReason::kSubprotocolParseError);
+ EXPECT_EQ(stream_->web_transport()->GetNegotiatedSubprotocol(), std::nullopt);
+}
+
+TEST_P(QuicSpdyStreamTest, ProcessWebTransportHeadersAsServer) {
+ if (!UsesHttp3()) {
+ return;
+ }
+
+ InitializeWithPerspective(kShouldProcessData, Perspective::IS_SERVER);
session_->set_local_http_datagram_support(HttpDatagramSupport::kRfc);
session_->EnableWebTransport();
QuicSpdySessionPeer::EnableWebTransport(session_.get());
@@ -3326,6 +3405,7 @@
headers_[":method"] = "CONNECT";
headers_[":protocol"] = "webtransport";
+ headers_["wt-available-protocols"] = "moqt-00, moqt-01; foo=bar";
stream_->OnStreamHeadersPriority(
spdy::SpdyStreamPrecedence(kV3HighestPriority));
@@ -3335,6 +3415,15 @@
EXPECT_FALSE(stream_->IsDoneReading());
ASSERT_TRUE(stream_->web_transport() != nullptr);
EXPECT_EQ(stream_->id(), stream_->web_transport()->id());
+
+ EXPECT_CALL(*stream_, WriteHeadersMock(false));
+ EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _))
+ .Times(AnyNumber());
+ quiche::HttpHeaderBlock response_headers;
+ response_headers[":status"] = "200";
+ response_headers["wt-protocol"] = "moqt-01";
+ stream_->WriteHeaders(std::move(response_headers), /*fin=*/false, nullptr);
+ EXPECT_EQ(stream_->web_transport()->GetNegotiatedSubprotocol(), "moqt-01");
}
TEST_P(QuicSpdyStreamTest, IncomingWebTransportStreamWhenUnsupported) {
diff --git a/quiche/quic/core/http/web_transport_http3.cc b/quiche/quic/core/http/web_transport_http3.cc
index d0687ee..86d6086 100644
--- a/quiche/quic/core/http/web_transport_http3.cc
+++ b/quiche/quic/core/http/web_transport_http3.cc
@@ -12,6 +12,8 @@
#include <vector>
+#include "absl/algorithm/container.h"
+#include "absl/status/statusor.h"
#include "absl/strings/string_view.h"
#include "quiche/quic/core/http/quic_spdy_session.h"
#include "quiche/quic/core/http/quic_spdy_stream.h"
@@ -24,8 +26,10 @@
#include "quiche/quic/core/quic_versions.h"
#include "quiche/quic/platform/api/quic_bug_tracker.h"
#include "quiche/common/capsule.h"
+#include "quiche/common/http/http_header_block.h"
#include "quiche/common/platform/api/quiche_logging.h"
#include "quiche/web_transport/web_transport.h"
+#include "quiche/web_transport/web_transport_headers.h"
#define ENDPOINT \
(session_->perspective() == Perspective::IS_SERVER ? "Server: " : "Client: ")
@@ -184,6 +188,12 @@
rejection_reason_ = WebTransportHttp3RejectionReason::kWrongStatusCode;
return;
}
+ WebTransportHttp3RejectionReason subprotocol_result =
+ MaybeSetSubprotocolFromResponseHeaders(headers);
+ if (subprotocol_result != WebTransportHttp3RejectionReason::kNone) {
+ rejection_reason_ = subprotocol_result;
+ return;
+ }
}
QUIC_DVLOG(1) << ENDPOINT << "WebTransport session " << id_ << " ready.";
@@ -473,4 +483,34 @@
webtransport_error_code / 0x1e;
}
+WebTransportHttp3RejectionReason
+WebTransportHttp3::MaybeSetSubprotocolFromResponseHeaders(
+ const quiche::HttpHeaderBlock& headers) {
+ auto subprotocol_it = headers.find(webtransport::kSubprotocolResponseHeader);
+ if (subprotocol_it == headers.end()) {
+ return WebTransportHttp3RejectionReason::kNone;
+ }
+
+ absl::StatusOr<std::string> subprotocol =
+ webtransport::ParseSubprotocolResponseHeader(subprotocol_it->second);
+ if (!subprotocol.ok()) {
+ QUIC_DVLOG(1) << ENDPOINT
+ << "WebTransport server has malformed WT-Protocol "
+ "header, rejecting.";
+ return WebTransportHttp3RejectionReason::kSubprotocolParseError;
+ }
+
+ if (session_->perspective() == Perspective::IS_CLIENT &&
+ !absl::c_linear_search(subprotocols_offered_, *subprotocol)) {
+ QUIC_DVLOG(1) << ENDPOINT
+ << "WebTransport server has offered a subprotocol value \""
+ << *subprotocol
+ << "\", which was not one of the ones offered, rejecting.";
+ return WebTransportHttp3RejectionReason::kSubprotocolMismatch;
+ }
+
+ subprotocol_selected_ = *std::move(subprotocol);
+ return WebTransportHttp3RejectionReason::kNone;
+}
+
} // namespace quic
diff --git a/quiche/quic/core/http/web_transport_http3.h b/quiche/quic/core/http/web_transport_http3.h
index b6d5110..c9950b6 100644
--- a/quiche/quic/core/http/web_transport_http3.h
+++ b/quiche/quic/core/http/web_transport_http3.h
@@ -7,9 +7,13 @@
#include <memory>
#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
#include "absl/base/attributes.h"
#include "absl/container/flat_hash_set.h"
+#include "absl/strings/string_view.h"
#include "absl/time/time.h"
#include "quiche/quic/core/http/quic_spdy_session.h"
#include "quiche/quic/core/http/web_transport_stream_adapter.h"
@@ -34,6 +38,8 @@
kWrongStatusCode,
kMissingDraftVersion,
kUnsupportedDraftVersion,
+ kSubprotocolMismatch,
+ kSubprotocolParseError,
};
// A session of WebTransport over HTTP/3. The session is owned by
@@ -117,6 +123,18 @@
void OnGoAwayReceived();
void OnDrainSessionReceived();
+ const std::vector<std::string>& subprotocols_offered() const {
+ return subprotocols_offered_;
+ }
+ void set_subprotocols_offered(std::vector<std::string> subprotocols_offered) {
+ subprotocols_offered_ = std::move(subprotocols_offered);
+ }
+ std::optional<std::string> GetNegotiatedSubprotocol() const override {
+ return subprotocol_selected_;
+ }
+ WebTransportHttp3RejectionReason MaybeSetSubprotocolFromResponseHeaders(
+ const quiche::HttpHeaderBlock& headers);
+
private:
// Notifies the visitor that the connection has been closed. Ensures that the
// visitor is only ever called once.
@@ -136,6 +154,12 @@
bool close_received_ = false;
bool close_notified_ = false;
+ // On client side, stores the offered subprotocols.
+ std::vector<std::string> subprotocols_offered_;
+ // Stores the actually selected subprotocol, both on the client and on the
+ // server.
+ std::optional<std::string> subprotocol_selected_;
+
quiche::SingleUseCallback<void()> drain_callback_ = nullptr;
WebTransportHttp3RejectionReason rejection_reason_ =
diff --git a/quiche/quic/core/quic_generic_session.h b/quiche/quic/core/quic_generic_session.h
index 384f0a3..d8c220a 100644
--- a/quiche/quic/core/quic_generic_session.h
+++ b/quiche/quic/core/quic_generic_session.h
@@ -7,6 +7,9 @@
#include <cstdint>
#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
#include "absl/algorithm/container.h"
#include "absl/strings/string_view.h"
@@ -106,6 +109,9 @@
}
void NotifySessionDraining() override {}
void SetOnDraining(quiche::SingleUseCallback<void()>) override {}
+ std::optional<std::string> GetNegotiatedSubprotocol() const override {
+ return alpn_;
+ }
void CloseSession(webtransport::SessionErrorCode error_code,
absl::string_view error_message) override {
diff --git a/quiche/web_transport/encapsulated/encapsulated_web_transport.cc b/quiche/web_transport/encapsulated/encapsulated_web_transport.cc
index 30ab1d4..7371945 100644
--- a/quiche/web_transport/encapsulated/encapsulated_web_transport.cc
+++ b/quiche/web_transport/encapsulated/encapsulated_web_transport.cc
@@ -794,4 +794,11 @@
QUICHE_BUG_IF(EncapsulatedWebTransport_SetPriority_order, !status.ok())
<< status;
}
+
+std::optional<std::string> EncapsulatedSession::GetNegotiatedSubprotocol()
+ const {
+ // TODO: implement.
+ return std::nullopt;
+}
+
} // namespace webtransport
diff --git a/quiche/web_transport/encapsulated/encapsulated_web_transport.h b/quiche/web_transport/encapsulated/encapsulated_web_transport.h
index 0fe2b97..e4e7f7a 100644
--- a/quiche/web_transport/encapsulated/encapsulated_web_transport.h
+++ b/quiche/web_transport/encapsulated/encapsulated_web_transport.h
@@ -102,6 +102,7 @@
SessionStats GetSessionStats() override;
void NotifySessionDraining() override;
void SetOnDraining(quiche::SingleUseCallback<void()> callback) override;
+ std::optional<std::string> GetNegotiatedSubprotocol() const override;
// quiche::WriteStreamVisitor implementation.
void OnCanWrite() override;
diff --git a/quiche/web_transport/test_tools/mock_web_transport.h b/quiche/web_transport/test_tools/mock_web_transport.h
index 4cb3aaf..656c25c 100644
--- a/quiche/web_transport/test_tools/mock_web_transport.h
+++ b/quiche/web_transport/test_tools/mock_web_transport.h
@@ -10,6 +10,7 @@
#include <cstddef>
#include <cstdint>
#include <memory>
+#include <optional>
#include <string>
#include "absl/status/status.h"
@@ -94,6 +95,8 @@
MOCK_METHOD(void, NotifySessionDraining, (), (override));
MOCK_METHOD(void, SetOnDraining, (quiche::SingleUseCallback<void()>),
(override));
+ MOCK_METHOD(std::optional<std::string>, GetNegotiatedSubprotocol, (),
+ (const, override));
};
} // namespace test
diff --git a/quiche/web_transport/web_transport.h b/quiche/web_transport/web_transport.h
index 2c558ad..5cd92a0 100644
--- a/quiche/web_transport/web_transport.h
+++ b/quiche/web_transport/web_transport.h
@@ -11,6 +11,7 @@
#include <cstddef>
#include <cstdint>
#include <memory>
+#include <optional>
#include <string>
// The dependencies of this API should be kept minimal and independent of
@@ -260,6 +261,10 @@
// capsule), or the underlying connection (HTTP GOAWAY) is being drained by
// the peer.
virtual void SetOnDraining(quiche::SingleUseCallback<void()> callback) = 0;
+
+ // Returns the negotiated subprotocol, or std::nullopt, if none was
+ // negotiated.
+ virtual std::optional<std::string> GetNegotiatedSubprotocol() const = 0;
};
} // namespace webtransport