Perform header-based draft version negotiation in WebTransport over HTTP/3.

The client-side validation can be disabled, which we will need to do in Chrome until the WPT server supports it.

PiperOrigin-RevId: 407210837
diff --git a/quic/core/http/quic_spdy_session.cc b/quic/core/http/quic_spdy_session.cc
index af54b06..54fda98 100644
--- a/quic/core/http/quic_spdy_session.cc
+++ b/quic/core/http/quic_spdy_session.cc
@@ -877,6 +877,8 @@
 
 bool QuicSpdySession::ShouldNegotiateDatagramContexts() { return false; }
 
+bool QuicSpdySession::ShouldValidateWebTransportVersion() const { return true; }
+
 bool QuicSpdySession::WillNegotiateWebTransport() {
   return LocalHttpDatagramSupport() != HttpDatagramSupport::kNone &&
          version().UsesHttp3() && ShouldNegotiateWebTransport();
diff --git a/quic/core/http/quic_spdy_session.h b/quic/core/http/quic_spdy_session.h
index 9f95075..50603de 100644
--- a/quic/core/http/quic_spdy_session.h
+++ b/quic/core/http/quic_spdy_session.h
@@ -471,6 +471,11 @@
   // created WebTransport sessions over HTTP/3.
   virtual bool ShouldNegotiateDatagramContexts();
 
+  // Indicates whether the client should check that the
+  // `Sec-Webtransport-Http3-Draft` header is valid.
+  // TODO(vasilvv): remove this once this is enabled in Chromium.
+  virtual bool ShouldValidateWebTransportVersion() const;
+
  protected:
   // Override CreateIncomingStream(), CreateOutgoingBidirectionalStream() and
   // CreateOutgoingUnidirectionalStream() with QuicSpdyStream return type to
diff --git a/quic/core/http/quic_spdy_session_test.cc b/quic/core/http/quic_spdy_session_test.cc
index 20cebee..bf86955 100644
--- a/quic/core/http/quic_spdy_session_test.cc
+++ b/quic/core/http/quic_spdy_session_test.cc
@@ -583,6 +583,8 @@
     headers.OnHeader(":protocol", "webtransport");
     if (session_.http_datagram_support() == HttpDatagramSupport::kDraft00) {
       headers.OnHeader("datagram-flow-id", absl::StrCat(session_id));
+    } else {
+      headers.OnHeader("sec-webtransport-http3-draft02", "1");
     }
     stream->OnStreamHeaderList(/*fin=*/true, 0, headers);
     if (session_.http_datagram_support() != HttpDatagramSupport::kDraft00) {
diff --git a/quic/core/http/quic_spdy_stream.cc b/quic/core/http/quic_spdy_stream.cc
index 55265de..7cb50c0 100644
--- a/quic/core/http/quic_spdy_stream.cc
+++ b/quic/core/http/quic_spdy_stream.cc
@@ -288,6 +288,12 @@
     header_block["sec-use-datagram-contexts"] = "?1";
   }
 
+  if (web_transport_ != nullptr &&
+      spdy_session_->http_datagram_support() != HttpDatagramSupport::kDraft00 &&
+      spdy_session_->perspective() == Perspective::IS_SERVER) {
+    header_block["sec-webtransport-http3-draft"] = "draft02";
+  }
+
   size_t bytes_written =
       WriteHeadersImpl(std::move(header_block), fin, std::move(ack_listener));
   if (!VersionUsesHttp3(transport_version()) && fin) {
@@ -1233,6 +1239,7 @@
   std::string method;
   std::string protocol;
   absl::optional<QuicDatagramStreamId> flow_id;
+  bool version_indicated = false;
   for (const auto& header : header_list_) {
     const std::string& header_name = header.first;
     const std::string& header_value = header.second;
@@ -1265,12 +1272,29 @@
       }
       flow_id = flow_id_out;
     }
+    if (header_name == "sec-webtransport-http3-draft02") {
+      if (header_value != "1") {
+        QUIC_DLOG(ERROR) << ENDPOINT
+                         << "Rejecting WebTransport due to invalid value of "
+                            "Sec-Webtransport-Http3-Draft02 header";
+        return;
+      }
+      version_indicated = true;
+    }
   }
 
   if (method != "CONNECT" || protocol != "webtransport") {
     return;
   }
 
+  if (!version_indicated &&
+      spdy_session_->http_datagram_support() != HttpDatagramSupport::kDraft00) {
+    QUIC_DLOG(ERROR)
+        << ENDPOINT
+        << "WebTransport request rejected due to missing version header.";
+    return;
+  }
+
   if (spdy_session_->http_datagram_support() == HttpDatagramSupport::kDraft00) {
     if (!flow_id.has_value()) {
       QUIC_DLOG(ERROR)
@@ -1318,6 +1342,8 @@
 
   if (spdy_session_->http_datagram_support() == HttpDatagramSupport::kDraft00) {
     headers["datagram-flow-id"] = absl::StrCat(id());
+  } else {
+    headers["sec-webtransport-http3-draft02"] = "1";
   }
 
   web_transport_ = std::make_unique<WebTransportHttp3>(
diff --git a/quic/core/http/quic_spdy_stream_test.cc b/quic/core/http/quic_spdy_stream_test.cc
index ce48043..d75ea97 100644
--- a/quic/core/http/quic_spdy_stream_test.cc
+++ b/quic/core/http/quic_spdy_stream_test.cc
@@ -3109,6 +3109,7 @@
 
   headers_[":method"] = "CONNECT";
   headers_[":protocol"] = "webtransport";
+  headers_["sec-webtransport-http3-draft02"] = "1";
 
   stream_->OnStreamHeadersPriority(
       spdy::SpdyStreamPrecedence(kV3HighestPriority));
diff --git a/quic/core/http/web_transport_http3.cc b/quic/core/http/web_transport_http3.cc
index 128c44e..c2d02f0 100644
--- a/quic/core/http/web_transport_http3.cc
+++ b/quic/core/http/web_transport_http3.cc
@@ -179,6 +179,7 @@
       QUIC_DVLOG(1) << ENDPOINT
                     << "Received WebTransport headers from server without "
                        "a valid status code, rejecting.";
+      rejection_reason_ = WebTransportHttp3RejectionReason::kNoStatusCode;
       return;
     }
     bool valid_status = status_code >= 200 && status_code <= 299;
@@ -187,8 +188,32 @@
                     << "Received WebTransport headers from server with "
                        "status code "
                     << status_code << ", rejecting.";
+      rejection_reason_ = WebTransportHttp3RejectionReason::kWrongStatusCode;
       return;
     }
+    bool should_validate_version =
+        session_->http_datagram_support() != HttpDatagramSupport::kDraft00 &&
+        session_->ShouldValidateWebTransportVersion();
+    if (should_validate_version) {
+      auto draft_version_it = headers.find("sec-webtransport-http3-draft");
+      if (draft_version_it == headers.end()) {
+        QUIC_DVLOG(1) << ENDPOINT
+                      << "Received WebTransport headers from server without "
+                         "a draft version, rejecting.";
+        rejection_reason_ =
+            WebTransportHttp3RejectionReason::kMissingDraftVersion;
+        return;
+      }
+      if (draft_version_it->second != "draft02") {
+        QUIC_DVLOG(1) << ENDPOINT
+                      << "Received WebTransport headers from server with "
+                         "an unknown draft version ("
+                      << draft_version_it->second << "), rejecting.";
+        rejection_reason_ =
+            WebTransportHttp3RejectionReason::kUnsupportedDraftVersion;
+        return;
+      }
+    }
   }
 
   QUIC_DVLOG(1) << ENDPOINT << "WebTransport session " << id_ << " ready.";
diff --git a/quic/core/http/web_transport_http3.h b/quic/core/http/web_transport_http3.h
index cd446c1..542b941 100644
--- a/quic/core/http/web_transport_http3.h
+++ b/quic/core/http/web_transport_http3.h
@@ -23,6 +23,14 @@
 class QuicSpdySession;
 class QuicSpdyStream;
 
+enum class WebTransportHttp3RejectionReason {
+  kNone,
+  kNoStatusCode,
+  kWrongStatusCode,
+  kMissingDraftVersion,
+  kUnsupportedDraftVersion,
+};
+
 // A session of WebTransport over HTTP/3.  The session is owned by
 // QuicSpdyStream object for the CONNECT stream that established it.
 //
@@ -96,6 +104,9 @@
                        absl::string_view close_details) override;
 
   bool close_received() const { return close_received_; }
+  WebTransportHttp3RejectionReason rejection_reason() const {
+    return rejection_reason_;
+  }
 
  private:
   // Notifies the visitor that the connection has been closed.  Ensures that the
@@ -123,6 +134,8 @@
   bool close_received_ = false;
   bool close_notified_ = false;
 
+  WebTransportHttp3RejectionReason rejection_reason_ =
+      WebTransportHttp3RejectionReason::kNone;
   // Those are set to default values, which are used if the session is not
   // closed cleanly using an appropriate capsule.
   WebTransportSessionError error_code_ = 0;