| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/quic/core/quic_client_promised_info.h" |
| |
| #include <memory> |
| |
| #include "base/macros.h" |
| #include "net/quic/core/spdy_utils.h" |
| #include "net/quic/test_tools/crypto_test_utils.h" |
| #include "net/test/gtest_util.h" |
| #include "net/tools/quic/quic_client_session.h" |
| |
| using std::string; |
| using testing::StrictMock; |
| |
| namespace net { |
| namespace test { |
| |
| class QuicClientPromisedInfoPeer { |
| public: |
| static QuicAlarm* GetAlarm(QuicClientPromisedInfo* promised_stream) { |
| return promised_stream->cleanup_alarm_.get(); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(QuicClientPromisedInfoPeer); |
| }; |
| |
| namespace { |
| |
| class MockQuicClientSession : public QuicClientSession { |
| public: |
| explicit MockQuicClientSession(QuicConnection* connection, |
| QuicClientPushPromiseIndex* push_promise_index) |
| : QuicClientSession( |
| DefaultQuicConfig(), |
| connection, |
| QuicServerId("example.com", 443, PRIVACY_MODE_DISABLED), |
| &crypto_config_, |
| push_promise_index), |
| crypto_config_(CryptoTestUtils::ProofVerifierForTesting()), |
| authorized_(true) {} |
| ~MockQuicClientSession() override {} |
| |
| bool IsAuthorized(const string& authority) override { return authorized_; } |
| |
| void set_authorized(bool authorized) { authorized_ = authorized; } |
| |
| MOCK_METHOD1(CloseStream, void(QuicStreamId stream_id)); |
| |
| private: |
| QuicCryptoClientConfig crypto_config_; |
| |
| bool authorized_; |
| |
| DISALLOW_COPY_AND_ASSIGN(MockQuicClientSession); |
| }; |
| |
| class QuicClientPromisedInfoTest : public ::testing::Test { |
| public: |
| class StreamVisitor; |
| |
| QuicClientPromisedInfoTest() |
| : connection_(new StrictMock<MockQuicConnection>(&helper_, |
| &alarm_factory_, |
| Perspective::IS_CLIENT)), |
| session_(connection_, &push_promise_index_), |
| body_("hello world"), |
| promise_id_(kServerDataStreamId1) { |
| session_.Initialize(); |
| |
| headers_[":status"] = "200"; |
| headers_["content-length"] = "11"; |
| |
| headers_string_ = SpdyUtils::SerializeUncompressedHeaders(headers_); |
| |
| stream_.reset(new QuicSpdyClientStream(kClientDataStreamId1, &session_)); |
| stream_visitor_.reset(new StreamVisitor()); |
| stream_->set_visitor(stream_visitor_.get()); |
| |
| push_promise_[":path"] = "/bar"; |
| push_promise_[":authority"] = "www.google.com"; |
| push_promise_[":version"] = "HTTP/1.1"; |
| push_promise_[":method"] = "GET"; |
| push_promise_[":scheme"] = "https"; |
| |
| promise_url_ = SpdyUtils::GetUrlFromHeaderBlock(push_promise_); |
| serialized_push_promise_ = |
| SpdyUtils::SerializeUncompressedHeaders(push_promise_); |
| |
| client_request_ = push_promise_.Clone(); |
| } |
| |
| class StreamVisitor : public QuicSpdyClientStream::Visitor { |
| void OnClose(QuicSpdyStream* stream) override { |
| DVLOG(1) << "stream " << stream->id(); |
| } |
| }; |
| |
| class PushPromiseDelegate : public QuicClientPushPromiseIndex::Delegate { |
| public: |
| explicit PushPromiseDelegate(bool match) |
| : match_(match), |
| rendezvous_fired_(false), |
| rendezvous_stream_(nullptr) {} |
| |
| bool CheckVary(const SpdyHeaderBlock& client_request, |
| const SpdyHeaderBlock& promise_request, |
| const SpdyHeaderBlock& promise_response) override { |
| DVLOG(1) << "match " << match_; |
| return match_; |
| } |
| |
| void OnRendezvousResult(QuicSpdyStream* stream) override { |
| rendezvous_fired_ = true; |
| rendezvous_stream_ = stream; |
| } |
| |
| QuicSpdyStream* rendezvous_stream() { return rendezvous_stream_; } |
| bool rendezvous_fired() { return rendezvous_fired_; } |
| |
| private: |
| bool match_; |
| bool rendezvous_fired_; |
| QuicSpdyStream* rendezvous_stream_; |
| }; |
| |
| void ReceivePromise(QuicStreamId id) { |
| stream_->OnStreamHeaders(serialized_push_promise_); |
| |
| stream_->OnPromiseHeadersComplete(id, serialized_push_promise_.size()); |
| } |
| |
| MockQuicConnectionHelper helper_; |
| MockAlarmFactory alarm_factory_; |
| StrictMock<MockQuicConnection>* connection_; |
| QuicClientPushPromiseIndex push_promise_index_; |
| |
| MockQuicClientSession session_; |
| std::unique_ptr<QuicSpdyClientStream> stream_; |
| std::unique_ptr<StreamVisitor> stream_visitor_; |
| std::unique_ptr<QuicSpdyClientStream> promised_stream_; |
| SpdyHeaderBlock headers_; |
| string headers_string_; |
| string body_; |
| SpdyHeaderBlock push_promise_; |
| QuicStreamId promise_id_; |
| string promise_url_; |
| string serialized_push_promise_; |
| SpdyHeaderBlock client_request_; |
| }; |
| |
| TEST_F(QuicClientPromisedInfoTest, PushPromise) { |
| ReceivePromise(promise_id_); |
| |
| // Verify that the promise is in the unclaimed streams map. |
| EXPECT_NE(session_.GetPromisedById(promise_id_), nullptr); |
| } |
| |
| TEST_F(QuicClientPromisedInfoTest, PushPromiseCleanupAlarm) { |
| ReceivePromise(promise_id_); |
| |
| // Verify that the promise is in the unclaimed streams map. |
| QuicClientPromisedInfo* promised = session_.GetPromisedById(promise_id_); |
| ASSERT_NE(promised, nullptr); |
| |
| // Fire the alarm that will cancel the promised stream. |
| if (FLAGS_quic_send_push_stream_timed_out_error) { |
| EXPECT_CALL(*connection_, |
| SendRstStream(promise_id_, QUIC_PUSH_STREAM_TIMED_OUT, 0)); |
| } else { |
| EXPECT_CALL(*connection_, |
| SendRstStream(promise_id_, QUIC_STREAM_CANCELLED, 0)); |
| } |
| alarm_factory_.FireAlarm(QuicClientPromisedInfoPeer::GetAlarm(promised)); |
| |
| // Verify that the promise is gone after the alarm fires. |
| EXPECT_EQ(session_.GetPromisedById(promise_id_), nullptr); |
| EXPECT_EQ(session_.GetPromisedByUrl(promise_url_), nullptr); |
| } |
| |
| TEST_F(QuicClientPromisedInfoTest, PushPromiseInvalidMethod) { |
| // Promise with an unsafe method |
| push_promise_[":method"] = "PUT"; |
| serialized_push_promise_ = |
| SpdyUtils::SerializeUncompressedHeaders(push_promise_); |
| |
| EXPECT_CALL(*connection_, |
| SendRstStream(promise_id_, QUIC_INVALID_PROMISE_METHOD, 0)); |
| ReceivePromise(promise_id_); |
| |
| // Verify that the promise headers were ignored |
| EXPECT_EQ(session_.GetPromisedById(promise_id_), nullptr); |
| EXPECT_EQ(session_.GetPromisedByUrl(promise_url_), nullptr); |
| } |
| |
| TEST_F(QuicClientPromisedInfoTest, PushPromiseInvalidUrl) { |
| // Remove required header field to make URL invalid |
| push_promise_.erase(":authority"); |
| serialized_push_promise_ = |
| SpdyUtils::SerializeUncompressedHeaders(push_promise_); |
| |
| EXPECT_CALL(*connection_, |
| SendRstStream(promise_id_, QUIC_INVALID_PROMISE_URL, 0)); |
| ReceivePromise(promise_id_); |
| |
| // Verify that the promise headers were ignored |
| EXPECT_EQ(session_.GetPromisedById(promise_id_), nullptr); |
| EXPECT_EQ(session_.GetPromisedByUrl(promise_url_), nullptr); |
| } |
| |
| TEST_F(QuicClientPromisedInfoTest, PushPromiseUnauthorizedUrl) { |
| session_.set_authorized(false); |
| |
| EXPECT_CALL(*connection_, |
| SendRstStream(promise_id_, QUIC_UNAUTHORIZED_PROMISE_URL, 0)); |
| |
| ReceivePromise(promise_id_); |
| |
| QuicClientPromisedInfo* promised = session_.GetPromisedById(promise_id_); |
| ASSERT_EQ(promised, nullptr); |
| } |
| |
| TEST_F(QuicClientPromisedInfoTest, PushPromiseMismatch) { |
| ReceivePromise(promise_id_); |
| |
| QuicClientPromisedInfo* promised = session_.GetPromisedById(promise_id_); |
| ASSERT_NE(promised, nullptr); |
| |
| // Need to send the promised response headers and initiate the |
| // rendezvous for secondary validation to proceed. |
| QuicSpdyClientStream* promise_stream = static_cast<QuicSpdyClientStream*>( |
| session_.GetOrCreateStream(promise_id_)); |
| promise_stream->OnStreamHeaders(headers_string_); |
| promise_stream->OnStreamHeadersComplete(false, headers_string_.size()); |
| |
| PushPromiseDelegate delegate(/*match=*/false); |
| EXPECT_CALL(*connection_, |
| SendRstStream(promise_id_, QUIC_PROMISE_VARY_MISMATCH, 0)); |
| EXPECT_CALL(session_, CloseStream(promise_id_)); |
| |
| promised->HandleClientRequest(client_request_, &delegate); |
| } |
| |
| TEST_F(QuicClientPromisedInfoTest, PushPromiseVaryWaits) { |
| ReceivePromise(promise_id_); |
| |
| QuicClientPromisedInfo* promised = session_.GetPromisedById(promise_id_); |
| ASSERT_NE(promised, nullptr); |
| |
| // Now initiate rendezvous. |
| PushPromiseDelegate delegate(/*match=*/true); |
| promised->HandleClientRequest(client_request_, &delegate); |
| |
| // Promise is still there, waiting for response. |
| EXPECT_NE(session_.GetPromisedById(promise_id_), nullptr); |
| |
| // Send Response, should trigger promise validation and complete rendezvous |
| QuicSpdyClientStream* promise_stream = static_cast<QuicSpdyClientStream*>( |
| session_.GetOrCreateStream(promise_id_)); |
| ASSERT_NE(promise_stream, nullptr); |
| promise_stream->OnStreamHeaders(headers_string_); |
| promise_stream->OnStreamHeadersComplete(false, headers_string_.size()); |
| |
| // Promise is gone |
| EXPECT_EQ(session_.GetPromisedById(promise_id_), nullptr); |
| } |
| |
| TEST_F(QuicClientPromisedInfoTest, PushPromiseVaryNoWait) { |
| ReceivePromise(promise_id_); |
| |
| QuicClientPromisedInfo* promised = session_.GetPromisedById(promise_id_); |
| ASSERT_NE(promised, nullptr); |
| |
| QuicSpdyClientStream* promise_stream = static_cast<QuicSpdyClientStream*>( |
| session_.GetOrCreateStream(promise_id_)); |
| ASSERT_NE(promise_stream, nullptr); |
| |
| // Send Response, should trigger promise validation and complete rendezvous |
| promise_stream->OnStreamHeaders(headers_string_); |
| promise_stream->OnStreamHeadersComplete(false, headers_string_.size()); |
| |
| // Now initiate rendezvous. |
| PushPromiseDelegate delegate(/*match=*/true); |
| promised->HandleClientRequest(client_request_, &delegate); |
| |
| // Promise is gone |
| EXPECT_EQ(session_.GetPromisedById(promise_id_), nullptr); |
| // Have a push stream |
| EXPECT_TRUE(delegate.rendezvous_fired()); |
| |
| EXPECT_NE(delegate.rendezvous_stream(), nullptr); |
| } |
| |
| TEST_F(QuicClientPromisedInfoTest, PushPromiseWaitCancels) { |
| ReceivePromise(promise_id_); |
| |
| QuicClientPromisedInfo* promised = session_.GetPromisedById(promise_id_); |
| ASSERT_NE(promised, nullptr); |
| |
| // Now initiate rendezvous. |
| PushPromiseDelegate delegate(/*match=*/true); |
| promised->HandleClientRequest(client_request_, &delegate); |
| |
| // Promise is still there, waiting for response. |
| EXPECT_NE(session_.GetPromisedById(promise_id_), nullptr); |
| |
| // Create response stream, but no data yet. |
| session_.GetOrCreateStream(promise_id_); |
| |
| // Cancel the promised stream. |
| EXPECT_CALL(session_, CloseStream(promise_id_)); |
| EXPECT_CALL(*connection_, |
| SendRstStream(promise_id_, QUIC_STREAM_CANCELLED, 0)); |
| promised->Cancel(); |
| |
| // Promise is gone |
| EXPECT_EQ(session_.GetPromisedById(promise_id_), nullptr); |
| } |
| |
| TEST_F(QuicClientPromisedInfoTest, PushPromiseDataClosed) { |
| ReceivePromise(promise_id_); |
| |
| QuicClientPromisedInfo* promised = session_.GetPromisedById(promise_id_); |
| ASSERT_NE(promised, nullptr); |
| |
| QuicSpdyClientStream* promise_stream = static_cast<QuicSpdyClientStream*>( |
| session_.GetOrCreateStream(promise_id_)); |
| ASSERT_NE(promise_stream, nullptr); |
| |
| // Send response, rendezvous will be able to finish synchronously. |
| promise_stream->OnStreamHeaders(headers_string_); |
| promise_stream->OnStreamHeadersComplete(false, headers_string_.size()); |
| |
| EXPECT_CALL(session_, CloseStream(promise_id_)); |
| EXPECT_CALL(*connection_, |
| SendRstStream(promise_id_, QUIC_STREAM_PEER_GOING_AWAY, 0)); |
| session_.SendRstStream(promise_id_, QUIC_STREAM_PEER_GOING_AWAY, 0); |
| |
| // Now initiate rendezvous. |
| PushPromiseDelegate delegate(/*match=*/true); |
| EXPECT_EQ(promised->HandleClientRequest(client_request_, &delegate), |
| QUIC_FAILURE); |
| |
| // Got an indication of the stream failure, client should retry |
| // request. |
| EXPECT_FALSE(delegate.rendezvous_fired()); |
| EXPECT_EQ(delegate.rendezvous_stream(), nullptr); |
| |
| // Promise is gone |
| EXPECT_EQ(session_.GetPromisedById(promise_id_), nullptr); |
| } |
| |
| } // namespace |
| } // namespace test |
| } // namespace net |