Implement the Devious Baton protocol in QUICHE

Also add a web_transport_test_server binary that exposes WebTransport test visitors in addition to the devious-baton endpoint.

PiperOrigin-RevId: 550376350
diff --git a/build/source_list.bzl b/build/source_list.bzl
index f4149a8..c9a704d 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -404,6 +404,7 @@
     "spdy/core/spdy_protocol.h",
     "spdy/core/spdy_simple_arena.h",
     "spdy/core/zero_copy_output_buffer.h",
+    "web_transport/complete_buffer_visitor.h",
     "web_transport/web_transport.h",
 ]
 quiche_core_srcs = [
@@ -701,6 +702,7 @@
     "spdy/core/spdy_prefixed_buffer_reader.cc",
     "spdy/core/spdy_protocol.cc",
     "spdy/core/spdy_simple_arena.cc",
+    "web_transport/complete_buffer_visitor.cc",
 ]
 quiche_tool_support_hdrs = [
     "common/platform/api/quiche_command_line_flags.h",
@@ -711,6 +713,7 @@
     "quic/tools/connect_server_backend.h",
     "quic/tools/connect_tunnel.h",
     "quic/tools/connect_udp_tunnel.h",
+    "quic/tools/devious_baton.h",
     "quic/tools/fake_proof_verifier.h",
     "quic/tools/quic_backend_response.h",
     "quic/tools/quic_client_base.h",
@@ -735,6 +738,7 @@
     "quic/tools/connect_server_backend.cc",
     "quic/tools/connect_tunnel.cc",
     "quic/tools/connect_udp_tunnel.cc",
+    "quic/tools/devious_baton.cc",
     "quic/tools/quic_backend_response.cc",
     "quic/tools/quic_client_base.cc",
     "quic/tools/quic_memory_cache_backend.cc",
@@ -1351,6 +1355,7 @@
     "quic/tools/quic_server_factory.cc",
     "quic/tools/quic_toy_client.cc",
     "quic/tools/quic_toy_server.cc",
+    "quic/tools/web_transport_test_server.cc",
 ]
 nghttp2_hdrs = [
     "http2/adapter/callback_visitor.h",
diff --git a/build/source_list.gni b/build/source_list.gni
index d5ee1d3..d701cd5 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -404,6 +404,7 @@
     "src/quiche/spdy/core/spdy_protocol.h",
     "src/quiche/spdy/core/spdy_simple_arena.h",
     "src/quiche/spdy/core/zero_copy_output_buffer.h",
+    "src/quiche/web_transport/complete_buffer_visitor.h",
     "src/quiche/web_transport/web_transport.h",
 ]
 quiche_core_srcs = [
@@ -701,6 +702,7 @@
     "src/quiche/spdy/core/spdy_prefixed_buffer_reader.cc",
     "src/quiche/spdy/core/spdy_protocol.cc",
     "src/quiche/spdy/core/spdy_simple_arena.cc",
+    "src/quiche/web_transport/complete_buffer_visitor.cc",
 ]
 quiche_tool_support_hdrs = [
     "src/quiche/common/platform/api/quiche_command_line_flags.h",
@@ -711,6 +713,7 @@
     "src/quiche/quic/tools/connect_server_backend.h",
     "src/quiche/quic/tools/connect_tunnel.h",
     "src/quiche/quic/tools/connect_udp_tunnel.h",
+    "src/quiche/quic/tools/devious_baton.h",
     "src/quiche/quic/tools/fake_proof_verifier.h",
     "src/quiche/quic/tools/quic_backend_response.h",
     "src/quiche/quic/tools/quic_client_base.h",
@@ -735,6 +738,7 @@
     "src/quiche/quic/tools/connect_server_backend.cc",
     "src/quiche/quic/tools/connect_tunnel.cc",
     "src/quiche/quic/tools/connect_udp_tunnel.cc",
+    "src/quiche/quic/tools/devious_baton.cc",
     "src/quiche/quic/tools/quic_backend_response.cc",
     "src/quiche/quic/tools/quic_client_base.cc",
     "src/quiche/quic/tools/quic_memory_cache_backend.cc",
@@ -1354,6 +1358,7 @@
     "src/quiche/quic/tools/quic_server_factory.cc",
     "src/quiche/quic/tools/quic_toy_client.cc",
     "src/quiche/quic/tools/quic_toy_server.cc",
+    "src/quiche/quic/tools/web_transport_test_server.cc",
 ]
 nghttp2_hdrs = [
     "src/quiche/http2/adapter/callback_visitor.h",
diff --git a/build/source_list.json b/build/source_list.json
index 2a33437..118a360 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -403,6 +403,7 @@
     "quiche/spdy/core/spdy_protocol.h",
     "quiche/spdy/core/spdy_simple_arena.h",
     "quiche/spdy/core/zero_copy_output_buffer.h",
+    "quiche/web_transport/complete_buffer_visitor.h",
     "quiche/web_transport/web_transport.h"
   ],
   "quiche_core_srcs": [
@@ -699,7 +700,8 @@
     "quiche/spdy/core/spdy_pinnable_buffer_piece.cc",
     "quiche/spdy/core/spdy_prefixed_buffer_reader.cc",
     "quiche/spdy/core/spdy_protocol.cc",
-    "quiche/spdy/core/spdy_simple_arena.cc"
+    "quiche/spdy/core/spdy_simple_arena.cc",
+    "quiche/web_transport/complete_buffer_visitor.cc"
   ],
   "quiche_tool_support_hdrs": [
     "quiche/common/platform/api/quiche_command_line_flags.h",
@@ -710,6 +712,7 @@
     "quiche/quic/tools/connect_server_backend.h",
     "quiche/quic/tools/connect_tunnel.h",
     "quiche/quic/tools/connect_udp_tunnel.h",
+    "quiche/quic/tools/devious_baton.h",
     "quiche/quic/tools/fake_proof_verifier.h",
     "quiche/quic/tools/quic_backend_response.h",
     "quiche/quic/tools/quic_client_base.h",
@@ -734,6 +737,7 @@
     "quiche/quic/tools/connect_server_backend.cc",
     "quiche/quic/tools/connect_tunnel.cc",
     "quiche/quic/tools/connect_udp_tunnel.cc",
+    "quiche/quic/tools/devious_baton.cc",
     "quiche/quic/tools/quic_backend_response.cc",
     "quiche/quic/tools/quic_client_base.cc",
     "quiche/quic/tools/quic_memory_cache_backend.cc",
@@ -1352,7 +1356,8 @@
     "quiche/quic/tools/quic_server_bin.cc",
     "quiche/quic/tools/quic_server_factory.cc",
     "quiche/quic/tools/quic_toy_client.cc",
-    "quiche/quic/tools/quic_toy_server.cc"
+    "quiche/quic/tools/quic_toy_server.cc",
+    "quiche/quic/tools/web_transport_test_server.cc"
   ],
   "nghttp2_hdrs": [
     "quiche/http2/adapter/callback_visitor.h",
diff --git a/quiche/BUILD.bazel b/quiche/BUILD.bazel
index 1b46de3..67bf84c 100644
--- a/quiche/BUILD.bazel
+++ b/quiche/BUILD.bazel
@@ -237,6 +237,7 @@
         "@com_google_absl//absl/base:core_headers",
         "@com_google_absl//absl/container:flat_hash_map",
         "@com_google_absl//absl/container:flat_hash_set",
+        "@com_google_absl//absl/functional:bind_front",
         "@com_google_absl//absl/memory",
         "@com_google_absl//absl/status",
         "@com_google_absl//absl/status:statusor",
@@ -531,6 +532,24 @@
     ],
 )
 
+cc_binary(
+    name = "web_transport_test_server",
+    srcs = ["quic/tools/web_transport_test_server.cc"],
+    deps = [
+        ":io_tool_support",
+        ":quic_server_factory",
+        ":quic_toy_server",
+        ":quiche_core",
+        ":quiche_platform_default",
+        ":quiche_platform_default_tools",
+        ":quiche_tool_support",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/status:statusor",
+        "@com_google_absl//absl/strings",
+        "@com_google_googleurl//url",
+    ],
+)
+
 # Indicate that QUICHE APIs are explicitly unstable by providing only
 # appropriately named aliases as publicly visible targets.
 alias(
diff --git a/quiche/common/wire_serialization.h b/quiche/common/wire_serialization.h
index 7cc2596..4cb482b 100644
--- a/quiche/common/wire_serialization.h
+++ b/quiche/common/wire_serialization.h
@@ -398,6 +398,25 @@
   return buffer;
 }
 
+// SerializeIntoBuffer() that returns std::string instead of QuicheBuffer.
+template <typename... Ts>
+absl::StatusOr<std::string> SerializeIntoString(Ts... data) {
+  size_t buffer_size = ComputeLengthOnWire(data...);
+  if (buffer_size == 0) {
+    return std::string();
+  }
+
+  std::string buffer;
+  buffer.resize(buffer_size);
+  QuicheDataWriter writer(buffer.size(), buffer.data());
+  QUICHE_RETURN_IF_ERROR(SerializeIntoWriter(writer, data...));
+  if (writer.remaining() != 0) {
+    return absl::InternalError(absl::StrCat(
+        "Excess ", writer.remaining(), " bytes allocated while serializing"));
+  }
+  return buffer;
+}
+
 }  // namespace quiche
 
 #endif  // QUICHE_COMMON_WIRE_SERIALIZATION_H_
diff --git a/quiche/quic/tools/devious_baton.cc b/quiche/quic/tools/devious_baton.cc
new file mode 100644
index 0000000..a06ce86
--- /dev/null
+++ b/quiche/quic/tools/devious_baton.cc
@@ -0,0 +1,192 @@
+// Copyright 2023 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/devious_baton.h"
+
+#include <cstdint>
+#include <functional>
+#include <memory>
+
+#include "absl/functional/bind_front.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/string_view.h"
+#include "quiche/quic/core/crypto/quic_random.h"
+#include "quiche/quic/core/quic_types.h"
+#include "quiche/common/quiche_data_reader.h"
+#include "quiche/common/wire_serialization.h"
+#include "quiche/web_transport/complete_buffer_visitor.h"
+#include "quiche/web_transport/web_transport.h"
+
+namespace quic {
+
+namespace {
+
+constexpr QuicByteCount kMaxPaddingSize = 64;
+constexpr char kPaddingData[kMaxPaddingSize] = {0};
+
+absl::StatusOr<DeviousBatonValue> Parse(absl::string_view message) {
+  quiche::QuicheDataReader reader(message);
+  uint64_t padding_size;
+  if (!reader.ReadVarInt62(&padding_size)) {
+    return absl::InvalidArgumentError("Failed to read the padding size");
+  }
+  if (!reader.Seek(padding_size)) {
+    return absl::InvalidArgumentError("Failed to skip padding");
+  }
+  DeviousBatonValue value;
+  if (!reader.ReadUInt8(&value)) {
+    return absl::InvalidArgumentError("Failed to read the baton");
+  }
+  if (!reader.IsDoneReading()) {
+    return absl::InvalidArgumentError("Trailing data after the baton");
+  }
+  return value;
+}
+
+std::string Serialize(DeviousBatonValue value) {
+  // Randomize padding size for extra deviousness.
+  QuicByteCount padding_size =
+      QuicRandom::GetInstance()->InsecureRandUint64() % kMaxPaddingSize;
+  absl::string_view padding(kPaddingData, padding_size);
+
+  absl::StatusOr<std::string> result = quiche::SerializeIntoString(
+      quiche::WireStringWithLengthPrefix<quiche::WireVarInt62>(padding),
+      quiche::WireUint8(value));
+  QUICHE_DCHECK(result.ok());
+  return *std::move(result);
+}
+
+class IncomingBidiBatonVisitor : public webtransport::CompleteBufferVisitor {
+ public:
+  IncomingBidiBatonVisitor(webtransport::Session& session,
+                           webtransport::Stream& stream)
+      : CompleteBufferVisitor(
+            &stream, absl::bind_front(
+                         &IncomingBidiBatonVisitor::OnAllDataReceived, this)),
+        session_(&session) {}
+
+ private:
+  void OnAllDataReceived(std::string data) {
+    absl::StatusOr<DeviousBatonValue> value = Parse(data);
+    if (!value.ok()) {
+      session_->CloseSession(kDeviousBatonErrorBruh,
+                             absl::StrCat("Failed to parse incoming baton: ",
+                                          value.status().message()));
+      return;
+    }
+    DeviousBatonValue next_value = 1 + *value;
+    if (next_value != 0) {
+      SetOutgoingData(Serialize(*value + 1));
+    }
+  }
+
+  webtransport::Session* session_;
+};
+
+}  // namespace
+
+void DeviousBatonSessionVisitor::OnSessionReady() {
+  if (!is_server_) {
+    return;
+  }
+  for (int i = 0; i < count_; ++i) {
+    webtransport::Stream* stream = session_->OpenOutgoingUnidirectionalStream();
+    if (stream == nullptr) {
+      session_->CloseSession(
+          kDeviousBatonErrorDaYamn,
+          "Insufficient flow control when opening initial baton streams");
+      return;
+    }
+    stream->SetVisitor(std::make_unique<webtransport::CompleteBufferVisitor>(
+        stream, Serialize(initial_value_)));
+    stream->visitor()->OnCanWrite();
+  }
+}
+
+void DeviousBatonSessionVisitor::OnSessionClosed(
+    webtransport::SessionErrorCode error_code,
+    const std::string& error_message) {
+  QUICHE_LOG(INFO) << "Devious Baton session closed with error " << error_code
+                   << " (message: " << error_message << ")";
+}
+
+void DeviousBatonSessionVisitor::OnIncomingBidirectionalStreamAvailable() {
+  while (true) {
+    webtransport::Stream* stream =
+        session_->AcceptIncomingBidirectionalStream();
+    if (stream == nullptr) {
+      return;
+    }
+    stream->SetVisitor(
+        std::make_unique<IncomingBidiBatonVisitor>(*session_, *stream));
+    stream->visitor()->OnCanRead();
+  }
+}
+
+void DeviousBatonSessionVisitor::OnIncomingUnidirectionalStreamAvailable() {
+  while (true) {
+    webtransport::Stream* stream =
+        session_->AcceptIncomingUnidirectionalStream();
+    if (stream == nullptr) {
+      return;
+    }
+    stream->SetVisitor(std::make_unique<webtransport::CompleteBufferVisitor>(
+        stream, CreateResponseCallback(
+                    &DeviousBatonSessionVisitor::SendBidirectionalBaton)));
+    stream->visitor()->OnCanRead();
+  }
+}
+
+void DeviousBatonSessionVisitor::OnDatagramReceived(
+    absl::string_view datagram) {
+  // TODO(vasilvv): implement datagram behavior.
+}
+
+void DeviousBatonSessionVisitor::OnCanCreateNewOutgoingBidirectionalStream() {
+  while (!outgoing_bidi_batons_.empty()) {
+    webtransport::Stream* stream = session_->OpenOutgoingBidirectionalStream();
+    if (stream == nullptr) {
+      return;
+    }
+    stream->SetVisitor(std::make_unique<webtransport::CompleteBufferVisitor>(
+        stream, Serialize(outgoing_bidi_batons_.front()),
+        CreateResponseCallback(
+            &DeviousBatonSessionVisitor::SendUnidirectionalBaton)));
+    outgoing_bidi_batons_.pop_front();
+    stream->visitor()->OnCanWrite();
+  }
+}
+
+void DeviousBatonSessionVisitor::OnCanCreateNewOutgoingUnidirectionalStream() {
+  while (!outgoing_unidi_batons_.empty()) {
+    webtransport::Stream* stream = session_->OpenOutgoingUnidirectionalStream();
+    if (stream == nullptr) {
+      return;
+    }
+    stream->SetVisitor(std::make_unique<webtransport::CompleteBufferVisitor>(
+        stream, Serialize(outgoing_unidi_batons_.front())));
+    outgoing_unidi_batons_.pop_front();
+    stream->visitor()->OnCanWrite();
+  }
+}
+
+quiche::SingleUseCallback<void(std::string)>
+DeviousBatonSessionVisitor::CreateResponseCallback(SendFunction send_function) {
+  return [this, send_function](std::string data) {
+    absl::StatusOr<DeviousBatonValue> value = Parse(data);
+    if (!value.ok()) {
+      session_->CloseSession(kDeviousBatonErrorBruh,
+                             absl::StrCat("Failed to parse incoming baton: ",
+                                          value.status().message()));
+      return;
+    }
+    DeviousBatonValue new_value = 1 + *value;
+    if (new_value != 0) {
+      std::invoke(send_function, this, *value);
+    }
+  };
+}
+
+}  // namespace quic
diff --git a/quiche/quic/tools/devious_baton.h b/quiche/quic/tools/devious_baton.h
new file mode 100644
index 0000000..50337d2
--- /dev/null
+++ b/quiche/quic/tools/devious_baton.h
@@ -0,0 +1,73 @@
+// Copyright 2023 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_DEVIOUS_BATON_H_
+#define QUICHE_QUIC_TOOLS_DEVIOUS_BATON_H_
+
+#include "quiche/common/quiche_callbacks.h"
+#include "quiche/common/quiche_circular_deque.h"
+#include "quiche/web_transport/web_transport.h"
+
+namespace quic {
+
+// https://www.ietf.org/id/draft-frindell-webtrans-devious-baton-00.html#name-session-error-codes
+inline constexpr webtransport::SessionErrorCode kDeviousBatonErrorDaYamn =
+    0x01;  // Insufficient flow control credit
+inline constexpr webtransport::SessionErrorCode kDeviousBatonErrorBruh =
+    0x02;  // Parse error
+inline constexpr webtransport::SessionErrorCode kDeviousBatonErrorSus =
+    0x03;  // Unexpected message
+inline constexpr webtransport::SessionErrorCode kDeviousBatonErrorBored =
+    0x04;  // Timeout
+
+using DeviousBatonValue = uint8_t;
+
+// Implementation of the Devious Baton protocol as described in
+// https://www.ietf.org/id/draft-frindell-webtrans-devious-baton-00.html
+class DeviousBatonSessionVisitor : public webtransport::SessionVisitor {
+ public:
+  DeviousBatonSessionVisitor(webtransport::Session* session, bool is_server,
+                             int initial_value, int count)
+      : session_(session),
+        is_server_(is_server),
+        initial_value_(initial_value),
+        count_(count) {}
+
+  void OnSessionReady() override;
+  void OnSessionClosed(webtransport::SessionErrorCode error_code,
+                       const std::string& error_message) override;
+  void OnIncomingBidirectionalStreamAvailable() override;
+  void OnIncomingUnidirectionalStreamAvailable() override;
+  void OnDatagramReceived(absl::string_view datagram) override;
+  void OnCanCreateNewOutgoingBidirectionalStream() override;
+  void OnCanCreateNewOutgoingUnidirectionalStream() override;
+
+ private:
+  using SendFunction = void (DeviousBatonSessionVisitor::*)(DeviousBatonValue);
+  void SendUnidirectionalBaton(DeviousBatonValue value) {
+    outgoing_unidi_batons_.push_back(value);
+    OnCanCreateNewOutgoingUnidirectionalStream();
+  }
+  void SendBidirectionalBaton(DeviousBatonValue value) {
+    outgoing_bidi_batons_.push_back(value);
+    OnCanCreateNewOutgoingBidirectionalStream();
+  }
+
+  // Creates a callback that parses an incoming baton, parses it (while
+  // potentially handling parse errors), and then passes it into the
+  // `send_function`.
+  quiche::SingleUseCallback<void(std::string)> CreateResponseCallback(
+      SendFunction send_function);
+
+  webtransport::Session* session_;
+  bool is_server_;
+  DeviousBatonValue initial_value_;
+  int count_;
+  quiche::QuicheCircularDeque<DeviousBatonValue> outgoing_unidi_batons_;
+  quiche::QuicheCircularDeque<DeviousBatonValue> outgoing_bidi_batons_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_TOOLS_DEVIOUS_BATON_H_
diff --git a/quiche/quic/tools/web_transport_test_server.cc b/quiche/quic/tools/web_transport_test_server.cc
new file mode 100644
index 0000000..3e974e6
--- /dev/null
+++ b/quiche/quic/tools/web_transport_test_server.cc
@@ -0,0 +1,140 @@
+// Copyright 2023 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 <memory>
+
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/numbers.h"
+#include "absl/strings/string_view.h"
+#include "url/third_party/mozilla/url_parse.h"
+#include "quiche/quic/core/quic_error_codes.h"
+#include "quiche/quic/core/web_transport_interface.h"
+#include "quiche/quic/platform/api/quic_socket_address.h"
+#include "quiche/quic/tools/devious_baton.h"
+#include "quiche/quic/tools/quic_server.h"
+#include "quiche/quic/tools/quic_simple_server_backend.h"
+#include "quiche/quic/tools/web_transport_test_visitors.h"
+#include "quiche/common/platform/api/quiche_command_line_flags.h"
+#include "quiche/common/platform/api/quiche_default_proof_providers.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/platform/api/quiche_system_event_loop.h"
+#include "quiche/common/quiche_random.h"
+#include "quiche/web_transport/web_transport.h"
+
+DEFINE_QUICHE_COMMAND_LINE_FLAG(
+    int32_t, port, 6121, "The port the WebTransport server will listen on.");
+
+namespace quic {
+namespace {
+
+absl::StatusOr<std::unique_ptr<webtransport::SessionVisitor>> ProcessRequest(
+    const GURL& url, WebTransportSession* session) {
+  if (url.path() == "/webtransport/echo") {
+    return std::make_unique<EchoWebTransportSessionVisitor>(session);
+  }
+  if (url.path() == "/webtransport/devious-baton") {
+    int count = 1;
+    DeviousBatonValue initial_value =
+        quiche::QuicheRandom::GetInstance()->RandUint64() % 256;
+    std::string query = url.query();
+    url::Component query_component, key_component, value_component;
+    query_component.begin = 0;
+    query_component.len = query.size();
+    while (url::ExtractQueryKeyValue(query.data(), &query_component,
+                                     &key_component, &value_component)) {
+      absl::string_view key(query.data() + key_component.begin,
+                            key_component.len);
+      absl::string_view value(query.data() + value_component.begin,
+                              value_component.len);
+      int parsed_value;
+      if (!absl::SimpleAtoi(value, &parsed_value) || parsed_value < 0 ||
+          parsed_value > 255) {
+        if (key == "count" || key == "baton") {
+          return absl::InvalidArgumentError(
+              absl::StrCat("Failed to parse query param ", key));
+        }
+        continue;
+      }
+      if (key == "count") {
+        count = parsed_value;
+      }
+      if (key == "baton") {
+        initial_value = parsed_value;
+      }
+    }
+    return std::make_unique<DeviousBatonSessionVisitor>(
+        session, /*is_server=*/true, initial_value, count);
+  }
+  return absl::NotFoundError("Path not found");
+}
+
+class WebTransportTestBackend : public QuicSimpleServerBackend {
+ public:
+  bool InitializeBackend(const std::string&) override { return true; }
+  bool IsBackendInitialized() const override { return true; }
+  void FetchResponseFromBackend(const spdy::Http2HeaderBlock&,
+                                const std::string&,
+                                RequestHandler* request_handler) override {
+    request_handler->TerminateStreamWithError(
+        QuicResetStreamError::FromInternal(QUIC_STREAM_INTERNAL_ERROR));
+  }
+  void CloseBackendResponseStream(RequestHandler*) override {}
+  bool SupportsWebTransport() override { return true; }
+  WebTransportResponse ProcessWebTransportRequest(
+      const spdy::Http2HeaderBlock& request_headers,
+      WebTransportSession* session) override {
+    WebTransportResponse response;
+    response.response_headers[":status"] = "400";
+
+    auto path = request_headers.find(":path");
+    if (path == request_headers.end()) {
+      return response;
+    }
+    GURL url(absl::StrCat("https://localhost", path->second));
+    if (!url.is_valid()) {
+      return response;
+    }
+    absl::StatusOr<std::unique_ptr<webtransport::SessionVisitor>> processed =
+        ProcessRequest(url, session);
+    switch (processed.status().code()) {
+      case absl::StatusCode::kOk:
+        response.response_headers[":status"] = "200";
+        response.visitor = *std::move(processed);
+        return response;
+      case absl::StatusCode::kNotFound:
+        response.response_headers[":status"] = "404";
+        return response;
+      case absl::StatusCode::kInvalidArgument:
+        response.response_headers[":status"] = "400";
+        return response;
+      default:
+        response.response_headers[":status"] = "500";
+        return response;
+    }
+  }
+};
+
+int Main(int argc, char** argv) {
+  quiche::QuicheSystemEventLoop event_loop("web_transport_test_server");
+  const char* usage = "Usage: web_transport_test_server [options]";
+  std::vector<std::string> non_option_args =
+      quiche::QuicheParseCommandLineFlags(usage, argc, argv);
+
+  WebTransportTestBackend backend;
+  QuicServer server(quiche::CreateDefaultProofSource(), &backend);
+  quic::QuicSocketAddress addr(quic::QuicIpAddress::Any6(),
+                               quiche::GetQuicheCommandLineFlag(FLAGS_port));
+  if (!server.CreateUDPSocketAndListen(addr)) {
+    QUICHE_LOG(ERROR) << "Failed to bind the port address";
+  }
+  QUICHE_LOG(INFO) << "Bound the server on " << addr;
+  server.HandleEventsForever();
+  return 0;
+}
+
+}  // namespace
+}  // namespace quic
+
+int main(int argc, char** argv) { return quic::Main(argc, argv); }
diff --git a/quiche/quic/tools/web_transport_test_visitors.h b/quiche/quic/tools/web_transport_test_visitors.h
index 8823562..685a892 100644
--- a/quiche/quic/tools/web_transport_test_visitors.h
+++ b/quiche/quic/tools/web_transport_test_visitors.h
@@ -11,13 +11,11 @@
 #include "quiche/quic/core/web_transport_interface.h"
 #include "quiche/quic/platform/api/quic_logging.h"
 #include "quiche/common/platform/api/quiche_logging.h"
-#include "quiche/common/platform/api/quiche_mem_slice.h"
-#include "quiche/common/quiche_callbacks.h"
 #include "quiche/common/quiche_circular_deque.h"
 #include "quiche/common/quiche_stream.h"
 #include "quiche/common/simple_buffer_allocator.h"
+#include "quiche/web_transport/complete_buffer_visitor.h"
 #include "quiche/web_transport/web_transport.h"
-#include "quiche/spdy/core/http2_header_block.h"
 
 namespace quic {
 
@@ -149,75 +147,10 @@
   bool stop_sending_received_ = false;
 };
 
-// Buffers all of the data and calls |callback| with the entirety of the stream
-// data.
-class WebTransportUnidirectionalEchoReadVisitor
-    : public WebTransportStreamVisitor {
- public:
-  using Callback = quiche::MultiUseCallback<void(const std::string&)>;
-
-  WebTransportUnidirectionalEchoReadVisitor(WebTransportStream* stream,
-                                            Callback callback)
-      : stream_(stream), callback_(std::move(callback)) {}
-
-  void OnCanRead() override {
-    WebTransportStream::ReadResult result = stream_->Read(&buffer_);
-    QUIC_DVLOG(1) << "Attempted reading on WebTransport unidirectional stream "
-                  << stream_->GetStreamId()
-                  << ", bytes read: " << result.bytes_read;
-    if (result.fin) {
-      QUIC_DVLOG(1) << "Finished receiving data on a WebTransport stream "
-                    << stream_->GetStreamId() << ", queueing up the echo";
-      callback_(buffer_);
-    }
-  }
-
-  void OnCanWrite() override { QUICHE_NOTREACHED(); }
-
-  void OnResetStreamReceived(WebTransportStreamError /*error*/) override {}
-  void OnStopSendingReceived(WebTransportStreamError /*error*/) override {}
-  void OnWriteSideInDataRecvdState() override {}
-
- private:
-  WebTransportStream* stream_;
-  std::string buffer_;
-  Callback callback_;
-};
-
-// Sends supplied data.
-class WebTransportUnidirectionalEchoWriteVisitor
-    : public WebTransportStreamVisitor {
- public:
-  WebTransportUnidirectionalEchoWriteVisitor(WebTransportStream* stream,
-                                             const std::string& data)
-      : stream_(stream), data_(data) {}
-
-  void OnCanRead() override { QUICHE_NOTREACHED(); }
-  void OnCanWrite() override {
-    if (data_.empty()) {
-      return;
-    }
-    absl::Status write_status = quiche::WriteIntoStream(*stream_, data_);
-    if (!write_status.ok()) {
-      QUICHE_DLOG_IF(WARNING, !absl::IsUnavailable(write_status))
-          << "Failed to write into stream: " << write_status;
-      return;
-    }
-    data_ = "";
-    absl::Status fin_status = quiche::SendFinOnStream(*stream_);
-    QUICHE_DVLOG(1)
-        << "WebTransportUnidirectionalEchoWriteVisitor finished sending data.";
-    QUICHE_DCHECK(fin_status.ok());
-  }
-
-  void OnResetStreamReceived(WebTransportStreamError /*error*/) override {}
-  void OnStopSendingReceived(WebTransportStreamError /*error*/) override {}
-  void OnWriteSideInDataRecvdState() override {}
-
- private:
-  WebTransportStream* stream_;
-  std::string data_;
-};
+using WebTransportUnidirectionalEchoReadVisitor =
+    ::webtransport::CompleteBufferVisitor;
+using WebTransportUnidirectionalEchoWriteVisitor =
+    ::webtransport::CompleteBufferVisitor;
 
 // A session visitor which sets unidirectional or bidirectional stream visitors
 // to echo.
diff --git a/quiche/web_transport/complete_buffer_visitor.cc b/quiche/web_transport/complete_buffer_visitor.cc
new file mode 100644
index 0000000..d0ef471
--- /dev/null
+++ b/quiche/web_transport/complete_buffer_visitor.cc
@@ -0,0 +1,51 @@
+// Copyright 2023 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/web_transport/complete_buffer_visitor.h"
+
+#include <utility>
+
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/quiche_stream.h"
+
+namespace webtransport {
+
+void CompleteBufferVisitor::OnCanRead() {
+  if (!incoming_data_callback_.has_value()) {
+    return;
+  }
+  Stream::ReadResult result = stream_->Read(&incoming_data_buffer_);
+  if (result.fin) {
+    (*std::move(incoming_data_callback_))(std::move(incoming_data_buffer_));
+    incoming_data_callback_.reset();
+  }
+}
+
+void CompleteBufferVisitor::OnCanWrite() {
+  if (!outgoing_data_.has_value()) {
+    return;
+  }
+  if (!stream_->CanWrite()) {
+    return;
+  }
+  quiche::StreamWriteOptions options;
+  options.set_send_fin(true);
+  absl::Status status =
+      quiche::WriteIntoStream(*stream_, *outgoing_data_, options);
+  if (!status.ok()) {
+    QUICHE_DLOG(WARNING) << "Write from OnCanWrite() failed: " << status;
+    return;
+  }
+  outgoing_data_.reset();
+}
+
+void CompleteBufferVisitor::SetOutgoingData(std::string data) {
+  QUICHE_DCHECK(!outgoing_data_.has_value());
+  outgoing_data_ = std::move(data);
+  if (stream_->CanWrite()) {
+    OnCanWrite();
+  }
+}
+
+}  // namespace webtransport
diff --git a/quiche/web_transport/complete_buffer_visitor.h b/quiche/web_transport/complete_buffer_visitor.h
new file mode 100644
index 0000000..498f148
--- /dev/null
+++ b/quiche/web_transport/complete_buffer_visitor.h
@@ -0,0 +1,55 @@
+// Copyright 2023 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_WEB_TRANSPORT_COMPLETE_BUFFER_VISITOR_H_
+#define QUICHE_WEB_TRANSPORT_COMPLETE_BUFFER_VISITOR_H_
+
+#include "absl/types/optional.h"
+#include "absl/utility/utility.h"
+#include "quiche/common/quiche_callbacks.h"
+#include "quiche/web_transport/web_transport.h"
+
+namespace webtransport {
+
+// A visitor that will buffer the entirety of the incoming stream into a string,
+// and will send a pre-specified string all at once.
+class QUICHE_EXPORT CompleteBufferVisitor : public StreamVisitor {
+ public:
+  using AllDataReadCallback = quiche::SingleUseCallback<void(std::string)>;
+
+  CompleteBufferVisitor(webtransport::Stream* stream, std::string outgoing_data)
+      : stream_(stream),
+        outgoing_data_(absl::in_place, std::move(outgoing_data)) {}
+  CompleteBufferVisitor(webtransport::Stream* stream,
+                        AllDataReadCallback incoming_data_callback)
+      : stream_(stream),
+        incoming_data_callback_(absl::in_place,
+                                std::move(incoming_data_callback)) {}
+  CompleteBufferVisitor(webtransport::Stream* stream, std::string outgoing_data,
+                        AllDataReadCallback incoming_data_callback)
+      : stream_(stream),
+        outgoing_data_(absl::in_place, std::move(outgoing_data)),
+        incoming_data_callback_(absl::in_place,
+                                std::move(incoming_data_callback)) {}
+
+  void OnCanRead() override;
+  void OnCanWrite() override;
+
+  void OnResetStreamReceived(StreamErrorCode) override {}
+  void OnStopSendingReceived(StreamErrorCode) override {}
+  void OnWriteSideInDataRecvdState() override {}
+
+ protected:
+  void SetOutgoingData(std::string data);
+
+ private:
+  webtransport::Stream* stream_;
+  absl::optional<std::string> outgoing_data_;
+  absl::optional<AllDataReadCallback> incoming_data_callback_;
+  std::string incoming_data_buffer_;
+};
+
+}  // namespace webtransport
+
+#endif  // QUICHE_WEB_TRANSPORT_COMPLETE_BUFFER_VISITOR_H_