blob: 0e598c8028dc4815c1a13c0c7a641bb13713d4db [file] [log] [blame]
// Copyright (c) 2012 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 <stdint.h>
#include "base/base64.h"
#include "base/command_line.h"
#include "base/json/json_string_value_serializer.h"
#include "base/macros.h"
#include "base/prefs/pref_service.h"
#include "base/synchronization/lock.h"
#include "base/thread_task_runner_handle.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/net/predictor.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/test_utils.h"
#include "net/base/host_port_pair.h"
#include "net/base/ip_endpoint.h"
#include "net/base/net_errors.h"
#include "net/dns/host_resolver_proc.h"
#include "net/dns/mock_host_resolver.h"
#include "net/socket/stream_socket.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/embedded_test_server_connection_listener.h"
#include "testing/gmock/include/gmock/gmock.h"
using content::BrowserThread;
using testing::HasSubstr;
namespace {
const char kBlinkPreconnectFeature[] = "LinkPreconnect";
const char kChromiumHostname[] = "chromium.org";
const char kInvalidLongHostname[] = "illegally-long-hostname-over-255-"
"characters-should-not-send-an-ipc-message-to-the-browser-"
"0000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000000000000000000000000"
"000000000000000000000000000000000000000000000000000000.org";
// Gets notified by the EmbeddedTestServer on incoming connections being
// accepted or read from, keeps track of them and exposes that info to
// the tests.
// A port being reused is currently considered an error. If a test
// needs to verify multiple connections are opened in sequence, that will need
// to be changed.
class ConnectionListener
: public net::test_server::EmbeddedTestServerConnectionListener {
public:
ConnectionListener() : task_runner_(base::ThreadTaskRunnerHandle::Get()) {}
~ConnectionListener() override {}
// Get called from the EmbeddedTestServer thread to be notified that
// a connection was accepted.
void AcceptedSocket(const net::StreamSocket& connection) override {
base::AutoLock lock(lock_);
uint16_t socket = GetPort(connection);
EXPECT_TRUE(sockets_.find(socket) == sockets_.end());
sockets_[socket] = SOCKET_ACCEPTED;
task_runner_->PostTask(FROM_HERE, accept_loop_.QuitClosure());
}
// Get called from the EmbeddedTestServer thread to be notified that
// a connection was read from.
void ReadFromSocket(const net::StreamSocket& connection) override {
base::AutoLock lock(lock_);
uint16_t socket = GetPort(connection);
EXPECT_FALSE(sockets_.find(socket) == sockets_.end());
sockets_[socket] = SOCKET_READ_FROM;
task_runner_->PostTask(FROM_HERE, read_loop_.QuitClosure());
}
// Returns the number of sockets that were accepted by the server.
size_t GetAcceptedSocketCount() const {
base::AutoLock lock(lock_);
return sockets_.size();
}
// Returns the number of sockets that were read from by the server.
size_t GetReadSocketCount() const {
base::AutoLock lock(lock_);
size_t read_sockets = 0;
for (const auto& socket : sockets_) {
if (socket.second == SOCKET_READ_FROM)
++read_sockets;
}
return read_sockets;
}
void WaitUntilFirstConnectionAccepted() { accept_loop_.Run(); }
void WaitUntilFirstConnectionRead() { read_loop_.Run(); }
private:
static uint16_t GetPort(const net::StreamSocket& connection) {
// Get the remote port of the peer, since the local port will always be the
// port the test server is listening on. This isn't strictly correct - it's
// possible for multiple peers to connect with the same remote port but
// different remote IPs - but the tests here assume that connections to the
// test server (running on localhost) will always come from localhost, and
// thus the peer port is all thats needed to distinguish two connections.
// This also would be problematic if the OS reused ports, but that's not
// something to worry about for these tests.
net::IPEndPoint address;
EXPECT_EQ(net::OK, connection.GetPeerAddress(&address));
return address.port();
}
enum SocketStatus { SOCKET_ACCEPTED, SOCKET_READ_FROM };
typedef base::hash_map<uint16_t, SocketStatus> SocketContainer;
SocketContainer sockets_;
base::RunLoop accept_loop_;
base::RunLoop read_loop_;
scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
mutable base::Lock lock_;
DISALLOW_COPY_AND_ASSIGN(ConnectionListener);
};
// Records a history of all hostnames for which resolving has been requested,
// and immediately fails the resolution requests themselves.
class HostResolutionRequestRecorder : public net::HostResolverProc {
public:
HostResolutionRequestRecorder()
: HostResolverProc(NULL),
is_waiting_for_hostname_(false) {
}
int Resolve(const std::string& host,
net::AddressFamily address_family,
net::HostResolverFlags host_resolver_flags,
net::AddressList* addrlist,
int* os_error) override {
BrowserThread::PostTask(
BrowserThread::UI,
FROM_HERE,
base::Bind(&HostResolutionRequestRecorder::AddToHistory,
base::Unretained(this),
host));
return net::ERR_NAME_NOT_RESOLVED;
}
int RequestedHostnameCount() const {
return requested_hostnames_.size();
}
bool HasHostBeenRequested(const std::string& hostname) const {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
return std::find(requested_hostnames_.begin(),
requested_hostnames_.end(),
hostname) != requested_hostnames_.end();
}
void WaitUntilHostHasBeenRequested(const std::string& hostname) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(!is_waiting_for_hostname_);
if (HasHostBeenRequested(hostname))
return;
waiting_for_hostname_ = hostname;
is_waiting_for_hostname_ = true;
content::RunMessageLoop();
}
private:
~HostResolutionRequestRecorder() override {}
void AddToHistory(const std::string& hostname) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
requested_hostnames_.push_back(hostname);
if (is_waiting_for_hostname_ && waiting_for_hostname_ == hostname) {
is_waiting_for_hostname_ = false;
waiting_for_hostname_.clear();
base::MessageLoop::current()->QuitWhenIdle();
}
}
// The hostname which WaitUntilHostHasBeenRequested is currently waiting for
// to be requested.
std::string waiting_for_hostname_;
// Whether WaitUntilHostHasBeenRequested is waiting for a hostname to be
// requested and thus is running a nested message loop.
bool is_waiting_for_hostname_;
// A list of hostnames for which resolution has already been requested. Only
// to be accessed from the UI thread.
std::vector<std::string> requested_hostnames_;
DISALLOW_COPY_AND_ASSIGN(HostResolutionRequestRecorder);
};
} // namespace
namespace chrome_browser_net {
class PredictorBrowserTest : public InProcessBrowserTest {
public:
PredictorBrowserTest()
: startup_url_("http://host1:1"),
referring_url_("http://host2:1"),
target_url_("http://host3:1"),
host_resolution_request_recorder_(new HostResolutionRequestRecorder) {
}
protected:
void SetUpInProcessBrowserTestFixture() override {
scoped_host_resolver_proc_.reset(new net::ScopedDefaultHostResolverProc(
host_resolution_request_recorder_.get()));
InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
command_line->AppendSwitchASCII(
switches::kEnableBlinkFeatures, kBlinkPreconnectFeature);
}
void SetUpOnMainThread() override {
connection_listener_.reset(new ConnectionListener());
embedded_test_server()->SetConnectionListener(connection_listener_.get());
ASSERT_TRUE(embedded_test_server()->Start());
}
void TearDownOnMainThread() override {
ASSERT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
}
// Navigates to a data URL containing the given content, with a MIME type of
// text/html.
void NavigateToDataURLWithContent(const std::string& content) {
std::string encoded_content;
base::Base64Encode(content, &encoded_content);
std::string data_uri_content = "data:text/html;base64," + encoded_content;
ui_test_utils::NavigateToURL(browser(), GURL(data_uri_content));
}
void TearDownInProcessBrowserTestFixture() override {
InProcessBrowserTest::TearDownInProcessBrowserTestFixture();
scoped_host_resolver_proc_.reset();
}
void LearnAboutInitialNavigation(const GURL& url) {
Predictor* predictor = browser()->profile()->GetNetworkPredictor();
BrowserThread::PostTask(BrowserThread::IO,
FROM_HERE,
base::Bind(&Predictor::LearnAboutInitialNavigation,
base::Unretained(predictor),
url));
content::RunAllPendingInMessageLoop(BrowserThread::IO);
}
void LearnFromNavigation(const GURL& referring_url, const GURL& target_url) {
Predictor* predictor = browser()->profile()->GetNetworkPredictor();
BrowserThread::PostTask(BrowserThread::IO,
FROM_HERE,
base::Bind(&Predictor::LearnFromNavigation,
base::Unretained(predictor),
referring_url,
target_url));
content::RunAllPendingInMessageLoop(BrowserThread::IO);
}
void PrepareFrameSubresources(const GURL& url) {
Predictor* predictor = browser()->profile()->GetNetworkPredictor();
predictor->PredictFrameSubresources(url, GURL());
}
void GetListFromPrefsAsString(const char* list_path,
std::string* value_as_string) const {
PrefService* prefs = browser()->profile()->GetPrefs();
const base::ListValue* list_value = prefs->GetList(list_path);
JSONStringValueSerializer serializer(value_as_string);
serializer.Serialize(*list_value);
}
bool HasHostBeenRequested(const std::string& hostname) const {
return host_resolution_request_recorder_->HasHostBeenRequested(hostname);
}
void WaitUntilHostHasBeenRequested(const std::string& hostname) {
host_resolution_request_recorder_->WaitUntilHostHasBeenRequested(hostname);
}
int RequestedHostnameCount() const {
return host_resolution_request_recorder_->RequestedHostnameCount();
}
const GURL startup_url_;
const GURL referring_url_;
const GURL target_url_;
scoped_ptr<ConnectionListener> connection_listener_;
private:
scoped_refptr<HostResolutionRequestRecorder>
host_resolution_request_recorder_;
scoped_ptr<net::ScopedDefaultHostResolverProc> scoped_host_resolver_proc_;
};
IN_PROC_BROWSER_TEST_F(PredictorBrowserTest, PRE_ShutdownStartupCycle) {
// Prepare state that will be serialized on this shut-down and read on next
// start-up.
LearnAboutInitialNavigation(startup_url_);
LearnFromNavigation(referring_url_, target_url_);
}
IN_PROC_BROWSER_TEST_F(PredictorBrowserTest, ShutdownStartupCycle) {
// Make sure that the Preferences file is actually wiped of all DNS prefetch
// related data after start-up.
std::string cleared_startup_list;
std::string cleared_referral_list;
GetListFromPrefsAsString(prefs::kDnsPrefetchingStartupList,
&cleared_startup_list);
GetListFromPrefsAsString(prefs::kDnsPrefetchingHostReferralList,
&cleared_referral_list);
EXPECT_THAT(cleared_startup_list, Not(HasSubstr(startup_url_.host())));
EXPECT_THAT(cleared_referral_list, Not(HasSubstr(referring_url_.host())));
EXPECT_THAT(cleared_referral_list, Not(HasSubstr(target_url_.host())));
// But also make sure this data has been first loaded into the Predictor, by
// inspecting that the Predictor starts making the expected hostname requests.
PrepareFrameSubresources(referring_url_);
WaitUntilHostHasBeenRequested(startup_url_.host());
WaitUntilHostHasBeenRequested(target_url_.host());
}
// Flaky on Windows: http://crbug.com/469120
#if defined(OS_WIN)
#define MAYBE_DnsPrefetch DISABLED_DnsPrefetch
#else
#define MAYBE_DnsPrefetch DnsPrefetch
#endif
IN_PROC_BROWSER_TEST_F(PredictorBrowserTest, MAYBE_DnsPrefetch) {
int hostnames_requested_before_load = RequestedHostnameCount();
ui_test_utils::NavigateToURL(
browser(),
GURL(embedded_test_server()->GetURL("/predictor/dns_prefetch.html")));
WaitUntilHostHasBeenRequested(kChromiumHostname);
ASSERT_FALSE(HasHostBeenRequested(kInvalidLongHostname));
ASSERT_EQ(hostnames_requested_before_load + 1, RequestedHostnameCount());
}
// Tests that preconnect warms up a socket connection to a test server.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(PredictorBrowserTest, PreconnectNonCORS) {
GURL preconnect_url = embedded_test_server()->base_url();
std::string preconnect_content =
"<link rel=\"preconnect\" href=\"" + preconnect_url.spec() + "\">";
NavigateToDataURLWithContent(preconnect_content);
connection_listener_->WaitUntilFirstConnectionAccepted();
EXPECT_EQ(1u, connection_listener_->GetAcceptedSocketCount());
EXPECT_EQ(0u, connection_listener_->GetReadSocketCount());
}
// Tests that preconnect warms up a socket connection to a test server,
// and that that socket is later used when fetching a resource.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(PredictorBrowserTest, PreconnectAndFetchNonCORS) {
GURL preconnect_url = embedded_test_server()->base_url();
// First navigation to content with a preconnect hint.
std::string preconnect_content =
"<link rel=\"preconnect\" href=\"" + preconnect_url.spec() + "\">";
NavigateToDataURLWithContent(preconnect_content);
connection_listener_->WaitUntilFirstConnectionAccepted();
EXPECT_EQ(1u, connection_listener_->GetAcceptedSocketCount());
EXPECT_EQ(0u, connection_listener_->GetReadSocketCount());
// Second navigation to content with an img.
std::string img_content =
"<img src=\"" + preconnect_url.spec() + "test.gif\">";
NavigateToDataURLWithContent(img_content);
connection_listener_->WaitUntilFirstConnectionRead();
EXPECT_EQ(1u, connection_listener_->GetAcceptedSocketCount());
EXPECT_EQ(1u, connection_listener_->GetReadSocketCount());
}
// Tests that preconnect warms up a CORS connection to a test
// server, and that socket is later used when fetching a CORS resource.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(PredictorBrowserTest, PreconnectAndFetchCORS) {
GURL preconnect_url = embedded_test_server()->base_url();
// First navigation to content with a preconnect hint.
std::string preconnect_content = "<link rel=\"preconnect\" href=\"" +
preconnect_url.spec() + "\" crossorigin>";
NavigateToDataURLWithContent(preconnect_content);
connection_listener_->WaitUntilFirstConnectionAccepted();
EXPECT_EQ(1u, connection_listener_->GetAcceptedSocketCount());
EXPECT_EQ(0u, connection_listener_->GetReadSocketCount());
// Second navigation to content with a font.
std::string font_content = "<script>var font = new FontFace('FontA', 'url(" +
preconnect_url.spec() +
"test.woff2)');font.load();</script>";
NavigateToDataURLWithContent(font_content);
connection_listener_->WaitUntilFirstConnectionRead();
EXPECT_EQ(1u, connection_listener_->GetAcceptedSocketCount());
EXPECT_EQ(1u, connection_listener_->GetReadSocketCount());
}
// Tests that preconnect warms up a non-CORS connection to a test
// server, but that socket is not used when fetching a CORS resource.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(PredictorBrowserTest, PreconnectNonCORSAndFetchCORS) {
GURL preconnect_url = embedded_test_server()->base_url();
// First navigation to content with a preconnect hint.
std::string preconnect_content =
"<link rel=\"preconnect\" href=\"" + preconnect_url.spec() + "\">";
NavigateToDataURLWithContent(preconnect_content);
connection_listener_->WaitUntilFirstConnectionAccepted();
EXPECT_EQ(1u, connection_listener_->GetAcceptedSocketCount());
EXPECT_EQ(0u, connection_listener_->GetReadSocketCount());
// Second navigation to content with a font.
std::string font_content = "<script>var font = new FontFace('FontA', 'url(" +
preconnect_url.spec() +
"test.woff2)');font.load();</script>";
NavigateToDataURLWithContent(font_content);
connection_listener_->WaitUntilFirstConnectionRead();
EXPECT_EQ(2u, connection_listener_->GetAcceptedSocketCount());
EXPECT_EQ(1u, connection_listener_->GetReadSocketCount());
}
// Tests that preconnect warms up a CORS connection to a test server,
// but that socket is not used when fetching a non-CORS resource.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(PredictorBrowserTest, PreconnectCORSAndFetchNonCORS) {
GURL preconnect_url = embedded_test_server()->base_url();
// First navigation to content with a preconnect hint.
std::string preconnect_content = "<link rel=\"preconnect\" href=\"" +
preconnect_url.spec() + "\" crossorigin>";
NavigateToDataURLWithContent(preconnect_content);
connection_listener_->WaitUntilFirstConnectionAccepted();
EXPECT_EQ(1u, connection_listener_->GetAcceptedSocketCount());
EXPECT_EQ(0u, connection_listener_->GetReadSocketCount());
// Second navigation to content with an img.
std::string img_content =
"<img src=\"" + preconnect_url.spec() + "test.gif\">";
NavigateToDataURLWithContent(img_content);
connection_listener_->WaitUntilFirstConnectionRead();
EXPECT_EQ(2u, connection_listener_->GetAcceptedSocketCount());
EXPECT_EQ(1u, connection_listener_->GetReadSocketCount());
}
} // namespace chrome_browser_net