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