CONNECT-IP payload parse/serialization

Implement the payload format from RFC 9484, Section 6.

PiperOrigin-RevId: 586798600
diff --git a/build/source_list.bzl b/build/source_list.bzl
index df02f45..ade85e5 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -13,6 +13,7 @@
     "common/capsule.h",
     "common/http/http_header_block.h",
     "common/http/http_header_storage.h",
+    "common/masque/connect_ip_datagram_payload.h",
     "common/masque/connect_udp_datagram_payload.h",
     "common/platform/api/quiche_bug_tracker.h",
     "common/platform/api/quiche_client_stats.h",
@@ -400,6 +401,7 @@
     "common/capsule.cc",
     "common/http/http_header_block.cc",
     "common/http/http_header_storage.cc",
+    "common/masque/connect_ip_datagram_payload.cc",
     "common/masque/connect_udp_datagram_payload.cc",
     "common/platform/api/quiche_hostname_utils.cc",
     "common/platform/api/quiche_mutex.cc",
@@ -1052,6 +1054,7 @@
     "common/capsule_test.cc",
     "common/http/http_header_block_test.cc",
     "common/http/http_header_storage_test.cc",
+    "common/masque/connect_ip_datagram_payload_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",
diff --git a/build/source_list.gni b/build/source_list.gni
index 16a7eea..c514ab2 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -13,6 +13,7 @@
     "src/quiche/common/capsule.h",
     "src/quiche/common/http/http_header_block.h",
     "src/quiche/common/http/http_header_storage.h",
+    "src/quiche/common/masque/connect_ip_datagram_payload.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",
@@ -400,6 +401,7 @@
     "src/quiche/common/capsule.cc",
     "src/quiche/common/http/http_header_block.cc",
     "src/quiche/common/http/http_header_storage.cc",
+    "src/quiche/common/masque/connect_ip_datagram_payload.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",
@@ -1053,6 +1055,7 @@
     "src/quiche/common/capsule_test.cc",
     "src/quiche/common/http/http_header_block_test.cc",
     "src/quiche/common/http/http_header_storage_test.cc",
+    "src/quiche/common/masque/connect_ip_datagram_payload_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",
diff --git a/build/source_list.json b/build/source_list.json
index 41946db..68779fa 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -12,6 +12,7 @@
     "quiche/common/capsule.h",
     "quiche/common/http/http_header_block.h",
     "quiche/common/http/http_header_storage.h",
+    "quiche/common/masque/connect_ip_datagram_payload.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",
@@ -399,6 +400,7 @@
     "quiche/common/capsule.cc",
     "quiche/common/http/http_header_block.cc",
     "quiche/common/http/http_header_storage.cc",
+    "quiche/common/masque/connect_ip_datagram_payload.cc",
     "quiche/common/masque/connect_udp_datagram_payload.cc",
     "quiche/common/platform/api/quiche_hostname_utils.cc",
     "quiche/common/platform/api/quiche_mutex.cc",
@@ -1052,6 +1054,7 @@
     "quiche/common/capsule_test.cc",
     "quiche/common/http/http_header_block_test.cc",
     "quiche/common/http/http_header_storage_test.cc",
+    "quiche/common/masque/connect_ip_datagram_payload_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",
diff --git a/quiche/common/masque/connect_ip_datagram_payload.cc b/quiche/common/masque/connect_ip_datagram_payload.cc
new file mode 100644
index 0000000..cf97c21
--- /dev/null
+++ b/quiche/common/masque/connect_ip_datagram_payload.cc
@@ -0,0 +1,131 @@
+// Copyright 2023 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_ip_datagram_payload.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <string>
+
+#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<ConnectIpDatagramPayload> ConnectIpDatagramPayload::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 IP proxy payload";
+    return nullptr;
+  }
+
+  if (ContextId{context_id} == ConnectIpDatagramIpPacketPayload::kContextId) {
+    return std::make_unique<ConnectIpDatagramIpPacketPayload>(
+        data_reader.ReadRemainingPayload());
+  } else {
+    return std::make_unique<ConnectIpDatagramUnknownPayload>(
+        ContextId{context_id}, data_reader.ReadRemainingPayload());
+  }
+}
+
+std::string ConnectIpDatagramPayload::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;
+}
+
+ConnectIpDatagramIpPacketPayload::ConnectIpDatagramIpPacketPayload(
+    absl::string_view ip_packet)
+    : ip_packet_(ip_packet) {}
+
+ConnectIpDatagramPayload::ContextId
+ConnectIpDatagramIpPacketPayload::GetContextId() const {
+  return kContextId;
+}
+
+ConnectIpDatagramPayload::Type ConnectIpDatagramIpPacketPayload::GetType()
+    const {
+  return Type::kIpPacket;
+}
+
+absl::string_view ConnectIpDatagramIpPacketPayload::GetIpProxyingPayload()
+    const {
+  return ip_packet_;
+}
+
+size_t ConnectIpDatagramIpPacketPayload::SerializedLength() const {
+  return ip_packet_.size() +
+         QuicheDataWriter::GetVarInt62Len(uint64_t{kContextId});
+}
+
+bool ConnectIpDatagramIpPacketPayload::SerializeTo(
+    QuicheDataWriter& writer) const {
+  if (!writer.WriteVarInt62(uint64_t{kContextId})) {
+    return false;
+  }
+
+  if (!writer.WriteStringPiece(ip_packet_)) {
+    return false;
+  }
+
+  return true;
+}
+
+ConnectIpDatagramUnknownPayload::ConnectIpDatagramUnknownPayload(
+    ContextId context_id, absl::string_view ip_proxying_payload)
+    : context_id_(context_id), ip_proxying_payload_(ip_proxying_payload) {
+  if (context_id == ConnectIpDatagramIpPacketPayload::kContextId) {
+    QUICHE_BUG(ip_proxy_unknown_payload_ip_context)
+        << "ConnectIpDatagramUnknownPayload created with IP packet context "
+           "ID (0). Should instead create a "
+           "ConnectIpDatagramIpPacketPayload.";
+  }
+}
+
+ConnectIpDatagramPayload::ContextId
+ConnectIpDatagramUnknownPayload::GetContextId() const {
+  return context_id_;
+}
+
+ConnectIpDatagramPayload::Type ConnectIpDatagramUnknownPayload::GetType()
+    const {
+  return Type::kUnknown;
+}
+absl::string_view ConnectIpDatagramUnknownPayload::GetIpProxyingPayload()
+    const {
+  return ip_proxying_payload_;
+}
+
+size_t ConnectIpDatagramUnknownPayload::SerializedLength() const {
+  return ip_proxying_payload_.size() +
+         QuicheDataWriter::GetVarInt62Len(uint64_t{context_id_});
+}
+
+bool ConnectIpDatagramUnknownPayload::SerializeTo(
+    QuicheDataWriter& writer) const {
+  if (!writer.WriteVarInt62(uint64_t{context_id_})) {
+    return false;
+  }
+
+  if (!writer.WriteStringPiece(ip_proxying_payload_)) {
+    return false;
+  }
+
+  return true;
+}
+
+}  // namespace quiche
diff --git a/quiche/common/masque/connect_ip_datagram_payload.h b/quiche/common/masque/connect_ip_datagram_payload.h
new file mode 100644
index 0000000..978b74c
--- /dev/null
+++ b/quiche/common/masque/connect_ip_datagram_payload.h
@@ -0,0 +1,100 @@
+// Copyright 2023 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_IP_DATAGRAM_PAYLOAD_H_
+#define QUICHE_COMMON_MASQUE_CONNECT_IP_DATAGRAM_PAYLOAD_H_
+
+#include <cstddef>
+#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 {
+
+// IP-proxying HTTP Datagram payload for use with CONNECT-IP. See RFC 9484,
+// Section 6.
+class QUICHE_EXPORT ConnectIpDatagramPayload {
+ public:
+  using ContextId = uint64_t;
+  enum class Type { kIpPacket, kUnknown };
+
+  // Parse from `datagram_payload` (a wire-format IP-proxying HTTP datagram
+  // payload). Returns nullptr on error. The created ConnectIpDatagramPayload
+  // object may use absl::string_views pointing into `datagram_payload`, so the
+  // data pointed to by `datagram_payload` must outlive the created
+  // ConnectIpDatagramPayload object.
+  static std::unique_ptr<ConnectIpDatagramPayload> Parse(
+      absl::string_view datagram_payload);
+
+  ConnectIpDatagramPayload() = default;
+
+  ConnectIpDatagramPayload(const ConnectIpDatagramPayload&) = delete;
+  ConnectIpDatagramPayload& operator=(const ConnectIpDatagramPayload&) = delete;
+
+  virtual ~ConnectIpDatagramPayload() = default;
+
+  virtual ContextId GetContextId() const = 0;
+  virtual Type GetType() const = 0;
+  // Get the inner payload (the IP Proxying Payload).
+  virtual absl::string_view GetIpProxyingPayload() const = 0;
+
+  // Length of this IP-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;
+};
+
+// IP-proxying HTTP Datagram payload that encodes an IP packet.
+class QUICHE_EXPORT ConnectIpDatagramIpPacketPayload final
+    : public ConnectIpDatagramPayload {
+ public:
+  static constexpr ContextId kContextId = 0;
+
+  // The string pointed to by `ip_packet` must outlive the created
+  // ConnectIpDatagramIpPacketPayload.
+  explicit ConnectIpDatagramIpPacketPayload(absl::string_view ip_packet);
+
+  ContextId GetContextId() const override;
+  Type GetType() const override;
+  absl::string_view GetIpProxyingPayload() const override;
+  size_t SerializedLength() const override;
+  bool SerializeTo(QuicheDataWriter& writer) const override;
+
+  absl::string_view ip_packet() const { return ip_packet_; }
+
+ private:
+  absl::string_view ip_packet_;
+};
+
+class QUICHE_EXPORT ConnectIpDatagramUnknownPayload final
+    : public ConnectIpDatagramPayload {
+ public:
+  // `ip_proxying_payload` represents the inner payload contained by the IP-
+  // proxying HTTP datagram payload. The string pointed to by `inner_payload`
+  // must outlive the created ConnectIpDatagramUnknownPayload.
+  ConnectIpDatagramUnknownPayload(ContextId context_id,
+                                  absl::string_view ip_proxying_payload);
+
+  ContextId GetContextId() const override;
+  Type GetType() const override;
+  absl::string_view GetIpProxyingPayload() const override;
+  size_t SerializedLength() const override;
+  bool SerializeTo(QuicheDataWriter& writer) const override;
+
+ private:
+  ContextId context_id_;
+  absl::string_view ip_proxying_payload_;  // The inner payload.
+};
+
+}  // namespace quiche
+
+#endif  // QUICHE_COMMON_MASQUE_CONNECT_IP_DATAGRAM_PAYLOAD_H_
diff --git a/quiche/common/masque/connect_ip_datagram_payload_test.cc b/quiche/common/masque/connect_ip_datagram_payload_test.cc
new file mode 100644
index 0000000..2eb6f51
--- /dev/null
+++ b/quiche/common/masque/connect_ip_datagram_payload_test.cc
@@ -0,0 +1,62 @@
+// Copyright 2023 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_ip_datagram_payload.h"
+
+#include <memory>
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "quiche/common/platform/api/quiche_test.h"
+
+namespace quiche::test {
+namespace {
+
+TEST(ConnectIpDatagramPayloadTest, ParseIpPacket) {
+  static constexpr char kDatagramPayload[] = "\x00packet";
+
+  std::unique_ptr<ConnectIpDatagramPayload> parsed =
+      ConnectIpDatagramPayload::Parse(
+          absl::string_view(kDatagramPayload, sizeof(kDatagramPayload) - 1));
+  ASSERT_TRUE(parsed);
+
+  EXPECT_EQ(parsed->GetContextId(),
+            ConnectIpDatagramIpPacketPayload::kContextId);
+  EXPECT_EQ(parsed->GetType(), ConnectIpDatagramPayload::Type::kIpPacket);
+  EXPECT_EQ(parsed->GetIpProxyingPayload(), "packet");
+}
+
+TEST(ConnectIpDatagramPayloadTest, SerializeIpPacket) {
+  static constexpr absl::string_view kIpPacket = "packet";
+
+  ConnectIpDatagramIpPacketPayload payload(kIpPacket);
+  EXPECT_EQ(payload.GetIpProxyingPayload(), kIpPacket);
+
+  EXPECT_EQ(payload.Serialize(), std::string("\x00packet", 7));
+}
+
+TEST(ConnectIpDatagramPayloadTest, ParseUnknownPacket) {
+  static constexpr char kDatagramPayload[] = "\x05packet";
+
+  std::unique_ptr<ConnectIpDatagramPayload> parsed =
+      ConnectIpDatagramPayload::Parse(
+          absl::string_view(kDatagramPayload, sizeof(kDatagramPayload) - 1));
+  ASSERT_TRUE(parsed);
+
+  EXPECT_EQ(parsed->GetContextId(), 5);
+  EXPECT_EQ(parsed->GetType(), ConnectIpDatagramPayload::Type::kUnknown);
+  EXPECT_EQ(parsed->GetIpProxyingPayload(), "packet");
+}
+
+TEST(ConnectIpDatagramPayloadTest, SerializeUnknownPacket) {
+  static constexpr absl::string_view kInnerIpProxyingPayload = "packet";
+
+  ConnectIpDatagramUnknownPayload payload(4u, kInnerIpProxyingPayload);
+  EXPECT_EQ(payload.GetIpProxyingPayload(), kInnerIpProxyingPayload);
+
+  EXPECT_EQ(payload.Serialize(), std::string("\x04packet", 7));
+}
+
+}  // namespace
+}  // namespace quiche::test