| #include "quiche/http2/adapter/nghttp2.h" |
| |
| #include "absl/strings/str_cat.h" |
| #include "quiche/http2/adapter/mock_nghttp2_callbacks.h" |
| #include "quiche/http2/adapter/nghttp2_test_utils.h" |
| #include "quiche/http2/adapter/nghttp2_util.h" |
| #include "quiche/http2/adapter/test_frame_sequence.h" |
| #include "quiche/http2/adapter/test_utils.h" |
| #include "quiche/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 quiche::test::QuicheTest { |
| 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 |