Adds metadata decoding support in the oghttp2 stack.
Also includes some test utility changes to facilitate the new tests.
PiperOrigin-RevId: 389029208
diff --git a/http2/adapter/callback_visitor.cc b/http2/adapter/callback_visitor.cc
index 3ecc064..4381749 100644
--- a/http2/adapter/callback_visitor.cc
+++ b/http2/adapter/callback_visitor.cc
@@ -388,9 +388,6 @@
size_t payload_length) {
QUICHE_VLOG(1) << "OnBeginMetadataForStream(stream_id=" << stream_id
<< ", payload_length=" << payload_length << ")";
- if (callbacks_->on_frame_recv_callback) {
- callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_);
- }
}
void CallbackVisitor::OnMetadataForStream(Http2StreamId stream_id,
diff --git a/http2/adapter/http2_protocol.h b/http2/adapter/http2_protocol.h
index e25d177..5e892e0 100644
--- a/http2/adapter/http2_protocol.h
+++ b/http2/adapter/http2_protocol.h
@@ -131,8 +131,9 @@
kServer,
};
-const uint8_t kMetadataFrameType = 0x4d;
-const uint8_t kMetadataEndFlag = 0x04;
+inline constexpr uint8_t kMetadataFrameType = 0x4d;
+inline constexpr uint8_t kMetadataEndFlag = 0x04;
+inline constexpr uint16_t kMetadataExtensionId = 0x4d44;
} // namespace adapter
} // namespace http2
diff --git a/http2/adapter/nghttp2_adapter_test.cc b/http2/adapter/nghttp2_adapter_test.cc
index e08a683..785e476 100644
--- a/http2/adapter/nghttp2_adapter_test.cc
+++ b/http2/adapter/nghttp2_adapter_test.cc
@@ -381,6 +381,7 @@
EXPECT_CALL(visitor, OnSettingsEnd());
EXPECT_CALL(visitor, OnFrameHeader(0, _, kMetadataFrameType, 4));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(0, _));
EXPECT_CALL(visitor, OnMetadataForStream(0, _));
EXPECT_CALL(visitor, OnMetadataEndForStream(0));
EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4));
@@ -391,6 +392,7 @@
OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT"));
EXPECT_CALL(visitor, OnEndHeadersForStream(1));
EXPECT_CALL(visitor, OnFrameHeader(1, _, kMetadataFrameType, 4));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(1, _));
EXPECT_CALL(visitor, OnMetadataForStream(1, _));
EXPECT_CALL(visitor, OnMetadataEndForStream(1));
EXPECT_CALL(visitor, OnFrameHeader(1, 26, DATA, 1));
@@ -1482,6 +1484,69 @@
EXPECT_EQ(frames.size(), result);
}
+TEST(NgHttp2AdapterTest, ClientSendsMetadataWithContinuation) {
+ DataSavingVisitor visitor;
+ auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);
+ EXPECT_FALSE(adapter->session().want_write());
+
+ const std::string frames =
+ TestFrameSequence()
+ .ClientPreface()
+ .Metadata(0, "Example connection metadata in multiple frames", true)
+ .Headers(1,
+ {{":method", "GET"},
+ {":scheme", "https"},
+ {":authority", "example.com"},
+ {":path", "/this/is/request/one"}},
+ /*fin=*/false,
+ /*add_continuation=*/true)
+ .Metadata(1,
+ "Some stream metadata that's also sent in multiple frames",
+ true)
+ .Serialize();
+ testing::InSequence s;
+
+ // Client preface (empty SETTINGS)
+ EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+ EXPECT_CALL(visitor, OnSettingsStart());
+ EXPECT_CALL(visitor, OnSettingsEnd());
+ // Metadata on stream 0
+ EXPECT_CALL(visitor, OnFrameHeader(0, _, kMetadataFrameType, 0));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(0, _));
+ EXPECT_CALL(visitor, OnMetadataForStream(0, _));
+ EXPECT_CALL(visitor, OnFrameHeader(0, _, kMetadataFrameType, 4));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(0, _));
+ EXPECT_CALL(visitor, OnMetadataForStream(0, _));
+ EXPECT_CALL(visitor, OnMetadataEndForStream(0));
+
+ // Stream 1
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 0));
+ EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https"));
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, CONTINUATION, 4));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one"));
+ EXPECT_CALL(visitor, OnEndHeadersForStream(1));
+ // Metadata on stream 1
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, kMetadataFrameType, 0));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(1, _));
+ EXPECT_CALL(visitor, OnMetadataForStream(1, _));
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, kMetadataFrameType, 4));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(1, _));
+ EXPECT_CALL(visitor, OnMetadataForStream(1, _));
+ EXPECT_CALL(visitor, OnMetadataEndForStream(1));
+
+ const size_t result = adapter->ProcessBytes(frames);
+ EXPECT_EQ(frames.size(), result);
+ EXPECT_EQ(TestFrameSequence::MetadataBlockForPayload(
+ "Example connection metadata in multiple frames"),
+ absl::StrJoin(visitor.GetMetadata(0), ""));
+ EXPECT_EQ(TestFrameSequence::MetadataBlockForPayload(
+ "Some stream metadata that's also sent in multiple frames"),
+ absl::StrJoin(visitor.GetMetadata(1), ""));
+}
+
TEST(NgHttp2AdapterTest, ServerSendsInvalidTrailers) {
DataSavingVisitor visitor;
auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);
diff --git a/http2/adapter/nghttp2_callbacks.cc b/http2/adapter/nghttp2_callbacks.cc
index 51b2054..4a2221e 100644
--- a/http2/adapter/nghttp2_callbacks.cc
+++ b/http2/adapter/nghttp2_callbacks.cc
@@ -42,6 +42,8 @@
header->flags);
if (header->type == NGHTTP2_DATA) {
visitor->OnBeginDataForStream(header->stream_id, header->length);
+ } else if (header->type == kMetadataFrameType) {
+ visitor->OnBeginMetadataForStream(header->stream_id, header->length);
}
return 0;
}
diff --git a/http2/adapter/oghttp2_adapter_test.cc b/http2/adapter/oghttp2_adapter_test.cc
index 7c038ce..803374e 100644
--- a/http2/adapter/oghttp2_adapter_test.cc
+++ b/http2/adapter/oghttp2_adapter_test.cc
@@ -187,8 +187,9 @@
EXPECT_CALL(visitor, OnSettingsEnd());
EXPECT_CALL(visitor, OnFrameHeader(0, _, kMetadataFrameType, 4));
- // EXPECT_CALL(visitor, OnMetadataForStream(0, _));
- // EXPECT_CALL(visitor, OnMetadataEndForStream(0));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(0, _));
+ EXPECT_CALL(visitor, OnMetadataForStream(0, _));
+ EXPECT_CALL(visitor, OnMetadataEndForStream(0));
EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4));
EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200"));
@@ -197,8 +198,9 @@
OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT"));
EXPECT_CALL(visitor, OnEndHeadersForStream(1));
EXPECT_CALL(visitor, OnFrameHeader(1, _, kMetadataFrameType, 4));
- // EXPECT_CALL(visitor, OnMetadataForStream(1, _));
- // EXPECT_CALL(visitor, OnMetadataEndForStream(1));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(1, _));
+ EXPECT_CALL(visitor, OnMetadataForStream(1, _));
+ EXPECT_CALL(visitor, OnMetadataEndForStream(1));
EXPECT_CALL(visitor, OnFrameHeader(1, 26, DATA, 1));
EXPECT_CALL(visitor, OnBeginDataForStream(1, 26));
EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body."));
@@ -687,6 +689,70 @@
EXPECT_EQ(frames.size(), result);
}
+TEST(OgHttp2AdapterServerTest, ClientSendsMetadataWithContinuation) {
+ DataSavingVisitor visitor;
+ OgHttp2Adapter::Options options{.perspective = Perspective::kServer};
+ auto adapter = OgHttp2Adapter::Create(visitor, options);
+ EXPECT_FALSE(adapter->session().want_write());
+
+ const std::string frames =
+ TestFrameSequence()
+ .ClientPreface()
+ .Metadata(0, "Example connection metadata in multiple frames", true)
+ .Headers(1,
+ {{":method", "GET"},
+ {":scheme", "https"},
+ {":authority", "example.com"},
+ {":path", "/this/is/request/one"}},
+ /*fin=*/false,
+ /*add_continuation=*/true)
+ .Metadata(1,
+ "Some stream metadata that's also sent in multiple frames",
+ true)
+ .Serialize();
+ testing::InSequence s;
+
+ // Client preface (empty SETTINGS)
+ EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0));
+ EXPECT_CALL(visitor, OnSettingsStart());
+ EXPECT_CALL(visitor, OnSettingsEnd());
+ // Metadata on stream 0
+ EXPECT_CALL(visitor, OnFrameHeader(0, _, kMetadataFrameType, 0));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(0, _));
+ EXPECT_CALL(visitor, OnMetadataForStream(0, _));
+ EXPECT_CALL(visitor, OnFrameHeader(0, _, kMetadataFrameType, 4));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(0, _));
+ EXPECT_CALL(visitor, OnMetadataForStream(0, _));
+ EXPECT_CALL(visitor, OnMetadataEndForStream(0));
+
+ // Stream 1
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 0));
+ EXPECT_CALL(visitor, OnBeginHeadersForStream(1));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https"));
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, CONTINUATION, 4));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com"));
+ EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one"));
+ EXPECT_CALL(visitor, OnEndHeadersForStream(1));
+ // Metadata on stream 1
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, kMetadataFrameType, 0));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(1, _));
+ EXPECT_CALL(visitor, OnMetadataForStream(1, _));
+ EXPECT_CALL(visitor, OnFrameHeader(1, _, kMetadataFrameType, 4));
+ EXPECT_CALL(visitor, OnBeginMetadataForStream(1, _));
+ EXPECT_CALL(visitor, OnMetadataForStream(1, _));
+ EXPECT_CALL(visitor, OnMetadataEndForStream(1));
+
+ const size_t result = adapter->ProcessBytes(frames);
+ EXPECT_EQ(frames.size(), result);
+ EXPECT_EQ(TestFrameSequence::MetadataBlockForPayload(
+ "Example connection metadata in multiple frames"),
+ absl::StrJoin(visitor.GetMetadata(0), ""));
+ EXPECT_EQ(TestFrameSequence::MetadataBlockForPayload(
+ "Some stream metadata that's also sent in multiple frames"),
+ absl::StrJoin(visitor.GetMetadata(1), ""));
+}
+
TEST(OgHttp2AdapterServerTest, ServerSendsInvalidTrailers) {
DataSavingVisitor visitor;
OgHttp2Adapter::Options options{.perspective = Perspective::kServer};
diff --git a/http2/adapter/oghttp2_session.cc b/http2/adapter/oghttp2_session.cc
index c4280bd..3a32b2e 100644
--- a/http2/adapter/oghttp2_session.cc
+++ b/http2/adapter/oghttp2_session.cc
@@ -144,6 +144,7 @@
}),
options_(options) {
decoder_.set_visitor(this);
+ decoder_.set_extension_visitor(this);
if (options_.perspective == Perspective::kServer) {
remaining_preface_ = {spdy::kHttp2ConnectionHeaderPrefix,
spdy::kHttp2ConnectionHeaderPrefixSize};
@@ -650,6 +651,9 @@
void OgHttp2Session::OnSetting(spdy::SpdySettingsId id, uint32_t value) {
visitor_.OnSetting({id, value});
+ if (id == kMetadataExtensionId) {
+ peer_supports_metadata_ = (value != 0);
+ }
}
void OgHttp2Session::OnSettingsEnd() {
@@ -748,6 +752,39 @@
}
}
+bool OgHttp2Session::OnFrameHeader(spdy::SpdyStreamId stream_id, size_t length,
+ uint8_t type, uint8_t flags) {
+ if (type == kMetadataFrameType) {
+ QUICHE_DCHECK_EQ(metadata_length_, 0u);
+ visitor_.OnBeginMetadataForStream(stream_id, length);
+ metadata_stream_id_ = stream_id;
+ metadata_length_ = length;
+ end_metadata_ = flags & kMetadataEndFlag;
+ return true;
+ } else {
+ QUICHE_DLOG(INFO) << "Unexpected frame type " << static_cast<int>(type)
+ << " received by the extension visitor.";
+ return false;
+ }
+}
+
+void OgHttp2Session::OnFramePayload(const char* data, size_t len) {
+ if (metadata_length_ > 0) {
+ QUICHE_DCHECK_LE(len, metadata_length_);
+ visitor_.OnMetadataForStream(metadata_stream_id_,
+ absl::string_view(data, len));
+ metadata_length_ -= len;
+ if (metadata_length_ == 0 && end_metadata_) {
+ visitor_.OnMetadataEndForStream(metadata_stream_id_);
+ metadata_stream_id_ = 0;
+ end_metadata_ = false;
+ }
+ } else {
+ QUICHE_DLOG(INFO) << "Unexpected metadata payload for stream "
+ << metadata_stream_id_;
+ }
+}
+
void OgHttp2Session::MaybeSetupPreface() {
if (!queued_preface_) {
if (options_.perspective == Perspective::kClient) {
diff --git a/http2/adapter/oghttp2_session.h b/http2/adapter/oghttp2_session.h
index 5188828..77b9da7 100644
--- a/http2/adapter/oghttp2_session.h
+++ b/http2/adapter/oghttp2_session.h
@@ -20,7 +20,8 @@
// This class manages state associated with a single multiplexed HTTP/2 session.
class QUICHE_EXPORT_PRIVATE OgHttp2Session
: public Http2Session,
- public spdy::SpdyFramerVisitorInterface {
+ public spdy::SpdyFramerVisitorInterface,
+ public spdy::ExtensionVisitorInterface {
public:
struct QUICHE_EXPORT_PRIVATE Options {
Perspective perspective = Perspective::kClient;
@@ -152,6 +153,13 @@
void OnHeaderStatus(Http2StreamId stream_id,
Http2VisitorInterface::OnHeaderResult result);
+ // Returns true if a recognized extension frame is received.
+ bool OnFrameHeader(spdy::SpdyStreamId stream_id, size_t length, uint8_t type,
+ uint8_t flags) override;
+
+ // Handles the payload for a recognized extension frame.
+ void OnFramePayload(const char* data, size_t len) override;
+
private:
using MetadataSequence = std::vector<std::unique_ptr<MetadataSource>>;
struct QUICHE_EXPORT_PRIVATE StreamState {
@@ -256,6 +264,8 @@
Http2StreamId next_stream_id_ = 1;
Http2StreamId highest_received_stream_id_ = 0;
+ Http2StreamId metadata_stream_id_ = 0;
+ size_t metadata_length_ = 0;
int connection_send_window_ = kInitialFlowControlWindowSize;
// The initial flow control receive window size for any newly created streams.
int stream_receive_window_limit_ = kInitialFlowControlWindowSize;
@@ -263,6 +273,8 @@
Options options_;
bool received_goaway_ = false;
bool queued_preface_ = false;
+ bool peer_supports_metadata_ = false;
+ bool end_metadata_ = false;
// Replace this with a stream ID, for multiple GOAWAY support.
bool queued_goaway_ = false;
diff --git a/http2/adapter/test_frame_sequence.cc b/http2/adapter/test_frame_sequence.cc
index c5058a7..bb1517d 100644
--- a/http2/adapter/test_frame_sequence.cc
+++ b/http2/adapter/test_frame_sequence.cc
@@ -148,17 +148,21 @@
}
TestFrameSequence& TestFrameSequence::Metadata(Http2StreamId stream_id,
- absl::string_view payload) {
- // Encode the payload using a header block.
- spdy::SpdyHeaderBlock block;
- block["example-payload"] = payload;
- spdy::HpackEncoder encoder;
- encoder.DisableCompression();
- std::string encoded_payload;
- encoder.EncodeHeaderSet(block, &encoded_payload);
- frames_.push_back(absl::make_unique<spdy::SpdyUnknownIR>(
- stream_id, kMetadataFrameType, kMetadataEndFlag,
- std::move(encoded_payload)));
+ absl::string_view payload,
+ bool multiple_frames) {
+ const std::string encoded_payload = MetadataBlockForPayload(payload);
+ if (multiple_frames) {
+ const size_t pos = encoded_payload.size() / 2;
+ frames_.push_back(absl::make_unique<spdy::SpdyUnknownIR>(
+ stream_id, kMetadataFrameType, 0, encoded_payload.substr(0, pos)));
+ frames_.push_back(absl::make_unique<spdy::SpdyUnknownIR>(
+ stream_id, kMetadataFrameType, kMetadataEndFlag,
+ encoded_payload.substr(pos)));
+ } else {
+ frames_.push_back(absl::make_unique<spdy::SpdyUnknownIR>(
+ stream_id, kMetadataFrameType, kMetadataEndFlag,
+ std::move(encoded_payload)));
+ }
return *this;
}
@@ -175,6 +179,18 @@
return result;
}
+std::string TestFrameSequence::MetadataBlockForPayload(
+ absl::string_view payload) {
+ // Encode the payload using a header block.
+ spdy::SpdyHeaderBlock block;
+ block["example-payload"] = payload;
+ spdy::HpackEncoder encoder;
+ encoder.DisableCompression();
+ std::string encoded_payload;
+ encoder.EncodeHeaderSet(block, &encoded_payload);
+ return encoded_payload;
+}
+
} // namespace test
} // namespace adapter
} // namespace http2
diff --git a/http2/adapter/test_frame_sequence.h b/http2/adapter/test_frame_sequence.h
index 4f05756..99740d3 100644
--- a/http2/adapter/test_frame_sequence.h
+++ b/http2/adapter/test_frame_sequence.h
@@ -50,10 +50,13 @@
int weight,
bool exclusive);
TestFrameSequence& Metadata(Http2StreamId stream_id,
- absl::string_view payload);
+ absl::string_view payload,
+ bool multiple_frames = false);
std::string Serialize();
+ static std::string MetadataBlockForPayload(absl::string_view);
+
private:
std::string preface_;
std::vector<std::unique_ptr<spdy::SpdyFrameIR>> frames_;
diff --git a/http2/adapter/test_utils.h b/http2/adapter/test_utils.h
index ff7728d..0c18cdb 100644
--- a/http2/adapter/test_utils.h
+++ b/http2/adapter/test_utils.h
@@ -32,6 +32,24 @@
return to_accept;
}
+ void OnMetadataForStream(Http2StreamId stream_id,
+ absl::string_view metadata) override {
+ testing::StrictMock<MockHttp2Visitor>::OnMetadataForStream(stream_id,
+ metadata);
+ auto result =
+ metadata_map_.try_emplace(stream_id, std::vector<std::string>());
+ result.first->second.push_back(std::string(metadata));
+ }
+
+ const std::vector<std::string> GetMetadata(Http2StreamId stream_id) {
+ auto it = metadata_map_.find(stream_id);
+ if (it == metadata_map_.end()) {
+ return {};
+ } else {
+ return it->second;
+ }
+ }
+
const std::string& data() { return data_; }
void Clear() { data_.clear(); }
@@ -42,6 +60,7 @@
private:
std::string data_;
+ absl::flat_hash_map<Http2StreamId, std::vector<std::string>> metadata_map_;
size_t send_limit_ = std::numeric_limits<size_t>::max();
bool is_write_blocked_ = false;
};