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