blob: 6ba6e2d0755897734db5910fccf85c309d0c7084 [file] [log] [blame]
// Copyright 2024 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/synchronization/lock.h"
#include "base/thread_annotations.h"
#include "base/timer/elapsed_timer.h"
#include "content/browser/preloading/prefetch/prefetch_document_manager.h"
#include "content/browser/preloading/preloading.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.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/prefetch_test_util.h"
#include "content/public/test/preloading_test_util.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/mojom/loader/referrer.mojom.h"
#include "third_party/blink/public/mojom/speculation_rules/speculation_rules.mojom.h"
namespace content {
namespace {
using net::test_server::ControllableHttpResponse;
class NavPrefetchBrowserTest : public ContentBrowserTest {
public:
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
attempt_ukm_entry_builder_ =
std::make_unique<test::PreloadingAttemptUkmEntryBuilder>(
content_preloading_predictor::kSpeculationRules);
}
void StartPrefetch(const GURL& url) {
auto* prefetch_document_manager =
PrefetchDocumentManager::GetOrCreateForCurrentDocument(
shell()->web_contents()->GetPrimaryMainFrame());
auto candidate = blink::mojom::SpeculationCandidate::New();
candidate->url = url;
candidate->action = blink::mojom::SpeculationAction::kPrefetch;
candidate->eagerness = blink::mojom::SpeculationEagerness::kEager;
candidate->referrer = Referrer::SanitizeForRequest(
url, blink::mojom::Referrer(
shell()->web_contents()->GetURL(),
network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin));
std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
candidates.push_back(std::move(candidate));
prefetch_document_manager->ProcessCandidates(candidates);
}
RenderFrameHostImpl& GetPrimaryMainFrameHost() {
return static_cast<WebContentsImpl*>(shell()->web_contents())
->GetPrimaryPage()
.GetMainDocument();
}
void MonitorResourceRequest(const net::test_server::HttpRequest& request) {
// This should be called on `EmbeddedTestServer::io_thread_`.
EXPECT_FALSE(BrowserThread::CurrentlyOn(content::BrowserThread::UI));
base::AutoLock auto_lock(lock_);
request_count_by_path_[request.GetURL().PathForRequest()]++;
}
int GetRequestCount(const GURL& url) {
EXPECT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::UI));
base::AutoLock auto_lock(lock_);
return request_count_by_path_[url.PathForRequest()];
}
ukm::TestAutoSetUkmRecorder* test_ukm_recorder() {
return ukm_recorder_.get();
}
const test::PreloadingAttemptUkmEntryBuilder&
attempt_entry_builder() {
return *attempt_ukm_entry_builder_;
}
private:
base::ScopedMockElapsedTimersForTest test_timer_;
std::map<std::string, int> request_count_by_path_ GUARDED_BY(lock_);
std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
std::unique_ptr<test::PreloadingAttemptUkmEntryBuilder>
attempt_ukm_entry_builder_;
// Disable sampling for UKM preloading logs.
test::PreloadingConfigOverride preloading_config_override_;
base::Lock lock_;
};
IN_PROC_BROWSER_TEST_F(NavPrefetchBrowserTest,
DoesNotHangIfCancelledWhileWaitingForHead) {
ControllableHttpResponse response1(embedded_test_server(), "/next");
ControllableHttpResponse response2(embedded_test_server(), "/next");
ASSERT_TRUE(embedded_test_server()->Start());
GURL referrer_url = embedded_test_server()->GetURL("/empty.html");
GURL next_url = embedded_test_server()->GetURL("/next");
ASSERT_TRUE(NavigateToURL(shell(), referrer_url));
// Prefetch the next page and wait for that request to arrive.
StartPrefetch(next_url);
response1.WaitForRequest();
// Start a navigation which may block on head (since we haven't sent it).
RenderFrameHost* rfh = shell()->web_contents()->GetPrimaryMainFrame();
TestFrameNavigationObserver nav_observer(rfh);
ASSERT_TRUE(BeginNavigateToURLFromRenderer(rfh, next_url));
ASSERT_TRUE(nav_observer.navigation_started());
// Cancel the prefetch.
auto* prefetch_document_manager =
PrefetchDocumentManager::GetOrCreateForCurrentDocument(rfh);
std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
prefetch_document_manager->ProcessCandidates(candidates);
// Wait for a new request, and respond to it.
response2.WaitForRequest();
response2.Send(net::HTTP_OK);
response2.Done();
// The navigation should now succeed.
nav_observer.Wait();
EXPECT_EQ(nav_observer.last_committed_url(), next_url);
EXPECT_TRUE(nav_observer.last_navigation_succeeded());
}
// TODO(crbug.com/345352974): Make it a web platform test instead.
IN_PROC_BROWSER_TEST_F(NavPrefetchBrowserTest, ServedToRedirectionChain) {
net::test_server::EmbeddedTestServer ssl_server{
net::test_server::EmbeddedTestServer::TYPE_HTTPS};
ssl_server.RegisterRequestMonitor(base::BindRepeating(
&NavPrefetchBrowserTest::MonitorResourceRequest, base::Unretained(this)));
ssl_server.AddDefaultHandlers(GetTestDataFilePath());
ssl_server.SetSSLConfig(
net::test_server::EmbeddedTestServer::CERT_TEST_NAMES);
ASSERT_TRUE(ssl_server.Start());
GURL initiator_url = ssl_server.GetURL("a.test", "/empty.html");
GURL des_url = ssl_server.GetURL("a.test", "/title2.html");
GURL next_nav_url =
ssl_server.GetURL("b.test", "/server-redirect?" + des_url.spec());
ASSERT_TRUE(NavigateToURL(shell(), initiator_url));
test::TestPrefetchWatcher test_prefetch_watcher;
StartPrefetch(des_url);
test_prefetch_watcher.WaitUntilPrefetchResponseCompleted(
GetPrimaryMainFrameHost().GetDocumentToken(), des_url);
ASSERT_EQ(GetRequestCount(des_url), 1);
TestNavigationObserver nav_observer(shell()->web_contents());
nav_observer.set_wait_event(TestNavigationObserver::WaitEvent::kLoadStopped);
std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
JsReplace("location = $1", next_nav_url));
nav_observer.Wait();
EXPECT_TRUE(test_prefetch_watcher.PrefetchUsedInLastNavigation());
EXPECT_EQ(GetRequestCount(des_url), 1);
ukm::SourceId ukm_source_id =
shell()->web_contents()->GetPrimaryMainFrame()->GetPageUkmSourceId();
// Navigate primary page to flush the metrics.
ASSERT_TRUE(NavigateToURL(shell(), initiator_url));
test::ExpectPreloadingAttemptUkm(
*test_ukm_recorder(),
{attempt_entry_builder().BuildEntry(
ukm_source_id, PreloadingType::kPrefetch,
PreloadingEligibility::kEligible, PreloadingHoldbackStatus::kAllowed,
PreloadingTriggeringOutcome::kSuccess,
PreloadingFailureReason::kUnspecified,
/*accurate=*/true,
base::ScopedMockElapsedTimersForTest::kMockElapsedTime,
blink::mojom::SpeculationEagerness::kEager)});
}
// TODO(crbug.com/345352974): Make it a web platform test instead.
IN_PROC_BROWSER_TEST_F(NavPrefetchBrowserTest, SetCookieViaHTTPResponse) {
net::test_server::EmbeddedTestServer ssl_server{
net::test_server::EmbeddedTestServer::TYPE_HTTPS};
ssl_server.RegisterRequestMonitor(base::BindRepeating(
&NavPrefetchBrowserTest::MonitorResourceRequest, base::Unretained(this)));
ssl_server.AddDefaultHandlers(GetTestDataFilePath());
ssl_server.SetSSLConfig(
net::test_server::EmbeddedTestServer::CERT_TEST_NAMES);
ASSERT_TRUE(ssl_server.Start());
GURL initiator_url = ssl_server.GetURL("a.test", "/empty.html");
const std::string server_cookie = "host_cookie=1";
GURL des_url = ssl_server.GetURL("b.test", "/set-cookie?" + server_cookie);
ASSERT_TRUE(NavigateToURL(shell(), initiator_url));
// 1. Prefetch a resource which sets cookie.
test::TestPrefetchWatcher test_prefetch_watcher;
StartPrefetch(des_url);
test_prefetch_watcher.WaitUntilPrefetchResponseCompleted(
GetPrimaryMainFrameHost().GetDocumentToken(), des_url);
ASSERT_EQ(GetRequestCount(des_url), 1);
// 2. Activate the prefetched result.
TestNavigationObserver nav_observer(shell()->web_contents());
nav_observer.set_wait_event(TestNavigationObserver::WaitEvent::kLoadStopped);
std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
JsReplace("location = $1", des_url));
nav_observer.Wait();
// 3. The cookie was written into the real cookie storage.
EXPECT_TRUE(test_prefetch_watcher.PrefetchUsedInLastNavigation());
EXPECT_EQ(GetRequestCount(des_url), 1);
EXPECT_EQ(EvalJs(shell()->web_contents()->GetPrimaryMainFrame(),
"document.cookie"), server_cookie);
// 4. Navigate to another same-site page to confirm the cookie is persistent.
GURL after_prefetch_url = ssl_server.GetURL("b.test", "/title2.html");
std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
JsReplace("location = $1", after_prefetch_url));
EXPECT_EQ(EvalJs(shell()->web_contents()->GetPrimaryMainFrame(),
"document.cookie"), server_cookie);
}
// TODO(crbug.com/345352974): Make it a web platform test instead.
IN_PROC_BROWSER_TEST_F(NavPrefetchBrowserTest,
NeverSetCookieForDiscardedPrefetch) {
net::test_server::EmbeddedTestServer ssl_server{
net::test_server::EmbeddedTestServer::TYPE_HTTPS};
ssl_server.RegisterRequestMonitor(base::BindRepeating(
&NavPrefetchBrowserTest::MonitorResourceRequest, base::Unretained(this)));
ssl_server.AddDefaultHandlers(GetTestDataFilePath());
ssl_server.SetSSLConfig(
net::test_server::EmbeddedTestServer::CERT_TEST_NAMES);
ASSERT_TRUE(ssl_server.Start());
GURL initiator_url = ssl_server.GetURL("a.test", "/empty.html");
const std::string server_cookie = "host_cookie=1";
GURL des_url = ssl_server.GetURL("b.test", "/set-cookie?" + server_cookie);
ASSERT_TRUE(NavigateToURL(shell(), initiator_url));
// 1. Prefetch a resource which sets cookie.
test::TestPrefetchWatcher test_prefetch_watcher;
StartPrefetch(des_url);
test_prefetch_watcher.WaitUntilPrefetchResponseCompleted(
GetPrimaryMainFrameHost().GetDocumentToken(), des_url);
ASSERT_EQ(GetRequestCount(des_url), 1);
// 2. Navigate to another URL to invalidate the prefetch result.
TestNavigationObserver nav_observer(shell()->web_contents());
nav_observer.set_wait_event(TestNavigationObserver::WaitEvent::kLoadStopped);
GURL after_prefetch_url = ssl_server.GetURL("b.test", "/title2.html");
std::ignore = ExecJs(shell()->web_contents()->GetPrimaryMainFrame(),
JsReplace("location = $1", after_prefetch_url));
nav_observer.Wait();
// 3. Check the cookie set by discarded prefetch response cannot affect the
// real jar.
EXPECT_EQ(EvalJs(shell()->web_contents()->GetPrimaryMainFrame(),
"document.cookie").ExtractString(), "");
}
} // namespace
} // namespace content