blob: 899f11cf9d2ebf5a3071826397cbb2d107f7d249 [file]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "content/browser/loader/prefetch_browsertest_base.h"
#include "content/browser/loader/prefetch_url_loader_service_context.h"
#include "content/browser/loader/subresource_proxying_url_loader_service.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_package/mock_signed_exchange_handler.h"
#include "content/browser/web_package/prefetched_signed_exchange_cache.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/test/browser_test.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/public/test/fenced_frame_test_util.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/url_loader_monitor.h"
#include "content/shell/browser/shell.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/base/features.h"
#include "net/base/filename_util.h"
#include "net/base/isolation_info.h"
#include "net/base/load_flags.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/scoped_mutually_exclusive_feature_list.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_client.h"
#include "services/network/test/test_url_loader_factory.h"
#include "third_party/blink/public/common/features.h"
namespace content {
namespace {
enum class SplitCacheTestCase {
kDisabled,
kEnabledTripleKeyed,
kEnabledTriplePlusCredsBool,
};
const struct {
const SplitCacheTestCase test_case;
base::test::FeatureRef feature;
} kTestCaseToFeatureMapping[] = {
{SplitCacheTestCase::kEnabledTriplePlusCredsBool,
net::features::kSplitCacheByIncludeCredentials}};
} // namespace
class PrefetchBrowserTest
: public PrefetchBrowserTestBase,
public testing::WithParamInterface<SplitCacheTestCase> {
public:
PrefetchBrowserTest()
: cross_origin_server_(std::make_unique<net::EmbeddedTestServer>()),
split_cache_test_case_(GetParam()),
split_cache_experiment_feature_list_(GetParam(),
kTestCaseToFeatureMapping) {
std::vector<base::test::FeatureRef> enabled_features;
std::vector<base::test::FeatureRef> disabled_features;
if (IsSplitCacheEnabled()) {
enabled_features.emplace_back(
net::features::kSplitCacheByNetworkIsolationKey);
} else {
disabled_features.emplace_back(
net::features::kSplitCacheByNetworkIsolationKey);
}
enabled_features.emplace_back(net::features::kHttpCacheNoVarySearch);
split_cache_enabled_feature_list_.InitWithFeatures(enabled_features,
disabled_features);
}
PrefetchBrowserTest(const PrefetchBrowserTest&) = delete;
PrefetchBrowserTest& operator=(const PrefetchBrowserTest&) = delete;
~PrefetchBrowserTest() override = default;
void SetUpOnMainThread() override {
PrefetchBrowserTestBase::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
}
bool IsSplitCacheEnabled() const {
return split_cache_test_case_ != SplitCacheTestCase::kDisabled;
}
protected:
std::unique_ptr<net::EmbeddedTestServer> cross_origin_server_;
const SplitCacheTestCase split_cache_test_case_;
private:
net::test::ScopedMutuallyExclusiveFeatureList
split_cache_experiment_feature_list_;
base::test::ScopedFeatureList split_cache_enabled_feature_list_;
};
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginDocumentHasNoSameSiteCookies) {
const char* prefetch_path = "/prefetch.html";
const char* target_path = "/target.html";
RegisterResponse(
target_path,
ResponseEntry("<head><title>Prefetch Target</title></head>"));
base::RunLoop prefetch_waiter;
auto request_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), target_path, &prefetch_waiter);
RegisterRequestHandler(cross_origin_server_.get());
ASSERT_TRUE(cross_origin_server_->Start());
const GURL cross_origin_target_url =
cross_origin_server_->GetURL("3p.example", target_path);
RegisterResponse(
prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' as='document' href='%s'></body>",
cross_origin_target_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, request_counter->GetRequestCount());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
URLLoaderMonitor monitor({cross_origin_target_url});
// Loading a page that prefetches the target URL would increment the
// |request_counter|.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
prefetch_waiter.Run();
EXPECT_EQ(1, request_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
monitor.WaitForUrls();
std::optional<network::ResourceRequest> request =
monitor.GetRequestInfo(cross_origin_target_url);
ASSERT_TRUE(request);
ASSERT_TRUE(request->site_for_cookies.IsNull());
ASSERT_TRUE(request->trusted_params);
url::Origin cross_origin = url::Origin::Create(cross_origin_target_url);
EXPECT_TRUE(net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, cross_origin,
cross_origin, net::SiteForCookies())
.IsEqualForTesting(request->trusted_params->isolation_info));
}
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginDocumentReusedAsNavigation) {
const char* prefetch_path = "/prefetch.html";
const char* target_path = "/target.html";
RegisterResponse(target_path,
ResponseEntry("<head><title>Prefetch Target</title></head>",
// The empty content type prevents this
// response from being blocked by ORB.
/*content_types=*/""));
base::RunLoop prefetch_waiter;
auto request_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), target_path, &prefetch_waiter);
RegisterRequestHandler(cross_origin_server_.get());
ASSERT_TRUE(cross_origin_server_->Start());
const GURL cross_origin_target_url =
cross_origin_server_->GetURL("3p.example", target_path);
RegisterResponse(
prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' as='document' href='%s'></body>",
cross_origin_target_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, request_counter->GetRequestCount());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
// Loading a page that prefetches the target URL would increment the
// |request_counter|.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
prefetch_waiter.Run();
EXPECT_EQ(1, request_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Shutdown the servers.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
EXPECT_TRUE(cross_origin_server_->ShutdownAndWaitUntilComplete());
// Subsequent navigation to the cross-origin target URL shouldn't hit the
// network, and should be loaded from cache.
NavigateToURLAndWaitTitle(cross_origin_target_url, "Prefetch Target");
}
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginDocumentFromOpaqueOrigin) {
// Prefetching as=document from a data: URL does not crash the renderer.
EXPECT_TRUE(NavigateToURL(
shell(),
GURL("data:text/html,<title>Data URL Prefetch Target</title><link "
"rel=prefetch as=document href=https://google.com>")));
}
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(
PrefetchBrowserTest,
DISABLED_CrossOriginDocumentNotReusedAsNestedFrameNavigation) {
// TODO(crbug.com/40093267): Remove this early-return when SplitCache is
// enabled by default.
if (!IsSplitCacheEnabled()) {
GTEST_SKIP() << "This test is relevant only with SplitCache.";
}
const char* prefetch_path = "/prefetch.html";
const char* host_path = "/host.html";
const char* iframe_path = "/iframe.html";
RegisterResponse(
host_path,
ResponseEntry(base::StringPrintf(
"<head><title>Cross-Origin Host</title></head><body><iframe "
"onload='document.title=\"Host Loaded\"' src='%s'></iframe></body>",
iframe_path)));
RegisterResponse(iframe_path, ResponseEntry("<h1>I am an iframe</h1>"));
base::RunLoop prefetch_waiter;
auto cross_origin_iframe_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), iframe_path, &prefetch_waiter);
RegisterRequestHandler(cross_origin_server_.get());
ASSERT_TRUE(cross_origin_server_->Start());
const GURL cross_origin_host_url =
cross_origin_server_->GetURL("3p.example", host_path);
const GURL cross_origin_iframe_url =
cross_origin_server_->GetURL("3p.example", iframe_path);
RegisterResponse(
prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' as='document' href='%s'></body>",
cross_origin_iframe_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, cross_origin_iframe_counter->GetRequestCount());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
// Loading a page that prefetches the cross-origin iframe URL increments its
// counter.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
prefetch_waiter.Run();
EXPECT_EQ(1, cross_origin_iframe_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Subsequent navigation to the cross-origin host site will trigger an iframe
// load which will not reuse the iframe that was prefetched from
// |prefetch_path|. This is because cross-origin document prefetches must
// only be reused for top-level navigations, and cannot be reused as
// cross-origin iframes.
NavigateToURLAndWaitTitle(cross_origin_host_url, "Host Loaded");
EXPECT_EQ(2, cross_origin_iframe_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Shutdown the servers.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
EXPECT_TRUE(cross_origin_server_->ShutdownAndWaitUntilComplete());
}
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginSubresourceNotReused) {
// TODO(crbug.com/40093267): Remove this early-return when SplitCache is
// enabled by default.
if (!IsSplitCacheEnabled()) {
GTEST_SKIP() << "This test is relevant only with SplitCache.";
}
const char* prefetch_path = "/prefetch.html";
const char* host_path = "/host.html";
const char* subresource_path = "/subresource.js";
RegisterResponse(
host_path,
ResponseEntry(base::StringPrintf(
"<head><title>Cross-Origin Host</title></head><body><script src='%s' "
"onload='document.title=\"Host Loaded\"'></script></body>",
subresource_path)));
RegisterResponse(subresource_path, ResponseEntry("console.log('I loaded')"));
base::RunLoop prefetch_waiter;
auto cross_origin_subresource_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), subresource_path, &prefetch_waiter);
RegisterRequestHandler(cross_origin_server_.get());
ASSERT_TRUE(cross_origin_server_->Start());
const GURL cross_origin_host_url =
cross_origin_server_->GetURL("3p.example", host_path);
const GURL cross_origin_subresource_url =
cross_origin_server_->GetURL("3p.example", subresource_path);
RegisterResponse(prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s'></body>",
cross_origin_subresource_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, cross_origin_subresource_counter->GetRequestCount());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
// Loading a page that prefetches the cross-origin subresource URL
// increments its counter.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
prefetch_waiter.Run();
EXPECT_EQ(1, cross_origin_subresource_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Subsequent navigation to the cross-origin host attempting to reuse the
// resource that was prefetched results in the request hitting the network.
// This is because cross-origin subresources must only be reused within the
// frame they were fetched from.
NavigateToURLAndWaitTitle(cross_origin_host_url, "Host Loaded");
EXPECT_EQ(2, cross_origin_subresource_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Shutdown the servers.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
EXPECT_TRUE(cross_origin_server_->ShutdownAndWaitUntilComplete());
}
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginSubresourceReusedByCurrentFrame) {
const char* prefetch_path = "/prefetch.html";
const char* use_prefetch_path = "/use-prefetch.html";
const char* subresource_path = "/subresource.js";
RegisterResponse(subresource_path, ResponseEntry("console.log('I loaded')"));
base::RunLoop prefetch_waiter;
auto cross_origin_subresource_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), subresource_path, &prefetch_waiter);
RegisterRequestHandler(cross_origin_server_.get());
ASSERT_TRUE(cross_origin_server_->Start());
const GURL cross_origin_subresource_url =
cross_origin_server_->GetURL("3p.example", subresource_path);
RegisterResponse(prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s'></body>",
cross_origin_subresource_url.spec().c_str())));
RegisterResponse(use_prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><script src='%s' onload='document.title=\"Use "
"Prefetch Loaded\"'></script></body>",
cross_origin_subresource_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, cross_origin_subresource_counter->GetRequestCount());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
// Loading a page that prefetches the cross-origin subresource URL
// increments its counter.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
prefetch_waiter.Run();
EXPECT_EQ(1, cross_origin_subresource_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Shut down the cross-origin server.
EXPECT_TRUE(cross_origin_server_->ShutdownAndWaitUntilComplete());
// Subsequent navigation to the same-origin document that attempts to reuse
// the cross-origin prefetch is able to reuse the resource from the cache.
NavigateToURLAndWaitTitle(embedded_test_server()->GetURL(use_prefetch_path),
"Use Prefetch Loaded");
// Shutdown the same-origin server.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
}
// This tests more of an implementation detail than anything. A single resource
// must be committed to the cache partition corresponding to a single
// NetworkAnonymizationKey. This means that even though it is considered "safe"
// to reused cross-origin subresource prefetches for top-level navigations, we
// can't actually do this, because the subresource is only reusable from the
// frame that fetched it.
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginSubresourceNotReusedAsNavigation) {
// TODO(crbug.com/40093267): Remove this early-return when SplitCache is
// enabled by default.
if (!IsSplitCacheEnabled()) {
GTEST_SKIP() << "This test is relevant only with SplitCache.";
}
const char* prefetch_path = "/prefetch.html";
const char* subresource_path = "/subresource.js";
RegisterResponse(subresource_path, ResponseEntry("console.log('I loaded');"));
base::RunLoop prefetch_waiter;
auto cross_origin_subresource_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), subresource_path, &prefetch_waiter);
RegisterRequestHandler(cross_origin_server_.get());
ASSERT_TRUE(cross_origin_server_->Start());
const GURL cross_origin_subresource_url =
cross_origin_server_->GetURL("3p.example", subresource_path);
RegisterResponse(prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s'></body>",
cross_origin_subresource_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, cross_origin_subresource_counter->GetRequestCount());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
// Loading a page that prefetches the cross-origin subresource URL
// increments its counter.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
prefetch_waiter.Run();
EXPECT_EQ(1, cross_origin_subresource_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Shutdown the same-origin server.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
// Subsequent navigation to the cross-origin subresource itself will not be
// reused from the cache, because the cached resource is not partitioned under
// the cross-origin it is served from.
EXPECT_TRUE(NavigateToURL(shell(), cross_origin_subresource_url));
EXPECT_EQ(2, cross_origin_subresource_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Shutdown the cross-origin server.
EXPECT_TRUE(cross_origin_server_->ShutdownAndWaitUntilComplete());
}
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, Simple) {
const char* prefetch_path = "/prefetch.html";
const char* target_path = "/target.html";
RegisterResponse(
prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s'></body>", target_path)));
RegisterResponse(
target_path,
ResponseEntry("<head><title>Prefetch Target</title></head>", "text/html",
{{"cache-control", "public, max-age=3600"}}));
base::RunLoop prefetch_waiter;
auto request_counter = RequestCounter::CreateAndMonitor(
embedded_test_server(), target_path, &prefetch_waiter);
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, request_counter->GetRequestCount());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
const GURL target_url = embedded_test_server()->GetURL(target_path);
// Loading a page that prefetches the target URL would increment the
// |request_counter|.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
prefetch_waiter.Run();
EXPECT_EQ(1, request_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Shutdown the server.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
// Subsequent navigation to the target URL wouldn't hit the network for
// the target URL. The target content should still be read correctly.
NavigateToURLAndWaitTitle(target_url, "Prefetch Target");
}
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, DoublePrefetch) {
const char* prefetch_path = "/prefetch.html";
const char* target_path = "/target.html";
RegisterResponse(prefetch_path, ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s'>"
"<link rel='prefetch' href='%s'></body>",
target_path, target_path)));
RegisterResponse(
target_path,
ResponseEntry("<head><title>Prefetch Target</title></head>", "text/html",
{{"cache-control", "public, max-age=3600"}}));
base::RunLoop prefetch_waiter;
auto request_counter = RequestCounter::CreateAndMonitor(
embedded_test_server(), target_path, &prefetch_waiter);
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, request_counter->GetRequestCount());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
const GURL target_url = embedded_test_server()->GetURL(target_path);
// Loading a page that prefetches the target URL would increment the
// |request_counter|, but it should hit only once.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
prefetch_waiter.Run();
EXPECT_EQ(1, request_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Shutdown the server.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
// Subsequent navigation to the target URL wouldn't hit the network for
// the target URL. The target content should still be read correctly.
NavigateToURLAndWaitTitle(target_url, "Prefetch Target");
}
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, NoCacheAndNoStore) {
const char* prefetch_path = "/prefetch.html";
const char* nocache_path = "/target1.html";
const char* nostore_path = "/target2.html";
RegisterResponse(prefetch_path, ResponseEntry(base::StringPrintf(
"<body>"
"<link rel='prefetch' href='%s'>"
"<link rel='prefetch' href='%s'></body>",
nocache_path, nostore_path)));
RegisterResponse(nocache_path,
ResponseEntry("<head><title>NoCache Target</title></head>",
"text/html", {{"cache-control", "no-cache"}}));
RegisterResponse(nostore_path,
ResponseEntry("<head><title>NoStore Target</title></head>",
"text/html", {{"cache-control", "no-store"}}));
base::RunLoop nocache_waiter;
base::RunLoop nostore_waiter;
auto nocache_request_counter = RequestCounter::CreateAndMonitor(
embedded_test_server(), nocache_path, &nocache_waiter);
auto nostore_request_counter = RequestCounter::CreateAndMonitor(
embedded_test_server(), nostore_path, &nostore_waiter);
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
// Loading a page that prefetches the target URL would increment the
// fetch count for the both targets.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
nocache_waiter.Run();
nostore_waiter.Run();
EXPECT_EQ(1, nocache_request_counter->GetRequestCount());
EXPECT_EQ(1, nostore_request_counter->GetRequestCount());
EXPECT_EQ(2, GetPrefetchURLLoaderCallCount());
// Subsequent navigation to the no-cache URL do hit the network, because
// prefetch respects cache semantics.
NavigateToURLAndWaitTitle(embedded_test_server()->GetURL(nocache_path),
"NoCache Target");
EXPECT_EQ(2, nocache_request_counter->GetRequestCount());
// Subsequent navigation to the no-store URL hit the network again, for the
// same reason.
NavigateToURLAndWaitTitle(embedded_test_server()->GetURL(nostore_path),
"NoStore Target");
EXPECT_EQ(2, nostore_request_counter->GetRequestCount());
EXPECT_EQ(2, GetPrefetchURLLoaderCallCount());
}
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, WithPreload) {
const char* prefetch_path = "/prefetch.html";
const char* target_path = "/target.html";
const char* preload_path = "/preload.js";
RegisterResponse(
prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s'></body>", target_path)));
RegisterResponse(
target_path,
ResponseEntry("<head><title>Prefetch Target</title><script "
"src=\"./preload.js\"></script></head>",
"text/html",
{{"link", "</preload.js>;rel=\"preload\";as=\"script\""},
{"cache-control", "public, max-age=600"}}));
RegisterResponse(preload_path,
ResponseEntry("document.title=\"done\";", "text/javascript",
{{"cache-control", "public, max-age=600"}}));
base::RunLoop preload_waiter;
auto target_request_counter =
RequestCounter::CreateAndMonitor(embedded_test_server(), target_path);
auto preload_request_counter = RequestCounter::CreateAndMonitor(
embedded_test_server(), preload_path, &preload_waiter);
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
const GURL target_url = embedded_test_server()->GetURL(target_path);
// Loading a page that prefetches the target URL would increment both
// |target_request_counter| and |preload_request_counter|.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
preload_waiter.Run();
EXPECT_EQ(1, target_request_counter->GetRequestCount());
EXPECT_EQ(1, preload_request_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
WaitUntilLoaded(embedded_test_server()->GetURL(preload_path));
// Shutdown the server.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
NavigateToURLAndWaitTitle(target_url, "done");
}
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginWithPreloadHasNoSameSiteCookies) {
const char* target_path = "/target.html";
const char* preload_path = "/preload.js";
RegisterResponse(
target_path,
ResponseEntry("<head><title>Prefetch Target</title><script "
"src=\"./preload.js\"></script></head>",
"text/html",
{{"link", "</preload.js>;rel=\"preload\";as=\"script\""},
{"access-control-allow-origin", "*"}}));
RegisterResponse(preload_path,
ResponseEntry("document.title=\"done\";", "text/javascript",
{{"cache-control", "public, max-age=600"}}));
base::RunLoop preload_waiter;
auto target_request_counter =
RequestCounter::CreateAndMonitor(cross_origin_server_.get(), target_path);
auto preload_request_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), preload_path, &preload_waiter);
RegisterRequestHandler(cross_origin_server_.get());
ASSERT_TRUE(cross_origin_server_->Start());
const GURL cross_origin_target_url =
cross_origin_server_->GetURL("3p.example", target_path);
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s' as='document' "
"crossorigin='anonymous'></body>",
cross_origin_target_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
URLLoaderMonitor monitor({cross_origin_target_url});
// Loading a page that prefetches the target URL would increment both
// |target_request_counter| and |preload_request_counter|.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
preload_waiter.Run();
EXPECT_EQ(1, target_request_counter->GetRequestCount());
EXPECT_EQ(1, preload_request_counter->GetRequestCount());
EXPECT_EQ(2, GetPrefetchURLLoaderCallCount());
GURL cross_origin_preload_url =
cross_origin_server_->GetURL("3p.example", preload_path);
WaitUntilLoaded(cross_origin_preload_url);
monitor.WaitForUrls();
std::optional<network::ResourceRequest> request =
monitor.GetRequestInfo(cross_origin_target_url);
ASSERT_TRUE(request);
ASSERT_TRUE(request->site_for_cookies.IsNull());
ASSERT_TRUE(request->trusted_params);
url::Origin cross_origin = url::Origin::Create(cross_origin_target_url);
EXPECT_TRUE(net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, cross_origin,
cross_origin, net::SiteForCookies())
.IsEqualForTesting(request->trusted_params->isolation_info));
}
// Variants of this test:
// - PrefetchBrowserTest.CrossOriginWithPreloadAnonymous
// - PrefetchBrowserTest.CrossOriginWithPreloadCredentialled
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginWithPreloadAnonymous) {
const char* target_path = "/target.html";
const char* preload_path = "/preload.js";
RegisterResponse(
target_path,
ResponseEntry("<head><title>Prefetch Target</title><script "
"src=\"./preload.js\"></script></head>",
"text/html",
{{"link", "</preload.js>;rel=\"preload\";as=\"script\""},
{"access-control-allow-origin", "*"}}));
RegisterResponse(preload_path,
ResponseEntry("document.title=\"done\";", "text/javascript",
{{"cache-control", "public, max-age=600"}}));
base::RunLoop preload_waiter;
auto target_request_counter =
RequestCounter::CreateAndMonitor(cross_origin_server_.get(), target_path);
auto preload_request_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), preload_path, &preload_waiter);
RegisterRequestHandler(cross_origin_server_.get());
base::RunLoop preload_waiter_second_request;
auto preload_request_counter_second_request =
RequestCounter::CreateAndMonitor(cross_origin_server_.get(), preload_path,
&preload_waiter_second_request);
ASSERT_TRUE(cross_origin_server_->Start());
const GURL cross_origin_target_url =
cross_origin_server_->GetURL("3p.example", target_path);
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s' as='document' "
"crossorigin='anonymous'></body>",
cross_origin_target_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
// Loading a page that prefetches the target URL would increment both
// |target_request_counter| and |preload_request_counter|.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
preload_waiter.Run();
EXPECT_EQ(1, target_request_counter->GetRequestCount());
EXPECT_EQ(1, preload_request_counter->GetRequestCount());
EXPECT_EQ(2, GetPrefetchURLLoaderCallCount());
GURL cross_origin_preload_url =
cross_origin_server_->GetURL("3p.example", preload_path);
WaitUntilLoaded(cross_origin_preload_url);
// When SplitCache is enabled and the prefetch resource and its headers are
// fetched with a modified IsolationInfo, the preload header resource must
// not be reusable by any other origin but its parent prefetch's.
// TODO(crbug.com/40093267): When SplitCache is enabled by default, get rid of
// the below conditional.
if (IsSplitCacheEnabled()) {
// Spin up another server, hosting a page with a preload header identical to
// the one in |target_path|.
const char* reuse_preload_attempt_path = "/reuse.html";
RegisterResponse(
reuse_preload_attempt_path,
ResponseEntry(
base::StringPrintf("<head><title>Other site</title><script "
"src='%s'></script></head>",
cross_origin_preload_url.spec().c_str()),
"text/html",
{{"link",
base::StringPrintf("<%s>;rel=\"preload\";as=\"script\"",
cross_origin_preload_url.spec().c_str())},
{"access-control-allow-origin", "*"}}));
std::unique_ptr<net::EmbeddedTestServer> other_cross_origin_server =
std::make_unique<net::EmbeddedTestServer>();
RegisterRequestHandler(other_cross_origin_server.get());
ASSERT_TRUE(other_cross_origin_server->Start());
// Navigate to a page on the above-created server. A request for the same
// preload header fetched earlier must not be reusable, and must hit the
// network.
EXPECT_TRUE(NavigateToURL(
shell(), other_cross_origin_server->GetURL(
"other3p.example", reuse_preload_attempt_path)));
preload_waiter_second_request.Run();
EXPECT_EQ(2, preload_request_counter_second_request->GetRequestCount());
// We won't need this server again.
EXPECT_TRUE(other_cross_origin_server->ShutdownAndWaitUntilComplete());
}
if (split_cache_test_case_ ==
SplitCacheTestCase::kEnabledTriplePlusCredsBool) {
// The navigation is requested with credentials, but the prefetch is
// requested anonymously. As a result of "SplitCacheByIncludeCredentials",
// those aren't considered the same for the HTTP cache. Early return.
// See the variant of this test in:
// PrefetchBrowserTest.CrossOriginWithPreloadCredentialled
return;
}
// Shutdown the servers.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
EXPECT_TRUE(cross_origin_server_->ShutdownAndWaitUntilComplete());
EXPECT_TRUE(ExecJs(shell()->web_contents(), "document.title = 'not done';"));
// Subsequent navigation to the target URL wouldn't hit the network for
// the target URL. The target content should still be read correctly.
NavigateToURLAndWaitTitle(cross_origin_target_url, "done");
}
// Regression test for crbug.com/357325599 - If a Link header with
// rel="preload" has as="document" (which is invalid), we shouldn't attempt to
// treat this as a rel="prefetch" as="document" and instead should just ignore
// the header.
// TODO(crbug.com/40256279): De-flake and enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginWithInvalidPreloadAsDocument) {
const char* target_path = "/target.html";
const char* preload_path = "/preload.js";
RegisterResponse(
target_path,
ResponseEntry("<head><title>Prefetch Target</title><script "
"src=\"./preload.js\"></script></head>",
"text/html",
{{"link", "</preload.js>;rel=\"preload\";as=\"document\""},
{"access-control-allow-origin", "*"}}));
RegisterResponse(preload_path,
ResponseEntry("document.title=\"done\";", "text/javascript",
{{"cache-control", "public, max-age=600"}}));
auto target_request_counter =
RequestCounter::CreateAndMonitor(cross_origin_server_.get(), target_path);
auto preload_request_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), preload_path);
RegisterRequestHandler(cross_origin_server_.get());
base::RunLoop preload_waiter_second_request;
auto preload_request_counter_second_request =
RequestCounter::CreateAndMonitor(cross_origin_server_.get(), preload_path,
&preload_waiter_second_request);
ASSERT_TRUE(cross_origin_server_->Start());
const GURL cross_origin_target_url =
cross_origin_server_->GetURL("3p.example", target_path);
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s' as='document' "
"crossorigin='anonymous'></body>",
cross_origin_target_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
// Loading a page that prefetches the target URL would increment
// `target_request_counter` but not `preload_request_counter` because the
// preload header should be ignored.
base::test::TestFuture<void> prefetch_future;
RegisterPrefetchLoaderCallback(prefetch_future.GetCallback());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
EXPECT_TRUE(prefetch_future.Wait());
EXPECT_EQ(1, target_request_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
EXPECT_EQ(0, preload_request_counter->GetRequestCount());
// Subsequent navigation to the target URL should result in the preloaded JS
// being served from the network.
NavigateToURLAndWaitTitle(cross_origin_target_url, "done");
EXPECT_EQ(1, preload_request_counter->GetRequestCount());
}
// Variants of this test:
// - PrefetchBrowserTest.CrossOriginWithPreloadAnonymous
// - PrefetchBrowserTest.CrossOriginWithPreloadCredentialled
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginWithPreloadCredentialled) {
ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
const auto port = embedded_test_server()->port();
const char target_path[] = "/target.html";
const char preload_path[] = "/preload.js";
RegisterResponse(
target_path,
ResponseEntry("<head><title>Prefetch Target</title><script "
"src=\"./preload.js\"></script></head>",
"text/html",
{
{
"link",
"</preload.js>;rel=\"preload\";as=\"script\"",
},
{
"Access-Control-Allow-Origin",
"http://prefetch.com:" + base::NumberToString(port),
},
{
"Access-Control-Allow-Credentials",
"true",
},
}));
RegisterResponse(preload_path,
ResponseEntry("document.title=\"done\";", "text/javascript",
{{"cache-control", "public, max-age=600"}}));
base::RunLoop preload_waiter;
auto target_request_counter =
RequestCounter::CreateAndMonitor(cross_origin_server_.get(), target_path);
auto preload_request_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), preload_path, &preload_waiter);
RegisterRequestHandler(cross_origin_server_.get());
base::RunLoop preload_waiter_second_request;
auto preload_request_counter_second_request =
RequestCounter::CreateAndMonitor(cross_origin_server_.get(), preload_path,
&preload_waiter_second_request);
ASSERT_TRUE(cross_origin_server_->Start());
const GURL cross_origin_target_url =
cross_origin_server_->GetURL("3p.example", target_path);
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s' as='document' "
"crossorigin='use-credentials'></body>",
cross_origin_target_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
embedded_test_server()->StartAcceptingConnections();
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
// Loading a page that prefetches the target URL would increment both
// |target_request_counter| and |preload_request_counter|.
EXPECT_TRUE(NavigateToURL(
shell(), embedded_test_server()->GetURL("prefetch.com", prefetch_path)));
preload_waiter.Run();
EXPECT_EQ(1, target_request_counter->GetRequestCount());
EXPECT_EQ(1, preload_request_counter->GetRequestCount());
EXPECT_EQ(2, GetPrefetchURLLoaderCallCount());
GURL cross_origin_preload_url =
cross_origin_server_->GetURL("3p.example", preload_path);
WaitUntilLoaded(cross_origin_preload_url);
// Shutdown the servers.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
EXPECT_TRUE(cross_origin_server_->ShutdownAndWaitUntilComplete());
// Subsequent navigation to the target URL wouldn't hit the network for
// the target URL. The target content should still be read correctly.
NavigateToURLAndWaitTitle(cross_origin_target_url, "done");
}
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, SignedExchangeWithPreload) {
const char* prefetch_path = "/prefetch.html";
const char* target_sxg_path = "/target.sxg";
const char* target_path = "/target.html";
const char* preload_path_in_sxg = "/preload.js";
RegisterResponse(
prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' href='%s'></body>", target_sxg_path)));
RegisterResponse(
target_sxg_path,
// We mock the SignedExchangeHandler, so just return a HTML content
// as "application/signed-exchange;v=b3".
ResponseEntry(MockSignedExchangeHandler::kMockSxgPrefix +
"<head><title>Prefetch Target (SXG)</title><script "
"src=\"./preload.js\"></script></head>",
"application/signed-exchange;v=b3",
{{"x-content-type-options", "nosniff"}}));
RegisterResponse(preload_path_in_sxg,
ResponseEntry("document.title=\"done\";", "text/javascript",
{{"cache-control", "public, max-age=600"}}));
base::RunLoop preload_waiter;
base::RunLoop prefetch_waiter;
auto target_request_counter = RequestCounter::CreateAndMonitor(
embedded_test_server(), target_sxg_path, &prefetch_waiter);
auto preload_request_counter = RequestCounter::CreateAndMonitor(
embedded_test_server(), preload_path_in_sxg, &preload_waiter);
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
const GURL preload_url_in_sxg =
embedded_test_server()->GetURL(preload_path_in_sxg);
const GURL target_sxg_url = embedded_test_server()->GetURL(target_sxg_path);
MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams(
target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
GURL(embedded_test_server()->GetURL(target_path)), "text/html",
{{"Link", base::StringPrintf("<%s>;rel=\"preload\";as=\"script\"",
preload_url_in_sxg.spec().c_str())}},
net::SHA256HashValue({{0x00}}))});
ScopedSignedExchangeHandlerFactory scoped_factory(&factory);
// Loading a page that prefetches the target URL would increment both
// |target_request_counter| and |preload_request_counter|.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
prefetch_waiter.Run();
EXPECT_EQ(1, target_request_counter->GetRequestCount());
// 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_request_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
// Shutdown the server.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
// Subsequent navigation to the target URL wouldn't hit the network for
// the target URL. The target content should still be read correctly.
NavigateToURLAndWaitTitle(target_sxg_url, "done");
}
// TODO(crbug.com/40256279): De-flake and re-enable.
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest,
DISABLED_CrossOriginSignedExchangeWithPreload) {
const char* prefetch_path = "/prefetch.html";
const char* target_sxg_path = "/target.sxg";
const char* target_path = "/target.html";
const char* preload_path_in_sxg = "/preload.js";
RegisterResponse(
target_sxg_path,
// We mock the SignedExchangeHandler, so just return a HTML content
// as "application/signed-exchange;v=b3".
ResponseEntry(MockSignedExchangeHandler::kMockSxgPrefix +
"<head><title>Prefetch Target (SXG)</title><script "
"src=\"./preload.js\"></script></head>",
"application/signed-exchange;v=b3",
{{"x-content-type-options", "nosniff"}}));
RegisterResponse(preload_path_in_sxg,
ResponseEntry("document.title=\"done\";", "text/javascript",
{{"cache-control", "public, max-age=600"}}));
base::RunLoop preload_waiter;
base::RunLoop prefetch_waiter;
auto target_request_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), target_sxg_path, &prefetch_waiter);
auto preload_request_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), preload_path_in_sxg, &preload_waiter);
RegisterRequestHandler(cross_origin_server_.get());
ASSERT_TRUE(cross_origin_server_->Start());
const GURL target_sxg_url =
cross_origin_server_->GetURL("3p.example", target_sxg_path);
const GURL preload_url_in_sxg =
cross_origin_server_->GetURL("3p.example", preload_path_in_sxg);
RegisterResponse(
prefetch_path,
ResponseEntry(base::StringPrintf(
"<body><link rel='prefetch' as='document' href='%s'></body>",
target_sxg_url.spec().c_str())));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
MockSignedExchangeHandlerFactory factory({MockSignedExchangeHandlerParams(
target_sxg_url, SignedExchangeLoadResult::kSuccess, net::OK,
GURL(cross_origin_server_->GetURL("3p.example", target_path)),
"text/html",
{{"Link", base::StringPrintf("<%s>;rel=\"preload\";as=\"script\"",
preload_url_in_sxg.spec().c_str())}},
net::SHA256HashValue({{0x00}}))});
ScopedSignedExchangeHandlerFactory scoped_factory(&factory);
// Loading a page that prefetches the target URL would increment both
// |target_request_counter| and |preload_request_counter|.
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
prefetch_waiter.Run();
EXPECT_EQ(1, target_request_counter->GetRequestCount());
// 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_request_counter->GetRequestCount());
EXPECT_EQ(2, GetPrefetchURLLoaderCallCount());
WaitUntilLoaded(preload_url_in_sxg);
// Shutdown the servers.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
EXPECT_TRUE(cross_origin_server_->ShutdownAndWaitUntilComplete());
// Subsequent navigation to the target URL wouldn't hit the network for
// the target URL. The target content should still be read correctly.
NavigateToURLAndWaitTitle(target_sxg_url, "done");
}
IN_PROC_BROWSER_TEST_P(PrefetchBrowserTest, FileToHttp) {
const char* target_path = "/target.html";
RegisterResponse(
target_path,
ResponseEntry("<head><title>Prefetch Target</title></head>",
// The empty content type prevents this
// response from being blocked by ORB.
/*content_types=*/"",
{{"cache-control", "public, max-age=31536000"}}));
base::RunLoop prefetch_waiter;
auto request_counter = RequestCounter::CreateAndMonitor(
embedded_test_server(), target_path, &prefetch_waiter);
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_EQ(0, request_counter->GetRequestCount());
EXPECT_EQ(0, GetPrefetchURLLoaderCallCount());
const GURL target_url = embedded_test_server()->GetURL(target_path);
{
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
base::FilePath file_path = temp_dir.GetPath().AppendASCII("test.html");
std::string file_content = base::StringPrintf(
"<body><link rel='prefetch' as='document' href='%s'></body>",
target_url.spec().c_str());
ASSERT_TRUE(base::WriteFile(file_path, file_content));
// Loading a page that prefetches the target URL would increment the
// |request_counter|.
GURL file_url = net::FilePathToFileURL(file_path);
EXPECT_TRUE(NavigateToURL(shell(), file_url));
prefetch_waiter.Run();
EXPECT_EQ(1, request_counter->GetRequestCount());
EXPECT_EQ(1, GetPrefetchURLLoaderCallCount());
}
// Shutdown the server.
EXPECT_TRUE(embedded_test_server()->ShutdownAndWaitUntilComplete());
// Subsequent navigation to the target URL wouldn't hit the network for
// the target URL. The target content should still be read correctly.
NavigateToURLAndWaitTitle(target_url, "Prefetch Target");
}
class PrefetchLoadFlagsBrowserTest : public PrefetchBrowserTestBase {
public:
PrefetchLoadFlagsBrowserTest()
: cross_origin_server_(std::make_unique<net::EmbeddedTestServer>()) {}
void SetUpOnMainThread() override {
PrefetchBrowserTestBase::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
}
protected:
// Binds a remote to the browser-side `SubresourceProxyingURLLoaderService`
// for the current main frame, mirroring the binding that the renderer would
// receive at navigation commit time.
mojo::Remote<network::mojom::URLLoaderFactory> BindFactoryForMainFrame() {
RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
StoragePartitionImpl* partition =
static_cast<StoragePartitionImpl*>(rfh->GetStoragePartition());
mojo::Remote<network::mojom::URLLoaderFactory> remote;
partition->GetSubresourceProxyingURLLoaderService()->GetFactory(
remote.BindNewPipeAndPassReceiver(), rfh->GetFrameTreeNodeId(),
proxied_factory_.GetSafeWeakWrapper(), rfh->GetWeakPtr(),
/*prefetched_signed_exchange_cache=*/nullptr);
return remote;
}
network::ResourceRequest CreateCrossOriginPrefetchRequest(
const GURL& target_url) {
network::ResourceRequest request;
request.url = target_url;
request.load_flags =
net::LOAD_PREFETCH | net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME;
request.request_initiator = shell()
->web_contents()
->GetPrimaryMainFrame()
->GetLastCommittedOrigin();
return request;
}
std::unique_ptr<net::EmbeddedTestServer> cross_origin_server_;
network::TestURLLoaderFactory proxied_factory_;
};
// Verifies that a cross-origin prefetch request from the renderer is rejected
// if it carries load flags that are not permitted on requests originating from
// an untrusted process. The request reaches the network service via a trusted
// loader factory, so the browser must validate the flags before forwarding.
IN_PROC_BROWSER_TEST_F(PrefetchLoadFlagsBrowserTest,
CrossOriginPrefetchRejectsRestrictedLoadFlags) {
const char* prefetch_path = "/prefetch.html";
const char* target_path = "/target.html";
RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
RegisterResponse(target_path,
ResponseEntry("<head><title>Target</title></head>",
/*content_types=*/""));
RegisterRequestHandler(embedded_test_server());
RegisterRequestHandler(cross_origin_server_.get());
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(cross_origin_server_->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
const GURL target_url =
cross_origin_server_->GetURL("3p.example", target_path);
mojo::Remote<network::mojom::URLLoaderFactory> factory =
BindFactoryForMainFrame();
for (int restricted_flag :
{net::LOAD_BYPASS_PROXY, net::LOAD_DISABLE_CERT_NETWORK_FETCHES}) {
SCOPED_TRACE(testing::Message() << "restricted_flag=" << restricted_flag);
network::ResourceRequest request =
CreateCrossOriginPrefetchRequest(target_url);
request.load_flags |= restricted_flag;
mojo::test::BadMessageObserver bad_message_observer;
network::TestURLLoaderClient client;
mojo::Remote<network::mojom::URLLoader> loader;
factory->CreateLoaderAndStart(
loader.BindNewPipeAndPassReceiver(), /*request_id=*/0,
network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
client.RunUntilComplete();
EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
EXPECT_EQ("Prefetch/CreatePrefetchLoaderAndStart: restricted load flag",
bad_message_observer.WaitForBadMessage());
// The receiver is removed when a bad message is reported, so rebind for
// the next iteration.
factory.reset();
factory = BindFactoryForMainFrame();
}
}
// Verifies that a cross-origin prefetch request carrying only load flags that
// are permitted on requests from an untrusted process is forwarded to the
// network service.
IN_PROC_BROWSER_TEST_F(PrefetchLoadFlagsBrowserTest,
CrossOriginPrefetchAllowsRendererLoadFlags) {
const char* prefetch_path = "/prefetch.html";
const char* target_path = "/target.html";
RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
RegisterResponse(target_path,
ResponseEntry("<head><title>Target</title></head>",
/*content_types=*/""));
RegisterRequestHandler(embedded_test_server());
base::RunLoop prefetch_waiter;
auto request_counter = RequestCounter::CreateAndMonitor(
cross_origin_server_.get(), target_path, &prefetch_waiter);
RegisterRequestHandler(cross_origin_server_.get());
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(cross_origin_server_->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
const GURL target_url =
cross_origin_server_->GetURL("3p.example", target_path);
mojo::Remote<network::mojom::URLLoaderFactory> factory =
BindFactoryForMainFrame();
network::ResourceRequest request =
CreateCrossOriginPrefetchRequest(target_url);
request.load_flags |= net::LOAD_SUPPORT_ASYNC_REVALIDATION;
network::TestURLLoaderClient client;
mojo::Remote<network::mojom::URLLoader> loader;
factory->CreateLoaderAndStart(
loader.BindNewPipeAndPassReceiver(), /*request_id=*/0,
network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
client.RunUntilComplete();
EXPECT_EQ(net::OK, client.completion_status().error_code);
prefetch_waiter.Run();
EXPECT_EQ(1, request_counter->GetRequestCount());
}
class PrefetchRecursiveBrowserTest : public PrefetchBrowserTestBase {
public:
PrefetchRecursiveBrowserTest() = default;
void SetUpOnMainThread() override {
PrefetchBrowserTestBase::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
}
protected:
mojo::Remote<network::mojom::URLLoaderFactory> BindFactoryForMainFrame() {
RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame());
StoragePartitionImpl* partition =
static_cast<StoragePartitionImpl*>(rfh->GetStoragePartition());
mojo::Remote<network::mojom::URLLoaderFactory> remote;
bind_context_ =
partition->GetSubresourceProxyingURLLoaderService()->GetFactory(
remote.BindNewPipeAndPassReceiver(), rfh->GetFrameTreeNodeId(),
proxied_factory_.GetSafeWeakWrapper(), rfh->GetWeakPtr(),
/*prefetched_signed_exchange_cache=*/nullptr);
return remote;
}
// Registers a recursive-prefetch token on the current `BindContext` bound to
// the given origin, and points the context's cross-origin factory at
// `cross_origin_factory_` so forwarded requests can be observed.
base::UnguessableToken RegisterRecursivePrefetchToken(
const url::Origin& origin) {
CHECK(bind_context_);
bind_context_->cross_origin_factory =
cross_origin_factory_.GetSafeWeakWrapper();
base::UnguessableToken token = base::UnguessableToken::Create();
bind_context_->total_tokens_generated++;
bind_context_->prefetch_isolation_infos.Put(
token,
net::IsolationInfo::Create(net::IsolationInfo::RequestType::kOther,
origin, origin, net::SiteForCookies()));
return token;
}
network::TestURLLoaderFactory proxied_factory_;
network::TestURLLoaderFactory cross_origin_factory_;
base::WeakPtr<SubresourceProxyingURLLoaderService::BindContext> bind_context_;
};
// Verifies that a recursive prefetch whose request URL is cross-origin with
// the initiating document is forwarded via the cross-origin factory using the
// `IsolationInfo` associated with its `recursive_prefetch_token`.
IN_PROC_BROWSER_TEST_F(PrefetchRecursiveBrowserTest,
RecursivePrefetchAllowsCrossOriginTarget) {
base::HistogramTester histogram_tester;
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
mojo::Remote<network::mojom::URLLoaderFactory> factory =
BindFactoryForMainFrame();
const url::Origin token_origin =
url::Origin::Create(GURL("https://3p.example"));
base::UnguessableToken token = RegisterRecursivePrefetchToken(token_origin);
const GURL target_url("https://3p.example/subresource.js");
network::ResourceRequest request;
request.url = target_url;
request.load_flags = net::LOAD_PREFETCH;
request.request_initiator = shell()
->web_contents()
->GetPrimaryMainFrame()
->GetLastCommittedOrigin();
request.recursive_prefetch_token = token;
network::TestURLLoaderClient client;
mojo::Remote<network::mojom::URLLoader> loader;
factory->CreateLoaderAndStart(
loader.BindNewPipeAndPassReceiver(), /*request_id=*/0,
network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
factory.FlushForTesting();
ASSERT_EQ(1, cross_origin_factory_.NumPending());
const network::ResourceRequest& forwarded =
cross_origin_factory_.GetPendingRequest(0)->request;
EXPECT_EQ(target_url, forwarded.url);
ASSERT_TRUE(forwarded.trusted_params);
EXPECT_EQ(token_origin,
forwarded.trusted_params->isolation_info.top_frame_origin());
EXPECT_EQ(token_origin,
forwarded.trusted_params->isolation_info.frame_origin());
histogram_tester.ExpectUniqueSample(
"Prefetch.RecursivePrefetch.TokenLookupResult",
RecursivePrefetchTokenLookupResult::kSuccess, 1);
}
// Verifies that a recursive prefetch whose request URL is same-origin with the
// initiating document is rejected as a bad message and not forwarded to the
// network.
IN_PROC_BROWSER_TEST_F(PrefetchRecursiveBrowserTest,
RecursivePrefetchRejectsInitiatorOriginTarget) {
base::HistogramTester histogram_tester;
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
mojo::Remote<network::mojom::URLLoaderFactory> factory =
BindFactoryForMainFrame();
const url::Origin token_origin =
url::Origin::Create(GURL("https://3p.example"));
base::UnguessableToken token = RegisterRecursivePrefetchToken(token_origin);
// Use a target URL that matches the initiating document's origin.
const GURL same_origin_target_url =
embedded_test_server()->GetURL("/subresource.js");
network::ResourceRequest request;
request.url = same_origin_target_url;
request.load_flags = net::LOAD_PREFETCH;
request.request_initiator = shell()
->web_contents()
->GetPrimaryMainFrame()
->GetLastCommittedOrigin();
request.recursive_prefetch_token = token;
mojo::test::BadMessageObserver bad_message_observer;
network::TestURLLoaderClient client;
mojo::Remote<network::mojom::URLLoader> loader;
factory->CreateLoaderAndStart(
loader.BindNewPipeAndPassReceiver(), /*request_id=*/0,
network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
client.RunUntilComplete();
EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
EXPECT_EQ("Prefetch/CreatePrefetchLoaderAndStart: initiator origin",
bad_message_observer.WaitForBadMessage());
EXPECT_EQ(0, cross_origin_factory_.NumPending());
histogram_tester.ExpectUniqueSample(
"Prefetch.RecursivePrefetch.TokenLookupResult",
RecursivePrefetchTokenLookupResult::kRejectedSameOrigin, 1);
}
// Verifies that presenting an invalid or evicted recursive prefetch token fails
// gracefully with ERR_INVALID_ARGUMENT and logs kTokenNotFound without crashing
// or reporting a bad message.
IN_PROC_BROWSER_TEST_F(PrefetchRecursiveBrowserTest,
RecursivePrefetchRejectsInvalidOrEvictedToken) {
base::HistogramTester histogram_tester;
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
mojo::Remote<network::mojom::URLLoaderFactory> factory =
BindFactoryForMainFrame();
bind_context_->cross_origin_factory =
cross_origin_factory_.GetSafeWeakWrapper();
const GURL target_url("https://3p.example/subresource.js");
network::ResourceRequest request;
request.url = target_url;
request.load_flags = net::LOAD_PREFETCH;
request.request_initiator = shell()
->web_contents()
->GetPrimaryMainFrame()
->GetLastCommittedOrigin();
request.recursive_prefetch_token = base::UnguessableToken::Create();
network::TestURLLoaderClient client;
mojo::Remote<network::mojom::URLLoader> loader;
factory->CreateLoaderAndStart(
loader.BindNewPipeAndPassReceiver(), /*request_id=*/0,
network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
client.RunUntilComplete();
EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
EXPECT_EQ(0, cross_origin_factory_.NumPending());
histogram_tester.ExpectUniqueSample(
"Prefetch.RecursivePrefetch.TokenLookupResult",
RecursivePrefetchTokenLookupResult::kTokenNotFound, 1);
}
// Verifies that stored isolation infos for recursive prefetch tokens are managed
// by an LRU cache, evicting older tokens when the limit is exceeded.
IN_PROC_BROWSER_TEST_F(PrefetchRecursiveBrowserTest,
RecursivePrefetchTokenLRUEviction) {
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
mojo::Remote<network::mojom::URLLoaderFactory> factory =
BindFactoryForMainFrame();
ASSERT_TRUE(bind_context_);
std::vector<base::UnguessableToken> tokens;
constexpr size_t kNumTokens =
SubresourceProxyingURLLoaderService::BindContext::
kMaxPrefetchIsolationInfoEntries +
5;
for (size_t i = 0; i < kNumTokens; ++i) {
tokens.push_back(RegisterRecursivePrefetchToken(
url::Origin::Create(GURL(base::StringPrintf("https://%zu.example", i)))));
}
EXPECT_EQ(
SubresourceProxyingURLLoaderService::BindContext::
kMaxPrefetchIsolationInfoEntries,
bind_context_->prefetch_isolation_infos.size());
// The earliest 5 tokens should have been evicted.
for (size_t i = 0; i < 5; ++i) {
EXPECT_EQ(bind_context_->prefetch_isolation_infos.end(),
bind_context_->prefetch_isolation_infos.Peek(tokens[i]));
}
// The latest tokens should still be present.
for (size_t i = 5; i < kNumTokens; ++i) {
EXPECT_NE(bind_context_->prefetch_isolation_infos.end(),
bind_context_->prefetch_isolation_infos.Peek(tokens[i]));
}
}
// Verifies that a prefetch carrying both LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME
// and a recursive_prefetch_token is rejected with a bad message.
IN_PROC_BROWSER_TEST_F(PrefetchRecursiveBrowserTest,
RejectsRestrictedMainFrameWithRecursiveToken) {
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
mojo::Remote<network::mojom::URLLoaderFactory> factory =
BindFactoryForMainFrame();
const url::Origin token_origin =
url::Origin::Create(GURL("https://3p.example"));
base::UnguessableToken token = RegisterRecursivePrefetchToken(token_origin);
network::ResourceRequest request;
request.url = GURL("https://3p.example/target.html");
request.load_flags =
net::LOAD_PREFETCH | net::LOAD_RESTRICTED_PREFETCH_FOR_MAIN_FRAME;
request.request_initiator = shell()
->web_contents()
->GetPrimaryMainFrame()
->GetLastCommittedOrigin();
request.recursive_prefetch_token = token;
mojo::test::BadMessageObserver bad_message_observer;
network::TestURLLoaderClient client;
mojo::Remote<network::mojom::URLLoader> loader;
factory->CreateLoaderAndStart(
loader.BindNewPipeAndPassReceiver(), /*request_id=*/0,
network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
client.RunUntilComplete();
EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
EXPECT_EQ("Prefetch/CreatePrefetchLoaderAndStart: recursive token with "
"restricted main frame prefetch",
bad_message_observer.WaitForBadMessage());
EXPECT_EQ(0, cross_origin_factory_.NumPending());
}
// Verifies that a recursive prefetch request missing request_initiator is
// rejected with a bad message.
IN_PROC_BROWSER_TEST_F(PrefetchRecursiveBrowserTest,
RejectsRecursivePrefetchWithoutRequestInitiator) {
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
mojo::Remote<network::mojom::URLLoaderFactory> factory =
BindFactoryForMainFrame();
const url::Origin token_origin =
url::Origin::Create(GURL("https://3p.example"));
base::UnguessableToken token = RegisterRecursivePrefetchToken(token_origin);
network::ResourceRequest request;
request.url = GURL("https://3p.example/subresource.js");
request.load_flags = net::LOAD_PREFETCH;
request.request_initiator = std::nullopt;
request.recursive_prefetch_token = token;
mojo::test::BadMessageObserver bad_message_observer;
network::TestURLLoaderClient client;
mojo::Remote<network::mojom::URLLoader> loader;
factory->CreateLoaderAndStart(
loader.BindNewPipeAndPassReceiver(), /*request_id=*/0,
network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
client.RunUntilComplete();
EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
EXPECT_EQ("Prefetch/CreatePrefetchLoaderAndStart: no request_initiator",
bad_message_observer.WaitForBadMessage());
EXPECT_EQ(0, cross_origin_factory_.NumPending());
}
// Verifies that a recursive prefetch request whose request_initiator does not
// match the frame's committed origin is rejected with a bad message.
IN_PROC_BROWSER_TEST_F(PrefetchRecursiveBrowserTest,
RejectsRecursivePrefetchWithFrameOriginMismatch) {
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
mojo::Remote<network::mojom::URLLoaderFactory> factory =
BindFactoryForMainFrame();
const url::Origin token_origin =
url::Origin::Create(GURL("https://3p.example"));
base::UnguessableToken token = RegisterRecursivePrefetchToken(token_origin);
network::ResourceRequest request;
request.url = GURL("https://3p.example/subresource.js");
request.load_flags = net::LOAD_PREFETCH;
request.request_initiator =
url::Origin::Create(GURL("https://attacker.example"));
request.recursive_prefetch_token = token;
mojo::test::BadMessageObserver bad_message_observer;
network::TestURLLoaderClient client;
mojo::Remote<network::mojom::URLLoader> loader;
factory->CreateLoaderAndStart(
loader.BindNewPipeAndPassReceiver(), /*request_id=*/0,
network::mojom::kURLLoadOptionNone, request, client.CreateRemote(),
net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS));
client.RunUntilComplete();
EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code);
EXPECT_EQ("Prefetch/CreatePrefetchLoaderAndStart: frame origin mismatch",
bad_message_observer.WaitForBadMessage());
EXPECT_EQ(0, cross_origin_factory_.NumPending());
}
// Verifies that Prefetch.RecursivePrefetch.TokensPerDocument is recorded upon
// document navigation when tokens were generated.
IN_PROC_BROWSER_TEST_F(PrefetchRecursiveBrowserTest,
TokensPerDocumentHistogramRecordedOnNavigation) {
base::HistogramTester histogram_tester;
const char* prefetch_path = "/prefetch.html";
RegisterResponse(prefetch_path, ResponseEntry("<body></body>"));
RegisterRequestHandler(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
EXPECT_TRUE(
NavigateToURL(shell(), embedded_test_server()->GetURL(prefetch_path)));
mojo::Remote<network::mojom::URLLoaderFactory> factory =
BindFactoryForMainFrame();
ASSERT_TRUE(bind_context_);
for (int i = 0; i < 3; ++i) {
RegisterRecursivePrefetchToken(
url::Origin::Create(GURL(base::StringPrintf("https://%d.example", i))));
}
factory.reset();
EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
histogram_tester.ExpectUniqueSample(
"Prefetch.RecursivePrefetch.TokensPerDocument", /*sample=*/3,
/*expected_bucket_count=*/1);
}
INSTANTIATE_TEST_SUITE_P(
All,
PrefetchBrowserTest,
testing::ValuesIn({SplitCacheTestCase::kDisabled,
SplitCacheTestCase::kEnabledTripleKeyed,
SplitCacheTestCase::kEnabledTriplePlusCredsBool}),
[](const testing::TestParamInfo<SplitCacheTestCase>& info) {
switch (info.param) {
case SplitCacheTestCase::kDisabled:
return "SplitCacheDisabled";
case SplitCacheTestCase::kEnabledTripleKeyed:
return "SplitCacheEnabledTripleKeyed";
case SplitCacheTestCase::kEnabledTriplePlusCredsBool:
return "SplitCacheEnabledTriplePlusCredsBool";
}
});
} // namespace content