1 #include "quiche/http2/adapter/nghttp2.h"
2
3 #include "absl/strings/str_cat.h"
4 #include "quiche/http2/adapter/mock_nghttp2_callbacks.h"
5 #include "quiche/http2/adapter/nghttp2_test_utils.h"
6 #include "quiche/http2/adapter/nghttp2_util.h"
7 #include "quiche/http2/adapter/test_frame_sequence.h"
8 #include "quiche/http2/adapter/test_utils.h"
9 #include "quiche/common/platform/api/quiche_test.h"
10
11 namespace http2 {
12 namespace adapter {
13 namespace test {
14 namespace {
15
16 using testing::_;
17
18 enum FrameType {
19 DATA,
20 HEADERS,
21 PRIORITY,
22 RST_STREAM,
23 SETTINGS,
24 PUSH_PROMISE,
25 PING,
26 GOAWAY,
27 WINDOW_UPDATE,
28 };
29
GetOptions()30 nghttp2_option* GetOptions() {
31 nghttp2_option* options;
32 nghttp2_option_new(&options);
33 // Set some common options for compatibility.
34 nghttp2_option_set_no_closed_streams(options, 1);
35 nghttp2_option_set_no_auto_window_update(options, 1);
36 nghttp2_option_set_max_send_header_block_length(options, 0x2000000);
37 nghttp2_option_set_max_outbound_ack(options, 10000);
38 return options;
39 }
40
41 class Nghttp2Test : public quiche::test::QuicheTest {
42 public:
Nghttp2Test()43 Nghttp2Test() : session_(MakeSessionPtr(nullptr)) {}
44
SetUp()45 void SetUp() override { InitializeSession(); }
46
47 virtual Perspective GetPerspective() = 0;
48
InitializeSession()49 void InitializeSession() {
50 auto nghttp2_callbacks = MockNghttp2Callbacks::GetCallbacks();
51 nghttp2_option* options = GetOptions();
52 nghttp2_session* ptr;
53 if (GetPerspective() == Perspective::kClient) {
54 nghttp2_session_client_new2(&ptr, nghttp2_callbacks.get(),
55 &mock_callbacks_, options);
56 } else {
57 nghttp2_session_server_new2(&ptr, nghttp2_callbacks.get(),
58 &mock_callbacks_, options);
59 }
60 nghttp2_option_del(options);
61
62 // Sets up the Send() callback to append to |serialized_|.
63 EXPECT_CALL(mock_callbacks_, Send(_, _, _))
64 .WillRepeatedly(
65 [this](const uint8_t* data, size_t length, int /*flags*/) {
66 absl::StrAppend(&serialized_, ToStringView(data, length));
67 return length;
68 });
69 // Sets up the SendData() callback to fetch and append data from a
70 // TestDataSource.
71 EXPECT_CALL(mock_callbacks_, SendData(_, _, _, _))
72 .WillRepeatedly([this](nghttp2_frame* /*frame*/, const uint8_t* framehd,
73 size_t length, nghttp2_data_source* source) {
74 QUICHE_LOG(INFO) << "Appending frame header and " << length
75 << " bytes of data";
76 auto* s = static_cast<TestDataSource*>(source->ptr);
77 absl::StrAppend(&serialized_, ToStringView(framehd, 9),
78 s->ReadNext(length));
79 return 0;
80 });
81 session_ = MakeSessionPtr(ptr);
82 }
83
84 testing::StrictMock<MockNghttp2Callbacks> mock_callbacks_;
85 nghttp2_session_unique_ptr session_;
86 std::string serialized_;
87 };
88
89 class Nghttp2ClientTest : public Nghttp2Test {
90 public:
GetPerspective()91 Perspective GetPerspective() override { return Perspective::kClient; }
92 };
93
94 // Verifies nghttp2 behavior when acting as a client.
TEST_F(Nghttp2ClientTest,ClientReceivesUnexpectedHeaders)95 TEST_F(Nghttp2ClientTest, ClientReceivesUnexpectedHeaders) {
96 const std::string initial_frames = TestFrameSequence()
97 .ServerPreface()
98 .Ping(42)
99 .WindowUpdate(0, 1000)
100 .Serialize();
101
102 testing::InSequence seq;
103 EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(0, SETTINGS, 0)));
104 EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsSettings(testing::IsEmpty())));
105 EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(0, PING, 0)));
106 EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsPing(42)));
107 EXPECT_CALL(mock_callbacks_,
108 OnBeginFrame(HasFrameHeader(0, WINDOW_UPDATE, 0)));
109 EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsWindowUpdate(1000)));
110
111 ssize_t result = nghttp2_session_mem_recv(
112 session_.get(), ToUint8Ptr(initial_frames.data()), initial_frames.size());
113 ASSERT_EQ(result, initial_frames.size());
114
115 const std::string unexpected_stream_frames =
116 TestFrameSequence()
117 .Headers(1,
118 {{":status", "200"},
119 {"server", "my-fake-server"},
120 {"date", "Tue, 6 Apr 2021 12:54:01 GMT"}},
121 /*fin=*/false)
122 .Data(1, "This is the response body.")
123 .RstStream(3, Http2ErrorCode::INTERNAL_ERROR)
124 .GoAway(5, Http2ErrorCode::ENHANCE_YOUR_CALM, "calm down!!")
125 .Serialize();
126
127 EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(1, HEADERS, _)));
128 EXPECT_CALL(mock_callbacks_, OnInvalidFrameRecv(IsHeaders(1, _, _), _));
129 // No events from the DATA, RST_STREAM or GOAWAY.
130
131 nghttp2_session_mem_recv(session_.get(),
132 ToUint8Ptr(unexpected_stream_frames.data()),
133 unexpected_stream_frames.size());
134 }
135
136 // Tests the request-sending behavior of nghttp2 when acting as a client.
TEST_F(Nghttp2ClientTest,ClientSendsRequest)137 TEST_F(Nghttp2ClientTest, ClientSendsRequest) {
138 int result = nghttp2_session_send(session_.get());
139 ASSERT_EQ(result, 0);
140
141 EXPECT_THAT(serialized_, testing::StrEq(spdy::kHttp2ConnectionHeaderPrefix));
142 serialized_.clear();
143
144 const std::string initial_frames =
145 TestFrameSequence().ServerPreface().Serialize();
146 testing::InSequence s;
147
148 // Server preface (empty SETTINGS)
149 EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(0, SETTINGS, 0)));
150 EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsSettings(testing::IsEmpty())));
151
152 ssize_t recv_result = nghttp2_session_mem_recv(
153 session_.get(), ToUint8Ptr(initial_frames.data()), initial_frames.size());
154 EXPECT_EQ(initial_frames.size(), recv_result);
155
156 // Client wants to send a SETTINGS ack.
157 EXPECT_CALL(mock_callbacks_, BeforeFrameSend(IsSettings(testing::IsEmpty())));
158 EXPECT_CALL(mock_callbacks_, OnFrameSend(IsSettings(testing::IsEmpty())));
159 EXPECT_TRUE(nghttp2_session_want_write(session_.get()));
160 result = nghttp2_session_send(session_.get());
161 EXPECT_THAT(serialized_, EqualsFrames({spdy::SpdyFrameType::SETTINGS}));
162 serialized_.clear();
163
164 EXPECT_FALSE(nghttp2_session_want_write(session_.get()));
165
166 // The following sets up the client request.
167 std::vector<std::pair<absl::string_view, absl::string_view>> headers = {
168 {":method", "POST"},
169 {":scheme", "http"},
170 {":authority", "example.com"},
171 {":path", "/this/is/request/one"}};
172 std::vector<nghttp2_nv> nvs;
173 for (const auto& h : headers) {
174 nvs.push_back({.name = ToUint8Ptr(h.first.data()),
175 .value = ToUint8Ptr(h.second.data()),
176 .namelen = h.first.size(),
177 .valuelen = h.second.size(),
178 .flags = NGHTTP2_NV_FLAG_NONE});
179 }
180 const absl::string_view kBody = "This is an example request body.";
181 TestDataSource source{kBody};
182 nghttp2_data_provider provider = source.MakeDataProvider();
183 // After submitting the request, the client will want to write.
184 int stream_id =
185 nghttp2_submit_request(session_.get(), nullptr /* pri_spec */, nvs.data(),
186 nvs.size(), &provider, nullptr /* stream_data */);
187 EXPECT_GT(stream_id, 0);
188 EXPECT_TRUE(nghttp2_session_want_write(session_.get()));
189
190 // We expect that the client will want to write HEADERS, then DATA.
191 EXPECT_CALL(mock_callbacks_, BeforeFrameSend(IsHeaders(stream_id, _, _)));
192 EXPECT_CALL(mock_callbacks_, OnFrameSend(IsHeaders(stream_id, _, _)));
193 EXPECT_CALL(mock_callbacks_, OnFrameSend(IsData(stream_id, kBody.size(), _)));
194 nghttp2_session_send(session_.get());
195 EXPECT_THAT(serialized_, EqualsFrames({spdy::SpdyFrameType::HEADERS,
196 spdy::SpdyFrameType::DATA}));
197 EXPECT_THAT(serialized_, testing::HasSubstr(kBody));
198
199 // Once the request is flushed, the client no longer wants to write.
200 EXPECT_FALSE(nghttp2_session_want_write(session_.get()));
201 }
202
203 class Nghttp2ServerTest : public Nghttp2Test {
204 public:
GetPerspective()205 Perspective GetPerspective() override { return Perspective::kServer; }
206 };
207
208 // Verifies the behavior when a stream ends early.
TEST_F(Nghttp2ServerTest,MismatchedContentLength)209 TEST_F(Nghttp2ServerTest, MismatchedContentLength) {
210 const std::string initial_frames =
211 TestFrameSequence()
212 .ClientPreface()
213 .Headers(1,
214 {{":method", "POST"},
215 {":scheme", "https"},
216 {":authority", "example.com"},
217 {":path", "/"},
218 {"content-length", "50"}},
219 /*fin=*/false)
220 .Data(1, "Less than 50 bytes.", true)
221 .Serialize();
222
223 testing::InSequence seq;
224 EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(0, SETTINGS, _)));
225
226 EXPECT_CALL(mock_callbacks_, OnFrameRecv(IsSettings(testing::IsEmpty())));
227
228 // HEADERS on stream 1
229 EXPECT_CALL(mock_callbacks_, OnBeginFrame(HasFrameHeader(
230 1, HEADERS, NGHTTP2_FLAG_END_HEADERS)));
231
232 EXPECT_CALL(mock_callbacks_,
233 OnBeginHeaders(IsHeaders(1, NGHTTP2_FLAG_END_HEADERS,
234 NGHTTP2_HCAT_REQUEST)));
235
236 EXPECT_CALL(mock_callbacks_, OnHeader(_, ":method", "POST", _));
237 EXPECT_CALL(mock_callbacks_, OnHeader(_, ":scheme", "https", _));
238 EXPECT_CALL(mock_callbacks_, OnHeader(_, ":authority", "example.com", _));
239 EXPECT_CALL(mock_callbacks_, OnHeader(_, ":path", "/", _));
240 EXPECT_CALL(mock_callbacks_, OnHeader(_, "content-length", "50", _));
241 EXPECT_CALL(mock_callbacks_,
242 OnFrameRecv(IsHeaders(1, NGHTTP2_FLAG_END_HEADERS,
243 NGHTTP2_HCAT_REQUEST)));
244
245 // DATA on stream 1
246 EXPECT_CALL(mock_callbacks_,
247 OnBeginFrame(HasFrameHeader(1, DATA, NGHTTP2_FLAG_END_STREAM)));
248
249 EXPECT_CALL(mock_callbacks_, OnDataChunkRecv(NGHTTP2_FLAG_END_STREAM, 1,
250 "Less than 50 bytes."));
251
252 // No OnFrameRecv() callback for the DATA frame, since there is a
253 // Content-Length mismatch error.
254
255 ssize_t result = nghttp2_session_mem_recv(
256 session_.get(), ToUint8Ptr(initial_frames.data()), initial_frames.size());
257 ASSERT_EQ(result, initial_frames.size());
258 }
259
260 } // namespace
261 } // namespace test
262 } // namespace adapter
263 } // namespace http2
264