| #include <memory> | 
 | #include <string> | 
 | #include <vector> | 
 |  | 
 | #include "quiche/http2/adapter/http2_protocol.h" | 
 | #include "quiche/http2/adapter/mock_http2_visitor.h" | 
 | #include "quiche/http2/adapter/nghttp2_adapter.h" | 
 | #include "quiche/http2/adapter/oghttp2_adapter.h" | 
 | #include "quiche/http2/adapter/recording_http2_visitor.h" | 
 | #include "quiche/http2/adapter/test_frame_sequence.h" | 
 | #include "quiche/http2/adapter/test_utils.h" | 
 | #include "quiche/http2/core/spdy_protocol.h" | 
 | #include "quiche/common/platform/api/quiche_test.h" | 
 |  | 
 | namespace http2 { | 
 | namespace adapter { | 
 | namespace test { | 
 | namespace { | 
 |  | 
 | using ::testing::_; | 
 | using ::testing::AssertionResult; | 
 | using ::testing::InvokeWithoutArgs; | 
 |  | 
 | enum FrameType { | 
 |   DATA, | 
 |   HEADERS, | 
 |   PRIORITY, | 
 |   RST_STREAM, | 
 |   SETTINGS, | 
 |   PUSH_PROMISE, | 
 |   PING, | 
 |   GOAWAY, | 
 |   WINDOW_UPDATE, | 
 |   CONTINUATION, | 
 | }; | 
 |  | 
 | enum class Impl { | 
 |   kNgHttp2, | 
 |   kOgHttp2, | 
 | }; | 
 |  | 
 | absl::string_view ToString(Impl impl) { | 
 |   if (impl == Impl::kNgHttp2) { | 
 |     return "nghttp2"; | 
 |   } | 
 |   return "oghttp2"; | 
 | } | 
 |  | 
 | class ComparisonTest : public ::quiche::test::QuicheTest { | 
 |  public: | 
 |   // The range of characters over which to run a TestEachChar invocation. | 
 |   using CharRange = std::pair<char, char>; | 
 |   // The function that creates and appends a HEADERS frame to the | 
 |   // TestFrameSequence, given a particular character. | 
 |   using AddHeadersFn = absl::AnyInvocable<void(char, TestFrameSequence&)>; | 
 |  | 
 |   std::vector<Impl> implementations() { | 
 |     return {Impl::kNgHttp2, Impl::kOgHttp2}; | 
 |   } | 
 |  | 
 |   std::unique_ptr<Http2Adapter> CreateAdapter(Http2VisitorInterface& visitor, | 
 |                                               Impl impl, Perspective p) { | 
 |     switch (impl) { | 
 |       case Impl::kNgHttp2: | 
 |         if (p == Perspective::kClient) { | 
 |           return NgHttp2Adapter::CreateClientAdapter(visitor); | 
 |         } else { | 
 |           return NgHttp2Adapter::CreateServerAdapter(visitor); | 
 |         } | 
 |       case Impl::kOgHttp2: | 
 |         OgHttp2Adapter::Options options; | 
 |         options.perspective = p; | 
 |         return OgHttp2Adapter::Create(visitor, options); | 
 |     } | 
 |     return nullptr;  // Unreachable unless enum is corrupted. | 
 |   } | 
 |  | 
 |   AssertionResult TestEachChar(CharRange range, AddHeadersFn add_headers) { | 
 |     const char low = range.first; | 
 |     const char high = range.second; | 
 |     // An int is used as the loop variable so that it does not overflow when the | 
 |     // value is the maximum possible character value. | 
 |     for (int i = low; i < high; ++i) { | 
 |       const char c = static_cast<char>(i); | 
 |  | 
 |       TestFrameSequence sequence; | 
 |       sequence.ClientPreface(); | 
 |       add_headers(c, sequence); | 
 |       const std::string frames = sequence.Serialize(); | 
 |  | 
 |       // Accumulates frame validation results. | 
 |       std::vector<bool> frame_valid_results; | 
 |       bool frame_valid = true; | 
 |  | 
 |       testing::NiceMock<MockHttp2Visitor> visitor; | 
 |       ON_CALL(visitor, OnInvalidFrame) | 
 |           .WillByDefault(InvokeWithoutArgs([&frame_valid]() { | 
 |             // Records that the frame was not valid. | 
 |             frame_valid = false; | 
 |             return true; | 
 |           })); | 
 |  | 
 |       for (Impl impl : implementations()) { | 
 |         frame_valid = true; | 
 |         auto adapter = CreateAdapter(visitor, impl, Perspective::kServer); | 
 |         const int64_t result = adapter->ProcessBytes(frames); | 
 |         if (frames.size() != static_cast<size_t>(result)) { | 
 |           return testing::AssertionFailure() | 
 |                  << "Failed to parse encoded bytes! (Expected " << frames.size() | 
 |                  << ", saw " << result << ")"; | 
 |         } | 
 |         frame_valid_results.push_back(frame_valid); | 
 |       } | 
 |       // All implementations should agree on whether the frame was valid. | 
 |       for (bool result : frame_valid_results) { | 
 |         if (result != frame_valid_results.back()) { | 
 |           return testing::AssertionFailure() | 
 |                  << "All implementations should agree!"; | 
 |         } | 
 |       } | 
 |     } | 
 |     return testing::AssertionSuccess(); | 
 |   } | 
 | }; | 
 |  | 
 | // Verifies that the implementations consider the same set of characters valid | 
 | // in paths. | 
 | TEST_F(ComparisonTest, PathCharValidation) { | 
 |   // Iterates over all character values. | 
 |   const CharRange test_range = {std::numeric_limits<char>::min(), | 
 |                                 std::numeric_limits<char>::max()}; | 
 |   auto add_headers_frame = [](char c, TestFrameSequence& seq) { | 
 |     // Constructs a path with the desired character. | 
 |     const std::string path_value = | 
 |         absl::StrCat("/aaa", absl::string_view(&c, 1), "bbb"); | 
 |  | 
 |     SCOPED_TRACE(absl::StrCat("Path: [", absl::CEscape(path_value), "]")); | 
 |     seq.Headers(1, | 
 |                 {{":method", "GET"}, | 
 |                  {":scheme", "https"}, | 
 |                  {":authority", "example.com"}, | 
 |                  {":path", path_value}, | 
 |                  {"name", "value"}}, | 
 |                 /*fin=*/true); | 
 |   }; | 
 |   EXPECT_TRUE(TestEachChar(test_range, std::move(add_headers_frame))); | 
 | } | 
 |  | 
 | // Verifies that the implementations consider the same set of characters valid | 
 | // in HTTP header field names. | 
 | TEST_F(ComparisonTest, HeaderNameCharValidation) { | 
 |   // Iterates over all character values. | 
 |   const CharRange test_range = {std::numeric_limits<char>::min(), | 
 |                                 std::numeric_limits<char>::max()}; | 
 |  | 
 |   auto add_headers_frame = [](char c, TestFrameSequence& seq) { | 
 |     // Constructs a header name with the desired character. | 
 |     const std::string name_text = | 
 |         absl::StrCat("na", absl::string_view(&c, 1), "me"); | 
 |  | 
 |     SCOPED_TRACE(absl::StrCat("Name: [", absl::CEscape(name_text), "]")); | 
 |  | 
 |     // Constructs a request with the desired header name text. | 
 |     seq.Headers(1, | 
 |                 {{":method", "GET"}, | 
 |                  {":scheme", "https"}, | 
 |                  {":authority", "example.com"}, | 
 |                  {":path", "/my/fun/path?with_query"}, | 
 |                  {name_text, "value"}}, | 
 |                 /*fin=*/true); | 
 |   }; | 
 |   EXPECT_TRUE(TestEachChar(test_range, std::move(add_headers_frame))); | 
 | } | 
 |  | 
 | // Verifies that the implementations consider the same set of characters valid | 
 | // in HTTP header field values. | 
 | TEST_F(ComparisonTest, HeaderValueCharValidation) { | 
 |   // Iterates over all character values except \0, which cannot be properly | 
 |   // encoded by the test utility. | 
 |   const CharRange test_range = {1, std::numeric_limits<char>::max()}; | 
 |   auto add_headers_frame = [](char c, TestFrameSequence& seq) { | 
 |     // Constructs a header value with the desired character. | 
 |     const std::string value_text = | 
 |         absl::StrCat("va", absl::string_view(&c, 1), "lue"); | 
 |  | 
 |     SCOPED_TRACE(absl::StrCat("Value: [", absl::CEscape(value_text), "]")); | 
 |  | 
 |     // Constructs a request with the desired header value text. | 
 |     seq.Headers(1, | 
 |                 {{":method", "GET"}, | 
 |                  {":scheme", "https"}, | 
 |                  {":authority", "example.com"}, | 
 |                  {":path", "/my/fun/path?with_query"}, | 
 |                  {"name", value_text}}, | 
 |                 /*fin=*/true); | 
 |   }; | 
 |   EXPECT_TRUE(TestEachChar(test_range, std::move(add_headers_frame))); | 
 | } | 
 |  | 
 | TEST_F(ComparisonTest, StreamCloseAfterReset) { | 
 |   for (Impl impl : implementations()) { | 
 |     SCOPED_TRACE(absl::StrCat("Implementation: ", ToString(impl))); | 
 |  | 
 |     testing::InSequence s; | 
 |  | 
 |     TestVisitor visitor; | 
 |     std::unique_ptr<Http2Adapter> adapter = | 
 |         CreateAdapter(visitor, impl, Perspective::kClient); | 
 |  | 
 |     const std::vector<Header> request_headers = | 
 |         ToHeaders({{":method", "POST"}, | 
 |                    {":scheme", "https"}, | 
 |                    {":authority", "example.com"}, | 
 |                    {":path", "/"}}); | 
 |  | 
 |     const int32_t stream_id = | 
 |         adapter->SubmitRequest(request_headers, false, nullptr); | 
 |     EXPECT_GT(stream_id, 0); | 
 |  | 
 |     if (impl == Impl::kOgHttp2) { | 
 |       // oghttp2 generates an empty SETTINGS frame, per the HTTP/2 spec. | 
 |       EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); | 
 |       EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); | 
 |     } | 
 |  | 
 |     EXPECT_CALL(visitor, OnBeforeFrameSent(HEADERS, 1, _, 0x4)); | 
 |     EXPECT_CALL(visitor, OnFrameSent(HEADERS, 1, _, 0x4, 0)); | 
 |  | 
 |     int result = adapter->Send(); | 
 |     EXPECT_EQ(result, 0); | 
 |  | 
 |     // The WINDOW_UPDATE frame before the RST_STREAM is dropped. | 
 |  | 
 |     EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, 1, _, _)); | 
 |     EXPECT_CALL(visitor, OnFrameSent(RST_STREAM, 1, _, _, _)); | 
 |  | 
 |     // The WINDOW_UPDATE frame after the RST_STREAM is dropped. | 
 |  | 
 |     EXPECT_CALL(visitor, OnCloseStream(1, _)); | 
 |  | 
 |     adapter->SubmitWindowUpdate(1, 10000); | 
 |     adapter->SubmitRst(1, Http2ErrorCode::CANCEL); | 
 |     adapter->SubmitWindowUpdate(1, 10000); | 
 |  | 
 |     result = adapter->Send(); | 
 |     EXPECT_EQ(result, 0); | 
 |   } | 
 | } | 
 |  | 
 | TEST(AdapterImplComparisonTest, ClientHandlesFrames) { | 
 |   RecordingHttp2Visitor nghttp2_visitor; | 
 |   std::unique_ptr<NgHttp2Adapter> nghttp2_adapter = | 
 |       NgHttp2Adapter::CreateClientAdapter(nghttp2_visitor); | 
 |  | 
 |   RecordingHttp2Visitor oghttp2_visitor; | 
 |   OgHttp2Adapter::Options options; | 
 |   options.perspective = Perspective::kClient; | 
 |   std::unique_ptr<OgHttp2Adapter> oghttp2_adapter = | 
 |       OgHttp2Adapter::Create(oghttp2_visitor, options); | 
 |  | 
 |   const std::string initial_frames = TestFrameSequence() | 
 |                                          .ServerPreface() | 
 |                                          .Ping(42) | 
 |                                          .WindowUpdate(0, 1000) | 
 |                                          .Serialize(); | 
 |  | 
 |   nghttp2_adapter->ProcessBytes(initial_frames); | 
 |   oghttp2_adapter->ProcessBytes(initial_frames); | 
 |  | 
 |   EXPECT_EQ(nghttp2_visitor.GetEventSequence(), | 
 |             oghttp2_visitor.GetEventSequence()); | 
 |  | 
 |   // TODO(b/181586191): Consider consistent behavior for delivering events on | 
 |   // non-existent streams between nghttp2_adapter and oghttp2_adapter. | 
 | } | 
 |  | 
 | TEST(AdapterImplComparisonTest, SubmitWindowUpdateBumpsWindow) { | 
 |   RecordingHttp2Visitor nghttp2_visitor; | 
 |   std::unique_ptr<NgHttp2Adapter> nghttp2_adapter = | 
 |       NgHttp2Adapter::CreateClientAdapter(nghttp2_visitor); | 
 |  | 
 |   RecordingHttp2Visitor oghttp2_visitor; | 
 |   OgHttp2Adapter::Options options; | 
 |   options.perspective = Perspective::kClient; | 
 |   std::unique_ptr<OgHttp2Adapter> oghttp2_adapter = | 
 |       OgHttp2Adapter::Create(oghttp2_visitor, options); | 
 |  | 
 |   int result; | 
 |  | 
 |   const std::vector<Header> request_headers = | 
 |       ToHeaders({{":method", "POST"}, | 
 |                  {":scheme", "https"}, | 
 |                  {":authority", "example.com"}, | 
 |                  {":path", "/"}}); | 
 |   const int kInitialFlowControlWindow = 65535; | 
 |   const int kConnectionWindowIncrease = 192 * 1024; | 
 |  | 
 |   const int32_t nghttp2_stream_id = | 
 |       nghttp2_adapter->SubmitRequest(request_headers, true, nullptr); | 
 |  | 
 |   // Both the connection and stream flow control windows are increased. | 
 |   nghttp2_adapter->SubmitWindowUpdate(0, kConnectionWindowIncrease); | 
 |   nghttp2_adapter->SubmitWindowUpdate(nghttp2_stream_id, | 
 |                                       kConnectionWindowIncrease); | 
 |   result = nghttp2_adapter->Send(); | 
 |   EXPECT_EQ(0, result); | 
 |   int nghttp2_window = nghttp2_adapter->GetReceiveWindowSize(); | 
 |   EXPECT_EQ(kInitialFlowControlWindow + kConnectionWindowIncrease, | 
 |             nghttp2_window); | 
 |  | 
 |   const int32_t oghttp2_stream_id = | 
 |       oghttp2_adapter->SubmitRequest(request_headers, true, nullptr); | 
 |   // Both the connection and stream flow control windows are increased. | 
 |   oghttp2_adapter->SubmitWindowUpdate(0, kConnectionWindowIncrease); | 
 |   oghttp2_adapter->SubmitWindowUpdate(oghttp2_stream_id, | 
 |                                       kConnectionWindowIncrease); | 
 |   result = oghttp2_adapter->Send(); | 
 |   EXPECT_EQ(0, result); | 
 |   int oghttp2_window = oghttp2_adapter->GetReceiveWindowSize(); | 
 |   EXPECT_EQ(kInitialFlowControlWindow + kConnectionWindowIncrease, | 
 |             oghttp2_window); | 
 |  | 
 |   // nghttp2 and oghttp2 agree on the advertised window. | 
 |   EXPECT_EQ(nghttp2_window, oghttp2_window); | 
 |  | 
 |   ASSERT_EQ(nghttp2_stream_id, oghttp2_stream_id); | 
 |  | 
 |   const int kMaxFrameSize = 16 * 1024; | 
 |   const std::string body_chunk(kMaxFrameSize, 'a'); | 
 |   auto sequence = TestFrameSequence(); | 
 |   sequence.ServerPreface().Headers(nghttp2_stream_id, {{":status", "200"}}, | 
 |                                    /*fin=*/false); | 
 |   // This loop generates enough DATA frames to consume the window increase. | 
 |   const int kNumFrames = kConnectionWindowIncrease / kMaxFrameSize; | 
 |   for (int i = 0; i < kNumFrames; ++i) { | 
 |     sequence.Data(nghttp2_stream_id, body_chunk); | 
 |   } | 
 |   const std::string frames = sequence.Serialize(); | 
 |  | 
 |   nghttp2_adapter->ProcessBytes(frames); | 
 |   // Marking the data consumed causes a window update, which is reflected in the | 
 |   // advertised window size. | 
 |   nghttp2_adapter->MarkDataConsumedForStream(nghttp2_stream_id, | 
 |                                              kNumFrames * kMaxFrameSize); | 
 |   result = nghttp2_adapter->Send(); | 
 |   EXPECT_EQ(0, result); | 
 |   nghttp2_window = nghttp2_adapter->GetReceiveWindowSize(); | 
 |  | 
 |   oghttp2_adapter->ProcessBytes(frames); | 
 |   // Marking the data consumed causes a window update, which is reflected in the | 
 |   // advertised window size. | 
 |   oghttp2_adapter->MarkDataConsumedForStream(oghttp2_stream_id, | 
 |                                              kNumFrames * kMaxFrameSize); | 
 |   result = oghttp2_adapter->Send(); | 
 |   EXPECT_EQ(0, result); | 
 |   oghttp2_window = oghttp2_adapter->GetReceiveWindowSize(); | 
 |  | 
 |   const int kMinExpectation = | 
 |       (kInitialFlowControlWindow + kConnectionWindowIncrease) / 2; | 
 |   EXPECT_GT(nghttp2_window, kMinExpectation); | 
 |   EXPECT_GT(oghttp2_window, kMinExpectation); | 
 | } | 
 |  | 
 | TEST(AdapterImplComparisonTest, ServerHandlesFrames) { | 
 |   RecordingHttp2Visitor nghttp2_visitor; | 
 |   std::unique_ptr<NgHttp2Adapter> nghttp2_adapter = | 
 |       NgHttp2Adapter::CreateServerAdapter(nghttp2_visitor); | 
 |  | 
 |   RecordingHttp2Visitor oghttp2_visitor; | 
 |   OgHttp2Adapter::Options options; | 
 |   options.perspective = Perspective::kServer; | 
 |   std::unique_ptr<OgHttp2Adapter> oghttp2_adapter = | 
 |       OgHttp2Adapter::Create(oghttp2_visitor, options); | 
 |  | 
 |   const std::string frames = TestFrameSequence() | 
 |                                  .ClientPreface() | 
 |                                  .Ping(42) | 
 |                                  .WindowUpdate(0, 1000) | 
 |                                  .Headers(1, | 
 |                                           {{":method", "POST"}, | 
 |                                            {":scheme", "https"}, | 
 |                                            {":authority", "example.com"}, | 
 |                                            {":path", "/this/is/request/one"}}, | 
 |                                           /*fin=*/false) | 
 |                                  .WindowUpdate(1, 2000) | 
 |                                  .Data(1, "This is the request body.") | 
 |                                  .Headers(3, | 
 |                                           {{":method", "GET"}, | 
 |                                            {":scheme", "http"}, | 
 |                                            {":authority", "example.com"}, | 
 |                                            {":path", "/this/is/request/two"}}, | 
 |                                           /*fin=*/true) | 
 |                                  .RstStream(3, Http2ErrorCode::CANCEL) | 
 |                                  .Ping(47) | 
 |                                  .Serialize(); | 
 |  | 
 |   nghttp2_adapter->ProcessBytes(frames); | 
 |   oghttp2_adapter->ProcessBytes(frames); | 
 |  | 
 |   EXPECT_EQ(nghttp2_visitor.GetEventSequence(), | 
 |             oghttp2_visitor.GetEventSequence()); | 
 | } | 
 |  | 
 | }  // namespace | 
 | }  // namespace test | 
 | }  // namespace adapter | 
 | }  // namespace http2 |