CONNECT-UDP datagram payload parse/serialization

Implement payload format from RFC 9298, Section 5.

PiperOrigin-RevId: 476896033
diff --git a/build/source_list.bzl b/build/source_list.bzl
index b2f73be..96f60f4 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -20,6 +20,7 @@
     "balsa/noop_balsa_visitor.h",
     "balsa/simple_buffer.h",
     "balsa/standard_header_map.h",
+    "common/masque/connect_udp_datagram_payload.h",
     "common/platform/api/quiche_bug_tracker.h",
     "common/platform/api/quiche_client_stats.h",
     "common/platform/api/quiche_containers.h",
@@ -407,6 +408,7 @@
     "balsa/http_validation_policy.cc",
     "balsa/simple_buffer.cc",
     "balsa/standard_header_map.cc",
+    "common/masque/connect_udp_datagram_payload.cc",
     "common/platform/api/quiche_hostname_utils.cc",
     "common/platform/api/quiche_mutex.cc",
     "common/quiche_buffer_allocator.cc",
@@ -1029,6 +1031,7 @@
     "balsa/header_properties_test.cc",
     "balsa/simple_buffer_test.cc",
     "binary_http/binary_http_message_test.cc",
+    "common/masque/connect_udp_datagram_payload_test.cc",
     "common/platform/api/quiche_file_utils_test.cc",
     "common/platform/api/quiche_hostname_utils_test.cc",
     "common/platform/api/quiche_lower_case_string_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index 18fcd44..8f7f359 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -20,6 +20,7 @@
     "src/quiche/balsa/noop_balsa_visitor.h",
     "src/quiche/balsa/simple_buffer.h",
     "src/quiche/balsa/standard_header_map.h",
+    "src/quiche/common/masque/connect_udp_datagram_payload.h",
     "src/quiche/common/platform/api/quiche_bug_tracker.h",
     "src/quiche/common/platform/api/quiche_client_stats.h",
     "src/quiche/common/platform/api/quiche_containers.h",
@@ -407,6 +408,7 @@
     "src/quiche/balsa/http_validation_policy.cc",
     "src/quiche/balsa/simple_buffer.cc",
     "src/quiche/balsa/standard_header_map.cc",
+    "src/quiche/common/masque/connect_udp_datagram_payload.cc",
     "src/quiche/common/platform/api/quiche_hostname_utils.cc",
     "src/quiche/common/platform/api/quiche_mutex.cc",
     "src/quiche/common/quiche_buffer_allocator.cc",
@@ -1029,6 +1031,7 @@
     "src/quiche/balsa/header_properties_test.cc",
     "src/quiche/balsa/simple_buffer_test.cc",
     "src/quiche/binary_http/binary_http_message_test.cc",
+    "src/quiche/common/masque/connect_udp_datagram_payload_test.cc",
     "src/quiche/common/platform/api/quiche_file_utils_test.cc",
     "src/quiche/common/platform/api/quiche_hostname_utils_test.cc",
     "src/quiche/common/platform/api/quiche_lower_case_string_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index df96337..1f74bf4 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -19,6 +19,7 @@
     "quiche/balsa/noop_balsa_visitor.h",
     "quiche/balsa/simple_buffer.h",
     "quiche/balsa/standard_header_map.h",
+    "quiche/common/masque/connect_udp_datagram_payload.h",
     "quiche/common/platform/api/quiche_bug_tracker.h",
     "quiche/common/platform/api/quiche_client_stats.h",
     "quiche/common/platform/api/quiche_containers.h",
@@ -406,6 +407,7 @@
     "quiche/balsa/http_validation_policy.cc",
     "quiche/balsa/simple_buffer.cc",
     "quiche/balsa/standard_header_map.cc",
+    "quiche/common/masque/connect_udp_datagram_payload.cc",
     "quiche/common/platform/api/quiche_hostname_utils.cc",
     "quiche/common/platform/api/quiche_mutex.cc",
     "quiche/common/quiche_buffer_allocator.cc",
@@ -1028,6 +1030,7 @@
     "quiche/balsa/header_properties_test.cc",
     "quiche/balsa/simple_buffer_test.cc",
     "quiche/binary_http/binary_http_message_test.cc",
+    "quiche/common/masque/connect_udp_datagram_payload_test.cc",
     "quiche/common/platform/api/quiche_file_utils_test.cc",
     "quiche/common/platform/api/quiche_hostname_utils_test.cc",
     "quiche/common/platform/api/quiche_lower_case_string_test.cc",
diff --git a/quiche/common/masque/connect_udp_datagram_payload.cc b/quiche/common/masque/connect_udp_datagram_payload.cc
new file mode 100644
index 0000000..ae01817
--- /dev/null
+++ b/quiche/common/masque/connect_udp_datagram_payload.cc
@@ -0,0 +1,130 @@
+// Copyright 2022 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/common/masque/connect_udp_datagram_payload.h"
+
+#include <cstdint>
+#include <memory>
+#include <utility>
+
+#include "absl/strings/string_view.h"
+#include "quiche/common/platform/api/quiche_bug_tracker.h"
+#include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/quiche_data_reader.h"
+#include "quiche/common/quiche_data_writer.h"
+
+namespace quiche {
+
+// static
+std::unique_ptr<ConnectUdpDatagramPayload> ConnectUdpDatagramPayload::Parse(
+    absl::string_view datagram_payload) {
+  QuicheDataReader data_reader(datagram_payload);
+
+  uint64_t context_id;
+  if (!data_reader.ReadVarInt62(&context_id)) {
+    QUICHE_DVLOG(1) << "Could not parse malformed UDP proxy payload";
+    return nullptr;
+  }
+
+  if (ContextId{context_id} == ConnectUdpDatagramUdpPacketPayload::kContextId) {
+    return std::make_unique<ConnectUdpDatagramUdpPacketPayload>(
+        data_reader.ReadRemainingPayload());
+  } else {
+    return std::make_unique<ConnectUdpDatagramUnknownPayload>(
+        ContextId{context_id}, data_reader.ReadRemainingPayload());
+  }
+}
+
+std::string ConnectUdpDatagramPayload::Serialize() const {
+  std::string buffer(SerializedLength(), '\0');
+  QuicheDataWriter writer(buffer.size(), buffer.data());
+
+  bool result = SerializeTo(writer);
+  QUICHE_DCHECK(result);
+  QUICHE_DCHECK_EQ(writer.remaining(), 0u);
+
+  return buffer;
+}
+
+ConnectUdpDatagramUdpPacketPayload::ConnectUdpDatagramUdpPacketPayload(
+    absl::string_view udp_packet)
+    : udp_packet_(udp_packet) {}
+
+ConnectUdpDatagramPayload::ContextId
+ConnectUdpDatagramUdpPacketPayload::GetContextId() const {
+  return kContextId;
+}
+
+ConnectUdpDatagramPayload::Type ConnectUdpDatagramUdpPacketPayload::GetType()
+    const {
+  return Type::kUdpPacket;
+}
+
+absl::string_view ConnectUdpDatagramUdpPacketPayload::GetUdpProxyingPayload()
+    const {
+  return udp_packet_;
+}
+
+size_t ConnectUdpDatagramUdpPacketPayload::SerializedLength() const {
+  return udp_packet_.size() +
+         QuicheDataWriter::GetVarInt62Len(uint64_t{kContextId});
+}
+
+bool ConnectUdpDatagramUdpPacketPayload::SerializeTo(
+    QuicheDataWriter& writer) const {
+  if (!writer.WriteVarInt62(uint64_t{kContextId})) {
+    return false;
+  }
+
+  if (!writer.WriteStringPiece(udp_packet_)) {
+    return false;
+  }
+
+  return true;
+}
+
+ConnectUdpDatagramUnknownPayload::ConnectUdpDatagramUnknownPayload(
+    ContextId context_id, absl::string_view udp_proxying_payload)
+    : context_id_(context_id), udp_proxying_payload_(udp_proxying_payload) {
+  if (context_id == ConnectUdpDatagramUdpPacketPayload::kContextId) {
+    QUICHE_BUG(udp_proxy_unknown_payload_udp_context)
+        << "ConnectUdpDatagramUnknownPayload created with UDP packet context "
+           "type (0). Should instead create a "
+           "ConnectUdpDatagramUdpPacketPayload.";
+  }
+}
+
+ConnectUdpDatagramPayload::ContextId
+ConnectUdpDatagramUnknownPayload::GetContextId() const {
+  return context_id_;
+}
+
+ConnectUdpDatagramPayload::Type ConnectUdpDatagramUnknownPayload::GetType()
+    const {
+  return Type::kUnknown;
+}
+absl::string_view ConnectUdpDatagramUnknownPayload::GetUdpProxyingPayload()
+    const {
+  return udp_proxying_payload_;
+}
+
+size_t ConnectUdpDatagramUnknownPayload::SerializedLength() const {
+  return udp_proxying_payload_.size() +
+         QuicheDataWriter::GetVarInt62Len(uint64_t{context_id_});
+}
+
+bool ConnectUdpDatagramUnknownPayload::SerializeTo(
+    QuicheDataWriter& writer) const {
+  if (!writer.WriteVarInt62(uint64_t{context_id_})) {
+    return false;
+  }
+
+  if (!writer.WriteStringPiece(udp_proxying_payload_)) {
+    return false;
+  }
+
+  return true;
+}
+
+}  // namespace quiche
diff --git a/quiche/common/masque/connect_udp_datagram_payload.h b/quiche/common/masque/connect_udp_datagram_payload.h
new file mode 100644
index 0000000..8091670
--- /dev/null
+++ b/quiche/common/masque/connect_udp_datagram_payload.h
@@ -0,0 +1,100 @@
+// Copyright 2022 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef QUICHE_COMMON_MASQUE_CONNECT_UDP_DATAGRAM_PAYLOAD_H_
+#define QUICHE_COMMON_MASQUE_CONNECT_UDP_DATAGRAM_PAYLOAD_H_
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/common/quiche_data_writer.h"
+
+namespace quiche {
+
+// UDP-proxying HTTP Datagram payload for use with CONNECT-UDP. See RFC 9298,
+// Section 5.
+class QUICHE_EXPORT_PRIVATE ConnectUdpDatagramPayload {
+ public:
+  using ContextId = uint64_t;
+  enum class Type { kUdpPacket, kUnknown };
+
+  // Parse from `datagram_payload` (a wire-format UDP-proxying HTTP datagram
+  // payload). Returns nullptr on error. The created ConnectUdpDatagramPayload
+  // object may use absl::string_views pointing into `datagram_payload`, so the
+  // data pointed to by `datagram_payload` must outlive the created
+  // ConnectUdpDatagramPayload object.
+  static std::unique_ptr<ConnectUdpDatagramPayload> Parse(
+      absl::string_view datagram_payload);
+
+  ConnectUdpDatagramPayload() = default;
+
+  ConnectUdpDatagramPayload(const ConnectUdpDatagramPayload&) = delete;
+  ConnectUdpDatagramPayload& operator=(const ConnectUdpDatagramPayload&) =
+      delete;
+
+  virtual ~ConnectUdpDatagramPayload() = default;
+
+  virtual ContextId GetContextId() const = 0;
+  virtual Type GetType() const = 0;
+  // Get the inner payload (the UDP Proxying Payload).
+  virtual absl::string_view GetUdpProxyingPayload() const = 0;
+
+  // Length of this UDP-proxying HTTP datagram payload in wire format.
+  virtual size_t SerializedLength() const = 0;
+  // Write a wire-format buffer for the payload. Returns false on write failure
+  // (typically due to `writer` buffer being full).
+  virtual bool SerializeTo(QuicheDataWriter& writer) const = 0;
+
+  // Write a wire-format buffer.
+  std::string Serialize() const;
+};
+
+// UDP-proxying HTTP Datagram payload that encodes a UDP packet.
+class QUICHE_EXPORT_PRIVATE ConnectUdpDatagramUdpPacketPayload final
+    : public ConnectUdpDatagramPayload {
+ public:
+  static constexpr ContextId kContextId = 0;
+
+  // The string pointed to by `udp_packet` must outlive the created
+  // ConnectUdpDatagramUdpPacketPayload.
+  explicit ConnectUdpDatagramUdpPacketPayload(absl::string_view udp_packet);
+
+  ContextId GetContextId() const override;
+  Type GetType() const override;
+  absl::string_view GetUdpProxyingPayload() const override;
+  size_t SerializedLength() const override;
+  bool SerializeTo(QuicheDataWriter& writer) const override;
+
+  absl::string_view udp_packet() const { return udp_packet_; }
+
+ private:
+  absl::string_view udp_packet_;
+};
+
+class QUICHE_EXPORT_PRIVATE ConnectUdpDatagramUnknownPayload final
+    : public ConnectUdpDatagramPayload {
+ public:
+  // `udp_proxying_payload` represents the inner payload contained by the UDP-
+  // proxying HTTP datagram payload. The string pointed to by `inner_payload`
+  // must outlive the created ConnectUdpDatagramUnknownPayload.
+  ConnectUdpDatagramUnknownPayload(ContextId context_id,
+                                   absl::string_view udp_proxying_payload);
+
+  ContextId GetContextId() const override;
+  Type GetType() const override;
+  absl::string_view GetUdpProxyingPayload() const override;
+  size_t SerializedLength() const override;
+  bool SerializeTo(QuicheDataWriter& writer) const override;
+
+ private:
+  ContextId context_id_;
+  absl::string_view udp_proxying_payload_;  // The inner payload.
+};
+
+}  // namespace quiche
+
+#endif  // QUICHE_COMMON_MASQUE_CONNECT_UDP_DATAGRAM_PAYLOAD_H_
diff --git a/quiche/common/masque/connect_udp_datagram_payload_test.cc b/quiche/common/masque/connect_udp_datagram_payload_test.cc
new file mode 100644
index 0000000..52503e3
--- /dev/null
+++ b/quiche/common/masque/connect_udp_datagram_payload_test.cc
@@ -0,0 +1,61 @@
+// Copyright 2022 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "quiche/common/masque/connect_udp_datagram_payload.h"
+
+#include <memory>
+
+#include "absl/strings/string_view.h"
+#include "quiche/common/platform/api/quiche_test.h"
+
+namespace quiche::test {
+namespace {
+
+TEST(ConnectUdpDatagramPayloadTest, ParseUdpPacket) {
+  static constexpr char kDatagramPayload[] = "\x00packet";
+
+  std::unique_ptr<ConnectUdpDatagramPayload> parsed =
+      ConnectUdpDatagramPayload::Parse(
+          absl::string_view(kDatagramPayload, sizeof(kDatagramPayload) - 1));
+  ASSERT_TRUE(parsed);
+
+  EXPECT_EQ(parsed->GetContextId(),
+            ConnectUdpDatagramUdpPacketPayload::kContextId);
+  EXPECT_EQ(parsed->GetType(), ConnectUdpDatagramPayload::Type::kUdpPacket);
+  EXPECT_EQ(parsed->GetUdpProxyingPayload(), "packet");
+}
+
+TEST(ConnectUdpDatagramPayloadTest, SerializeUdpPacket) {
+  static constexpr absl::string_view kUdpPacket = "packet";
+
+  ConnectUdpDatagramUdpPacketPayload payload(kUdpPacket);
+  EXPECT_EQ(payload.GetUdpProxyingPayload(), kUdpPacket);
+
+  EXPECT_EQ(payload.Serialize(), std::string("\x00packet", 7));
+}
+
+TEST(ConnectUdpDatagramPayloadTest, ParseUnknownPacket) {
+  static constexpr char kDatagramPayload[] = "\x05packet";
+
+  std::unique_ptr<ConnectUdpDatagramPayload> parsed =
+      ConnectUdpDatagramPayload::Parse(
+          absl::string_view(kDatagramPayload, sizeof(kDatagramPayload) - 1));
+  ASSERT_TRUE(parsed);
+
+  EXPECT_EQ(parsed->GetContextId(), 5);
+  EXPECT_EQ(parsed->GetType(), ConnectUdpDatagramPayload::Type::kUnknown);
+  EXPECT_EQ(parsed->GetUdpProxyingPayload(), "packet");
+}
+
+TEST(ConnectUdpDatagramPayloadTest, SerializeUnknownPacket) {
+  static constexpr absl::string_view kInnerUdpProxyingPayload = "packet";
+
+  ConnectUdpDatagramUnknownPayload payload(4u, kInnerUdpProxyingPayload);
+  EXPECT_EQ(payload.GetUdpProxyingPayload(), kInnerUdpProxyingPayload);
+
+  EXPECT_EQ(payload.Serialize(), std::string("\x04packet", 7));
+}
+
+}  // namespace
+}  // namespace quiche::test