blob: 86774bb8540e619f9471c5138b7b9b922ce6b54c [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "content/browser/devtools/protocol/devtools_protocol_test_support.h"
#include "content/browser/loader/navigation_early_hints_manager.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/base/features.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_status_code.h"
#include "net/test/cert_test_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/embedded_test_server_connection_listener.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/quic_simple_test_server.h"
#include "net/test/test_data_directory.h"
#include "net/third_party/quiche/src/quiche/common/http/http_header_block.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/link_header.mojom.h"
namespace content {
using PreloadedResources = NavigationEarlyHintsManager::PreloadedResources;
namespace {
struct HeaderField {
HeaderField(const std::string& name, const std::string& value)
: name(name), value(value) {}
std::string name;
std::string value;
};
struct ResponseEntry {
ResponseEntry(const std::string& path, net::HttpStatusCode status_code)
: path(path) {
headers[":path"] = path;
headers[":status"] = base::StringPrintf("%d", status_code);
}
void AddEarlyHints(const std::vector<HeaderField>& header_fields) {
quiche::HttpHeaderBlock hints_headers;
for (const auto& header : header_fields)
hints_headers.AppendValueOrAddHeader(header.name, header.value);
early_hints.push_back(std::move(hints_headers));
}
std::string path;
quiche::HttpHeaderBlock headers;
std::string body;
std::vector<quiche::HttpHeaderBlock> early_hints;
};
const char kPageWithHintedScriptPath[] = "/page_with_hinted_js.html";
const char kPageWithHintedScriptBody[] = "<script src=\"/hinted.js\"></script>";
const char kPageWithHintedCorsScriptPath[] = "/page_with_hinted_cors_js.html";
const char kPageWithHintedCorsScriptBody[] =
"<script src=\"/hinted.js\" crossorigin></script>";
const char kPageWithIframePath[] = "/page_with_iframe.html";
const char kPageWithIframeBody[] =
"<iframe src=\"page_with_hinted_js.html\"></iframe>";
const char kPageWithHintedModuleScriptPath[] =
"/page_with_hinted_module_js.html";
const char kPageWithHintedModuleScriptBody[] =
"<script src=\"/hinted.js\" type=\"module\"></script>";
const char kHintedScriptPath[] = "/hinted.js";
const char kHintedScriptBody[] = "document.title = 'Done';";
const char kHintedStylesheetPath[] = "/hinted.css";
const char kHintedStylesheetBody[] = "/*empty*/";
const char kEmptyPagePath[] = "/empty.html";
const char kEmptyPageBody[] = "<html></html>";
const char kRedirectedPagePath[] = "/redirected.html";
const char kRedirectedPageBody[] = "<script src=\"/hinted.js\"></script>";
// Listens to sockets on an EmbeddedTestServer for preconnect tests. Created
// on the UI thread. EmbeddedTestServerConnectionListener methods are called
// from a different thread than the UI thread.
class PreconnectListener
: public net::test_server::EmbeddedTestServerConnectionListener {
public:
PreconnectListener()
: task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()),
weak_ptr_factory_(this) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
~PreconnectListener() override = default;
// net::test_server::EmbeddedTestServerConnectionListener implementation:
std::unique_ptr<net::StreamSocket> AcceptedSocket(
std::unique_ptr<net::StreamSocket> connection) override {
task_runner_->PostTask(
FROM_HERE, base::BindOnce(&PreconnectListener::AcceptedSocketOnUIThread,
weak_ptr_factory_.GetWeakPtr()));
return connection;
}
void ReadFromSocket(const net::StreamSocket& connection, int rv) override {}
size_t num_accepted_sockets() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
return num_accepted_sockets_;
}
private:
void AcceptedSocketOnUIThread() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
++num_accepted_sockets_;
}
scoped_refptr<base::SequencedTaskRunner> task_runner_;
size_t num_accepted_sockets_ = 0;
base::WeakPtrFactory<PreconnectListener> weak_ptr_factory_;
};
} // namespace
// Most tests use EmbeddedTestServer but this uses QuicSimpleTestServer because
// Early Hints are only plumbed over HTTP/2 or HTTP/3 (QUIC).
class NavigationEarlyHintsTest : public DevToolsProtocolTest {
public:
NavigationEarlyHintsTest() {
feature_list_.InitWithFeatures(
std::vector<base::test::FeatureRef>{
net::features::kSplitCacheByNetworkIsolationKey},
std::vector<base::test::FeatureRef>{
net::features::kMigrateSessionsOnNetworkChangeV2});
}
~NavigationEarlyHintsTest() override = default;
void SetUpOnMainThread() override {
DevToolsProtocolTest::SetUpOnMainThread();
ConfigureMockCertVerifier();
host_resolver()->AddRule("*", "127.0.0.1");
cross_origin_server_.RegisterRequestHandler(
base::BindRepeating(&NavigationEarlyHintsTest::HandleCrossOriginRequest,
base::Unretained(this)));
preconnect_listener_ = std::make_unique<PreconnectListener>();
cross_origin_server().SetConnectionListener(preconnect_listener_.get());
ASSERT_TRUE(cross_origin_server_.Start());
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(switches::kOriginToForceQuicOn, "*");
mock_cert_verifier_.SetUpCommandLine(command_line);
ASSERT_TRUE(net::QuicSimpleTestServer::Start());
DevToolsProtocolTest::SetUpCommandLine(command_line);
}
void TearDown() override {
base::ScopedAllowBaseSyncPrimitivesForTesting allow_wait;
net::QuicSimpleTestServer::Shutdown();
DevToolsProtocolTest::TearDown();
}
net::test_server::EmbeddedTestServer& cross_origin_server() {
return cross_origin_server_;
}
std::string WaitForHintedScriptDevtoolsRequestId() {
base::Value::Dict result;
while (true) {
result = WaitForNotification("Network.requestWillBeSent", true);
const base::Value* request_url = result.FindByDottedPath("request.url");
if (request_url->GetString() ==
net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath).spec()) {
return *result.FindString("requestId");
}
}
}
base::Value::Dict WaitForDevtoolsEarlyHints() {
base::Value::Dict result;
while (true) {
result = WaitForNotification("Network.responseReceivedEarlyHints", true);
return result;
}
}
base::Value::Dict WaitForResponseReceived(const std::string& request_id) {
base::Value::Dict result;
while (true) {
result = WaitForNotification("Network.responseReceived", true);
const std::string* received_id = result.FindString("requestId");
if (received_id && *received_id == request_id) {
return result;
}
}
}
protected:
base::test::ScopedFeatureList& feature_list() { return feature_list_; }
PreconnectListener& preconnect_listener() { return *preconnect_listener_; }
void SetUpInProcessBrowserTestFixture() override {
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
void ConfigureMockCertVerifier() {
auto test_cert =
net::ImportCertFromFile(net::GetTestCertsDirectory(), "quic-chain.pem");
net::CertVerifyResult verify_result;
verify_result.verified_cert = test_cert;
mock_cert_verifier_.mock_cert_verifier()->AddResultForCert(
test_cert, verify_result, net::OK);
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
}
HeaderField CreatePreloadLinkForScript() {
return HeaderField(
"link",
base::StringPrintf("<%s>; rel=preload; as=script", kHintedScriptPath));
}
HeaderField CreatePreloadLinkForCorsScript() {
return HeaderField(
"link", base::StringPrintf("<%s>; rel=preload; as=script; crossorigin",
kHintedScriptPath));
}
HeaderField CreateModulePreloadLink() {
return HeaderField("link", base::StringPrintf("<%s>; rel=modulepreload",
kHintedScriptPath));
}
HeaderField CreatePreloadLinkForStylesheet() {
return HeaderField("link", base::StringPrintf("<%s>; rel=preload; as=style",
kHintedStylesheetPath));
}
void RegisterResponse(const ResponseEntry& entry) {
net::QuicSimpleTestServer::AddResponseWithEarlyHints(
entry.path, entry.headers, entry.body, entry.early_hints);
}
void RegisterHintedScriptResource() {
ResponseEntry hinted_script_entry(kHintedScriptPath, net::HTTP_OK);
hinted_script_entry.headers["content-type"] = "application/javascript";
hinted_script_entry.headers["cache-control"] = "max-age=3600";
hinted_script_entry.body = kHintedScriptBody;
RegisterResponse(hinted_script_entry);
}
void RegisterHintedStylesheetResource() {
ResponseEntry hinted_script_entry(kHintedStylesheetPath, net::HTTP_OK);
hinted_script_entry.headers["content-type"] = "text/css";
hinted_script_entry.body = kHintedStylesheetBody;
RegisterResponse(hinted_script_entry);
}
void RegisterRedirectedPage() {
ResponseEntry entry(kRedirectedPagePath, net::HTTP_OK);
entry.body = kRedirectedPageBody;
RegisterResponse(entry);
}
ResponseEntry CreatePageEntryWithHintedScript(
net::HttpStatusCode status_code) {
RegisterHintedScriptResource();
ResponseEntry entry(kPageWithHintedScriptPath, status_code);
entry.body = kPageWithHintedScriptBody;
HeaderField link_header = CreatePreloadLinkForScript();
entry.AddEarlyHints({std::move(link_header)});
return entry;
}
ResponseEntry CreateEmptyPageEntryWithHintedScript() {
RegisterHintedScriptResource();
ResponseEntry entry(kEmptyPagePath, net::HTTP_OK);
entry.body = kEmptyPageBody;
HeaderField link_header = CreatePreloadLinkForScript();
entry.AddEarlyHints({std::move(link_header)});
return entry;
}
ResponseEntry CreatePageEntryWithHintedCorsScript(
net::HttpStatusCode status_code) {
RegisterHintedScriptResource();
ResponseEntry entry(kPageWithHintedCorsScriptPath, status_code);
entry.body = kPageWithHintedCorsScriptBody;
HeaderField link_header = CreatePreloadLinkForCorsScript();
entry.AddEarlyHints({std::move(link_header)});
return entry;
}
ResponseEntry CreatePageEntryWithHintedModuleScript(
net::HttpStatusCode status_code) {
RegisterHintedScriptResource();
ResponseEntry entry(kPageWithHintedModuleScriptPath, status_code);
entry.body = kPageWithHintedModuleScriptBody;
HeaderField link_header = CreateModulePreloadLink();
entry.AddEarlyHints({std::move(link_header)});
return entry;
}
bool NavigateToURLAndWaitTitle(const GURL& url, const std::string& title) {
return NavigateToURLAndWaitTitleWithCommitURL(url, url, title);
}
bool NavigateToURLAndWaitTitleWithCommitURL(const GURL& url,
const GURL& expected_commit_url,
const std::string& title) {
std::u16string title16 = base::ASCIIToUTF16(title);
TitleWatcher title_watcher(shell()->web_contents(), title16);
if (!NavigateToURL(shell(), url, expected_commit_url)) {
return false;
}
return title16 == title_watcher.WaitAndGetTitle();
}
NavigationEarlyHintsManager* GetEarlyHintsManager(RenderFrameHostImpl* rfh) {
return rfh->early_hints_manager();
}
PreloadedResources WaitForPreloadedResources() {
return WaitForPreloadedResources(static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame()));
}
PreloadedResources WaitForPreloadedResources(RenderFrameHostImpl* rfh) {
base::RunLoop loop;
PreloadedResources result;
if (!GetEarlyHintsManager(rfh)) {
return result;
}
GetEarlyHintsManager(rfh)->WaitForPreloadsFinishedForTesting(
base::BindLambdaForTesting([&](PreloadedResources preloaded_resources) {
result = preloaded_resources;
loop.Quit();
}));
loop.Run();
return result;
}
enum class FetchResult {
kFetched,
kBlocked,
};
FetchResult FetchScriptOnDocument(ToRenderFrameHost target, GURL src) {
EvalJsResult result = EvalJs(target, JsReplace(R"(
new Promise(resolve => {
const script = document.createElement("script");
script.src = $1;
script.onerror = () => resolve("blocked");
script.onload = () => resolve("fetched");
document.body.appendChild(script);
});
)",
src));
return result.ExtractString() == "fetched" ? FetchResult::kFetched
: FetchResult::kBlocked;
}
private:
std::unique_ptr<net::test_server::HttpResponse> HandleCrossOriginRequest(
const net::test_server::HttpRequest& request) {
GURL relative_url = request.base_url.Resolve(request.relative_url);
if (relative_url.path() == kEmptyPagePath) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("text/html");
response->set_content("");
return std::move(response);
}
if (relative_url.path() == kRedirectedPagePath) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("text/html");
response->set_content(kRedirectedPageBody);
return std::move(response);
}
if (relative_url.path() != kHintedScriptPath) {
return nullptr;
}
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->set_content_type("application/javascript");
response->set_content(kHintedScriptBody);
std::string query = relative_url.query();
if (query == "corp-cross-origin") {
response->AddCustomHeader("Cross-Origin-Resource-Policy", "cross-origin");
} else if (query == "corp-same-origin") {
response->AddCustomHeader("Cross-Origin-Resource-Policy", "same-origin");
}
return std::move(response);
}
base::test::ScopedFeatureList feature_list_;
ContentMockCertVerifier mock_cert_verifier_;
// For tests that fetch resources from a cross origin server.
net::EmbeddedTestServer cross_origin_server_;
std::unique_ptr<PreconnectListener> preconnect_listener_;
};
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, Basic) {
base::HistogramTester histograms;
ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_OK);
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURLAndWaitTitle(
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
"Done"));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_EQ(preloads.size(), 1UL);
GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
auto it = preloads.find(preloaded_url);
ASSERT_TRUE(it != preloads.end());
ASSERT_FALSE(it->second.was_canceled);
ASSERT_TRUE(it->second.error_code.has_value());
EXPECT_EQ(it->second.error_code.value(), net::OK);
histograms.ExpectTotalCount(
"Navigation.EarlyHints.WillStartRequestToEarlyHintsTime", 1);
histograms.ExpectTotalCount(
"Navigation.EarlyHints.EarlyHintsToResponseStartTime", 1);
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, CorsAttribute) {
ResponseEntry entry = CreatePageEntryWithHintedCorsScript(net::HTTP_OK);
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURLAndWaitTitle(
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedCorsScriptPath),
"Done"));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_EQ(preloads.size(), 1UL);
GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
auto it = preloads.find(preloaded_url);
ASSERT_TRUE(it != preloads.end());
ASSERT_FALSE(it->second.was_canceled);
ASSERT_TRUE(it->second.error_code.has_value());
EXPECT_EQ(it->second.error_code.value(), net::OK);
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, ModulePreload) {
ResponseEntry entry = CreatePageEntryWithHintedModuleScript(net::HTTP_OK);
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURLAndWaitTitle(
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedModuleScriptPath),
"Done"));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_EQ(preloads.size(), 1UL);
GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
auto it = preloads.find(preloaded_url);
ASSERT_TRUE(it != preloads.end());
ASSERT_FALSE(it->second.was_canceled);
ASSERT_TRUE(it->second.error_code.has_value());
EXPECT_EQ(it->second.error_code.value(), net::OK);
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, DisallowPreloadFromIframe) {
ResponseEntry page_entry(kPageWithIframePath, net::HTTP_OK);
page_entry.body = kPageWithIframeBody;
RegisterResponse(page_entry);
ResponseEntry iframe_entry = CreatePageEntryWithHintedScript(net::HTTP_OK);
RegisterResponse(iframe_entry);
EXPECT_TRUE(NavigateToURL(
shell(), net::QuicSimpleTestServer::GetFileURL(kPageWithIframePath)));
// Find RenderFrameHost for the iframe.
std::vector<RenderFrameHost*> all_frames =
CollectAllRenderFrameHosts(shell()->web_contents());
ASSERT_EQ(all_frames.size(), 2UL);
ASSERT_EQ(all_frames[0], all_frames[1]->GetParent());
RenderFrameHostImpl* iframe_host =
static_cast<RenderFrameHostImpl*>(all_frames[1]);
EXPECT_TRUE(WaitForLoadStop(WebContents::FromRenderFrameHost(iframe_host)));
ASSERT_EQ(iframe_host->GetLastCommittedURL(),
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath));
// NavigationEarlyHintsManager should not be created for subframes. If it were
// created it should have been created before navigation commit.
EXPECT_EQ(iframe_host->early_hints_manager(), nullptr);
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, NavigationServerError) {
ResponseEntry entry =
CreatePageEntryWithHintedScript(net::HTTP_INTERNAL_SERVER_ERROR);
entry.body = "Internal Server Error";
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
kPageWithHintedScriptPath)));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_EQ(preloads.size(), 1UL);
GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
auto it = preloads.find(preloaded_url);
ASSERT_NE(it, preloads.end());
ASSERT_FALSE(it->second.was_canceled);
ASSERT_TRUE(it->second.error_code.has_value());
EXPECT_EQ(it->second.error_code.value(), net::OK);
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, RedirectSameOrigin) {
RegisterRedirectedPage();
ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_FOUND);
entry.headers["location"] = kRedirectedPagePath;
entry.body = "";
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURLAndWaitTitleWithCommitURL(
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
net::QuicSimpleTestServer::GetFileURL(kRedirectedPagePath), "Done"));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_EQ(preloads.size(), 1UL);
GURL preloaded_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
auto it = preloads.find(preloaded_url);
ASSERT_TRUE(it != preloads.end());
ASSERT_FALSE(it->second.was_canceled);
ASSERT_TRUE(it->second.error_code.has_value());
EXPECT_EQ(it->second.error_code.value(), net::OK);
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, RedirectCrossOrigin) {
const GURL kRedirectedUrl = cross_origin_server().GetURL(kRedirectedPagePath);
ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_FOUND);
entry.headers["location"] = kRedirectedUrl.spec();
entry.body = "";
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURLAndWaitTitleWithCommitURL(
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
kRedirectedUrl, "Done"));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_TRUE(preloads.empty());
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, InvalidPreloadLink) {
const std::string kPath = "/hinted.html";
RegisterHintedScriptResource();
ResponseEntry entry(kPath, net::HTTP_OK);
entry.body = "body";
entry.AddEarlyHints(
{HeaderField("link", base::StringPrintf("<%s>; rel=preload; as=invalid",
kHintedScriptPath))});
RegisterResponse(entry);
EXPECT_TRUE(
NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(kPath)));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_TRUE(preloads.empty());
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, MultipleEarlyHints) {
RegisterHintedScriptResource();
RegisterHintedStylesheetResource();
ResponseEntry entry(kPageWithHintedScriptPath, net::HTTP_OK);
entry.body = kPageWithHintedScriptBody;
// Set two Early Hints responses which contain duplicate preload link headers.
// The second response should be ignored.
HeaderField script_link_header = CreatePreloadLinkForScript();
HeaderField stylesheet_link_header = CreatePreloadLinkForStylesheet();
entry.AddEarlyHints({script_link_header});
entry.AddEarlyHints({script_link_header, stylesheet_link_header});
RegisterResponse(entry);
EXPECT_TRUE(NavigateToURLAndWaitTitle(
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath),
"Done"));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_EQ(preloads.size(), 1UL);
GURL script_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
GURL stylesheet_url =
net::QuicSimpleTestServer::GetFileURL(kHintedStylesheetPath);
EXPECT_TRUE(preloads.contains(script_url));
EXPECT_FALSE(preloads.contains(stylesheet_url));
}
const char kPageWithCrossOriginScriptPage[] =
"/page_with_cross_origin_script.html";
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, CORP_Pass) {
// The server response's is a script with
// `Cross-Origin-Resource-Policy: cross-origin`.
const GURL kCrossOriginScriptUrl =
cross_origin_server().GetURL("/hinted.js?corp-cross-origin");
ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK);
HeaderField link_header = HeaderField(
"link", base::StringPrintf("<%s>; rel=preload; as=script",
kCrossOriginScriptUrl.spec().c_str()));
page_entry.AddEarlyHints({std::move(link_header)});
RegisterResponse(page_entry);
EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
kPageWithCrossOriginScriptPage)));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl),
FetchResult::kFetched);
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_EQ(preloads.size(), 1UL);
auto it = preloads.find(kCrossOriginScriptUrl);
ASSERT_TRUE(it != preloads.end());
ASSERT_FALSE(it->second.was_canceled);
ASSERT_TRUE(it->second.error_code.has_value());
EXPECT_EQ(it->second.error_code.value(), net::OK);
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, CORP_Blocked) {
// The server response's is a script with
// `Cross-Origin-Resource-Policy: same-origin`.
const GURL kCrossOriginScriptUrl =
cross_origin_server().GetURL("/hinted.js?corp-same-origin");
ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK);
HeaderField link_header = HeaderField(
"link", base::StringPrintf("<%s>; rel=preload; as=script",
kCrossOriginScriptUrl.spec().c_str()));
page_entry.AddEarlyHints({std::move(link_header)});
RegisterResponse(page_entry);
EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
kPageWithCrossOriginScriptPage)));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
// The script fetch should be blocked.
EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl),
FetchResult::kBlocked);
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, COEP_Pass) {
// The server sends `Cross-Origin-Resource-Policy: cross-origin`.
const GURL kCrossOriginScriptUrl =
cross_origin_server().GetURL("/hinted.js?corp-cross-origin");
ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK);
page_entry.headers["cross-origin-embedder-policy"] = "require-corp";
HeaderField link_header = HeaderField(
"link", base::StringPrintf("<%s>; rel=preload; as=script",
kCrossOriginScriptUrl.spec().c_str()));
HeaderField coep =
HeaderField("cross-origin-embedder-policy", "require-corp");
page_entry.AddEarlyHints({std::move(link_header), std::move(coep)});
RegisterResponse(page_entry);
EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
kPageWithCrossOriginScriptPage)));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl),
FetchResult::kFetched);
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, COEP_Block) {
// The server does not send `Cross-Origin-Resource-Policy` header.
const GURL kCrossOriginScriptUrl = cross_origin_server().GetURL("/hinted.js");
ResponseEntry page_entry(kPageWithCrossOriginScriptPage, net::HTTP_OK);
page_entry.headers["cross-origin-embedder-policy"] = "require-corp";
HeaderField link_header = HeaderField(
"link", base::StringPrintf("<%s>; rel=preload; as=script",
kCrossOriginScriptUrl.spec().c_str()));
HeaderField coep =
HeaderField("cross-origin-embedder-policy", "require-corp");
page_entry.AddEarlyHints({std::move(link_header), std::move(coep)});
RegisterResponse(page_entry);
EXPECT_TRUE(NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(
kPageWithCrossOriginScriptPage)));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(FetchScriptOnDocument(shell(), kCrossOriginScriptUrl),
FetchResult::kBlocked);
}
// Test that network isolation key is set correctly for Early Hints preload.
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, NetworkAnonymizationKey) {
const GURL kHintedScriptUrl =
net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
ResponseEntry entry = CreateEmptyPageEntryWithHintedScript();
RegisterResponse(entry);
std::optional<bool> is_cached;
URLLoaderInterceptor interceptor(
base::BindLambdaForTesting(
[&](URLLoaderInterceptor::RequestParams* params) { return false; }),
base::BindLambdaForTesting(
[&](const GURL& request_url,
const network::URLLoaderCompletionStatus& status) {
if (request_url != kHintedScriptUrl) {
return;
}
is_cached = status.exists_in_cache;
}),
base::NullCallback());
ASSERT_TRUE(NavigateToURL(
shell(), net::QuicSimpleTestServer::GetFileURL(kEmptyPagePath)));
// Make sure the hinted resource is preloaded.
PreloadedResources preloads = WaitForPreloadedResources();
auto it = preloads.find(kHintedScriptUrl);
ASSERT_TRUE(it != preloads.end());
ASSERT_FALSE(it->second.was_canceled);
ASSERT_EQ(it->second.error_code.value(), net::OK);
ASSERT_FALSE(is_cached.value());
is_cached = std::nullopt;
// Fetch the hinted resource from the main frame. It should come from the
// cache.
FetchScriptOnDocument(shell(), kHintedScriptUrl);
ASSERT_TRUE(is_cached.value());
// Reset `is_cached` to make sure it is set true or false.
is_cached = std::nullopt;
// Create an iframe with a different origin and fetch the hinted resource from
// the iframe. It should not come from the cache.
auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents());
RenderFrameHost* iframe =
CreateSubframe(web_contents, /*frame_id=*/"",
cross_origin_server().GetURL("/empty.html"),
/*wait_for_navigation=*/true);
FetchScriptOnDocument(iframe, kHintedScriptUrl);
ASSERT_FALSE(is_cached.value());
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, SimplePreconnect) {
const char kPageWithPreconnect[] = "/page_with_preconnect.html";
const GURL kPreconnectUrl = cross_origin_server().GetURL("/");
ResponseEntry page_entry(kPageWithPreconnect, net::HTTP_OK);
HeaderField link_header =
HeaderField("link", base::StringPrintf("<%s>; rel=preconnect",
kPreconnectUrl.spec().c_str()));
page_entry.AddEarlyHints({std::move(link_header)});
RegisterResponse(page_entry);
ASSERT_TRUE(NavigateToURL(
shell(), net::QuicSimpleTestServer::GetFileURL(kPageWithPreconnect)));
ASSERT_TRUE(WaitForLoadStop(shell()->web_contents()));
EXPECT_EQ(preconnect_listener().num_accepted_sockets(), 1UL);
EXPECT_TRUE(
GetEarlyHintsManager(static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame()))
->WasResourceHintsReceived());
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, InvalidHeader_NewLine) {
const std::string kPath = "/header-contains-newline.html";
ResponseEntry entry(kPath, net::HTTP_OK);
entry.AddEarlyHints({HeaderField("invalid-header", "foo\r\nbar")});
RegisterResponse(entry);
EXPECT_FALSE(
NavigateToURL(shell(), net::QuicSimpleTestServer::GetFileURL(kPath)));
}
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsTest, DevtoolsEventsForEarlyHint) {
ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_OK);
RegisterResponse(entry);
shell()->LoadURL(GURL("about:blank"));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
Attach();
// Send this synchronously, otherwise it might not be sent yet when the
// navigation start and the message sending gets suspended.
SendCommandSync("Network.enable");
GURL target_url =
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath);
EXPECT_TRUE(NavigateToURL(shell(), target_url, target_url));
std::string hinted_id = WaitForHintedScriptDevtoolsRequestId();
{
base::Value::Dict early_hints_event = WaitForDevtoolsEarlyHints();
base::Value::Dict* early_hints_headers =
early_hints_event.FindDict("headers");
ASSERT_TRUE(early_hints_headers);
HeaderField link_header = CreatePreloadLinkForScript();
EXPECT_EQ(*early_hints_headers->FindString(link_header.name),
link_header.value);
}
{
base::Value::Dict result = WaitForResponseReceived(hinted_id);
base::Value* from_early_hints_value =
result.FindByDottedPath("response.fromEarlyHints");
ASSERT_TRUE(from_early_hints_value);
EXPECT_TRUE(from_early_hints_value->is_bool());
EXPECT_TRUE(from_early_hints_value->GetBool());
}
}
class NavigationEarlyHintsAddressSpaceTest : public NavigationEarlyHintsTest {
public:
NavigationEarlyHintsAddressSpaceTest() = default;
~NavigationEarlyHintsAddressSpaceTest() override = default;
void SetUpCommandLine(base::CommandLine* command_line) override {
NavigationEarlyHintsTest::SetUpCommandLine(command_line);
private_server_.AddDefaultHandlers(GetTestDataFilePath());
ASSERT_TRUE(private_server_.Start());
// Treat the main test server as public for IPAddressSpace tests.
command_line->AppendSwitchASCII(
network::switches::kIpAddressSpaceOverrides,
base::StringPrintf("127.0.0.1:%d=public",
net::QuicSimpleTestServer::GetPort()));
}
net::test_server::EmbeddedTestServer& private_server() {
return private_server_;
}
private:
// For tests that trigger private network requests.
net::EmbeddedTestServer private_server_;
};
// Tests that Early Hints preload is blocked when hints comes from the public
// network but a hinted resource is located in a private network.
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsAddressSpaceTest,
PublicToPrivateRequestBlocked) {
const GURL kPrivateResourceUrl = private_server().GetURL("/blank.jpg");
ResponseEntry page_entry(kEmptyPagePath, net::HTTP_OK);
HeaderField link_header = HeaderField(
"link", base::StringPrintf("<%s>; rel=preload; as=image",
kPrivateResourceUrl.spec().c_str()));
page_entry.AddEarlyHints({std::move(link_header)});
RegisterResponse(page_entry);
EXPECT_TRUE(NavigateToURL(
shell(), net::QuicSimpleTestServer::GetFileURL(kEmptyPagePath)));
EXPECT_TRUE(WaitForLoadStop(shell()->web_contents()));
PreloadedResources preloads = WaitForPreloadedResources();
EXPECT_EQ(preloads.size(), 1UL);
auto it = preloads.find(kPrivateResourceUrl);
ASSERT_TRUE(it != preloads.end());
ASSERT_FALSE(it->second.was_canceled);
ASSERT_TRUE(it->second.error_code.has_value());
EXPECT_EQ(it->second.error_code.value(),
net::ERR_BLOCKED_BY_PRIVATE_NETWORK_ACCESS_CHECKS);
EXPECT_EQ(it->second.cors_error_status->cors_error,
network::mojom::CorsError::kInsecurePrivateNetwork);
}
class NavigationEarlyHintsPrerenderTest : public NavigationEarlyHintsTest {
public:
NavigationEarlyHintsPrerenderTest()
: prerender_helper_(base::BindRepeating(
&NavigationEarlyHintsPrerenderTest::web_contents,
base::Unretained(this))) {}
~NavigationEarlyHintsPrerenderTest() override = default;
test::PrerenderTestHelper* prerender_helper() { return &prerender_helper_; }
WebContents* web_contents() { return shell()->web_contents(); }
private:
test::PrerenderTestHelper prerender_helper_;
};
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsPrerenderTest,
AllowPreloadInPrerendering) {
EXPECT_TRUE(NavigateToURL(
shell(), net::QuicSimpleTestServer::GetFileURL("/title1.html")));
ResponseEntry entry = CreatePageEntryWithHintedScript(net::HTTP_OK);
RegisterResponse(entry);
// Loads a page in the prerender.
FrameTreeNodeId host_id = prerender_helper()->AddPrerender(
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath));
RenderFrameHostImpl* prerender_rfh = static_cast<RenderFrameHostImpl*>(
prerender_helper()->GetPrerenderedMainFrameHost(host_id));
EXPECT_NE(prerender_rfh, nullptr);
EXPECT_NE(prerender_rfh->early_hints_manager(), nullptr);
PreloadedResources preloads = WaitForPreloadedResources(prerender_rfh);
EXPECT_EQ(preloads.size(), 1UL);
GURL script_url = net::QuicSimpleTestServer::GetFileURL(kHintedScriptPath);
EXPECT_TRUE(preloads.contains(script_url));
}
class NavigationEarlyHintsFencedFrameTest : public NavigationEarlyHintsTest {
public:
NavigationEarlyHintsFencedFrameTest() = default;
test::FencedFrameTestHelper& fenced_frame_test_helper() {
return fenced_frame_test_helper_;
}
ResponseEntry CreatePageEntryWithHintedScriptInFencedFrame(
net::HttpStatusCode status_code) {
RegisterHintedScriptResource();
ResponseEntry entry(kPageWithHintedScriptPath, status_code);
entry.headers["supports-loading-mode"] = "fenced-frame";
entry.body = kPageWithHintedScriptBody;
HeaderField link_header = CreatePreloadLinkForScript();
HeaderField fenced_frame_header =
HeaderField("supports-loading-mode", "fenced-frame");
entry.AddEarlyHints(
{std::move(link_header), std::move(fenced_frame_header)});
return entry;
}
private:
test::FencedFrameTestHelper fenced_frame_test_helper_;
};
IN_PROC_BROWSER_TEST_F(NavigationEarlyHintsFencedFrameTest,
DisallowPreloadInFencedFrame) {
EXPECT_TRUE(NavigateToURL(
shell(), net::QuicSimpleTestServer::GetFileURL("/title1.html")));
ResponseEntry entry =
CreatePageEntryWithHintedScriptInFencedFrame(net::HTTP_OK);
RegisterResponse(entry);
// Create a fenced frame.
RenderFrameHostImpl* fenced_frame_host = static_cast<RenderFrameHostImpl*>(
fenced_frame_test_helper().CreateFencedFrame(
shell()->web_contents()->GetPrimaryMainFrame(),
net::QuicSimpleTestServer::GetFileURL(kPageWithHintedScriptPath)));
EXPECT_NE(fenced_frame_host, nullptr);
EXPECT_EQ(fenced_frame_host->early_hints_manager(), nullptr);
}
namespace {
const char kHttp1EarlyHintsPath[] = "/early-hints";
class Http1EarlyHintsResponse : public net::test_server::HttpResponse {
public:
Http1EarlyHintsResponse() = default;
~Http1EarlyHintsResponse() override = default;
void SendResponse(
base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) override {
base::StringPairs early_hints_headers = {
{"Link", "</cacheable.js>; rel=preload; as=script"}};
delegate->SendResponseHeaders(net::HTTP_EARLY_HINTS, "Early Hints",
early_hints_headers);
base::StringPairs final_response_headers = {
{"Content-Type", "text/html"},
{"Link", "</cacheable.js>; rel=preload; as=script"}};
delegate->SendResponseHeaders(net::HTTP_OK, "OK", final_response_headers);
delegate->SendContentsAndFinish("<script src=\"cacheable.js\"></script>");
}
};
std::unique_ptr<net::test_server::HttpResponse> HandleHttpEarlyHintsRequest(
const net::test_server::HttpRequest& request) {
const GURL relative_url = request.base_url.Resolve(request.relative_url);
if (relative_url.path() == kHttp1EarlyHintsPath) {
return std::make_unique<Http1EarlyHintsResponse>();
}
return nullptr;
}
} // namespace
class NavigationEarlyHintsHttp1Test : public ContentBrowserTest,
public testing::WithParamInterface<bool> {
public:
NavigationEarlyHintsHttp1Test() {
if (EnableEarlyHintsForHttp1()) {
scoped_feature_list_.InitAndEnableFeature(
net::features::kEnableEarlyHintsOnHttp11);
} else {
scoped_feature_list_.InitAndDisableFeature(
net::features::kEnableEarlyHintsOnHttp11);
}
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->AddDefaultHandlers();
embedded_test_server()->RegisterRequestHandler(
base::BindRepeating(&HandleHttpEarlyHintsRequest));
ASSERT_TRUE(embedded_test_server()->Start());
}
bool EnableEarlyHintsForHttp1() { return GetParam(); }
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(All, NavigationEarlyHintsHttp1Test, testing::Bool());
// Tests that Early Hints are allowed or disallowed on HTTP/1.1 based on a
// feature flag.
IN_PROC_BROWSER_TEST_P(NavigationEarlyHintsHttp1Test, AllowEarlyHints) {
const GURL url = embedded_test_server()->GetURL(kHttp1EarlyHintsPath);
ASSERT_TRUE(NavigateToURL(shell(), url));
NavigationEarlyHintsManager* early_hints_manager =
static_cast<RenderFrameHostImpl*>(
shell()->web_contents()->GetPrimaryMainFrame())
->early_hints_manager();
if (EnableEarlyHintsForHttp1()) {
ASSERT_TRUE(early_hints_manager->WasResourceHintsReceived());
} else {
ASSERT_TRUE(early_hints_manager == nullptr);
}
}
} // namespace content