blob: c48c7d3158f8c7d83b9510e6e8e3d22f6f5344cb [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 <optional>
#include "base/strings/string_number_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "content/browser/preloading/prefetch/mock_prefetch_service_delegate.h"
#include "content/browser/preloading/prefetch/prefetch_document_manager.h"
#include "content/browser/preloading/prefetch/prefetch_features.h"
#include "content/browser/preloading/prefetch/prefetch_service.h"
#include "content/browser/renderer_host/browsing_context_group_swap.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/public/browser/prefetch_service_delegate.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/referrer.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/shell/browser/shell.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_response.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
namespace {
class BrowsingContextGroupSwapObserver : public WebContentsObserver {
public:
explicit BrowsingContextGroupSwapObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
void DidFinishNavigation(NavigationHandle* navigation_handle) override {
latest_swap_ = NavigationRequest::From(navigation_handle)
->browsing_context_group_swap();
}
const BrowsingContextGroupSwap& Get() const { return latest_swap_.value(); }
private:
std::optional<BrowsingContextGroupSwap> latest_swap_;
};
class ContaminationDelayBrowserTest : public ContentBrowserTest {
protected:
ContaminationDelayBrowserTest() {
scoped_feature_list_.InitWithFeaturesAndParameters(
{{features::kPrefetchStateContaminationMitigation,
{{"swaps_bcg", "true"}}},
// This is needed specifically for CrOS MSAN, where we apply a 10x
// multiplier to all test timeouts, which happens to be enough to push
// the response delay in this test (which is scaled in that way to
// match the slowdown of everything else) over the default prefetch
// timeout. To be resilient also to changes in that value, it is
// expressly overridden here to be a timeout that is much longer and
// scales with the timeout multiplier.
{features::kPrefetchUseContentRefactor,
{{"prefetch_timeout_ms",
base::NumberToString(
TestTimeouts::action_max_timeout().InMilliseconds())}}}},
{});
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
embedded_test_server()->ServeFilesFromSourceDirectory(
GetTestDataFilePath());
embedded_test_server()->RegisterRequestHandler(
base::BindRepeating(&ContaminationDelayBrowserTest::MaybeServeRequest,
base::Unretained(this)));
EXPECT_TRUE(embedded_test_server()->Start());
}
base::TimeDelta response_delay() const { return response_delay_; }
void set_response_delay(base::TimeDelta delay) { response_delay_ = delay; }
void Prefetch(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::kImmediate;
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);
ASSERT_TRUE(base::test::RunUntil([&] {
return prefetch_document_manager->GetReferringPageMetrics()
.prefetch_successful_count >= 1;
})) << "timed out waiting for prefetch to complete ("
<< prefetch_document_manager->GetReferringPageMetrics()
.prefetch_attempted_count
<< " attempted)";
}
private:
std::unique_ptr<net::test_server::HttpResponse> MaybeServeRequest(
const net::test_server::HttpRequest& request) {
GURL url = request.GetURL();
if (url.path_piece() == "/delayed") {
return std::make_unique<net::test_server::DelayedHttpResponse>(
response_delay());
}
if (url.path_piece() == "/redirect-cross-site") {
auto response = std::make_unique<net::test_server::DelayedHttpResponse>(
response_delay());
response->set_code(net::HTTP_TEMPORARY_REDIRECT);
response->AddCustomHeader("Location",
embedded_test_server()
->GetURL("prefetch.localhost", "/delayed")
.spec());
return response;
}
return nullptr;
}
base::test::ScopedFeatureList scoped_feature_list_;
base::TimeDelta response_delay_ = TestTimeouts::tiny_timeout() * 12;
};
IN_PROC_BROWSER_TEST_F(ContaminationDelayBrowserTest, CrossSite) {
set_response_delay(TestTimeouts::tiny_timeout() * 4);
GURL referrer_url =
embedded_test_server()->GetURL("referrer.localhost", "/title1.html");
GURL prefetch_url =
embedded_test_server()->GetURL("prefetch.localhost", "/delayed");
ASSERT_TRUE(NavigateToURL(shell(), referrer_url));
Prefetch(prefetch_url);
scoped_refptr<SiteInstance> site_instance =
shell()->web_contents()->GetSiteInstance();
base::HistogramTester histogram_tester;
BrowsingContextGroupSwapObserver swap_observer(shell()->web_contents());
base::ElapsedTimer timer;
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), prefetch_url));
EXPECT_GE(timer.Elapsed(), response_delay());
EXPECT_EQ(BrowsingContextGroupSwapType::kSecuritySwap,
swap_observer.Get().type());
EXPECT_FALSE(site_instance->IsRelatedSiteInstance(
shell()->web_contents()->GetSiteInstance()));
histogram_tester.ExpectUniqueSample(
"Preloading.PrefetchBCGSwap.RelatedActiveContents", 1, 1);
}
IN_PROC_BROWSER_TEST_F(ContaminationDelayBrowserTest, IgnoresSameOrigin) {
GURL referrer_url =
embedded_test_server()->GetURL("referrer.localhost", "/title1.html");
GURL prefetch_url =
embedded_test_server()->GetURL("referrer.localhost", "/delayed");
ASSERT_TRUE(NavigateToURL(shell(), referrer_url));
Prefetch(prefetch_url);
BrowsingContextGroupSwapObserver swap_observer(shell()->web_contents());
base::ElapsedTimer timer;
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), prefetch_url));
EXPECT_LT(timer.Elapsed(), response_delay());
EXPECT_THAT(swap_observer.Get().type(),
::testing::AnyOf(BrowsingContextGroupSwapType::kNoSwap,
BrowsingContextGroupSwapType::kProactiveSwap));
}
IN_PROC_BROWSER_TEST_F(ContaminationDelayBrowserTest, IgnoresSameSite) {
GURL referrer_url =
embedded_test_server()->GetURL("referrer.localhost", "/title1.html");
GURL prefetch_url =
embedded_test_server()->GetURL("sub.referrer.localhost", "/delayed");
ASSERT_TRUE(NavigateToURL(shell(), referrer_url));
Prefetch(prefetch_url);
BrowsingContextGroupSwapObserver swap_observer(shell()->web_contents());
base::ElapsedTimer timer;
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), prefetch_url));
EXPECT_LT(timer.Elapsed(), response_delay());
EXPECT_THAT(swap_observer.Get().type(),
::testing::AnyOf(BrowsingContextGroupSwapType::kNoSwap,
BrowsingContextGroupSwapType::kProactiveSwap));
}
IN_PROC_BROWSER_TEST_F(ContaminationDelayBrowserTest, IgnoresIfExempt) {
GURL referrer_url =
embedded_test_server()->GetURL("referrer.localhost", "/title1.html");
GURL prefetch_url =
embedded_test_server()->GetURL("prefetch.localhost", "/delayed");
auto* prefetch_service = PrefetchService::GetFromFrameTreeNodeId(
shell()->web_contents()->GetPrimaryMainFrame()->GetFrameTreeNodeId());
auto owned_delegate = std::make_unique<MockPrefetchServiceDelegate>();
EXPECT_CALL(*owned_delegate, IsContaminationExempt(referrer_url))
.WillRepeatedly(testing::Return(true));
prefetch_service->SetPrefetchServiceDelegateForTesting(
std::move(owned_delegate));
ASSERT_TRUE(NavigateToURL(shell(), referrer_url));
Prefetch(prefetch_url);
BrowsingContextGroupSwapObserver swap_observer(shell()->web_contents());
base::ElapsedTimer timer;
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), prefetch_url));
EXPECT_LT(timer.Elapsed(), response_delay());
EXPECT_THAT(swap_observer.Get().type(),
::testing::AnyOf(BrowsingContextGroupSwapType::kNoSwap,
BrowsingContextGroupSwapType::kProactiveSwap));
}
#ifndef NDEBUG
// TODO(http://crbug.com/404944178): This test is flaky on debug builds.
// https://ci.chromium.org/ui/test/chromium/ninja%3A%2F%2Fcontent%2Ftest%3Acontent_browsertests%2FContaminationDelayBrowserTest.IgnoresIfExempt_BrowserInitiated
#define MAYBE_IgnoresIfExempt_BrowserInitiated \
DISABLED_IgnoresIfExempt_BrowserInitiated
#else
#define MAYBE_IgnoresIfExempt_BrowserInitiated \
IgnoresIfExempt_BrowserInitiated
#endif
IN_PROC_BROWSER_TEST_F(ContaminationDelayBrowserTest,
MAYBE_IgnoresIfExempt_BrowserInitiated) {
url::Origin referring_origin = url::Origin::Create(
embedded_test_server()->GetURL("referrer.localhost", "/title1.html"));
GURL prefetch_url =
embedded_test_server()->GetURL("prefetch.localhost", "/delayed");
auto* prefetch_service = PrefetchService::GetFromFrameTreeNodeId(
shell()->web_contents()->GetPrimaryMainFrame()->GetFrameTreeNodeId());
// TODO(crbug.com/40946257): Currently `OnPrefetchLikely` will never be called
// for browser-initiated triggers, so we set `num_on_prefetch_likely_calls` to
// 0 here, and instead use `TestPrefetchWatcher` to confirm whether prefetch
// is actually triggered.
auto owned_delegate = std::make_unique<MockPrefetchServiceDelegate>(
/*num_on_prefetch_likely_calls=*/0);
EXPECT_CALL(*owned_delegate, IsContaminationExemptPerOrigin(referring_origin))
.WillRepeatedly(testing::Return(true));
prefetch_service->SetPrefetchServiceDelegateForTesting(
std::move(owned_delegate));
auto test_prefetch_watcher = std::make_unique<test::TestPrefetchWatcher>();
auto handle = shell()->web_contents()->StartPrefetch(
prefetch_url, /*use_prefetch_proxy=*/false,
test::kPreloadingEmbedderHistgramSuffixForTesting,
blink::mojom::Referrer(), referring_origin,
/*no_vary_search_hint=*/std::nullopt,
/*priority=*/std::nullopt,
PreloadPipelineInfo::Create(
/*planned_max_preloading_type=*/PreloadingType::kPrefetch),
/*attempt=*/nullptr, /*holdback_status_override=*/std::nullopt,
/*ttl=*/std::nullopt);
test_prefetch_watcher->WaitUntilPrefetchResponseCompleted(std::nullopt,
prefetch_url);
BrowsingContextGroupSwapObserver swap_observer(shell()->web_contents());
base::ElapsedTimer timer;
ASSERT_TRUE(NavigateToURL(shell(), prefetch_url));
EXPECT_TRUE(test_prefetch_watcher->PrefetchUsedInLastNavigation());
EXPECT_LT(timer.Elapsed(), response_delay());
EXPECT_THAT(swap_observer.Get().type(),
::testing::AnyOf(BrowsingContextGroupSwapType::kNoSwap,
BrowsingContextGroupSwapType::kProactiveSwap));
test_prefetch_watcher.reset();
handle.reset();
}
IN_PROC_BROWSER_TEST_F(ContaminationDelayBrowserTest, DelayAfterRedirect) {
set_response_delay(TestTimeouts::tiny_timeout() * 8);
GURL referrer_url =
embedded_test_server()->GetURL("referrer.localhost", "/title1.html");
GURL prefetch_url = embedded_test_server()->GetURL("referrer.localhost",
"/redirect-cross-site");
GURL commit_url =
embedded_test_server()->GetURL("prefetch.localhost", "/delayed");
ASSERT_TRUE(NavigateToURL(shell(), referrer_url));
Prefetch(prefetch_url);
scoped_refptr<SiteInstance> site_instance =
shell()->web_contents()->GetSiteInstance();
BrowsingContextGroupSwapObserver swap_observer(shell()->web_contents());
base::ElapsedTimer timer;
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), prefetch_url, commit_url));
EXPECT_LT(timer.Elapsed(), response_delay() * 2);
EXPECT_GE(timer.Elapsed(), response_delay());
EXPECT_EQ(BrowsingContextGroupSwapType::kSecuritySwap,
swap_observer.Get().type());
EXPECT_FALSE(site_instance->IsRelatedSiteInstance(
shell()->web_contents()->GetSiteInstance()));
}
IN_PROC_BROWSER_TEST_F(ContaminationDelayBrowserTest,
CrossSiteWithRelatedContents) {
set_response_delay(TestTimeouts::tiny_timeout() * 4);
GURL referrer_url =
embedded_test_server()->GetURL("referrer.localhost", "/title1.html");
GURL prefetch_url =
embedded_test_server()->GetURL("prefetch.localhost", "/delayed");
ASSERT_TRUE(NavigateToURL(shell(), referrer_url));
Prefetch(prefetch_url);
Shell::CreateNewWindow(
shell()->web_contents()->GetBrowserContext(),
embedded_test_server()->GetURL("referrer.localhost", "/title2.html"),
shell()->web_contents()->GetSiteInstance(), {800, 600});
scoped_refptr<SiteInstance> site_instance =
shell()->web_contents()->GetSiteInstance();
base::HistogramTester histogram_tester;
BrowsingContextGroupSwapObserver swap_observer(shell()->web_contents());
base::ElapsedTimer timer;
ASSERT_TRUE(NavigateToURLFromRenderer(shell(), prefetch_url));
EXPECT_GE(timer.Elapsed(), response_delay());
EXPECT_EQ(BrowsingContextGroupSwapType::kSecuritySwap,
swap_observer.Get().type());
EXPECT_FALSE(site_instance->IsRelatedSiteInstance(
shell()->web_contents()->GetSiteInstance()));
histogram_tester.ExpectUniqueSample(
"Preloading.PrefetchBCGSwap.RelatedActiveContents", 2, 1);
}
} // namespace
} // namespace content