|  | #include "http2/adapter/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 |