blob: ae4f499da9f31b3a13588406a156426934c0c5da [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_observer.h"
#include "content/public/common/content_features.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/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"}}},
{features::kPrefetchRedirects, {}},
// 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::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,
/*devtools_observer=*/nullptr);
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));
}
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