First capsule interop attempt
diff --git a/quic/core/http/http_constants.cc b/quic/core/http/http_constants.cc index f3ed523..2c0ec24 100644 --- a/quic/core/http/http_constants.cc +++ b/quic/core/http/http_constants.cc
@@ -17,7 +17,8 @@ RETURN_STRING_LITERAL(SETTINGS_QPACK_MAX_TABLE_CAPACITY); RETURN_STRING_LITERAL(SETTINGS_MAX_FIELD_SECTION_SIZE); RETURN_STRING_LITERAL(SETTINGS_QPACK_BLOCKED_STREAMS); - RETURN_STRING_LITERAL(SETTINGS_H3_DATAGRAM); + RETURN_STRING_LITERAL(SETTINGS_H3_DATAGRAM_DRAFT00); + RETURN_STRING_LITERAL(SETTINGS_H3_DATAGRAM_DRAFT03); RETURN_STRING_LITERAL(SETTINGS_WEBTRANS_DRAFT00); } return absl::StrCat("UNSUPPORTED_SETTINGS_TYPE(", identifier, ")");
diff --git a/quic/core/http/http_constants.h b/quic/core/http/http_constants.h index c13a1a8..94328ac 100644 --- a/quic/core/http/http_constants.h +++ b/quic/core/http/http_constants.h
@@ -38,8 +38,10 @@ // Same value as spdy::SETTINGS_MAX_HEADER_LIST_SIZE. SETTINGS_MAX_FIELD_SECTION_SIZE = 0x06, SETTINGS_QPACK_BLOCKED_STREAMS = 0x07, - // draft-ietf-masque-h3-datagram. - SETTINGS_H3_DATAGRAM = 0x276, + // draft-ietf-masque-h3-datagram-00. + SETTINGS_H3_DATAGRAM_DRAFT00 = 0x276, + // draft-ietf-masque-h3-datagram-03. + SETTINGS_H3_DATAGRAM_DRAFT03 = 0xffd276, // draft-ietf-webtrans-http3-00 SETTINGS_WEBTRANS_DRAFT00 = 0x2b603742, };
diff --git a/quic/core/http/http_decoder.cc b/quic/core/http/http_decoder.cc index b20789b..c22fc32 100644 --- a/quic/core/http/http_decoder.cc +++ b/quic/core/http/http_decoder.cc
@@ -266,6 +266,8 @@ case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH): continue_processing = visitor_->OnAcceptChFrameStart(header_length); break; + case static_cast<uint64_t>(HttpFrameType::CAPSULE): + break; default: continue_processing = visitor_->OnUnknownFrameStart( current_frame_type_, header_length, current_frame_length_); @@ -335,6 +337,10 @@ continue_processing = BufferOrParsePayload(reader); break; } + case static_cast<uint64_t>(HttpFrameType::CAPSULE): { + continue_processing = BufferOrParsePayload(reader); + break; + } default: { continue_processing = HandleUnknownFramePayload(reader); break; @@ -401,6 +407,12 @@ continue_processing = BufferOrParsePayload(reader); break; } + case static_cast<uint64_t>(HttpFrameType::CAPSULE): { + // If frame payload is not empty, FinishParsing() is skipped. + QUICHE_DCHECK_EQ(0u, current_frame_length_); + continue_processing = BufferOrParsePayload(reader); + break; + } default: continue_processing = visitor_->OnUnknownFrameEnd(); } @@ -537,6 +549,13 @@ } return visitor_->OnAcceptChFrame(frame); } + case static_cast<uint64_t>(HttpFrameType::CAPSULE): { + CapsuleFrame frame; + if (!ParseCapsuleFrame(reader, &frame)) { + return false; + } + return visitor_->OnCapsuleFrame(frame); + } default: // Only above frame types are parsed by ParseEntirePayload(). QUICHE_NOTREACHED(); @@ -662,6 +681,59 @@ return true; } +bool HttpDecoder::ParseCapsuleFrame(QuicDataReader* reader, + CapsuleFrame* frame) { + uint64_t capsule_type64; + if (!reader->ReadVarInt62(&capsule_type64)) { + RaiseError(QUIC_HTTP_FRAME_ERROR, "Unable to parse capsule type"); + return false; + } + *frame = CapsuleFrame(static_cast<CapsuleType>(capsule_type64)); + switch (frame->capsule_type) { + case CapsuleType::REGISTER_DATAGRAM_CONTEXT: + if (!reader->ReadVarInt62( + &frame->register_datagram_context_capsule.context_id)) { + RaiseError( + QUIC_HTTP_FRAME_ERROR, + "Unable to parse capsule REGISTER_DATAGRAM_CONTEXT context ID"); + return false; + } + frame->register_datagram_context_capsule.context_extensions = + reader->ReadRemainingPayload(); + break; + case CapsuleType::CLOSE_DATAGRAM_CONTEXT: + if (!reader->ReadVarInt62( + &frame->close_datagram_context_capsule.context_id)) { + RaiseError(QUIC_HTTP_FRAME_ERROR, + "Unable to parse capsule CLOSE_DATAGRAM_CONTEXT context ID"); + return false; + } + frame->close_datagram_context_capsule.context_extensions = + reader->ReadRemainingPayload(); + break; + case CapsuleType::DATAGRAM: + if (datagram_context_id_present_) { + uint64_t context_id; + if (!reader->ReadVarInt62(&context_id)) { + RaiseError(QUIC_HTTP_FRAME_ERROR, + "Unable to parse capsule DATAGRAM context ID"); + return false; + } + frame->datagram_capsule.context_id = context_id; + } + frame->datagram_capsule.http_datagram_payload = + reader->ReadRemainingPayload(); + break; + case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT: + frame->register_datagram_no_context_capsule.context_extensions = + reader->ReadRemainingPayload(); + break; + default: + frame->unknown_capsule_data = reader->ReadRemainingPayload(); + } + return true; +} + QuicByteCount HttpDecoder::MaxFrameLength(uint64_t frame_type) { switch (frame_type) { case static_cast<uint64_t>(HttpFrameType::SETTINGS): @@ -682,6 +754,9 @@ case static_cast<uint64_t>(HttpFrameType::ACCEPT_CH): // This limit is arbitrary. return 1024 * 1024; + case static_cast<uint64_t>(HttpFrameType::CAPSULE): + // This limit is arbitrary. + return 1024 * 1024; default: // Other frames require no data buffering, so it's safe to have no limit. return std::numeric_limits<QuicByteCount>::max();
diff --git a/quic/core/http/http_decoder.h b/quic/core/http/http_decoder.h index 7cbf205..8d96c84 100644 --- a/quic/core/http/http_decoder.h +++ b/quic/core/http/http_decoder.h
@@ -94,6 +94,9 @@ // Called when an ACCEPT_CH frame has been successfully parsed. virtual bool OnAcceptChFrame(const AcceptChFrame& frame) = 0; + // Called when a CAPSULE frame has been successfully parsed. + virtual bool OnCapsuleFrame(const CapsuleFrame& frame) = 0; + // Called when a WEBTRANSPORT_STREAM frame type and the session ID varint // immediately following it has been received. Any further parsing should // be done by the stream itself, and not the parser. Note that this does not @@ -148,6 +151,10 @@ // Returns true if input data processed so far ends on a frame boundary. bool AtFrameBoundary() const { return state_ == STATE_READING_FRAME_TYPE; } + void set_datagram_context_id_present(bool datagram_context_id_present) { + datagram_context_id_present_ = datagram_context_id_present; + } + private: friend test::HttpDecoderPeer; @@ -230,6 +237,9 @@ // Parses the payload of an ACCEPT_CH frame from |reader| into |frame|. bool ParseAcceptChFrame(QuicDataReader* reader, AcceptChFrame* frame); + // Parses the payload of an CAPSULE frame from |reader| into |frame|. + bool ParseCapsuleFrame(QuicDataReader* reader, CapsuleFrame* frame); + // Returns the max frame size of a given |frame_type|. QuicByteCount MaxFrameLength(uint64_t frame_type); @@ -237,6 +247,8 @@ Visitor* const visitor_; // Unowned. // Whether WEBTRANSPORT_STREAM should be parsed. bool allow_web_transport_stream_; + // Whether HTTP Datagram Context IDs are present. + bool datagram_context_id_present_ = false; // Current state of the parsing. HttpDecoderState state_; // Type of the frame currently being parsed.
diff --git a/quic/core/http/http_decoder_test.cc b/quic/core/http/http_decoder_test.cc index 7fa09ba..6a8491e 100644 --- a/quic/core/http/http_decoder_test.cc +++ b/quic/core/http/http_decoder_test.cc
@@ -89,6 +89,7 @@ (QuicByteCount header_length), (override)); MOCK_METHOD(bool, OnAcceptChFrame, (const AcceptChFrame& frame), (override)); + MOCK_METHOD(bool, OnCapsuleFrame, (const CapsuleFrame& frame), (override)); MOCK_METHOD(void, OnWebTransportStreamFrameType, (QuicByteCount header_length, WebTransportSessionId session_id), @@ -125,6 +126,7 @@ ON_CALL(visitor_, OnPriorityUpdateFrame(_)).WillByDefault(Return(true)); ON_CALL(visitor_, OnAcceptChFrameStart(_)).WillByDefault(Return(true)); ON_CALL(visitor_, OnAcceptChFrame(_)).WillByDefault(Return(true)); + ON_CALL(visitor_, OnCapsuleFrame(_)).WillByDefault(Return(true)); ON_CALL(visitor_, OnUnknownFrameStart(_, _, _)).WillByDefault(Return(true)); ON_CALL(visitor_, OnUnknownFramePayload(_)).WillByDefault(Return(true)); ON_CALL(visitor_, OnUnknownFrameEnd()).WillByDefault(Return(true)); @@ -1043,6 +1045,88 @@ EXPECT_EQ("", decoder_.error_detail()); } +TEST_F(HttpDecoderTest, RegisterDatagramContextCapsuleFrame) { + std::string input = absl::HexStringToBytes( + "80ffcab5" // type (CAPSULE) + "06" // length + "00" // capsule type (REGISTER_DATAGRAM_CONTEXT) + "08" // context ID + "01020304"); // context extensions + + CapsuleFrame frame(CapsuleType::REGISTER_DATAGRAM_CONTEXT); + frame.register_datagram_context_capsule.context_id = 8; + std::array<char, 4> context_extensions = {1, 2, 3, 4}; + frame.register_datagram_context_capsule.context_extensions = + absl::string_view(context_extensions.data(), context_extensions.size()); + EXPECT_CALL(visitor_, OnCapsuleFrame(frame)); + EXPECT_EQ(ProcessInput(input), input.size()); +} + +TEST_F(HttpDecoderTest, CloseDatagramContextCapsuleFrame) { + std::string input = absl::HexStringToBytes( + "80ffcab5" // type (CAPSULE) + "06" // length + "01" // capsule type (CLOSE_DATAGRAM_CONTEXT) + "08" // context ID + "01020304"); // context extensions + + CapsuleFrame frame(CapsuleType::CLOSE_DATAGRAM_CONTEXT); + frame.close_datagram_context_capsule.context_id = 8; + std::array<char, 4> context_extensions = {1, 2, 3, 4}; + frame.close_datagram_context_capsule.context_extensions = + absl::string_view(context_extensions.data(), context_extensions.size()); + EXPECT_CALL(visitor_, OnCapsuleFrame(frame)); + EXPECT_EQ(ProcessInput(input), input.size()); +} + +TEST_F(HttpDecoderTest, DatagramWithoutContextCapsuleFrame) { + std::string input = absl::HexStringToBytes( + "80ffcab5" // type (CAPSULE) + "05" // length + "02" // capsule type (DATAGRAM) + "01020304"); // context extensions + + CapsuleFrame frame(CapsuleType::DATAGRAM); + std::array<char, 4> datagram_payload = {1, 2, 3, 4}; + frame.datagram_capsule.http_datagram_payload = + absl::string_view(datagram_payload.data(), datagram_payload.size()); + EXPECT_CALL(visitor_, OnCapsuleFrame(frame)); + EXPECT_EQ(ProcessInput(input), input.size()); +} + +TEST_F(HttpDecoderTest, DatagramWithContextCapsuleFrame) { + decoder_.set_datagram_context_id_present(true); + std::string input = absl::HexStringToBytes( + "80ffcab5" // type (CAPSULE) + "06" // length + "02" // capsule type (DATAGRAM) + "08" // context ID + "01020304"); // context extensions + + CapsuleFrame frame(CapsuleType::DATAGRAM); + frame.datagram_capsule.context_id = 8; + std::array<char, 4> datagram_payload = {1, 2, 3, 4}; + frame.datagram_capsule.http_datagram_payload = + absl::string_view(datagram_payload.data(), datagram_payload.size()); + EXPECT_CALL(visitor_, OnCapsuleFrame(frame)); + EXPECT_EQ(ProcessInput(input), input.size()); +} + +TEST_F(HttpDecoderTest, RegisterDatagramNoContextCapsuleFrame) { + std::string input = absl::HexStringToBytes( + "80ffcab5" // type (CAPSULE) + "05" // length + "03" // capsule type (REGISTER_DATAGRAM_NO_CONTEXT) + "01020304"); // context extensions + + CapsuleFrame frame(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT); + std::array<char, 4> context_extensions = {1, 2, 3, 4}; + frame.register_datagram_no_context_capsule.context_extensions = + absl::string_view(context_extensions.data(), context_extensions.size()); + EXPECT_CALL(visitor_, OnCapsuleFrame(frame)); + EXPECT_EQ(ProcessInput(input), input.size()); +} + TEST_F(HttpDecoderTest, WebTransportStreamDisabled) { InSequence s;
diff --git a/quic/core/http/http_encoder.cc b/quic/core/http/http_encoder.cc index 25f6200..d9d161b 100644 --- a/quic/core/http/http_encoder.cc +++ b/quic/core/http/http_encoder.cc
@@ -276,4 +276,143 @@ return 0; } +// static +QuicByteCount HttpEncoder::SerializeCapsuleFrame( + const CapsuleFrame& capsule_frame, std::unique_ptr<char[]>* output) { + QuicByteCount capsule_type_length = QuicDataWriter::GetVarInt62Len( + static_cast<uint64_t>(capsule_frame.capsule_type)); + QuicByteCount capsule_data_length; + switch (capsule_frame.capsule_type) { + case CapsuleType::REGISTER_DATAGRAM_CONTEXT: + capsule_data_length = + QuicDataWriter::GetVarInt62Len( + capsule_frame.register_datagram_context_capsule.context_id) + + capsule_frame.register_datagram_context_capsule.context_extensions + .length(); + break; + case CapsuleType::CLOSE_DATAGRAM_CONTEXT: + capsule_data_length = + QuicDataWriter::GetVarInt62Len( + capsule_frame.close_datagram_context_capsule.context_id) + + capsule_frame.close_datagram_context_capsule.context_extensions + .length(); + break; + case CapsuleType::DATAGRAM: + capsule_data_length = + capsule_frame.datagram_capsule.http_datagram_payload.length(); + if (capsule_frame.datagram_capsule.context_id.has_value()) { + capsule_data_length += QuicDataWriter::GetVarInt62Len( + capsule_frame.datagram_capsule.context_id.value()); + } + break; + case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT: + capsule_data_length = capsule_frame.register_datagram_no_context_capsule + .context_extensions.length(); + break; + default: + capsule_data_length = capsule_frame.unknown_capsule_data.length(); + break; + } + QuicByteCount frame_length_field_value = + capsule_type_length + capsule_data_length; + QuicByteCount total_frame_length = + QuicDataWriter::GetVarInt62Len( + static_cast<uint64_t>(HttpFrameType::CAPSULE)) + + QuicDataWriter::GetVarInt62Len(frame_length_field_value) + + capsule_type_length + capsule_data_length; + *output = std::make_unique<char[]>(total_frame_length); + QuicDataWriter writer(total_frame_length, output->get()); + if (!writer.WriteVarInt62(static_cast<uint64_t>(HttpFrameType::CAPSULE))) { + QUIC_BUG(capsule frame type write fail) + << "Failed to write CAPSULE frame type"; + return 0; + } + if (!writer.WriteVarInt62(frame_length_field_value)) { + QUIC_BUG(capsule frame length write fail) + << "Failed to write CAPSULE frame length"; + return 0; + } + if (!writer.WriteVarInt62( + static_cast<uint64_t>(capsule_frame.capsule_type))) { + QUIC_BUG(capsule type write fail) << "Failed to write CAPSULE type"; + return 0; + } + switch (capsule_frame.capsule_type) { + case CapsuleType::REGISTER_DATAGRAM_CONTEXT: + if (!writer.WriteVarInt62( + capsule_frame.register_datagram_context_capsule.context_id)) { + QUIC_BUG(register context capsule context ID write fail) + << "Failed to write REGISTER_DATAGRAM_CONTEXT CAPSULE context ID"; + return 0; + } + if (!writer.WriteBytes(capsule_frame.register_datagram_context_capsule + .context_extensions.data(), + capsule_frame.register_datagram_context_capsule + .context_extensions.length())) { + QUIC_BUG(register context capsule extensions write fail) + << "Failed to write REGISTER_DATAGRAM_CONTEXT CAPSULE extensions"; + return 0; + } + break; + case CapsuleType::CLOSE_DATAGRAM_CONTEXT: + if (!writer.WriteVarInt62( + capsule_frame.close_datagram_context_capsule.context_id)) { + QUIC_BUG(close context capsule context ID write fail) + << "Failed to write CLOSE_DATAGRAM_CONTEXT CAPSULE context ID"; + return 0; + } + if (!writer.WriteBytes(capsule_frame.close_datagram_context_capsule + .context_extensions.data(), + capsule_frame.close_datagram_context_capsule + .context_extensions.length())) { + QUIC_BUG(close context capsule extensions write fail) + << "Failed to write CLOSE_DATAGRAM_CONTEXT CAPSULE extensions"; + return 0; + } + break; + case CapsuleType::DATAGRAM: + if (capsule_frame.datagram_capsule.context_id.has_value()) { + if (!writer.WriteVarInt62( + capsule_frame.datagram_capsule.context_id.value())) { + QUIC_BUG(datagram capsule context ID write fail) + << "Failed to write DATAGRAM CAPSULE context ID"; + return 0; + } + } + if (!writer.WriteBytes( + capsule_frame.datagram_capsule.http_datagram_payload.data(), + capsule_frame.datagram_capsule.http_datagram_payload.length())) { + QUIC_BUG(datagram capsule payload write fail) + << "Failed to write DATAGRAM CAPSULE payload"; + return 0; + } + break; + case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT: + if (!writer.WriteBytes(capsule_frame.register_datagram_no_context_capsule + .context_extensions.data(), + capsule_frame.register_datagram_no_context_capsule + .context_extensions.length())) { + QUIC_BUG(register no context capsule extensions write fail) + << "Failed to write REGISTER_DATAGRAM_NO_CONTEXT CAPSULE " + "extensions"; + return 0; + } + break; + default: + if (!writer.WriteBytes(capsule_frame.unknown_capsule_data.data(), + capsule_frame.unknown_capsule_data.length())) { + QUIC_BUG(capsule data write fail) << "Failed to write CAPSULE data"; + return 0; + } + break; + } + if (writer.remaining() != 0) { + QUIC_BUG(capsule write length mismatch) + << "CAPSULE serialization wrote " << writer.length() << " instead of " + << writer.capacity(); + return 0; + } + return total_frame_length; +} + } // namespace quic
diff --git a/quic/core/http/http_encoder.h b/quic/core/http/http_encoder.h index 585e739..6c8ab49 100644 --- a/quic/core/http/http_encoder.h +++ b/quic/core/http/http_encoder.h
@@ -66,6 +66,10 @@ static QuicByteCount SerializeWebTransportStreamFrameHeader( WebTransportSessionId session_id, std::unique_ptr<char[]>* output); + + // Serializes a CAPSULE frame as specified in draft-ietf-masque-h3-datagram. + static QuicByteCount SerializeCapsuleFrame(const CapsuleFrame& capsule_frame, + std::unique_ptr<char[]>* output); }; } // namespace quic
diff --git a/quic/core/http/http_encoder_test.cc b/quic/core/http/http_encoder_test.cc index c2b0f36..144c414 100644 --- a/quic/core/http/http_encoder_test.cc +++ b/quic/core/http/http_encoder_test.cc
@@ -5,6 +5,7 @@ #include "quic/core/http/http_encoder.h" #include "absl/base/macros.h" +#include "quic/core/http/http_frames.h" #include "quic/core/quic_simple_buffer_allocator.h" #include "quic/platform/api/quic_flags.h" #include "quic/platform/api/quic_test.h" @@ -138,5 +139,62 @@ "WEBTRANSPORT_STREAM", buffer.get(), length, output, sizeof(output)); } +TEST(HttpEncoderTest, SerializeRegisterDatagramContextCapsule) { + CapsuleFrame capsule_frame(CapsuleType::REGISTER_DATAGRAM_CONTEXT); + capsule_frame.register_datagram_context_capsule.context_id = 4; + uint8_t output[] = {0x80, 0xff, 0xca, 0xb5, // type (CAPSULE) + 0x02, // frame length. + 0x00, // capsule type (REGISTER_DATAGRAM_CONTEXT). + 0x04}; // context ID. + std::unique_ptr<char[]> buffer; + uint64_t length = HttpEncoder::SerializeCapsuleFrame(capsule_frame, &buffer); + quiche::test::CompareCharArraysWithHexError( + "REGISTER_DATAGRAM_CONTEXT", buffer.get(), length, + reinterpret_cast<char*>(output), sizeof(output)); +} + +TEST(HttpEncoderTest, SerializeCloseDatagramContextCapsule) { + CapsuleFrame capsule_frame(CapsuleType::CLOSE_DATAGRAM_CONTEXT); + capsule_frame.close_datagram_context_capsule.context_id = 4; + uint8_t output[] = {0x80, 0xff, 0xca, 0xb5, // type (CAPSULE) + 0x02, // frame length. + 0x01, // capsule type (CLOSE_DATAGRAM_CONTEXT). + 0x04}; // context ID. + std::unique_ptr<char[]> buffer; + uint64_t length = HttpEncoder::SerializeCapsuleFrame(capsule_frame, &buffer); + quiche::test::CompareCharArraysWithHexError( + "CLOSE_DATAGRAM_CONTEXT", buffer.get(), length, + reinterpret_cast<char*>(output), sizeof(output)); +} + +TEST(HttpEncoderTest, SerializeDatagramCapsule) { + uint8_t http_datagram_payload[5] = {0x21, 0x22, 0x23, 0x24, 0x25}; + CapsuleFrame capsule_frame(CapsuleType::DATAGRAM); + capsule_frame.datagram_capsule.http_datagram_payload = + absl::string_view(reinterpret_cast<char*>(http_datagram_payload), + sizeof(http_datagram_payload)); + uint8_t output[] = {0x80, 0xff, 0xca, 0xb5, // type (CAPSULE) + 0x06, // frame length. + 0x02, // capsule type (DATAGRAM). + 0x21, 0x22, 0x23, 0x24, 0x25}; // payload. + std::unique_ptr<char[]> buffer; + uint64_t length = HttpEncoder::SerializeCapsuleFrame(capsule_frame, &buffer); + quiche::test::CompareCharArraysWithHexError("DATAGRAM", buffer.get(), length, + reinterpret_cast<char*>(output), + sizeof(output)); +} + +TEST(HttpEncoderTest, SerializeRegisterDatagramNoContextCapsule) { + CapsuleFrame capsule_frame(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT); + uint8_t output[] = {0x80, 0xff, 0xca, 0xb5, // type (CAPSULE) + 0x01, // frame length. + 0x03}; // capsule type (REGISTER_DATAGRAM_NO_CONTEXT). + std::unique_ptr<char[]> buffer; + uint64_t length = HttpEncoder::SerializeCapsuleFrame(capsule_frame, &buffer); + quiche::test::CompareCharArraysWithHexError( + "REGISTER_DATAGRAM_NO_CONTEXT", buffer.get(), length, + reinterpret_cast<char*>(output), sizeof(output)); +} + } // namespace test } // namespace quic
diff --git a/quic/core/http/http_frames.h b/quic/core/http/http_frames.h index 6a00ad7..d678062 100644 --- a/quic/core/http/http_frames.h +++ b/quic/core/http/http_frames.h
@@ -7,6 +7,7 @@ #include <algorithm> #include <cstdint> +#include <limits> #include <map> #include <ostream> #include <sstream> @@ -14,6 +15,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" +#include "absl/types/optional.h" #include "quic/core/http/http_constants.h" #include "quic/core/quic_types.h" #include "spdy/core/spdy_protocol.h" @@ -37,6 +39,8 @@ PRIORITY_UPDATE_REQUEST_STREAM = 0xF0700, // https://www.ietf.org/archive/id/draft-ietf-webtrans-http3-00.html WEBTRANSPORT_STREAM = 0x41, + // https://datatracker.ietf.org/doc/html/draft-ietf-masque-h3-datagram-03 + CAPSULE = 0xffcab5, }; // 7.2.1. DATA @@ -182,6 +186,179 @@ } }; +enum class CapsuleType : uint64_t { + // Casing in this enum matches the IETF specification. + REGISTER_DATAGRAM_CONTEXT = 0x00, + CLOSE_DATAGRAM_CONTEXT = 0x01, + DATAGRAM = 0x02, + REGISTER_DATAGRAM_NO_CONTEXT = 0x03, +}; + +inline std::string CapsuleTypeToString(CapsuleType capsule_type) { + switch (capsule_type) { + case CapsuleType::REGISTER_DATAGRAM_CONTEXT: + return "REGISTER_DATAGRAM_CONTEXT"; + case CapsuleType::CLOSE_DATAGRAM_CONTEXT: + return "CLOSE_DATAGRAM_CONTEXT"; + case CapsuleType::DATAGRAM: + return "DATAGRAM"; + case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT: + return "REGISTER_DATAGRAM_NO_CONTEXT"; + } + return absl::StrCat("Unknown(", static_cast<uint64_t>(capsule_type), ")"); +} + +inline std::ostream& operator<<(std::ostream& os, + const CapsuleType& capsule_type) { + os << CapsuleTypeToString(capsule_type); + return os; +} + +// CAPSULE HTTP frame from draft-ietf-masque-h3-datagram. +struct QUIC_EXPORT_PRIVATE CapsuleFrame { + CapsuleType capsule_type; + union { + struct { + QuicDatagramContextId context_id; + absl::string_view context_extensions; + } register_datagram_context_capsule; + struct { + QuicDatagramContextId context_id; + absl::string_view context_extensions; + } close_datagram_context_capsule; + struct { + absl::optional<QuicDatagramContextId> context_id; + absl::string_view http_datagram_payload; + } datagram_capsule; + struct { + absl::string_view context_extensions; + } register_datagram_no_context_capsule; + absl::string_view unknown_capsule_data; + }; + + explicit CapsuleFrame(CapsuleType capsule_type) : capsule_type(capsule_type) { + switch (capsule_type) { + case CapsuleType::REGISTER_DATAGRAM_CONTEXT: + register_datagram_context_capsule.context_id = 0; + register_datagram_context_capsule.context_extensions = + absl::string_view(); + break; + case CapsuleType::CLOSE_DATAGRAM_CONTEXT: + close_datagram_context_capsule.context_id = 0; + close_datagram_context_capsule.context_extensions = absl::string_view(); + break; + case CapsuleType::DATAGRAM: + datagram_capsule.context_id = absl::nullopt; + datagram_capsule.http_datagram_payload = absl::string_view(); + break; + case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT: + register_datagram_no_context_capsule.context_extensions = + absl::string_view(); + break; + default: + unknown_capsule_data = absl::string_view(); + break; + } + } + + CapsuleFrame() + : CapsuleFrame( + static_cast<CapsuleType>(std::numeric_limits<uint64_t>::max())) {} + + CapsuleFrame& operator=(const CapsuleFrame& other) { + capsule_type = other.capsule_type; + switch (capsule_type) { + case CapsuleType::REGISTER_DATAGRAM_CONTEXT: + register_datagram_context_capsule.context_id = + other.register_datagram_context_capsule.context_id; + register_datagram_context_capsule.context_extensions = + other.register_datagram_context_capsule.context_extensions; + break; + case CapsuleType::CLOSE_DATAGRAM_CONTEXT: + close_datagram_context_capsule.context_id = + other.close_datagram_context_capsule.context_id; + close_datagram_context_capsule.context_extensions = + other.close_datagram_context_capsule.context_extensions; + break; + case CapsuleType::DATAGRAM: + datagram_capsule.context_id = other.datagram_capsule.context_id; + datagram_capsule.http_datagram_payload = + other.datagram_capsule.http_datagram_payload; + break; + case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT: + register_datagram_no_context_capsule.context_extensions = + other.register_datagram_no_context_capsule.context_extensions; + break; + default: + unknown_capsule_data = other.unknown_capsule_data; + break; + } + return *this; + } + + CapsuleFrame(const CapsuleFrame& other) : CapsuleFrame(other.capsule_type) { + *this = other; + } + + bool operator==(const CapsuleFrame& other) const { + if (capsule_type != other.capsule_type) { + return false; + } + switch (capsule_type) { + case CapsuleType::REGISTER_DATAGRAM_CONTEXT: + return register_datagram_context_capsule.context_id == + other.register_datagram_context_capsule.context_id && + register_datagram_context_capsule.context_extensions == + other.register_datagram_context_capsule.context_extensions; + case CapsuleType::CLOSE_DATAGRAM_CONTEXT: + return close_datagram_context_capsule.context_id == + other.close_datagram_context_capsule.context_id && + close_datagram_context_capsule.context_extensions == + other.close_datagram_context_capsule.context_extensions; + case CapsuleType::DATAGRAM: + return datagram_capsule.context_id == + other.datagram_capsule.context_id && + datagram_capsule.http_datagram_payload == + other.datagram_capsule.http_datagram_payload; + case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT: + return register_datagram_no_context_capsule.context_extensions == + other.register_datagram_no_context_capsule.context_extensions; + default: + return unknown_capsule_data == other.unknown_capsule_data; + } + } + + std::string ToString() const { + std::string rv = CapsuleTypeToString(capsule_type); + switch (capsule_type) { + case CapsuleType::REGISTER_DATAGRAM_CONTEXT: + absl::StrAppend(&rv, "(", register_datagram_context_capsule.context_id, + ")"); + break; + case CapsuleType::CLOSE_DATAGRAM_CONTEXT: + absl::StrAppend(&rv, "(", close_datagram_context_capsule.context_id, + ")"); + break; + case CapsuleType::DATAGRAM: + if (datagram_capsule.context_id.has_value()) { + absl::StrAppend(&rv, "(", datagram_capsule.context_id.value(), ")"); + } + break; + case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT: + break; + default: + break; + } + return rv; + } + + friend QUIC_EXPORT_PRIVATE std::ostream& operator<<( + std::ostream& os, const CapsuleFrame& frame) { + os << frame.ToString(); + return os; + } +}; + } // namespace quic #endif // QUICHE_QUIC_CORE_HTTP_HTTP_FRAMES_H_
diff --git a/quic/core/http/quic_receive_control_stream.cc b/quic/core/http/quic_receive_control_stream.cc index 9c164d0..b36bc7f 100644 --- a/quic/core/http/quic_receive_control_stream.cc +++ b/quic/core/http/quic_receive_control_stream.cc
@@ -200,6 +200,10 @@ return true; } +bool QuicReceiveControlStream::OnCapsuleFrame(const CapsuleFrame& /*frame*/) { + return ValidateFrameType(HttpFrameType::CAPSULE); +} + void QuicReceiveControlStream::OnWebTransportStreamFrameType( QuicByteCount /*header_length*/, WebTransportSessionId /*session_id*/) { @@ -237,7 +241,8 @@ (spdy_session()->perspective() == Perspective::IS_CLIENT && frame_type == HttpFrameType::MAX_PUSH_ID) || (spdy_session()->perspective() == Perspective::IS_SERVER && - frame_type == HttpFrameType::ACCEPT_CH)) { + frame_type == HttpFrameType::ACCEPT_CH) || + frame_type == HttpFrameType::CAPSULE) { stream_delegate()->OnStreamError( QUIC_HTTP_FRAME_UNEXPECTED_ON_CONTROL_STREAM, absl::StrCat("Invalid frame type ", static_cast<int>(frame_type),
diff --git a/quic/core/http/quic_receive_control_stream.h b/quic/core/http/quic_receive_control_stream.h index 3c61d3c..3390df0 100644 --- a/quic/core/http/quic_receive_control_stream.h +++ b/quic/core/http/quic_receive_control_stream.h
@@ -51,6 +51,7 @@ bool OnPriorityUpdateFrame(const PriorityUpdateFrame& frame) override; bool OnAcceptChFrameStart(QuicByteCount header_length) override; bool OnAcceptChFrame(const AcceptChFrame& frame) override; + bool OnCapsuleFrame(const CapsuleFrame& frame) override; void OnWebTransportStreamFrameType(QuicByteCount header_length, WebTransportSessionId session_id) override; bool OnUnknownFrameStart(uint64_t frame_type,
diff --git a/quic/core/http/quic_send_control_stream_test.cc b/quic/core/http/quic_send_control_stream_test.cc index 3066cb3..7823b0f 100644 --- a/quic/core/http/quic_send_control_stream_test.cc +++ b/quic/core/http/quic_send_control_stream_test.cc
@@ -135,22 +135,24 @@ "61"); // payload "a" if (QuicSpdySessionPeer::ShouldNegotiateHttp3Datagram(&session_)) { expected_write_data = absl::HexStringToBytes( - "00" // stream type: control stream - "04" // frame type: SETTINGS frame - "0e" // frame length - "01" // SETTINGS_QPACK_MAX_TABLE_CAPACITY - "40ff" // 255 - "06" // SETTINGS_MAX_HEADER_LIST_SIZE - "4400" // 1024 - "07" // SETTINGS_QPACK_BLOCKED_STREAMS - "10" // 16 - "4040" // 0x40 as the reserved settings id - "14" // 20 - "4276" // SETTINGS_H3_DATAGRAM - "01" // 1 - "4040" // 0x40 as the reserved frame type - "01" // 1 byte frame length - "61"); // payload "a" + "00" // stream type: control stream + "04" // frame type: SETTINGS frame + "0e" // frame length + "01" // SETTINGS_QPACK_MAX_TABLE_CAPACITY + "40ff" // 255 + "06" // SETTINGS_MAX_HEADER_LIST_SIZE + "4400" // 1024 + "07" // SETTINGS_QPACK_BLOCKED_STREAMS + "10" // 16 + "4040" // 0x40 as the reserved settings id + "14" // 20 + "4276" // SETTINGS_H3_DATAGRAM_DRAFT00 + "01" // 1 + "800ffd276" // SETTINGS_H3_DATAGRAM_DRAFT03 + "01" // 1 + "4040" // 0x40 as the reserved frame type + "01" // 1 byte frame length + "61"); // payload "a" } auto buffer = std::make_unique<char[]>(expected_write_data.size());
diff --git a/quic/core/http/quic_spdy_session.cc b/quic/core/http/quic_spdy_session.cc index dc773ae..f01da54 100644 --- a/quic/core/http/quic_spdy_session.cc +++ b/quic/core/http/quic_spdy_session.cc
@@ -135,6 +135,10 @@ session_->OnAcceptChFrameReceivedViaAlps(frame); return true; } + bool OnCapsuleFrame(const CapsuleFrame& /*frame*/) override { + error_detail_ = "CAPSULE frame forbidden"; + return false; + } void OnWebTransportStreamFrameType( QuicByteCount /*header_length*/, WebTransportSessionId /*session_id*/) override { @@ -521,7 +525,8 @@ settings_.values[SETTINGS_MAX_FIELD_SECTION_SIZE] = max_inbound_header_list_size_; if (ShouldNegotiateHttp3Datagram() && version().UsesHttp3()) { - settings_.values[SETTINGS_H3_DATAGRAM] = 1; + settings_.values[SETTINGS_H3_DATAGRAM_DRAFT00] = 1; + settings_.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1; } if (WillNegotiateWebTransport()) { settings_.values[SETTINGS_WEBTRANS_DRAFT00] = 1; @@ -1126,24 +1131,59 @@ absl::StrCat("received HTTP/2 specific setting in HTTP/3 session: ", id)); return false; - case SETTINGS_H3_DATAGRAM: { + case SETTINGS_H3_DATAGRAM_DRAFT00: { if (!ShouldNegotiateHttp3Datagram()) { break; } - QUIC_DVLOG(1) << ENDPOINT << "SETTINGS_H3_DATAGRAM received with value " + QUIC_DVLOG(1) << ENDPOINT + << "SETTINGS_H3_DATAGRAM_DRAFT00 received with value " << value; if (!version().UsesHttp3()) { break; } if (value != 0 && value != 1) { std::string error_details = absl::StrCat( - "received SETTINGS_H3_DATAGRAM with invalid value ", value); - QUIC_PEER_BUG(quic_peer_bug_10360_7) << ENDPOINT << error_details; + "received SETTINGS_H3_DATAGRAM_DRAFT00 with invalid value ", + value); + QUIC_PEER_BUG(bad SETTINGS_H3_DATAGRAM_DRAFT00) + << ENDPOINT << error_details; CloseConnectionWithDetails(QUIC_HTTP_RECEIVE_SPDY_SETTING, error_details); return false; } - h3_datagram_supported_ = !!value; + if (value && http_datagram_support_ != HttpDatagramSupport::kDraft03) { + // If both draft-00 and draft-03 are supported, use draft-03. + http_datagram_support_ = HttpDatagramSupport::kDraft00; +#if 0 + // DO_NOT_SUBMIT hack around Ericsson bug (they're sending 00 instead of 03): + http_datagram_support_ = HttpDatagramSupport::kDraft03; +#endif // 0 + } + break; + } + case SETTINGS_H3_DATAGRAM_DRAFT03: { + if (!ShouldNegotiateHttp3Datagram()) { + break; + } + QUIC_DVLOG(1) << ENDPOINT + << "SETTINGS_H3_DATAGRAM_DRAFT03 received with value " + << value; + if (!version().UsesHttp3()) { + break; + } + if (value != 0 && value != 1) { + std::string error_details = absl::StrCat( + "received SETTINGS_H3_DATAGRAM_DRAFT03 with invalid value ", + value); + QUIC_PEER_BUG(bad SETTINGS_H3_DATAGRAM_DRAFT03) + << ENDPOINT << error_details; + CloseConnectionWithDetails(QUIC_HTTP_RECEIVE_SPDY_SETTING, + error_details); + return false; + } + if (value) { + http_datagram_support_ = HttpDatagramSupport::kDraft03; + } break; } case SETTINGS_WEBTRANS_DRAFT00: @@ -1616,15 +1656,24 @@ QuicDatagramStreamId stream_id, absl::optional<QuicDatagramContextId> context_id, absl::string_view payload) { + if (!SupportsH3Datagram()) { + QUIC_BUG(send http datagram too early) + << "Refusing to send HTTP Datagram before SETTINGS received"; + return MESSAGE_STATUS_INTERNAL_ERROR; + } + uint64_t stream_id_to_write = stream_id; + if (http_datagram_support_ != HttpDatagramSupport::kDraft00) { + stream_id_to_write /= kHttpDatagramStreamIdDivisor; + } size_t slice_length = - QuicDataWriter::GetVarInt62Len(stream_id) + payload.length(); + QuicDataWriter::GetVarInt62Len(stream_id_to_write) + payload.length(); if (context_id.has_value()) { slice_length += QuicDataWriter::GetVarInt62Len(context_id.value()); } QuicBuffer buffer(connection()->helper()->GetStreamSendBufferAllocator(), slice_length); QuicDataWriter writer(slice_length, buffer.data()); - if (!writer.WriteVarInt62(stream_id)) { + if (!writer.WriteVarInt62(stream_id_to_write)) { QUIC_BUG(h3 datagram stream ID write fail) << "Failed to write HTTP/3 datagram stream ID"; return MESSAGE_STATUS_INTERNAL_ERROR; @@ -1665,8 +1714,8 @@ void QuicSpdySession::OnMessageReceived(absl::string_view message) { QuicSession::OnMessageReceived(message); - if (!h3_datagram_supported_) { - QUIC_DLOG(ERROR) << "Ignoring unexpected received HTTP/3 datagram"; + if (!SupportsH3Datagram()) { + QUIC_DLOG(INFO) << "Ignoring unexpected received HTTP/3 datagram"; return; } QuicDataReader reader(message); @@ -1675,7 +1724,11 @@ QUIC_DLOG(ERROR) << "Failed to parse stream ID in received HTTP/3 datagram"; return; } - if (perspective() == Perspective::IS_SERVER) { + if (http_datagram_support_ != HttpDatagramSupport::kDraft00) { + stream_id64 *= kHttpDatagramStreamIdDivisor; + } + if (perspective() == Perspective::IS_SERVER && + http_datagram_support_ == HttpDatagramSupport::kDraft00) { auto it = h3_datagram_flow_id_to_stream_id_map_.find(stream_id64); if (it == h3_datagram_flow_id_to_stream_id_map_.end()) { QUIC_DLOG(INFO) << "Received unknown HTTP/3 datagram flow ID " @@ -1705,10 +1758,14 @@ } bool QuicSpdySession::SupportsWebTransport() { - return WillNegotiateWebTransport() && h3_datagram_supported_ && + return WillNegotiateWebTransport() && SupportsH3Datagram() && peer_supports_webtransport_; } +bool QuicSpdySession::SupportsH3Datagram() const { + return http_datagram_support_ != HttpDatagramSupport::kNone; +} + WebTransportHttp3* QuicSpdySession::GetWebTransportSession( WebTransportSessionId id) { if (!SupportsWebTransport()) { @@ -1840,6 +1897,25 @@ return false; } +std::string HttpDatagramSupportToString( + HttpDatagramSupport http_datagram_support) { + switch (http_datagram_support) { + case HttpDatagramSupport::kNone: + return "None"; + case HttpDatagramSupport::kDraft00: + return "Draft00"; + case HttpDatagramSupport::kDraft03: + return "Draft03"; + } + return absl::StrCat("Unknown(", static_cast<int>(http_datagram_support), ")"); +} + +std::ostream& operator<<(std::ostream& os, + const HttpDatagramSupport& http_datagram_support) { + os << HttpDatagramSupportToString(http_datagram_support); + return os; +} + #undef ENDPOINT // undef for jumbo builds } // namespace quic
diff --git a/quic/core/http/quic_spdy_session.h b/quic/core/http/quic_spdy_session.h index 8085dd8..c471903 100644 --- a/quic/core/http/quic_spdy_session.h +++ b/quic/core/http/quic_spdy_session.h
@@ -6,6 +6,7 @@ #define QUICHE_QUIC_CORE_HTTP_QUIC_SPDY_SESSION_H_ #include <cstddef> +#include <cstdint> #include <list> #include <memory> #include <string> @@ -119,6 +120,19 @@ virtual void OnSettingsFrameResumed(const SettingsFrame& /*frame*/) {} }; +// Whether draft-ietf-masque-h3-datagram is supported on this session and if so +// which draft is currently in use. +enum class HttpDatagramSupport : uint8_t { + kNone = 0, // HTTP Datagrams are not supported for this session. + kDraft00 = 1, + kDraft03 = 2, +}; + +QUIC_EXPORT_PRIVATE std::string HttpDatagramSupportToString( + HttpDatagramSupport http_datagram_support); +QUIC_EXPORT_PRIVATE std::ostream& operator<<( + std::ostream& os, const HttpDatagramSupport& http_datagram_support); + // A QUIC session for HTTP. class QUIC_EXPORT_PRIVATE QuicSpdySession : public QuicSession, @@ -374,9 +388,11 @@ // extension. virtual void OnAcceptChFrameReceivedViaAlps(const AcceptChFrame& /*frame*/); - // Whether HTTP/3 datagrams are supported on this session, based on received - // SETTINGS. - bool h3_datagram_supported() const { return h3_datagram_supported_; } + // Whether HTTP datagrams are supported on this session and which draft is in + // use, based on received SETTINGS. + HttpDatagramSupport http_datagram_support() const { + return http_datagram_support_; + } // This must not be used except by QuicSpdyStream::SendHttp3Datagram. MessageStatus SendHttp3Datagram( @@ -400,7 +416,7 @@ bool SupportsWebTransport(); // Indicates whether both the peer and us support HTTP/3 Datagrams. - bool SupportsH3Datagram() { return h3_datagram_supported_; } + bool SupportsH3Datagram() const; // Indicates whether the HTTP/3 session will indicate WebTransport support to // the peer. @@ -654,8 +670,9 @@ // frame has been sent yet. absl::optional<uint64_t> last_sent_http3_goaway_id_; - // Whether both this endpoint and our peer support HTTP/3 datagrams. - bool h3_datagram_supported_ = false; + // Whether both this endpoint and our peer support HTTP datagrams and which + // draft is in use for this session. + HttpDatagramSupport http_datagram_support_ = HttpDatagramSupport::kNone; // Whether the peer has indicated WebTransport support. bool peer_supports_webtransport_ = false;
diff --git a/quic/core/http/quic_spdy_session_test.cc b/quic/core/http/quic_spdy_session_test.cc index c42b8ff..371a8c3 100644 --- a/quic/core/http/quic_spdy_session_test.cc +++ b/quic/core/http/quic_spdy_session_test.cc
@@ -549,7 +549,7 @@ void ReceiveWebTransportSettings() { SettingsFrame settings; - settings.values[SETTINGS_H3_DATAGRAM] = 1; + settings.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1; settings.values[SETTINGS_WEBTRANS_DRAFT00] = 1; std::string data = std::string(1, kControlStream) + EncodeSettings(settings); @@ -573,8 +573,14 @@ headers.OnHeaderBlockStart(); headers.OnHeader(":method", "CONNECT"); headers.OnHeader(":protocol", "webtransport"); - headers.OnHeader("datagram-flow-id", absl::StrCat(session_id)); + if (session_.http_datagram_support() == HttpDatagramSupport::kDraft00) { + headers.OnHeader("datagram-flow-id", absl::StrCat(session_id)); + } stream->OnStreamHeaderList(/*fin=*/true, 0, headers); + if (session_.http_datagram_support() == HttpDatagramSupport::kDraft03) { + stream->OnCapsuleFrame( + CapsuleFrame(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT)); + } WebTransportHttp3* web_transport = session_.GetWebTransportSession(session_id); ASSERT_TRUE(web_transport != nullptr); @@ -3404,16 +3410,17 @@ EXPECT_EQ("multiple SETTINGS frames", error.value()); } -TEST_P(QuicSpdySessionTestClient, H3DatagramSetting) { +TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingDraft00) { if (!version().UsesHttp3()) { return; } session_.set_should_negotiate_h3_datagram(true); // HTTP/3 datagrams aren't supported before SETTINGS are received. - EXPECT_FALSE(session_.h3_datagram_supported()); + EXPECT_FALSE(session_.SupportsH3Datagram()); + EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kNone); // Receive SETTINGS. SettingsFrame settings; - settings.values[SETTINGS_H3_DATAGRAM] = 1; + settings.values[SETTINGS_H3_DATAGRAM_DRAFT00] = 1; std::string data = std::string(1, kControlStream) + EncodeSettings(settings); QuicStreamId stream_id = GetNthServerInitiatedUnidirectionalStreamId(transport_version(), 3); @@ -3424,7 +3431,59 @@ EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(settings)); session_.OnStreamFrame(frame); // HTTP/3 datagrams are now supported. - EXPECT_TRUE(session_.h3_datagram_supported()); + EXPECT_TRUE(session_.SupportsH3Datagram()); + EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kDraft00); +} + +TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingDraft03) { + if (!version().UsesHttp3()) { + return; + } + session_.set_should_negotiate_h3_datagram(true); + // HTTP/3 datagrams aren't supported before SETTINGS are received. + EXPECT_FALSE(session_.SupportsH3Datagram()); + EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kNone); + // Receive SETTINGS. + SettingsFrame settings; + settings.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1; + std::string data = std::string(1, kControlStream) + EncodeSettings(settings); + QuicStreamId stream_id = + GetNthServerInitiatedUnidirectionalStreamId(transport_version(), 3); + QuicStreamFrame frame(stream_id, /*fin=*/false, /*offset=*/0, data); + StrictMock<MockHttp3DebugVisitor> debug_visitor; + session_.set_debug_visitor(&debug_visitor); + EXPECT_CALL(debug_visitor, OnPeerControlStreamCreated(stream_id)); + EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(settings)); + session_.OnStreamFrame(frame); + // HTTP/3 datagrams are now supported. + EXPECT_TRUE(session_.SupportsH3Datagram()); + EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kDraft03); +} + +TEST_P(QuicSpdySessionTestClient, HttpDatagramSettingDraft00And03) { + if (!version().UsesHttp3()) { + return; + } + session_.set_should_negotiate_h3_datagram(true); + // HTTP/3 datagrams aren't supported before SETTINGS are received. + EXPECT_FALSE(session_.SupportsH3Datagram()); + EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kNone); + // Receive SETTINGS. + SettingsFrame settings; + settings.values[SETTINGS_H3_DATAGRAM_DRAFT00] = 1; + settings.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1; + std::string data = std::string(1, kControlStream) + EncodeSettings(settings); + QuicStreamId stream_id = + GetNthServerInitiatedUnidirectionalStreamId(transport_version(), 3); + QuicStreamFrame frame(stream_id, /*fin=*/false, /*offset=*/0, data); + StrictMock<MockHttp3DebugVisitor> debug_visitor; + session_.set_debug_visitor(&debug_visitor); + EXPECT_CALL(debug_visitor, OnPeerControlStreamCreated(stream_id)); + EXPECT_CALL(debug_visitor, OnSettingsFrameReceived(settings)); + session_.OnStreamFrame(frame); + // HTTP/3 datagrams are now supported. + EXPECT_TRUE(session_.SupportsH3Datagram()); + EXPECT_EQ(session_.http_datagram_support(), HttpDatagramSupport::kDraft03); } TEST_P(QuicSpdySessionTestClient, WebTransportSetting) { @@ -3444,7 +3503,7 @@ CompleteHandshake(); SettingsFrame server_settings; - server_settings.values[SETTINGS_H3_DATAGRAM] = 1; + server_settings.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1; server_settings.values[SETTINGS_WEBTRANS_DRAFT00] = 1; std::string data = std::string(1, kControlStream) + EncodeSettings(server_settings); @@ -3474,7 +3533,7 @@ CompleteHandshake(); SettingsFrame server_settings; - server_settings.values[SETTINGS_H3_DATAGRAM] = 1; + server_settings.values[SETTINGS_H3_DATAGRAM_DRAFT03] = 1; server_settings.values[SETTINGS_WEBTRANS_DRAFT00] = 0; std::string data = std::string(1, kControlStream) + EncodeSettings(server_settings);
diff --git a/quic/core/http/quic_spdy_stream.cc b/quic/core/http/quic_spdy_stream.cc index bc23474..810994e 100644 --- a/quic/core/http/quic_spdy_stream.cc +++ b/quic/core/http/quic_spdy_stream.cc
@@ -15,6 +15,7 @@ #include "absl/strings/string_view.h" #include "quic/core/http/http_constants.h" #include "quic/core/http/http_decoder.h" +#include "quic/core/http/http_frames.h" #include "quic/core/http/quic_spdy_session.h" #include "quic/core/http/spdy_utils.h" #include "quic/core/http/web_transport_http3.h" @@ -128,6 +129,10 @@ return false; } + bool OnCapsuleFrame(const CapsuleFrame& frame) override { + return stream_->OnCapsuleFrame(frame); + } + void OnWebTransportStreamFrameType( QuicByteCount header_length, WebTransportSessionId session_id) override { @@ -287,6 +292,13 @@ SetFinSent(); CloseWriteSide(); } + + if (session()->perspective() == Perspective::IS_CLIENT && web_transport_) { + RegisterHttp3DatagramContextId(web_transport_->context_id(), + Http3DatagramContextExtensions(), + web_transport_.get()); + } + return bytes_written; } @@ -1206,6 +1218,13 @@ protocol = header_value; } if (header_name == "datagram-flow-id") { + if (spdy_session_->http_datagram_support() != + HttpDatagramSupport::kDraft00) { + QUIC_DLOG(ERROR) << ENDPOINT + << "Rejecting WebTransport due to unexpected " + "Datagram-Flow-Id header"; + return; + } if (flow_id.has_value() || header_value.empty()) { return; } @@ -1217,16 +1236,26 @@ } } - if (method != "CONNECT" || protocol != "webtransport" || - !flow_id.has_value()) { + if (method != "CONNECT" || protocol != "webtransport") { return; } - RegisterHttp3DatagramFlowId(*flow_id); + if (spdy_session_->http_datagram_support() == HttpDatagramSupport::kDraft00) { + if (!flow_id.has_value()) { + QUIC_DLOG(ERROR) + << ENDPOINT + << "Rejecting WebTransport due to missing Datagram-Flow-Id header"; + return; + } + RegisterHttp3DatagramFlowId(*flow_id); + } web_transport_ = std::make_unique<WebTransportHttp3>(spdy_session_, this, id()); + if (spdy_session_->http_datagram_support() != HttpDatagramSupport::kDraft00) { + return; + } // If we're in draft-ietf-masque-h3-datagram-00 mode, pretend we also received // a REGISTER_DATAGRAM_NO_CONTEXT capsule with no extensions. // TODO(b/181256914) remove this when we remove support for @@ -1255,14 +1284,12 @@ return; } - QuicDatagramStreamId stream_id = id(); - headers["datagram-flow-id"] = absl::StrCat(stream_id); + if (spdy_session_->http_datagram_support() == HttpDatagramSupport::kDraft00) { + headers["datagram-flow-id"] = absl::StrCat(id()); + } web_transport_ = std::make_unique<WebTransportHttp3>(spdy_session_, this, id()); - RegisterHttp3DatagramContextId(web_transport_->context_id(), - Http3DatagramContextExtensions(), - web_transport_.get()); } void QuicSpdyStream::OnCanWriteNewData() { @@ -1323,6 +1350,79 @@ : session_id(session_id), adapter(stream->spdy_session_, stream, stream->sequencer()) {} +bool QuicSpdyStream::OnCapsuleFrame(const CapsuleFrame& frame) { + QUIC_DLOG(INFO) << ENDPOINT << "Stream " << id() << " received capsule " + << frame; + if (!headers_decompressed_) { + QUIC_PEER_BUG(capsule before headers) + << ENDPOINT << "Stream " << id() << " received capsule " << frame + << " before headers"; + return false; + } + switch (frame.capsule_type) { + case CapsuleType::REGISTER_DATAGRAM_CONTEXT: + if (datagram_registration_visitor_ == nullptr) { + QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << frame + << " without any registration visitor"; + return false; + } + datagram_registration_visitor_->OnContextReceived( + id(), frame.register_datagram_context_capsule.context_id, + Http3DatagramContextExtensions()); + break; + case CapsuleType::CLOSE_DATAGRAM_CONTEXT: + if (datagram_registration_visitor_ == nullptr) { + QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << frame + << " without any registration visitor"; + return false; + } + datagram_registration_visitor_->OnContextClosed( + id(), frame.close_datagram_context_capsule.context_id, + Http3DatagramContextExtensions()); + break; + case CapsuleType::DATAGRAM: { + Http3DatagramVisitor* visitor = nullptr; + if (frame.datagram_capsule.context_id.has_value()) { + auto it = datagram_context_visitors_.find( + frame.datagram_capsule.context_id.value()); + if (it != datagram_context_visitors_.end()) { + visitor = it->second; + } + } else { + visitor = datagram_no_context_visitor_; + } + if (visitor == nullptr) { + QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << frame + << " without any registration visitor"; + return true; + } + visitor->OnHttp3Datagram(id(), frame.datagram_capsule.context_id, + frame.datagram_capsule.http_datagram_payload); + } break; + case CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT: + if (datagram_registration_visitor_ == nullptr) { + QUIC_DLOG(ERROR) << ENDPOINT << "Received capsule " << frame + << " without any registration visitor"; + return false; + } + datagram_registration_visitor_->OnContextReceived( + id(), /*context_id=*/absl::nullopt, Http3DatagramContextExtensions()); + break; + } + return true; +} + +void QuicSpdyStream::WriteCapsuleFrame(const CapsuleFrame& frame) { + QUIC_DLOG(INFO) << ENDPOINT << "Stream " << id() << " sending capsule " + << frame; + std::unique_ptr<char[]> buffer; + QuicByteCount frame_length = + HttpEncoder::SerializeCapsuleFrame(frame, &buffer); + QUICHE_DCHECK_GT(frame_length, 0u); + WriteOrBufferData(absl::string_view(buffer.get(), frame_length), + /*fin=*/false, /*ack_listener=*/nullptr); +} + MessageStatus QuicSpdyStream::SendHttp3Datagram( absl::optional<QuicDatagramContextId> context_id, absl::string_view payload) { @@ -1368,19 +1468,22 @@ if (visitor == nullptr) { QUIC_BUG(null datagram visitor) << ENDPOINT << "Null datagram visitor for stream ID " << id() - << " context ID " << (context_id.has_value() ? context_id.value() : 0); + << " context ID " + << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none"); return; } if (datagram_registration_visitor_ == nullptr) { QUIC_BUG(context registration without registration visitor) << ENDPOINT << "Cannot register context ID " - << (context_id.has_value() ? context_id.value() : 0) + << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none") << " without registration visitor for stream ID " << id(); return; } QUIC_DLOG(INFO) << ENDPOINT << "Registering datagram context ID " - << (context_id.has_value() ? context_id.value() : 0) + << (context_id.has_value() ? absl::StrCat(context_id.value()) + : "none") << " with stream ID " << id(); + if (context_id.has_value()) { if (datagram_no_context_visitor_ != nullptr) { QUIC_BUG(h3 datagram context ID mix1) @@ -1392,28 +1495,45 @@ } auto insertion_result = datagram_context_visitors_.insert({context_id.value(), visitor}); - QUIC_BUG_IF(h3 datagram double context registration, - !insertion_result.second) - << ENDPOINT << "Attempted to doubly register HTTP/3 stream ID " << id() - << " context ID " << context_id.value(); - return; + if (!insertion_result.second) { + QUIC_BUG(h3 datagram double context registration) + << ENDPOINT << "Attempted to doubly register HTTP/3 stream ID " + << id() << " context ID " << context_id.value(); + return; + } + } else { + // Registration without a context ID. + if (!datagram_context_visitors_.empty()) { + QUIC_BUG(h3 datagram context ID mix2) + << ENDPOINT + << "Attempted to mix registrations with and without context IDs " + "for stream ID " + << id(); + return; + } + if (datagram_no_context_visitor_ != nullptr) { + QUIC_BUG(h3 datagram double no context registration) + << ENDPOINT << "Attempted to doubly register HTTP/3 stream ID " + << id() << " with no context ID"; + return; + } + datagram_no_context_visitor_ = visitor; } - // Registration without a context ID. - if (!datagram_context_visitors_.empty()) { - QUIC_BUG(h3 datagram context ID mix2) - << ENDPOINT - << "Attempted to mix registrations with and without context IDs " - "for stream ID " - << id(); - return; + if (spdy_session_->http_datagram_support() == HttpDatagramSupport::kDraft03) { + const bool is_client = session()->perspective() == Perspective::IS_CLIENT; + if (context_id.has_value()) { + const bool is_client_context = context_id.value() % 2 == 0; + if (is_client == is_client_context) { + CapsuleFrame capsule_frame(CapsuleType::REGISTER_DATAGRAM_CONTEXT); + capsule_frame.register_datagram_context_capsule.context_id = + context_id.value(); + WriteCapsuleFrame(capsule_frame); + } + } else if (is_client) { + WriteCapsuleFrame( + CapsuleFrame(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT)); + } } - if (datagram_no_context_visitor_ != nullptr) { - QUIC_BUG(h3 datagram double no context registration) - << ENDPOINT << "Attempted to doubly register HTTP/3 stream ID " << id() - << " with no context ID"; - return; - } - datagram_no_context_visitor_ = visitor; } void QuicSpdyStream::UnregisterHttp3DatagramContextId( @@ -1421,26 +1541,34 @@ if (datagram_registration_visitor_ == nullptr) { QUIC_BUG(context unregistration without registration visitor) << ENDPOINT << "Cannot unregister context ID " - << (context_id.has_value() ? context_id.value() : 0) + << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none") << " without registration visitor for stream ID " << id(); return; } QUIC_DLOG(INFO) << ENDPOINT << "Unregistering datagram context ID " - << (context_id.has_value() ? context_id.value() : 0) + << (context_id.has_value() ? absl::StrCat(context_id.value()) + : "none") << " with stream ID " << id(); if (context_id.has_value()) { size_t num_erased = datagram_context_visitors_.erase(context_id.value()); QUIC_BUG_IF(h3 datagram unregister unknown context, num_erased != 1) << "Attempted to unregister unknown HTTP/3 context ID " << context_id.value() << " on stream ID " << id(); - return; + } else { + // Unregistration without a context ID. + QUIC_BUG_IF(h3 datagram unknown context unregistration, + datagram_no_context_visitor_ == nullptr) + << "Attempted to unregister unknown no context on HTTP/3 stream ID " + << id(); + datagram_no_context_visitor_ = nullptr; } - // Unregistration without a context ID. - QUIC_BUG_IF(h3 datagram unknown context unregistration, - datagram_no_context_visitor_ == nullptr) - << "Attempted to unregister unknown no context on HTTP/3 stream ID " - << id(); - datagram_no_context_visitor_ = nullptr; + if (spdy_session_->http_datagram_support() == HttpDatagramSupport::kDraft03 && + context_id.has_value()) { + CapsuleFrame capsule_frame(CapsuleType::CLOSE_DATAGRAM_CONTEXT); + capsule_frame.close_datagram_context_capsule.context_id = + context_id.value(); + WriteCapsuleFrame(capsule_frame); + } } void QuicSpdyStream::MoveHttp3DatagramContextIdRegistration( @@ -1449,12 +1577,13 @@ if (datagram_registration_visitor_ == nullptr) { QUIC_BUG(context move without registration visitor) << ENDPOINT << "Cannot move context ID " - << (context_id.has_value() ? context_id.value() : 0) + << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none") << " without registration visitor for stream ID " << id(); return; } QUIC_DLOG(INFO) << ENDPOINT << "Moving datagram context ID " - << (context_id.has_value() ? context_id.value() : 0) + << (context_id.has_value() ? absl::StrCat(context_id.value()) + : "none") << " with stream ID " << id(); if (context_id.has_value()) { QUIC_BUG_IF(h3 datagram move unknown context,
diff --git a/quic/core/http/quic_spdy_stream.h b/quic/core/http/quic_spdy_stream.h index 4df1196..d9a784d 100644 --- a/quic/core/http/quic_spdy_stream.h +++ b/quic/core/http/quic_spdy_stream.h
@@ -21,6 +21,7 @@ #include "absl/types/span.h" #include "quic/core/http/http_decoder.h" #include "quic/core/http/http_encoder.h" +#include "quic/core/http/http_frames.h" #include "quic/core/http/quic_header_list.h" #include "quic/core/http/quic_spdy_stream_body_manager.h" #include "quic/core/qpack/qpack_decoded_headers_accumulator.h" @@ -253,6 +254,8 @@ // rejected due to buffer being full. |write_size| must be non-zero. bool CanWriteNewBodyData(QuicByteCount write_size) const; + bool OnCapsuleFrame(const CapsuleFrame& frame); + // Sends an HTTP/3 datagram. The stream and context IDs are not part of // |payload|. MessageStatus SendHttp3Datagram( @@ -410,6 +413,8 @@ ABSL_MUST_USE_RESULT bool WriteDataFrameHeader(QuicByteCount data_length, bool force_write); + void WriteCapsuleFrame(const CapsuleFrame& frame); + QuicSpdySession* spdy_session_; bool on_body_available_called_because_sequencer_is_closed_;
diff --git a/quic/core/http/quic_spdy_stream_test.cc b/quic/core/http/quic_spdy_stream_test.cc index c19dc3c..50e4905 100644 --- a/quic/core/http/quic_spdy_stream_test.cc +++ b/quic/core/http/quic_spdy_stream_test.cc
@@ -16,6 +16,7 @@ #include "absl/strings/string_view.h" #include "quic/core/crypto/null_encrypter.h" #include "quic/core/http/http_encoder.h" +#include "quic/core/http/quic_spdy_session.h" #include "quic/core/http/spdy_utils.h" #include "quic/core/http/web_transport_http3.h" #include "quic/core/quic_connection.h" @@ -3017,7 +3018,7 @@ } } -TEST_P(QuicSpdyStreamTest, ProcessOutgoingWebTransportHeaders) { +TEST_P(QuicSpdyStreamTest, ProcessOutgoingWebTransportHeadersDatagramDraft00) { if (!UsesHttp3()) { return; } @@ -3025,7 +3026,9 @@ InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT); session_->set_should_negotiate_h3_datagram(true); session_->EnableWebTransport(); - QuicSpdySessionPeer::EnableWebTransport(*session_); + QuicSpdySessionPeer::EnableWebTransport(session_.get()); + QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(), + HttpDatagramSupport::kDraft00); EXPECT_CALL(*stream_, WriteHeadersMock(false)); EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _)) @@ -3040,7 +3043,31 @@ EXPECT_EQ(stream_->id(), stream_->web_transport()->id()); } -TEST_P(QuicSpdyStreamTest, ProcessIncomingWebTransportHeaders) { +TEST_P(QuicSpdyStreamTest, ProcessOutgoingWebTransportHeadersDatagramDraft03) { + if (!UsesHttp3()) { + return; + } + + InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT); + session_->set_should_negotiate_h3_datagram(true); + session_->EnableWebTransport(); + QuicSpdySessionPeer::EnableWebTransport(session_.get()); + QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(), + HttpDatagramSupport::kDraft03); + + EXPECT_CALL(*stream_, WriteHeadersMock(false)); + EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _)) + .Times(AnyNumber()); + + spdy::SpdyHeaderBlock headers; + headers[":method"] = "CONNECT"; + headers[":protocol"] = "webtransport"; + stream_->WriteHeaders(std::move(headers), /*fin=*/false, nullptr); + ASSERT_TRUE(stream_->web_transport() != nullptr); + EXPECT_EQ(stream_->id(), stream_->web_transport()->id()); +} + +TEST_P(QuicSpdyStreamTest, ProcessIncomingWebTransportHeadersDraft03) { if (!UsesHttp3()) { return; } @@ -3048,7 +3075,36 @@ Initialize(kShouldProcessData); session_->set_should_negotiate_h3_datagram(true); session_->EnableWebTransport(); - QuicSpdySessionPeer::EnableWebTransport(*session_); + QuicSpdySessionPeer::EnableWebTransport(session_.get()); + QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(), + HttpDatagramSupport::kDraft03); + + headers_[":method"] = "CONNECT"; + headers_[":protocol"] = "webtransport"; + + stream_->OnStreamHeadersPriority( + spdy::SpdyStreamPrecedence(kV3HighestPriority)); + ProcessHeaders(false, headers_); + CapsuleFrame capsule_frame(CapsuleType::REGISTER_DATAGRAM_NO_CONTEXT); + stream_->OnCapsuleFrame(capsule_frame); + EXPECT_EQ("", stream_->data()); + EXPECT_FALSE(stream_->header_list().empty()); + EXPECT_FALSE(stream_->IsDoneReading()); + ASSERT_TRUE(stream_->web_transport() != nullptr); + EXPECT_EQ(stream_->id(), stream_->web_transport()->id()); +} + +TEST_P(QuicSpdyStreamTest, ProcessIncomingWebTransportHeadersDraft00) { + if (!UsesHttp3()) { + return; + } + + Initialize(kShouldProcessData); + session_->set_should_negotiate_h3_datagram(true); + session_->EnableWebTransport(); + QuicSpdySessionPeer::EnableWebTransport(session_.get()); + QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(), + HttpDatagramSupport::kDraft00); headers_[":method"] = "CONNECT"; headers_[":protocol"] = "webtransport"; @@ -3075,7 +3131,9 @@ Initialize(kShouldProcessData); session_->set_should_negotiate_h3_datagram(true); session_->EnableWebTransport(); - QuicSpdySessionPeer::EnableWebTransport(*session_); + QuicSpdySessionPeer::EnableWebTransport(session_.get()); + QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(), + HttpDatagramSupport::kDraft00); headers_[":method"] = "CONNECT"; headers_[":protocol"] = "webtransport"; @@ -3119,13 +3177,14 @@ stream_->UnregisterHttp3DatagramRegistrationVisitor(); } -TEST_P(QuicSpdyStreamTest, H3DatagramRegistrationWithoutContext) { +TEST_P(QuicSpdyStreamTest, HttpDatagramRegistrationWithoutContextDraft00) { if (!UsesHttp3()) { return; } - Initialize(kShouldProcessData); + InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT); session_->set_should_negotiate_h3_datagram(true); - QuicSpdySessionPeer::SetH3DatagramSupported(session_.get(), true); + QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(), + HttpDatagramSupport::kDraft00); session_->RegisterHttp3DatagramFlowId(stream_->id(), stream_->id()); ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor> h3_datagram_registration_visitor; @@ -3168,23 +3227,39 @@ session_->UnregisterHttp3DatagramFlowId(stream_->id()); } -TEST_P(QuicSpdyStreamTest, H3DatagramRegistrationWithContext) { +TEST_P(QuicSpdyStreamTest, H3DatagramRegistrationWithoutContextDraft03) { if (!UsesHttp3()) { return; } - Initialize(kShouldProcessData); + InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT); session_->set_should_negotiate_h3_datagram(true); - QuicSpdySessionPeer::SetH3DatagramSupported(session_.get(), true); - session_->RegisterHttp3DatagramFlowId(stream_->id(), stream_->id()); + QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(), + HttpDatagramSupport::kDraft03); ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor> h3_datagram_registration_visitor; SavingHttp3DatagramVisitor h3_datagram_visitor; - absl::optional<QuicDatagramContextId> context_id = 42; + absl::optional<QuicDatagramContextId> context_id; Http3DatagramContextExtensions extensions; + ASSERT_EQ(QuicDataWriter::GetVarInt62Len(stream_->id()), 1); + std::array<char, 256> datagram; + datagram[0] = stream_->id(); + for (size_t i = 1; i < datagram.size(); i++) { + datagram[i] = i; + } stream_->RegisterHttp3DatagramRegistrationVisitor( &h3_datagram_registration_visitor); + + // Expect us to send a REGISTER_DATAGRAM_NO_CONTEXT capsule. + EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _)); + stream_->RegisterHttp3DatagramContextId(context_id, extensions, &h3_datagram_visitor); + session_->OnMessageReceived( + absl::string_view(datagram.data(), datagram.size())); + EXPECT_THAT(h3_datagram_visitor.received_h3_datagrams(), + ElementsAre(SavingHttp3DatagramVisitor::SavedHttp3Datagram{ + stream_->id(), context_id, + std::string(&datagram[1], datagram.size() - 1)})); // Test move. ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor> h3_datagram_registration_visitor2; @@ -3192,9 +3267,72 @@ SavingHttp3DatagramVisitor h3_datagram_visitor2; stream_->MoveHttp3DatagramContextIdRegistration(context_id, &h3_datagram_visitor2); + EXPECT_TRUE(h3_datagram_visitor2.received_h3_datagrams().empty()); + session_->OnMessageReceived( + absl::string_view(datagram.data(), datagram.size())); + EXPECT_THAT(h3_datagram_visitor2.received_h3_datagrams(), + ElementsAre(SavingHttp3DatagramVisitor::SavedHttp3Datagram{ + stream_->id(), context_id, + std::string(&datagram[1], datagram.size() - 1)})); // Cleanup. stream_->UnregisterHttp3DatagramContextId(context_id); stream_->UnregisterHttp3DatagramRegistrationVisitor(); +} + +TEST_P(QuicSpdyStreamTest, HttpDatagramRegistrationWithContext) { + if (!UsesHttp3()) { + return; + } + InitializeWithPerspective(kShouldProcessData, Perspective::IS_CLIENT); + session_->set_should_negotiate_h3_datagram(true); + QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(), + HttpDatagramSupport::kDraft03); + ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor> + h3_datagram_registration_visitor; + SavingHttp3DatagramVisitor h3_datagram_visitor; + absl::optional<QuicDatagramContextId> context_id = 42; + Http3DatagramContextExtensions extensions; + ASSERT_EQ(QuicDataWriter::GetVarInt62Len(stream_->id()), 1); + std::array<char, 256> datagram; + datagram[0] = stream_->id(); + datagram[1] = context_id.value(); + for (size_t i = 2; i < datagram.size(); i++) { + datagram[i] = i; + } + stream_->RegisterHttp3DatagramRegistrationVisitor( + &h3_datagram_registration_visitor); + + // Expect us to send a REGISTER_DATAGRAM_CONTEXT capsule. + EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _)); + + stream_->RegisterHttp3DatagramContextId(context_id, extensions, + &h3_datagram_visitor); + session_->OnMessageReceived( + absl::string_view(datagram.data(), datagram.size())); + EXPECT_THAT(h3_datagram_visitor.received_h3_datagrams(), + ElementsAre(SavingHttp3DatagramVisitor::SavedHttp3Datagram{ + stream_->id(), context_id, + std::string(&datagram[2], datagram.size() - 2)})); + // Test move. + ::testing::NiceMock<MockHttp3DatagramRegistrationVisitor> + h3_datagram_registration_visitor2; + stream_->MoveHttp3DatagramRegistration(&h3_datagram_registration_visitor2); + SavingHttp3DatagramVisitor h3_datagram_visitor2; + stream_->MoveHttp3DatagramContextIdRegistration(context_id, + &h3_datagram_visitor2); + EXPECT_TRUE(h3_datagram_visitor2.received_h3_datagrams().empty()); + session_->OnMessageReceived( + absl::string_view(datagram.data(), datagram.size())); + EXPECT_THAT(h3_datagram_visitor2.received_h3_datagrams(), + ElementsAre(SavingHttp3DatagramVisitor::SavedHttp3Datagram{ + stream_->id(), context_id, + std::string(&datagram[2], datagram.size() - 2)})); + // Cleanup. + + // Expect us to send a CLOSE_DATAGRAM_CONTEXT capsule. + EXPECT_CALL(*session_, WritevData(stream_->id(), _, _, _, _, _)); + stream_->UnregisterHttp3DatagramContextId(context_id); + stream_->UnregisterHttp3DatagramRegistrationVisitor(); session_->UnregisterHttp3DatagramFlowId(stream_->id()); } @@ -3204,7 +3342,8 @@ } Initialize(kShouldProcessData); session_->set_should_negotiate_h3_datagram(true); - QuicSpdySessionPeer::SetH3DatagramSupported(session_.get(), true); + QuicSpdySessionPeer::SetHttpDatagramSupport(session_.get(), + HttpDatagramSupport::kDraft03); absl::optional<QuicDatagramContextId> context_id; std::string h3_datagram_payload = {1, 2, 3, 4, 5, 6}; EXPECT_CALL(*connection_, SendMessage(1, _, false))
diff --git a/quic/core/quic_constants.h b/quic/core/quic_constants.h index 162de89..97f7567 100644 --- a/quic/core/quic_constants.h +++ b/quic/core/quic_constants.h
@@ -301,6 +301,10 @@ kDatagramContextIdIncrement = 2, }; +enum : uint64_t { + kHttpDatagramStreamIdDivisor = 4, +}; + } // namespace quic #endif // QUICHE_QUIC_CORE_QUIC_CONSTANTS_H_
diff --git a/quic/masque/masque_client_session.cc b/quic/masque/masque_client_session.cc index 0d86569..f3c70e2 100644 --- a/quic/masque/masque_client_session.cc +++ b/quic/masque/masque_client_session.cc
@@ -3,7 +3,9 @@ // found in the LICENSE file. #include "quic/masque/masque_client_session.h" + #include "absl/algorithm/container.h" +#include "absl/strings/str_cat.h" #include "quic/core/http/spdy_utils.h" #include "quic/core/quic_data_reader.h" #include "quic/core/quic_utils.h" @@ -101,7 +103,9 @@ headers[":scheme"] = "masque"; headers[":path"] = "/"; headers[":authority"] = target_server_address.ToString(); - SpdyUtils::AddDatagramFlowIdHeader(&headers, stream->id()); + if (http_datagram_support() == HttpDatagramSupport::kDraft00) { + SpdyUtils::AddDatagramFlowIdHeader(&headers, stream->id()); + } size_t bytes_sent = stream->SendRequest(std::move(headers), /*body=*/"", /*fin=*/false); if (bytes_sent == 0) { @@ -142,8 +146,8 @@ << " compressed with stream ID " << connect_udp->stream()->id() << " context ID " << (connect_udp->context_id().has_value() - ? connect_udp->context_id().value() - : 0) + ? absl::StrCat(connect_udp->context_id().value()) + : "none") << " and got message status " << MessageStatusToString(message_status); } @@ -182,8 +186,8 @@ QUIC_DLOG(INFO) << "Removing state for stream ID " << it->stream()->id() << " context ID " << (it->context_id().has_value() - ? it->context_id().value() - : 0); + ? absl::StrCat(it->context_id().value()) + : "none"); auto* stream = it->stream(); it = connect_udp_client_states_.erase(it); if (!stream->write_side_closed()) { @@ -224,8 +228,8 @@ QUIC_DLOG(INFO) << "Stream " << stream_id << " was closed, removing state for context ID " << (it->context_id().has_value() - ? it->context_id().value() - : 0); + ? absl::StrCat(it->context_id().value()) + : "none"); auto* encapsulated_client_session = it->encapsulated_client_session(); it = connect_udp_client_states_.erase(it); encapsulated_client_session->CloseConnection( @@ -241,6 +245,7 @@ } bool MasqueClientSession::OnSettingsFrame(const SettingsFrame& frame) { + QUIC_DLOG(INFO) << "Received SETTINGS: " << frame; if (!QuicSpdyClientSession::OnSettingsFrame(frame)) { QUIC_DLOG(ERROR) << "Failed to parse received settings"; return false; @@ -249,6 +254,7 @@ QUIC_DLOG(ERROR) << "Refusing to use MASQUE without HTTP/3 Datagrams"; return false; } + QUIC_DLOG(INFO) << "Using HTTP Datagram: " << http_datagram_support(); owner_->OnSettingsReceived(); return true; } @@ -308,7 +314,8 @@ QUIC_DVLOG(1) << "Sent " << payload.size() << " bytes to connection for stream ID " << stream_id << " context ID " - << (context_id.has_value() ? context_id.value() : 0); + << (context_id.has_value() ? absl::StrCat(context_id.value()) + : "none"); } void MasqueClientSession::ConnectUdpClientState::OnContextReceived( @@ -321,11 +328,13 @@ return; } if (context_id != context_id_) { - QUIC_DLOG(INFO) << "Ignoring unexpected context ID " - << (context_id.has_value() ? context_id.value() : 0) - << " instead of " - << (context_id_.has_value() ? context_id_.value() : 0) - << " on stream ID " << stream_->id(); + QUIC_DLOG(INFO) + << "Ignoring unexpected context ID " + << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none") + << " instead of " + << (context_id_.has_value() ? absl::StrCat(context_id_.value()) + : "none") + << " on stream ID " << stream_->id(); return; } // Do nothing since the client registers first and we currently ignore @@ -342,11 +351,13 @@ return; } if (context_id != context_id_) { - QUIC_DLOG(INFO) << "Ignoring unexpected close of context ID " - << (context_id.has_value() ? context_id.value() : 0) - << " instead of " - << (context_id_.has_value() ? context_id_.value() : 0) - << " on stream ID " << stream_->id(); + QUIC_DLOG(INFO) + << "Ignoring unexpected close of context ID " + << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none") + << " instead of " + << (context_id_.has_value() ? absl::StrCat(context_id_.value()) + : "none") + << " on stream ID " << stream_->id(); return; } QUIC_DLOG(INFO) << "Received datagram context close on stream ID "
diff --git a/quic/masque/masque_server_session.cc b/quic/masque/masque_server_session.cc index effc17f..a38ae9d 100644 --- a/quic/masque/masque_server_session.cc +++ b/quic/masque/masque_server_session.cc
@@ -202,13 +202,15 @@ QUIC_DLOG(ERROR) << "MASQUE request with bad method \"" << method << "\""; return CreateBackendErrorResponse("400", "Bad method"); } - absl::optional<QuicDatagramStreamId> flow_id = - SpdyUtils::ParseDatagramFlowIdHeader(request_headers); - if (!flow_id.has_value()) { - QUIC_DLOG(ERROR) - << "MASQUE request with bad or missing DatagramFlowId header"; - return CreateBackendErrorResponse("400", - "Bad or missing DatagramFlowId header"); + absl::optional<QuicDatagramStreamId> flow_id; + if (http_datagram_support() == HttpDatagramSupport::kDraft00) { + flow_id = SpdyUtils::ParseDatagramFlowIdHeader(request_headers); + if (!flow_id.has_value()) { + QUIC_DLOG(ERROR) + << "MASQUE request with bad or missing DatagramFlowId header"; + return CreateBackendErrorResponse( + "400", "Bad or missing DatagramFlowId header"); + } } QuicUrl url(absl::StrCat("https://", authority)); if (!url.IsValid() || url.PathParamsQuery() != "/") { @@ -235,7 +237,9 @@ info_list, freeaddrinfo); QuicSocketAddress target_server_address(info_list->ai_addr, info_list->ai_addrlen); - QUIC_DLOG(INFO) << "Got CONNECT_UDP request flow_id=" << *flow_id + QUIC_DLOG(INFO) << "Got CONNECT_UDP request on stream ID " + << request_handler->stream_id() << " flow_id=" + << (flow_id.has_value() ? absl::StrCat(*flow_id) : "none") << " target_server_address=\"" << target_server_address << "\""; @@ -264,20 +268,26 @@ << request_handler->stream_id(); return CreateBackendErrorResponse("500", "Bad stream type"); } - stream->RegisterHttp3DatagramFlowId(*flow_id); + if (flow_id.has_value()) { + stream->RegisterHttp3DatagramFlowId(*flow_id); + } connect_udp_server_states_.push_back( ConnectUdpServerState(stream, context_id, target_server_address, fd_wrapper.extract_fd(), this)); - // TODO(b/181256914) remove this when we drop support for - // draft-ietf-masque-h3-datagram-00 in favor of later drafts. - Http3DatagramContextExtensions extensions; - stream->RegisterHttp3DatagramContextId(context_id, extensions, - &connect_udp_server_states_.back()); + if (http_datagram_support() == HttpDatagramSupport::kDraft00) { + // TODO(b/181256914) remove this when we drop support for + // draft-ietf-masque-h3-datagram-00 in favor of later drafts. + Http3DatagramContextExtensions extensions; + stream->RegisterHttp3DatagramContextId( + context_id, extensions, &connect_udp_server_states_.back()); + } spdy::Http2HeaderBlock response_headers; response_headers[":status"] = "200"; - SpdyUtils::AddDatagramFlowIdHeader(&response_headers, *flow_id); + if (flow_id.has_value()) { + SpdyUtils::AddDatagramFlowIdHeader(&response_headers, *flow_id); + } auto response = std::make_unique<QuicBackendResponse>(); response->set_response_type(QuicBackendResponse::INCOMPLETE_RESPONSE); response->set_headers(std::move(response_headers)); @@ -424,6 +434,19 @@ return std::string("MasqueServerSession-") + connection_id().ToString(); } +bool MasqueServerSession::OnSettingsFrame(const SettingsFrame& frame) { + QUIC_DLOG(INFO) << "Received SETTINGS: " << frame; + if (!QuicSimpleServerSession::OnSettingsFrame(frame)) { + return false; + } + if (!SupportsH3Datagram()) { + QUIC_DLOG(ERROR) << "Refusing to use MASQUE without HTTP Datagrams"; + return false; + } + QUIC_DLOG(INFO) << "Using HTTP Datagram: " << http_datagram_support(); + return true; +} + MasqueServerSession::ConnectUdpServerState::ConnectUdpServerState( QuicSpdyStream* stream, absl::optional<QuicDatagramContextId> context_id, const QuicSocketAddress& target_server_address, QuicUdpSocketFd fd, @@ -440,10 +463,10 @@ MasqueServerSession::ConnectUdpServerState::~ConnectUdpServerState() { if (stream() != nullptr) { - stream()->UnregisterHttp3DatagramRegistrationVisitor(); if (context_registered_) { stream()->UnregisterHttp3DatagramContextId(context_id()); } + stream()->UnregisterHttp3DatagramRegistrationVisitor(); } if (fd_ == kQuicInvalidSocketFd) { return; @@ -515,17 +538,20 @@ context_id_ = context_id; } if (context_id != context_id_) { - QUIC_DLOG(INFO) << "Ignoring unexpected context ID " - << (context_id.has_value() ? context_id.value() : 0) - << " instead of " - << (context_id_.has_value() ? context_id_.value() : 0) - << " on stream ID " << stream()->id(); + QUIC_DLOG(INFO) + << "Ignoring unexpected context ID " + << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none") + << " instead of " + << (context_id_.has_value() ? absl::StrCat(context_id_.value()) + : "none") + << " on stream ID " << stream()->id(); return; } if (context_registered_) { QUIC_BUG(MASQUE server double datagram context registration) << "Try to re-register stream ID " << stream_id << " context ID " - << (context_id_.has_value() ? context_id_.value() : 0); + << (context_id_.has_value() ? absl::StrCat(context_id_.value()) + : "none"); return; } context_registered_ = true; @@ -543,11 +569,13 @@ return; } if (context_id != context_id_) { - QUIC_DLOG(INFO) << "Ignoring unexpected close of context ID " - << (context_id.has_value() ? context_id.value() : 0) - << " instead of " - << (context_id_.has_value() ? context_id_.value() : 0) - << " on stream ID " << stream()->id(); + QUIC_DLOG(INFO) + << "Ignoring unexpected close of context ID " + << (context_id.has_value() ? absl::StrCat(context_id.value()) : "none") + << " instead of " + << (context_id_.has_value() ? absl::StrCat(context_id_.value()) + : "none") + << " on stream ID " << stream()->id(); return; } QUIC_DLOG(INFO) << "Received datagram context close on stream ID "
diff --git a/quic/masque/masque_server_session.h b/quic/masque/masque_server_session.h index 1bec14c..5729655 100644 --- a/quic/masque/masque_server_session.h +++ b/quic/masque/masque_server_session.h
@@ -136,6 +136,8 @@ bool context_registered_ = false; }; + // From QuicSpdySession. + bool OnSettingsFrame(const SettingsFrame& frame) override; bool ShouldNegotiateHttp3Datagram() override { return true; } MasqueServerBackend* masque_server_backend_; // Unowned.
diff --git a/quic/test_tools/quic_spdy_session_peer.cc b/quic/test_tools/quic_spdy_session_peer.cc index a27f073..d0fd669 100644 --- a/quic/test_tools/quic_spdy_session_peer.cc +++ b/quic/test_tools/quic_spdy_session_peer.cc
@@ -100,9 +100,9 @@ } // static -void QuicSpdySessionPeer::SetH3DatagramSupported(QuicSpdySession* session, - bool h3_datagram_supported) { - session->h3_datagram_supported_ = h3_datagram_supported; +void QuicSpdySessionPeer::SetHttpDatagramSupport( + QuicSpdySession* session, HttpDatagramSupport http_datagram_support) { + session->http_datagram_support_ = http_datagram_support; } // static @@ -112,10 +112,10 @@ } // static -void QuicSpdySessionPeer::EnableWebTransport(QuicSpdySession& session) { - QUICHE_DCHECK(session.WillNegotiateWebTransport()); - session.h3_datagram_supported_ = true; - session.peer_supports_webtransport_ = true; +void QuicSpdySessionPeer::EnableWebTransport(QuicSpdySession* session) { + QUICHE_DCHECK(session->WillNegotiateWebTransport()); + SetHttpDatagramSupport(session, HttpDatagramSupport::kDraft03); + session->peer_supports_webtransport_ = true; } } // namespace test
diff --git a/quic/test_tools/quic_spdy_session_peer.h b/quic/test_tools/quic_spdy_session_peer.h index 0d06c4d..2aa204b 100644 --- a/quic/test_tools/quic_spdy_session_peer.h +++ b/quic/test_tools/quic_spdy_session_peer.h
@@ -7,6 +7,7 @@ #include "quic/core/http/quic_receive_control_stream.h" #include "quic/core/http/quic_send_control_stream.h" +#include "quic/core/http/quic_spdy_session.h" #include "quic/core/qpack/qpack_receive_stream.h" #include "quic/core/qpack/qpack_send_stream.h" #include "quic/core/quic_packets.h" @@ -16,7 +17,6 @@ namespace quic { class QuicHeadersStream; -class QuicSpdySession; namespace test { @@ -50,10 +50,10 @@ QuicSpdySession* session); static QpackReceiveStream* GetQpackEncoderReceiveStream( QuicSpdySession* session); - static void SetH3DatagramSupported(QuicSpdySession* session, - bool h3_datagram_supported); + static void SetHttpDatagramSupport(QuicSpdySession* session, + HttpDatagramSupport http_datagram_support); static bool ShouldNegotiateHttp3Datagram(QuicSpdySession* session); - static void EnableWebTransport(QuicSpdySession& session); + static void EnableWebTransport(QuicSpdySession* session); }; } // namespace test