// Copyright 2018 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 #include #include #include "base/run_loop.h" #include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "base/task/post_task.h" #include "base/test/scoped_feature_list.h" #include "content/browser/loader/prefetch_url_loader_service.h" #include "content/browser/storage_partition_impl.h" #include "content/browser/web_package/mock_signed_exchange_handler.h" #include "content/browser/web_package/signed_exchange_loader.h" #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/web_contents.h" #include "content/public/common/content_features.h" #include "content/public/test/browser_test_utils.h" #include "content/public/test/content_browser_test.h" #include "content/public/test/content_browser_test_utils.h" #include "content/shell/browser/shell.h" #include "net/test/embedded_test_server/http_request.h" #include "net/test/embedded_test_server/http_response.h" #include "services/network/public/cpp/features.h" namespace content { struct PrefetchBrowserTestParam { PrefetchBrowserTestParam(bool signed_exchange_enabled) : signed_exchange_enabled(signed_exchange_enabled) {} const bool signed_exchange_enabled; }; struct ScopedSignedExchangeHandlerFactory { explicit ScopedSignedExchangeHandlerFactory( SignedExchangeHandlerFactory* factory) { SignedExchangeLoader::SetSignedExchangeHandlerFactoryForTest(factory); } ~ScopedSignedExchangeHandlerFactory() { SignedExchangeLoader::SetSignedExchangeHandlerFactoryForTest(nullptr); } }; class PrefetchBrowserTest : public ContentBrowserTest, public testing::WithParamInterface { public: struct ResponseEntry { ResponseEntry() = default; explicit ResponseEntry( const std::string& content, const std::string& content_type = "text/html", const std::vector>& headers = {}) : content(content), content_type(content_type), headers(headers) {} ~ResponseEntry() = default; std::string content; std::string content_type; std::vector> headers; }; PrefetchBrowserTest() = default; ~PrefetchBrowserTest() = default; void SetUp() override { std::vector enable_features; if (GetParam().signed_exchange_enabled) enable_features.push_back(features::kSignedHTTPExchange); feature_list_.InitWithFeatures(enable_features, {}); ContentBrowserTest::SetUp(); } void SetUpOnMainThread() override { ContentBrowserTest::SetUpOnMainThread(); StoragePartitionImpl* partition = static_cast( BrowserContext::GetDefaultStoragePartition( shell()->web_contents()->GetBrowserContext())); base::PostTaskWithTraits( FROM_HERE, {BrowserThread::IO}, BindOnce( &PrefetchURLLoaderService::RegisterPrefetchLoaderCallbackForTest, base::RetainedRef(partition->GetPrefetchURLLoaderService()), base::BindRepeating(&PrefetchBrowserTest::OnPrefetchURLLoaderCalled, base::Unretained(this)))); } void RegisterResponse(const std::string& url, const ResponseEntry& entry) { response_map_[url] = entry; } std::unique_ptr ServeResponses( const net::test_server::HttpRequest& request) { auto found = response_map_.find(request.relative_url); if (found != response_map_.end()) { auto response = std::make_unique(); response->set_code(net::HTTP_OK); response->set_content(found->second.content); response->set_content_type(found->second.content_type); for (const auto& header : found->second.headers) response->AddCustomHeader(header.first, header.second); return std::move(response); } return nullptr; } void WatchURLAndRunClosure( const std::string& relative_url, int* visit_count, base::OnceClosure closure, const net::test_server::HttpRequest& request) { if (request.relative_url == relative_url) { (*visit_count)++; if (closure) std::move(closure).Run(); } } void OnPrefetchURLLoaderCalled() { prefetch_url_loader_called_++; } bool CheckPrefetchURLLoaderCountIfSupported(int expected) const { if (!base::FeatureList::IsEnabled(features::kSignedHTTPExchange)) return true; return prefetch_url_loader_called_ == expected; } int prefetch_url_loader_called_ = 0; private: base::test::ScopedFeatureList feature_list_; std::map response_map_; DISALLOW_COPY_AND_ASSIGN(PrefetchBrowserTest); }; IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, Simple) { int target_fetch_count = 0; const char* prefetch_url = "/prefetch.html"; const char* target_url = "/target.html"; RegisterResponse( prefetch_url, ResponseEntry(base::StringPrintf( "", target_url))); RegisterResponse( target_url, ResponseEntry("Prefetch Target")); base::RunLoop prefetch_waiter; embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( &PrefetchBrowserTest::WatchURLAndRunClosure, base::Unretained(this), target_url, &target_fetch_count, prefetch_waiter.QuitClosure())); embedded_test_server()->RegisterRequestHandler(base::BindRepeating( &PrefetchBrowserTest::ServeResponses, base::Unretained(this))); ASSERT_TRUE(embedded_test_server()->Start()); EXPECT_EQ(0, target_fetch_count); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(0)); // Loading a page that prefetches the target URL would increment the // |target_fetch_count|. NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_url)); prefetch_waiter.Run(); EXPECT_EQ(1, target_fetch_count); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(1)); // Subsequent navigation to the target URL wouldn't hit the network for // the target URL (therefore not increment |target_fetch_count|. // The target content should still be read correctly. base::string16 title = base::ASCIIToUTF16("Prefetch Target"); TitleWatcher title_watcher(shell()->web_contents(), title); NavigateToURL(shell(), embedded_test_server()->GetURL(target_url)); EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); EXPECT_EQ(1, target_fetch_count); } IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, DoublePrefetch) { int target_fetch_count = 0; const char* prefetch_url = "/prefetch.html"; const char* target_url = "/target.html"; RegisterResponse(prefetch_url, ResponseEntry(base::StringPrintf( "" "", target_url, target_url))); RegisterResponse( target_url, ResponseEntry("Prefetch Target")); base::RunLoop prefetch_waiter; embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( &PrefetchBrowserTest::WatchURLAndRunClosure, base::Unretained(this), target_url, &target_fetch_count, prefetch_waiter.QuitClosure())); embedded_test_server()->RegisterRequestHandler(base::BindRepeating( &PrefetchBrowserTest::ServeResponses, base::Unretained(this))); ASSERT_TRUE(embedded_test_server()->Start()); EXPECT_EQ(0, target_fetch_count); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(0)); // Loading a page that prefetches the target URL would increment the // |target_fetch_count|, but it should hit only once. NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_url)); prefetch_waiter.Run(); EXPECT_EQ(1, target_fetch_count); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(1)); // Subsequent navigation to the target URL wouldn't hit the network for // the target URL (therefore not increment |target_fetch_count|. // The target content should still be read correctly. base::string16 title = base::ASCIIToUTF16("Prefetch Target"); TitleWatcher title_watcher(shell()->web_contents(), title); NavigateToURL(shell(), embedded_test_server()->GetURL(target_url)); EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); EXPECT_EQ(1, target_fetch_count); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(1)); } IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, NoCacheAndNoStore) { int nocache_fetch_count = 0; int nostore_fetch_count = 0; const char* prefetch_url = "/prefetch.html"; const char* nocache_url = "/target1.html"; const char* nostore_url = "/target2.html"; RegisterResponse(prefetch_url, ResponseEntry(base::StringPrintf( "" "" "", nocache_url, nostore_url))); RegisterResponse(nocache_url, ResponseEntry("NoCache Target", "text/html", {{"cache-control", "no-cache"}})); RegisterResponse(nostore_url, ResponseEntry("NoStore Target", "text/html", {{"cache-control", "no-store"}})); base::RunLoop nocache_waiter; base::RunLoop nostore_waiter; embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( &PrefetchBrowserTest::WatchURLAndRunClosure, base::Unretained(this), nocache_url, &nocache_fetch_count, nocache_waiter.QuitClosure())); embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( &PrefetchBrowserTest::WatchURLAndRunClosure, base::Unretained(this), nostore_url, &nostore_fetch_count, nostore_waiter.QuitClosure())); embedded_test_server()->RegisterRequestHandler(base::BindRepeating( &PrefetchBrowserTest::ServeResponses, base::Unretained(this))); ASSERT_TRUE(embedded_test_server()->Start()); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(0)); // Loading a page that prefetches the target URL would increment the // fetch count for the both targets. NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_url)); nocache_waiter.Run(); nostore_waiter.Run(); EXPECT_EQ(1, nocache_fetch_count); EXPECT_EQ(1, nostore_fetch_count); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(2)); { // Subsequent navigation to the no-cache URL wouldn't hit the network, // because no-cache resource is kept available up to kPrefetchReuseMins. base::string16 title = base::ASCIIToUTF16("NoCache Target"); TitleWatcher title_watcher(shell()->web_contents(), title); NavigateToURL(shell(), embedded_test_server()->GetURL(nocache_url)); EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); EXPECT_EQ(1, nocache_fetch_count); } { // Subsequent navigation to the no-store URL hit the network again, // because no-store resource is not cached even for prefetch. base::string16 title = base::ASCIIToUTF16("NoStore Target"); TitleWatcher title_watcher(shell()->web_contents(), title); NavigateToURL(shell(), embedded_test_server()->GetURL(nostore_url)); EXPECT_EQ(title, title_watcher.WaitAndGetTitle()); EXPECT_EQ(2, nostore_fetch_count); } EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(2)); } IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, WithPreload) { int target_fetch_count = 0; int preload_fetch_count = 0; const char* prefetch_url = "/prefetch.html"; const char* target_url = "/target.html"; const char* preload_url = "/preload.js"; RegisterResponse( prefetch_url, ResponseEntry(base::StringPrintf( "", target_url))); RegisterResponse( target_url, ResponseEntry( "Prefetch Target", "text/html", {{"link", "<./preload.js>;rel=\"preload\";as=\"script\""}})); RegisterResponse(preload_url, ResponseEntry("function foo() {}", "text/javascript")); base::RunLoop preload_waiter; embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( &PrefetchBrowserTest::WatchURLAndRunClosure, base::Unretained(this), target_url, &target_fetch_count, base::RepeatingClosure())); embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( &PrefetchBrowserTest::WatchURLAndRunClosure, base::Unretained(this), preload_url, &preload_fetch_count, preload_waiter.QuitClosure())); embedded_test_server()->RegisterRequestHandler(base::BindRepeating( &PrefetchBrowserTest::ServeResponses, base::Unretained(this))); ASSERT_TRUE(embedded_test_server()->Start()); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(0)); // Loading a page that prefetches the target URL would increment both // |target_fetch_count| and |preload_fetch_count|. NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_url)); preload_waiter.Run(); EXPECT_EQ(1, target_fetch_count); EXPECT_EQ(1, preload_fetch_count); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(1)); int preload_url_entries = 0; while (preload_url_entries == 0) { const bool result = ExecuteScriptAndExtractInt( shell()->web_contents(), base::StringPrintf( "window.domAutomationController.send(performance.getEntriesByName('" "%s').length)", embedded_test_server()->GetURL(preload_url).spec().c_str()), &preload_url_entries); ASSERT_TRUE(result); } EXPECT_GE(preload_url_entries, 1); } IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, WebPackageWithPreload) { int target_fetch_count = 0; int preload_fetch_count = 0; const char* prefetch_url = "/prefetch.html"; const char* target_sxg = "/target.sxg"; const char* target_url = "/target.html"; const char* preload_url_in_sxg = "/preload.js"; RegisterResponse( prefetch_url, ResponseEntry(base::StringPrintf( "", target_sxg))); RegisterResponse( target_sxg, // We mock the SignedExchangeHandler, so just return a HTML content // as "application/signed-exchange;v=b2". ResponseEntry("Prefetch Target (SXG)", "application/signed-exchange;v=b2")); RegisterResponse(preload_url_in_sxg, ResponseEntry("function foo() {}", "text/javascript")); base::RunLoop preload_waiter; base::RunLoop prefetch_waiter; embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( &PrefetchBrowserTest::WatchURLAndRunClosure, base::Unretained(this), target_sxg, &target_fetch_count, prefetch_waiter.QuitClosure())); embedded_test_server()->RegisterRequestMonitor(base::BindRepeating( &PrefetchBrowserTest::WatchURLAndRunClosure, base::Unretained(this), preload_url_in_sxg, &preload_fetch_count, preload_waiter.QuitClosure())); embedded_test_server()->RegisterRequestHandler(base::BindRepeating( &PrefetchBrowserTest::ServeResponses, base::Unretained(this))); ASSERT_TRUE(embedded_test_server()->Start()); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(0)); MockSignedExchangeHandlerFactory factory( SignedExchangeLoadResult::kSuccess, net::OK, GURL(embedded_test_server()->GetURL(target_url)), "text/html", {base::StringPrintf( "Link: <%s>;rel=\"preload\";as=\"script\"", embedded_test_server()->GetURL(preload_url_in_sxg).spec().c_str())}); ScopedSignedExchangeHandlerFactory scoped_factory(&factory); // Loading a page that prefetches the target URL would increment both // |target_fetch_count| and |preload_fetch_count|. NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_url)); prefetch_waiter.Run(); EXPECT_EQ(1, target_fetch_count); EXPECT_TRUE(CheckPrefetchURLLoaderCountIfSupported(1)); // Test after this point requires SignedHTTPExchange support if (!base::FeatureList::IsEnabled(features::kSignedHTTPExchange)) return; // If the header in the .sxg file is correctly extracted, we should // be able to also see the preload. preload_waiter.Run(); EXPECT_EQ(1, preload_fetch_count); } INSTANTIATE_TEST_CASE_P(PrefetchBrowserTest, PrefetchBrowserTest, testing::Values(PrefetchBrowserTestParam(true), PrefetchBrowserTestParam(false))); } // namespace content