blob: 7636579d037abbc2196d64ab4d75aa23ace5fe38 [file] [log] [blame]
// 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 "quiche/quic/core/http/quic_client_promised_info.h"
#include <memory>
#include <string>
#include <utility>
#include "quiche/quic/core/crypto/null_encrypter.h"
#include "quiche/quic/core/http/quic_spdy_client_session.h"
#include "quiche/quic/core/http/spdy_server_push_utils.h"
#include "quiche/quic/core/quic_utils.h"
#include "quiche/quic/platform/api/quic_logging.h"
#include "quiche/quic/platform/api/quic_socket_address.h"
#include "quiche/quic/platform/api/quic_test.h"
#include "quiche/quic/test_tools/crypto_test_utils.h"
#include "quiche/quic/test_tools/quic_client_promised_info_peer.h"
#include "quiche/quic/test_tools/quic_spdy_session_peer.h"
#include "quiche/quic/test_tools/quic_test_utils.h"
using spdy::SpdyHeaderBlock;
using testing::_;
using testing::StrictMock;
namespace quic {
namespace test {
namespace {
class MockQuicSpdyClientSession : public QuicSpdyClientSession {
public:
explicit MockQuicSpdyClientSession(
const ParsedQuicVersionVector& supported_versions,
QuicConnection* connection,
QuicClientPushPromiseIndex* push_promise_index)
: QuicSpdyClientSession(DefaultQuicConfig(), supported_versions,
connection,
QuicServerId("example.com", 443, false),
&crypto_config_, push_promise_index),
crypto_config_(crypto_test_utils::ProofVerifierForTesting()),
authorized_(true) {}
MockQuicSpdyClientSession(const MockQuicSpdyClientSession&) = delete;
MockQuicSpdyClientSession& operator=(const MockQuicSpdyClientSession&) =
delete;
~MockQuicSpdyClientSession() override {}
bool IsAuthorized(const std::string& /*authority*/) override {
return authorized_;
}
void set_authorized(bool authorized) { authorized_ = authorized; }
MOCK_METHOD(bool, WriteControlFrame,
(const QuicFrame& frame, TransmissionType type), (override));
private:
QuicCryptoClientConfig crypto_config_;
bool authorized_;
};
class QuicClientPromisedInfoTest : public QuicTest {
public:
class StreamVisitor;
QuicClientPromisedInfoTest()
: connection_(new StrictMock<MockQuicConnection>(
&helper_, &alarm_factory_, Perspective::IS_CLIENT)),
session_(connection_->supported_versions(), connection_,
&push_promise_index_),
body_("hello world"),
promise_id_(
QuicUtils::GetInvalidStreamId(connection_->transport_version())) {
connection_->AdvanceTime(QuicTime::Delta::FromSeconds(1));
connection_->SetEncrypter(
ENCRYPTION_FORWARD_SECURE,
std::make_unique<NullEncrypter>(connection_->perspective()));
session_.Initialize();
headers_[":status"] = "200";
headers_["content-length"] = "11";
stream_ = std::make_unique<QuicSpdyClientStream>(
GetNthClientInitiatedBidirectionalStreamId(
connection_->transport_version(), 0),
&session_, BIDIRECTIONAL);
stream_visitor_ = std::make_unique<StreamVisitor>();
stream_->set_visitor(stream_visitor_.get());
push_promise_[":path"] = "/bar";
push_promise_[":authority"] = "www.google.com";
push_promise_[":method"] = "GET";
push_promise_[":scheme"] = "https";
promise_url_ =
SpdyServerPushUtils::GetPromisedUrlFromHeaders(push_promise_);
client_request_ = push_promise_.Clone();
promise_id_ = GetNthServerInitiatedUnidirectionalStreamId(
connection_->transport_version(), 0);
}
class StreamVisitor : public QuicSpdyClientStream::Visitor {
void OnClose(QuicSpdyStream* stream) override {
QUIC_DVLOG(1) << "stream " << stream->id();
}
};
void ReceivePromise(QuicStreamId id) {
auto headers = AsHeaderList(push_promise_);
stream_->OnPromiseHeaderList(id, headers.uncompressed_header_bytes(),
headers);
}
MockQuicConnectionHelper helper_;
MockAlarmFactory alarm_factory_;
StrictMock<MockQuicConnection>* connection_;
QuicClientPushPromiseIndex push_promise_index_;
MockQuicSpdyClientSession session_;
std::unique_ptr<QuicSpdyClientStream> stream_;
std::unique_ptr<StreamVisitor> stream_visitor_;
std::unique_ptr<QuicSpdyClientStream> promised_stream_;
SpdyHeaderBlock headers_;
std::string body_;
SpdyHeaderBlock push_promise_;
QuicStreamId promise_id_;
std::string promise_url_;
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.
EXPECT_CALL(session_, WriteControlFrame(_, _));
EXPECT_CALL(*connection_,
OnStreamReset(promise_id_, QUIC_PUSH_STREAM_TIMED_OUT));
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";
EXPECT_CALL(session_, WriteControlFrame(_, _));
EXPECT_CALL(*connection_,
OnStreamReset(promise_id_, QUIC_INVALID_PROMISE_METHOD));
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, PushPromiseMissingMethod) {
// Promise with a missing method
push_promise_.erase(":method");
EXPECT_CALL(session_, WriteControlFrame(_, _));
EXPECT_CALL(*connection_,
OnStreamReset(promise_id_, QUIC_INVALID_PROMISE_METHOD));
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");
EXPECT_CALL(session_, WriteControlFrame(_, _));
EXPECT_CALL(*connection_,
OnStreamReset(promise_id_, QUIC_INVALID_PROMISE_URL));
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(session_, WriteControlFrame(_, _));
EXPECT_CALL(*connection_,
OnStreamReset(promise_id_, QUIC_UNAUTHORIZED_PROMISE_URL));
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_));
auto headers = AsHeaderList(headers_);
promise_stream->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
headers);
TestPushPromiseDelegate delegate(/*match=*/false);
EXPECT_CALL(session_, WriteControlFrame(_, _));
EXPECT_CALL(*connection_,
OnStreamReset(promise_id_, QUIC_PROMISE_VARY_MISMATCH));
promised->HandleClientRequest(client_request_, &delegate);
}
TEST_F(QuicClientPromisedInfoTest, PushPromiseVaryWaits) {
ReceivePromise(promise_id_);
QuicClientPromisedInfo* promised = session_.GetPromisedById(promise_id_);
EXPECT_FALSE(promised->is_validating());
ASSERT_NE(promised, nullptr);
// Now initiate rendezvous.
TestPushPromiseDelegate delegate(/*match=*/true);
promised->HandleClientRequest(client_request_, &delegate);
EXPECT_TRUE(promised->is_validating());
// 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);
auto headers = AsHeaderList(headers_);
promise_stream->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
headers);
// 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
auto headers = AsHeaderList(headers_);
promise_stream->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
headers);
// Now initiate rendezvous.
TestPushPromiseDelegate 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.
TestPushPromiseDelegate 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_, WriteControlFrame(_, _));
EXPECT_CALL(*connection_, OnStreamReset(promise_id_, QUIC_STREAM_CANCELLED));
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.
auto headers = AsHeaderList(headers_);
promise_stream->OnStreamHeaderList(false, headers.uncompressed_header_bytes(),
headers);
EXPECT_CALL(session_, WriteControlFrame(_, _));
EXPECT_CALL(*connection_,
OnStreamReset(promise_id_, QUIC_STREAM_PEER_GOING_AWAY));
session_.ResetStream(promise_id_, QUIC_STREAM_PEER_GOING_AWAY);
// Now initiate rendezvous.
TestPushPromiseDelegate 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 quic