diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2021-10-26 13:57:00 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2021-11-02 11:31:01 +0000 |
commit | 1943b3c2a1dcee36c233724fc4ee7613d71b9cf6 (patch) | |
tree | 8c1b5f12357025c197da5427ae02cfdc2f3570d6 /chromium/net/third_party/quiche/src/http2/adapter | |
parent | 21ba0c5d4bf8fba15dddd97cd693bad2358b77fd (diff) | |
download | qtwebengine-chromium-1943b3c2a1dcee36c233724fc4ee7613d71b9cf6.tar.gz |
BASELINE: Update Chromium to 94.0.4606.111
Change-Id: I924781584def20fc800bedf6ff41fdb96c438193
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/net/third_party/quiche/src/http2/adapter')
47 files changed, 6635 insertions, 1046 deletions
diff --git a/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor.cc b/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor.cc index eaf4b6169d2..4381749d430 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor.cc @@ -36,14 +36,63 @@ struct nghttp2_session_callbacks { namespace http2 { namespace adapter { +CallbackVisitor::CallbackVisitor(Perspective perspective, + const nghttp2_session_callbacks& callbacks, + void* user_data) + : perspective_(perspective), + callbacks_(MakeCallbacksPtr(nullptr)), + user_data_(user_data) { + nghttp2_session_callbacks* c; + nghttp2_session_callbacks_new(&c); + *c = callbacks; + callbacks_ = MakeCallbacksPtr(c); +} + +ssize_t CallbackVisitor::OnReadyToSend(absl::string_view serialized) { + if (!callbacks_->send_callback) { + return kSendError; + } + ssize_t result = callbacks_->send_callback( + nullptr, ToUint8Ptr(serialized.data()), serialized.size(), 0, user_data_); + QUICHE_VLOG(1) << "CallbackVisitor::OnReadyToSend returning " << result; + if (result > 0) { + return result; + } else if (result == NGHTTP2_ERR_WOULDBLOCK) { + return kSendBlocked; + } else { + return kSendError; + } +} + void CallbackVisitor::OnConnectionError() { - QUICHE_LOG(FATAL) << "Not implemented"; + QUICHE_LOG(ERROR) << "OnConnectionError not implemented"; } void CallbackVisitor::OnFrameHeader(Http2StreamId stream_id, size_t length, uint8_t type, uint8_t flags) { + QUICHE_VLOG(1) << "CallbackVisitor::OnFrameHeader(stream_id: " << stream_id + << ", len: " << length << ", type: " << int(type) + << ", flags: " << int(flags) << ")"; + if (static_cast<FrameType>(type) == FrameType::CONTINUATION) { + // Treat CONTINUATION as HEADERS + QUICHE_DCHECK_EQ(current_frame_.hd.stream_id, stream_id); + current_frame_.hd.length += length; + current_frame_.hd.flags |= flags; + QUICHE_DLOG_IF(ERROR, length == 0) << "Empty CONTINUATION!"; + // Still need to deliver the CONTINUATION to the begin frame callback. + nghttp2_frame_hd hd; + memset(&hd, 0, sizeof(hd)); + hd.stream_id = stream_id; + hd.length = length; + hd.type = type; + hd.flags = flags; + if (callbacks_->on_begin_frame_callback) { + callbacks_->on_begin_frame_callback(nullptr, &hd, user_data_); + } + return; + } // The general strategy is to clear |current_frame_| at the start of a new // frame, accumulate frame information from the various callback events, then // invoke the on_frame_recv_callback() with the accumulated frame data. @@ -52,7 +101,10 @@ void CallbackVisitor::OnFrameHeader(Http2StreamId stream_id, current_frame_.hd.length = length; current_frame_.hd.type = type; current_frame_.hd.flags = flags; - callbacks_->on_begin_frame_callback(nullptr, ¤t_frame_.hd, user_data_); + if (callbacks_->on_begin_frame_callback) { + callbacks_->on_begin_frame_callback(nullptr, ¤t_frame_.hd, + user_data_); + } } void CallbackVisitor::OnSettingsStart() {} @@ -64,106 +116,147 @@ void CallbackVisitor::OnSetting(Http2Setting setting) { void CallbackVisitor::OnSettingsEnd() { current_frame_.settings.niv = settings_.size(); current_frame_.settings.iv = settings_.data(); - callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + QUICHE_VLOG(1) << "OnSettingsEnd, received settings of size " + << current_frame_.settings.niv; + if (callbacks_->on_frame_recv_callback) { + callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + } settings_.clear(); } void CallbackVisitor::OnSettingsAck() { // ACK is part of the flags, which were set in OnFrameHeader(). - callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + if (callbacks_->on_frame_recv_callback) { + callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + } } -void CallbackVisitor::OnBeginHeadersForStream(Http2StreamId stream_id) { - auto it = stream_map_.find(stream_id); - if (it == stream_map_.end()) { - auto p = stream_map_.insert({stream_id, absl::make_unique<StreamInfo>()}); - it = p.first; - } +bool CallbackVisitor::OnBeginHeadersForStream(Http2StreamId stream_id) { + auto it = GetStreamInfo(stream_id); if (it->second->received_headers) { // At least one headers frame has already been received. + QUICHE_VLOG(1) + << "Headers already received for stream " << stream_id + << ", these are trailers or headers following a 100 response"; current_frame_.headers.cat = NGHTTP2_HCAT_HEADERS; } else { switch (perspective_) { case Perspective::kClient: + QUICHE_VLOG(1) << "First headers at the client for stream " << stream_id + << "; these are response headers"; current_frame_.headers.cat = NGHTTP2_HCAT_RESPONSE; break; case Perspective::kServer: + QUICHE_VLOG(1) << "First headers at the server for stream " << stream_id + << "; these are request headers"; current_frame_.headers.cat = NGHTTP2_HCAT_REQUEST; break; } } - callbacks_->on_begin_headers_callback(nullptr, ¤t_frame_, user_data_); it->second->received_headers = true; + if (callbacks_->on_begin_headers_callback) { + const int result = callbacks_->on_begin_headers_callback( + nullptr, ¤t_frame_, user_data_); + return result == 0; + } + return true; } -void CallbackVisitor::OnHeaderForStream(Http2StreamId stream_id, - absl::string_view name, - absl::string_view value) { - callbacks_->on_header_callback( - nullptr, ¤t_frame_, ToUint8Ptr(name.data()), name.size(), - ToUint8Ptr(value.data()), value.size(), NGHTTP2_NV_FLAG_NONE, user_data_); +Http2VisitorInterface::OnHeaderResult CallbackVisitor::OnHeaderForStream( + Http2StreamId /*stream_id*/, absl::string_view name, + absl::string_view value) { + if (callbacks_->on_header_callback) { + const int result = callbacks_->on_header_callback( + nullptr, ¤t_frame_, ToUint8Ptr(name.data()), name.size(), + ToUint8Ptr(value.data()), value.size(), NGHTTP2_NV_FLAG_NONE, + user_data_); + if (result == 0) { + return HEADER_OK; + } else if (result == NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE) { + return HEADER_RST_STREAM; + } else { + // Assume NGHTTP2_ERR_CALLBACK_FAILURE. + return HEADER_CONNECTION_ERROR; + } + } + return HEADER_OK; } -void CallbackVisitor::OnEndHeadersForStream(Http2StreamId stream_id) { - callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); +void CallbackVisitor::OnEndHeadersForStream(Http2StreamId /*stream_id*/) { + if (callbacks_->on_frame_recv_callback) { + const int result = callbacks_->on_frame_recv_callback( + nullptr, ¤t_frame_, user_data_); + QUICHE_DCHECK_EQ(0, result); + } } -void CallbackVisitor::OnBeginDataForStream(Http2StreamId stream_id, +void CallbackVisitor::OnBeginDataForStream(Http2StreamId /*stream_id*/, size_t payload_length) { // TODO(b/181586191): Interpret padding, subtract padding from // |remaining_data_|. remaining_data_ = payload_length; - if (remaining_data_ == 0) { + if (remaining_data_ == 0 && callbacks_->on_frame_recv_callback) { callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); } } void CallbackVisitor::OnDataForStream(Http2StreamId stream_id, absl::string_view data) { - callbacks_->on_data_chunk_recv_callback(nullptr, current_frame_.hd.flags, - stream_id, ToUint8Ptr(data.data()), - data.size(), user_data_); + if (callbacks_->on_data_chunk_recv_callback) { + callbacks_->on_data_chunk_recv_callback(nullptr, current_frame_.hd.flags, + stream_id, ToUint8Ptr(data.data()), + data.size(), user_data_); + } remaining_data_ -= data.size(); - if (remaining_data_ == 0) { + if (remaining_data_ == 0 && callbacks_->on_frame_recv_callback) { callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); } } -void CallbackVisitor::OnEndStream(Http2StreamId stream_id) {} +void CallbackVisitor::OnEndStream(Http2StreamId /*stream_id*/) {} -void CallbackVisitor::OnRstStream(Http2StreamId stream_id, +void CallbackVisitor::OnRstStream(Http2StreamId /*stream_id*/, Http2ErrorCode error_code) { current_frame_.rst_stream.error_code = static_cast<uint32_t>(error_code); - callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + if (callbacks_->on_frame_recv_callback) { + callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + } } void CallbackVisitor::OnCloseStream(Http2StreamId stream_id, Http2ErrorCode error_code) { - callbacks_->on_stream_close_callback( - nullptr, stream_id, static_cast<uint32_t>(error_code), user_data_); + if (callbacks_->on_stream_close_callback) { + QUICHE_VLOG(1) << "OnCloseStream(stream_id: " << stream_id + << ", error_code: " << int(error_code) << ")"; + callbacks_->on_stream_close_callback( + nullptr, stream_id, static_cast<uint32_t>(error_code), user_data_); + } } -void CallbackVisitor::OnPriorityForStream(Http2StreamId stream_id, +void CallbackVisitor::OnPriorityForStream(Http2StreamId /*stream_id*/, Http2StreamId parent_stream_id, - int weight, - bool exclusive) { + int weight, bool exclusive) { current_frame_.priority.pri_spec.stream_id = parent_stream_id; current_frame_.priority.pri_spec.weight = weight; current_frame_.priority.pri_spec.exclusive = exclusive; - callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + if (callbacks_->on_frame_recv_callback) { + callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + } } -void CallbackVisitor::OnPing(Http2PingId ping_id, bool is_ack) { +void CallbackVisitor::OnPing(Http2PingId ping_id, bool /*is_ack*/) { uint64_t network_order_opaque_data = quiche::QuicheEndian::HostToNet64(ping_id); std::memcpy(current_frame_.ping.opaque_data, &network_order_opaque_data, sizeof(network_order_opaque_data)); - callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + if (callbacks_->on_frame_recv_callback) { + callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + } } -void CallbackVisitor::OnPushPromiseForStream(Http2StreamId stream_id, - Http2StreamId promised_stream_id) { - QUICHE_LOG(FATAL) << "Not implemented"; +void CallbackVisitor::OnPushPromiseForStream( + Http2StreamId /*stream_id*/, Http2StreamId /*promised_stream_id*/) { + QUICHE_LOG(DFATAL) << "Not implemented"; } void CallbackVisitor::OnGoAway(Http2StreamId last_accepted_stream_id, @@ -173,42 +266,173 @@ void CallbackVisitor::OnGoAway(Http2StreamId last_accepted_stream_id, current_frame_.goaway.error_code = static_cast<uint32_t>(error_code); current_frame_.goaway.opaque_data = ToUint8Ptr(opaque_data.data()); current_frame_.goaway.opaque_data_len = opaque_data.size(); - callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + if (callbacks_->on_frame_recv_callback) { + callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + } } -void CallbackVisitor::OnWindowUpdate(Http2StreamId stream_id, +void CallbackVisitor::OnWindowUpdate(Http2StreamId /*stream_id*/, int window_increment) { current_frame_.window_update.window_size_increment = window_increment; - callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + if (callbacks_->on_frame_recv_callback) { + callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + } +} + +void CallbackVisitor::PopulateFrame(nghttp2_frame& frame, uint8_t frame_type, + Http2StreamId stream_id, size_t length, + uint8_t flags, uint32_t error_code, + bool sent_headers) { + frame.hd.type = frame_type; + frame.hd.stream_id = stream_id; + frame.hd.length = length; + frame.hd.flags = flags; + const FrameType frame_type_enum = static_cast<FrameType>(frame_type); + if (frame_type_enum == FrameType::HEADERS) { + if (sent_headers) { + frame.headers.cat = NGHTTP2_HCAT_HEADERS; + } else { + switch (perspective_) { + case Perspective::kClient: + QUICHE_VLOG(1) << "First headers sent by the client for stream " + << stream_id << "; these are request headers"; + frame.headers.cat = NGHTTP2_HCAT_REQUEST; + break; + case Perspective::kServer: + QUICHE_VLOG(1) << "First headers sent by the server for stream " + << stream_id << "; these are response headers"; + frame.headers.cat = NGHTTP2_HCAT_RESPONSE; + break; + } + } + } else if (frame_type_enum == FrameType::RST_STREAM) { + frame.rst_stream.error_code = error_code; + } else if (frame_type_enum == FrameType::GOAWAY) { + frame.goaway.error_code = error_code; + } } -void CallbackVisitor::OnReadyToSendDataForStream(Http2StreamId stream_id, - char* destination_buffer, - size_t length, - ssize_t* written, - bool* end_stream) { - QUICHE_LOG(FATAL) << "Not implemented"; +int CallbackVisitor::OnBeforeFrameSent(uint8_t frame_type, + Http2StreamId stream_id, size_t length, + uint8_t flags) { + if (callbacks_->before_frame_send_callback) { + QUICHE_VLOG(1) << "OnBeforeFrameSent(type=" << int(frame_type) + << ", stream_id=" << stream_id << ", length=" << length + << ", flags=" << int(flags) << ")"; + nghttp2_frame frame; + auto it = GetStreamInfo(stream_id); + // The implementation of the before_frame_send_callback doesn't look at the + // error code, so for now it's populated with 0. + PopulateFrame(frame, frame_type, stream_id, length, flags, /*error_code=*/0, + it->second->before_sent_headers); + it->second->before_sent_headers = true; + return callbacks_->before_frame_send_callback(nullptr, &frame, user_data_); + } + return 0; +} + +int CallbackVisitor::OnFrameSent(uint8_t frame_type, Http2StreamId stream_id, + size_t length, uint8_t flags, + uint32_t error_code) { + if (callbacks_->on_frame_send_callback) { + QUICHE_VLOG(1) << "OnFrameSent(type=" << int(frame_type) + << ", stream_id=" << stream_id << ", length=" << length + << ", flags=" << int(flags) << ", error_code=" << error_code + << ")"; + nghttp2_frame frame; + auto it = GetStreamInfo(stream_id); + PopulateFrame(frame, frame_type, stream_id, length, flags, error_code, + it->second->sent_headers); + it->second->sent_headers = true; + return callbacks_->on_frame_send_callback(nullptr, &frame, user_data_); + } + return 0; +} + +void CallbackVisitor::OnReadyToSendDataForStream(Http2StreamId /*stream_id*/, + char* /*destination_buffer*/, + size_t /*length*/, + ssize_t* /*written*/, + bool* /*end_stream*/) { + QUICHE_LOG(DFATAL) << "Not implemented"; +} + +bool CallbackVisitor::OnInvalidFrame(Http2StreamId stream_id, int error_code) { + QUICHE_VLOG(1) << "OnInvalidFrame(" << stream_id << ", " << error_code << ")"; + QUICHE_DCHECK_EQ(stream_id, current_frame_.hd.stream_id); + if (callbacks_->on_invalid_frame_recv_callback) { + return 0 == callbacks_->on_invalid_frame_recv_callback( + nullptr, ¤t_frame_, error_code, user_data_); + } + return true; } void CallbackVisitor::OnReadyToSendMetadataForStream(Http2StreamId stream_id, char* buffer, size_t length, ssize_t* written) { - QUICHE_LOG(FATAL) << "Not implemented"; + if (callbacks_->pack_extension_callback) { + nghttp2_frame frame; + frame.hd.type = kMetadataFrameType; + frame.hd.stream_id = stream_id; + frame.hd.flags = 0; + frame.hd.length = 0; + *written = callbacks_->pack_extension_callback(nullptr, ToUint8Ptr(buffer), + length, &frame, user_data_); + } + QUICHE_VLOG(1) << "OnReadyToSendMetadataForStream(stream_id=" << stream_id + << ", length=" << length << ", written=" << *written << ")"; } void CallbackVisitor::OnBeginMetadataForStream(Http2StreamId stream_id, size_t payload_length) { - QUICHE_LOG(FATAL) << "Not implemented"; + QUICHE_VLOG(1) << "OnBeginMetadataForStream(stream_id=" << stream_id + << ", payload_length=" << payload_length << ")"; } void CallbackVisitor::OnMetadataForStream(Http2StreamId stream_id, absl::string_view metadata) { - QUICHE_LOG(FATAL) << "Not implemented"; + QUICHE_VLOG(1) << "OnMetadataForStream(stream_id=" << stream_id + << ", len=" << metadata.size() << ")"; + if (callbacks_->on_extension_chunk_recv_callback) { + int result = callbacks_->on_extension_chunk_recv_callback( + nullptr, ¤t_frame_.hd, ToUint8Ptr(metadata.data()), + metadata.size(), user_data_); + QUICHE_DCHECK_EQ(0, result); + } +} + +bool CallbackVisitor::OnMetadataEndForStream(Http2StreamId stream_id) { + QUICHE_LOG_IF(DFATAL, current_frame_.hd.flags != kMetadataEndFlag); + QUICHE_VLOG(1) << "OnMetadataEndForStream(stream_id=" << stream_id << ")"; + if (callbacks_->unpack_extension_callback) { + void* payload; + int result = callbacks_->unpack_extension_callback( + nullptr, &payload, ¤t_frame_.hd, user_data_); + if (callbacks_->on_frame_recv_callback) { + current_frame_.ext.payload = payload; + callbacks_->on_frame_recv_callback(nullptr, ¤t_frame_, user_data_); + } + return (result == 0); + } + return true; } -void CallbackVisitor::OnMetadataEndForStream(Http2StreamId stream_id) { - QUICHE_LOG(FATAL) << "Not implemented"; +void CallbackVisitor::OnErrorDebug(absl::string_view message) { + if (callbacks_->error_callback2) { + callbacks_->error_callback2(nullptr, -1, message.data(), message.size(), + user_data_); + } +} + +CallbackVisitor::StreamInfoMap::iterator CallbackVisitor::GetStreamInfo( + Http2StreamId stream_id) { + auto it = stream_map_.find(stream_id); + if (it == stream_map_.end()) { + auto p = stream_map_.insert({stream_id, absl::make_unique<StreamInfo>()}); + it = p.first; + } + return it; } } // namespace adapter diff --git a/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor.h b/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor.h index 94746bf8edb..b5afc83a103 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor.h @@ -8,21 +8,20 @@ #include "http2/adapter/http2_visitor_interface.h" #include "http2/adapter/nghttp2_util.h" #include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" +#include "common/platform/api/quiche_export.h" namespace http2 { namespace adapter { // This visitor implementation accepts a set of nghttp2 callbacks and a "user // data" pointer, and invokes the callbacks according to HTTP/2 events received. -class CallbackVisitor : public Http2VisitorInterface { +class QUICHE_EXPORT_PRIVATE CallbackVisitor : public Http2VisitorInterface { public: explicit CallbackVisitor(Perspective perspective, - nghttp2_session_callbacks_unique_ptr callbacks, - void* user_data) - : perspective_(perspective), - callbacks_(std::move(callbacks)), - user_data_(user_data) {} + const nghttp2_session_callbacks& callbacks, + void* user_data); + ssize_t OnReadyToSend(absl::string_view serialized) override; void OnConnectionError() override; void OnFrameHeader(Http2StreamId stream_id, size_t length, @@ -32,10 +31,10 @@ class CallbackVisitor : public Http2VisitorInterface { void OnSetting(Http2Setting setting) override; void OnSettingsEnd() override; void OnSettingsAck() override; - void OnBeginHeadersForStream(Http2StreamId stream_id) override; - void OnHeaderForStream(Http2StreamId stream_id, - absl::string_view name, - absl::string_view value) override; + bool OnBeginHeadersForStream(Http2StreamId stream_id) override; + OnHeaderResult OnHeaderForStream(Http2StreamId stream_id, + absl::string_view name, + absl::string_view value) override; void OnEndHeadersForStream(Http2StreamId stream_id) override; void OnBeginDataForStream(Http2StreamId stream_id, size_t payload_length) override; @@ -56,11 +55,16 @@ class CallbackVisitor : public Http2VisitorInterface { Http2ErrorCode error_code, absl::string_view opaque_data) override; void OnWindowUpdate(Http2StreamId stream_id, int window_increment) override; + int OnBeforeFrameSent(uint8_t frame_type, Http2StreamId stream_id, + size_t length, uint8_t flags) override; + int OnFrameSent(uint8_t frame_type, Http2StreamId stream_id, size_t length, + uint8_t flags, uint32_t error_code) override; void OnReadyToSendDataForStream(Http2StreamId stream_id, char* destination_buffer, size_t length, ssize_t* written, bool* end_stream) override; + bool OnInvalidFrame(Http2StreamId stream_id, int error_code) override; void OnReadyToSendMetadataForStream(Http2StreamId stream_id, char* buffer, size_t length, @@ -69,9 +73,25 @@ class CallbackVisitor : public Http2VisitorInterface { size_t payload_length) override; void OnMetadataForStream(Http2StreamId stream_id, absl::string_view metadata) override; - void OnMetadataEndForStream(Http2StreamId stream_id) override; + bool OnMetadataEndForStream(Http2StreamId stream_id) override; + void OnErrorDebug(absl::string_view message) override; private: + struct QUICHE_EXPORT_PRIVATE StreamInfo { + bool before_sent_headers = false; + bool sent_headers = false; + bool received_headers = false; + }; + + using StreamInfoMap = + absl::flat_hash_map<Http2StreamId, std::unique_ptr<StreamInfo>>; + + void PopulateFrame(nghttp2_frame& frame, uint8_t frame_type, + Http2StreamId stream_id, size_t length, uint8_t flags, + uint32_t error_code, bool sent_headers); + // Creates the StreamInfoMap entry if it doesn't exist. + StreamInfoMap::iterator GetStreamInfo(Http2StreamId stream_id); + Perspective perspective_; nghttp2_session_callbacks_unique_ptr callbacks_; void* user_data_; @@ -80,10 +100,7 @@ class CallbackVisitor : public Http2VisitorInterface { std::vector<nghttp2_settings_entry> settings_; size_t remaining_data_ = 0; - struct StreamInfo { - bool received_headers = false; - }; - absl::flat_hash_map<Http2StreamId, std::unique_ptr<StreamInfo>> stream_map_; + StreamInfoMap stream_map_; }; } // namespace adapter diff --git a/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor_test.cc b/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor_test.cc index 4dfcb5fcddb..2cb31ed24f4 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor_test.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/callback_visitor_test.cc @@ -1,6 +1,7 @@ #include "http2/adapter/callback_visitor.h" #include "http2/adapter/mock_nghttp2_callbacks.h" +#include "http2/adapter/nghttp2_test_utils.h" #include "http2/adapter/test_utils.h" #include "common/platform/api/quiche_test.h" @@ -21,13 +22,14 @@ enum FrameType { PING, GOAWAY, WINDOW_UPDATE, + CONTINUATION, }; // Tests connection-level events. TEST(ClientCallbackVisitorUnitTest, ConnectionFrames) { testing::StrictMock<MockNghttp2Callbacks> callbacks; CallbackVisitor visitor(Perspective::kClient, - MockNghttp2Callbacks::GetCallbacks(), &callbacks); + *MockNghttp2Callbacks::GetCallbacks(), &callbacks); testing::InSequence seq; @@ -73,7 +75,7 @@ TEST(ClientCallbackVisitorUnitTest, ConnectionFrames) { TEST(ClientCallbackVisitorUnitTest, StreamFrames) { testing::StrictMock<MockNghttp2Callbacks> callbacks; CallbackVisitor visitor(Perspective::kClient, - MockNghttp2Callbacks::GetCallbacks(), &callbacks); + *MockNghttp2Callbacks::GetCallbacks(), &callbacks); testing::InSequence seq; @@ -153,12 +155,69 @@ TEST(ClientCallbackVisitorUnitTest, StreamFrames) { EXPECT_CALL(callbacks, OnStreamClose(5, NGHTTP2_REFUSED_STREAM)); visitor.OnCloseStream(5, Http2ErrorCode::REFUSED_STREAM); + + // Metadata events + constexpr size_t kMetadataBufferSize = 256; + char metadata_dest[kMetadataBufferSize]; + ssize_t written = 0; + + const absl::string_view kExampleFrame = "This is supposed to be metadata."; + EXPECT_CALL( + callbacks, + OnPackExtension(_, kMetadataBufferSize, + Field(&nghttp2_frame::hd, + HasFrameHeaderRef(7, kMetadataFrameType, _)))) + .WillOnce(testing::Invoke( + [kExampleFrame](uint8_t* buf, size_t /*len*/, + const nghttp2_frame* /*frame*/) -> ssize_t { + std::memcpy(buf, kExampleFrame.data(), kExampleFrame.size()); + return kExampleFrame.size(); + })); + visitor.OnReadyToSendMetadataForStream(7, metadata_dest, kMetadataBufferSize, + &written); + ASSERT_EQ(written, kExampleFrame.size()); + EXPECT_EQ(absl::string_view(metadata_dest, written), kExampleFrame); +} + +TEST(ClientCallbackVisitorUnitTest, HeadersWithContinuation) { + testing::StrictMock<MockNghttp2Callbacks> callbacks; + CallbackVisitor visitor(Perspective::kClient, + *MockNghttp2Callbacks::GetCallbacks(), &callbacks); + + testing::InSequence seq; + + // HEADERS on stream 1 + EXPECT_CALL(callbacks, OnBeginFrame(HasFrameHeader(1, HEADERS, 0x0))); + visitor.OnFrameHeader(1, 23, HEADERS, 0x0); + + EXPECT_CALL(callbacks, + OnBeginHeaders(IsHeaders(1, _, NGHTTP2_HCAT_RESPONSE))); + visitor.OnBeginHeadersForStream(1); + + EXPECT_CALL(callbacks, OnHeader(_, ":status", "200", _)); + visitor.OnHeaderForStream(1, ":status", "200"); + + EXPECT_CALL(callbacks, OnHeader(_, "server", "my-fake-server", _)); + visitor.OnHeaderForStream(1, "server", "my-fake-server"); + + EXPECT_CALL(callbacks, OnBeginFrame(HasFrameHeader(1, CONTINUATION, 0x4))); + visitor.OnFrameHeader(1, 23, CONTINUATION, 0x4); + + EXPECT_CALL(callbacks, + OnHeader(_, "date", "Tue, 6 Apr 2021 12:54:01 GMT", _)); + visitor.OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT"); + + EXPECT_CALL(callbacks, OnHeader(_, "trailer", "x-server-status", _)); + visitor.OnHeaderForStream(1, "trailer", "x-server-status"); + + EXPECT_CALL(callbacks, OnFrameRecv(IsHeaders(1, _, NGHTTP2_HCAT_RESPONSE))); + visitor.OnEndHeadersForStream(1); } TEST(ServerCallbackVisitorUnitTest, ConnectionFrames) { testing::StrictMock<MockNghttp2Callbacks> callbacks; CallbackVisitor visitor(Perspective::kServer, - MockNghttp2Callbacks::GetCallbacks(), &callbacks); + *MockNghttp2Callbacks::GetCallbacks(), &callbacks); testing::InSequence seq; @@ -196,7 +255,7 @@ TEST(ServerCallbackVisitorUnitTest, ConnectionFrames) { TEST(ServerCallbackVisitorUnitTest, StreamFrames) { testing::StrictMock<MockNghttp2Callbacks> callbacks; CallbackVisitor visitor(Perspective::kServer, - MockNghttp2Callbacks::GetCallbacks(), &callbacks); + *MockNghttp2Callbacks::GetCallbacks(), &callbacks); testing::InSequence seq; @@ -252,6 +311,28 @@ TEST(ServerCallbackVisitorUnitTest, StreamFrames) { EXPECT_CALL(callbacks, OnStreamClose(3, NGHTTP2_INTERNAL_ERROR)); visitor.OnCloseStream(3, Http2ErrorCode::INTERNAL_ERROR); + + // Metadata events + constexpr size_t kMetadataBufferSize = 256; + char metadata_dest[kMetadataBufferSize]; + ssize_t written = 0; + + const absl::string_view kExampleFrame = "This is supposed to be metadata."; + EXPECT_CALL( + callbacks, + OnPackExtension(_, kMetadataBufferSize, + Field(&nghttp2_frame::hd, + HasFrameHeaderRef(5, kMetadataFrameType, _)))) + .WillOnce(testing::Invoke( + [kExampleFrame](uint8_t* buf, size_t /*len*/, + const nghttp2_frame* /*frame*/) -> ssize_t { + std::memcpy(buf, kExampleFrame.data(), kExampleFrame.size()); + return kExampleFrame.size(); + })); + visitor.OnReadyToSendMetadataForStream(5, metadata_dest, kMetadataBufferSize, + &written); + ASSERT_EQ(written, kExampleFrame.size()); + EXPECT_EQ(absl::string_view(metadata_dest, written), kExampleFrame); } } // namespace diff --git a/chromium/net/third_party/quiche/src/http2/adapter/data_source.cc b/chromium/net/third_party/quiche/src/http2/adapter/data_source.cc deleted file mode 100644 index 13f89d28581..00000000000 --- a/chromium/net/third_party/quiche/src/http2/adapter/data_source.cc +++ /dev/null @@ -1,23 +0,0 @@ -#include "http2/adapter/data_source.h" - -namespace http2 { -namespace adapter { - -StringDataSource::StringDataSource(std::string data) - : data_(std::move(data)), remaining_(data_) { - state_ = remaining_.empty() ? DONE : READY; -} - -absl::string_view StringDataSource::NextData() const { - return remaining_; -} - -void StringDataSource::Consume(size_t bytes) { - remaining_.remove_prefix(std::min(bytes, remaining_.size())); - if (remaining_.empty()) { - state_ = DONE; - } -} - -} // namespace adapter -} // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/data_source.h b/chromium/net/third_party/quiche/src/http2/adapter/data_source.h index e170104a597..d86714148c3 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/data_source.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/data_source.h @@ -2,49 +2,46 @@ #define QUICHE_HTTP2_ADAPTER_DATA_SOURCE_H_ #include <string> +#include <utility> #include "absl/strings/string_view.h" +#include "common/platform/api/quiche_export.h" namespace http2 { namespace adapter { -// Represents a HTTP message body. -class DataSource { +// Represents a source of DATA frames for transmission to the peer. +class QUICHE_EXPORT_PRIVATE DataFrameSource { public: - virtual ~DataSource() {} + virtual ~DataFrameSource() {} - enum State { - // The source is not done, but cannot currently provide more data. - NOT_READY, - // The source can provide more data. - READY, - // The source is done. - DONE, - }; + enum : ssize_t { kBlocked = 0, kError = -1 }; - State state() const { return state_; } + // Returns the number of bytes to send in the next DATA frame, and whether + // this frame indicates the end of the data. Returns {kBlocked, false} if + // blocked, {kError, false} on error. + virtual std::pair<ssize_t, bool> SelectPayloadLength(size_t max_length) = 0; - // The next range of data provided by this data source. - virtual absl::string_view NextData() const = 0; + // This method is called with a frame header and a payload length to send. The + // source should send or buffer the entire frame and return true, or return + // false without sending or buffering anything. + virtual bool Send(absl::string_view frame_header, size_t payload_length) = 0; - // Indicates that |bytes| bytes have been consumed by the caller. - virtual void Consume(size_t bytes) = 0; - - protected: - State state_ = NOT_READY; + // If true, the end of this data source indicates the end of the stream. + // Otherwise, this data will be followed by trailers. + virtual bool send_fin() const = 0; }; -// A simple implementation constructible from a string_view or std::string. -class StringDataSource : public DataSource { +// Represents a source of metadata frames for transmission to the peer. +class QUICHE_EXPORT_PRIVATE MetadataSource { public: - explicit StringDataSource(std::string data); - - absl::string_view NextData() const override; - void Consume(size_t bytes) override; + virtual ~MetadataSource() {} - private: - const std::string data_; - absl::string_view remaining_; + // This method is called with a destination buffer and length. It should + // return the number of payload bytes copied to |dest|, or a negative integer + // to indicate an error, as well as a boolean indicating whether the metadata + // has been completely copied. + virtual std::pair<ssize_t, bool> Pack(uint8_t* dest, size_t dest_len) = 0; }; } // namespace adapter diff --git a/chromium/net/third_party/quiche/src/http2/adapter/data_source_test.cc b/chromium/net/third_party/quiche/src/http2/adapter/data_source_test.cc deleted file mode 100644 index c290124b430..00000000000 --- a/chromium/net/third_party/quiche/src/http2/adapter/data_source_test.cc +++ /dev/null @@ -1,40 +0,0 @@ -#include "http2/adapter/data_source.h" - -#include "common/platform/api/quiche_test.h" - -namespace http2 { -namespace adapter { -namespace test { -namespace { - -TEST(StringDataSourceTest, EmptyString) { - StringDataSource source(""); - - EXPECT_EQ(source.state(), DataSource::DONE); - EXPECT_THAT(source.NextData(), testing::IsEmpty()); -} - -TEST(StringDataSourceTest, PartialConsume) { - StringDataSource source("I'm a HTTP message body. Really!"); - - EXPECT_EQ(source.state(), DataSource::READY); - EXPECT_THAT(source.NextData(), testing::Not(testing::IsEmpty())); - source.Consume(6); - EXPECT_EQ(source.state(), DataSource::READY); - EXPECT_THAT(source.NextData(), testing::StartsWith("HTTP")); - - source.Consume(0); - EXPECT_EQ(source.state(), DataSource::READY); - EXPECT_THAT(source.NextData(), testing::StartsWith("HTTP")); - - // Consumes more than the remaining bytes available. - source.Consume(50); - EXPECT_THAT(source.NextData(), testing::IsEmpty()) - << "next data: " << source.NextData(); - EXPECT_EQ(source.state(), DataSource::DONE); -} - -} // namespace -} // namespace test -} // namespace adapter -} // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/http2_adapter.h b/chromium/net/third_party/quiche/src/http2/adapter/http2_adapter.h index ef2ae0a5baf..789be98ae3b 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/http2_adapter.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/http2_adapter.h @@ -4,9 +4,11 @@ #include "absl/strings/string_view.h" #include "absl/types/optional.h" #include "absl/types/span.h" +#include "http2/adapter/data_source.h" #include "http2/adapter/http2_protocol.h" #include "http2/adapter/http2_session.h" #include "http2/adapter/http2_visitor_interface.h" +#include "common/platform/api/quiche_export.h" namespace http2 { namespace adapter { @@ -17,11 +19,13 @@ namespace adapter { // invokes corresponding callbacks on its passed-in Http2VisitorInterface. // Http2Adapter is a base class shared between client-side and server-side // implementations. -class Http2Adapter { +class QUICHE_EXPORT_PRIVATE Http2Adapter { public: Http2Adapter(const Http2Adapter&) = delete; Http2Adapter& operator=(const Http2Adapter&) = delete; + virtual bool IsServerSession() const = 0; + // Processes the incoming |bytes| as HTTP/2 and invokes callbacks on the // |visitor_| as appropriate. virtual ssize_t ProcessBytes(absl::string_view bytes) = 0; @@ -36,17 +40,15 @@ class Http2Adapter { int weight, bool exclusive) = 0; - // Submits a PING on the connection. Note that nghttp2 automatically submits - // PING acks upon receiving non-ack PINGs from the peer, so callers only use - // this method to originate PINGs. See nghttp2_option_set_no_auto_ping_ack(). + // Submits a PING on the connection. virtual void SubmitPing(Http2PingId ping_id) = 0; + // Starts a graceful shutdown. A no-op for clients. + virtual void SubmitShutdownNotice() = 0; + // Submits a GOAWAY on the connection. Note that |last_accepted_stream_id| - // refers to stream IDs initiated by the peer. For client-side, this last - // stream ID must be even (or 0); for server-side, this last stream ID must be - // odd (or 0). To submit a GOAWAY with |last_accepted_stream_id| with the - // maximum stream ID, signaling imminent connection termination, call - // SubmitShutdownNotice() instead (though this is only possible server-side). + // refers to stream IDs initiated by the peer. For a server sending this + // frame, this last stream ID must be odd (or 0). virtual void SubmitGoAway(Http2StreamId last_accepted_stream_id, Http2ErrorCode error_code, absl::string_view opaque_data) = 0; @@ -56,27 +58,85 @@ class Http2Adapter { virtual void SubmitWindowUpdate(Http2StreamId stream_id, int window_increment) = 0; - // Submits a METADATA frame for the given stream (a |stream_id| of 0 indicates - // connection-level METADATA). If |fin|, the frame will also have the - // END_METADATA flag set. - virtual void SubmitMetadata(Http2StreamId stream_id, bool fin) = 0; + // Submits a RST_STREAM for the given |stream_id| and |error_code|. + virtual void SubmitRst(Http2StreamId stream_id, + Http2ErrorCode error_code) = 0; + + // Submits a sequence of METADATA frames for the given stream. A |stream_id| + // of 0 indicates connection-level METADATA. + virtual void SubmitMetadata(Http2StreamId stream_id, + std::unique_ptr<MetadataSource> source) = 0; + + // Invokes the visitor's OnReadyToSend() method for serialized frame data. + // Returns 0 on success. + virtual int Send() = 0; + + // Returns the connection-level flow control window advertised by the peer. + virtual int GetSendWindowSize() const = 0; - // Returns serialized bytes for writing to the wire. - // Writes should be submitted to Http2Adapter first, so that Http2Adapter - // has data to serialize and return in this method. - virtual std::string GetBytesToWrite(absl::optional<size_t> max_bytes) = 0; + // Returns the stream-level flow control window advertised by the peer. + virtual int GetStreamSendWindowSize(Http2StreamId stream_id) const = 0; - // Returns the connection-level flow control window for the peer. - virtual int GetPeerConnectionWindow() const = 0; + // Returns the current upper bound on the flow control receive window for this + // stream. This value does not account for data received from the peer. + virtual int GetStreamReceiveWindowLimit(Http2StreamId stream_id) const = 0; + + // Returns the amount of data a peer could send on a given stream. This is + // the outstanding stream receive window. + virtual int GetStreamReceiveWindowSize(Http2StreamId stream_id) const = 0; + + // Returns the total amount of data a peer could send on the connection. This + // is the outstanding connection receive window. + virtual int GetReceiveWindowSize() const = 0; + + // Returns the size of the HPACK encoder's dynamic table, including the + // per-entry overhead from the specification. + virtual int GetHpackEncoderDynamicTableSize() const = 0; + + // Returns the size of the HPACK decoder's dynamic table, including the + // per-entry overhead from the specification. + virtual int GetHpackDecoderDynamicTableSize() const = 0; + + // Gets the highest stream ID value seen in a frame received by this endpoint. + // This method is only guaranteed to work for server endpoints. + virtual Http2StreamId GetHighestReceivedStreamId() const = 0; // Marks the given amount of data as consumed for the given stream, which - // enables the nghttp2 layer to trigger WINDOW_UPDATEs as appropriate. + // enables the implementation layer to send WINDOW_UPDATEs as appropriate. virtual void MarkDataConsumedForStream(Http2StreamId stream_id, size_t num_bytes) = 0; - // Submits a RST_STREAM for the given stream. - virtual void SubmitRst(Http2StreamId stream_id, - Http2ErrorCode error_code) = 0; + // Returns the assigned stream ID if the operation succeeds. Otherwise, + // returns a negative integer indicating an error code. |data_source| may be + // nullptr if the request does not have a body. + virtual int32_t SubmitRequest(absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source, + void* user_data) = 0; + + // Returns 0 on success. |data_source| may be nullptr if the response does not + // have a body. + virtual int SubmitResponse(Http2StreamId stream_id, + absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source) = 0; + + // Queues trailers to be sent after any outstanding data on the stream with ID + // |stream_id|. Returns 0 on success. + virtual int SubmitTrailer(Http2StreamId stream_id, + absl::Span<const Header> trailers) = 0; + + // Sets a user data pointer for the given stream. Can be called after + // SubmitRequest/SubmitResponse, or after receiving any frame for a given + // stream. + virtual void SetStreamUserData(Http2StreamId stream_id, void* user_data) = 0; + + // Returns nullptr if the stream does not exist, or if stream user data has + // not been set. + virtual void* GetStreamUserData(Http2StreamId stream_id) = 0; + + // Resumes a stream that was previously blocked (for example, due to + // DataFrameSource::SelectPayloadLength() returning kBlocked). Returns true if + // the stream was successfully resumed. + virtual bool ResumeStream(Http2StreamId stream_id) = 0; protected: // Subclasses should expose a public factory method for constructing and diff --git a/chromium/net/third_party/quiche/src/http2/adapter/http2_protocol.cc b/chromium/net/third_party/quiche/src/http2/adapter/http2_protocol.cc index 8d59aeedda0..dc0a57acac5 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/http2_protocol.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/http2_protocol.cc @@ -12,6 +12,10 @@ const char kHttp2AuthorityPseudoHeader[] = ":authority"; const char kHttp2PathPseudoHeader[] = ":path"; const char kHttp2StatusPseudoHeader[] = ":status"; +const uint8_t kMetadataFrameType = 0x4d; +const uint8_t kMetadataEndFlag = 0x04; +const uint16_t kMetadataExtensionId = 0x4d44; + std::pair<absl::string_view, bool> GetStringView(const HeaderRep& rep) { if (absl::holds_alternative<absl::string_view>(rep)) { return std::make_pair(absl::get<absl::string_view>(rep), true); diff --git a/chromium/net/third_party/quiche/src/http2/adapter/http2_protocol.h b/chromium/net/third_party/quiche/src/http2/adapter/http2_protocol.h index 1e1dd39a21a..9f72fbe9d05 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/http2_protocol.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/http2_protocol.h @@ -5,10 +5,10 @@ #include <string> #include <utility> -#include "base/integral_types.h" #include "absl/base/attributes.h" #include "absl/strings/string_view.h" #include "absl/types/variant.h" +#include "common/platform/api/quiche_export.h" namespace http2 { namespace adapter { @@ -34,7 +34,7 @@ std::pair<absl::string_view, bool> GetStringView(const HeaderRep& rep); using Header = std::pair<HeaderRep, HeaderRep>; // Represents an HTTP/2 SETTINGS key-value parameter. -struct Http2Setting { +struct QUICHE_EXPORT_PRIVATE Http2Setting { Http2SettingsId id; uint32_t value; }; @@ -50,17 +50,40 @@ const Http2StreamId kConnectionStreamId = 0; // 7540 Section 6.5.2 (SETTINGS_MAX_FRAME_SIZE). const int kDefaultFramePayloadSizeLimit = 16 * 1024; -// The default value for the initial stream flow control window size, according -// to RFC 7540 Section 6.9.2. -const int kDefaultInitialStreamWindowSize = 64 * 1024 - 1; +// The default value for the initial stream and connection flow control window +// size, according to RFC 7540 Section 6.9.2. +const int kInitialFlowControlWindowSize = 64 * 1024 - 1; // The pseudo-header fields as specified in RFC 7540 Section 8.1.2.3 (request) // and Section 8.1.2.4 (response). -ABSL_CONST_INIT extern const char kHttp2MethodPseudoHeader[]; -ABSL_CONST_INIT extern const char kHttp2SchemePseudoHeader[]; -ABSL_CONST_INIT extern const char kHttp2AuthorityPseudoHeader[]; -ABSL_CONST_INIT extern const char kHttp2PathPseudoHeader[]; -ABSL_CONST_INIT extern const char kHttp2StatusPseudoHeader[]; +ABSL_CONST_INIT QUICHE_EXPORT_PRIVATE extern const char + kHttp2MethodPseudoHeader[]; +ABSL_CONST_INIT QUICHE_EXPORT_PRIVATE extern const char + kHttp2SchemePseudoHeader[]; +ABSL_CONST_INIT QUICHE_EXPORT_PRIVATE extern const char + kHttp2AuthorityPseudoHeader[]; +ABSL_CONST_INIT QUICHE_EXPORT_PRIVATE extern const char + kHttp2PathPseudoHeader[]; +ABSL_CONST_INIT QUICHE_EXPORT_PRIVATE extern const char + kHttp2StatusPseudoHeader[]; + +ABSL_CONST_INIT QUICHE_EXPORT_PRIVATE extern const uint8_t kMetadataFrameType; +ABSL_CONST_INIT QUICHE_EXPORT_PRIVATE extern const uint8_t kMetadataEndFlag; +ABSL_CONST_INIT QUICHE_EXPORT_PRIVATE extern const uint16_t + kMetadataExtensionId; + +enum class FrameType : uint8_t { + DATA = 0x0, + HEADERS, + PRIORITY, + RST_STREAM, + SETTINGS, + PUSH_PROMISE, + PING, + GOAWAY, + WINDOW_UPDATE, + CONTINUATION, +}; // HTTP/2 error codes as specified in RFC 7540 Section 7. enum class Http2ErrorCode { @@ -94,7 +117,8 @@ enum Http2KnownSettingsId : Http2SettingsId { INITIAL_WINDOW_SIZE = 0x4, MAX_FRAME_SIZE = 0x5, MAX_HEADER_LIST_SIZE = 0x6, - MAX_SETTING = MAX_HEADER_LIST_SIZE + ENABLE_CONNECT_PROTOCOL = 0x8, // See RFC 8441 + MAX_SETTING = ENABLE_CONNECT_PROTOCOL }; // Returns a human-readable string representation of the given SETTINGS |id| for diff --git a/chromium/net/third_party/quiche/src/http2/adapter/http2_session.h b/chromium/net/third_party/quiche/src/http2/adapter/http2_session.h index 0a6321c0ccc..3b57d8f1aba 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/http2_session.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/http2_session.h @@ -5,14 +5,15 @@ #include "absl/strings/string_view.h" #include "http2/adapter/http2_protocol.h" +#include "common/platform/api/quiche_export.h" namespace http2 { namespace adapter { -struct Http2SessionCallbacks {}; +struct QUICHE_EXPORT_PRIVATE Http2SessionCallbacks {}; // A class to represent the state of a single HTTP/2 connection. -class Http2Session { +class QUICHE_EXPORT_PRIVATE Http2Session { public: Http2Session() = default; virtual ~Http2Session() {} diff --git a/chromium/net/third_party/quiche/src/http2/adapter/http2_util.h b/chromium/net/third_party/quiche/src/http2/adapter/http2_util.h index 3ace28b2ce9..88e9a49e6e4 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/http2_util.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/http2_util.h @@ -2,13 +2,16 @@ #define QUICHE_HTTP2_ADAPTER_HTTP2_UTIL_H_ #include "http2/adapter/http2_protocol.h" +#include "common/platform/api/quiche_export.h" #include "spdy/core/spdy_protocol.h" namespace http2 { namespace adapter { -spdy::SpdyErrorCode TranslateErrorCode(Http2ErrorCode code); -Http2ErrorCode TranslateErrorCode(spdy::SpdyErrorCode code); +QUICHE_EXPORT_PRIVATE spdy::SpdyErrorCode TranslateErrorCode( + Http2ErrorCode code); +QUICHE_EXPORT_PRIVATE Http2ErrorCode +TranslateErrorCode(spdy::SpdyErrorCode code); } // namespace adapter } // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/http2_visitor_interface.h b/chromium/net/third_party/quiche/src/http2/adapter/http2_visitor_interface.h index 12729e75651..292021a23f4 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/http2_visitor_interface.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/http2_visitor_interface.h @@ -5,6 +5,7 @@ #include "absl/strings/string_view.h" #include "http2/adapter/http2_protocol.h" +#include "common/platform/api/quiche_export.h" namespace http2 { namespace adapter { @@ -44,20 +45,24 @@ namespace adapter { // - OnCloseStream() // // More details are at RFC 7540 (go/http2spec). -class Http2VisitorInterface { +class QUICHE_EXPORT_PRIVATE Http2VisitorInterface { public: Http2VisitorInterface(const Http2VisitorInterface&) = delete; Http2VisitorInterface& operator=(const Http2VisitorInterface&) = delete; virtual ~Http2VisitorInterface() = default; + static const ssize_t kSendBlocked = 0; + static const ssize_t kSendError = -1; + // Called when there are serialized frames to send. Should return how many + // bytes were actually sent. May return kSendBlocked or kSendError. + virtual ssize_t OnReadyToSend(absl::string_view serialized) = 0; + // Called when a connection-level processing error has been encountered. virtual void OnConnectionError() = 0; // Called when the header for a frame is received. - virtual void OnFrameHeader(Http2StreamId stream_id, - size_t length, - uint8_t type, - uint8_t flags) {} + virtual void OnFrameHeader(Http2StreamId /*stream_id*/, size_t /*length*/, + uint8_t /*type*/, uint8_t /*flags*/) {} // Called when a non-ack SETTINGS frame is received. virtual void OnSettingsStart() = 0; @@ -72,15 +77,28 @@ class Http2VisitorInterface { virtual void OnSettingsAck() = 0; // Called when the connection receives the header block for a HEADERS frame on - // a stream but has not yet parsed individual headers. - virtual void OnBeginHeadersForStream(Http2StreamId stream_id) = 0; + // a stream but has not yet parsed individual headers. Returns false if a + // fatal error has occurred. + virtual bool OnBeginHeadersForStream(Http2StreamId stream_id) = 0; // Called when the connection receives the header |key| and |value| for a // stream. The HTTP/2 pseudo-headers defined in RFC 7540 Sections 8.1.2.3 and // 8.1.2.4 are also conveyed in this callback. This method is called after - // OnBeginHeadersForStream(). - virtual void OnHeaderForStream(Http2StreamId stream_id, absl::string_view key, - absl::string_view value) = 0; + // OnBeginHeadersForStream(). May return HEADER_RST_STREAM to indicate the + // header block should be rejected. This will cause the library to queue a + // RST_STREAM frame, which will have a default error code of INTERNAL_ERROR. + // The visitor implementation may choose to queue a RST_STREAM with a + // different error code instead, which should be done before returning + // HEADER_RST_STREAM. Returning HEADER_CONNECTION_ERROR will lead to a + // non-recoverable error on the connection. + enum OnHeaderResult { + HEADER_OK, + HEADER_CONNECTION_ERROR, + HEADER_RST_STREAM, + }; + virtual OnHeaderResult OnHeaderForStream(Http2StreamId stream_id, + absl::string_view key, + absl::string_view value) = 0; // Called when the connection has received the complete header block for a // logical HEADERS frame on a stream (which may contain CONTINUATION frames, @@ -133,6 +151,18 @@ class Http2VisitorInterface { virtual void OnWindowUpdate(Http2StreamId stream_id, int window_increment) = 0; + // Called immediately before a frame of the given type is sent. Should return + // 0 on success. + virtual int OnBeforeFrameSent(uint8_t frame_type, Http2StreamId stream_id, + size_t length, uint8_t flags) = 0; + + // Called immediately after a frame of the given type is sent. Should return 0 + // on success. |error_code| is only populated for RST_STREAM and GOAWAY frame + // types. + virtual int OnFrameSent(uint8_t frame_type, Http2StreamId stream_id, + size_t length, uint8_t flags, + uint32_t error_code) = 0; + // Called when the connection is ready to send data for a stream. The // implementation should write at most |length| bytes of the data payload to // the |destination_buffer| and set |end_stream| to true IFF there will be no @@ -144,6 +174,12 @@ class Http2VisitorInterface { ssize_t* written, bool* end_stream) = 0; + // Called when the connection receives an invalid frame. |error_code| is a + // negative integer error code generated by the library. A return value of + // false will result in the connection entering an error state, with no + // further frame processing possible. + virtual bool OnInvalidFrame(Http2StreamId stream_id, int error_code) = 0; + // Called when the connection is ready to write metadata for |stream_id| to // the wire. The implementation should write at most |length| bytes of the // serialized metadata payload to the |buffer| and set |written| to the number @@ -155,6 +191,7 @@ class Http2VisitorInterface { // Called when the connection receives the beginning of a METADATA frame // (which may itself be the middle of a logical metadata block). The metadata // payload will be provided via subsequent calls to OnMetadataForStream(). + // TODO(birenroy): Consider removing this unnecessary method. virtual void OnBeginMetadataForStream(Http2StreamId stream_id, size_t payload_length) = 0; @@ -165,7 +202,11 @@ class Http2VisitorInterface { // Called when the connection has finished receiving a logical metadata block // for a stream. Note that there may be multiple metadata blocks for a stream. - virtual void OnMetadataEndForStream(Http2StreamId stream_id) = 0; + // Returns false if there was an error unpacking the metadata payload. + virtual bool OnMetadataEndForStream(Http2StreamId stream_id) = 0; + + // Invoked with an error message from the application. + virtual void OnErrorDebug(absl::string_view message) = 0; protected: Http2VisitorInterface() = default; diff --git a/chromium/net/third_party/quiche/src/http2/adapter/mock_http2_visitor.h b/chromium/net/third_party/quiche/src/http2/adapter/mock_http2_visitor.h index 171d40f443f..3daa8053962 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/mock_http2_visitor.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/mock_http2_visitor.h @@ -2,6 +2,7 @@ #define QUICHE_HTTP2_ADAPTER_MOCK_HTTP2_VISITOR_INTERFACE_H_ #include "http2/adapter/http2_visitor_interface.h" +#include "common/platform/api/quiche_export.h" #include "common/platform/api/quiche_test.h" namespace http2 { @@ -9,10 +10,20 @@ namespace adapter { namespace test { // A mock visitor class, for use in tests. -class MockHttp2Visitor : public Http2VisitorInterface { +class QUICHE_NO_EXPORT MockHttp2Visitor : public Http2VisitorInterface { public: - MockHttp2Visitor() = default; - + MockHttp2Visitor() { + ON_CALL(*this, OnBeginHeadersForStream) + .WillByDefault(testing::Return(true)); + ON_CALL(*this, OnHeaderForStream).WillByDefault(testing::Return(HEADER_OK)); + ON_CALL(*this, OnInvalidFrame).WillByDefault(testing::Return(true)); + ON_CALL(*this, OnMetadataEndForStream).WillByDefault(testing::Return(true)); + } + + MOCK_METHOD(ssize_t, + OnReadyToSend, + (absl::string_view serialized), + (override)); MOCK_METHOD(void, OnConnectionError, (), (override)); MOCK_METHOD( void, @@ -23,15 +34,11 @@ class MockHttp2Visitor : public Http2VisitorInterface { MOCK_METHOD(void, OnSetting, (Http2Setting setting), (override)); MOCK_METHOD(void, OnSettingsEnd, (), (override)); MOCK_METHOD(void, OnSettingsAck, (), (override)); - MOCK_METHOD(void, - OnBeginHeadersForStream, - (Http2StreamId stream_id), + MOCK_METHOD(bool, OnBeginHeadersForStream, (Http2StreamId stream_id), (override)); - MOCK_METHOD(void, - OnHeaderForStream, - (Http2StreamId stream_id, - absl::string_view key, + MOCK_METHOD(OnHeaderResult, OnHeaderForStream, + (Http2StreamId stream_id, absl::string_view key, absl::string_view value), (override)); @@ -89,6 +96,19 @@ class MockHttp2Visitor : public Http2VisitorInterface { (Http2StreamId stream_id, int window_increment), (override)); + MOCK_METHOD(int, OnBeforeFrameSent, + (uint8_t frame_type, Http2StreamId stream_id, size_t length, + uint8_t flags), + (override)); + + MOCK_METHOD(int, OnFrameSent, + (uint8_t frame_type, Http2StreamId stream_id, size_t length, + uint8_t flags, uint32_t error_code), + (override)); + + MOCK_METHOD(bool, OnInvalidFrame, (Http2StreamId stream_id, int error_code), + (override)); + MOCK_METHOD(void, OnReadyToSendDataForStream, (Http2StreamId stream_id, @@ -114,10 +134,10 @@ class MockHttp2Visitor : public Http2VisitorInterface { (Http2StreamId stream_id, absl::string_view metadata), (override)); - MOCK_METHOD(void, - OnMetadataEndForStream, - (Http2StreamId stream_id), + MOCK_METHOD(bool, OnMetadataEndForStream, (Http2StreamId stream_id), (override)); + + MOCK_METHOD(void, OnErrorDebug, (absl::string_view message), (override)); }; } // namespace test diff --git a/chromium/net/third_party/quiche/src/http2/adapter/mock_nghttp2_callbacks.cc b/chromium/net/third_party/quiche/src/http2/adapter/mock_nghttp2_callbacks.cc index 4699a371add..4243347e758 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/mock_nghttp2_callbacks.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/mock_nghttp2_callbacks.cc @@ -109,12 +109,19 @@ nghttp2_session_callbacks_unique_ptr MockNghttp2Callbacks::GetCallbacks() { nghttp2_session_callbacks_set_error_callback2( callbacks, - [](nghttp2_session* session, int lib_error_code, const char* msg, + [](nghttp2_session* /*session*/, int lib_error_code, const char* msg, size_t len, void* user_data) -> int { return static_cast<MockNghttp2Callbacks*>(user_data)->OnErrorCallback2( lib_error_code, msg, len); }); + nghttp2_session_callbacks_set_pack_extension_callback( + callbacks, + [](nghttp2_session*, uint8_t* buf, size_t len, const nghttp2_frame* frame, + void* user_data) -> ssize_t { + return static_cast<MockNghttp2Callbacks*>(user_data)->OnPackExtension( + buf, len, frame); + }); return MakeCallbacksPtr(callbacks); } diff --git a/chromium/net/third_party/quiche/src/http2/adapter/mock_nghttp2_callbacks.h b/chromium/net/third_party/quiche/src/http2/adapter/mock_nghttp2_callbacks.h index e2794575c8d..08d15be7d7d 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/mock_nghttp2_callbacks.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/mock_nghttp2_callbacks.h @@ -4,6 +4,7 @@ #include "absl/strings/string_view.h" #include "http2/adapter/nghttp2_util.h" #include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" +#include "common/platform/api/quiche_export.h" #include "common/platform/api/quiche_test.h" namespace http2 { @@ -12,7 +13,7 @@ namespace test { // This class provides a set of mock nghttp2 callbacks for use in unit test // expectations. -class MockNghttp2Callbacks { +class QUICHE_NO_EXPORT MockNghttp2Callbacks { public: MockNghttp2Callbacks() = default; @@ -71,6 +72,9 @@ class MockNghttp2Callbacks { OnErrorCallback2, (int lib_error_code, const char* msg, size_t len), ()); + + MOCK_METHOD(ssize_t, OnPackExtension, + (uint8_t * buf, size_t len, const nghttp2_frame* frame), ()); }; } // namespace test diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter.cc index 878c040ef72..9b444a3243d 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter.cc @@ -4,6 +4,7 @@ #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "http2/adapter/nghttp2_callbacks.h" +#include "http2/adapter/nghttp2_data_provider.h" #include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" #include "common/platform/api/quiche_logging.h" #include "common/quiche_endian.h" @@ -13,20 +14,26 @@ namespace adapter { /* static */ std::unique_ptr<NgHttp2Adapter> NgHttp2Adapter::CreateClientAdapter( - Http2VisitorInterface& visitor) { - auto adapter = new NgHttp2Adapter(visitor, Perspective::kClient); + Http2VisitorInterface& visitor, const nghttp2_option* options) { + auto adapter = new NgHttp2Adapter(visitor, Perspective::kClient, options); adapter->Initialize(); return absl::WrapUnique(adapter); } /* static */ std::unique_ptr<NgHttp2Adapter> NgHttp2Adapter::CreateServerAdapter( - Http2VisitorInterface& visitor) { - auto adapter = new NgHttp2Adapter(visitor, Perspective::kServer); + Http2VisitorInterface& visitor, const nghttp2_option* options) { + auto adapter = new NgHttp2Adapter(visitor, Perspective::kServer, options); adapter->Initialize(); return absl::WrapUnique(adapter); } +bool NgHttp2Adapter::IsServerSession() const { + int result = nghttp2_session_check_server_session(session_->raw_ptr()); + QUICHE_DCHECK_EQ(perspective_ == Perspective::kServer, result > 0); + return result > 0; +} + ssize_t NgHttp2Adapter::ProcessBytes(absl::string_view bytes) { const ssize_t processed_bytes = session_->ProcessBytes(bytes); if (processed_bytes < 0) { @@ -64,6 +71,10 @@ void NgHttp2Adapter::SubmitPing(Http2PingId ping_id) { nghttp2_submit_ping(session_->raw_ptr(), NGHTTP2_FLAG_NONE, opaque_data); } +void NgHttp2Adapter::SubmitShutdownNotice() { + nghttp2_submit_shutdown_notice(session_->raw_ptr()); +} + void NgHttp2Adapter::SubmitGoAway(Http2StreamId last_accepted_stream_id, Http2ErrorCode error_code, absl::string_view opaque_data) { @@ -79,32 +90,55 @@ void NgHttp2Adapter::SubmitWindowUpdate(Http2StreamId stream_id, stream_id, window_increment); } -void NgHttp2Adapter::SubmitMetadata(Http2StreamId stream_id, - bool end_metadata) { +void NgHttp2Adapter::SubmitMetadata( + Http2StreamId /*stream_id*/, std::unique_ptr<MetadataSource> /*source*/) { QUICHE_LOG(DFATAL) << "Not implemented"; } -std::string NgHttp2Adapter::GetBytesToWrite(absl::optional<size_t> max_bytes) { - ssize_t num_bytes = 0; - std::string result; - do { - const uint8_t* data = nullptr; - num_bytes = nghttp2_session_mem_send(session_->raw_ptr(), &data); - if (num_bytes > 0) { - absl::StrAppend( - &result, - absl::string_view(reinterpret_cast<const char*>(data), num_bytes)); - } else if (num_bytes < 0) { - visitor_.OnConnectionError(); - } - } while (num_bytes > 0); +int NgHttp2Adapter::Send() { + const int result = nghttp2_session_send(session_->raw_ptr()); + if (result != 0) { + QUICHE_VLOG(1) << "nghttp2_session_send returned " << result; + visitor_.OnConnectionError(); + } return result; } -int NgHttp2Adapter::GetPeerConnectionWindow() const { +int NgHttp2Adapter::GetSendWindowSize() const { return session_->GetRemoteWindowSize(); } +int NgHttp2Adapter::GetStreamSendWindowSize(Http2StreamId stream_id) const { + return nghttp2_session_get_stream_remote_window_size(session_->raw_ptr(), + stream_id); +} + +int NgHttp2Adapter::GetStreamReceiveWindowLimit(Http2StreamId stream_id) const { + return nghttp2_session_get_stream_effective_local_window_size( + session_->raw_ptr(), stream_id); +} + +int NgHttp2Adapter::GetStreamReceiveWindowSize(Http2StreamId stream_id) const { + return nghttp2_session_get_stream_local_window_size(session_->raw_ptr(), + stream_id); +} + +int NgHttp2Adapter::GetReceiveWindowSize() const { + return nghttp2_session_get_local_window_size(session_->raw_ptr()); +} + +int NgHttp2Adapter::GetHpackEncoderDynamicTableSize() const { + return nghttp2_session_get_hd_deflate_dynamic_table_size(session_->raw_ptr()); +} + +int NgHttp2Adapter::GetHpackDecoderDynamicTableSize() const { + return nghttp2_session_get_hd_inflate_dynamic_table_size(session_->raw_ptr()); +} + +Http2StreamId NgHttp2Adapter::GetHighestReceivedStreamId() const { + return nghttp2_session_get_last_proc_stream_id(session_->raw_ptr()); +} + void NgHttp2Adapter::MarkDataConsumedForStream(Http2StreamId stream_id, size_t num_bytes) { int rc = session_->Consume(stream_id, num_bytes); @@ -125,24 +159,96 @@ void NgHttp2Adapter::SubmitRst(Http2StreamId stream_id, } } +int32_t NgHttp2Adapter::SubmitRequest( + absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source, void* stream_user_data) { + auto nvs = GetNghttp2Nvs(headers); + std::unique_ptr<nghttp2_data_provider> provider = + MakeDataProvider(data_source.get()); + + int32_t stream_id = + nghttp2_submit_request(session_->raw_ptr(), nullptr, nvs.data(), + nvs.size(), provider.get(), stream_user_data); + // TODO(birenroy): clean up data source on stream close + sources_.emplace(stream_id, std::move(data_source)); + QUICHE_VLOG(1) << "Submitted request with " << nvs.size() + << " request headers and user data " << stream_user_data + << "; resulted in stream " << stream_id; + return stream_id; +} + +int NgHttp2Adapter::SubmitResponse( + Http2StreamId stream_id, absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source) { + auto nvs = GetNghttp2Nvs(headers); + std::unique_ptr<nghttp2_data_provider> provider = + MakeDataProvider(data_source.get()); + + // TODO(birenroy): clean up data source on stream close + sources_.emplace(stream_id, std::move(data_source)); + + int result = nghttp2_submit_response(session_->raw_ptr(), stream_id, + nvs.data(), nvs.size(), provider.get()); + QUICHE_VLOG(1) << "Submitted response with " << nvs.size() + << " response headers; result = " << result; + return result; +} + +int NgHttp2Adapter::SubmitTrailer(Http2StreamId stream_id, + absl::Span<const Header> trailers) { + auto nvs = GetNghttp2Nvs(trailers); + int result = nghttp2_submit_trailer(session_->raw_ptr(), stream_id, + nvs.data(), nvs.size()); + QUICHE_VLOG(1) << "Submitted trailers with " << nvs.size() + << " response trailers; result = " << result; + return result; +} + +void NgHttp2Adapter::SetStreamUserData(Http2StreamId stream_id, + void* stream_user_data) { + nghttp2_session_set_stream_user_data(session_->raw_ptr(), stream_id, + stream_user_data); +} + +void* NgHttp2Adapter::GetStreamUserData(Http2StreamId stream_id) { + return nghttp2_session_get_stream_user_data(session_->raw_ptr(), stream_id); +} + +bool NgHttp2Adapter::ResumeStream(Http2StreamId stream_id) { + return 0 == nghttp2_session_resume_data(session_->raw_ptr(), stream_id); +} + NgHttp2Adapter::NgHttp2Adapter(Http2VisitorInterface& visitor, - Perspective perspective) - : Http2Adapter(visitor), visitor_(visitor), perspective_(perspective) {} + Perspective perspective, + const nghttp2_option* options) + : Http2Adapter(visitor), + visitor_(visitor), + options_(options), + perspective_(perspective) {} NgHttp2Adapter::~NgHttp2Adapter() {} void NgHttp2Adapter::Initialize() { - nghttp2_option* options; - nghttp2_option_new(&options); - // Set some common options for compatibility. - nghttp2_option_set_no_closed_streams(options, 1); - nghttp2_option_set_no_auto_window_update(options, 1); - nghttp2_option_set_max_send_header_block_length(options, 0x2000000); - nghttp2_option_set_max_outbound_ack(options, 10000); - - session_ = - absl::make_unique<NgHttp2Session>(perspective_, callbacks::Create(), - options, static_cast<void*>(&visitor_)); + nghttp2_option* owned_options = nullptr; + if (options_ == nullptr) { + nghttp2_option_new(&owned_options); + // Set some common options for compatibility. + nghttp2_option_set_no_closed_streams(owned_options, 1); + nghttp2_option_set_no_auto_window_update(owned_options, 1); + nghttp2_option_set_max_send_header_block_length(owned_options, 0x2000000); + nghttp2_option_set_max_outbound_ack(owned_options, 10000); + nghttp2_option_set_user_recv_extension_type(owned_options, + kMetadataFrameType); + options_ = owned_options; + } + + session_ = absl::make_unique<NgHttp2Session>(perspective_, + callbacks::Create(), options_, + static_cast<void*>(&visitor_)); + if (owned_options != nullptr) { + nghttp2_option_del(owned_options); + } + options_ = nullptr; } } // namespace adapter diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter.h b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter.h index 13c2ffcc3f4..151f9a7ae5a 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter.h @@ -1,35 +1,34 @@ #ifndef QUICHE_HTTP2_ADAPTER_NGHTTP2_ADAPTER_H_ #define QUICHE_HTTP2_ADAPTER_NGHTTP2_ADAPTER_H_ +#include "absl/container/flat_hash_map.h" #include "http2/adapter/http2_adapter.h" #include "http2/adapter/http2_protocol.h" #include "http2/adapter/nghttp2_session.h" #include "http2/adapter/nghttp2_util.h" +#include "common/platform/api/quiche_export.h" namespace http2 { namespace adapter { -class NgHttp2Adapter : public Http2Adapter { +class QUICHE_EXPORT_PRIVATE NgHttp2Adapter : public Http2Adapter { public: ~NgHttp2Adapter() override; - // Creates an adapter that functions as a client. + // Creates an adapter that functions as a client. Does not take ownership of + // |options|. static std::unique_ptr<NgHttp2Adapter> CreateClientAdapter( - Http2VisitorInterface& visitor); + Http2VisitorInterface& visitor, const nghttp2_option* options = nullptr); - // Creates an adapter that functions as a server. + // Creates an adapter that functions as a server. Does not take ownership of + // |options|. static std::unique_ptr<NgHttp2Adapter> CreateServerAdapter( - Http2VisitorInterface& visitor); + Http2VisitorInterface& visitor, const nghttp2_option* options = nullptr); - // Processes the incoming |bytes| as HTTP/2 and invokes callbacks on the - // |visitor_| as appropriate. - ssize_t ProcessBytes(absl::string_view bytes) override; + bool IsServerSession() const override; - // Submits the |settings| to be written to the peer, e.g., as part of the - // HTTP/2 connection preface. + ssize_t ProcessBytes(absl::string_view bytes) override; void SubmitSettings(absl::Span<const Http2Setting> settings) override; - - // Submits a PRIORITY frame for the given stream. void SubmitPriorityForStream(Http2StreamId stream_id, Http2StreamId parent_stream_id, int weight, @@ -40,47 +39,58 @@ class NgHttp2Adapter : public Http2Adapter { // this method to originate PINGs. See nghttp2_option_set_no_auto_ping_ack(). void SubmitPing(Http2PingId ping_id) override; - // Submits a GOAWAY on the connection. Note that |last_accepted_stream_id| - // refers to stream IDs initiated by the peer. For client-side, this last - // stream ID must be even (or 0); for server-side, this last stream ID must be - // odd (or 0). - // TODO(birenroy): Add a graceful shutdown behavior to the API. + void SubmitShutdownNotice() override; void SubmitGoAway(Http2StreamId last_accepted_stream_id, Http2ErrorCode error_code, absl::string_view opaque_data) override; - // Submits a WINDOW_UPDATE for the given stream (a |stream_id| of 0 indicates - // a connection-level WINDOW_UPDATE). void SubmitWindowUpdate(Http2StreamId stream_id, int window_increment) override; - // Submits a METADATA frame for the given stream (a |stream_id| of 0 indicates - // connection-level METADATA). If |end_metadata|, the frame will also have the - // END_METADATA flag set. - void SubmitMetadata(Http2StreamId stream_id, bool end_metadata) override; + void SubmitRst(Http2StreamId stream_id, Http2ErrorCode error_code) override; + + void SubmitMetadata(Http2StreamId stream_id, + std::unique_ptr<MetadataSource> source) override; + + int Send() override; + + int GetSendWindowSize() const override; + int GetStreamSendWindowSize(Http2StreamId stream_id) const override; - // Returns serialized bytes for writing to the wire. Writes should be - // submitted to Nghttp2Adapter first, so that Nghttp2Adapter has data to - // serialize and return in this method. - std::string GetBytesToWrite(absl::optional<size_t> max_bytes) override; + int GetStreamReceiveWindowLimit(Http2StreamId stream_id) const override; + int GetStreamReceiveWindowSize(Http2StreamId stream_id) const override; + int GetReceiveWindowSize() const override; - // Returns the connection-level flow control window for the peer. - int GetPeerConnectionWindow() const override; + int GetHpackEncoderDynamicTableSize() const override; + int GetHpackDecoderDynamicTableSize() const override; + + Http2StreamId GetHighestReceivedStreamId() const override; - // Marks the given amount of data as consumed for the given stream, which - // enables the nghttp2 layer to trigger WINDOW_UPDATEs as appropriate. void MarkDataConsumedForStream(Http2StreamId stream_id, size_t num_bytes) override; - // Submits a RST_STREAM with the desired |error_code|. - void SubmitRst(Http2StreamId stream_id, Http2ErrorCode error_code) override; + int32_t SubmitRequest(absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source, + void* user_data) override; + + int SubmitResponse(Http2StreamId stream_id, absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source) override; + + int SubmitTrailer(Http2StreamId stream_id, + absl::Span<const Header> trailers) override; + + void SetStreamUserData(Http2StreamId stream_id, void* user_data) override; + void* GetStreamUserData(Http2StreamId stream_id) override; + + bool ResumeStream(Http2StreamId stream_id) override; // TODO(b/181586191): Temporary accessor until equivalent functionality is // available in this adapter class. NgHttp2Session& session() { return *session_; } private: - NgHttp2Adapter(Http2VisitorInterface& visitor, Perspective perspective); + NgHttp2Adapter(Http2VisitorInterface& visitor, Perspective perspective, + const nghttp2_option* options); // Performs any necessary initialization of the underlying HTTP/2 session, // such as preparing initial SETTINGS. @@ -88,7 +98,10 @@ class NgHttp2Adapter : public Http2Adapter { std::unique_ptr<NgHttp2Session> session_; Http2VisitorInterface& visitor_; + const nghttp2_option* options_; Perspective perspective_; + + absl::flat_hash_map<int32_t, std::unique_ptr<DataFrameSource>> sources_; }; } // namespace adapter diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter_test.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter_test.cc index 3aec115dbeb..785e476c94d 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter_test.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_adapter_test.cc @@ -1,6 +1,7 @@ #include "http2/adapter/nghttp2_adapter.h" #include "http2/adapter/mock_http2_visitor.h" +#include "http2/adapter/nghttp2_test_utils.h" #include "http2/adapter/test_frame_sequence.h" #include "http2/adapter/test_utils.h" #include "common/platform/api/quiche_test.h" @@ -22,21 +23,46 @@ enum FrameType { PING, GOAWAY, WINDOW_UPDATE, + CONTINUATION, }; +// This send callback assumes |source|'s pointer is a TestDataSource, and +// |user_data| is a Http2VisitorInterface. +int TestSendCallback(nghttp2_session*, nghttp2_frame* /*frame*/, + const uint8_t* framehd, size_t length, + nghttp2_data_source* source, void* user_data) { + auto* visitor = static_cast<Http2VisitorInterface*>(user_data); + // Send the frame header via the visitor. + ssize_t result = visitor->OnReadyToSend(ToStringView(framehd, 9)); + if (result == 0) { + return NGHTTP2_ERR_WOULDBLOCK; + } + auto* test_source = static_cast<TestDataSource*>(source->ptr); + absl::string_view payload = test_source->ReadNext(length); + // Send the frame payload via the visitor. + visitor->OnReadyToSend(payload); + return 0; +} + TEST(NgHttp2AdapterTest, ClientConstruction) { testing::StrictMock<MockHttp2Visitor> visitor; auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); ASSERT_NE(nullptr, adapter); EXPECT_TRUE(adapter->session().want_read()); EXPECT_FALSE(adapter->session().want_write()); + EXPECT_FALSE(adapter->IsServerSession()); } TEST(NgHttp2AdapterTest, ClientHandlesFrames) { - testing::StrictMock<MockHttp2Visitor> visitor; + DataSavingVisitor visitor; auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); - std::string serialized = adapter->GetBytesToWrite(absl::nullopt); - EXPECT_THAT(serialized, testing::StrEq(spdy::kHttp2ConnectionHeaderPrefix)); + int result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), + testing::StrEq(spdy::kHttp2ConnectionHeaderPrefix)); + visitor.Clear(); + + EXPECT_EQ(0, adapter->GetHighestReceivedStreamId()); const std::string initial_frames = TestFrameSequence() .ServerPreface() @@ -58,56 +84,95 @@ TEST(NgHttp2AdapterTest, ClientHandlesFrames) { const ssize_t initial_result = adapter->ProcessBytes(initial_frames); EXPECT_EQ(initial_frames.size(), initial_result); - EXPECT_EQ(adapter->GetPeerConnectionWindow(), - kDefaultInitialStreamWindowSize + 1000); + EXPECT_EQ(adapter->GetSendWindowSize(), kInitialFlowControlWindowSize + 1000); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(PING, 0, 8, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(PING, 0, 8, 0x1, 0)); + + result = adapter->Send(); // Some bytes should have been serialized. - serialized = adapter->GetBytesToWrite(absl::nullopt); - EXPECT_THAT(serialized, EqualsFrames({spdy::SpdyFrameType::SETTINGS, - spdy::SpdyFrameType::PING})); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::PING})); + visitor.Clear(); const std::vector<const Header> headers1 = ToHeaders({{":method", "GET"}, {":scheme", "http"}, {":authority", "example.com"}, {":path", "/this/is/request/one"}}); - const auto nvs1 = GetNghttp2Nvs(headers1); const std::vector<const Header> headers2 = ToHeaders({{":method", "GET"}, {":scheme", "http"}, {":authority", "example.com"}, {":path", "/this/is/request/two"}}); - const auto nvs2 = GetNghttp2Nvs(headers2); const std::vector<const Header> headers3 = ToHeaders({{":method", "GET"}, {":scheme", "http"}, {":authority", "example.com"}, {":path", "/this/is/request/three"}}); - const auto nvs3 = GetNghttp2Nvs(headers3); + const char* kSentinel1 = "arbitrary pointer 1"; + const char* kSentinel3 = "arbitrary pointer 3"; const int32_t stream_id1 = - nghttp2_submit_request(adapter->session().raw_ptr(), nullptr, nvs1.data(), - nvs1.size(), nullptr, nullptr); + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); ASSERT_GT(stream_id1, 0); QUICHE_LOG(INFO) << "Created stream: " << stream_id1; - const int32_t stream_id2 = - nghttp2_submit_request(adapter->session().raw_ptr(), nullptr, nvs2.data(), - nvs2.size(), nullptr, nullptr); + const int32_t stream_id2 = adapter->SubmitRequest(headers2, nullptr, nullptr); ASSERT_GT(stream_id2, 0); QUICHE_LOG(INFO) << "Created stream: " << stream_id2; const int32_t stream_id3 = - nghttp2_submit_request(adapter->session().raw_ptr(), nullptr, nvs3.data(), - nvs3.size(), nullptr, nullptr); + adapter->SubmitRequest(headers3, nullptr, const_cast<char*>(kSentinel3)); ASSERT_GT(stream_id3, 0); QUICHE_LOG(INFO) << "Created stream: " << stream_id3; - serialized = adapter->GetBytesToWrite(absl::nullopt); - EXPECT_THAT(serialized, EqualsFrames({spdy::SpdyFrameType::HEADERS, - spdy::SpdyFrameType::HEADERS, - spdy::SpdyFrameType::HEADERS})); + const char* kSentinel2 = "arbitrary pointer 2"; + adapter->SetStreamUserData(stream_id2, const_cast<char*>(kSentinel2)); + adapter->SetStreamUserData(stream_id3, nullptr); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id2, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id2, _, 0x5, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id3, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id3, _, 0x5, 0)); + + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS, + spdy::SpdyFrameType::HEADERS, + spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + // All streams are active and have not yet received any data, so the receive + // window should be at the initial value. + EXPECT_EQ(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowSize(stream_id1)); + EXPECT_EQ(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowSize(stream_id2)); + EXPECT_EQ(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowSize(stream_id3)); + + // Upper bound on the flow control receive window should be the initial value. + EXPECT_EQ(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowLimit(stream_id1)); + + // Connection has not yet received any data. + EXPECT_EQ(kInitialFlowControlWindowSize, adapter->GetReceiveWindowSize()); + + EXPECT_EQ(0, adapter->GetHighestReceivedStreamId()); + + EXPECT_EQ(kSentinel1, adapter->GetStreamUserData(stream_id1)); + EXPECT_EQ(kSentinel2, adapter->GetStreamUserData(stream_id2)); + EXPECT_EQ(nullptr, adapter->GetStreamUserData(stream_id3)); + + EXPECT_EQ(0, adapter->GetHpackDecoderDynamicTableSize()); const std::string stream_frames = TestFrameSequence() @@ -140,6 +205,29 @@ TEST(NgHttp2AdapterTest, ClientHandlesFrames) { const ssize_t stream_result = adapter->ProcessBytes(stream_frames); EXPECT_EQ(stream_frames.size(), stream_result); + // First stream has received some data. + EXPECT_GT(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowSize(stream_id1)); + // Second stream was closed. + EXPECT_EQ(-1, adapter->GetStreamReceiveWindowSize(stream_id2)); + // Third stream has not received any data. + EXPECT_EQ(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowSize(stream_id3)); + + // Connection window should be the same as the first stream. + EXPECT_EQ(adapter->GetReceiveWindowSize(), + adapter->GetStreamReceiveWindowSize(stream_id1)); + + // Upper bound on the flow control receive window should still be the initial + // value. + EXPECT_EQ(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowLimit(stream_id1)); + + EXPECT_GT(adapter->GetHpackDecoderDynamicTableSize(), 0); + + // Should be 3, but this method only works for server adapters. + EXPECT_EQ(0, adapter->GetHighestReceivedStreamId()); + // Even though the client recieved a GOAWAY, streams 1 and 5 are still active. EXPECT_TRUE(adapter->session().want_read()); @@ -154,14 +242,801 @@ TEST(NgHttp2AdapterTest, ClientHandlesFrames) { .Data(1, "", true) .RstStream(5, Http2ErrorCode::REFUSED_STREAM) .Serialize()); + + // Should be 5, but this method only works for server adapters. + EXPECT_EQ(0, adapter->GetHighestReceivedStreamId()); + // After receiving END_STREAM for 1 and RST_STREAM for 5, the session no // longer expects reads. EXPECT_FALSE(adapter->session().want_read()); // Client will not have anything else to write. EXPECT_FALSE(adapter->session().want_write()); - serialized = adapter->GetBytesToWrite(absl::nullopt); - EXPECT_THAT(serialized, testing::IsEmpty()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), testing::IsEmpty()); +} + +TEST(NgHttp2AdapterTest, ClientHandlesTrailers) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .Headers(1, {{"final-status", "A-OK"}}, + /*fin=*/true) + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, + OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnFrameHeader(1, 26, DATA, 0)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, 26)); + EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body.")); + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, "final-status", "A-OK")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + + const ssize_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); +} + +TEST(NgHttp2AdapterTest, ClientHandlesMetadata) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Metadata(0, "Example connection metadata") + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Metadata(1, "Example stream metadata") + .Data(1, "This is the response body.", true) + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + 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)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, + 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)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, 26)); + EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body.")); + EXPECT_CALL(visitor, OnEndStream(1)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + + const ssize_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); +} + +TEST(NgHttp2AdapterTest, ClientHandlesInvalidTrailers) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .Headers(1, {{":bad-status", "9000"}}, + /*fin=*/true) + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, + OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnFrameHeader(1, 26, DATA, 0)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, 26)); + EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body.")); + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL( + visitor, + OnErrorDebug("Invalid HTTP header field was received: frame type: 1, " + "stream: 1, name: [:bad-status], value: [9000]")); + EXPECT_CALL(visitor, OnInvalidFrame(1, -531)); + + // Bad status trailer will cause a PROTOCOL_ERROR. The header is never + // delivered in an OnHeaderForStream callback. + + const ssize_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, stream_id1, 4, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(RST_STREAM, stream_id1, 4, 0x0, 1)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::PROTOCOL_ERROR)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::RST_STREAM})); +} + +TEST(NgHttp2AdapterTest, ClientRstStreamWhileHandlingHeaders) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, + OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT")) + .WillOnce(testing::DoAll( + testing::InvokeWithoutArgs([&adapter]() { + adapter->SubmitRst(1, Http2ErrorCode::REFUSED_STREAM); + }), + testing::Return(Http2VisitorInterface::HEADER_RST_STREAM))); + + const ssize_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, stream_id1, 4, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, stream_id1, 4, 0x0, + static_cast<int>(Http2ErrorCode::REFUSED_STREAM))); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::REFUSED_STREAM)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::RST_STREAM})); +} + +TEST(NgHttp2AdapterTest, ClientConnectionErrorWhileHandlingHeaders) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, + OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT")) + .WillOnce( + testing::Return(Http2VisitorInterface::HEADER_CONNECTION_ERROR)); + EXPECT_CALL(visitor, OnConnectionError()); + + const ssize_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(-902 /* NGHTTP2_ERR_CALLBACK_FAILURE */, stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); +} + +TEST(NgHttp2AdapterTest, ClientRejectsHeaders) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)) + .WillOnce(testing::Return(false)); + // Rejecting headers leads to a connection error. + EXPECT_CALL(visitor, OnConnectionError()); + + const ssize_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(NGHTTP2_ERR_CALLBACK_FAILURE, stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); +} + +TEST(NgHttp2AdapterTest, ClientSubmitRequest) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + int result = adapter->Send(); + EXPECT_EQ(0, result); + // Client preface does not appear to include the mandatory SETTINGS frame. + EXPECT_THAT(visitor.data(), + testing::StrEq(spdy::kHttp2ConnectionHeaderPrefix)); + visitor.Clear(); + + const std::string initial_frames = + TestFrameSequence().ServerPreface().Serialize(); + testing::InSequence s; + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + const ssize_t initial_result = adapter->ProcessBytes(initial_frames); + EXPECT_EQ(initial_frames.size(), initial_result); + + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); + visitor.Clear(); + + EXPECT_EQ(0, adapter->GetHpackEncoderDynamicTableSize()); + EXPECT_FALSE(adapter->session().want_write()); + const char* kSentinel = ""; + const absl::string_view kBody = "This is an example request body."; + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, true); + body1->AppendPayload(kBody); + body1->EndData(); + int stream_id = + adapter->SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + std::move(body1), const_cast<char*>(kSentinel)); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, stream_id, _, 0x1, 0)); + + result = adapter->Send(); + EXPECT_EQ(0, result); + + EXPECT_EQ(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowSize(stream_id)); + EXPECT_EQ(kInitialFlowControlWindowSize, adapter->GetReceiveWindowSize()); + EXPECT_EQ(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowLimit(stream_id)); + + EXPECT_GT(adapter->GetHpackEncoderDynamicTableSize(), 0); + + // Some data was sent, so the remaining send window size should be less than + // the default. + EXPECT_LT(adapter->GetStreamSendWindowSize(stream_id), + kInitialFlowControlWindowSize); + EXPECT_GT(adapter->GetStreamSendWindowSize(stream_id), 0); + // Send window for a nonexistent stream is not available. + EXPECT_EQ(-1, adapter->GetStreamSendWindowSize(stream_id + 2)); + + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS, + spdy::SpdyFrameType::DATA})); + EXPECT_THAT(visitor.data(), testing::HasSubstr(kBody)); + visitor.Clear(); + EXPECT_FALSE(adapter->session().want_write()); + + stream_id = + adapter->SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + nullptr, nullptr); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(adapter->session().want_write()); + const char* kSentinel2 = "arbitrary pointer 2"; + EXPECT_EQ(nullptr, adapter->GetStreamUserData(stream_id)); + adapter->SetStreamUserData(stream_id, const_cast<char*>(kSentinel2)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x5, 0)); + + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS})); + + EXPECT_EQ(kSentinel2, adapter->GetStreamUserData(stream_id)); + + // No data was sent (just HEADERS), so the remaining send window size should + // still be the default. + EXPECT_EQ(adapter->GetStreamSendWindowSize(stream_id), + kInitialFlowControlWindowSize); +} + +// This is really a test of the MakeZeroCopyDataFrameSource adapter, but I +// wasn't sure where else to put it. +TEST(NgHttp2AdapterTest, ClientSubmitRequestWithDataProvider) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + int result = adapter->Send(); + EXPECT_EQ(0, result); + // Client preface does not appear to include the mandatory SETTINGS frame. + EXPECT_THAT(visitor.data(), + testing::StrEq(spdy::kHttp2ConnectionHeaderPrefix)); + visitor.Clear(); + + const std::string initial_frames = + TestFrameSequence().ServerPreface().Serialize(); + testing::InSequence s; + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + const ssize_t initial_result = adapter->ProcessBytes(initial_frames); + EXPECT_EQ(initial_frames.size(), initial_result); + + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); + visitor.Clear(); + + EXPECT_FALSE(adapter->session().want_write()); + const absl::string_view kBody = "This is an example request body."; + // This test will use TestDataSource as the source of the body payload data. + TestDataSource body1{kBody}; + // The TestDataSource is wrapped in the nghttp2_data_provider data type. + nghttp2_data_provider provider = body1.MakeDataProvider(); + nghttp2_send_data_callback send_callback = &TestSendCallback; + + // This call transforms it back into a DataFrameSource, which is compatible + // with the Http2Adapter API. + std::unique_ptr<DataFrameSource> frame_source = + MakeZeroCopyDataFrameSource(provider, &visitor, std::move(send_callback)); + int stream_id = + adapter->SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + std::move(frame_source), nullptr); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, stream_id, _, 0x1, 0)); + + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS, + spdy::SpdyFrameType::DATA})); + EXPECT_THAT(visitor.data(), testing::HasSubstr(kBody)); + EXPECT_FALSE(adapter->session().want_write()); +} + +// This test verifies how nghttp2 behaves when a data source becomes +// read-blocked. +TEST(NgHttp2AdapterTest, ClientSubmitRequestWithDataProviderAndReadBlock) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + + const absl::string_view kBody = "This is an example request body."; + // This test will use TestDataSource as the source of the body payload data. + TestDataSource body1{kBody}; + body1.set_is_data_available(false); + // The TestDataSource is wrapped in the nghttp2_data_provider data type. + nghttp2_data_provider provider = body1.MakeDataProvider(); + nghttp2_send_data_callback send_callback = &TestSendCallback; + + // This call transforms it back into a DataFrameSource, which is compatible + // with the Http2Adapter API. + std::unique_ptr<DataFrameSource> frame_source = + MakeZeroCopyDataFrameSource(provider, &visitor, std::move(send_callback)); + int stream_id = + adapter->SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + std::move(frame_source), nullptr); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x4, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + // Client preface does not appear to include the mandatory SETTINGS frame. + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(serialized, EqualsFrames({spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + EXPECT_FALSE(adapter->session().want_write()); + + // Resume the deferred stream. + body1.set_is_data_available(true); + EXPECT_TRUE(adapter->ResumeStream(stream_id)); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnFrameSent(DATA, stream_id, _, 0x1, 0)); + + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::DATA})); + EXPECT_FALSE(adapter->session().want_write()); + + // Stream data is done, so this stream cannot be resumed. + EXPECT_FALSE(adapter->ResumeStream(stream_id)); + EXPECT_FALSE(adapter->session().want_write()); +} + +// This test verifies how nghttp2 behaves when a data source is read block, then +// ends with an empty DATA frame. +TEST(NgHttp2AdapterTest, ClientSubmitRequestEmptyDataWithFin) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + + const absl::string_view kEmptyBody = ""; + // This test will use TestDataSource as the source of the body payload data. + TestDataSource body1{kEmptyBody}; + body1.set_is_data_available(false); + // The TestDataSource is wrapped in the nghttp2_data_provider data type. + nghttp2_data_provider provider = body1.MakeDataProvider(); + nghttp2_send_data_callback send_callback = &TestSendCallback; + + // This call transforms it back into a DataFrameSource, which is compatible + // with the Http2Adapter API. + std::unique_ptr<DataFrameSource> frame_source = + MakeZeroCopyDataFrameSource(provider, &visitor, std::move(send_callback)); + int stream_id = + adapter->SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + std::move(frame_source), nullptr); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x4, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + // Client preface does not appear to include the mandatory SETTINGS frame. + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(serialized, EqualsFrames({spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + EXPECT_FALSE(adapter->session().want_write()); + + // Resume the deferred stream. + body1.set_is_data_available(true); + EXPECT_TRUE(adapter->ResumeStream(stream_id)); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnFrameSent(DATA, stream_id, 0, 0x1, 0)); + + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::DATA})); + EXPECT_FALSE(adapter->session().want_write()); + + // Stream data is done, so this stream cannot be resumed. + EXPECT_FALSE(adapter->ResumeStream(stream_id)); + EXPECT_FALSE(adapter->session().want_write()); +} + +// This test verifies how nghttp2 behaves when a connection becomes +// write-blocked. +TEST(NgHttp2AdapterTest, ClientSubmitRequestWithDataProviderAndWriteBlock) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateClientAdapter(visitor); + + const absl::string_view kBody = "This is an example request body."; + // This test will use TestDataSource as the source of the body payload data. + TestDataSource body1{kBody}; + // The TestDataSource is wrapped in the nghttp2_data_provider data type. + nghttp2_data_provider provider = body1.MakeDataProvider(); + nghttp2_send_data_callback send_callback = &TestSendCallback; + + // This call transforms it back into a DataFrameSource, which is compatible + // with the Http2Adapter API. + std::unique_ptr<DataFrameSource> frame_source = + MakeZeroCopyDataFrameSource(provider, &visitor, std::move(send_callback)); + int stream_id = + adapter->SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + std::move(frame_source), nullptr); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(adapter->session().want_write()); + + visitor.set_is_write_blocked(true); + int result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), testing::IsEmpty()); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, stream_id, _, 0x1, 0)); + + visitor.set_is_write_blocked(false); + result = adapter->Send(); + EXPECT_EQ(0, result); + + // Client preface does not appear to include the mandatory SETTINGS frame. + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(serialized, EqualsFrames({spdy::SpdyFrameType::HEADERS, + spdy::SpdyFrameType::DATA})); + EXPECT_FALSE(adapter->session().want_write()); } TEST(NgHttp2AdapterTest, ServerConstruction) { @@ -170,12 +1045,16 @@ TEST(NgHttp2AdapterTest, ServerConstruction) { ASSERT_NE(nullptr, adapter); EXPECT_TRUE(adapter->session().want_read()); EXPECT_FALSE(adapter->session().want_write()); + EXPECT_TRUE(adapter->IsServerSession()); } TEST(NgHttp2AdapterTest, ServerHandlesFrames) { - testing::StrictMock<MockHttp2Visitor> visitor; + DataSavingVisitor visitor; auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor); + EXPECT_EQ(0, adapter->GetHighestReceivedStreamId()); + EXPECT_EQ(0, adapter->GetHpackDecoderDynamicTableSize()); + const std::string frames = TestFrameSequence() .ClientPreface() .Ping(42) @@ -199,6 +1078,8 @@ TEST(NgHttp2AdapterTest, ServerHandlesFrames) { .Serialize(); testing::InSequence s; + const char* kSentinel1 = "arbitrary pointer 1"; + // Client preface (empty SETTINGS) EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); EXPECT_CALL(visitor, OnSettingsStart()); @@ -214,7 +1095,10 @@ TEST(NgHttp2AdapterTest, ServerHandlesFrames) { EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); - EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)) + .WillOnce(testing::InvokeWithoutArgs([&adapter, kSentinel1]() { + adapter->SetStreamUserData(1, const_cast<char*>(kSentinel1)); + })); EXPECT_CALL(visitor, OnFrameHeader(1, 4, WINDOW_UPDATE, 0)); EXPECT_CALL(visitor, OnWindowUpdate(1, 2000)); EXPECT_CALL(visitor, OnFrameHeader(1, 25, DATA, 0)); @@ -237,16 +1121,506 @@ TEST(NgHttp2AdapterTest, ServerHandlesFrames) { const ssize_t result = adapter->ProcessBytes(frames); EXPECT_EQ(frames.size(), result); - EXPECT_EQ(adapter->GetPeerConnectionWindow(), - kDefaultInitialStreamWindowSize + 1000); + EXPECT_EQ(kSentinel1, adapter->GetStreamUserData(1)); + + EXPECT_GT(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowSize(1)); + EXPECT_EQ(adapter->GetStreamReceiveWindowSize(1), + adapter->GetReceiveWindowSize()); + // Upper bound should still be the original value. + EXPECT_EQ(kInitialFlowControlWindowSize, + adapter->GetStreamReceiveWindowLimit(1)); + + EXPECT_GT(adapter->GetHpackDecoderDynamicTableSize(), 0); + + // Because stream 3 has already been closed, it's not possible to set user + // data. + const char* kSentinel3 = "another arbitrary pointer"; + adapter->SetStreamUserData(3, const_cast<char*>(kSentinel3)); + EXPECT_EQ(nullptr, adapter->GetStreamUserData(3)); + + EXPECT_EQ(3, adapter->GetHighestReceivedStreamId()); + + EXPECT_EQ(adapter->GetSendWindowSize(), kInitialFlowControlWindowSize + 1000); EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(PING, 0, 8, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(PING, 0, 8, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(PING, 0, 8, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(PING, 0, 8, 0x1, 0)); + + int send_result = adapter->Send(); // Some bytes should have been serialized. - std::string serialized = adapter->GetBytesToWrite(absl::nullopt); + EXPECT_EQ(0, send_result); // SETTINGS ack, two PING acks. - EXPECT_THAT(serialized, EqualsFrames({spdy::SpdyFrameType::SETTINGS, - spdy::SpdyFrameType::PING, - spdy::SpdyFrameType::PING})); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::PING, + spdy::SpdyFrameType::PING})); +} + +TEST(NgHttp2AdapterTest, ServerErrorWhileHandlingHeaders) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "POST"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}, + {"accept", "some bogus value!"}}, + /*fin=*/false) + .WindowUpdate(1, 2000) + .Data(1, "This is the request body.") + .WindowUpdate(0, 2000) + .Serialize(); + testing::InSequence s; + + // Client preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "accept", "some bogus value!")) + .WillOnce(testing::Return(Http2VisitorInterface::HEADER_RST_STREAM)); + EXPECT_CALL(visitor, OnFrameHeader(1, 4, WINDOW_UPDATE, 0)); + EXPECT_CALL(visitor, OnWindowUpdate(1, 2000)); + // DATA frame is not delivered to the visitor. + EXPECT_CALL(visitor, OnFrameHeader(0, 4, WINDOW_UPDATE, 0)); + EXPECT_CALL(visitor, OnWindowUpdate(0, 2000)); + + const ssize_t result = adapter->ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, 1, 4, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, 1, 4, 0x0, + static_cast<int>(Http2ErrorCode::INTERNAL_ERROR))); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::INTERNAL_ERROR)); + + int send_result = adapter->Send(); + // Some bytes should have been serialized. + EXPECT_EQ(0, send_result); + // SETTINGS ack + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::RST_STREAM})); +} + +TEST(NgHttp2AdapterTest, ServerSubmitResponse) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor); + EXPECT_FALSE(adapter->session().want_write()); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "GET"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}, + /*fin=*/true) + .Serialize(); + testing::InSequence s; + + const char* kSentinel1 = "arbitrary pointer 1"; + + // Client preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + // Stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)) + .WillOnce(testing::InvokeWithoutArgs([&adapter, kSentinel1]() { + adapter->SetStreamUserData(1, const_cast<char*>(kSentinel1)); + })); + EXPECT_CALL(visitor, OnEndStream(1)); + + const ssize_t result = adapter->ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + EXPECT_EQ(1, adapter->GetHighestReceivedStreamId()); + + // Server will want to send a SETTINGS ack. + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + int send_result = adapter->Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); + visitor.Clear(); + + EXPECT_EQ(0, adapter->GetHpackEncoderDynamicTableSize()); + + EXPECT_FALSE(adapter->session().want_write()); + const absl::string_view kBody = "This is an example response body."; + // A data fin is not sent so that the stream remains open, and the flow + // control state can be verified. + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, false); + body1->AppendPayload(kBody); + int submit_result = adapter->SubmitResponse( + 1, + ToHeaders({{":status", "404"}, + {"x-comment", "I have no idea what you're talking about."}}), + std::move(body1)); + EXPECT_EQ(submit_result, 0); + EXPECT_TRUE(adapter->session().want_write()); + + // Stream user data should have been set successfully after receiving headers. + EXPECT_EQ(kSentinel1, adapter->GetStreamUserData(1)); + adapter->SetStreamUserData(1, nullptr); + EXPECT_EQ(nullptr, adapter->GetStreamUserData(1)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0)); + + send_result = adapter->Send(); + EXPECT_EQ(0, send_result); + + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS, + spdy::SpdyFrameType::DATA})); + EXPECT_THAT(visitor.data(), testing::HasSubstr(kBody)); + EXPECT_FALSE(adapter->session().want_write()); + + // Some data was sent, so the remaining send window size should be less than + // the default. + EXPECT_LT(adapter->GetStreamSendWindowSize(1), kInitialFlowControlWindowSize); + EXPECT_GT(adapter->GetStreamSendWindowSize(1), 0); + // Send window for a nonexistent stream is not available. + EXPECT_EQ(adapter->GetStreamSendWindowSize(3), -1); + + EXPECT_GT(adapter->GetHpackEncoderDynamicTableSize(), 0); +} + +// Should also test: client attempts shutdown, server attempts shutdown after an +// explicit GOAWAY. +TEST(NgHttp2AdapterTest, ServerSendsShutdown) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "POST"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}, + /*fin=*/false) + .Serialize(); + testing::InSequence s; + + // Client preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + + const ssize_t result = adapter->ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + adapter->SubmitShutdownNotice(); + + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(GOAWAY, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(GOAWAY, 0, _, 0x0, 0)); + + int send_result = adapter->Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::GOAWAY})); +} + +TEST(NgHttp2AdapterTest, ServerSendsTrailers) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor); + EXPECT_FALSE(adapter->session().want_write()); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "GET"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}, + /*fin=*/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()); + // Stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + + const ssize_t result = adapter->ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + // Server will want to send a SETTINGS ack. + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + int send_result = adapter->Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); + visitor.Clear(); + + EXPECT_FALSE(adapter->session().want_write()); + const absl::string_view kBody = "This is an example response body."; + + // The body source must indicate that the end of the body is not the end of + // the stream. + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, false); + body1->AppendPayload(kBody); + body1->EndData(); + int submit_result = adapter->SubmitResponse( + 1, ToHeaders({{":status", "200"}, {"x-comment", "Sure, sounds good."}}), + std::move(body1)); + EXPECT_EQ(submit_result, 0); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0)); + + send_result = adapter->Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS, + spdy::SpdyFrameType::DATA})); + EXPECT_THAT(visitor.data(), testing::HasSubstr(kBody)); + visitor.Clear(); + EXPECT_FALSE(adapter->session().want_write()); + + // The body source has been exhausted by the call to Send() above. + int trailer_result = adapter->SubmitTrailer( + 1, ToHeaders({{"final-status", "a-ok"}, + {"x-comment", "trailers sure are cool"}})); + ASSERT_EQ(trailer_result, 0); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x5, 0)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + + send_result = adapter->Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS})); +} + +TEST(NgHttp2AdapterTest, ClientSendsContinuation) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor); + EXPECT_FALSE(adapter->session().want_write()); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "GET"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}, + /*fin=*/true, + /*add_continuation=*/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()); + // Stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 1)); + 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)); + EXPECT_CALL(visitor, OnEndStream(1)); + + const size_t result = adapter->ProcessBytes(frames); + 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); + EXPECT_FALSE(adapter->session().want_write()); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "GET"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}, + /*fin=*/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()); + // Stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + + const ssize_t result = adapter->ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + const absl::string_view kBody = "This is an example response body."; + + // The body source must indicate that the end of the body is not the end of + // the stream. + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, false); + body1->AppendPayload(kBody); + body1->EndData(); + int submit_result = adapter->SubmitResponse( + 1, ToHeaders({{":status", "200"}, {"x-comment", "Sure, sounds good."}}), + std::move(body1)); + EXPECT_EQ(submit_result, 0); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0)); + + int send_result = adapter->Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::HEADERS, + spdy::SpdyFrameType::DATA})); + EXPECT_THAT(visitor.data(), testing::HasSubstr(kBody)); + visitor.Clear(); + EXPECT_FALSE(adapter->session().want_write()); + + // The body source has been exhausted by the call to Send() above. + int trailer_result = + adapter->SubmitTrailer(1, ToHeaders({{":final-status", "a-ok"}})); + ASSERT_EQ(trailer_result, 0); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x5, 0)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + + send_result = adapter->Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS})); } } // namespace diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_callbacks.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_callbacks.cc index 8774873c95f..4a2221ea2ee 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_callbacks.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_callbacks.cc @@ -6,6 +6,7 @@ #include "absl/strings/string_view.h" #include "http2/adapter/http2_protocol.h" #include "http2/adapter/http2_visitor_interface.h" +#include "http2/adapter/nghttp2_data_provider.h" #include "http2/adapter/nghttp2_util.h" #include "third_party/nghttp2/nghttp2.h" #include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" @@ -16,14 +17,33 @@ namespace http2 { namespace adapter { namespace callbacks { +ssize_t OnReadyToSend(nghttp2_session* /* session */, const uint8_t* data, + size_t length, int flags, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); + auto* visitor = static_cast<Http2VisitorInterface*>(user_data); + const ssize_t result = visitor->OnReadyToSend(ToStringView(data, length)); + QUICHE_VLOG(1) << "OnReadyToSend(length=" << length << ", flags=" << flags + << ") returning " << result; + if (result > 0) { + return result; + } else if (result == Http2VisitorInterface::kSendBlocked) { + return -504; // NGHTTP2_ERR_WOULDBLOCK + } else { + return -902; // NGHTTP2_ERR_CALLBACK_FAILURE + } +} + int OnBeginFrame(nghttp2_session* /* session */, const nghttp2_frame_hd* header, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); auto* visitor = static_cast<Http2VisitorInterface*>(user_data); visitor->OnFrameHeader(header->stream_id, header->length, header->type, 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; } @@ -31,10 +51,9 @@ int OnBeginFrame(nghttp2_session* /* session */, int OnFrameReceived(nghttp2_session* /* session */, const nghttp2_frame* frame, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); auto* visitor = static_cast<Http2VisitorInterface*>(user_data); const Http2StreamId stream_id = frame->hd.stream_id; - QUICHE_VLOG(2) << "Frame " << static_cast<int>(frame->hd.type) - << " for stream " << stream_id; switch (frame->hd.type) { // The beginning of the DATA frame is handled in OnBeginFrame(), and the // beginning of the header block is handled in client/server-specific @@ -71,7 +90,7 @@ int OnFrameReceived(nghttp2_session* /* session */, visitor->OnSettingsAck(); } else { visitor->OnSettingsStart(); - for (int i = 0; i < frame->settings.niv; ++i) { + for (size_t i = 0; i < frame->settings.niv; ++i) { nghttp2_settings_entry entry = frame->settings.iv[i]; // The nghttp2_settings_entry uses int32_t for the ID; we must cast. visitor->OnSetting(Http2Setting{ @@ -129,29 +148,67 @@ int OnFrameReceived(nghttp2_session* /* session */, int OnBeginHeaders(nghttp2_session* /* session */, const nghttp2_frame* frame, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); auto* visitor = static_cast<Http2VisitorInterface*>(user_data); - visitor->OnBeginHeadersForStream(frame->hd.stream_id); - return 0; + const bool result = visitor->OnBeginHeadersForStream(frame->hd.stream_id); + return result ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE; } -int OnHeader(nghttp2_session* /* session */, - const nghttp2_frame* frame, - nghttp2_rcbuf* name, - nghttp2_rcbuf* value, - uint8_t flags, +int OnHeader(nghttp2_session* /* session */, const nghttp2_frame* frame, + nghttp2_rcbuf* name, nghttp2_rcbuf* value, uint8_t /*flags*/, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); auto* visitor = static_cast<Http2VisitorInterface*>(user_data); - visitor->OnHeaderForStream(frame->hd.stream_id, ToStringView(name), - ToStringView(value)); - return 0; + const Http2VisitorInterface::OnHeaderResult result = + visitor->OnHeaderForStream(frame->hd.stream_id, ToStringView(name), + ToStringView(value)); + switch (result) { + case Http2VisitorInterface::HEADER_OK: + return 0; + case Http2VisitorInterface::HEADER_CONNECTION_ERROR: + return NGHTTP2_ERR_CALLBACK_FAILURE; + case Http2VisitorInterface::HEADER_RST_STREAM: + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } } -int OnDataChunk(nghttp2_session* /* session */, - uint8_t flags, - Http2StreamId stream_id, - const uint8_t* data, - size_t len, +int OnBeforeFrameSent(nghttp2_session* /* session */, + const nghttp2_frame* frame, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); + LogBeforeSend(*frame); + auto* visitor = static_cast<Http2VisitorInterface*>(user_data); + return visitor->OnBeforeFrameSent(frame->hd.type, frame->hd.stream_id, + frame->hd.length, frame->hd.flags); +} + +int OnFrameSent(nghttp2_session* /* session */, const nghttp2_frame* frame, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); + auto* visitor = static_cast<Http2VisitorInterface*>(user_data); + uint32_t error_code = 0; + if (frame->hd.type == NGHTTP2_RST_STREAM) { + error_code = frame->rst_stream.error_code; + } else if (frame->hd.type == NGHTTP2_GOAWAY) { + error_code = frame->goaway.error_code; + } + return visitor->OnFrameSent(frame->hd.type, frame->hd.stream_id, + frame->hd.length, frame->hd.flags, error_code); +} + +int OnInvalidFrameReceived(nghttp2_session* /* session */, + const nghttp2_frame* frame, int lib_error_code, + void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); + auto* visitor = static_cast<Http2VisitorInterface*>(user_data); + const bool result = + visitor->OnInvalidFrame(frame->hd.stream_id, lib_error_code); + return result ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE; +} + +int OnDataChunk(nghttp2_session* /* session */, uint8_t /*flags*/, + Http2StreamId stream_id, const uint8_t* data, size_t len, + void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); auto* visitor = static_cast<Http2VisitorInterface*>(user_data); visitor->OnDataForStream( stream_id, absl::string_view(reinterpret_cast<const char*>(data), len)); @@ -162,34 +219,63 @@ int OnStreamClosed(nghttp2_session* /* session */, Http2StreamId stream_id, uint32_t error_code, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); auto* visitor = static_cast<Http2VisitorInterface*>(user_data); visitor->OnCloseStream(stream_id, ToHttp2ErrorCode(error_code)); return 0; } -ssize_t OnReadyToReadDataForStream(nghttp2_session* /* session */, - Http2StreamId stream_id, - uint8_t* dest_buffer, - size_t max_length, - uint32_t* data_flags, - nghttp2_data_source* source, - void* user_data) { - auto* visitor = static_cast<Http2VisitorInterface*>(source->ptr); - ssize_t bytes_to_send = 0; - bool end_stream = false; - visitor->OnReadyToSendDataForStream(stream_id, - reinterpret_cast<char*>(dest_buffer), - max_length, &bytes_to_send, &end_stream); - if (bytes_to_send >= 0 && end_stream) { - *data_flags |= NGHTTP2_DATA_FLAG_EOF; +int OnExtensionChunkReceived(nghttp2_session* /*session*/, + const nghttp2_frame_hd* hd, const uint8_t* data, + size_t len, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); + auto* visitor = static_cast<Http2VisitorInterface*>(user_data); + if (hd->type != kMetadataFrameType) { + QUICHE_LOG(ERROR) << "Unexpected frame type: " + << static_cast<int>(hd->type); + return NGHTTP2_ERR_CANCEL; + } + visitor->OnMetadataForStream(hd->stream_id, ToStringView(data, len)); + return 0; +} + +int OnUnpackExtensionCallback(nghttp2_session* /*session*/, void** /*payload*/, + const nghttp2_frame_hd* hd, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); + auto* visitor = static_cast<Http2VisitorInterface*>(user_data); + if (hd->flags == kMetadataEndFlag) { + const bool result = visitor->OnMetadataEndForStream(hd->stream_id); + if (!result) { + return NGHTTP2_ERR_CALLBACK_FAILURE; + } } - return bytes_to_send; + return 0; +} + +ssize_t OnPackExtensionCallback(nghttp2_session* /*session*/, uint8_t* buf, + size_t len, const nghttp2_frame* frame, + void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); + auto* visitor = static_cast<Http2VisitorInterface*>(user_data); + ssize_t written = 0; + visitor->OnReadyToSendMetadataForStream( + frame->hd.stream_id, reinterpret_cast<char*>(buf), len, &written); + return written; +} + +int OnError(nghttp2_session* /*session*/, int /*lib_error_code*/, + const char* msg, size_t len, void* user_data) { + QUICHE_CHECK_NE(user_data, nullptr); + auto* visitor = static_cast<Http2VisitorInterface*>(user_data); + visitor->OnErrorDebug(absl::string_view(msg, len)); + return 0; } nghttp2_session_callbacks_unique_ptr Create() { nghttp2_session_callbacks* callbacks; nghttp2_session_callbacks_new(&callbacks); + nghttp2_session_callbacks_set_send_callback(callbacks, &OnReadyToSend); nghttp2_session_callbacks_set_on_begin_frame_callback(callbacks, &OnBeginFrame); nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, @@ -201,6 +287,21 @@ nghttp2_session_callbacks_unique_ptr Create() { &OnDataChunk); nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, &OnStreamClosed); + nghttp2_session_callbacks_set_before_frame_send_callback(callbacks, + &OnBeforeFrameSent); + nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, &OnFrameSent); + nghttp2_session_callbacks_set_on_invalid_frame_recv_callback( + callbacks, &OnInvalidFrameReceived); + nghttp2_session_callbacks_set_error_callback2(callbacks, &OnError); + // on_frame_not_send_callback <- just ignored + nghttp2_session_callbacks_set_send_data_callback( + callbacks, &DataFrameSourceSendCallback); + nghttp2_session_callbacks_set_pack_extension_callback( + callbacks, &OnPackExtensionCallback); + nghttp2_session_callbacks_set_unpack_extension_callback( + callbacks, &OnUnpackExtensionCallback); + nghttp2_session_callbacks_set_on_extension_chunk_recv_callback( + callbacks, &OnExtensionChunkReceived); return MakeCallbacksPtr(callbacks); } diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_callbacks.h b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_callbacks.h index 5fbaee041c5..696b6847509 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_callbacks.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_callbacks.h @@ -13,6 +13,13 @@ namespace callbacks { // beginning of its lifetime. It is expected that |user_data| holds an // Http2VisitorInterface. +// Callback once the library is ready to send serialized frames. +ssize_t OnReadyToSend(nghttp2_session* session, + const uint8_t* data, + size_t length, + int flags, + void* user_data); + // Callback once a frame header has been received. int OnBeginFrame(nghttp2_session* session, const nghttp2_frame_hd* header, void* user_data); @@ -31,7 +38,19 @@ int OnHeader(nghttp2_session* session, const nghttp2_frame* frame, nghttp2_rcbuf* name, nghttp2_rcbuf* value, uint8_t flags, void* user_data); -// Callback once a chunk of data (from a DATA frame payload) has been received. +// Invoked immediately before sending a frame. +int OnBeforeFrameSent(nghttp2_session* session, const nghttp2_frame* frame, + void* user_data); + +// Invoked immediately after a frame is sent. +int OnFrameSent(nghttp2_session* session, const nghttp2_frame* frame, + void* user_data); + +// Invoked when an invalid frame is received. +int OnInvalidFrameReceived(nghttp2_session* session, const nghttp2_frame* frame, + int lib_error_code, void* user_data); + +// Invoked when a chunk of data (from a DATA frame payload) has been received. int OnDataChunk(nghttp2_session* session, uint8_t flags, Http2StreamId stream_id, const uint8_t* data, size_t len, void* user_data); @@ -40,13 +59,25 @@ int OnDataChunk(nghttp2_session* session, uint8_t flags, int OnStreamClosed(nghttp2_session* session, Http2StreamId stream_id, uint32_t error_code, void* user_data); -// Callback once nghttp2 is ready to read data from |source| into |dest_buffer|. -ssize_t OnReadyToReadDataForStream(nghttp2_session* session, - Http2StreamId stream_id, - uint8_t* dest_buffer, size_t max_length, - uint32_t* data_flags, - nghttp2_data_source* source, - void* user_data); +// Invoked when nghttp2 has a chunk of extension frame data to pass to the +// application. +int OnExtensionChunkReceived(nghttp2_session* session, + const nghttp2_frame_hd* hd, const uint8_t* data, + size_t len, void* user_data); + +// Invoked when nghttp2 wants the application to unpack an extension payload. +int OnUnpackExtensionCallback(nghttp2_session* session, void** payload, + const nghttp2_frame_hd* hd, void* user_data); + +// Invoked when nghttp2 is ready to pack an extension payload. Returns the +// number of bytes serialized to |buf|. +ssize_t OnPackExtensionCallback(nghttp2_session* session, uint8_t* buf, + size_t len, const nghttp2_frame* frame, + void* user_data); + +// Invoked when the library has an error message to deliver. +int OnError(nghttp2_session* session, int lib_error_code, const char* msg, + size_t len, void* user_data); nghttp2_session_callbacks_unique_ptr Create(); diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_data_provider.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_data_provider.cc new file mode 100644 index 00000000000..200aa67ae29 --- /dev/null +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_data_provider.cc @@ -0,0 +1,63 @@ +#include "http2/adapter/nghttp2_data_provider.h" + +#include "http2/adapter/http2_visitor_interface.h" +#include "http2/adapter/nghttp2_util.h" + +namespace http2 { +namespace adapter { +namespace callbacks { + +namespace { +const size_t kFrameHeaderSize = 9; +} + +ssize_t DataFrameSourceReadCallback(nghttp2_session* /* session */, + int32_t /* stream_id */, + uint8_t* /* buf */, + size_t length, + uint32_t* data_flags, + nghttp2_data_source* source, + void* /* user_data */) { + *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; + auto* frame_source = static_cast<DataFrameSource*>(source->ptr); + auto [result_length, done] = frame_source->SelectPayloadLength(length); + if (result_length == 0 && !done) { + return NGHTTP2_ERR_DEFERRED; + } else if (result_length == DataFrameSource::kError) { + return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; + } + if (done) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } + if (!frame_source->send_fin()) { + *data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; + } + return result_length; +} + +int DataFrameSourceSendCallback(nghttp2_session* /* session */, + nghttp2_frame* /* frame */, + const uint8_t* framehd, + size_t length, + nghttp2_data_source* source, + void* /* user_data */) { + auto* frame_source = static_cast<DataFrameSource*>(source->ptr); + frame_source->Send(ToStringView(framehd, kFrameHeaderSize), length); + return 0; +} + +} // namespace callbacks + +std::unique_ptr<nghttp2_data_provider> MakeDataProvider( + DataFrameSource* source) { + if (source == nullptr) { + return nullptr; + } + auto provider = absl::make_unique<nghttp2_data_provider>(); + provider->source.ptr = source; + provider->read_callback = &callbacks::DataFrameSourceReadCallback; + return provider; +} + +} // namespace adapter +} // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_data_provider.h b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_data_provider.h new file mode 100644 index 00000000000..241bab91cdd --- /dev/null +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_data_provider.h @@ -0,0 +1,37 @@ +#ifndef QUICHE_HTTP2_ADAPTER_NGHTTP2_DATA_PROVIDER_H_ +#define QUICHE_HTTP2_ADAPTER_NGHTTP2_DATA_PROVIDER_H_ + +#include "http2/adapter/data_source.h" +#include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" + +namespace http2 { +namespace adapter { +namespace callbacks { + +// Assumes |source| is a DataFrameSource. +ssize_t DataFrameSourceReadCallback(nghttp2_session* /*session */, + int32_t /* stream_id */, + uint8_t* /* buf */, + size_t length, + uint32_t* data_flags, + nghttp2_data_source* source, + void* /* user_data */); + +int DataFrameSourceSendCallback(nghttp2_session* /* session */, + nghttp2_frame* /* frame */, + const uint8_t* framehd, + size_t length, + nghttp2_data_source* source, + void* /* user_data */); + +} // namespace callbacks + +// Transforms a DataFrameSource into a nghttp2_data_provider. Does not take +// ownership of |source|. Returns nullptr if |source| is nullptr. +std::unique_ptr<nghttp2_data_provider> MakeDataProvider( + DataFrameSource* source); + +} // namespace adapter +} // namespace http2 + +#endif // QUICHE_HTTP2_ADAPTER_NGHTTP2_DATA_PROVIDER_H_ diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_data_provider_test.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_data_provider_test.cc new file mode 100644 index 00000000000..af8d98187f2 --- /dev/null +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_data_provider_test.cc @@ -0,0 +1,117 @@ +#include "http2/adapter/nghttp2_data_provider.h" + +#include "http2/adapter/test_utils.h" +#include "common/platform/api/quiche_test.h" + +namespace http2 { +namespace adapter { +namespace test { + +const size_t kFrameHeaderSize = 9; + +// Verifies that a nghttp2_data_provider derived from a DataFrameSource works +// correctly with nghttp2-style callbacks when the amount of data read is less +// than what the source provides. +TEST(DataProviderTest, ReadLessThanSourceProvides) { + DataSavingVisitor visitor; + TestDataFrameSource source(visitor, true); + source.AppendPayload("Example payload"); + source.EndData(); + auto provider = MakeDataProvider(&source); + uint32_t data_flags = 0; + const int32_t kStreamId = 1; + const size_t kReadLength = 10; + // Read callback selects a payload length given an upper bound. + ssize_t result = + provider->read_callback(nullptr, kStreamId, nullptr, kReadLength, + &data_flags, &provider->source, nullptr); + ASSERT_EQ(kReadLength, result); + EXPECT_EQ(NGHTTP2_DATA_FLAG_NO_COPY, data_flags); + + const uint8_t framehd[kFrameHeaderSize] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + // Sends the frame header and some payload bytes. + int send_result = callbacks::DataFrameSourceSendCallback( + nullptr, nullptr, framehd, result, &provider->source, nullptr); + EXPECT_EQ(0, send_result); + // Data accepted by the visitor includes a frame header and kReadLength bytes + // of payload. + EXPECT_EQ(visitor.data().size(), kFrameHeaderSize + kReadLength); +} + +// Verifies that a nghttp2_data_provider derived from a DataFrameSource works +// correctly with nghttp2-style callbacks when the amount of data read is more +// than what the source provides. +TEST(DataProviderTest, ReadMoreThanSourceProvides) { + DataSavingVisitor visitor; + const absl::string_view kPayload = "Example payload"; + TestDataFrameSource source(visitor, true); + source.AppendPayload(kPayload); + source.EndData(); + auto provider = MakeDataProvider(&source); + uint32_t data_flags = 0; + const int32_t kStreamId = 1; + const size_t kReadLength = 30; + // Read callback selects a payload length given an upper bound. + ssize_t result = + provider->read_callback(nullptr, kStreamId, nullptr, kReadLength, + &data_flags, &provider->source, nullptr); + ASSERT_EQ(kPayload.size(), result); + EXPECT_EQ(NGHTTP2_DATA_FLAG_NO_COPY | NGHTTP2_DATA_FLAG_EOF, data_flags); + + const uint8_t framehd[kFrameHeaderSize] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + // Sends the frame header and some payload bytes. + int send_result = callbacks::DataFrameSourceSendCallback( + nullptr, nullptr, framehd, result, &provider->source, nullptr); + EXPECT_EQ(0, send_result); + // Data accepted by the visitor includes a frame header and the entire + // payload. + EXPECT_EQ(visitor.data().size(), kFrameHeaderSize + kPayload.size()); +} + +// Verifies that a nghttp2_data_provider derived from a DataFrameSource works +// correctly with nghttp2-style callbacks when the source is blocked. +TEST(DataProviderTest, ReadFromBlockedSource) { + DataSavingVisitor visitor; + // Source has no payload, but also no fin, so it's blocked. + TestDataFrameSource source(visitor, false); + auto provider = MakeDataProvider(&source); + uint32_t data_flags = 0; + const int32_t kStreamId = 1; + const size_t kReadLength = 10; + ssize_t result = + provider->read_callback(nullptr, kStreamId, nullptr, kReadLength, + &data_flags, &provider->source, nullptr); + // Read operation is deferred, since the source is blocked. + EXPECT_EQ(NGHTTP2_ERR_DEFERRED, result); +} + +// Verifies that a nghttp2_data_provider derived from a DataFrameSource works +// correctly with nghttp2-style callbacks when the source provides only fin and +// no data. +TEST(DataProviderTest, ReadFromZeroLengthSource) { + DataSavingVisitor visitor; + // Empty payload and fin=true indicates the source is done. + TestDataFrameSource source(visitor, true); + source.EndData(); + auto provider = MakeDataProvider(&source); + uint32_t data_flags = 0; + const int32_t kStreamId = 1; + const size_t kReadLength = 10; + ssize_t result = + provider->read_callback(nullptr, kStreamId, nullptr, kReadLength, + &data_flags, &provider->source, nullptr); + ASSERT_EQ(0, result); + EXPECT_EQ(NGHTTP2_DATA_FLAG_NO_COPY | NGHTTP2_DATA_FLAG_EOF, data_flags); + + const uint8_t framehd[kFrameHeaderSize] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; + int send_result = callbacks::DataFrameSourceSendCallback( + nullptr, nullptr, framehd, result, &provider->source, nullptr); + EXPECT_EQ(0, send_result); + // Data accepted by the visitor includes a frame header with fin and zero + // bytes of payload. + EXPECT_EQ(visitor.data().size(), kFrameHeaderSize); +} + +} // namespace test +} // namespace adapter +} // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session.cc index d434b06e0f5..9868958bc67 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session.cc @@ -4,35 +4,21 @@ namespace http2 { namespace adapter { -namespace { - -void DeleteOptions(nghttp2_option* options) { - if (options) { - nghttp2_option_del(options); - } -} - -} // namespace NgHttp2Session::NgHttp2Session(Perspective perspective, nghttp2_session_callbacks_unique_ptr callbacks, - nghttp2_option* options, - void* userdata) - : session_(MakeSessionPtr(nullptr)), - options_(options, DeleteOptions), - perspective_(perspective) { + const nghttp2_option* options, void* userdata) + : session_(MakeSessionPtr(nullptr)), perspective_(perspective) { nghttp2_session* session; switch (perspective) { case Perspective::kClient: - nghttp2_session_client_new2(&session, callbacks.get(), userdata, - options_.get()); + nghttp2_session_client_new2(&session, callbacks.get(), userdata, options); break; case Perspective::kServer: - nghttp2_session_server_new2(&session, callbacks.get(), userdata, - options_.get()); + nghttp2_session_server_new2(&session, callbacks.get(), userdata, options); break; } - session_.reset(session); + session_ = MakeSessionPtr(session); } NgHttp2Session::~NgHttp2Session() { diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session.h b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session.h index d446a07c75b..4339875588c 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session.h @@ -4,18 +4,18 @@ #include "http2/adapter/http2_session.h" #include "http2/adapter/nghttp2_util.h" #include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" +#include "common/platform/api/quiche_export.h" namespace http2 { namespace adapter { // A C++ wrapper around common nghttp2_session operations. -class NgHttp2Session : public Http2Session { +class QUICHE_EXPORT_PRIVATE NgHttp2Session : public Http2Session { public: - // Takes ownership of |options|. + // Does not take ownership of |options|. NgHttp2Session(Perspective perspective, nghttp2_session_callbacks_unique_ptr callbacks, - nghttp2_option* options, - void* userdata); + const nghttp2_option* options, void* userdata); ~NgHttp2Session() override; ssize_t ProcessBytes(absl::string_view bytes) override; @@ -29,10 +29,7 @@ class NgHttp2Session : public Http2Session { nghttp2_session* raw_ptr() const { return session_.get(); } private: - using OptionsDeleter = void (&)(nghttp2_option*); - nghttp2_session_unique_ptr session_; - std::unique_ptr<nghttp2_option, OptionsDeleter> options_; Perspective perspective_; }; diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session_test.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session_test.cc index 169d2f20dd0..487843bcc52 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session_test.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_session_test.cc @@ -26,47 +26,36 @@ enum FrameType { WINDOW_UPDATE, }; -ssize_t SaveSessionOutput(nghttp2_session* /* session*/, - const uint8_t* data, - size_t length, - int /* flags */, - void* user_data) { - auto visitor = static_cast<DataSavingVisitor*>(user_data); - visitor->Save(ToStringView(data, length)); - return length; -} - class NgHttp2SessionTest : public testing::Test { public: - nghttp2_option* CreateOptions() { - nghttp2_option* options; - nghttp2_option_new(&options); - nghttp2_option_set_no_auto_window_update(options, 1); - return options; + void SetUp() override { + nghttp2_option_new(&options_); + nghttp2_option_set_no_auto_window_update(options_, 1); } + void TearDown() override { nghttp2_option_del(options_); } + nghttp2_session_callbacks_unique_ptr CreateCallbacks() { nghttp2_session_callbacks_unique_ptr callbacks = callbacks::Create(); - nghttp2_session_callbacks_set_send_callback(callbacks.get(), - &SaveSessionOutput); return callbacks; } DataSavingVisitor visitor_; + nghttp2_option* options_ = nullptr; }; TEST_F(NgHttp2SessionTest, ClientConstruction) { - NgHttp2Session session(Perspective::kClient, CreateCallbacks(), - CreateOptions(), &visitor_); + NgHttp2Session session(Perspective::kClient, CreateCallbacks(), options_, + &visitor_); EXPECT_TRUE(session.want_read()); EXPECT_FALSE(session.want_write()); - EXPECT_EQ(session.GetRemoteWindowSize(), kDefaultInitialStreamWindowSize); + EXPECT_EQ(session.GetRemoteWindowSize(), kInitialFlowControlWindowSize); EXPECT_NE(session.raw_ptr(), nullptr); } TEST_F(NgHttp2SessionTest, ClientHandlesFrames) { - NgHttp2Session session(Perspective::kClient, CreateCallbacks(), - CreateOptions(), &visitor_); + NgHttp2Session session(Perspective::kClient, CreateCallbacks(), options_, + &visitor_); ASSERT_EQ(0, nghttp2_session_send(session.raw_ptr())); ASSERT_GT(visitor_.data().size(), 0); @@ -92,7 +81,13 @@ TEST_F(NgHttp2SessionTest, ClientHandlesFrames) { EXPECT_EQ(initial_frames.size(), initial_result); EXPECT_EQ(session.GetRemoteWindowSize(), - kDefaultInitialStreamWindowSize + 1000); + kInitialFlowControlWindowSize + 1000); + + EXPECT_CALL(visitor_, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor_, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor_, OnBeforeFrameSent(PING, 0, 8, 0x1)); + EXPECT_CALL(visitor_, OnFrameSent(PING, 0, 8, 0x1, 0)); + ASSERT_EQ(0, nghttp2_session_send(session.raw_ptr())); // Some bytes should have been serialized. absl::string_view serialized = visitor_.data(); @@ -139,6 +134,13 @@ TEST_F(NgHttp2SessionTest, ClientHandlesFrames) { ASSERT_GT(stream_id3, 0); QUICHE_LOG(INFO) << "Created stream: " << stream_id3; + EXPECT_CALL(visitor_, OnBeforeFrameSent(HEADERS, 1, _, 0x5)); + EXPECT_CALL(visitor_, OnFrameSent(HEADERS, 1, _, 0x5, 0)); + EXPECT_CALL(visitor_, OnBeforeFrameSent(HEADERS, 3, _, 0x5)); + EXPECT_CALL(visitor_, OnFrameSent(HEADERS, 3, _, 0x5, 0)); + EXPECT_CALL(visitor_, OnBeforeFrameSent(HEADERS, 5, _, 0x5)); + EXPECT_CALL(visitor_, OnFrameSent(HEADERS, 5, _, 0x5, 0)); + ASSERT_EQ(0, nghttp2_session_send(session.raw_ptr())); serialized = visitor_.data(); EXPECT_THAT(serialized, EqualsFrames({spdy::SpdyFrameType::HEADERS, @@ -203,17 +205,17 @@ TEST_F(NgHttp2SessionTest, ClientHandlesFrames) { } TEST_F(NgHttp2SessionTest, ServerConstruction) { - NgHttp2Session session(Perspective::kServer, CreateCallbacks(), - CreateOptions(), &visitor_); + NgHttp2Session session(Perspective::kServer, CreateCallbacks(), options_, + &visitor_); EXPECT_TRUE(session.want_read()); EXPECT_FALSE(session.want_write()); - EXPECT_EQ(session.GetRemoteWindowSize(), kDefaultInitialStreamWindowSize); + EXPECT_EQ(session.GetRemoteWindowSize(), kInitialFlowControlWindowSize); EXPECT_NE(session.raw_ptr(), nullptr); } TEST_F(NgHttp2SessionTest, ServerHandlesFrames) { - NgHttp2Session session(Perspective::kServer, CreateCallbacks(), - CreateOptions(), &visitor_); + NgHttp2Session session(Perspective::kServer, CreateCallbacks(), options_, + &visitor_); const std::string frames = TestFrameSequence() .ClientPreface() @@ -277,7 +279,14 @@ TEST_F(NgHttp2SessionTest, ServerHandlesFrames) { EXPECT_EQ(frames.size(), result); EXPECT_EQ(session.GetRemoteWindowSize(), - kDefaultInitialStreamWindowSize + 1000); + kInitialFlowControlWindowSize + 1000); + + EXPECT_CALL(visitor_, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor_, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor_, OnBeforeFrameSent(PING, 0, 8, 0x1)); + EXPECT_CALL(visitor_, OnFrameSent(PING, 0, 8, 0x1, 0)); + EXPECT_CALL(visitor_, OnBeforeFrameSent(PING, 0, 8, 0x1)); + EXPECT_CALL(visitor_, OnFrameSent(PING, 0, 8, 0x1, 0)); EXPECT_TRUE(session.want_write()); ASSERT_EQ(0, nghttp2_session_send(session.raw_ptr())); diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_test.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_test.cc new file mode 100644 index 00000000000..0b977dc1815 --- /dev/null +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_test.cc @@ -0,0 +1,205 @@ +#include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" + +#include "absl/strings/str_cat.h" +#include "http2/adapter/mock_nghttp2_callbacks.h" +#include "http2/adapter/nghttp2_test_utils.h" +#include "http2/adapter/nghttp2_util.h" +#include "http2/adapter/test_frame_sequence.h" +#include "http2/adapter/test_utils.h" +#include "common/platform/api/quiche_test.h" + +namespace http2 { +namespace adapter { +namespace test { +namespace { + +using testing::_; + +enum FrameType { + DATA, + HEADERS, + PRIORITY, + RST_STREAM, + SETTINGS, + PUSH_PROMISE, + PING, + GOAWAY, + WINDOW_UPDATE, +}; + +nghttp2_option* GetOptions() { + nghttp2_option* options; + nghttp2_option_new(&options); + // Set some common options for compatibility. + nghttp2_option_set_no_closed_streams(options, 1); + nghttp2_option_set_no_auto_window_update(options, 1); + nghttp2_option_set_max_send_header_block_length(options, 0x2000000); + nghttp2_option_set_max_outbound_ack(options, 10000); + return options; +} + +class Nghttp2Test : public testing::Test { + public: + Nghttp2Test() : session_(MakeSessionPtr(nullptr)) {} + + void SetUp() override { InitializeSession(); } + + virtual Perspective GetPerspective() = 0; + + void InitializeSession() { + auto nghttp2_callbacks = MockNghttp2Callbacks::GetCallbacks(); + nghttp2_option* options = GetOptions(); + nghttp2_session* ptr; + if (GetPerspective() == Perspective::kClient) { + nghttp2_session_client_new2(&ptr, nghttp2_callbacks.get(), + &mock_callbacks_, options); + } else { + nghttp2_session_server_new2(&ptr, nghttp2_callbacks.get(), + &mock_callbacks_, options); + } + nghttp2_option_del(options); + + // Sets up the Send() callback to append to |serialized_|. + EXPECT_CALL(mock_callbacks_, Send(_, _, _)) + .WillRepeatedly( + [this](const uint8_t* data, size_t length, int /*flags*/) { + absl::StrAppend(&serialized_, ToStringView(data, length)); + return length; + }); + // Sets up the SendData() callback to fetch and append data from a + // TestDataSource. + EXPECT_CALL(mock_callbacks_, SendData(_, _, _, _)) + .WillRepeatedly([this](nghttp2_frame* /*frame*/, const uint8_t* framehd, + size_t length, nghttp2_data_source* source) { + QUICHE_LOG(INFO) << "Appending frame header and " << length + << " bytes of data"; + auto* s = static_cast<TestDataSource*>(source->ptr); + absl::StrAppend(&serialized_, ToStringView(framehd, 9), + s->ReadNext(length)); + return 0; + }); + session_ = MakeSessionPtr(ptr); + } + + testing::StrictMock<MockNghttp2Callbacks> mock_callbacks_; + nghttp2_session_unique_ptr session_; + std::string serialized_; +}; + +class Nghttp2ClientTest : public Nghttp2Test { + public: + Perspective GetPerspective() override { return Perspective::kClient; } +}; + +// Verifies nghttp2 behavior when acting as a client. +TEST_F(Nghttp2ClientTest, ClientReceivesUnexpectedHeaders) { + const std::string initial_frames = TestFrameSequence() + .ServerPreface() + .Ping(42) + .WindowUpdate(0, 1000) + .Serialize(); + + testing::InSequence seq; + EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(0, SETTINGS, 0))); + EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsSettings(testing::IsEmpty()))); + EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(0, PING, 0))); + EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsPing(42))); + EXPECT_CALL(mock_callbacks_, + OnBeginFrame(HasFrameHeader(0, WINDOW_UPDATE, 0))); + EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsWindowUpdate(1000))); + + ssize_t result = nghttp2_session_mem_recv( + session_.get(), ToUint8Ptr(initial_frames.data()), initial_frames.size()); + ASSERT_EQ(result, initial_frames.size()); + + const std::string unexpected_stream_frames = + TestFrameSequence() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .RstStream(3, Http2ErrorCode::INTERNAL_ERROR) + .GoAway(5, Http2ErrorCode::ENHANCE_YOUR_CALM, "calm down!!") + .Serialize(); + + EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(1, HEADERS, _))); + EXPECT_CALL(mock_callbacks_, OnInvalidFrameRecv(IsHeaders(1, _, _), _)); + // No events from the DATA, RST_STREAM or GOAWAY. + + nghttp2_session_mem_recv(session_.get(), + ToUint8Ptr(unexpected_stream_frames.data()), + unexpected_stream_frames.size()); +} + +// Tests the request-sending behavior of nghttp2 when acting as a client. +TEST_F(Nghttp2ClientTest, ClientSendsRequest) { + int result = nghttp2_session_send(session_.get()); + ASSERT_EQ(result, 0); + + EXPECT_THAT(serialized_, testing::StrEq(spdy::kHttp2ConnectionHeaderPrefix)); + serialized_.clear(); + + const std::string initial_frames = + TestFrameSequence().ServerPreface().Serialize(); + testing::InSequence s; + + // Server preface (empty SETTINGS) + EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(0, SETTINGS, 0))); + EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsSettings(testing::IsEmpty()))); + + ssize_t recv_result = nghttp2_session_mem_recv( + session_.get(), ToUint8Ptr(initial_frames.data()), initial_frames.size()); + EXPECT_EQ(initial_frames.size(), recv_result); + + // Client wants to send a SETTINGS ack. + EXPECT_CALL(mock_callbacks_, BeforeFrameSend(IsSettings(testing::IsEmpty()))); + EXPECT_CALL(mock_callbacks_, OnFrameSend(IsSettings(testing::IsEmpty()))); + EXPECT_TRUE(nghttp2_session_want_write(session_.get())); + result = nghttp2_session_send(session_.get()); + EXPECT_THAT(serialized_, EqualsFrames({spdy::SpdyFrameType::SETTINGS})); + serialized_.clear(); + + EXPECT_FALSE(nghttp2_session_want_write(session_.get())); + + // The following sets up the client request. + std::vector<std::pair<absl::string_view, absl::string_view>> headers = { + {":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}; + std::vector<nghttp2_nv> nvs; + for (const auto& h : headers) { + nvs.push_back({.name = ToUint8Ptr(h.first.data()), + .value = ToUint8Ptr(h.second.data()), + .namelen = h.first.size(), + .valuelen = h.second.size()}); + } + const absl::string_view kBody = "This is an example request body."; + TestDataSource source{kBody}; + nghttp2_data_provider provider = source.MakeDataProvider(); + // After submitting the request, the client will want to write. + int stream_id = + nghttp2_submit_request(session_.get(), nullptr /* pri_spec */, nvs.data(), + nvs.size(), &provider, nullptr /* stream_data */); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(nghttp2_session_want_write(session_.get())); + + // We expect that the client will want to write HEADERS, then DATA. + EXPECT_CALL(mock_callbacks_, BeforeFrameSend(IsHeaders(stream_id, _, _))); + EXPECT_CALL(mock_callbacks_, OnFrameSend(IsHeaders(stream_id, _, _))); + EXPECT_CALL(mock_callbacks_, OnFrameSend(IsData(stream_id, kBody.size(), _))); + nghttp2_session_send(session_.get()); + EXPECT_THAT(serialized_, EqualsFrames({spdy::SpdyFrameType::HEADERS, + spdy::SpdyFrameType::DATA})); + EXPECT_THAT(serialized_, testing::HasSubstr(kBody)); + + // Once the request is flushed, the client no longer wants to write. + EXPECT_FALSE(nghttp2_session_want_write(session_.get())); +} + +} // namespace +} // namespace test +} // namespace adapter +} // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_test_utils.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_test_utils.cc new file mode 100644 index 00000000000..d9a04f27772 --- /dev/null +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_test_utils.cc @@ -0,0 +1,454 @@ +#include "http2/adapter/nghttp2_test_utils.h" + +#include "http2/adapter/nghttp2_util.h" +#include "common/quiche_endian.h" + +namespace http2 { +namespace adapter { +namespace test { + +namespace { + +// Custom gMock matcher, used to implement HasFrameHeader(). +class FrameHeaderMatcher { + public: + FrameHeaderMatcher(int32_t streamid, uint8_t type, + const testing::Matcher<int> flags) + : stream_id_(streamid), type_(type), flags_(flags) {} + + bool Match(const nghttp2_frame_hd& frame, + testing::MatchResultListener* listener) const { + bool matched = true; + if (stream_id_ != frame.stream_id) { + *listener << "; expected stream " << stream_id_ << ", saw " + << frame.stream_id; + matched = false; + } + if (type_ != frame.type) { + *listener << "; expected frame type " << type_ << ", saw " + << static_cast<int>(frame.type); + matched = false; + } + if (!flags_.MatchAndExplain(frame.flags, listener)) { + matched = false; + } + return matched; + } + + void DescribeTo(std::ostream* os) const { + *os << "contains a frame header with stream " << stream_id_ << ", type " + << type_ << ", "; + flags_.DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const { + *os << "does not contain a frame header with stream " << stream_id_ + << ", type " << type_ << ", "; + flags_.DescribeNegationTo(os); + } + + private: + const int32_t stream_id_; + const int type_; + const testing::Matcher<int> flags_; +}; + +class PointerToFrameHeaderMatcher + : public FrameHeaderMatcher, + public testing::MatcherInterface<const nghttp2_frame_hd*> { + public: + PointerToFrameHeaderMatcher(int32_t streamid, uint8_t type, + const testing::Matcher<int> flags) + : FrameHeaderMatcher(streamid, type, flags) {} + + bool MatchAndExplain(const nghttp2_frame_hd* frame, + testing::MatchResultListener* listener) const override { + return FrameHeaderMatcher::Match(*frame, listener); + } + + void DescribeTo(std::ostream* os) const override { + FrameHeaderMatcher::DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const override { + FrameHeaderMatcher::DescribeNegationTo(os); + } +}; + +class ReferenceToFrameHeaderMatcher + : public FrameHeaderMatcher, + public testing::MatcherInterface<const nghttp2_frame_hd&> { + public: + ReferenceToFrameHeaderMatcher(int32_t streamid, uint8_t type, + const testing::Matcher<int> flags) + : FrameHeaderMatcher(streamid, type, flags) {} + + bool MatchAndExplain(const nghttp2_frame_hd& frame, + testing::MatchResultListener* listener) const override { + return FrameHeaderMatcher::Match(frame, listener); + } + + void DescribeTo(std::ostream* os) const override { + FrameHeaderMatcher::DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const override { + FrameHeaderMatcher::DescribeNegationTo(os); + } +}; + +class DataMatcher : public testing::MatcherInterface<const nghttp2_frame*> { + public: + DataMatcher(const testing::Matcher<uint32_t> stream_id, + const testing::Matcher<size_t> length, + const testing::Matcher<int> flags) + : stream_id_(stream_id), length_(length), flags_(flags) {} + + bool MatchAndExplain(const nghttp2_frame* frame, + testing::MatchResultListener* listener) const override { + if (frame->hd.type != NGHTTP2_DATA) { + *listener << "; expected DATA frame, saw frame of type " + << static_cast<int>(frame->hd.type); + return false; + } + bool matched = true; + if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) { + matched = false; + } + if (!length_.MatchAndExplain(frame->hd.length, listener)) { + matched = false; + } + if (!flags_.MatchAndExplain(frame->hd.flags, listener)) { + matched = false; + } + return matched; + } + + void DescribeTo(std::ostream* os) const override { + *os << "contains a DATA frame, "; + stream_id_.DescribeTo(os); + length_.DescribeTo(os); + flags_.DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const override { + *os << "does not contain a DATA frame, "; + stream_id_.DescribeNegationTo(os); + length_.DescribeNegationTo(os); + flags_.DescribeNegationTo(os); + } + + private: + const testing::Matcher<uint32_t> stream_id_; + const testing::Matcher<size_t> length_; + const testing::Matcher<int> flags_; +}; + +class HeadersMatcher : public testing::MatcherInterface<const nghttp2_frame*> { + public: + HeadersMatcher(const testing::Matcher<uint32_t> stream_id, + const testing::Matcher<int> flags, + const testing::Matcher<int> category) + : stream_id_(stream_id), flags_(flags), category_(category) {} + + bool MatchAndExplain(const nghttp2_frame* frame, + testing::MatchResultListener* listener) const override { + if (frame->hd.type != NGHTTP2_HEADERS) { + *listener << "; expected HEADERS frame, saw frame of type " + << static_cast<int>(frame->hd.type); + return false; + } + bool matched = true; + if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) { + matched = false; + } + if (!flags_.MatchAndExplain(frame->hd.flags, listener)) { + matched = false; + } + if (!category_.MatchAndExplain(frame->headers.cat, listener)) { + matched = false; + } + return matched; + } + + void DescribeTo(std::ostream* os) const override { + *os << "contains a HEADERS frame, "; + stream_id_.DescribeTo(os); + flags_.DescribeTo(os); + category_.DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const override { + *os << "does not contain a HEADERS frame, "; + stream_id_.DescribeNegationTo(os); + flags_.DescribeNegationTo(os); + category_.DescribeNegationTo(os); + } + + private: + const testing::Matcher<uint32_t> stream_id_; + const testing::Matcher<int> flags_; + const testing::Matcher<int> category_; +}; + +class RstStreamMatcher + : public testing::MatcherInterface<const nghttp2_frame*> { + public: + RstStreamMatcher(const testing::Matcher<uint32_t> stream_id, + const testing::Matcher<uint32_t> error_code) + : stream_id_(stream_id), error_code_(error_code) {} + + bool MatchAndExplain(const nghttp2_frame* frame, + testing::MatchResultListener* listener) const override { + if (frame->hd.type != NGHTTP2_RST_STREAM) { + *listener << "; expected RST_STREAM frame, saw frame of type " + << static_cast<int>(frame->hd.type); + return false; + } + bool matched = true; + if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) { + matched = false; + } + if (!error_code_.MatchAndExplain(frame->rst_stream.error_code, listener)) { + matched = false; + } + return matched; + } + + void DescribeTo(std::ostream* os) const override { + *os << "contains a RST_STREAM frame, "; + stream_id_.DescribeTo(os); + error_code_.DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const override { + *os << "does not contain a RST_STREAM frame, "; + stream_id_.DescribeNegationTo(os); + error_code_.DescribeNegationTo(os); + } + + private: + const testing::Matcher<uint32_t> stream_id_; + const testing::Matcher<uint32_t> error_code_; +}; + +class SettingsMatcher : public testing::MatcherInterface<const nghttp2_frame*> { + public: + SettingsMatcher(const testing::Matcher<std::vector<Http2Setting>> values) + : values_(values) {} + + bool MatchAndExplain(const nghttp2_frame* frame, + testing::MatchResultListener* listener) const override { + if (frame->hd.type != NGHTTP2_SETTINGS) { + *listener << "; expected SETTINGS frame, saw frame of type " + << static_cast<int>(frame->hd.type); + return false; + } + std::vector<Http2Setting> settings; + settings.reserve(frame->settings.niv); + for (size_t i = 0; i < frame->settings.niv; ++i) { + const auto& p = frame->settings.iv[i]; + settings.push_back({static_cast<uint16_t>(p.settings_id), p.value}); + } + return values_.MatchAndExplain(settings, listener); + } + + void DescribeTo(std::ostream* os) const override { + *os << "contains a SETTINGS frame, "; + values_.DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const override { + *os << "does not contain a SETTINGS frame, "; + values_.DescribeNegationTo(os); + } + + private: + const testing::Matcher<std::vector<Http2Setting>> values_; +}; + +class PingMatcher : public testing::MatcherInterface<const nghttp2_frame*> { + public: + PingMatcher(const testing::Matcher<uint64_t> id, bool is_ack) + : id_(id), is_ack_(is_ack) {} + + bool MatchAndExplain(const nghttp2_frame* frame, + testing::MatchResultListener* listener) const override { + if (frame->hd.type != NGHTTP2_PING) { + *listener << "; expected PING frame, saw frame of type " + << static_cast<int>(frame->hd.type); + return false; + } + bool matched = true; + bool frame_ack = frame->hd.flags & NGHTTP2_FLAG_ACK; + if (is_ack_ != frame_ack) { + *listener << "; expected is_ack=" << is_ack_ << ", saw " << frame_ack; + matched = false; + } + uint64_t data; + std::memcpy(&data, frame->ping.opaque_data, sizeof(data)); + data = quiche::QuicheEndian::HostToNet64(data); + if (!id_.MatchAndExplain(data, listener)) { + matched = false; + } + return matched; + } + + void DescribeTo(std::ostream* os) const override { + *os << "contains a PING frame, "; + id_.DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const override { + *os << "does not contain a PING frame, "; + id_.DescribeNegationTo(os); + } + + private: + const testing::Matcher<uint64_t> id_; + const bool is_ack_; +}; + +class GoAwayMatcher : public testing::MatcherInterface<const nghttp2_frame*> { + public: + GoAwayMatcher(const testing::Matcher<uint32_t> last_stream_id, + const testing::Matcher<uint32_t> error_code, + const testing::Matcher<absl::string_view> opaque_data) + : last_stream_id_(last_stream_id), + error_code_(error_code), + opaque_data_(opaque_data) {} + + bool MatchAndExplain(const nghttp2_frame* frame, + testing::MatchResultListener* listener) const override { + if (frame->hd.type != NGHTTP2_GOAWAY) { + *listener << "; expected GOAWAY frame, saw frame of type " + << static_cast<int>(frame->hd.type); + return false; + } + bool matched = true; + if (!last_stream_id_.MatchAndExplain(frame->goaway.last_stream_id, + listener)) { + matched = false; + } + if (!error_code_.MatchAndExplain(frame->goaway.error_code, listener)) { + matched = false; + } + auto opaque_data = + ToStringView(frame->goaway.opaque_data, frame->goaway.opaque_data_len); + if (!opaque_data_.MatchAndExplain(opaque_data, listener)) { + matched = false; + } + return matched; + } + + void DescribeTo(std::ostream* os) const override { + *os << "contains a GOAWAY frame, "; + last_stream_id_.DescribeTo(os); + error_code_.DescribeTo(os); + opaque_data_.DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const override { + *os << "does not contain a GOAWAY frame, "; + last_stream_id_.DescribeNegationTo(os); + error_code_.DescribeNegationTo(os); + opaque_data_.DescribeNegationTo(os); + } + + private: + const testing::Matcher<uint32_t> last_stream_id_; + const testing::Matcher<uint32_t> error_code_; + const testing::Matcher<absl::string_view> opaque_data_; +}; + +class WindowUpdateMatcher + : public testing::MatcherInterface<const nghttp2_frame*> { + public: + WindowUpdateMatcher(const testing::Matcher<uint32_t> delta) : delta_(delta) {} + + bool MatchAndExplain(const nghttp2_frame* frame, + testing::MatchResultListener* listener) const override { + if (frame->hd.type != NGHTTP2_WINDOW_UPDATE) { + *listener << "; expected WINDOW_UPDATE frame, saw frame of type " + << static_cast<int>(frame->hd.type); + return false; + } + return delta_.MatchAndExplain(frame->window_update.window_size_increment, + listener); + } + + void DescribeTo(std::ostream* os) const override { + *os << "contains a WINDOW_UPDATE frame, "; + delta_.DescribeTo(os); + } + + void DescribeNegationTo(std::ostream* os) const override { + *os << "does not contain a WINDOW_UPDATE frame, "; + delta_.DescribeNegationTo(os); + } + + private: + const testing::Matcher<uint32_t> delta_; +}; + +} // namespace + +testing::Matcher<const nghttp2_frame_hd*> HasFrameHeader( + uint32_t streamid, uint8_t type, const testing::Matcher<int> flags) { + return MakeMatcher(new PointerToFrameHeaderMatcher(streamid, type, flags)); +} + +testing::Matcher<const nghttp2_frame_hd&> HasFrameHeaderRef( + uint32_t streamid, uint8_t type, const testing::Matcher<int> flags) { + return MakeMatcher(new ReferenceToFrameHeaderMatcher(streamid, type, flags)); +} + +testing::Matcher<const nghttp2_frame*> IsData( + const testing::Matcher<uint32_t> stream_id, + const testing::Matcher<size_t> length, const testing::Matcher<int> flags) { + return MakeMatcher(new DataMatcher(stream_id, length, flags)); +} + +testing::Matcher<const nghttp2_frame*> IsHeaders( + const testing::Matcher<uint32_t> stream_id, + const testing::Matcher<int> flags, const testing::Matcher<int> category) { + return MakeMatcher(new HeadersMatcher(stream_id, flags, category)); +} + +testing::Matcher<const nghttp2_frame*> IsRstStream( + const testing::Matcher<uint32_t> stream_id, + const testing::Matcher<uint32_t> error_code) { + return MakeMatcher(new RstStreamMatcher(stream_id, error_code)); +} + +testing::Matcher<const nghttp2_frame*> IsSettings( + const testing::Matcher<std::vector<Http2Setting>> values) { + return MakeMatcher(new SettingsMatcher(values)); +} + +testing::Matcher<const nghttp2_frame*> IsPing( + const testing::Matcher<uint64_t> id) { + return MakeMatcher(new PingMatcher(id, false)); +} + +testing::Matcher<const nghttp2_frame*> IsPingAck( + const testing::Matcher<uint64_t> id) { + return MakeMatcher(new PingMatcher(id, true)); +} + +testing::Matcher<const nghttp2_frame*> IsGoAway( + const testing::Matcher<uint32_t> last_stream_id, + const testing::Matcher<uint32_t> error_code, + const testing::Matcher<absl::string_view> opaque_data) { + return MakeMatcher( + new GoAwayMatcher(last_stream_id, error_code, opaque_data)); +} + +testing::Matcher<const nghttp2_frame*> IsWindowUpdate( + const testing::Matcher<uint32_t> delta) { + return MakeMatcher(new WindowUpdateMatcher(delta)); +} + +} // namespace test +} // namespace adapter +} // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_test_utils.h b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_test_utils.h new file mode 100644 index 00000000000..9b772ffda63 --- /dev/null +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_test_utils.h @@ -0,0 +1,99 @@ +#ifndef QUICHE_HTTP2_ADAPTER_NGHTTP2_TEST_UTILS_H_ +#define QUICHE_HTTP2_ADAPTER_NGHTTP2_TEST_UTILS_H_ + +#include <vector> + +#include "absl/strings/string_view.h" +#include "http2/adapter/http2_protocol.h" +#include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" +#include "common/platform/api/quiche_export.h" +#include "common/platform/api/quiche_test.h" + +namespace http2 { +namespace adapter { +namespace test { + +// A simple class that can easily be adapted to act as a nghttp2_data_source. +class QUICHE_NO_EXPORT TestDataSource { + public: + explicit TestDataSource(absl::string_view data) : data_(std::string(data)) {} + + absl::string_view ReadNext(size_t size) { + const size_t to_send = std::min(size, remaining_.size()); + auto ret = remaining_.substr(0, to_send); + remaining_.remove_prefix(to_send); + return ret; + } + + size_t SelectPayloadLength(size_t max_length) { + return std::min(max_length, remaining_.size()); + } + + nghttp2_data_provider MakeDataProvider() { + return nghttp2_data_provider{ + .source = {.ptr = this}, + .read_callback = [](nghttp2_session*, int32_t, uint8_t*, size_t length, + uint32_t* data_flags, nghttp2_data_source* source, + void*) -> ssize_t { + *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY; + auto* s = static_cast<TestDataSource*>(source->ptr); + if (!s->is_data_available()) { + return NGHTTP2_ERR_DEFERRED; + } + const ssize_t ret = s->SelectPayloadLength(length); + if (ret < static_cast<ssize_t>(length)) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } + return ret; + }}; + } + + bool is_data_available() const { return is_data_available_; } + void set_is_data_available(bool value) { is_data_available_ = value; } + + private: + const std::string data_; + absl::string_view remaining_ = data_; + bool is_data_available_ = true; +}; + +// Matchers for nghttp2 data types. +testing::Matcher<const nghttp2_frame_hd*> HasFrameHeader( + uint32_t streamid, uint8_t type, const testing::Matcher<int> flags); +testing::Matcher<const nghttp2_frame_hd&> HasFrameHeaderRef( + uint32_t streamid, uint8_t type, const testing::Matcher<int> flags); + +testing::Matcher<const nghttp2_frame*> IsData( + const testing::Matcher<uint32_t> stream_id, + const testing::Matcher<size_t> length, const testing::Matcher<int> flags); + +testing::Matcher<const nghttp2_frame*> IsHeaders( + const testing::Matcher<uint32_t> stream_id, + const testing::Matcher<int> flags, const testing::Matcher<int> category); + +testing::Matcher<const nghttp2_frame*> IsRstStream( + const testing::Matcher<uint32_t> stream_id, + const testing::Matcher<uint32_t> error_code); + +testing::Matcher<const nghttp2_frame*> IsSettings( + const testing::Matcher<std::vector<Http2Setting>> values); + +testing::Matcher<const nghttp2_frame*> IsPing( + const testing::Matcher<uint64_t> id); + +testing::Matcher<const nghttp2_frame*> IsPingAck( + const testing::Matcher<uint64_t> id); + +testing::Matcher<const nghttp2_frame*> IsGoAway( + const testing::Matcher<uint32_t> last_stream_id, + const testing::Matcher<uint32_t> error_code, + const testing::Matcher<absl::string_view> opaque_data); + +testing::Matcher<const nghttp2_frame*> IsWindowUpdate( + const testing::Matcher<uint32_t> delta); + +} // namespace test +} // namespace adapter +} // namespace http2 + +#endif // QUICHE_HTTP2_ADAPTER_NGHTTP2_TEST_UTILS_H_ diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util.cc index 8d23a55278a..efb811fc35c 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util.cc @@ -2,10 +2,13 @@ #include <cstdint> +#include "absl/strings/str_join.h" #include "absl/strings/string_view.h" +#include "absl/types/span.h" #include "http2/adapter/http2_protocol.h" #include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" #include "common/platform/api/quiche_logging.h" +#include "common/quiche_endian.h" namespace http2 { namespace adapter { @@ -28,11 +31,11 @@ void DeleteSession(nghttp2_session* session) { nghttp2_session_callbacks_unique_ptr MakeCallbacksPtr( nghttp2_session_callbacks* callbacks) { - return nghttp2_session_callbacks_unique_ptr(callbacks, DeleteCallbacks); + return nghttp2_session_callbacks_unique_ptr(callbacks, &DeleteCallbacks); } nghttp2_session_unique_ptr MakeSessionPtr(nghttp2_session* session) { - return nghttp2_session_unique_ptr(session, DeleteSession); + return nghttp2_session_unique_ptr(session, &DeleteSession); } uint8_t* ToUint8Ptr(char* str) { return reinterpret_cast<uint8_t*>(str); } @@ -56,7 +59,8 @@ absl::string_view ToStringView(const uint8_t* pointer, size_t length) { std::vector<nghttp2_nv> GetNghttp2Nvs(absl::Span<const Header> headers) { const int num_headers = headers.size(); - auto nghttp2_nvs = std::vector<nghttp2_nv>(num_headers); + std::vector<nghttp2_nv> nghttp2_nvs; + nghttp2_nvs.reserve(num_headers); for (int i = 0; i < num_headers; ++i) { nghttp2_nv header; uint8_t flags = NGHTTP2_NV_FLAG_NONE; @@ -85,7 +89,8 @@ std::vector<nghttp2_nv> GetResponseNghttp2Nvs( absl::string_view response_code) { // Allocate enough for all headers and also the :status pseudoheader. const int num_headers = headers.size(); - auto nghttp2_nvs = std::vector<nghttp2_nv>(num_headers + 1); + std::vector<nghttp2_nv> nghttp2_nvs; + nghttp2_nvs.reserve(num_headers + 1); // Add the :status pseudoheader first. nghttp2_nv status; @@ -97,7 +102,7 @@ std::vector<nghttp2_nv> GetResponseNghttp2Nvs( nghttp2_nvs.push_back(std::move(status)); // Add the remaining headers. - for (const auto header_pair : headers) { + for (const auto& header_pair : headers) { nghttp2_nv header; header.name = ToUint8Ptr(header_pair.first.data()); header.namelen = header_pair.first.size(); @@ -117,5 +122,148 @@ Http2ErrorCode ToHttp2ErrorCode(uint32_t wire_error_code) { return static_cast<Http2ErrorCode>(wire_error_code); } +class Nghttp2DataFrameSource : public DataFrameSource { + public: + Nghttp2DataFrameSource(nghttp2_data_provider provider, + nghttp2_send_data_callback send_data, + void* user_data) + : provider_(std::move(provider)), + send_data_(std::move(send_data)), + user_data_(user_data) {} + + std::pair<ssize_t, bool> SelectPayloadLength(size_t max_length) override { + const int32_t stream_id = 0; + uint32_t data_flags = 0; + QUICHE_LOG(INFO) << "Invoking read callback"; + ssize_t result = provider_.read_callback( + nullptr /* session */, stream_id, nullptr /* buf */, max_length, + &data_flags, &provider_.source, nullptr /* user_data */); + if (result == NGHTTP2_ERR_DEFERRED) { + return {kBlocked, false}; + } else if (result < 0) { + return {kError, false}; + } else if ((data_flags & NGHTTP2_DATA_FLAG_NO_COPY) == 0) { + QUICHE_LOG(ERROR) << "Source did not use the zero-copy API!"; + return {kError, false}; + } else { + if (data_flags & NGHTTP2_DATA_FLAG_NO_END_STREAM) { + send_fin_ = false; + } + const bool eof = data_flags & NGHTTP2_DATA_FLAG_EOF; + return {result, eof}; + } + } + + bool Send(absl::string_view frame_header, size_t payload_length) override { + nghttp2_frame frame; + frame.hd.type = 0; + frame.hd.length = payload_length; + frame.hd.flags = 0; + frame.hd.stream_id = 0; + frame.data.padlen = 0; + const int result = send_data_( + nullptr /* session */, &frame, ToUint8Ptr(frame_header.data()), + payload_length, &provider_.source, user_data_); + QUICHE_LOG_IF(ERROR, result < 0 && result != NGHTTP2_ERR_WOULDBLOCK) + << "Unexpected error code from send: " << result; + return result == 0; + } + + bool send_fin() const override { return send_fin_; } + + private: + nghttp2_data_provider provider_; + nghttp2_send_data_callback send_data_; + void* user_data_; + bool send_fin_ = true; +}; + +std::unique_ptr<DataFrameSource> MakeZeroCopyDataFrameSource( + nghttp2_data_provider provider, + void* user_data, + nghttp2_send_data_callback send_data) { + return absl::make_unique<Nghttp2DataFrameSource>( + std::move(provider), std::move(send_data), user_data); +} + +absl::string_view ErrorString(uint32_t error_code) { + return Http2ErrorCodeToString(static_cast<Http2ErrorCode>(error_code)); +} + +size_t PaddingLength(uint8_t flags, size_t padlen) { + return (flags & 0x8 ? 1 : 0) + padlen; +} + +struct NvFormatter { + void operator()(std::string* out, const nghttp2_nv& nv) { + absl::StrAppend(out, ToStringView(nv.name, nv.namelen), ": ", + ToStringView(nv.value, nv.valuelen)); + } +}; + +std::string NvsAsString(nghttp2_nv* nva, size_t nvlen) { + return absl::StrJoin(absl::MakeConstSpan(nva, nvlen), ", ", NvFormatter()); +} + +#define HTTP2_FRAME_SEND_LOG QUICHE_VLOG(1) + +void LogBeforeSend(const nghttp2_frame& frame) { + switch (static_cast<FrameType>(frame.hd.type)) { + case FrameType::DATA: + HTTP2_FRAME_SEND_LOG << "Sending DATA on stream " << frame.hd.stream_id + << " with length " + << frame.hd.length - PaddingLength(frame.hd.flags, + frame.data.padlen) + << " and padding " + << PaddingLength(frame.hd.flags, frame.data.padlen); + break; + case FrameType::HEADERS: + HTTP2_FRAME_SEND_LOG << "Sending HEADERS on stream " << frame.hd.stream_id + << " with headers [" + << NvsAsString(frame.headers.nva, + frame.headers.nvlen) + << "]"; + break; + case FrameType::PRIORITY: + HTTP2_FRAME_SEND_LOG << "Sending PRIORITY"; + break; + case FrameType::RST_STREAM: + HTTP2_FRAME_SEND_LOG << "Sending RST_STREAM on stream " + << frame.hd.stream_id << " with error code " + << ErrorString(frame.rst_stream.error_code); + break; + case FrameType::SETTINGS: + HTTP2_FRAME_SEND_LOG << "Sending SETTINGS with " << frame.settings.niv + << " entries, is_ack: " << (frame.hd.flags & 0x01); + break; + case FrameType::PUSH_PROMISE: + HTTP2_FRAME_SEND_LOG << "Sending PUSH_PROMISE"; + break; + case FrameType::PING: { + Http2PingId ping_id; + std::memcpy(&ping_id, frame.ping.opaque_data, sizeof(Http2PingId)); + HTTP2_FRAME_SEND_LOG << "Sending PING with unique_id " + << quiche::QuicheEndian::NetToHost64(ping_id) + << ", is_ack: " << (frame.hd.flags & 0x01); + break; + } + case FrameType::GOAWAY: + HTTP2_FRAME_SEND_LOG << "Sending GOAWAY with last_stream: " + << frame.goaway.last_stream_id << " and error " + << ErrorString(frame.goaway.error_code); + break; + case FrameType::WINDOW_UPDATE: + HTTP2_FRAME_SEND_LOG << "Sending WINDOW_UPDATE on stream " + << frame.hd.stream_id << " with update delta " + << frame.window_update.window_size_increment; + break; + case FrameType::CONTINUATION: + HTTP2_FRAME_SEND_LOG << "Sending CONTINUATION, which is unexpected"; + break; + } +} + +#undef HTTP2_FRAME_SEND_LOG + } // namespace adapter } // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util.h b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util.h index e2c3e7a37c7..78d4702a3c5 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util.h @@ -8,6 +8,7 @@ #include "absl/strings/string_view.h" #include "absl/types/span.h" +#include "http2/adapter/data_source.h" #include "http2/adapter/http2_protocol.h" #include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" #include "spdy/core/spdy_header_block.h" @@ -20,8 +21,8 @@ inline constexpr int kStreamCallbackFailureStatus = NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; inline constexpr int kCancelStatus = NGHTTP2_ERR_CANCEL; -using CallbacksDeleter = void (&)(nghttp2_session_callbacks*); -using SessionDeleter = void (&)(nghttp2_session*); +using CallbacksDeleter = void (*)(nghttp2_session_callbacks*); +using SessionDeleter = void (*)(nghttp2_session*); using nghttp2_session_callbacks_unique_ptr = std::unique_ptr<nghttp2_session_callbacks, CallbacksDeleter>; @@ -55,6 +56,16 @@ std::vector<nghttp2_nv> GetResponseNghttp2Nvs( // based on the RFC 7540 Section 7 suggestion. Http2ErrorCode ToHttp2ErrorCode(uint32_t wire_error_code); +// Transforms a nghttp2_data_provider into a DataFrameSource. Assumes that +// |provider| uses the zero-copy nghttp2_data_source_read_callback API. Unsafe +// otherwise. +std::unique_ptr<DataFrameSource> MakeZeroCopyDataFrameSource( + nghttp2_data_provider provider, + void* user_data, + nghttp2_send_data_callback send_data); + +void LogBeforeSend(const nghttp2_frame& frame); + } // namespace adapter } // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util_test.cc b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util_test.cc new file mode 100644 index 00000000000..1ec00ba32ec --- /dev/null +++ b/chromium/net/third_party/quiche/src/http2/adapter/nghttp2_util_test.cc @@ -0,0 +1,109 @@ +#include "http2/adapter/nghttp2_util.h" + +#include "http2/adapter/nghttp2_test_utils.h" +#include "http2/adapter/test_utils.h" +#include "common/platform/api/quiche_test.h" + +namespace http2 { +namespace adapter { +namespace test { +namespace { + +// This send callback assumes |source|'s pointer is a TestDataSource, and +// |user_data| is a std::string. +int FakeSendCallback(nghttp2_session*, nghttp2_frame* /*frame*/, + const uint8_t* framehd, size_t length, + nghttp2_data_source* source, void* user_data) { + auto* dest = static_cast<std::string*>(user_data); + // Appends the frame header to the string. + absl::StrAppend(dest, ToStringView(framehd, 9)); + auto* test_source = static_cast<TestDataSource*>(source->ptr); + absl::string_view payload = test_source->ReadNext(length); + // Appends the frame payload to the string. + absl::StrAppend(dest, payload); + return 0; +} + +TEST(MakeZeroCopyDataFrameSource, EmptyPayload) { + std::string result; + + const absl::string_view kEmptyBody = ""; + TestDataSource body1{kEmptyBody}; + // The TestDataSource is wrapped in the nghttp2_data_provider data type. + nghttp2_data_provider provider = body1.MakeDataProvider(); + + // This call transforms it back into a DataFrameSource, which is compatible + // with the Http2Adapter API. + std::unique_ptr<DataFrameSource> frame_source = + MakeZeroCopyDataFrameSource(provider, &result, FakeSendCallback); + auto [length, eof] = frame_source->SelectPayloadLength(100); + EXPECT_EQ(length, 0); + EXPECT_TRUE(eof); + frame_source->Send("ninebytes", 0); + EXPECT_EQ(result, "ninebytes"); +} + +TEST(MakeZeroCopyDataFrameSource, ShortPayload) { + std::string result; + + const absl::string_view kShortBody = + "<html><head><title>Example Page!</title></head>" + "<body><div><span><table><tr><th><blink>Wow!!" + "</blink></th></tr></table></span></div></body>" + "</html>"; + TestDataSource body1{kShortBody}; + // The TestDataSource is wrapped in the nghttp2_data_provider data type. + nghttp2_data_provider provider = body1.MakeDataProvider(); + + // This call transforms it back into a DataFrameSource, which is compatible + // with the Http2Adapter API. + std::unique_ptr<DataFrameSource> frame_source = + MakeZeroCopyDataFrameSource(provider, &result, FakeSendCallback); + auto [length, eof] = frame_source->SelectPayloadLength(200); + EXPECT_EQ(length, kShortBody.size()); + EXPECT_TRUE(eof); + frame_source->Send("ninebytes", length); + EXPECT_EQ(result, absl::StrCat("ninebytes", kShortBody)); +} + +TEST(MakeZeroCopyDataFrameSource, MultiFramePayload) { + std::string result; + + const absl::string_view kShortBody = + "<html><head><title>Example Page!</title></head>" + "<body><div><span><table><tr><th><blink>Wow!!" + "</blink></th></tr></table></span></div></body>" + "</html>"; + TestDataSource body1{kShortBody}; + // The TestDataSource is wrapped in the nghttp2_data_provider data type. + nghttp2_data_provider provider = body1.MakeDataProvider(); + + // This call transforms it back into a DataFrameSource, which is compatible + // with the Http2Adapter API. + std::unique_ptr<DataFrameSource> frame_source = + MakeZeroCopyDataFrameSource(provider, &result, FakeSendCallback); + auto ret = frame_source->SelectPayloadLength(50); + EXPECT_EQ(ret.first, 50); + EXPECT_FALSE(ret.second); + frame_source->Send("ninebyte1", ret.first); + + ret = frame_source->SelectPayloadLength(50); + EXPECT_EQ(ret.first, 50); + EXPECT_FALSE(ret.second); + frame_source->Send("ninebyte2", ret.first); + + ret = frame_source->SelectPayloadLength(50); + EXPECT_EQ(ret.first, 44); + EXPECT_TRUE(ret.second); + frame_source->Send("ninebyte3", ret.first); + + EXPECT_EQ(result, + "ninebyte1<html><head><title>Example Page!</title></head><bo" + "ninebyte2dy><div><span><table><tr><th><blink>Wow!!</blink><" + "ninebyte3/th></tr></table></span></div></body></html>"); +} + +} // namespace +} // namespace test +} // namespace adapter +} // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter.cc b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter.cc index 9f287facb9f..27f011ec62f 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter.cc @@ -30,6 +30,10 @@ std::unique_ptr<OgHttp2Adapter> OgHttp2Adapter::Create( OgHttp2Adapter::~OgHttp2Adapter() {} +bool OgHttp2Adapter::IsServerSession() const { + return session_->IsServerSession(); +} + ssize_t OgHttp2Adapter::ProcessBytes(absl::string_view bytes) { return session_->ProcessBytes(bytes); } @@ -54,6 +58,10 @@ void OgHttp2Adapter::SubmitPing(Http2PingId ping_id) { session_->EnqueueFrame(absl::make_unique<SpdyPingIR>(ping_id)); } +void OgHttp2Adapter::SubmitShutdownNotice() { + session_->StartGracefulShutdown(); +} + void OgHttp2Adapter::SubmitGoAway(Http2StreamId last_accepted_stream_id, Http2ErrorCode error_code, absl::string_view opaque_data) { @@ -67,18 +75,45 @@ void OgHttp2Adapter::SubmitWindowUpdate(Http2StreamId stream_id, absl::make_unique<SpdyWindowUpdateIR>(stream_id, window_increment)); } -void OgHttp2Adapter::SubmitMetadata(Http2StreamId stream_id, bool fin) { - QUICHE_BUG(oghttp2_submit_metadata) << "Not implemented"; +void OgHttp2Adapter::SubmitMetadata(Http2StreamId stream_id, + std::unique_ptr<MetadataSource> source) { + session_->SubmitMetadata(stream_id, std::move(source)); } -std::string OgHttp2Adapter::GetBytesToWrite(absl::optional<size_t> max_bytes) { - return session_->GetBytesToWrite(max_bytes); -} +int OgHttp2Adapter::Send() { return session_->Send(); } -int OgHttp2Adapter::GetPeerConnectionWindow() const { +int OgHttp2Adapter::GetSendWindowSize() const { return session_->GetRemoteWindowSize(); } +int OgHttp2Adapter::GetStreamSendWindowSize(Http2StreamId stream_id) const { + return session_->GetStreamSendWindowSize(stream_id); +} + +int OgHttp2Adapter::GetStreamReceiveWindowLimit(Http2StreamId stream_id) const { + return session_->GetStreamReceiveWindowLimit(stream_id); +} + +int OgHttp2Adapter::GetStreamReceiveWindowSize(Http2StreamId stream_id) const { + return session_->GetStreamReceiveWindowSize(stream_id); +} + +int OgHttp2Adapter::GetReceiveWindowSize() const { + return session_->GetReceiveWindowSize(); +} + +int OgHttp2Adapter::GetHpackEncoderDynamicTableSize() const { + return session_->GetHpackEncoderDynamicTableSize(); +} + +int OgHttp2Adapter::GetHpackDecoderDynamicTableSize() const { + return session_->GetHpackDecoderDynamicTableSize(); +} + +Http2StreamId OgHttp2Adapter::GetHighestReceivedStreamId() const { + return session_->GetHighestReceivedStreamId(); +} + void OgHttp2Adapter::MarkDataConsumedForStream(Http2StreamId stream_id, size_t num_bytes) { session_->Consume(stream_id, num_bytes); @@ -90,6 +125,36 @@ void OgHttp2Adapter::SubmitRst(Http2StreamId stream_id, stream_id, TranslateErrorCode(error_code))); } +int32_t OgHttp2Adapter::SubmitRequest( + absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source, void* user_data) { + return session_->SubmitRequest(headers, std::move(data_source), user_data); +} + +int OgHttp2Adapter::SubmitResponse( + Http2StreamId stream_id, absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source) { + return session_->SubmitResponse(stream_id, headers, std::move(data_source)); +} + +int OgHttp2Adapter::SubmitTrailer(Http2StreamId stream_id, + absl::Span<const Header> trailers) { + return session_->SubmitTrailer(stream_id, trailers); +} + +void OgHttp2Adapter::SetStreamUserData(Http2StreamId stream_id, + void* user_data) { + session_->SetStreamUserData(stream_id, user_data); +} + +void* OgHttp2Adapter::GetStreamUserData(Http2StreamId stream_id) { + return session_->GetStreamUserData(stream_id); +} + +bool OgHttp2Adapter::ResumeStream(Http2StreamId stream_id) { + return session_->ResumeStream(stream_id); +} + const Http2Session& OgHttp2Adapter::session() const { return *session_; } diff --git a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter.h b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter.h index 572ba04e9fc..9c7bad3c575 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter.h @@ -6,11 +6,12 @@ #include "http2/adapter/http2_adapter.h" #include "http2/adapter/http2_session.h" #include "http2/adapter/oghttp2_session.h" +#include "common/platform/api/quiche_export.h" namespace http2 { namespace adapter { -class OgHttp2Adapter : public Http2Adapter { +class QUICHE_EXPORT_PRIVATE OgHttp2Adapter : public Http2Adapter { public: using Options = OgHttp2Session::Options; static std::unique_ptr<OgHttp2Adapter> Create(Http2VisitorInterface& visitor, @@ -19,6 +20,7 @@ class OgHttp2Adapter : public Http2Adapter { ~OgHttp2Adapter(); // From Http2Adapter. + bool IsServerSession() const override; ssize_t ProcessBytes(absl::string_view bytes) override; void SubmitSettings(absl::Span<const Http2Setting> settings) override; void SubmitPriorityForStream(Http2StreamId stream_id, @@ -26,17 +28,38 @@ class OgHttp2Adapter : public Http2Adapter { int weight, bool exclusive) override; void SubmitPing(Http2PingId ping_id) override; + void SubmitShutdownNotice() override; void SubmitGoAway(Http2StreamId last_accepted_stream_id, Http2ErrorCode error_code, absl::string_view opaque_data) override; void SubmitWindowUpdate(Http2StreamId stream_id, int window_increment) override; - void SubmitMetadata(Http2StreamId stream_id, bool fin) override; - std::string GetBytesToWrite(absl::optional<size_t> max_bytes) override; - int GetPeerConnectionWindow() const override; + void SubmitMetadata(Http2StreamId stream_id, + std::unique_ptr<MetadataSource> source) override; + int Send() override; + int GetSendWindowSize() const override; + int GetStreamSendWindowSize(Http2StreamId stream_id) const override; + int GetStreamReceiveWindowLimit(Http2StreamId stream_id) const override; + int GetStreamReceiveWindowSize(Http2StreamId stream_id) const override; + int GetReceiveWindowSize() const override; + int GetHpackEncoderDynamicTableSize() const override; + int GetHpackDecoderDynamicTableSize() const override; + Http2StreamId GetHighestReceivedStreamId() const override; void MarkDataConsumedForStream(Http2StreamId stream_id, size_t num_bytes) override; void SubmitRst(Http2StreamId stream_id, Http2ErrorCode error_code) override; + int32_t SubmitRequest(absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source, + void* user_data) override; + int SubmitResponse(Http2StreamId stream_id, absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source) override; + + int SubmitTrailer(Http2StreamId stream_id, + absl::Span<const Header> trailers) override; + + void SetStreamUserData(Http2StreamId stream_id, void* user_data) override; + void* GetStreamUserData(Http2StreamId stream_id) override; + bool ResumeStream(Http2StreamId stream_id) override; const Http2Session& session() const; diff --git a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter_test.cc b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter_test.cc index 4c849f6fd6d..03b45df6aa9 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter_test.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_adapter_test.cc @@ -1,6 +1,8 @@ #include "http2/adapter/oghttp2_adapter.h" +#include "absl/strings/str_join.h" #include "http2/adapter/mock_http2_visitor.h" +#include "http2/adapter/oghttp2_util.h" #include "http2/adapter/test_frame_sequence.h" #include "http2/adapter/test_utils.h" #include "common/platform/api/quiche_test.h" @@ -11,6 +13,23 @@ namespace adapter { namespace test { namespace { +using testing::_; + +enum FrameType { + DATA, + HEADERS, + PRIORITY, + RST_STREAM, + SETTINGS, + PUSH_PROMISE, + PING, + GOAWAY, + WINDOW_UPDATE, + CONTINUATION, +}; + +using spdy::SpdyFrameType; + class OgHttp2AdapterTest : public testing::Test { protected: void SetUp() override { @@ -18,10 +37,14 @@ class OgHttp2AdapterTest : public testing::Test { adapter_ = OgHttp2Adapter::Create(http2_visitor_, options); } - testing::StrictMock<MockHttp2Visitor> http2_visitor_; + DataSavingVisitor http2_visitor_; std::unique_ptr<OgHttp2Adapter> adapter_; }; +TEST_F(OgHttp2AdapterTest, IsServerSession) { + EXPECT_TRUE(adapter_->IsServerSession()); +} + TEST_F(OgHttp2AdapterTest, ProcessBytes) { testing::InSequence seq; EXPECT_CALL(http2_visitor_, OnFrameHeader(0, 0, 4, 0)); @@ -33,13 +56,527 @@ TEST_F(OgHttp2AdapterTest, ProcessBytes) { TestFrameSequence().ClientPreface().Ping(17).Serialize()); } +TEST(OgHttp2AdapterClientTest, ClientHandlesTrailers) { + DataSavingVisitor visitor; + OgHttp2Adapter::Options options{.perspective = Perspective::kClient}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .Headers(1, {{"final-status", "A-OK"}}, + /*fin=*/true) + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, + OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnFrameHeader(1, 26, DATA, 0)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, 26)); + EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body.")); + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, "final-status", "A-OK")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + + const size_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); +} + +TEST(OgHttp2AdapterClientTest, ClientHandlesMetadata) { + DataSavingVisitor visitor; + OgHttp2Adapter::Options options{.perspective = Perspective::kClient}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Metadata(0, "Example connection metadata") + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Metadata(1, "Example stream metadata") + .Data(1, "This is the response body.", true) + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + 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)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, + 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)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, 26)); + EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body.")); + EXPECT_CALL(visitor, OnEndStream(1)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + + const size_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); +} + +TEST(OgHttp2AdapterClientTest, ClientRstStreamWhileHandlingHeaders) { + DataSavingVisitor visitor; + OgHttp2Adapter::Options options{.perspective = Perspective::kClient}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, + OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT")) + .WillOnce(testing::DoAll( + testing::InvokeWithoutArgs([&adapter]() { + adapter->SubmitRst(1, Http2ErrorCode::REFUSED_STREAM); + }), + testing::Return(Http2VisitorInterface::HEADER_RST_STREAM))); + EXPECT_CALL(visitor, OnFrameHeader(1, _, DATA, 0)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, _)); + EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body.")); + + const size_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, stream_id1, 4, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, stream_id1, 4, 0x0, + static_cast<int>(Http2ErrorCode::REFUSED_STREAM))); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::RST_STREAM})); +} + +TEST(OgHttp2AdapterClientTest, ClientConnectionErrorWhileHandlingHeaders) { + DataSavingVisitor visitor; + OgHttp2Adapter::Options options{.perspective = Perspective::kClient}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, + OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT")) + .WillOnce( + testing::Return(Http2VisitorInterface::HEADER_CONNECTION_ERROR)); + EXPECT_CALL(visitor, OnConnectionError()); + // Note: OgHttp2Adapter continues processing bytes until the input is + // complete. + EXPECT_CALL(visitor, OnFrameHeader(1, _, DATA, 0)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, _)); + EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body.")); + + const size_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); +} + +TEST(OgHttp2AdapterClientTest, ClientRejectsHeaders) { + DataSavingVisitor visitor; + OgHttp2Adapter::Options options{.perspective = Perspective::kClient}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)) + .WillOnce(testing::Return(false)); + // Rejecting headers leads to a connection error. + EXPECT_CALL(visitor, OnConnectionError()); + // Note: OgHttp2Adapter continues processing bytes until the input is + // complete. + EXPECT_CALL(visitor, OnFrameHeader(1, _, DATA, 0)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, _)); + EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body.")); + + const size_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_result, stream_frames.size()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); +} + +// TODO(birenroy): Validate headers and re-enable this test. The library should +// invoke OnErrorDebug() with an error message for the invalid header. The +// library should also invoke OnInvalidFrame() for the invalid HEADERS frame. +TEST(OgHttp2AdapterClientTest, DISABLED_ClientHandlesInvalidTrailers) { + DataSavingVisitor visitor; + OgHttp2Adapter::Options options{.perspective = Perspective::kClient}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + testing::InSequence s; + + const std::vector<const Header> headers1 = + ToHeaders({{":method", "GET"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}); + + const char* kSentinel1 = "arbitrary pointer 1"; + const int32_t stream_id1 = + adapter->SubmitRequest(headers1, nullptr, const_cast<char*>(kSentinel1)); + ASSERT_GT(stream_id1, 0); + QUICHE_LOG(INFO) << "Created stream: " << stream_id1; + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id1, _, 0x5, 0)); + + int result = adapter->Send(); + EXPECT_EQ(0, result); + absl::string_view data = visitor.data(); + EXPECT_THAT(data, testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + data.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(data, EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::HEADERS})); + visitor.Clear(); + + const std::string stream_frames = + TestFrameSequence() + .ServerPreface() + .Headers(1, + {{":status", "200"}, + {"server", "my-fake-server"}, + {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, + /*fin=*/false) + .Data(1, "This is the response body.") + .Headers(1, {{":bad-status", "9000"}}, + /*fin=*/true) + .Serialize(); + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, + OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnFrameHeader(1, 26, DATA, 0)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, 26)); + EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body.")); + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + + // Bad status trailer will cause a PROTOCOL_ERROR. The header is never + // delivered in an OnHeaderForStream callback. + + const size_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), stream_result); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, stream_id1, 4, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(RST_STREAM, stream_id1, 4, 0x0, 1)); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::PROTOCOL_ERROR)); + + EXPECT_TRUE(adapter->session().want_write()); + result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::RST_STREAM})); +} + TEST_F(OgHttp2AdapterTest, SubmitMetadata) { - EXPECT_QUICHE_BUG(adapter_->SubmitMetadata(3, true), "Not implemented"); + auto source = absl::make_unique<TestMetadataSource>(ToHeaderBlock(ToHeaders( + {{"query-cost", "is too darn high"}, {"secret-sauce", "hollandaise"}}))); + adapter_->SubmitMetadata(1, std::move(source)); + EXPECT_TRUE(adapter_->session().want_write()); + + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(kMetadataFrameType, 1, _, 0x4)); + EXPECT_CALL(http2_visitor_, OnFrameSent(kMetadataFrameType, 1, _, 0x4, 0)); + + int result = adapter_->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT( + http2_visitor_.data(), + EqualsFrames({spdy::SpdyFrameType::SETTINGS, + static_cast<spdy::SpdyFrameType>(kMetadataFrameType)})); + EXPECT_FALSE(adapter_->session().want_write()); +} + +TEST_F(OgHttp2AdapterTest, SubmitConnectionMetadata) { + auto source = absl::make_unique<TestMetadataSource>(ToHeaderBlock(ToHeaders( + {{"query-cost", "is too darn high"}, {"secret-sauce", "hollandaise"}}))); + adapter_->SubmitMetadata(0, std::move(source)); + EXPECT_TRUE(adapter_->session().want_write()); + + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(kMetadataFrameType, 0, _, 0x4)); + EXPECT_CALL(http2_visitor_, OnFrameSent(kMetadataFrameType, 0, _, 0x4, 0)); + + int result = adapter_->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT( + http2_visitor_.data(), + EqualsFrames({spdy::SpdyFrameType::SETTINGS, + static_cast<spdy::SpdyFrameType>(kMetadataFrameType)})); + EXPECT_FALSE(adapter_->session().want_write()); } -TEST_F(OgHttp2AdapterTest, GetPeerConnectionWindow) { - const int peer_window = adapter_->GetPeerConnectionWindow(); - EXPECT_GT(peer_window, 0); +TEST_F(OgHttp2AdapterTest, GetSendWindowSize) { + const int peer_window = adapter_->GetSendWindowSize(); + EXPECT_EQ(peer_window, kInitialFlowControlWindowSize); } TEST_F(OgHttp2AdapterTest, MarkDataConsumedForStream) { @@ -62,12 +599,27 @@ TEST_F(OgHttp2AdapterTest, TestSerialize) { adapter_->SubmitWindowUpdate(3, 127); EXPECT_TRUE(adapter_->session().want_write()); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(PRIORITY, 3, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(PRIORITY, 3, _, 0x0, 0)); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(RST_STREAM, 3, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(RST_STREAM, 3, _, 0x0, 0x8)); + EXPECT_CALL(http2_visitor_, OnCloseStream(3, Http2ErrorCode::NO_ERROR)); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(PING, 0, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(PING, 0, _, 0x0, 0)); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(GOAWAY, 0, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(GOAWAY, 0, _, 0x0, 0)); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(WINDOW_UPDATE, 3, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(WINDOW_UPDATE, 3, _, 0x0, 0)); + + int result = adapter_->Send(); + EXPECT_EQ(0, result); EXPECT_THAT( - adapter_->GetBytesToWrite(absl::nullopt), - EqualsFrames( - {spdy::SpdyFrameType::SETTINGS, spdy::SpdyFrameType::PRIORITY, - spdy::SpdyFrameType::RST_STREAM, spdy::SpdyFrameType::PING, - spdy::SpdyFrameType::GOAWAY, spdy::SpdyFrameType::WINDOW_UPDATE})); + http2_visitor_.data(), + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::PRIORITY, + SpdyFrameType::RST_STREAM, SpdyFrameType::PING, + SpdyFrameType::GOAWAY, SpdyFrameType::WINDOW_UPDATE})); EXPECT_FALSE(adapter_->session().want_write()); } @@ -80,14 +632,271 @@ TEST_F(OgHttp2AdapterTest, TestPartialSerialize) { adapter_->SubmitPing(42); EXPECT_TRUE(adapter_->session().want_write()); - const std::string first_part = adapter_->GetBytesToWrite(10); + http2_visitor_.set_send_limit(20); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + int result = adapter_->Send(); + EXPECT_EQ(0, result); + EXPECT_TRUE(adapter_->session().want_write()); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(GOAWAY, 0, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(GOAWAY, 0, _, 0x0, 0)); + result = adapter_->Send(); + EXPECT_EQ(0, result); EXPECT_TRUE(adapter_->session().want_write()); - const std::string second_part = adapter_->GetBytesToWrite(absl::nullopt); + EXPECT_CALL(http2_visitor_, OnBeforeFrameSent(PING, 0, _, 0x0)); + EXPECT_CALL(http2_visitor_, OnFrameSent(PING, 0, _, 0x0, 0)); + result = adapter_->Send(); + EXPECT_EQ(0, result); EXPECT_FALSE(adapter_->session().want_write()); - EXPECT_THAT( - absl::StrCat(first_part, second_part), - EqualsFrames({spdy::SpdyFrameType::SETTINGS, spdy::SpdyFrameType::GOAWAY, - spdy::SpdyFrameType::PING})); + EXPECT_THAT(http2_visitor_.data(), + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::GOAWAY, + SpdyFrameType::PING})); +} + +TEST(OgHttp2AdapterServerTest, ClientSendsContinuation) { + 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() + .Headers(1, + {{":method", "GET"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}, + /*fin=*/true, + /*add_continuation=*/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()); + // Stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 1)); + 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)); + EXPECT_CALL(visitor, OnEndStream(1)); + + const size_t result = adapter->ProcessBytes(frames); + 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}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + EXPECT_FALSE(adapter->session().want_write()); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "GET"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}, + /*fin=*/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()); + // Stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + + const size_t result = adapter->ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + const absl::string_view kBody = "This is an example response body."; + + // The body source must indicate that the end of the body is not the end of + // the stream. + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, false); + body1->AppendPayload(kBody); + body1->EndData(); + int submit_result = adapter->SubmitResponse( + 1, ToHeaders({{":status", "200"}, {"x-comment", "Sure, sounds good."}}), + std::move(body1)); + EXPECT_EQ(submit_result, 0); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0)); + + int send_result = adapter->Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), + EqualsFrames( + {spdy::SpdyFrameType::SETTINGS, spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::HEADERS, spdy::SpdyFrameType::DATA})); + EXPECT_THAT(visitor.data(), testing::HasSubstr(kBody)); + visitor.Clear(); + EXPECT_FALSE(adapter->session().want_write()); + + // The body source has been exhausted by the call to Send() above. + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + int trailer_result = + adapter->SubmitTrailer(1, ToHeaders({{":final-status", "a-ok"}})); + ASSERT_EQ(trailer_result, 0); + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x5, 0)); + + send_result = adapter->Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS})); +} + +TEST(OgHttp2AdapterServerTest, ServerErrorWhileHandlingHeaders) { + DataSavingVisitor visitor; + OgHttp2Adapter::Options options{.perspective = Perspective::kServer}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "POST"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}, + {"accept", "some bogus value!"}}, + /*fin=*/false) + .WindowUpdate(1, 2000) + .Data(1, "This is the request body.") + .WindowUpdate(0, 2000) + .Serialize(); + testing::InSequence s; + + // Client preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnHeaderForStream(1, "accept", "some bogus value!")) + .WillOnce(testing::Return(Http2VisitorInterface::HEADER_RST_STREAM)); + EXPECT_CALL(visitor, OnFrameHeader(1, 4, WINDOW_UPDATE, 0)); + EXPECT_CALL(visitor, OnWindowUpdate(1, 2000)); + EXPECT_CALL(visitor, OnFrameHeader(1, _, DATA, 0)); + EXPECT_CALL(visitor, OnBeginDataForStream(1, _)); + EXPECT_CALL(visitor, OnDataForStream(1, "This is the request body.")); + EXPECT_CALL(visitor, OnFrameHeader(0, 4, WINDOW_UPDATE, 0)); + EXPECT_CALL(visitor, OnWindowUpdate(0, 2000)); + + const size_t result = adapter->ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + EXPECT_TRUE(adapter->session().want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, 0, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, 0, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, 1, 4, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, 1, 4, 0x0, + static_cast<int>(Http2ErrorCode::INTERNAL_ERROR))); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + + int send_result = adapter->Send(); + // Some bytes should have been serialized. + EXPECT_EQ(0, send_result); + // SETTINGS ack + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::RST_STREAM})); } } // namespace diff --git a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session.cc b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session.cc index 2222fed4b6b..3a32b2e70ea 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session.cc @@ -1,29 +1,150 @@ #include "http2/adapter/oghttp2_session.h" +#include <tuple> + #include "absl/strings/escaping.h" +#include "http2/adapter/oghttp2_util.h" namespace http2 { namespace adapter { +namespace { + +const size_t kMaxMetadataFrameSize = 16384; + +// TODO(birenroy): Consider incorporating spdy::FlagsSerializionVisitor here. +class FrameAttributeCollector : public spdy::SpdyFrameVisitor { + public: + FrameAttributeCollector() = default; + void VisitData(const spdy::SpdyDataIR& data) override { + frame_type_ = static_cast<uint8_t>(data.frame_type()); + stream_id_ = data.stream_id(); + length_ = + data.data_len() + (data.padded() ? 1 : 0) + data.padding_payload_len(); + flags_ = (data.fin() ? 0x1 : 0) | (data.padded() ? 0x8 : 0); + } + void VisitHeaders(const spdy::SpdyHeadersIR& headers) override { + frame_type_ = static_cast<uint8_t>(headers.frame_type()); + stream_id_ = headers.stream_id(); + length_ = headers.size() - spdy::kFrameHeaderSize; + flags_ = 0x4 | (headers.fin() ? 0x1 : 0) | (headers.padded() ? 0x8 : 0) | + (headers.has_priority() ? 0x20 : 0); + } + void VisitPriority(const spdy::SpdyPriorityIR& priority) override { + frame_type_ = static_cast<uint8_t>(priority.frame_type()); + frame_type_ = 2; + length_ = 5; + stream_id_ = priority.stream_id(); + } + void VisitRstStream(const spdy::SpdyRstStreamIR& rst_stream) override { + frame_type_ = static_cast<uint8_t>(rst_stream.frame_type()); + frame_type_ = 3; + length_ = 4; + stream_id_ = rst_stream.stream_id(); + error_code_ = rst_stream.error_code(); + } + void VisitSettings(const spdy::SpdySettingsIR& settings) override { + frame_type_ = static_cast<uint8_t>(settings.frame_type()); + frame_type_ = 4; + length_ = 6 * settings.values().size(); + flags_ = (settings.is_ack() ? 0x1 : 0); + } + void VisitPushPromise(const spdy::SpdyPushPromiseIR& push_promise) override { + frame_type_ = static_cast<uint8_t>(push_promise.frame_type()); + frame_type_ = 5; + length_ = push_promise.size() - spdy::kFrameHeaderSize; + stream_id_ = push_promise.stream_id(); + flags_ = (push_promise.padded() ? 0x8 : 0); + } + void VisitPing(const spdy::SpdyPingIR& ping) override { + frame_type_ = static_cast<uint8_t>(ping.frame_type()); + frame_type_ = 6; + length_ = 8; + flags_ = (ping.is_ack() ? 0x1 : 0); + } + void VisitGoAway(const spdy::SpdyGoAwayIR& goaway) override { + frame_type_ = static_cast<uint8_t>(goaway.frame_type()); + frame_type_ = 7; + length_ = goaway.size() - spdy::kFrameHeaderSize; + error_code_ = goaway.error_code(); + } + void VisitWindowUpdate( + const spdy::SpdyWindowUpdateIR& window_update) override { + frame_type_ = static_cast<uint8_t>(window_update.frame_type()); + frame_type_ = 8; + length_ = 4; + stream_id_ = window_update.stream_id(); + } + void VisitContinuation( + const spdy::SpdyContinuationIR& continuation) override { + frame_type_ = static_cast<uint8_t>(continuation.frame_type()); + stream_id_ = continuation.stream_id(); + flags_ = continuation.end_headers() ? 0x4 : 0; + length_ = continuation.size() - spdy::kFrameHeaderSize; + } + void VisitUnknown(const spdy::SpdyUnknownIR& unknown) override { + frame_type_ = static_cast<uint8_t>(unknown.frame_type()); + stream_id_ = unknown.stream_id(); + flags_ = unknown.flags(); + length_ = unknown.size() - spdy::kFrameHeaderSize; + } + void VisitAltSvc(const spdy::SpdyAltSvcIR& /*altsvc*/) override {} + void VisitPriorityUpdate( + const spdy::SpdyPriorityUpdateIR& /*priority_update*/) override {} + void VisitAcceptCh(const spdy::SpdyAcceptChIR& /*accept_ch*/) override {} + + uint32_t stream_id() { return stream_id_; } + uint32_t length() { return length_; } + uint32_t error_code() { return error_code_; } + uint8_t frame_type() { return frame_type_; } + uint8_t flags() { return flags_; } + + private: + uint32_t stream_id_ = 0; + uint32_t length_ = 0; + uint32_t error_code_ = 0; + uint8_t frame_type_ = 0; + uint8_t flags_ = 0; +}; + +} // namespace + void OgHttp2Session::PassthroughHeadersHandler::OnHeaderBlockStart() { - visitor_.OnBeginHeadersForStream(stream_id_); + const bool status = visitor_.OnBeginHeadersForStream(stream_id_); + if (!status) { + result_ = Http2VisitorInterface::HEADER_CONNECTION_ERROR; + } } void OgHttp2Session::PassthroughHeadersHandler::OnHeader( absl::string_view key, absl::string_view value) { - visitor_.OnHeaderForStream(stream_id_, key, value); + if (result_ == Http2VisitorInterface::HEADER_OK) { + result_ = visitor_.OnHeaderForStream(stream_id_, key, value); + } } void OgHttp2Session::PassthroughHeadersHandler::OnHeaderBlockEnd( size_t /* uncompressed_header_bytes */, size_t /* compressed_header_bytes */) { - visitor_.OnEndHeadersForStream(stream_id_); + if (result_ == Http2VisitorInterface::HEADER_OK) { + visitor_.OnEndHeadersForStream(stream_id_); + } else { + session_.OnHeaderStatus(stream_id_, result_); + } } OgHttp2Session::OgHttp2Session(Http2VisitorInterface& visitor, Options options) - : visitor_(visitor), headers_handler_(visitor), options_(options) { + : visitor_(visitor), + headers_handler_(*this, visitor), + connection_window_manager_(kInitialFlowControlWindowSize, + [this](size_t window_update_delta) { + SendWindowUpdate(kConnectionStreamId, + window_update_delta); + }), + options_(options) { decoder_.set_visitor(this); + decoder_.set_extension_visitor(this); if (options_.perspective == Perspective::kServer) { remaining_preface_ = {spdy::kHttp2ConnectionHeaderPrefix, spdy::kHttp2ConnectionHeaderPrefixSize}; @@ -32,6 +153,70 @@ OgHttp2Session::OgHttp2Session(Http2VisitorInterface& visitor, Options options) OgHttp2Session::~OgHttp2Session() {} +void OgHttp2Session::SetStreamUserData(Http2StreamId stream_id, + void* user_data) { + auto it = stream_map_.find(stream_id); + if (it != stream_map_.end()) { + it->second.user_data = user_data; + } +} + +void* OgHttp2Session::GetStreamUserData(Http2StreamId stream_id) { + auto it = stream_map_.find(stream_id); + if (it != stream_map_.end()) { + return it->second.user_data; + } + return nullptr; +} + +bool OgHttp2Session::ResumeStream(Http2StreamId stream_id) { + auto it = stream_map_.find(stream_id); + if (it->second.outbound_body == nullptr || + !write_scheduler_.StreamRegistered(stream_id)) { + return false; + } + write_scheduler_.MarkStreamReady(stream_id, /*add_to_front=*/false); + return true; +} + +int OgHttp2Session::GetStreamSendWindowSize(Http2StreamId stream_id) const { + auto it = stream_map_.find(stream_id); + if (it != stream_map_.end()) { + return it->second.send_window; + } + return -1; +} + +int OgHttp2Session::GetStreamReceiveWindowLimit(Http2StreamId stream_id) const { + auto it = stream_map_.find(stream_id); + if (it != stream_map_.end()) { + return it->second.window_manager.WindowSizeLimit(); + } + return -1; +} + +int OgHttp2Session::GetStreamReceiveWindowSize(Http2StreamId stream_id) const { + auto it = stream_map_.find(stream_id); + if (it != stream_map_.end()) { + return it->second.window_manager.CurrentWindowSize(); + } + return -1; +} + +int OgHttp2Session::GetReceiveWindowSize() const { + return connection_window_manager_.CurrentWindowSize(); +} + +int OgHttp2Session::GetHpackEncoderDynamicTableSize() const { + const spdy::HpackEncoder* encoder = framer_.GetHpackEncoder(); + return encoder == nullptr ? 0 : encoder->GetDynamicTableSize(); +} + +int OgHttp2Session::GetHpackDecoderDynamicTableSize() const { + const spdy::HpackDecoderAdapter* decoder = decoder_.GetHpackDecoder(); + return decoder == nullptr ? 0 : decoder->GetDynamicTableSize(); +} + ssize_t OgHttp2Session::ProcessBytes(absl::string_view bytes) { ssize_t preface_consumed = 0; if (!remaining_preface_.empty()) { @@ -68,27 +253,317 @@ int OgHttp2Session::Consume(Http2StreamId stream_id, size_t num_bytes) { } else { it->second.window_manager.MarkDataFlushed(num_bytes); } + connection_window_manager_.MarkDataFlushed(num_bytes); return 0; // Remove? } +void OgHttp2Session::StartGracefulShutdown() { + if (options_.perspective == Perspective::kServer) { + if (!queued_goaway_) { + EnqueueFrame(absl::make_unique<spdy::SpdyGoAwayIR>( + std::numeric_limits<int32_t>::max(), spdy::ERROR_CODE_NO_ERROR, + "graceful_shutdown")); + } + } else { + QUICHE_LOG(ERROR) << "Graceful shutdown not needed for clients."; + } +} + void OgHttp2Session::EnqueueFrame(std::unique_ptr<spdy::SpdyFrameIR> frame) { + if (frame->frame_type() == spdy::SpdyFrameType::GOAWAY) { + queued_goaway_ = true; + } else if (frame->frame_type() == spdy::SpdyFrameType::RST_STREAM) { + streams_reset_.insert(frame->stream_id()); + auto iter = stream_map_.find(frame->stream_id()); + if (iter != stream_map_.end()) { + iter->second.half_closed_local = true; + } + } frames_.push_back(std::move(frame)); } -std::string OgHttp2Session::GetBytesToWrite(absl::optional<size_t> max_bytes) { - const size_t serialized_max = - max_bytes ? max_bytes.value() : std::numeric_limits<size_t>::max(); - std::string serialized = std::move(serialized_prefix_); - while (serialized.size() < serialized_max && !frames_.empty()) { - spdy::SpdySerializedFrame frame = framer_.SerializeFrame(*frames_.front()); - absl::StrAppend(&serialized, absl::string_view(frame)); - frames_.pop_front(); +int OgHttp2Session::Send() { + MaybeSetupPreface(); + ssize_t result = std::numeric_limits<ssize_t>::max(); + // Flush any serialized prefix. + while (result > 0 && !serialized_prefix_.empty()) { + result = visitor_.OnReadyToSend(serialized_prefix_); + if (result > 0) { + serialized_prefix_.erase(0, result); + } + } + if (!serialized_prefix_.empty()) { + return result < 0 ? result : 0; + } + bool continue_writing = SendQueuedFrames(); + while (continue_writing && !connection_metadata_.empty()) { + continue_writing = SendMetadata(0, connection_metadata_); + } + // Wake streams for writes. + while (continue_writing && write_scheduler_.HasReadyStreams() && + connection_send_window_ > 0) { + const Http2StreamId stream_id = write_scheduler_.PopNextReadyStream(); + // TODO(birenroy): Add a return value to indicate write blockage, so streams + // aren't woken unnecessarily. + continue_writing = WriteForStream(stream_id); + } + if (continue_writing) { + SendQueuedFrames(); + } + return 0; +} + +bool OgHttp2Session::SendQueuedFrames() { + // Serialize and send frames in the queue. + while (!frames_.empty()) { + const auto& frame_ptr = frames_.front(); + FrameAttributeCollector c; + frame_ptr->Visit(&c); + visitor_.OnBeforeFrameSent(c.frame_type(), c.stream_id(), c.length(), + c.flags()); + spdy::SpdySerializedFrame frame = framer_.SerializeFrame(*frame_ptr); + const ssize_t result = visitor_.OnReadyToSend(absl::string_view(frame)); + if (result < 0) { + visitor_.OnConnectionError(); + return false; + } else if (result == 0) { + // Write blocked. + return false; + } else { + visitor_.OnFrameSent(c.frame_type(), c.stream_id(), c.length(), c.flags(), + c.error_code()); + if (static_cast<FrameType>(c.frame_type()) == FrameType::RST_STREAM) { + // If this endpoint is resetting the stream, the stream should be + // closed. This endpoint is already aware of the outbound RST_STREAM and + // its error code, so close with NO_ERROR. + visitor_.OnCloseStream(c.stream_id(), Http2ErrorCode::NO_ERROR); + } + + frames_.pop_front(); + if (static_cast<size_t>(result) < frame.size()) { + // The frame was partially written, so the rest must be buffered. + serialized_prefix_.assign(frame.data() + result, frame.size() - result); + return false; + } + } + } + return true; +} + +bool OgHttp2Session::WriteForStream(Http2StreamId stream_id) { + auto it = stream_map_.find(stream_id); + if (it == stream_map_.end()) { + QUICHE_LOG(ERROR) << "Can't find stream " << stream_id + << " which is ready to write!"; + return true; + } + StreamState& state = it->second; + bool connection_can_write = true; + if (!state.outbound_metadata.empty()) { + connection_can_write = SendMetadata(stream_id, state.outbound_metadata); + } + + if (state.outbound_body == nullptr) { + // No data to send, but there might be trailers. + if (state.trailers != nullptr) { + auto block_ptr = std::move(state.trailers); + if (state.half_closed_local) { + QUICHE_LOG(ERROR) << "Sent fin; can't send trailers."; + } else { + SendTrailers(stream_id, std::move(*block_ptr)); + MaybeCloseWithRstStream(stream_id, state); + } + } + return true; + } + bool source_can_produce = true; + int32_t available_window = std::min( + std::min(connection_send_window_, state.send_window), max_frame_payload_); + while (connection_can_write && available_window > 0 && + state.outbound_body != nullptr) { + ssize_t length; + bool end_data; + std::tie(length, end_data) = + state.outbound_body->SelectPayloadLength(available_window); + if (length == 0 && !end_data) { + source_can_produce = false; + break; + } else if (length == DataFrameSource::kError) { + source_can_produce = false; + visitor_.OnCloseStream(stream_id, Http2ErrorCode::INTERNAL_ERROR); + break; + } + const bool fin = end_data ? state.outbound_body->send_fin() : false; + if (length > 0 || fin) { + spdy::SpdyDataIR data(stream_id); + data.set_fin(fin); + data.SetDataShallow(length); + spdy::SpdySerializedFrame header = + spdy::SpdyFramer::SerializeDataFrameHeaderWithPaddingLengthField( + data); + QUICHE_DCHECK(serialized_prefix_.empty() && frames_.empty()); + const bool success = + state.outbound_body->Send(absl::string_view(header), length); + if (!success) { + connection_can_write = false; + break; + } + visitor_.OnFrameSent(/* DATA */ 0, stream_id, length, fin ? 0x1 : 0x0, 0); + connection_send_window_ -= length; + state.send_window -= length; + available_window = + std::min(std::min(connection_send_window_, state.send_window), + max_frame_payload_); + } + if (end_data) { + bool sent_trailers = false; + if (state.trailers != nullptr) { + auto block_ptr = std::move(state.trailers); + if (fin) { + QUICHE_LOG(ERROR) << "Sent fin; can't send trailers."; + } else { + SendTrailers(stream_id, std::move(*block_ptr)); + sent_trailers = true; + } + } + state.outbound_body = nullptr; + if (fin || sent_trailers) { + MaybeCloseWithRstStream(stream_id, state); + } + } + } + // If the stream still has data to send, it should be marked as ready in the + // write scheduler. + if (source_can_produce && state.send_window > 0 && + state.outbound_body != nullptr) { + write_scheduler_.MarkStreamReady(stream_id, false); + } + // Streams can continue writing as long as the connection is not write-blocked + // and there is additional flow control quota available. + return connection_can_write && available_window > 0; +} + +bool OgHttp2Session::SendMetadata(Http2StreamId stream_id, + OgHttp2Session::MetadataSequence& sequence) { + auto payload_buffer = absl::make_unique<uint8_t[]>(kMaxMetadataFrameSize); + while (!sequence.empty()) { + MetadataSource& source = *sequence.front(); + + ssize_t written; + bool end_metadata; + std::tie(written, end_metadata) = + source.Pack(payload_buffer.get(), kMaxMetadataFrameSize); + if (written < 0) { + // Did not touch the connection, so perhaps writes are still possible. + return true; + } + QUICHE_DCHECK_LE(static_cast<size_t>(written), kMaxMetadataFrameSize); + auto payload = absl::string_view( + reinterpret_cast<const char*>(payload_buffer.get()), written); + EnqueueFrame(absl::make_unique<spdy::SpdyUnknownIR>( + stream_id, kMetadataFrameType, end_metadata ? kMetadataEndFlag : 0u, + std::string(payload))); + if (end_metadata) { + sequence.erase(sequence.begin()); + } + } + return SendQueuedFrames(); +} + +int32_t OgHttp2Session::SubmitRequest( + absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source, void* user_data) { + // TODO(birenroy): return an error for the incorrect perspective + const Http2StreamId stream_id = next_stream_id_; + next_stream_id_ += 2; + // Convert headers to header block, create headers frame. + auto frame = + absl::make_unique<spdy::SpdyHeadersIR>(stream_id, ToHeaderBlock(headers)); + // Add data source and user data to stream state + auto iter = CreateStream(stream_id); + write_scheduler_.MarkStreamReady(stream_id, false); + if (data_source == nullptr) { + frame->set_fin(true); + iter->second.half_closed_local = true; + } else { + iter->second.outbound_body = std::move(data_source); + } + iter->second.user_data = user_data; + // Enqueue headers frame + EnqueueFrame(std::move(frame)); + return stream_id; +} + +int OgHttp2Session::SubmitResponse( + Http2StreamId stream_id, absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source) { + // TODO(birenroy): return an error for the incorrect perspective + auto iter = stream_map_.find(stream_id); + if (iter == stream_map_.end()) { + QUICHE_LOG(ERROR) << "Unable to find stream " << stream_id; + return -501; // NGHTTP2_ERR_INVALID_ARGUMENT + } + // Convert headers to header block, create headers frame + auto frame = + absl::make_unique<spdy::SpdyHeadersIR>(stream_id, ToHeaderBlock(headers)); + if (data_source == nullptr) { + frame->set_fin(true); + if (iter->second.half_closed_remote) { + visitor_.OnCloseStream(stream_id, Http2ErrorCode::NO_ERROR); + } + // TODO(birenroy): the server adapter should probably delete stream state + // when calling visitor_.OnCloseStream. + } else { + // Add data source to stream state + iter->second.outbound_body = std::move(data_source); + write_scheduler_.MarkStreamReady(stream_id, false); + } + EnqueueFrame(std::move(frame)); + return 0; +} + +int OgHttp2Session::SubmitTrailer(Http2StreamId stream_id, + absl::Span<const Header> trailers) { + // TODO(birenroy): Reject trailers when acting as a client? + auto iter = stream_map_.find(stream_id); + if (iter == stream_map_.end()) { + QUICHE_LOG(ERROR) << "Unable to find stream " << stream_id; + return -501; // NGHTTP2_ERR_INVALID_ARGUMENT + } + StreamState& state = iter->second; + if (state.half_closed_local) { + QUICHE_LOG(ERROR) << "Stream " << stream_id << " is half closed (local)"; + return -514; // NGHTTP2_ERR_INVALID_STREAM_STATE } - if (serialized.size() > serialized_max) { - serialized_prefix_ = serialized.substr(serialized_max); - serialized.resize(serialized_max); + if (state.trailers != nullptr) { + QUICHE_LOG(ERROR) << "Stream " << stream_id + << " already has trailers queued"; + return -514; // NGHTTP2_ERR_INVALID_STREAM_STATE + } + if (state.outbound_body == nullptr) { + // Enqueue trailers immediately. + SendTrailers(stream_id, ToHeaderBlock(trailers)); + MaybeCloseWithRstStream(stream_id, state); + } else { + QUICHE_LOG_IF(ERROR, state.outbound_body->send_fin()) + << "DataFrameSource will send fin, preventing trailers!"; + // Save trailers so they can be written once data is done. + state.trailers = + absl::make_unique<spdy::SpdyHeaderBlock>(ToHeaderBlock(trailers)); + write_scheduler_.MarkStreamReady(stream_id, false); + } + return 0; +} + +void OgHttp2Session::SubmitMetadata(Http2StreamId stream_id, + std::unique_ptr<MetadataSource> source) { + if (stream_id == 0) { + connection_metadata_.push_back(std::move(source)); + } else { + auto iter = CreateStream(stream_id); + iter->second.outbound_metadata.push_back(std::move(source)); + write_scheduler_.MarkStreamReady(stream_id, false); } - return serialized; } void OgHttp2Session::OnError(http2::Http2DecoderAdapter::SpdyFramerError error, @@ -103,32 +578,47 @@ void OgHttp2Session::OnCommonHeader(spdy::SpdyStreamId stream_id, size_t length, uint8_t type, uint8_t flags) { + highest_received_stream_id_ = std::max(static_cast<Http2StreamId>(stream_id), + highest_received_stream_id_); visitor_.OnFrameHeader(stream_id, length, type, flags); } void OgHttp2Session::OnDataFrameHeader(spdy::SpdyStreamId stream_id, - size_t length, - bool fin) { + size_t length, bool /*fin*/) { visitor_.OnBeginDataForStream(stream_id, length); } void OgHttp2Session::OnStreamFrameData(spdy::SpdyStreamId stream_id, const char* data, size_t len) { + MarkDataBuffered(stream_id, len); visitor_.OnDataForStream(stream_id, absl::string_view(data, len)); } void OgHttp2Session::OnStreamEnd(spdy::SpdyStreamId stream_id) { + auto iter = stream_map_.find(stream_id); + if (iter != stream_map_.end()) { + iter->second.half_closed_remote = true; + } visitor_.OnEndStream(stream_id); + if (iter != stream_map_.end() && iter->second.half_closed_local && + options_.perspective == Perspective::kClient) { + // From the client's perspective, the stream can be closed if it's already + // half_closed_local. + visitor_.OnCloseStream(stream_id, Http2ErrorCode::NO_ERROR); + } } -void OgHttp2Session::OnStreamPadLength(spdy::SpdyStreamId /*stream_id*/, - size_t /*value*/) { - // TODO(181586191): handle padding +void OgHttp2Session::OnStreamPadLength(spdy::SpdyStreamId stream_id, + size_t value) { + MarkDataBuffered(stream_id, 1 + value); + // TODO(181586191): Pass padding to the visitor? } -void OgHttp2Session::OnStreamPadding(spdy::SpdyStreamId stream_id, size_t len) { - // TODO(181586191): handle padding +void OgHttp2Session::OnStreamPadding(spdy::SpdyStreamId /*stream_id*/, size_t + /*len*/) { + // Flow control was accounted for in OnStreamPadLength(). + // TODO(181586191): Pass padding to the visitor? } spdy::SpdyHeadersHandlerInterface* OgHttp2Session::OnHeaderFrameStart( @@ -137,13 +627,21 @@ spdy::SpdyHeadersHandlerInterface* OgHttp2Session::OnHeaderFrameStart( return &headers_handler_; } -void OgHttp2Session::OnHeaderFrameEnd(spdy::SpdyStreamId stream_id) { +void OgHttp2Session::OnHeaderFrameEnd(spdy::SpdyStreamId /*stream_id*/) { headers_handler_.set_stream_id(0); } void OgHttp2Session::OnRstStream(spdy::SpdyStreamId stream_id, spdy::SpdyErrorCode error_code) { + auto iter = stream_map_.find(stream_id); + if (iter != stream_map_.end()) { + iter->second.half_closed_remote = true; + iter->second.outbound_body = nullptr; + write_scheduler_.UnregisterStream(stream_id); + } visitor_.OnRstStream(stream_id, TranslateErrorCode(error_code)); + // TODO(birenroy): Consider bundling "close stream" behavior into a dedicated + // method that also cleans up the stream map. visitor_.OnCloseStream(stream_id, TranslateErrorCode(error_code)); } @@ -153,10 +651,16 @@ void OgHttp2Session::OnSettings() { void OgHttp2Session::OnSetting(spdy::SpdySettingsId id, uint32_t value) { visitor_.OnSetting({id, value}); + if (id == kMetadataExtensionId) { + peer_supports_metadata_ = (value != 0); + } } void OgHttp2Session::OnSettingsEnd() { visitor_.OnSettingsEnd(); + auto settings = absl::make_unique<spdy::SpdySettingsIR>(); + settings->set_is_ack(true); + EnqueueFrame(std::move(settings)); } void OgHttp2Session::OnSettingsAck() { @@ -174,39 +678,46 @@ void OgHttp2Session::OnGoAway(spdy::SpdyStreamId last_accepted_stream_id, ""); } -bool OgHttp2Session::OnGoAwayFrameData(const char* goaway_data, size_t len) { +bool OgHttp2Session::OnGoAwayFrameData(const char* /*goaway_data*/, size_t + /*len*/) { // Opaque data is currently ignored. return true; } void OgHttp2Session::OnHeaders(spdy::SpdyStreamId stream_id, - bool has_priority, - int weight, - spdy::SpdyStreamId parent_stream_id, - bool exclusive, - bool fin, - bool end) {} + bool /*has_priority*/, int /*weight*/, + spdy::SpdyStreamId /*parent_stream_id*/, + bool /*exclusive*/, bool /*fin*/, bool /*end*/) { + if (options_.perspective == Perspective::kServer) { + CreateStream(stream_id); + } +} void OgHttp2Session::OnWindowUpdate(spdy::SpdyStreamId stream_id, int delta_window_size) { if (stream_id == 0) { - peer_window_ += delta_window_size; + connection_send_window_ += delta_window_size; } else { auto it = stream_map_.find(stream_id); if (it == stream_map_.end()) { QUICHE_VLOG(1) << "Stream " << stream_id << " not found!"; } else { + if (it->second.send_window == 0) { + // The stream was blocked on flow control. + write_scheduler_.MarkStreamReady(stream_id, false); + } it->second.send_window += delta_window_size; } } visitor_.OnWindowUpdate(stream_id, delta_window_size); } -void OgHttp2Session::OnPushPromise(spdy::SpdyStreamId stream_id, - spdy::SpdyStreamId promised_stream_id, - bool end) {} +void OgHttp2Session::OnPushPromise(spdy::SpdyStreamId /*stream_id*/, + spdy::SpdyStreamId /*promised_stream_id*/, + bool /*end*/) {} -void OgHttp2Session::OnContinuation(spdy::SpdyStreamId stream_id, bool end) {} +void OgHttp2Session::OnContinuation(spdy::SpdyStreamId /*stream_id*/, bool + /*end*/) {} void OgHttp2Session::OnAltSvc(spdy::SpdyStreamId /*stream_id*/, absl::string_view /*origin*/, @@ -214,18 +725,141 @@ void OgHttp2Session::OnAltSvc(spdy::SpdyStreamId /*stream_id*/, AlternativeServiceVector& /*altsvc_vector*/) { } -void OgHttp2Session::OnPriority(spdy::SpdyStreamId stream_id, - spdy::SpdyStreamId parent_stream_id, - int weight, - bool exclusive) {} +void OgHttp2Session::OnPriority(spdy::SpdyStreamId /*stream_id*/, + spdy::SpdyStreamId /*parent_stream_id*/, + int /*weight*/, bool /*exclusive*/) {} -void OgHttp2Session::OnPriorityUpdate(spdy::SpdyStreamId prioritized_stream_id, - absl::string_view priority_field_value) {} +void OgHttp2Session::OnPriorityUpdate( + spdy::SpdyStreamId /*prioritized_stream_id*/, + absl::string_view /*priority_field_value*/) {} -bool OgHttp2Session::OnUnknownFrame(spdy::SpdyStreamId stream_id, - uint8_t frame_type) { +bool OgHttp2Session::OnUnknownFrame(spdy::SpdyStreamId /*stream_id*/, + uint8_t /*frame_type*/) { return true; } +void OgHttp2Session::OnHeaderStatus( + Http2StreamId stream_id, Http2VisitorInterface::OnHeaderResult result) { + QUICHE_DCHECK_NE(result, Http2VisitorInterface::HEADER_OK); + if (result == Http2VisitorInterface::HEADER_RST_STREAM) { + auto it = streams_reset_.find(stream_id); + if (it == streams_reset_.end()) { + EnqueueFrame(absl::make_unique<spdy::SpdyRstStreamIR>( + stream_id, spdy::ERROR_CODE_INTERNAL_ERROR)); + } + } else if (result == Http2VisitorInterface::HEADER_CONNECTION_ERROR) { + visitor_.OnConnectionError(); + } +} + +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) { + serialized_prefix_.assign(spdy::kHttp2ConnectionHeaderPrefix, + spdy::kHttp2ConnectionHeaderPrefixSize); + } + // First frame must be a non-ack SETTINGS. + if (frames_.empty() || + frames_.front()->frame_type() != spdy::SpdyFrameType::SETTINGS || + reinterpret_cast<spdy::SpdySettingsIR*>(frames_.front().get()) + ->is_ack()) { + frames_.push_front(absl::make_unique<spdy::SpdySettingsIR>()); + } + queued_preface_ = true; + } +} + +void OgHttp2Session::SendWindowUpdate(Http2StreamId stream_id, + size_t update_delta) { + EnqueueFrame( + absl::make_unique<spdy::SpdyWindowUpdateIR>(stream_id, update_delta)); +} + +void OgHttp2Session::SendTrailers(Http2StreamId stream_id, + spdy::SpdyHeaderBlock trailers) { + auto frame = + absl::make_unique<spdy::SpdyHeadersIR>(stream_id, std::move(trailers)); + frame->set_fin(true); + EnqueueFrame(std::move(frame)); +} + +void OgHttp2Session::MaybeCloseWithRstStream(Http2StreamId stream_id, + StreamState& state) { + state.half_closed_local = true; + if (options_.perspective == Perspective::kServer) { + if (state.half_closed_remote) { + visitor_.OnCloseStream(stream_id, Http2ErrorCode::NO_ERROR); + } else { + // Since the peer has not yet ended the stream, this endpoint should + // send a RST_STREAM NO_ERROR. See RFC 7540 Section 8.1. + EnqueueFrame(absl::make_unique<spdy::SpdyRstStreamIR>( + stream_id, spdy::SpdyErrorCode::ERROR_CODE_NO_ERROR)); + // Enqueuing the RST_STREAM also invokes OnCloseStream. + } + // TODO(birenroy): the server adapter should probably delete stream state + // when calling visitor_.OnCloseStream. + } +} + +void OgHttp2Session::MarkDataBuffered(Http2StreamId stream_id, size_t bytes) { + connection_window_manager_.MarkDataBuffered(bytes); + auto it = stream_map_.find(stream_id); + if (it != stream_map_.end()) { + it->second.window_manager.MarkDataBuffered(bytes); + } +} + +OgHttp2Session::StreamStateMap::iterator OgHttp2Session::CreateStream( + Http2StreamId stream_id) { + WindowManager::WindowUpdateListener listener = + [this, stream_id](size_t window_update_delta) { + SendWindowUpdate(stream_id, window_update_delta); + }; + absl::flat_hash_map<Http2StreamId, StreamState>::iterator iter; + bool inserted; + std::tie(iter, inserted) = stream_map_.try_emplace( + stream_id, + StreamState(stream_receive_window_limit_, std::move(listener))); + if (inserted) { + // Add the stream to the write scheduler. + const WriteScheduler::StreamPrecedenceType precedence(3); + write_scheduler_.RegisterStream(stream_id, precedence); + } + return iter; +} + } // namespace adapter } // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session.h b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session.h index b0bc28b7002..77b9da73c38 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session.h @@ -3,11 +3,14 @@ #include <list> +#include "http2/adapter/data_source.h" #include "http2/adapter/http2_session.h" #include "http2/adapter/http2_util.h" #include "http2/adapter/http2_visitor_interface.h" #include "http2/adapter/window_manager.h" +#include "http2/core/priority_write_scheduler.h" #include "common/platform/api/quiche_bug_tracker.h" +#include "common/platform/api/quiche_export.h" #include "spdy/core/http2_frame_decoder_adapter.h" #include "spdy/core/spdy_framer.h" @@ -15,33 +18,80 @@ namespace http2 { namespace adapter { // This class manages state associated with a single multiplexed HTTP/2 session. -class OgHttp2Session : public Http2Session, - public spdy::SpdyFramerVisitorInterface { +class QUICHE_EXPORT_PRIVATE OgHttp2Session + : public Http2Session, + public spdy::SpdyFramerVisitorInterface, + public spdy::ExtensionVisitorInterface { public: - struct Options { + struct QUICHE_EXPORT_PRIVATE Options { Perspective perspective = Perspective::kClient; }; - OgHttp2Session(Http2VisitorInterface& visitor, Options /*options*/); + OgHttp2Session(Http2VisitorInterface& visitor, Options options); ~OgHttp2Session() override; // Enqueues a frame for transmission to the peer. void EnqueueFrame(std::unique_ptr<spdy::SpdyFrameIR> frame); - // If |want_write()| returns true, this method will return a non-empty string - // containing serialized HTTP/2 frames to write to the peer. - std::string GetBytesToWrite(absl::optional<size_t> max_bytes); + // Starts a graceful shutdown sequence. No-op if a GOAWAY has already been + // sent. + void StartGracefulShutdown(); + + // Invokes the visitor's OnReadyToSend() method for serialized frame data. + int Send(); + + int32_t SubmitRequest(absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source, + void* user_data); + int SubmitResponse(Http2StreamId stream_id, absl::Span<const Header> headers, + std::unique_ptr<DataFrameSource> data_source); + int SubmitTrailer(Http2StreamId stream_id, absl::Span<const Header> trailers); + void SubmitMetadata(Http2StreamId stream_id, + std::unique_ptr<MetadataSource> source); + + bool IsServerSession() const { + return options_.perspective == Perspective::kServer; + } + Http2StreamId GetHighestReceivedStreamId() const { + return highest_received_stream_id_; + } + void SetStreamUserData(Http2StreamId stream_id, void* user_data); + void* GetStreamUserData(Http2StreamId stream_id); + + // Resumes a stream that was previously blocked. Returns true on success. + bool ResumeStream(Http2StreamId stream_id); + + // Returns the peer's outstanding stream receive window for the given stream. + int GetStreamSendWindowSize(Http2StreamId stream_id) const; + + // Returns the current upper bound on the flow control receive window for this + // stream. + int GetStreamReceiveWindowLimit(Http2StreamId stream_id) const; + + // Returns the outstanding stream receive window, or -1 if the stream does not + // exist. + int GetStreamReceiveWindowSize(Http2StreamId stream_id) const; + + // Returns the outstanding connection receive window. + int GetReceiveWindowSize() const; + + // Returns the size of the HPACK encoder's dynamic table, including the + // per-entry overhead from the specification. + int GetHpackEncoderDynamicTableSize() const; + + // Returns the size of the HPACK decoder's dynamic table, including the + // per-entry overhead from the specification. + int GetHpackDecoderDynamicTableSize() const; // From Http2Session. ssize_t ProcessBytes(absl::string_view bytes) override; int Consume(Http2StreamId stream_id, size_t num_bytes) override; bool want_read() const override { return !received_goaway_; } bool want_write() const override { - return !frames_.empty() || !serialized_prefix_.empty(); - } - int GetRemoteWindowSize() const override { - return peer_window_; + return !frames_.empty() || !serialized_prefix_.empty() || + write_scheduler_.HasReadyStreams() || !connection_metadata_.empty(); } + int GetRemoteWindowSize() const override { return connection_send_window_; } // From SpdyFramerVisitorInterface void OnError(http2::Http2DecoderAdapter::SpdyFramerError error, @@ -72,7 +122,7 @@ class OgHttp2Session : public Http2Session, void OnPing(spdy::SpdyPingId unique_id, bool is_ack) override; void OnGoAway(spdy::SpdyStreamId last_accepted_stream_id, spdy::SpdyErrorCode error_code) override; - bool OnGoAwayFrameData(const char* goaway_data, size_t len); + bool OnGoAwayFrameData(const char* goaway_data, size_t len) override; void OnHeaders(spdy::SpdyStreamId stream_id, bool has_priority, int weight, @@ -86,10 +136,9 @@ class OgHttp2Session : public Http2Session, spdy::SpdyStreamId promised_stream_id, bool end) override; void OnContinuation(spdy::SpdyStreamId stream_id, bool end) override; - void OnAltSvc(spdy::SpdyStreamId /*stream_id*/, - absl::string_view /*origin*/, + void OnAltSvc(spdy::SpdyStreamId /*stream_id*/, absl::string_view /*origin*/, const spdy::SpdyAltSvcWireFormat:: - AlternativeServiceVector& /*altsvc_vector*/); + AlternativeServiceVector& /*altsvc_vector*/) override; void OnPriority(spdy::SpdyStreamId stream_id, spdy::SpdyStreamId parent_stream_id, int weight, @@ -99,40 +148,136 @@ class OgHttp2Session : public Http2Session, bool OnUnknownFrame(spdy::SpdyStreamId stream_id, uint8_t frame_type) override; + // Invoked when header processing encounters an invalid or otherwise + // problematic header. + 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: - struct StreamState { + using MetadataSequence = std::vector<std::unique_ptr<MetadataSource>>; + struct QUICHE_EXPORT_PRIVATE StreamState { + StreamState(int32_t stream_receive_window, + WindowManager::WindowUpdateListener listener) + : window_manager(stream_receive_window, std::move(listener)) {} + WindowManager window_manager; - int32_t send_window = 65535; + std::unique_ptr<DataFrameSource> outbound_body; + MetadataSequence outbound_metadata; + std::unique_ptr<spdy::SpdyHeaderBlock> trailers; + void* user_data = nullptr; + int32_t send_window = kInitialFlowControlWindowSize; bool half_closed_local = false; bool half_closed_remote = false; }; + using StreamStateMap = absl::flat_hash_map<Http2StreamId, StreamState>; - class PassthroughHeadersHandler : public spdy::SpdyHeadersHandlerInterface { + class QUICHE_EXPORT_PRIVATE PassthroughHeadersHandler + : public spdy::SpdyHeadersHandlerInterface { public: - explicit PassthroughHeadersHandler(Http2VisitorInterface& visitor) - : visitor_(visitor) {} - void set_stream_id(Http2StreamId stream_id) { stream_id_ = stream_id; } + explicit PassthroughHeadersHandler(OgHttp2Session& session, + Http2VisitorInterface& visitor) + : session_(session), visitor_(visitor) {} + void set_stream_id(Http2StreamId stream_id) { + stream_id_ = stream_id; + result_ = Http2VisitorInterface::HEADER_OK; + } void OnHeaderBlockStart() override; void OnHeader(absl::string_view key, absl::string_view value) override; void OnHeaderBlockEnd(size_t /* uncompressed_header_bytes */, size_t /* compressed_header_bytes */) override; private: + OgHttp2Session& session_; Http2VisitorInterface& visitor_; Http2StreamId stream_id_ = 0; + Http2VisitorInterface::OnHeaderResult result_ = + Http2VisitorInterface::HEADER_OK; }; + // Queues the connection preface, if not already done. + void MaybeSetupPreface(); + + void SendWindowUpdate(Http2StreamId stream_id, size_t update_delta); + + // Sends queued frames, returning true if all frames were flushed + // successfully. + bool SendQueuedFrames(); + + // Returns false if the connection is write-blocked (due to flow control or + // some other reason). + bool WriteForStream(Http2StreamId stream_id); + + bool SendMetadata(Http2StreamId stream_id, MetadataSequence& sequence); + + void SendTrailers(Http2StreamId stream_id, spdy::SpdyHeaderBlock trailers); + + // Encapsulates the RST_STREAM NO_ERROR behavior described in RFC 7540 + // Section 8.1. + void MaybeCloseWithRstStream(Http2StreamId stream_id, StreamState& state); + + // Performs flow control accounting for data sent by the peer. + void MarkDataBuffered(Http2StreamId stream_id, size_t bytes); + + // Creates a stream and returns an iterator pointing to it. + StreamStateMap::iterator CreateStream(Http2StreamId stream_id); + + // Receives events when inbound frames are parsed. Http2VisitorInterface& visitor_; + + // Encodes outbound frames. spdy::SpdyFramer framer_{spdy::SpdyFramer::ENABLE_COMPRESSION}; + + // Decodes inbound frames. http2::Http2DecoderAdapter decoder_; - absl::flat_hash_map<Http2StreamId, StreamState> stream_map_; + + // Maintains the state of all streams known to this session. + StreamStateMap stream_map_; + + // Maintains the queue of outbound frames, and any serialized bytes that have + // not yet been consumed. std::list<std::unique_ptr<spdy::SpdyFrameIR>> frames_; - PassthroughHeadersHandler headers_handler_; std::string serialized_prefix_; + + // Maintains the set of streams ready to write data to the peer. + using WriteScheduler = PriorityWriteScheduler<Http2StreamId>; + WriteScheduler write_scheduler_; + + // Delivers header name-value pairs to the visitor. + PassthroughHeadersHandler headers_handler_; + + // Tracks the remaining client connection preface, in the case of a server + // session. absl::string_view remaining_preface_; - int peer_window_ = 65535; + + WindowManager connection_window_manager_; + + absl::flat_hash_set<Http2StreamId> streams_reset_; + + MetadataSequence connection_metadata_; + + 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; + int max_frame_payload_ = 16384; 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; }; } // namespace adapter diff --git a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session_test.cc b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session_test.cc index ab155697d5d..3f24ea200ef 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session_test.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_session_test.cc @@ -2,6 +2,7 @@ #include "http2/adapter/mock_http2_visitor.h" #include "http2/adapter/test_frame_sequence.h" +#include "http2/adapter/test_utils.h" #include "common/platform/api/quiche_test.h" namespace http2 { @@ -9,6 +10,7 @@ namespace adapter { namespace test { namespace { +using spdy::SpdyFrameType; using testing::_; enum FrameType { @@ -31,7 +33,9 @@ TEST(OgHttp2SessionTest, ClientConstruction) { visitor, OgHttp2Session::Options{.perspective = Perspective::kClient}); EXPECT_TRUE(session.want_read()); EXPECT_FALSE(session.want_write()); - EXPECT_EQ(session.GetRemoteWindowSize(), kDefaultInitialStreamWindowSize); + EXPECT_EQ(session.GetRemoteWindowSize(), kInitialFlowControlWindowSize); + EXPECT_FALSE(session.IsServerSession()); + EXPECT_EQ(0, session.GetHighestReceivedStreamId()); } TEST(OgHttp2SessionTest, ClientHandlesFrames) { @@ -56,43 +60,433 @@ TEST(OgHttp2SessionTest, ClientHandlesFrames) { EXPECT_CALL(visitor, OnFrameHeader(0, 4, WINDOW_UPDATE, 0)); EXPECT_CALL(visitor, OnWindowUpdate(0, 1000)); - const ssize_t initial_result = session.ProcessBytes(initial_frames); + const size_t initial_result = session.ProcessBytes(initial_frames); EXPECT_EQ(initial_frames.size(), initial_result); EXPECT_EQ(session.GetRemoteWindowSize(), - kDefaultInitialStreamWindowSize + 1000); + kInitialFlowControlWindowSize + 1000); + EXPECT_EQ(0, session.GetHighestReceivedStreamId()); + + // Connection has not yet received any data. + EXPECT_EQ(kInitialFlowControlWindowSize, session.GetReceiveWindowSize()); + + EXPECT_EQ(0, session.GetHpackDecoderDynamicTableSize()); // Should OgHttp2Session require that streams 1 and 3 have been created? + // Submit a request to ensure the first stream is created. + const char* kSentinel1 = "arbitrary pointer 1"; + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, true); + body1->AppendPayload("This is an example request body."); + body1->EndData(); + int stream_id = + session.SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + std::move(body1), const_cast<char*>(kSentinel1)); + EXPECT_EQ(stream_id, 1); + const std::string stream_frames = TestFrameSequence() - .Headers(1, + .Headers(stream_id, {{":status", "200"}, {"server", "my-fake-server"}, {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}}, /*fin=*/false) - .Data(1, "This is the response body.") + .Data(stream_id, "This is the response body.") .RstStream(3, Http2ErrorCode::INTERNAL_ERROR) .GoAway(5, Http2ErrorCode::ENHANCE_YOUR_CALM, "calm down!!") .Serialize(); - EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 4)); - EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); - EXPECT_CALL(visitor, OnHeaderForStream(1, ":status", "200")); - EXPECT_CALL(visitor, OnHeaderForStream(1, "server", "my-fake-server")); + EXPECT_CALL(visitor, OnFrameHeader(stream_id, _, HEADERS, 4)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(stream_id)); + EXPECT_CALL(visitor, OnHeaderForStream(stream_id, ":status", "200")); EXPECT_CALL(visitor, - OnHeaderForStream(1, "date", "Tue, 6 Apr 2021 12:54:01 GMT")); - EXPECT_CALL(visitor, OnEndHeadersForStream(1)); - EXPECT_CALL(visitor, OnFrameHeader(1, 26, DATA, 0)); - EXPECT_CALL(visitor, OnBeginDataForStream(1, 26)); - EXPECT_CALL(visitor, OnDataForStream(1, "This is the response body.")); + OnHeaderForStream(stream_id, "server", "my-fake-server")); + EXPECT_CALL(visitor, OnHeaderForStream(stream_id, "date", + "Tue, 6 Apr 2021 12:54:01 GMT")); + EXPECT_CALL(visitor, OnEndHeadersForStream(stream_id)); + EXPECT_CALL(visitor, OnFrameHeader(stream_id, 26, DATA, 0)); + EXPECT_CALL(visitor, OnBeginDataForStream(stream_id, 26)); + EXPECT_CALL(visitor, + OnDataForStream(stream_id, "This is the response body.")); EXPECT_CALL(visitor, OnFrameHeader(3, 4, RST_STREAM, 0)); EXPECT_CALL(visitor, OnRstStream(3, Http2ErrorCode::INTERNAL_ERROR)); EXPECT_CALL(visitor, OnCloseStream(3, Http2ErrorCode::INTERNAL_ERROR)); EXPECT_CALL(visitor, OnFrameHeader(0, 19, GOAWAY, 0)); EXPECT_CALL(visitor, OnGoAway(5, Http2ErrorCode::ENHANCE_YOUR_CALM, "")); - const ssize_t stream_result = session.ProcessBytes(stream_frames); + const size_t stream_result = session.ProcessBytes(stream_frames); EXPECT_EQ(stream_frames.size(), stream_result); + EXPECT_EQ(3, session.GetHighestReceivedStreamId()); + + // The first stream is active and has received some data. + EXPECT_GT(kInitialFlowControlWindowSize, + session.GetStreamReceiveWindowSize(stream_id)); + // Connection receive window is equivalent to the first stream's. + EXPECT_EQ(session.GetReceiveWindowSize(), + session.GetStreamReceiveWindowSize(stream_id)); + // Receive window upper bound is still the initial value. + EXPECT_EQ(kInitialFlowControlWindowSize, + session.GetStreamReceiveWindowLimit(stream_id)); + + EXPECT_GT(session.GetHpackDecoderDynamicTableSize(), 0); +} + +// Verifies that a client session enqueues initial SETTINGS if Send() is called +// before any frames are explicitly queued. +TEST(OgHttp2SessionTest, ClientEnqueuesSettingsOnSend) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kClient}); + EXPECT_FALSE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + + int result = session.Send(); + EXPECT_EQ(0, result); + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(serialized, EqualsFrames({SpdyFrameType::SETTINGS})); +} + +// Verifies that a client session enqueues initial SETTINGS before whatever +// frame type is passed to the first invocation of EnqueueFrame(). +TEST(OgHttp2SessionTest, ClientEnqueuesSettingsBeforeOtherFrame) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kClient}); + EXPECT_FALSE(session.want_write()); + session.EnqueueFrame(absl::make_unique<spdy::SpdyPingIR>(42)); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(PING, 0, 8, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(PING, 0, 8, 0x0, 0)); + + int result = session.Send(); + EXPECT_EQ(0, result); + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(serialized, + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::PING})); +} + +// Verifies that if the first call to EnqueueFrame() passes a SETTINGS frame, +// the client session will not enqueue an additional SETTINGS frame. +TEST(OgHttp2SessionTest, ClientEnqueuesSettingsOnce) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kClient}); + EXPECT_FALSE(session.want_write()); + session.EnqueueFrame(absl::make_unique<spdy::SpdySettingsIR>()); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + + int result = session.Send(); + EXPECT_EQ(0, result); + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(serialized, EqualsFrames({SpdyFrameType::SETTINGS})); +} + +TEST(OgHttp2SessionTest, ClientSubmitRequest) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kClient}); + + EXPECT_FALSE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + + // Even though the user has not queued any frames for the session, it should + // still send the connection preface. + int result = session.Send(); + EXPECT_EQ(0, result); + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + // Initial SETTINGS. + EXPECT_THAT(serialized, EqualsFrames({SpdyFrameType::SETTINGS})); + visitor.Clear(); + + const std::string initial_frames = + TestFrameSequence().ServerPreface().Serialize(); + testing::InSequence s; + + // Server preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + const size_t initial_result = session.ProcessBytes(initial_frames); + EXPECT_EQ(initial_frames.size(), initial_result); + + // Session will want to write a SETTINGS ack. + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + + result = session.Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::SETTINGS})); + visitor.Clear(); + + EXPECT_EQ(0, session.GetHpackEncoderDynamicTableSize()); + + const char* kSentinel1 = "arbitrary pointer 1"; + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, true); + body1->AppendPayload("This is an example request body."); + body1->EndData(); + int stream_id = + session.SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + std::move(body1), const_cast<char*>(kSentinel1)); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(session.want_write()); + EXPECT_EQ(kSentinel1, session.GetStreamUserData(stream_id)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, stream_id, _, 0x1, 0)); + + result = session.Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS, + spdy::SpdyFrameType::DATA})); + visitor.Clear(); + EXPECT_FALSE(session.want_write()); + + // Some data was sent, so the remaining send window size should be less than + // the default. + EXPECT_LT(session.GetStreamSendWindowSize(stream_id), + kInitialFlowControlWindowSize); + EXPECT_GT(session.GetStreamSendWindowSize(stream_id), 0); + // Send window for a nonexistent stream is not available. + EXPECT_EQ(-1, session.GetStreamSendWindowSize(stream_id + 2)); + + EXPECT_GT(session.GetHpackEncoderDynamicTableSize(), 0); + + stream_id = + session.SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/two"}}), + nullptr, nullptr); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(session.want_write()); + const char* kSentinel2 = "arbitrary pointer 2"; + EXPECT_EQ(nullptr, session.GetStreamUserData(stream_id)); + session.SetStreamUserData(stream_id, const_cast<char*>(kSentinel2)); + EXPECT_EQ(kSentinel2, session.GetStreamUserData(stream_id)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x5, 0)); + + result = session.Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::HEADERS})); + + // No data was sent (just HEADERS), so the remaining send window size should + // still be the default. + EXPECT_EQ(session.GetStreamSendWindowSize(stream_id), + kInitialFlowControlWindowSize); +} + +// This test exercises the case where the client request body source is read +// blocked. +TEST(OgHttp2SessionTest, ClientSubmitRequestWithReadBlock) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kClient}); + EXPECT_FALSE(session.want_write()); + + const char* kSentinel1 = "arbitrary pointer 1"; + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, true); + TestDataFrameSource* body_ref = body1.get(); + int stream_id = + session.SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + std::move(body1), const_cast<char*>(kSentinel1)); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(session.want_write()); + EXPECT_EQ(kSentinel1, session.GetStreamUserData(stream_id)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x4, 0)); + + int result = session.Send(); + EXPECT_EQ(0, result); + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(serialized, + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::HEADERS})); + // No data frame, as body1 was read blocked. + visitor.Clear(); + EXPECT_FALSE(session.want_write()); + + body_ref->AppendPayload("This is an example request body."); + body_ref->EndData(); + EXPECT_TRUE(session.ResumeStream(stream_id)); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnFrameSent(DATA, stream_id, _, 0x1, 0)); + + result = session.Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::DATA})); + EXPECT_FALSE(session.want_write()); + + // Stream data is done, so this stream cannot be resumed. + EXPECT_FALSE(session.ResumeStream(stream_id)); + EXPECT_FALSE(session.want_write()); +} + +// This test exercises the case where the client request body source is read +// blocked, then ends with an empty DATA frame. +TEST(OgHttp2SessionTest, ClientSubmitRequestEmptyDataWithFin) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kClient}); + EXPECT_FALSE(session.want_write()); + + const char* kSentinel1 = "arbitrary pointer 1"; + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, true); + TestDataFrameSource* body_ref = body1.get(); + int stream_id = + session.SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + std::move(body1), const_cast<char*>(kSentinel1)); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(session.want_write()); + EXPECT_EQ(kSentinel1, session.GetStreamUserData(stream_id)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x4, 0)); + + int result = session.Send(); + EXPECT_EQ(0, result); + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(serialized, + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::HEADERS})); + // No data frame, as body1 was read blocked. + visitor.Clear(); + EXPECT_FALSE(session.want_write()); + + body_ref->EndData(); + EXPECT_TRUE(session.ResumeStream(stream_id)); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnFrameSent(DATA, stream_id, 0, 0x1, 0)); + + result = session.Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::DATA})); + EXPECT_FALSE(session.want_write()); + + // Stream data is done, so this stream cannot be resumed. + EXPECT_FALSE(session.ResumeStream(stream_id)); + EXPECT_FALSE(session.want_write()); +} + +// This test exercises the case where the connection to the peer is write +// blocked. +TEST(OgHttp2SessionTest, ClientSubmitRequestWithWriteBlock) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kClient}); + EXPECT_FALSE(session.want_write()); + + const char* kSentinel1 = "arbitrary pointer 1"; + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, true); + body1->AppendPayload("This is an example request body."); + body1->EndData(); + int stream_id = + session.SubmitRequest(ToHeaders({{":method", "POST"}, + {":scheme", "http"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}), + std::move(body1), const_cast<char*>(kSentinel1)); + EXPECT_GT(stream_id, 0); + EXPECT_TRUE(session.want_write()); + EXPECT_EQ(kSentinel1, session.GetStreamUserData(stream_id)); + visitor.set_is_write_blocked(true); + int result = session.Send(); + EXPECT_EQ(0, result); + + EXPECT_THAT(visitor.data(), testing::IsEmpty()); + EXPECT_TRUE(session.want_write()); + visitor.set_is_write_blocked(false); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, stream_id, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, stream_id, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, stream_id, _, 0x1, 0)); + + result = session.Send(); + EXPECT_EQ(0, result); + + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(serialized, + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::HEADERS, + SpdyFrameType::DATA})); + EXPECT_FALSE(session.want_write()); +} + +TEST(OgHttp2SessionTest, ClientStartShutdown) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kClient}); + + EXPECT_FALSE(session.want_write()); + + // No-op (except for logging) for a client implementation. + session.StartGracefulShutdown(); + EXPECT_FALSE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + + int result = session.Send(); + EXPECT_EQ(0, result); + + absl::string_view serialized = visitor.data(); + EXPECT_THAT(serialized, + testing::StartsWith(spdy::kHttp2ConnectionHeaderPrefix)); + serialized.remove_prefix(strlen(spdy::kHttp2ConnectionHeaderPrefix)); + EXPECT_THAT(serialized, EqualsFrames({SpdyFrameType::SETTINGS})); } TEST(OgHttp2SessionTest, ServerConstruction) { @@ -101,14 +495,18 @@ TEST(OgHttp2SessionTest, ServerConstruction) { visitor, OgHttp2Session::Options{.perspective = Perspective::kServer}); EXPECT_TRUE(session.want_read()); EXPECT_FALSE(session.want_write()); - EXPECT_EQ(session.GetRemoteWindowSize(), kDefaultInitialStreamWindowSize); + EXPECT_EQ(session.GetRemoteWindowSize(), kInitialFlowControlWindowSize); + EXPECT_TRUE(session.IsServerSession()); + EXPECT_EQ(0, session.GetHighestReceivedStreamId()); } TEST(OgHttp2SessionTest, ServerHandlesFrames) { - testing::StrictMock<MockHttp2Visitor> visitor; + DataSavingVisitor visitor; OgHttp2Session session( visitor, OgHttp2Session::Options{.perspective = Perspective::kServer}); + EXPECT_EQ(0, session.GetHpackDecoderDynamicTableSize()); + const std::string frames = TestFrameSequence() .ClientPreface() .Ping(42) @@ -132,6 +530,8 @@ TEST(OgHttp2SessionTest, ServerHandlesFrames) { .Serialize(); testing::InSequence s; + const char* kSentinel1 = "arbitrary pointer 1"; + // Client preface (empty SETTINGS) EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); EXPECT_CALL(visitor, OnSettingsStart()); @@ -147,7 +547,10 @@ TEST(OgHttp2SessionTest, ServerHandlesFrames) { EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); - EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)) + .WillOnce(testing::InvokeWithoutArgs([&session, kSentinel1]() { + session.SetStreamUserData(1, const_cast<char*>(kSentinel1)); + })); EXPECT_CALL(visitor, OnFrameHeader(1, 4, WINDOW_UPDATE, 0)); EXPECT_CALL(visitor, OnWindowUpdate(1, 2000)); EXPECT_CALL(visitor, OnFrameHeader(1, 25, DATA, 0)); @@ -167,11 +570,410 @@ TEST(OgHttp2SessionTest, ServerHandlesFrames) { EXPECT_CALL(visitor, OnFrameHeader(0, 8, PING, 0)); EXPECT_CALL(visitor, OnPing(47, false)); - const ssize_t result = session.ProcessBytes(frames); + const size_t result = session.ProcessBytes(frames); EXPECT_EQ(frames.size(), result); + EXPECT_EQ(kSentinel1, session.GetStreamUserData(1)); + + // The first stream is active and has received some data. + EXPECT_GT(kInitialFlowControlWindowSize, + session.GetStreamReceiveWindowSize(1)); + // Connection receive window is equivalent to the first stream's. + EXPECT_EQ(session.GetReceiveWindowSize(), + session.GetStreamReceiveWindowSize(1)); + // Receive window upper bound is still the initial value. + EXPECT_EQ(kInitialFlowControlWindowSize, + session.GetStreamReceiveWindowLimit(1)); + + EXPECT_GT(session.GetHpackDecoderDynamicTableSize(), 0); + + // TODO(birenroy): drop stream state when streams are closed. It should no + // longer be possible to set user data. + const char* kSentinel3 = "another arbitrary pointer"; + session.SetStreamUserData(3, const_cast<char*>(kSentinel3)); + EXPECT_EQ(kSentinel3, session.GetStreamUserData(3)); + EXPECT_EQ(session.GetRemoteWindowSize(), - kDefaultInitialStreamWindowSize + 1000); + kInitialFlowControlWindowSize + 1000); + EXPECT_EQ(3, session.GetHighestReceivedStreamId()); + + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + + // Some bytes should have been serialized. + int send_result = session.Send(); + EXPECT_EQ(0, send_result); + // Initial SETTINGS, SETTINGS ack. + // TODO(birenroy): automatically queue PING acks. + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::SETTINGS})); +} + +// Verifies that a server session enqueues initial SETTINGS before whatever +// frame type is passed to the first invocation of EnqueueFrame(). +TEST(OgHttp2SessionTest, ServerEnqueuesSettingsBeforeOtherFrame) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kServer}); + EXPECT_FALSE(session.want_write()); + session.EnqueueFrame(absl::make_unique<spdy::SpdyPingIR>(42)); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(PING, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(PING, 0, _, 0x0, 0)); + + int result = session.Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::PING})); +} + +// Verifies that if the first call to EnqueueFrame() passes a SETTINGS frame, +// the server session will not enqueue an additional SETTINGS frame. +TEST(OgHttp2SessionTest, ServerEnqueuesSettingsOnce) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kServer}); + EXPECT_FALSE(session.want_write()); + session.EnqueueFrame(absl::make_unique<spdy::SpdySettingsIR>()); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + + int result = session.Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::SETTINGS})); +} + +TEST(OgHttp2SessionTest, ServerSubmitResponse) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kServer}); + + EXPECT_FALSE(session.want_write()); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "GET"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}, + /*fin=*/true) + .Serialize(); + testing::InSequence s; + + const char* kSentinel1 = "arbitrary pointer 1"; + + // Client preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + // Stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)) + .WillOnce(testing::InvokeWithoutArgs([&session, kSentinel1]() { + session.SetStreamUserData(1, const_cast<char*>(kSentinel1)); + })); + EXPECT_CALL(visitor, OnEndStream(1)); + + const size_t result = session.ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + EXPECT_EQ(1, session.GetHighestReceivedStreamId()); + + EXPECT_EQ(0, session.GetHpackEncoderDynamicTableSize()); + + // Server will want to send initial SETTINGS, and a SETTINGS ack. + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + + int send_result = session.Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::SETTINGS})); + visitor.Clear(); + + EXPECT_FALSE(session.want_write()); + // A data fin is not sent so that the stream remains open, and the flow + // control state can be verified. + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, false); + body1->AppendPayload("This is an example response body."); + int submit_result = session.SubmitResponse( + 1, + ToHeaders({{":status", "404"}, + {"x-comment", "I have no idea what you're talking about."}}), + std::move(body1)); + EXPECT_EQ(submit_result, 0); + EXPECT_TRUE(session.want_write()); + + // Stream user data should have been set successfully after receiving headers. + EXPECT_EQ(kSentinel1, session.GetStreamUserData(1)); + session.SetStreamUserData(1, nullptr); + EXPECT_EQ(nullptr, session.GetStreamUserData(1)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0)); + + send_result = session.Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), + EqualsFrames({SpdyFrameType::HEADERS, SpdyFrameType::DATA})); + EXPECT_FALSE(session.want_write()); + + // Some data was sent, so the remaining send window size should be less than + // the default. + EXPECT_LT(session.GetStreamSendWindowSize(1), kInitialFlowControlWindowSize); + EXPECT_GT(session.GetStreamSendWindowSize(1), 0); + // Send window for a nonexistent stream is not available. + EXPECT_EQ(session.GetStreamSendWindowSize(3), -1); + + EXPECT_GT(session.GetHpackEncoderDynamicTableSize(), 0); +} + +TEST(OgHttp2SessionTest, ServerStartShutdown) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kServer}); + + EXPECT_FALSE(session.want_write()); + + session.StartGracefulShutdown(); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(GOAWAY, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(GOAWAY, 0, _, 0x0, 0)); + + int result = session.Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::GOAWAY})); +} + +TEST(OgHttp2SessionTest, ServerStartShutdownAfterGoaway) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kServer}); + + EXPECT_FALSE(session.want_write()); + + auto goaway = absl::make_unique<spdy::SpdyGoAwayIR>( + 1, spdy::ERROR_CODE_NO_ERROR, "and don't come back!"); + session.EnqueueFrame(std::move(goaway)); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(GOAWAY, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(GOAWAY, 0, _, 0x0, 0)); + + int result = session.Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::GOAWAY})); + + // No-op, since a GOAWAY has previously been enqueued. + session.StartGracefulShutdown(); + EXPECT_FALSE(session.want_write()); +} + +// Tests the case where the server queues trailers after the data stream is +// exhausted. +TEST(OgHttp2SessionTest, ServerSendsTrailers) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kServer}); + + EXPECT_FALSE(session.want_write()); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "GET"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}, + /*fin=*/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()); + // Stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + + const size_t result = session.ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + // Server will want to send initial SETTINGS, and a SETTINGS ack. + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + + int send_result = session.Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::SETTINGS})); + visitor.Clear(); + + EXPECT_FALSE(session.want_write()); + + // The body source must indicate that the end of the body is not the end of + // the stream. + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, false); + body1->AppendPayload("This is an example response body."); + body1->EndData(); + int submit_result = session.SubmitResponse( + 1, ToHeaders({{":status", "200"}, {"x-comment", "Sure, sounds good."}}), + std::move(body1)); + EXPECT_EQ(submit_result, 0); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0)); + + send_result = session.Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), + EqualsFrames({SpdyFrameType::HEADERS, SpdyFrameType::DATA})); + visitor.Clear(); + EXPECT_FALSE(session.want_write()); + + // The body source has been exhausted by the call to Send() above. + // TODO(birenroy): Fix this strange ordering. + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + int trailer_result = session.SubmitTrailer( + 1, ToHeaders({{"final-status", "a-ok"}, + {"x-comment", "trailers sure are cool"}})); + ASSERT_EQ(trailer_result, 0); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x5, 0)); + + send_result = session.Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), EqualsFrames({SpdyFrameType::HEADERS})); +} + +// Tests the case where the server queues trailers immediately after headers and +// data, and before any writes have taken place. +TEST(OgHttp2SessionTest, ServerQueuesTrailersWithResponse) { + DataSavingVisitor visitor; + OgHttp2Session session( + visitor, OgHttp2Session::Options{.perspective = Perspective::kServer}); + + EXPECT_FALSE(session.want_write()); + + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "GET"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}}, + /*fin=*/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()); + // Stream 1 + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "GET")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + + const size_t result = session.ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + // Server will want to send initial SETTINGS, and a SETTINGS ack. + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + + int send_result = session.Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), + EqualsFrames({SpdyFrameType::SETTINGS, SpdyFrameType::SETTINGS})); + visitor.Clear(); + + EXPECT_FALSE(session.want_write()); + + // The body source must indicate that the end of the body is not the end of + // the stream. + auto body1 = absl::make_unique<TestDataFrameSource>(visitor, false); + body1->AppendPayload("This is an example response body."); + body1->EndData(); + int submit_result = session.SubmitResponse( + 1, ToHeaders({{":status", "200"}, {"x-comment", "Sure, sounds good."}}), + std::move(body1)); + EXPECT_EQ(submit_result, 0); + EXPECT_TRUE(session.want_write()); + // There has not been a call to Send() yet, so neither headers nor body have + // been written. + int trailer_result = session.SubmitTrailer( + 1, ToHeaders({{"final-status", "a-ok"}, + {"x-comment", "trailers sure are cool"}})); + ASSERT_EQ(trailer_result, 0); + EXPECT_TRUE(session.want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0)); + EXPECT_CALL(visitor, OnFrameSent(DATA, 1, _, 0x0, 0)); + + // TODO(birenroy): Fix this strange ordering. + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::NO_ERROR)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x5)); + EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x5, 0)); + + send_result = session.Send(); + EXPECT_EQ(0, send_result); + EXPECT_THAT(visitor.data(), + EqualsFrames({SpdyFrameType::HEADERS, SpdyFrameType::DATA, + SpdyFrameType::HEADERS})); } } // namespace test diff --git a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_util.h b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_util.h index e222c96ccfd..3134ff7b004 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_util.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/oghttp2_util.h @@ -3,12 +3,14 @@ #include "absl/types/span.h" #include "http2/adapter/http2_protocol.h" +#include "common/platform/api/quiche_export.h" #include "spdy/core/spdy_header_block.h" namespace http2 { namespace adapter { -spdy::SpdyHeaderBlock ToHeaderBlock(absl::Span<const Header> headers); +QUICHE_EXPORT_PRIVATE spdy::SpdyHeaderBlock ToHeaderBlock( + absl::Span<const Header> headers); } // namespace adapter } // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/recording_http2_visitor.cc b/chromium/net/third_party/quiche/src/http2/adapter/recording_http2_visitor.cc index 78ec423e25e..75f3749e62c 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/recording_http2_visitor.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/recording_http2_visitor.cc @@ -7,6 +7,11 @@ namespace http2 { namespace adapter { namespace test { +ssize_t RecordingHttp2Visitor::OnReadyToSend(absl::string_view serialized) { + events_.push_back(absl::StrFormat("OnReadyToSend %d", serialized.size())); + return serialized.size(); +} + void RecordingHttp2Visitor::OnConnectionError() { events_.push_back("OnConnectionError"); } @@ -36,15 +41,16 @@ void RecordingHttp2Visitor::OnSettingsAck() { events_.push_back("OnSettingsAck"); } -void RecordingHttp2Visitor::OnBeginHeadersForStream(Http2StreamId stream_id) { +bool RecordingHttp2Visitor::OnBeginHeadersForStream(Http2StreamId stream_id) { events_.push_back(absl::StrFormat("OnBeginHeadersForStream %d", stream_id)); + return true; } -void RecordingHttp2Visitor::OnHeaderForStream(Http2StreamId stream_id, - absl::string_view name, - absl::string_view value) { +Http2VisitorInterface::OnHeaderResult RecordingHttp2Visitor::OnHeaderForStream( + Http2StreamId stream_id, absl::string_view name, absl::string_view value) { events_.push_back( absl::StrFormat("OnHeaderForStream %d %s %s", stream_id, name, value)); + return HEADER_OK; } void RecordingHttp2Visitor::OnEndHeadersForStream(Http2StreamId stream_id) { @@ -112,11 +118,32 @@ void RecordingHttp2Visitor::OnWindowUpdate(Http2StreamId stream_id, absl::StrFormat("OnWindowUpdate %d %d", stream_id, window_increment)); } -void RecordingHttp2Visitor::OnReadyToSendDataForStream(Http2StreamId stream_id, - char* destination_buffer, - size_t length, - ssize_t* written, - bool* end_stream) { +int RecordingHttp2Visitor::OnBeforeFrameSent(uint8_t frame_type, + Http2StreamId stream_id, + size_t length, uint8_t flags) { + events_.push_back(absl::StrFormat("OnBeforeFrameSent %d %d %d %d", frame_type, + stream_id, length, flags)); + return 0; +} + +int RecordingHttp2Visitor::OnFrameSent(uint8_t frame_type, + Http2StreamId stream_id, size_t length, + uint8_t flags, uint32_t error_code) { + events_.push_back(absl::StrFormat("OnFrameSent %d %d %d %d %d", frame_type, + stream_id, length, flags, error_code)); + return 0; +} + +bool RecordingHttp2Visitor::OnInvalidFrame(Http2StreamId stream_id, + int error_code) { + events_.push_back( + absl::StrFormat("OnInvalidFrame %d %d", stream_id, error_code)); + return true; +} + +void RecordingHttp2Visitor::OnReadyToSendDataForStream( + Http2StreamId stream_id, char* /*destination_buffer*/, size_t length, + ssize_t* /*written*/, bool* /*end_stream*/) { // TODO(b/181586191): Revisit this. The visitor is expected to write to the // |destination_buffer| and set the other pointer values appropriately. events_.push_back( @@ -124,10 +151,8 @@ void RecordingHttp2Visitor::OnReadyToSendDataForStream(Http2StreamId stream_id, } void RecordingHttp2Visitor::OnReadyToSendMetadataForStream( - Http2StreamId stream_id, - char* buffer, - size_t length, - ssize_t* written) { + Http2StreamId stream_id, char* /*buffer*/, size_t length, + ssize_t* /*written*/) { // TODO(b/181586191): Revisit this. The visitor is expected to write to the // |buffer| and set *written appropriately. events_.push_back(absl::StrFormat("OnReadyToSendMetadataForStream %d %d", @@ -146,8 +171,13 @@ void RecordingHttp2Visitor::OnMetadataForStream(Http2StreamId stream_id, absl::StrFormat("OnMetadataForStream %d %s", stream_id, metadata)); } -void RecordingHttp2Visitor::OnMetadataEndForStream(Http2StreamId stream_id) { +bool RecordingHttp2Visitor::OnMetadataEndForStream(Http2StreamId stream_id) { events_.push_back(absl::StrFormat("OnMetadataEndForStream %d", stream_id)); + return true; +} + +void RecordingHttp2Visitor::OnErrorDebug(absl::string_view message) { + events_.push_back(absl::StrFormat("OnErrorDebug %s", message)); } } // namespace test diff --git a/chromium/net/third_party/quiche/src/http2/adapter/recording_http2_visitor.h b/chromium/net/third_party/quiche/src/http2/adapter/recording_http2_visitor.h index 452b45185cb..25b92935b35 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/recording_http2_visitor.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/recording_http2_visitor.h @@ -5,6 +5,7 @@ #include <string> #include "http2/adapter/http2_visitor_interface.h" +#include "common/platform/api/quiche_export.h" #include "common/platform/api/quiche_test.h" namespace http2 { @@ -12,12 +13,13 @@ namespace adapter { namespace test { // A visitor implementation that records the sequence of callbacks it receives. -class RecordingHttp2Visitor : public Http2VisitorInterface { +class QUICHE_NO_EXPORT RecordingHttp2Visitor : public Http2VisitorInterface { public: using Event = std::string; using EventSequence = std::list<Event>; // From Http2VisitorInterface + ssize_t OnReadyToSend(absl::string_view serialized) override; void OnConnectionError() override; void OnFrameHeader(Http2StreamId stream_id, size_t length, @@ -27,10 +29,10 @@ class RecordingHttp2Visitor : public Http2VisitorInterface { void OnSetting(Http2Setting setting) override; void OnSettingsEnd() override; void OnSettingsAck() override; - void OnBeginHeadersForStream(Http2StreamId stream_id) override; - void OnHeaderForStream(Http2StreamId stream_id, - absl::string_view name, - absl::string_view value) override; + bool OnBeginHeadersForStream(Http2StreamId stream_id) override; + OnHeaderResult OnHeaderForStream(Http2StreamId stream_id, + absl::string_view name, + absl::string_view value) override; void OnEndHeadersForStream(Http2StreamId stream_id) override; void OnBeginDataForStream(Http2StreamId stream_id, size_t payload_length) override; @@ -51,6 +53,11 @@ class RecordingHttp2Visitor : public Http2VisitorInterface { Http2ErrorCode error_code, absl::string_view opaque_data) override; void OnWindowUpdate(Http2StreamId stream_id, int window_increment) override; + int OnBeforeFrameSent(uint8_t frame_type, Http2StreamId stream_id, + size_t length, uint8_t flags) override; + int OnFrameSent(uint8_t frame_type, Http2StreamId stream_id, size_t length, + uint8_t flags, uint32_t error_code) override; + bool OnInvalidFrame(Http2StreamId stream_id, int error_code) override; void OnReadyToSendDataForStream(Http2StreamId stream_id, char* destination_buffer, size_t length, @@ -64,7 +71,8 @@ class RecordingHttp2Visitor : public Http2VisitorInterface { size_t payload_length) override; void OnMetadataForStream(Http2StreamId stream_id, absl::string_view metadata) override; - void OnMetadataEndForStream(Http2StreamId stream_id) override; + bool OnMetadataEndForStream(Http2StreamId stream_id) override; + void OnErrorDebug(absl::string_view message) override; const EventSequence& GetEventSequence() const { return events_; } void Clear() { events_.clear(); } diff --git a/chromium/net/third_party/quiche/src/http2/adapter/test_frame_sequence.cc b/chromium/net/third_party/quiche/src/http2/adapter/test_frame_sequence.cc index 7be368ea6a5..bb1517d3d73 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/test_frame_sequence.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/test_frame_sequence.cc @@ -2,6 +2,7 @@ #include "http2/adapter/http2_util.h" #include "http2/adapter/oghttp2_util.h" +#include "spdy/core/hpack/hpack_encoder.h" #include "spdy/core/spdy_framer.h" namespace http2 { @@ -11,8 +12,9 @@ namespace test { std::vector<const Header> ToHeaders( absl::Span<const std::pair<absl::string_view, absl::string_view>> headers) { std::vector<const Header> out; - for (auto [name, value] : headers) { - out.push_back(std::make_pair(HeaderRep(name), HeaderRep(value))); + for (const auto& header : headers) { + out.push_back( + std::make_pair(HeaderRep(header.first), HeaderRep(header.second))); } return out; } @@ -88,24 +90,45 @@ TestFrameSequence& TestFrameSequence::GoAway(Http2StreamId last_good_stream_id, TestFrameSequence& TestFrameSequence::Headers( Http2StreamId stream_id, absl::Span<const std::pair<absl::string_view, absl::string_view>> headers, - bool fin) { - return Headers(stream_id, ToHeaders(headers), fin); + bool fin, bool add_continuation) { + return Headers(stream_id, ToHeaders(headers), fin, add_continuation); } TestFrameSequence& TestFrameSequence::Headers(Http2StreamId stream_id, spdy::Http2HeaderBlock block, - bool fin) { - auto headers = - absl::make_unique<spdy::SpdyHeadersIR>(stream_id, std::move(block)); - headers->set_fin(fin); - frames_.push_back(std::move(headers)); + bool fin, bool add_continuation) { + if (add_continuation) { + // The normal intermediate representations don't allow you to represent a + // nonterminal HEADERS frame explicitly, so we'll need to use + // SpdyUnknownIRs. For simplicity, and in order not to mess up HPACK state, + // the payload will be uncompressed. + std::string encoded_block; + spdy::HpackEncoder encoder; + encoder.DisableCompression(); + encoder.EncodeHeaderSet(block, &encoded_block); + const size_t pos = encoded_block.size() / 2; + const uint8_t flags = fin ? 0x1 : 0x0; + frames_.push_back(absl::make_unique<spdy::SpdyUnknownIR>( + stream_id, static_cast<uint8_t>(spdy::SpdyFrameType::HEADERS), flags, + encoded_block.substr(0, pos))); + + auto continuation = absl::make_unique<spdy::SpdyContinuationIR>(stream_id); + continuation->set_end_headers(true); + continuation->take_encoding(encoded_block.substr(pos)); + frames_.push_back(std::move(continuation)); + } else { + auto headers = + absl::make_unique<spdy::SpdyHeadersIR>(stream_id, std::move(block)); + headers->set_fin(fin); + frames_.push_back(std::move(headers)); + } return *this; } TestFrameSequence& TestFrameSequence::Headers(Http2StreamId stream_id, absl::Span<const Header> headers, - bool fin) { - return Headers(stream_id, ToHeaderBlock(headers), fin); + bool fin, bool add_continuation) { + return Headers(stream_id, ToHeaderBlock(headers), fin, add_continuation); } TestFrameSequence& TestFrameSequence::WindowUpdate(Http2StreamId stream_id, @@ -124,6 +147,25 @@ TestFrameSequence& TestFrameSequence::Priority(Http2StreamId stream_id, return *this; } +TestFrameSequence& TestFrameSequence::Metadata(Http2StreamId stream_id, + 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; +} + std::string TestFrameSequence::Serialize() { std::string result; if (!preface_.empty()) { @@ -137,6 +179,18 @@ std::string TestFrameSequence::Serialize() { 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/chromium/net/third_party/quiche/src/http2/adapter/test_frame_sequence.h b/chromium/net/third_party/quiche/src/http2/adapter/test_frame_sequence.h index cc5e8b5ff57..99740d38f9e 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/test_frame_sequence.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/test_frame_sequence.h @@ -6,16 +6,17 @@ #include <vector> #include "http2/adapter/http2_protocol.h" +#include "common/platform/api/quiche_export.h" #include "spdy/core/spdy_protocol.h" namespace http2 { namespace adapter { namespace test { -std::vector<const Header> ToHeaders( +std::vector<const Header> QUICHE_NO_EXPORT ToHeaders( absl::Span<const std::pair<absl::string_view, absl::string_view>> headers); -class TestFrameSequence { +class QUICHE_NO_EXPORT TestFrameSequence { public: TestFrameSequence() = default; @@ -36,21 +37,26 @@ class TestFrameSequence { TestFrameSequence& Headers( Http2StreamId stream_id, absl::Span<const std::pair<absl::string_view, absl::string_view>> headers, - bool fin = false); + bool fin = false, bool add_continuation = false); TestFrameSequence& Headers(Http2StreamId stream_id, - spdy::Http2HeaderBlock block, - bool fin = false); + spdy::Http2HeaderBlock block, bool fin = false, + bool add_continuation = false); TestFrameSequence& Headers(Http2StreamId stream_id, - absl::Span<const Header> headers, - bool fin = false); + absl::Span<const Header> headers, bool fin = false, + bool add_continuation = false); TestFrameSequence& WindowUpdate(Http2StreamId stream_id, int32_t delta); TestFrameSequence& Priority(Http2StreamId stream_id, Http2StreamId parent_stream_id, int weight, bool exclusive); + TestFrameSequence& Metadata(Http2StreamId stream_id, + 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/chromium/net/third_party/quiche/src/http2/adapter/test_utils.cc b/chromium/net/third_party/quiche/src/http2/adapter/test_utils.cc index 315a28f0717..b8acddbb87f 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/test_utils.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/test_utils.cc @@ -1,28 +1,123 @@ #include "http2/adapter/test_utils.h" -#include "http2/adapter/nghttp2_util.h" +#include <ostream> + +#include "absl/strings/str_format.h" #include "common/quiche_endian.h" +#include "spdy/core/hpack/hpack_encoder.h" #include "spdy/core/spdy_frame_reader.h" namespace http2 { namespace adapter { namespace test { + +TestDataFrameSource::TestDataFrameSource(Http2VisitorInterface& visitor, + bool has_fin) + : visitor_(visitor), has_fin_(has_fin) {} + +void TestDataFrameSource::AppendPayload(absl::string_view payload) { + QUICHE_CHECK(!end_data_); + if (!payload.empty()) { + payload_fragments_.push_back(std::string(payload)); + current_fragment_ = payload_fragments_.front(); + } +} + +void TestDataFrameSource::EndData() { end_data_ = true; } + +std::pair<ssize_t, bool> TestDataFrameSource::SelectPayloadLength( + size_t max_length) { + // The stream is done if there's no more data, or if |max_length| is at least + // as large as the remaining data. + const bool end_data = end_data_ && (current_fragment_.empty() || + (payload_fragments_.size() == 1 && + max_length >= current_fragment_.size())); + const ssize_t length = std::min(max_length, current_fragment_.size()); + return {length, end_data}; +} + +bool TestDataFrameSource::Send(absl::string_view frame_header, + size_t payload_length) { + QUICHE_LOG_IF(DFATAL, payload_length > current_fragment_.size()) + << "payload_length: " << payload_length + << " current_fragment_size: " << current_fragment_.size(); + const std::string concatenated = + absl::StrCat(frame_header, current_fragment_.substr(0, payload_length)); + const ssize_t result = visitor_.OnReadyToSend(concatenated); + if (result < 0) { + // Write encountered error. + visitor_.OnConnectionError(); + current_fragment_ = {}; + payload_fragments_.clear(); + return false; + } else if (result == 0) { + // Write blocked. + return false; + } else if (static_cast<const size_t>(result) < concatenated.size()) { + // Probably need to handle this better within this test class. + QUICHE_LOG(DFATAL) + << "DATA frame not fully flushed. Connection will be corrupt!"; + visitor_.OnConnectionError(); + current_fragment_ = {}; + payload_fragments_.clear(); + return false; + } + if (payload_length > 0) { + current_fragment_.remove_prefix(payload_length); + } + if (current_fragment_.empty() && !payload_fragments_.empty()) { + payload_fragments_.erase(payload_fragments_.begin()); + if (!payload_fragments_.empty()) { + current_fragment_ = payload_fragments_.front(); + } + } + return true; +} + +std::string EncodeHeaders(const spdy::SpdyHeaderBlock& entries) { + spdy::HpackEncoder encoder; + encoder.DisableCompression(); + std::string result; + QUICHE_CHECK(encoder.EncodeHeaderSet(entries, &result)); + return result; +} + +TestMetadataSource::TestMetadataSource(const spdy::SpdyHeaderBlock& entries) + : encoded_entries_(EncodeHeaders(entries)) { + remaining_ = encoded_entries_; +} + +std::pair<ssize_t, bool> TestMetadataSource::Pack(uint8_t* dest, + size_t dest_len) { + const size_t copied = std::min(dest_len, remaining_.size()); + std::memcpy(dest, remaining_.data(), copied); + remaining_.remove_prefix(copied); + return std::make_pair(copied, remaining_.empty()); +} + namespace { using TypeAndOptionalLength = std::pair<spdy::SpdyFrameType, absl::optional<size_t>>; -std::vector<std::pair<const char*, std::string>> LogFriendly( +std::ostream& operator<<( + std::ostream& os, const std::vector<TypeAndOptionalLength>& types_and_lengths) { - std::vector<std::pair<const char*, std::string>> out; - out.reserve(types_and_lengths.size()); - for (const auto type_and_length : types_and_lengths) { - out.push_back({spdy::FrameTypeToString(type_and_length.first), - type_and_length.second - ? absl::StrCat(type_and_length.second.value()) - : "<unspecified>"}); + for (const auto& type_and_length : types_and_lengths) { + os << "(" << spdy::FrameTypeToString(type_and_length.first) << ", " + << (type_and_length.second ? absl::StrCat(type_and_length.second.value()) + : "<unspecified>") + << ") "; + } + return os; +} + +std::string FrameTypeToString(uint8_t frame_type) { + if (spdy::IsDefinedFrameType(frame_type)) { + return spdy::FrameTypeToString(spdy::ParseFrameType(frame_type)); + } else { + return absl::StrFormat("0x%x", static_cast<int>(frame_type)); } - return out; } // Custom gMock matcher, used to implement EqualsFrames(). @@ -75,16 +170,8 @@ class SpdyControlFrameMatcher return false; } - if (!spdy::IsDefinedFrameType(raw_type)) { - *listener << "; expected type " << FrameTypeToString(expected_type) - << " but raw type " << static_cast<int>(raw_type) - << " is not a defined frame type!"; - return false; - } - - spdy::SpdyFrameType actual_type = spdy::ParseFrameType(raw_type); - if (actual_type != expected_type) { - *listener << "; actual type: " << FrameTypeToString(actual_type) + if (raw_type != static_cast<uint8_t>(expected_type)) { + *listener << "; actual type: " << FrameTypeToString(raw_type) << " but expected type: " << FrameTypeToString(expected_type); return false; } @@ -96,358 +183,18 @@ class SpdyControlFrameMatcher void DescribeTo(std::ostream* os) const override { *os << "Data contains frames of types in sequence " - << LogFriendly(expected_types_and_lengths_); + << expected_types_and_lengths_; } void DescribeNegationTo(std::ostream* os) const override { *os << "Data does not contain frames of types in sequence " - << LogFriendly(expected_types_and_lengths_); + << expected_types_and_lengths_; } private: const std::vector<TypeAndOptionalLength> expected_types_and_lengths_; }; -// Custom gMock matcher, used to implement HasFrameHeader(). -class FrameHeaderMatcher - : public testing::MatcherInterface<const nghttp2_frame_hd*> { - public: - FrameHeaderMatcher(int32_t streamid, - uint8_t type, - const testing::Matcher<int> flags) - : stream_id_(streamid), type_(type), flags_(flags) {} - - bool MatchAndExplain(const nghttp2_frame_hd* frame, - testing::MatchResultListener* listener) const override { - bool matched = true; - if (stream_id_ != frame->stream_id) { - *listener << "; expected stream " << stream_id_ << ", saw " - << frame->stream_id; - matched = false; - } - if (type_ != frame->type) { - *listener << "; expected frame type " << type_ << ", saw " - << static_cast<int>(frame->type); - matched = false; - } - if (!flags_.MatchAndExplain(frame->flags, listener)) { - matched = false; - } - return matched; - } - - void DescribeTo(std::ostream* os) const override { - *os << "contains a frame header with stream " << stream_id_ << ", type " - << type_ << ", "; - flags_.DescribeTo(os); - } - - void DescribeNegationTo(std::ostream* os) const override { - *os << "does not contain a frame header with stream " << stream_id_ - << ", type " << type_ << ", "; - flags_.DescribeNegationTo(os); - } - - private: - const int32_t stream_id_; - const int type_; - const testing::Matcher<int> flags_; -}; - -class DataMatcher : public testing::MatcherInterface<const nghttp2_frame*> { - public: - DataMatcher(const testing::Matcher<uint32_t> stream_id, - const testing::Matcher<size_t> length, - const testing::Matcher<int> flags) - : stream_id_(stream_id), length_(length), flags_(flags) {} - - bool MatchAndExplain(const nghttp2_frame* frame, - testing::MatchResultListener* listener) const override { - if (frame->hd.type != NGHTTP2_DATA) { - *listener << "; expected DATA frame, saw frame of type " - << static_cast<int>(frame->hd.type); - return false; - } - bool matched = true; - if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) { - matched = false; - } - if (!length_.MatchAndExplain(frame->hd.length, listener)) { - matched = false; - } - if (!flags_.MatchAndExplain(frame->hd.flags, listener)) { - matched = false; - } - return matched; - } - - void DescribeTo(std::ostream* os) const override { - *os << "contains a DATA frame, "; - stream_id_.DescribeTo(os); - length_.DescribeTo(os); - flags_.DescribeTo(os); - } - - void DescribeNegationTo(std::ostream* os) const override { - *os << "does not contain a DATA frame, "; - stream_id_.DescribeNegationTo(os); - length_.DescribeNegationTo(os); - flags_.DescribeNegationTo(os); - } - - private: - const testing::Matcher<uint32_t> stream_id_; - const testing::Matcher<size_t> length_; - const testing::Matcher<int> flags_; -}; - -class HeadersMatcher : public testing::MatcherInterface<const nghttp2_frame*> { - public: - HeadersMatcher(const testing::Matcher<uint32_t> stream_id, - const testing::Matcher<int> flags, - const testing::Matcher<int> category) - : stream_id_(stream_id), flags_(flags), category_(category) {} - - bool MatchAndExplain(const nghttp2_frame* frame, - testing::MatchResultListener* listener) const override { - if (frame->hd.type != NGHTTP2_HEADERS) { - *listener << "; expected HEADERS frame, saw frame of type " - << static_cast<int>(frame->hd.type); - return false; - } - bool matched = true; - if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) { - matched = false; - } - if (!flags_.MatchAndExplain(frame->hd.flags, listener)) { - matched = false; - } - if (!category_.MatchAndExplain(frame->headers.cat, listener)) { - matched = false; - } - return matched; - } - - void DescribeTo(std::ostream* os) const override { - *os << "contains a HEADERS frame, "; - stream_id_.DescribeTo(os); - flags_.DescribeTo(os); - category_.DescribeTo(os); - } - - void DescribeNegationTo(std::ostream* os) const override { - *os << "does not contain a HEADERS frame, "; - stream_id_.DescribeNegationTo(os); - flags_.DescribeNegationTo(os); - category_.DescribeNegationTo(os); - } - - private: - const testing::Matcher<uint32_t> stream_id_; - const testing::Matcher<int> flags_; - const testing::Matcher<int> category_; -}; - -class RstStreamMatcher - : public testing::MatcherInterface<const nghttp2_frame*> { - public: - RstStreamMatcher(const testing::Matcher<uint32_t> stream_id, - const testing::Matcher<uint32_t> error_code) - : stream_id_(stream_id), error_code_(error_code) {} - - bool MatchAndExplain(const nghttp2_frame* frame, - testing::MatchResultListener* listener) const override { - if (frame->hd.type != NGHTTP2_RST_STREAM) { - *listener << "; expected RST_STREAM frame, saw frame of type " - << static_cast<int>(frame->hd.type); - return false; - } - bool matched = true; - if (!stream_id_.MatchAndExplain(frame->hd.stream_id, listener)) { - matched = false; - } - if (!error_code_.MatchAndExplain(frame->rst_stream.error_code, listener)) { - matched = false; - } - return matched; - } - - void DescribeTo(std::ostream* os) const override { - *os << "contains a RST_STREAM frame, "; - stream_id_.DescribeTo(os); - error_code_.DescribeTo(os); - } - - void DescribeNegationTo(std::ostream* os) const override { - *os << "does not contain a RST_STREAM frame, "; - stream_id_.DescribeNegationTo(os); - error_code_.DescribeNegationTo(os); - } - - private: - const testing::Matcher<uint32_t> stream_id_; - const testing::Matcher<uint32_t> error_code_; -}; - -class SettingsMatcher : public testing::MatcherInterface<const nghttp2_frame*> { - public: - SettingsMatcher(const testing::Matcher<std::vector<Http2Setting>> values) - : values_(values) {} - - bool MatchAndExplain(const nghttp2_frame* frame, - testing::MatchResultListener* listener) const override { - if (frame->hd.type != NGHTTP2_SETTINGS) { - *listener << "; expected SETTINGS frame, saw frame of type " - << static_cast<int>(frame->hd.type); - return false; - } - std::vector<Http2Setting> settings; - settings.reserve(frame->settings.niv); - for (int i = 0; i < frame->settings.niv; ++i) { - const auto& p = frame->settings.iv[i]; - settings.push_back({static_cast<uint16_t>(p.settings_id), p.value}); - } - return values_.MatchAndExplain(settings, listener); - } - - void DescribeTo(std::ostream* os) const override { - *os << "contains a SETTINGS frame, "; - values_.DescribeTo(os); - } - - void DescribeNegationTo(std::ostream* os) const override { - *os << "does not contain a SETTINGS frame, "; - values_.DescribeNegationTo(os); - } - - private: - const testing::Matcher<std::vector<Http2Setting>> values_; -}; - -class PingMatcher : public testing::MatcherInterface<const nghttp2_frame*> { - public: - PingMatcher(const testing::Matcher<uint64_t> id, bool is_ack) - : id_(id), is_ack_(is_ack) {} - - bool MatchAndExplain(const nghttp2_frame* frame, - testing::MatchResultListener* listener) const override { - if (frame->hd.type != NGHTTP2_PING) { - *listener << "; expected PING frame, saw frame of type " - << static_cast<int>(frame->hd.type); - return false; - } - bool matched = true; - bool frame_ack = frame->hd.flags & NGHTTP2_FLAG_ACK; - if (is_ack_ != frame_ack) { - *listener << "; expected is_ack=" << is_ack_ << ", saw " << frame_ack; - matched = false; - } - uint64_t data; - std::memcpy(&data, frame->ping.opaque_data, sizeof(data)); - data = quiche::QuicheEndian::HostToNet64(data); - if (!id_.MatchAndExplain(data, listener)) { - matched = false; - } - return matched; - } - - void DescribeTo(std::ostream* os) const override { - *os << "contains a PING frame, "; - id_.DescribeTo(os); - } - - void DescribeNegationTo(std::ostream* os) const override { - *os << "does not contain a PING frame, "; - id_.DescribeNegationTo(os); - } - - private: - const testing::Matcher<uint64_t> id_; - const bool is_ack_; -}; - -class GoAwayMatcher : public testing::MatcherInterface<const nghttp2_frame*> { - public: - GoAwayMatcher(const testing::Matcher<uint32_t> last_stream_id, - const testing::Matcher<uint32_t> error_code, - const testing::Matcher<absl::string_view> opaque_data) - : last_stream_id_(last_stream_id), - error_code_(error_code), - opaque_data_(opaque_data) {} - - bool MatchAndExplain(const nghttp2_frame* frame, - testing::MatchResultListener* listener) const override { - if (frame->hd.type != NGHTTP2_GOAWAY) { - *listener << "; expected GOAWAY frame, saw frame of type " - << static_cast<int>(frame->hd.type); - return false; - } - bool matched = true; - if (!last_stream_id_.MatchAndExplain(frame->goaway.last_stream_id, - listener)) { - matched = false; - } - if (!error_code_.MatchAndExplain(frame->goaway.error_code, listener)) { - matched = false; - } - auto opaque_data = - ToStringView(frame->goaway.opaque_data, frame->goaway.opaque_data_len); - if (!opaque_data_.MatchAndExplain(opaque_data, listener)) { - matched = false; - } - return matched; - } - - void DescribeTo(std::ostream* os) const override { - *os << "contains a GOAWAY frame, "; - last_stream_id_.DescribeTo(os); - error_code_.DescribeTo(os); - opaque_data_.DescribeTo(os); - } - - void DescribeNegationTo(std::ostream* os) const override { - *os << "does not contain a GOAWAY frame, "; - last_stream_id_.DescribeNegationTo(os); - error_code_.DescribeNegationTo(os); - opaque_data_.DescribeNegationTo(os); - } - - private: - const testing::Matcher<uint32_t> last_stream_id_; - const testing::Matcher<uint32_t> error_code_; - const testing::Matcher<absl::string_view> opaque_data_; -}; - -class WindowUpdateMatcher - : public testing::MatcherInterface<const nghttp2_frame*> { - public: - WindowUpdateMatcher(const testing::Matcher<uint32_t> delta) : delta_(delta) {} - - bool MatchAndExplain(const nghttp2_frame* frame, - testing::MatchResultListener* listener) const override { - if (frame->hd.type != NGHTTP2_WINDOW_UPDATE) { - *listener << "; expected WINDOW_UPDATE frame, saw frame of type " - << static_cast<int>(frame->hd.type); - return false; - } - return delta_.MatchAndExplain(frame->window_update.window_size_increment, - listener); - } - - void DescribeTo(std::ostream* os) const override { - *os << "contains a WINDOW_UPDATE frame, "; - delta_.DescribeTo(os); - } - - void DescribeNegationTo(std::ostream* os) const override { - *os << "does not contain a WINDOW_UPDATE frame, "; - delta_.DescribeNegationTo(os); - } - - private: - const testing::Matcher<uint32_t> delta_; -}; - } // namespace testing::Matcher<absl::string_view> EqualsFrames( @@ -467,61 +214,6 @@ testing::Matcher<absl::string_view> EqualsFrames( return MakeMatcher(new SpdyControlFrameMatcher(std::move(types_and_lengths))); } -testing::Matcher<const nghttp2_frame_hd*> HasFrameHeader( - uint32_t streamid, - uint8_t type, - const testing::Matcher<int> flags) { - return MakeMatcher(new FrameHeaderMatcher(streamid, type, flags)); -} - -testing::Matcher<const nghttp2_frame*> IsData( - const testing::Matcher<uint32_t> stream_id, - const testing::Matcher<size_t> length, - const testing::Matcher<int> flags) { - return MakeMatcher(new DataMatcher(stream_id, length, flags)); -} - -testing::Matcher<const nghttp2_frame*> IsHeaders( - const testing::Matcher<uint32_t> stream_id, - const testing::Matcher<int> flags, - const testing::Matcher<int> category) { - return MakeMatcher(new HeadersMatcher(stream_id, flags, category)); -} - -testing::Matcher<const nghttp2_frame*> IsRstStream( - const testing::Matcher<uint32_t> stream_id, - const testing::Matcher<uint32_t> error_code) { - return MakeMatcher(new RstStreamMatcher(stream_id, error_code)); -} - -testing::Matcher<const nghttp2_frame*> IsSettings( - const testing::Matcher<std::vector<Http2Setting>> values) { - return MakeMatcher(new SettingsMatcher(values)); -} - -testing::Matcher<const nghttp2_frame*> IsPing( - const testing::Matcher<uint64_t> id) { - return MakeMatcher(new PingMatcher(id, false)); -} - -testing::Matcher<const nghttp2_frame*> IsPingAck( - const testing::Matcher<uint64_t> id) { - return MakeMatcher(new PingMatcher(id, true)); -} - -testing::Matcher<const nghttp2_frame*> IsGoAway( - const testing::Matcher<uint32_t> last_stream_id, - const testing::Matcher<uint32_t> error_code, - const testing::Matcher<absl::string_view> opaque_data) { - return MakeMatcher( - new GoAwayMatcher(last_stream_id, error_code, opaque_data)); -} - -testing::Matcher<const nghttp2_frame*> IsWindowUpdate( - const testing::Matcher<uint32_t> delta) { - return MakeMatcher(new WindowUpdateMatcher(delta)); -} - } // namespace test } // namespace adapter } // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/test_utils.h b/chromium/net/third_party/quiche/src/http2/adapter/test_utils.h index ef1ae29a650..d92617fa3c9 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/test_utils.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/test_utils.h @@ -4,26 +4,99 @@ #include <string> #include <vector> +#include "absl/container/flat_hash_map.h" #include "absl/strings/string_view.h" +#include "http2/adapter/data_source.h" #include "http2/adapter/http2_protocol.h" #include "http2/adapter/mock_http2_visitor.h" -#include "third_party/nghttp2/src/lib/includes/nghttp2/nghttp2.h" +#include "common/platform/api/quiche_export.h" #include "common/platform/api/quiche_test.h" +#include "spdy/core/spdy_header_block.h" #include "spdy/core/spdy_protocol.h" namespace http2 { namespace adapter { namespace test { -class DataSavingVisitor : public testing::StrictMock<MockHttp2Visitor> { +class QUICHE_NO_EXPORT DataSavingVisitor + : public testing::StrictMock<MockHttp2Visitor> { public: - void Save(absl::string_view data) { absl::StrAppend(&data_, data); } + ssize_t OnReadyToSend(absl::string_view data) override { + if (is_write_blocked_) { + return kSendBlocked; + } + const size_t to_accept = std::min(send_limit_, data.size()); + if (to_accept == 0) { + return kSendBlocked; + } + absl::StrAppend(&data_, data.substr(0, to_accept)); + 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(); } + void set_send_limit(size_t limit) { send_limit_ = limit; } + + bool is_write_blocked() const { return is_write_blocked_; } + void set_is_write_blocked(bool value) { is_write_blocked_ = value; } + 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; +}; + +// A test DataFrameSource. Starts out in the empty, blocked state. +class QUICHE_NO_EXPORT TestDataFrameSource : public DataFrameSource { + public: + TestDataFrameSource(Http2VisitorInterface& visitor, bool has_fin); + + void AppendPayload(absl::string_view payload); + void EndData(); + + std::pair<ssize_t, bool> SelectPayloadLength(size_t max_length) override; + bool Send(absl::string_view frame_header, size_t payload_length) override; + bool send_fin() const override { return has_fin_; } + + private: + Http2VisitorInterface& visitor_; + std::vector<std::string> payload_fragments_; + absl::string_view current_fragment_; + // Whether the stream should end with the final frame of data. + const bool has_fin_; + // Whether |payload_fragments_| contains the final segment of data. + bool end_data_ = false; +}; + +class QUICHE_NO_EXPORT TestMetadataSource : public MetadataSource { + public: + explicit TestMetadataSource(const spdy::SpdyHeaderBlock& entries); + + std::pair<ssize_t, bool> Pack(uint8_t* dest, size_t dest_len) override; + + private: + const std::string encoded_entries_; + absl::string_view remaining_; }; // These matchers check whether a string consists entirely of HTTP/2 frames of @@ -41,42 +114,6 @@ testing::Matcher<absl::string_view> EqualsFrames( testing::Matcher<absl::string_view> EqualsFrames( std::vector<spdy::SpdyFrameType> types); -testing::Matcher<const nghttp2_frame_hd*> HasFrameHeader( - uint32_t streamid, - uint8_t type, - const testing::Matcher<int> flags); - -testing::Matcher<const nghttp2_frame*> IsData( - const testing::Matcher<uint32_t> stream_id, - const testing::Matcher<size_t> length, - const testing::Matcher<int> flags); - -testing::Matcher<const nghttp2_frame*> IsHeaders( - const testing::Matcher<uint32_t> stream_id, - const testing::Matcher<int> flags, - const testing::Matcher<int> category); - -testing::Matcher<const nghttp2_frame*> IsRstStream( - const testing::Matcher<uint32_t> stream_id, - const testing::Matcher<uint32_t> error_code); - -testing::Matcher<const nghttp2_frame*> IsSettings( - const testing::Matcher<std::vector<Http2Setting>> values); - -testing::Matcher<const nghttp2_frame*> IsPing( - const testing::Matcher<uint64_t> id); - -testing::Matcher<const nghttp2_frame*> IsPingAck( - const testing::Matcher<uint64_t> id); - -testing::Matcher<const nghttp2_frame*> IsGoAway( - const testing::Matcher<uint32_t> last_stream_id, - const testing::Matcher<uint32_t> error_code, - const testing::Matcher<absl::string_view> opaque_data); - -testing::Matcher<const nghttp2_frame*> IsWindowUpdate( - const testing::Matcher<uint32_t> delta); - } // namespace test } // namespace adapter } // namespace http2 diff --git a/chromium/net/third_party/quiche/src/http2/adapter/window_manager.h b/chromium/net/third_party/quiche/src/http2/adapter/window_manager.h index 277c24f960e..f15982d466a 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/window_manager.h +++ b/chromium/net/third_party/quiche/src/http2/adapter/window_manager.h @@ -3,6 +3,8 @@ #include <functional> +#include "common/platform/api/quiche_export.h" + namespace http2 { namespace adapter { @@ -12,7 +14,7 @@ class WindowManagerPeer; // This class keeps track of a HTTP/2 flow control window, notifying a listener // when a window update needs to be sent. This class is not thread-safe. -class WindowManager { +class QUICHE_EXPORT_PRIVATE WindowManager { public: // A WindowUpdateListener is invoked when it is time to send a window update. typedef std::function<void(size_t)> WindowUpdateListener; diff --git a/chromium/net/third_party/quiche/src/http2/adapter/window_manager_test.cc b/chromium/net/third_party/quiche/src/http2/adapter/window_manager_test.cc index 5e0645365ec..e706156db69 100644 --- a/chromium/net/third_party/quiche/src/http2/adapter/window_manager_test.cc +++ b/chromium/net/third_party/quiche/src/http2/adapter/window_manager_test.cc @@ -90,21 +90,21 @@ TEST_F(WindowManagerTest, AvoidWindowUnderflow) { EXPECT_EQ(wm_.CurrentWindowSize(), wm_.WindowSizeLimit()); // Don't buffer more than the total window! wm_.MarkDataBuffered(wm_.WindowSizeLimit() + 1); - EXPECT_EQ(wm_.CurrentWindowSize(), 0); + EXPECT_EQ(wm_.CurrentWindowSize(), 0u); } // Window manager should GFE_BUG and avoid buffered underflow. TEST_F(WindowManagerTest, AvoidBufferedUnderflow) { - EXPECT_EQ(peer_.buffered(), 0); + EXPECT_EQ(peer_.buffered(), 0u); // Don't flush more than has been buffered! EXPECT_QUICHE_BUG(wm_.MarkDataFlushed(1), "buffered underflow"); - EXPECT_EQ(peer_.buffered(), 0); + EXPECT_EQ(peer_.buffered(), 0u); wm_.MarkDataBuffered(42); - EXPECT_EQ(peer_.buffered(), 42); + EXPECT_EQ(peer_.buffered(), 42u); // Don't flush more than has been buffered! EXPECT_QUICHE_BUG(wm_.MarkDataFlushed(43), "buffered underflow"); - EXPECT_EQ(peer_.buffered(), 0); + EXPECT_EQ(peer_.buffered(), 0u); } // This test verifies that WindowManager notifies its listener when window is |