Reject invalid :path values in oghttp2. This CL aligns oghttp2 more closely with the HTTP/2 spec WRT :path validation: https://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.3 In particular, the :path request pseudoheader must either begin with '/' or can be '*' for OPTIONS requests (note that oghttp2 performs this validation regardless of scheme). Http2RequestsTest.InvalidPathPseudoheaderExternalIp with oghttp2: http://sponge2/124d5146-d810-4416-8be9-db296b104a87 PiperOrigin-RevId: 424095088
diff --git a/http2/adapter/header_validator.cc b/http2/adapter/header_validator.cc index 4111652..d018991 100644 --- a/http2/adapter/header_validator.cc +++ b/http2/adapter/header_validator.cc
@@ -46,17 +46,30 @@ } bool ValidateRequestHeaders(const std::vector<std::string>& pseudo_headers, - absl::string_view method, bool allow_connect) { + absl::string_view method, absl::string_view path, + bool allow_connect) { QUICHE_VLOG(2) << "Request pseudo-headers: [" << absl::StrJoin(pseudo_headers, ", ") << "], allow_connect: " << allow_connect - << ", method: " << method; + << ", method: " << method << ", path: " << path; if (allow_connect && method == "CONNECT") { static const std::vector<std::string>* kConnectHeaders = new std::vector<std::string>( {":authority", ":method", ":path", ":protocol", ":scheme"}); return pseudo_headers == *kConnectHeaders; } + + if (path.empty()) { + return false; + } + if (path == "*") { + if (method != "OPTIONS") { + return false; + } + } else if (path[0] != '/') { + return false; + } + static const std::vector<std::string>* kRequiredHeaders = new std::vector<std::string>( {":authority", ":method", ":path", ":scheme"}); @@ -83,6 +96,7 @@ pseudo_headers_.clear(); status_.clear(); method_.clear(); + path_.clear(); content_length_.reset(); } @@ -128,6 +142,12 @@ method_ = std::string(value); } else if (key == ":authority" && !IsValidAuthority(value)) { return HEADER_FIELD_INVALID; + } else if (key == ":path") { + if (value.empty()) { + // For now, reject an empty path regardless of scheme. + return HEADER_FIELD_INVALID; + } + path_ = std::string(value); } pseudo_headers_.push_back(std::string(key)); } else if (key == "content-length") { @@ -145,7 +165,8 @@ std::sort(pseudo_headers_.begin(), pseudo_headers_.end()); switch (type) { case HeaderType::REQUEST: - return ValidateRequestHeaders(pseudo_headers_, method_, allow_connect_); + return ValidateRequestHeaders(pseudo_headers_, method_, path_, + allow_connect_); case HeaderType::REQUEST_TRAILER: return ValidateRequestTrailers(pseudo_headers_); case HeaderType::RESPONSE_100:
diff --git a/http2/adapter/header_validator.h b/http2/adapter/header_validator.h index ddcabdf..30f16d5 100644 --- a/http2/adapter/header_validator.h +++ b/http2/adapter/header_validator.h
@@ -54,6 +54,7 @@ std::vector<std::string> pseudo_headers_; std::string status_; std::string method_; + std::string path_; absl::optional<size_t> max_field_size_; absl::optional<size_t> content_length_; bool allow_connect_ = false;
diff --git a/http2/adapter/header_validator_test.cc b/http2/adapter/header_validator_test.cc index ba6d06a..c76a770 100644 --- a/http2/adapter/header_validator_test.cc +++ b/http2/adapter/header_validator_test.cc
@@ -10,6 +10,12 @@ using ::testing::Optional; +using Header = std::pair<absl::string_view, absl::string_view>; +constexpr Header kSampleRequestPseudoheaders[] = {{":authority", "www.foo.com"}, + {":method", "GET"}, + {":path", "/foo"}, + {":scheme", "https"}}; + TEST(HeaderValidatorTest, HeaderNameEmpty) { HeaderValidator v; HeaderValidator::HeaderStatus status = v.ValidateSingleHeader("", "value"); @@ -145,14 +151,12 @@ TEST(HeaderValidatorTest, RequestPseudoHeaders) { HeaderValidator v; - const absl::string_view headers[] = {":authority", ":method", ":path", - ":scheme"}; - for (absl::string_view to_skip : headers) { + for (Header to_skip : kSampleRequestPseudoheaders) { v.StartHeaderBlock(); - for (absl::string_view to_add : headers) { + for (Header to_add : kSampleRequestPseudoheaders) { if (to_add != to_skip) { EXPECT_EQ(HeaderValidator::HEADER_OK, - v.ValidateSingleHeader(to_add, "foo")); + v.ValidateSingleHeader(to_add.first, to_add.second)); } } // When any pseudo-header is missing, final validation will fail. @@ -161,31 +165,31 @@ // When all pseudo-headers are present, final validation will succeed. v.StartHeaderBlock(); - for (absl::string_view to_add : headers) { + for (Header to_add : kSampleRequestPseudoheaders) { EXPECT_EQ(HeaderValidator::HEADER_OK, - v.ValidateSingleHeader(to_add, "foo")); + v.ValidateSingleHeader(to_add.first, to_add.second)); } EXPECT_TRUE(v.FinishHeaderBlock(HeaderType::REQUEST)); // When an extra pseudo-header is present, final validation will fail. v.StartHeaderBlock(); - for (absl::string_view to_add : headers) { + for (Header to_add : kSampleRequestPseudoheaders) { EXPECT_EQ(HeaderValidator::HEADER_OK, - v.ValidateSingleHeader(to_add, "foo")); + v.ValidateSingleHeader(to_add.first, to_add.second)); } EXPECT_EQ(HeaderValidator::HEADER_OK, v.ValidateSingleHeader(":extra", "blah")); EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::REQUEST)); // When a required pseudo-header is repeated, final validation will fail. - for (absl::string_view to_repeat : headers) { + for (Header to_repeat : kSampleRequestPseudoheaders) { v.StartHeaderBlock(); - for (absl::string_view to_add : headers) { + for (Header to_add : kSampleRequestPseudoheaders) { EXPECT_EQ(HeaderValidator::HEADER_OK, - v.ValidateSingleHeader(to_add, "foo")); + v.ValidateSingleHeader(to_add.first, to_add.second)); if (to_add == to_repeat) { EXPECT_EQ(HeaderValidator::HEADER_OK, - v.ValidateSingleHeader(to_add, "foo")); + v.ValidateSingleHeader(to_add.first, to_add.second)); } } EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::REQUEST)); @@ -194,12 +198,10 @@ TEST(HeaderValidatorTest, WebsocketPseudoHeaders) { HeaderValidator v; - const absl::string_view headers[] = {":authority", ":method", ":path", - ":scheme"}; v.StartHeaderBlock(); - for (absl::string_view to_add : headers) { + for (Header to_add : kSampleRequestPseudoheaders) { EXPECT_EQ(HeaderValidator::HEADER_OK, - v.ValidateSingleHeader(to_add, "foo")); + v.ValidateSingleHeader(to_add.first, to_add.second)); } EXPECT_EQ(HeaderValidator::HEADER_OK, v.ValidateSingleHeader(":protocol", "websocket")); @@ -211,24 +213,24 @@ v.AllowConnect(); v.StartHeaderBlock(); - for (absl::string_view to_add : headers) { + for (Header to_add : kSampleRequestPseudoheaders) { EXPECT_EQ(HeaderValidator::HEADER_OK, - v.ValidateSingleHeader(to_add, "foo")); + v.ValidateSingleHeader(to_add.first, to_add.second)); } EXPECT_EQ(HeaderValidator::HEADER_OK, v.ValidateSingleHeader(":protocol", "websocket")); - // The method is "foo", not "CONNECT", so `:protocol` is still treated as an - // extra pseudo-header. + // The method is not "CONNECT", so `:protocol` is still treated as an extra + // pseudo-header. EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::REQUEST)); v.StartHeaderBlock(); - for (absl::string_view to_add : headers) { - if (to_add == ":method") { + for (Header to_add : kSampleRequestPseudoheaders) { + if (to_add.first == ":method") { EXPECT_EQ(HeaderValidator::HEADER_OK, - v.ValidateSingleHeader(to_add, "CONNECT")); + v.ValidateSingleHeader(to_add.first, "CONNECT")); } else { EXPECT_EQ(HeaderValidator::HEADER_OK, - v.ValidateSingleHeader(to_add, "foo")); + v.ValidateSingleHeader(to_add.first, to_add.second)); } } EXPECT_EQ(HeaderValidator::HEADER_OK, @@ -237,6 +239,69 @@ EXPECT_TRUE(v.FinishHeaderBlock(HeaderType::REQUEST)); } +TEST(HeaderValidatorTest, AsteriskPathPseudoHeader) { + HeaderValidator v; + + // An asterisk :path should not be allowed for non-OPTIONS requests. + v.StartHeaderBlock(); + for (Header to_add : kSampleRequestPseudoheaders) { + if (to_add.first == ":path") { + EXPECT_EQ(HeaderValidator::HEADER_OK, + v.ValidateSingleHeader(to_add.first, "*")); + } else { + EXPECT_EQ(HeaderValidator::HEADER_OK, + v.ValidateSingleHeader(to_add.first, to_add.second)); + } + } + EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::REQUEST)); + + // An asterisk :path should be allowed for OPTIONS requests. + v.StartHeaderBlock(); + for (Header to_add : kSampleRequestPseudoheaders) { + if (to_add.first == ":path") { + EXPECT_EQ(HeaderValidator::HEADER_OK, + v.ValidateSingleHeader(to_add.first, "*")); + } else if (to_add.first == ":method") { + EXPECT_EQ(HeaderValidator::HEADER_OK, + v.ValidateSingleHeader(to_add.first, "OPTIONS")); + } else { + EXPECT_EQ(HeaderValidator::HEADER_OK, + v.ValidateSingleHeader(to_add.first, to_add.second)); + } + } + EXPECT_TRUE(v.FinishHeaderBlock(HeaderType::REQUEST)); +} + +TEST(HeaderValidatorTest, InvalidPathPseudoHeader) { + HeaderValidator v; + + // An empty path should fail on single header validation and finish. + v.StartHeaderBlock(); + for (Header to_add : kSampleRequestPseudoheaders) { + if (to_add.first == ":path") { + EXPECT_EQ(HeaderValidator::HEADER_FIELD_INVALID, + v.ValidateSingleHeader(to_add.first, "")); + } else { + EXPECT_EQ(HeaderValidator::HEADER_OK, + v.ValidateSingleHeader(to_add.first, to_add.second)); + } + } + EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::REQUEST)); + + // A path that does not start with a slash should fail on finish. + v.StartHeaderBlock(); + for (Header to_add : kSampleRequestPseudoheaders) { + if (to_add.first == ":path") { + EXPECT_EQ(HeaderValidator::HEADER_OK, + v.ValidateSingleHeader(to_add.first, "shawarma")); + } else { + EXPECT_EQ(HeaderValidator::HEADER_OK, + v.ValidateSingleHeader(to_add.first, to_add.second)); + } + } + EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::REQUEST)); +} + TEST(HeaderValidatorTest, ResponsePseudoHeaders) { HeaderValidator v;
diff --git a/http2/adapter/nghttp2_adapter_test.cc b/http2/adapter/nghttp2_adapter_test.cc index b16241b..dbab8c6 100644 --- a/http2/adapter/nghttp2_adapter_test.cc +++ b/http2/adapter/nghttp2_adapter_test.cc
@@ -4910,6 +4910,134 @@ spdy::SpdyFrameType::RST_STREAM})); } +TEST(NgHttp2AdapterTest, ServerHandlesAsteriskPathForOptions) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor); + + testing::InSequence s; + + const std::string stream_frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":scheme", "https"}, + {":authority", "example.com"}, + {":path", "*"}, + {":method", "OPTIONS"}}, + /*fin=*/true) + .Serialize(); + + // 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, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, _, _)).Times(4); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + + const int64_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), static_cast<size_t>(stream_result)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + + EXPECT_TRUE(adapter->want_write()); + int result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); +} + +TEST(NgHttp2AdapterTest, ServerHandlesInvalidPath) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor); + + testing::InSequence s; + + const std::string stream_frames = + TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":scheme", "https"}, + {":authority", "example.com"}, + {":path", "*"}, + {":method", "GET"}}, + /*fin=*/true) + .Headers(3, + {{":scheme", "https"}, + {":authority", "example.com"}, + {":path", "other/non/slash/starter"}, + {":method", "GET"}}, + /*fin=*/true) + .Headers(5, + {{":scheme", "https"}, + {":authority", "example.com"}, + {":path", ""}, + {":method", "GET"}}, + /*fin=*/true) + .Serialize(); + + // 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, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, _, _)).Times(4); + EXPECT_CALL(visitor, + OnInvalidFrame( + 1, Http2VisitorInterface::InvalidFrameError::kHttpMessaging)); + + EXPECT_CALL(visitor, OnFrameHeader(3, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(3)); + EXPECT_CALL(visitor, OnHeaderForStream(3, _, _)).Times(4); + EXPECT_CALL(visitor, + OnInvalidFrame( + 3, Http2VisitorInterface::InvalidFrameError::kHttpMessaging)); + + EXPECT_CALL(visitor, OnFrameHeader(5, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(5)); + EXPECT_CALL(visitor, OnHeaderForStream(5, _, _)).Times(2); + EXPECT_CALL( + visitor, + OnErrorDebug("Invalid HTTP header field was received: frame type: 1, " + "stream: 5, name: [:path], value: []")); + EXPECT_CALL( + visitor, + OnInvalidFrame(5, Http2VisitorInterface::InvalidFrameError::kHttpHeader)); + + const int64_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), static_cast<size_t>(stream_result)); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, 1, _, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, 1, _, 0x0, + static_cast<int>(Http2ErrorCode::PROTOCOL_ERROR))); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::PROTOCOL_ERROR)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, 3, _, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, 3, _, 0x0, + static_cast<int>(Http2ErrorCode::PROTOCOL_ERROR))); + EXPECT_CALL(visitor, OnCloseStream(3, Http2ErrorCode::PROTOCOL_ERROR)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, 5, _, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, 5, _, 0x0, + static_cast<int>(Http2ErrorCode::PROTOCOL_ERROR))); + EXPECT_CALL(visitor, OnCloseStream(5, Http2ErrorCode::PROTOCOL_ERROR)); + + EXPECT_TRUE(adapter->want_write()); + int result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::RST_STREAM, + spdy::SpdyFrameType::RST_STREAM, + spdy::SpdyFrameType::RST_STREAM})); +} + } // namespace } // namespace test } // namespace adapter
diff --git a/http2/adapter/oghttp2_adapter_test.cc b/http2/adapter/oghttp2_adapter_test.cc index 3596ce0..672f6fb 100644 --- a/http2/adapter/oghttp2_adapter_test.cc +++ b/http2/adapter/oghttp2_adapter_test.cc
@@ -5103,6 +5103,138 @@ spdy::SpdyFrameType::RST_STREAM, spdy::SpdyFrameType::RST_STREAM})); } +TEST(NgHttp2AdapterTest, ServerHandlesAsteriskPathForOptions) { + DataSavingVisitor visitor; + OgHttp2Adapter::Options options{.perspective = Perspective::kServer}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + testing::InSequence s; + + const std::string stream_frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":scheme", "https"}, + {":authority", "example.com"}, + {":path", "*"}, + {":method", "OPTIONS"}}, + /*fin=*/true) + .Serialize(); + + // 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, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, _, _)).Times(4); + EXPECT_CALL(visitor, OnEndHeadersForStream(1)); + EXPECT_CALL(visitor, OnEndStream(1)); + + const int64_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), static_cast<size_t>(stream_result)); + + 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_TRUE(adapter->want_write()); + int result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::SETTINGS})); +} + +TEST(NgHttp2AdapterTest, ServerHandlesInvalidPath) { + DataSavingVisitor visitor; + OgHttp2Adapter::Options options{.perspective = Perspective::kServer}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + testing::InSequence s; + + const std::string stream_frames = + TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":scheme", "https"}, + {":authority", "example.com"}, + {":path", "*"}, + {":method", "GET"}}, + /*fin=*/true) + .Headers(3, + {{":scheme", "https"}, + {":authority", "example.com"}, + {":path", "other/non/slash/starter"}, + {":method", "GET"}}, + /*fin=*/true) + .Headers(5, + {{":scheme", "https"}, + {":authority", "example.com"}, + {":path", ""}, + {":method", "GET"}}, + /*fin=*/true) + .Serialize(); + + // 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, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, _, _)).Times(4); + EXPECT_CALL(visitor, + OnInvalidFrame( + 1, Http2VisitorInterface::InvalidFrameError::kHttpMessaging)); + + EXPECT_CALL(visitor, OnFrameHeader(3, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(3)); + EXPECT_CALL(visitor, OnHeaderForStream(3, _, _)).Times(4); + EXPECT_CALL(visitor, + OnInvalidFrame( + 3, Http2VisitorInterface::InvalidFrameError::kHttpMessaging)); + + EXPECT_CALL(visitor, OnFrameHeader(5, _, HEADERS, 5)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(5)); + EXPECT_CALL(visitor, OnHeaderForStream(5, _, _)).Times(2); + EXPECT_CALL( + visitor, + OnInvalidFrame(5, Http2VisitorInterface::InvalidFrameError::kHttpHeader)); + + const int64_t stream_result = adapter->ProcessBytes(stream_frames); + EXPECT_EQ(stream_frames.size(), static_cast<size_t>(stream_result)); + + 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(RST_STREAM, 1, _, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, 1, _, 0x0, + static_cast<int>(Http2ErrorCode::PROTOCOL_ERROR))); + EXPECT_CALL(visitor, OnCloseStream(1, Http2ErrorCode::HTTP2_NO_ERROR)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, 3, _, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, 3, _, 0x0, + static_cast<int>(Http2ErrorCode::PROTOCOL_ERROR))); + EXPECT_CALL(visitor, OnCloseStream(3, Http2ErrorCode::HTTP2_NO_ERROR)); + EXPECT_CALL(visitor, OnBeforeFrameSent(RST_STREAM, 5, _, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(RST_STREAM, 5, _, 0x0, + static_cast<int>(Http2ErrorCode::PROTOCOL_ERROR))); + EXPECT_CALL(visitor, OnCloseStream(5, Http2ErrorCode::HTTP2_NO_ERROR)); + + EXPECT_TRUE(adapter->want_write()); + int result = adapter->Send(); + EXPECT_EQ(0, result); + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::RST_STREAM, + spdy::SpdyFrameType::RST_STREAM, + spdy::SpdyFrameType::RST_STREAM})); +} + } // namespace } // namespace test } // namespace adapter