Create a dedicated WebTransport-only client class, and switch MoqtClient to using it. PiperOrigin-RevId: 937254495
diff --git a/build/source_list.bzl b/build/source_list.bzl index 9d12049..4077291 100644 --- a/build/source_list.bzl +++ b/build/source_list.bzl
@@ -761,6 +761,7 @@ "quic/tools/quic_url.h", "quic/tools/simple_ticket_crypter.h", "quic/tools/web_transport_only_backend.h", + "quic/tools/web_transport_only_client.h", "quic/tools/web_transport_test_visitors.h", ] quiche_tool_support_srcs = [ @@ -785,6 +786,7 @@ "quic/tools/quic_url.cc", "quic/tools/simple_ticket_crypter.cc", "quic/tools/web_transport_only_backend.cc", + "quic/tools/web_transport_only_client.cc", ] quiche_test_support_hdrs = [ "common/platform/api/quiche_expect_bug.h", @@ -1400,6 +1402,7 @@ "quic/tools/quic_simple_client_session_test.cc", "quic/tools/quic_tcp_like_trace_converter_test.cc", "quic/tools/simple_ticket_crypter_test.cc", + "quic/tools/web_transport_only_client_test.cc", "web_transport/encapsulated/encapsulated_web_transport_test.cc", "web_transport/test_tools/in_memory_stream_test.cc", "web_transport/web_transport_headers_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni index fdccb89..e2f084a 100644 --- a/build/source_list.gni +++ b/build/source_list.gni
@@ -761,6 +761,7 @@ "src/quiche/quic/tools/quic_url.h", "src/quiche/quic/tools/simple_ticket_crypter.h", "src/quiche/quic/tools/web_transport_only_backend.h", + "src/quiche/quic/tools/web_transport_only_client.h", "src/quiche/quic/tools/web_transport_test_visitors.h", ] quiche_tool_support_srcs = [ @@ -785,6 +786,7 @@ "src/quiche/quic/tools/quic_url.cc", "src/quiche/quic/tools/simple_ticket_crypter.cc", "src/quiche/quic/tools/web_transport_only_backend.cc", + "src/quiche/quic/tools/web_transport_only_client.cc", ] quiche_test_support_hdrs = [ "src/quiche/common/platform/api/quiche_expect_bug.h", @@ -1401,6 +1403,7 @@ "src/quiche/quic/tools/quic_simple_client_session_test.cc", "src/quiche/quic/tools/quic_tcp_like_trace_converter_test.cc", "src/quiche/quic/tools/simple_ticket_crypter_test.cc", + "src/quiche/quic/tools/web_transport_only_client_test.cc", "src/quiche/web_transport/encapsulated/encapsulated_web_transport_test.cc", "src/quiche/web_transport/test_tools/in_memory_stream_test.cc", "src/quiche/web_transport/web_transport_headers_test.cc",
diff --git a/build/source_list.json b/build/source_list.json index ba05903..2444384 100644 --- a/build/source_list.json +++ b/build/source_list.json
@@ -760,6 +760,7 @@ "quiche/quic/tools/quic_url.h", "quiche/quic/tools/simple_ticket_crypter.h", "quiche/quic/tools/web_transport_only_backend.h", + "quiche/quic/tools/web_transport_only_client.h", "quiche/quic/tools/web_transport_test_visitors.h" ], "quiche_tool_support_srcs": [ @@ -783,7 +784,8 @@ "quiche/quic/tools/quic_tcp_like_trace_converter.cc", "quiche/quic/tools/quic_url.cc", "quiche/quic/tools/simple_ticket_crypter.cc", - "quiche/quic/tools/web_transport_only_backend.cc" + "quiche/quic/tools/web_transport_only_backend.cc", + "quiche/quic/tools/web_transport_only_client.cc" ], "quiche_test_support_hdrs": [ "quiche/common/platform/api/quiche_expect_bug.h", @@ -1400,6 +1402,7 @@ "quiche/quic/tools/quic_simple_client_session_test.cc", "quiche/quic/tools/quic_tcp_like_trace_converter_test.cc", "quiche/quic/tools/simple_ticket_crypter_test.cc", + "quiche/quic/tools/web_transport_only_client_test.cc", "quiche/web_transport/encapsulated/encapsulated_web_transport_test.cc", "quiche/web_transport/test_tools/in_memory_stream_test.cc", "quiche/web_transport/web_transport_headers_test.cc",
diff --git a/quiche/quic/moqt/tools/moqt_client.cc b/quiche/quic/moqt/tools/moqt_client.cc index f2d185b..35dcce5 100644 --- a/quiche/quic/moqt/tools/moqt_client.cc +++ b/quiche/quic/moqt/tools/moqt_client.cc
@@ -9,12 +9,8 @@ #include <utility> #include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/string_view.h" #include "absl/types/span.h" #include "quiche/quic/core/crypto/proof_verifier.h" -#include "quiche/quic/core/http/quic_spdy_client_stream.h" -#include "quiche/quic/core/http/web_transport_http3.h" #include "quiche/quic/core/io/quic_event_loop.h" #include "quiche/quic/core/quic_server_id.h" #include "quiche/quic/core/quic_types.h" @@ -24,12 +20,9 @@ #include "quiche/quic/moqt/moqt_session_callbacks.h" #include "quiche/quic/moqt/moqt_session_interface.h" #include "quiche/quic/platform/api/quic_socket_address.h" -#include "quiche/quic/tools/quic_default_client.h" -#include "quiche/quic/tools/quic_event_loop_tools.h" #include "quiche/quic/tools/quic_name_lookup.h" -#include "quiche/common/http/http_header_block.h" -#include "quiche/common/platform/api/quiche_logging.h" -#include "quiche/web_transport/web_transport_headers.h" +#include "quiche/quic/tools/web_transport_only_client.h" +#include "quiche/web_transport/web_transport.h" namespace moqt { @@ -38,11 +31,11 @@ std::unique_ptr<quic::ProofVerifier> proof_verifier, quic::QuicEventLoop* event_loop, MoqtSessionParameters parameters) - : spdy_client_(peer_address, server_id, GetMoqtSupportedQuicVersions(), - event_loop, std::move(proof_verifier)), + : client_(peer_address, server_id, GetMoqtSupportedQuicVersions(), + GenerateQuicConfig(), event_loop, nullptr, + std::move(proof_verifier), nullptr), parameters_(parameters) { - TuneQuicConfig(*spdy_client_.config()); - spdy_client_.set_enable_web_transport(true); + TuneQuicConfig(*client_.config()); parameters_.perspective = quic::Perspective::IS_CLIENT; } @@ -55,58 +48,8 @@ absl::Status MoqtClient::ConnectInner(std::string path, MoqtSessionCallbacks& callbacks) { - if (!spdy_client_.Initialize()) { - return absl::InternalError("Initialization failed"); - } - if (!spdy_client_.Connect()) { - return absl::UnavailableError("Failed to establish a QUIC connection"); - } - bool settings_received = quic::ProcessEventsUntil( - spdy_client_.default_network_helper()->event_loop(), - [&] { return spdy_client_.client_session()->settings_received(); }); - if (!settings_received) { - return absl::UnavailableError( - "Timed out while waiting for server SETTINGS"); - } - if (!spdy_client_.client_session()->SupportsWebTransport()) { - QUICHE_DLOG(INFO) << "session: SupportsWebTransport = " - << spdy_client_.client_session()->SupportsWebTransport() - << ", SupportsH3Datagram = " - << spdy_client_.client_session()->SupportsH3Datagram() - << ", OneRttKeysAvailable = " - << spdy_client_.client_session()->OneRttKeysAvailable(); - return absl::FailedPreconditionError( - "Server does not support WebTransport"); - } - auto* stream = static_cast<quic::QuicSpdyClientStream*>( - spdy_client_.client_session()->CreateOutgoingBidirectionalStream()); - if (!stream) { - return absl::InternalError("Could not open a CONNECT stream"); - } - spdy_client_.set_store_response(true); + const std::string version = std::string(kDefaultMoqtVersion); - quiche::HttpHeaderBlock headers; - headers[":scheme"] = "https"; - headers[":authority"] = spdy_client_.server_id().host(); - headers[":path"] = path; - headers[":method"] = "CONNECT"; - headers[":protocol"] = "webtransport"; - std::string version = std::string(kDefaultMoqtVersion); - absl::StatusOr<std::string> serialized_version = - webtransport::SerializeSubprotocolRequestHeader( - absl::MakeSpan(&version, 1)); - if (!serialized_version.ok()) { - return serialized_version.status(); - } - headers["wt-available-protocols"] = *serialized_version; - stream->SendRequest(std::move(headers), "", false); - - quic::WebTransportHttp3* web_transport = stream->web_transport(); - if (web_transport == nullptr) { - return absl::InternalError("Failed to initialize WebTransport session"); - } - - // Ensure that we never have a dangling pointer to the session. MoqtSessionDeletedCallback deleted_callback = std::move(callbacks.session_deleted_callback); callbacks.session_deleted_callback = @@ -115,13 +58,19 @@ std::move(old)(); }; - auto session = std::make_unique<MoqtSession>( - web_transport, parameters_, - spdy_client_.default_network_helper()->event_loop()->CreateAlarmFactory(), - std::move(callbacks)); - session_ = session.get(); - web_transport->SetVisitor(std::move(session)); - return absl::OkStatus(); + return client_.ConnectSync( + path, + [&](webtransport::Session* session) { + auto moqt_session = + std::make_unique<MoqtSession>(session, parameters_, + client_.default_network_helper() + ->event_loop() + ->CreateAlarmFactory(), + std::move(callbacks)); + session_ = moqt_session.get(); + return moqt_session; + }, + absl::MakeSpan(&version, 1)); } } // namespace moqt
diff --git a/quiche/quic/moqt/tools/moqt_client.h b/quiche/quic/moqt/tools/moqt_client.h index 011ceb1..d78fd54 100644 --- a/quiche/quic/moqt/tools/moqt_client.h +++ b/quiche/quic/moqt/tools/moqt_client.h
@@ -17,12 +17,12 @@ #include "quiche/quic/moqt/moqt_session_callbacks.h" #include "quiche/quic/moqt/moqt_session_interface.h" #include "quiche/quic/platform/api/quic_socket_address.h" -#include "quiche/quic/tools/quic_default_client.h" +#include "quiche/quic/tools/web_transport_only_client.h" #include "quiche/common/platform/api/quiche_export.h" namespace moqt { -// A synchronous MoQT client based on QuicDefaultClient. +// A synchronous MoQT client based on WebTransportOnlyClient. class QUICHE_EXPORT MoqtClient { public: MoqtClient(quic::QuicSocketAddress peer_address, @@ -36,12 +36,12 @@ void Connect(std::string path, MoqtSessionCallbacks callbacks); MoqtSession* session() { return session_; } - quic::QuicSession* quic_session() { return spdy_client_.session(); } + quic::QuicSession* quic_session() { return client_.session(); } private: absl::Status ConnectInner(std::string path, MoqtSessionCallbacks& callbacks); - quic::QuicDefaultClient spdy_client_; + quic::WebTransportOnlyClient client_; MoqtSessionParameters parameters_; MoqtSession* session_ = nullptr; };
diff --git a/quiche/quic/tools/web_transport_only_client.cc b/quiche/quic/tools/web_transport_only_client.cc new file mode 100644 index 0000000..8cdb06f --- /dev/null +++ b/quiche/quic/tools/web_transport_only_client.cc
@@ -0,0 +1,213 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "quiche/quic/tools/web_transport_only_client.h" + +#include <memory> +#include <string> +#include <utility> + +#include "absl/base/casts.h" +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/quic/core/crypto/proof_verifier.h" +#include "quiche/quic/core/crypto/quic_crypto_client_config.h" +#include "quiche/quic/core/http/quic_connection_migration_manager.h" +#include "quiche/quic/core/http/quic_spdy_client_session.h" +#include "quiche/quic/core/http/quic_spdy_client_stream.h" +#include "quiche/quic/core/http/quic_spdy_session.h" +#include "quiche/quic/core/http/web_transport_http3.h" +#include "quiche/quic/core/io/quic_event_loop.h" +#include "quiche/quic/core/quic_config.h" +#include "quiche/quic/core/quic_connection.h" +#include "quiche/quic/core/quic_default_connection_helper.h" +#include "quiche/quic/core/quic_packet_writer.h" +#include "quiche/quic/core/quic_path_validator.h" +#include "quiche/quic/core/quic_server_id.h" +#include "quiche/quic/core/quic_session.h" +#include "quiche/quic/core/quic_types.h" +#include "quiche/quic/core/quic_versions.h" +#include "quiche/quic/platform/api/quic_socket_address.h" +#include "quiche/quic/tools/quic_client_base.h" +#include "quiche/quic/tools/quic_client_default_network_helper.h" +#include "quiche/quic/tools/quic_event_loop_tools.h" +#include "quiche/common/http/http_header_block.h" +#include "quiche/web_transport/web_transport_headers.h" + +namespace quic { + +WebTransportOnlyClient::Session::Session( + const QuicConfig& config, const ParsedQuicVersionVector& supported_versions, + QuicConnection* connection, QuicClientBase::NetworkHelper* network_helper, + const QuicServerId& server_id, QuicCryptoClientConfig* crypto_config) + : QuicSpdyClientSession( + config, supported_versions, connection, + /*visitor=*/nullptr, /*writer=*/nullptr, + /*migration_helper=*/nullptr, + QuicConnectionMigrationConfig{.allow_server_preferred_address = + false}, + server_id, crypto_config, QuicPriorityType::kWebTransport), + network_helper_(network_helper) {} + +WebTransportHttp3VersionSet +WebTransportOnlyClient::Session::LocallySupportedWebTransportVersions() const { + return kDefaultSupportedWebTransportVersions; +} + +HttpDatagramSupport +WebTransportOnlyClient::Session::LocalHttpDatagramSupport() { + return HttpDatagramSupport::kRfcAndDraft04; +} + +// TODO(vasilvv): the connection migration code below comes from +// QuicSimpleClientSession; should it live in QuicClientBase? +void WebTransportOnlyClient::Session::CreateContextForMultiPortPath( + std::unique_ptr<MultiPortPathContextObserver> context_observer) { + if (!network_helper_ || connection()->multi_port_stats() == nullptr) { + return; + } + QuicSocketAddress self_address = connection()->self_address(); + QuicSocketAddress server_address = connection()->peer_address(); + if (!network_helper_->CreateUDPSocketAndBind( + server_address, self_address.host(), self_address.port() + 1)) { + return; + } + QuicPacketWriter* writer = network_helper_->CreateQuicPacketWriter(); + if (writer == nullptr) { + return; + } + context_observer->OnMultiPortPathContextAvailable( + std::make_unique<PathMigrationContext>( + std::unique_ptr<QuicPacketWriter>(writer), + network_helper_->GetLatestClientAddress(), peer_address())); +} + +void WebTransportOnlyClient::Session::MigrateToMultiPortPath( + std::unique_ptr<QuicPathValidationContext> context) { + auto* path_migration_context = + static_cast<PathMigrationContext*>(context.get()); + MigratePath(path_migration_context->self_address(), + path_migration_context->peer_address(), + path_migration_context->ReleaseWriter(), /*owns_writer=*/true); +} + +WebTransportOnlyClient::WebTransportOnlyClient( + QuicSocketAddress server_address, const QuicServerId& server_id, + const ParsedQuicVersionVector& supported_versions, const QuicConfig& config, + QuicEventLoop* event_loop, + std::unique_ptr<QuicClientDefaultNetworkHelper> absl_nullable + network_helper, + std::unique_ptr<ProofVerifier> absl_nonnull proof_verifier, + std::unique_ptr<SessionCache> absl_nullable session_cache) + : QuicClientBase(server_id, supported_versions, config, + new QuicDefaultConnectionHelper(), + event_loop->CreateAlarmFactory().release(), + network_helper != nullptr + ? std::move(network_helper) + : std::make_unique<QuicClientDefaultNetworkHelper>( + event_loop, this), + std::move(proof_verifier), std::move(session_cache)) { + set_server_address(server_address); +} + +WebTransportOnlyClient::~WebTransportOnlyClient() { ResetSession(); } + +absl::Status WebTransportOnlyClient::ConnectSync( + std::string path, VisitorFactory visitor_factory, + absl::Span<const std::string> subprotocols, + const quiche::HttpHeaderBlock& extra_headers) { + if (!Initialize()) { + return absl::InternalError("Failed to initialize the client"); + } + if (!QuicClientBase::Connect()) { + return absl::InternalError("Failed to establish a QUIC connection"); + } + bool settings_received = + ProcessEventsUntil(default_network_helper()->event_loop(), + [&] { return client_session()->settings_received(); }); + if (!settings_received) { + return absl::DeadlineExceededError( + "Timed out while waiting for HTTP/3 SETTINGS"); + } + if (!client_session()->SupportsWebTransport()) { + return absl::FailedPreconditionError( + "QUIC server does not support WebTransport"); + } + auto* stream = absl::down_cast<QuicSpdyClientStream*>( + client_session()->CreateOutgoingBidirectionalStream()); + if (!stream) { + return absl::InternalError("Failed to create a CONNECT request stream"); + } + + quiche::HttpHeaderBlock headers = extra_headers.Clone(); + headers[":scheme"] = "https"; + headers[":authority"] = server_id().host(); + headers[":path"] = std::move(path); + headers[":method"] = "CONNECT"; + headers[":protocol"] = "webtransport"; + if (!subprotocols.empty()) { + absl::StatusOr<std::string> serialized = + webtransport::SerializeSubprotocolRequestHeader(subprotocols); + if (!serialized.ok()) { + return absl::InvalidArgumentError("Invalid subprotocol name supplied"); + } + headers["wt-available-protocols"] = *serialized; + } + stream->SendRequest(std::move(headers), "", false); + + WebTransportHttp3* web_transport = stream->web_transport(); + if (!web_transport) { + return absl::InternalError( + "Failed to associate a WebTransport session with an HTTP request"); + } + web_transport->SetVisitor(std::move(visitor_factory)(web_transport)); + return absl::OkStatus(); +} + +QuicClientDefaultNetworkHelper* +WebTransportOnlyClient::default_network_helper() { + return absl::down_cast<QuicClientDefaultNetworkHelper*>(network_helper()); +} + +WebTransportOnlyClient::Session* WebTransportOnlyClient::client_session() { + return absl::down_cast<Session*>(QuicClientBase::session()); +} + +void WebTransportOnlyClient::InitializeSession() { + client_session()->Initialize(); + client_session()->CryptoConnect(); +} + +bool WebTransportOnlyClient::EarlyDataAccepted() { + return client_session()->EarlyDataAccepted(); +} + +bool WebTransportOnlyClient::ReceivedInchoateReject() { + return client_session()->ReceivedInchoateReject(); +} + +int WebTransportOnlyClient::GetNumSentClientHellosFromSession() { + return client_session()->GetNumSentClientHellos(); +} + +int WebTransportOnlyClient::GetNumReceivedServerConfigUpdatesFromSession() { + return client_session()->GetNumReceivedServerConfigUpdates(); +} + +std::unique_ptr<QuicSession> WebTransportOnlyClient::CreateQuicClientSession( + const ParsedQuicVersionVector& supported_versions, + QuicConnection* connection) { + return std::make_unique<Session>(*config(), supported_versions, connection, + network_helper(), server_id(), + crypto_config()); +} + +bool WebTransportOnlyClient::HasActiveRequests() { + return client_session()->HasActiveRequestStreams(); +} + +} // namespace quic
diff --git a/quiche/quic/tools/web_transport_only_client.h b/quiche/quic/tools/web_transport_only_client.h new file mode 100644 index 0000000..278b87f --- /dev/null +++ b/quiche/quic/tools/web_transport_only_client.h
@@ -0,0 +1,116 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef QUICHE_QUIC_TOOLS_WEB_TRANSPORT_ONLY_CLIENT_H_ +#define QUICHE_QUIC_TOOLS_WEB_TRANSPORT_ONLY_CLIENT_H_ + +#include <memory> +#include <string> + +#include "absl/base/nullability.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/quic/core/crypto/proof_verifier.h" +#include "quiche/quic/core/crypto/quic_crypto_client_config.h" +#include "quiche/quic/core/http/quic_spdy_client_session.h" +#include "quiche/quic/core/http/quic_spdy_client_stream.h" +#include "quiche/quic/core/http/quic_spdy_session.h" +#include "quiche/quic/core/io/quic_event_loop.h" +#include "quiche/quic/core/quic_config.h" +#include "quiche/quic/core/quic_connection.h" +#include "quiche/quic/core/quic_path_validator.h" +#include "quiche/quic/core/quic_server_id.h" +#include "quiche/quic/core/quic_session.h" +#include "quiche/quic/core/quic_versions.h" +#include "quiche/quic/platform/api/quic_socket_address.h" +#include "quiche/quic/tools/quic_client_base.h" +#include "quiche/quic/tools/quic_client_default_network_helper.h" +#include "quiche/common/http/http_header_block.h" +#include "quiche/common/quiche_callbacks.h" +#include "quiche/web_transport/web_transport.h" + +namespace quic { + +// WebTransportOnlyClient is a dedicated client for applications that +// are written against the webtransport::Session API. It serves as the client +// counterpart to WebTransportOnlyDispatcher. +class WebTransportOnlyClient : public QuicClientBase { + public: + using VisitorFactory = + quiche::UnretainedCallback<std::unique_ptr<webtransport::SessionVisitor>( + webtransport::Session*)>; + + WebTransportOnlyClient( + QuicSocketAddress server_address, const QuicServerId& server_id, + const ParsedQuicVersionVector& supported_versions, + const QuicConfig& config, QuicEventLoop* event_loop, + std::unique_ptr<QuicClientDefaultNetworkHelper> absl_nullable + network_helper, + std::unique_ptr<ProofVerifier> absl_nonnull proof_verifier, + std::unique_ptr<SessionCache> absl_nullable session_cache); + + WebTransportOnlyClient(const WebTransportOnlyClient&) = delete; + WebTransportOnlyClient& operator=(const WebTransportOnlyClient&) = delete; + + ~WebTransportOnlyClient() override; + + // Synchronously establishes a WebTransport session to the server at `path`, + // configuring the session visitor by calling `visitor_factory` if the + // connection succeeds. + // + // Note that the function returns when the client creates the CONNECT stream, + // and not when the HTTP response for it is received. Doing so allows the + // client to open streams before receiving the HTTP response. To wait until + // the actual response is received, use OnSessionReady in the session visitor. + absl::Status ConnectSync(std::string path, VisitorFactory visitor_factory, + absl::Span<const std::string> subprotocols = {}, + const quiche::HttpHeaderBlock& extra_headers = {}); + + QuicClientDefaultNetworkHelper* default_network_helper(); + + protected: + // QuicClientBase implementation. + void InitializeSession() override; + bool EarlyDataAccepted() override; + bool ReceivedInchoateReject() override; + int GetNumSentClientHellosFromSession() override; + int GetNumReceivedServerConfigUpdatesFromSession() override; + bool HasActiveRequests() override; + + std::unique_ptr<QuicSession> CreateQuicClientSession( + const ParsedQuicVersionVector& supported_versions, + QuicConnection* connection) override; + + private: + class Session : public QuicSpdyClientSession { + public: + Session(const QuicConfig& config, + const ParsedQuicVersionVector& supported_versions, + QuicConnection* connection, + QuicClientBase::NetworkHelper* network_helper, + const QuicServerId& server_id, + QuicCryptoClientConfig* crypto_config); + ~Session() override = default; + + WebTransportHttp3VersionSet LocallySupportedWebTransportVersions() + const override; + HttpDatagramSupport LocalHttpDatagramSupport() override; + void CreateContextForMultiPortPath( + std::unique_ptr<MultiPortPathContextObserver> context_observer) + override; + void MigrateToMultiPortPath( + std::unique_ptr<QuicPathValidationContext> context) override; + + private: + QuicClientBase::NetworkHelper* network_helper_; + }; + + Session* client_session(); +}; + +} // namespace quic + +#endif // QUICHE_QUIC_TOOLS_WEB_TRANSPORT_ONLY_CLIENT_H_
diff --git a/quiche/quic/tools/web_transport_only_client_test.cc b/quiche/quic/tools/web_transport_only_client_test.cc new file mode 100644 index 0000000..33ebb5c --- /dev/null +++ b/quiche/quic/tools/web_transport_only_client_test.cc
@@ -0,0 +1,162 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "quiche/quic/tools/web_transport_only_client.h" + +#include <memory> +#include <string> +#include <utility> + +#include "absl/status/status.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/quic/core/crypto/quic_crypto_server_config.h" +#include "quiche/quic/core/crypto/quic_random.h" +#include "quiche/quic/core/deterministic_connection_id_generator.h" +#include "quiche/quic/core/http/web_transport_only_dispatcher.h" +#include "quiche/quic/core/http/web_transport_only_server_session.h" +#include "quiche/quic/core/io/quic_default_event_loop.h" +#include "quiche/quic/core/io/quic_event_loop.h" +#include "quiche/quic/core/io/quic_server_io_harness.h" +#include "quiche/quic/core/io/socket.h" +#include "quiche/quic/core/quic_config.h" +#include "quiche/quic/core/quic_connection_id.h" +#include "quiche/quic/core/quic_default_clock.h" +#include "quiche/quic/core/quic_default_connection_helper.h" +#include "quiche/quic/core/quic_version_manager.h" +#include "quiche/quic/core/quic_versions.h" +#include "quiche/quic/platform/api/quic_socket_address.h" +#include "quiche/quic/platform/api/quic_test.h" +#include "quiche/quic/test_tools/crypto_test_utils.h" +#include "quiche/quic/tools/quic_event_loop_tools.h" +#include "quiche/quic/tools/quic_simple_crypto_server_stream_helper.h" +#include "quiche/common/platform/api/quiche_logging.h" +#include "quiche/common/quiche_ip_address.h" +#include "quiche/common/test_tools/quiche_test_utils.h" +#include "quiche/web_transport/web_transport.h" + +namespace quic::test { +namespace { + +class TestSessionVisitor : public webtransport::SessionVisitor { + public: + explicit TestSessionVisitor(bool* established, bool* closed) + : established_(*established), closed_(*closed) {} + + void OnSessionReady() override { established_ = true; } + void OnSessionClosed(webtransport::SessionErrorCode /*error_code*/, + const std::string& /*error_message*/) override { + closed_ = true; + } + + void OnIncomingBidirectionalStreamAvailable() override {} + void OnIncomingUnidirectionalStreamAvailable() override {} + void OnDatagramReceived(absl::string_view /*datagram*/) override {} + void OnCanCreateNewOutgoingBidirectionalStream() override {} + void OnCanCreateNewOutgoingUnidirectionalStream() override {} + + private: + bool& established_; + bool& closed_; +}; + +class WebTransportOnlyTestServer { + public: + explicit WebTransportOnlyTestServer(bool* server_established, + bool* server_closed) + : crypto_config_("secret", QuicRandom::GetInstance(), + crypto_test_utils::ProofSourceForTesting(), + KeyExchangeSource::Default()), + version_manager_(CurrentSupportedVersionsWithTls()), + connection_id_generator_(kQuicDefaultConnectionIdLength), + event_loop_(GetDefaultEventLoop()->Create(QuicDefaultClock::Get())), + dispatcher_(&config_, &crypto_config_, &version_manager_, + std::make_unique<QuicDefaultConnectionHelper>(), + std::make_unique<QuicSimpleCryptoServerStreamHelper>(), + event_loop_->CreateAlarmFactory(), + kQuicDefaultConnectionIdLength, connection_id_generator_) { + dispatcher_.parameters().handler_factory = + [server_established, server_closed]( + webtransport::Session*, const WebTransportIncomingRequestDetails&) + -> absl::StatusOr<WebTransportConnectResponse> { + WebTransportConnectResponse response; + response.visitor = std::make_unique<TestSessionVisitor>( + server_established, server_closed); + return response; + }; + dispatcher_.parameters().subprotocol_callback = + [](absl::Span<const absl::string_view> subprotocols) { + return subprotocols.empty() ? -1 : 0; + }; + + QuicSocketAddress address(quiche::QuicheIpAddress::Loopback6(), /*port=*/0); + absl::StatusOr<OwnedSocketFd> fd = CreateAndBindServerSocket(address); + QUICHE_CHECK(fd.ok()); + fd_ = std::move(*fd); + + absl::StatusOr<std::unique_ptr<QuicServerIoHarness>> io = + QuicServerIoHarness::Create(event_loop_.get(), &dispatcher_, *fd_); + QUICHE_CHECK(io.ok()); + io_ = std::move(*io); + io_->InitializeWriter(); + } + + QuicSocketAddress server_address() const { return io_->local_address(); } + QuicEventLoop* event_loop() { return event_loop_.get(); } + + private: + QuicConfig config_; + QuicCryptoServerConfig crypto_config_; + QuicVersionManager version_manager_; + DeterministicConnectionIdGenerator connection_id_generator_; + std::unique_ptr<QuicEventLoop> event_loop_; + WebTransportOnlyDispatcher dispatcher_; + OwnedSocketFd fd_; + std::unique_ptr<QuicServerIoHarness> io_; +}; + +class WebTransportOnlyClientTest : public QuicTest {}; + +TEST_F(WebTransportOnlyClientTest, SuccessfulConnection) { + bool server_established = false; + bool server_closed = false; + WebTransportOnlyTestServer server(&server_established, &server_closed); + + bool client_established = false; + bool client_closed = false; + + auto client = std::make_unique<WebTransportOnlyClient>( + server.server_address(), QuicServerId("test.example.com", 443), + CurrentSupportedVersionsWithTls(), QuicConfig(), server.event_loop(), + /*network_helper=*/nullptr, crypto_test_utils::ProofVerifierForTesting(), + /*session_cache=*/nullptr); + + // Note: the `ConnectSync` call below implicitly drives both the client and + // the server, since both use the same event loop. + absl::Status status = + client->ConnectSync("/test", [&](webtransport::Session*) { + return std::make_unique<TestSessionVisitor>(&client_established, + &client_closed); + }); + QUICHE_ASSERT_OK(status); + + bool handshake_success = ProcessEventsUntil(server.event_loop(), [&] { + return client_established && server_established; + }); + EXPECT_TRUE(handshake_success); + EXPECT_FALSE(client_closed); + EXPECT_FALSE(server_closed); + + client->Disconnect(); + EXPECT_TRUE(client_closed); + + bool server_close_success = + ProcessEventsUntil(server.event_loop(), [&] { return server_closed; }); + EXPECT_TRUE(server_close_success); + EXPECT_TRUE(server_closed); +} + +} // namespace +} // namespace quic::test