blob: 431ea52f9e83a6ce76df9a1aa498fe77d082edd7 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/preloading/prefetch/prefetch_url_loader_helper.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "content/browser/preloading/prefetch/prefetch_features.h"
#include "content/browser/preloading/prefetch/prefetch_origin_prober.h"
#include "content/browser/preloading/prefetch/prefetch_params.h"
#include "content/browser/preloading/prefetch/prefetch_probe_result.h"
#include "content/browser/preloading/prefetch/prefetch_service.h"
#include "content/browser/preloading/prefetch/prefetch_serving_page_metrics_container.h"
#include "content/browser/preloading/prefetch/prefetch_status.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/public/browser/prefetch_metrics.h"
#include "content/public/browser/web_contents.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_partition_key_collection.h"
#include "services/network/public/cpp/resource_request.h"
#include "url/gurl.h"
#include "url/scheme_host_port.h"
namespace content {
namespace {
PrefetchServingPageMetricsContainer*
PrefetchServingPageMetricsContainerFromFrameTreeNodeId(
FrameTreeNodeId frame_tree_node_id) {
FrameTreeNode* frame_tree_node =
FrameTreeNode::GloballyFindByID(frame_tree_node_id);
if (!frame_tree_node || !frame_tree_node->navigation_request()) {
return nullptr;
}
return PrefetchServingPageMetricsContainer::GetForNavigationHandle(
*frame_tree_node->navigation_request());
}
void RecordCookieWaitTime(base::TimeDelta wait_time) {
UMA_HISTOGRAM_CUSTOM_TIMES(
"PrefetchProxy.AfterClick.Mainframe.CookieWaitTime", wait_time,
base::TimeDelta(), base::Seconds(5), 50);
}
// Stores state for the asynchronous work required to prepare a prefetch to
// serve.
struct OnGotPrefetchToServeState {
// Inputs.
const FrameTreeNodeId frame_tree_node_id;
const GURL tentative_url;
base::OnceCallback<void(PrefetchContainer::Reader)> callback;
PrefetchContainer::Reader reader;
// True if we've validated that cookies match (to the extent required).
// False if they don't. Absent if we don't know yet.
// Unused if `features::kPrefetchCookieIndices` is disabled.
std::optional<bool> cookies_matched;
// The probe result, once it has been determined.
// If it is empty, then this will be the next thing
// ContinueOnGotPrefetchToServe does.
std::optional<PrefetchProbeResult> probe_result;
// True if copying isolated cookies is either done or has been determined
// unnecessary.
bool cookie_copy_complete_if_required = false;
};
// Forward declarations are required for these to call each other while
// appearing in the order they occur.
void ContinueOnGotPrefetchToServe(
std::unique_ptr<OnGotPrefetchToServeState> state);
void StartCookieValidation(std::unique_ptr<OnGotPrefetchToServeState>& state);
void OnGotCookiesForValidation(
std::unique_ptr<OnGotPrefetchToServeState> state,
const std::vector<net::CookieWithAccessResult>& cookies,
const std::vector<net::CookieWithAccessResult>& excluded_cookies);
void StartProbe(std::unique_ptr<OnGotPrefetchToServeState>& state);
void OnProbeComplete(std::unique_ptr<OnGotPrefetchToServeState> state,
base::TimeTicks probe_start_time,
PrefetchProbeResult probe_result);
void EnsureCookiesCopied(std::unique_ptr<OnGotPrefetchToServeState>& state);
void OnCookieCopyComplete(std::unique_ptr<OnGotPrefetchToServeState> state,
base::TimeTicks cookie_copy_start_time);
// Overall structure of asynchronous execution (in coroutine style).
void ContinueOnGotPrefetchToServe(
std::unique_ptr<OnGotPrefetchToServeState> state) {
// If the cookies need to be matched, fetch them and confirm that they're
// correct.
if (base::FeatureList::IsEnabled(features::kPrefetchCookieIndices)) {
if (!state->cookies_matched.has_value()) {
StartCookieValidation(state);
if (!state) {
// Fetching the cookies asynchronously. Continue later.
return;
}
}
CHECK(state->cookies_matched.has_value());
if (!state->cookies_matched.value()) {
// Cookies did not match, but needed to. We're done here.
std::move(state->callback).Run({});
return;
}
}
// If probing hasn't happened yet, do it if necessary.
if (!state->probe_result.has_value()) {
StartProbe(state);
if (!state) {
// The probe is happening asynchronously (it took ownership of |state|),
// and this algorithm will continue later.
return;
}
if (!state->probe_result.has_value()) {
// Could not start a probe. We're done here.
std::move(state->callback).Run({});
return;
}
}
if (!PrefetchProbeResultIsSuccess(state->probe_result.value())) {
// Probe failed. We're done here.
std::move(state->callback).Run({});
return;
}
// Copy isolated cookies, if required.
if (!state->cookie_copy_complete_if_required) {
EnsureCookiesCopied(state);
if (!state) {
// Cookie copy is happening and this function will continue later.
return;
}
}
// All prerequisites should now be complete.
CHECK(!base::FeatureList::IsEnabled(features::kPrefetchCookieIndices) ||
state->cookies_matched.value_or(false));
CHECK(PrefetchProbeResultIsSuccess(state->probe_result.value()));
CHECK(state->cookie_copy_complete_if_required);
if (!state->reader) {
std::move(state->callback).Run({});
return;
}
switch (state->reader.GetServableState(PrefetchCacheableDuration())) {
case PrefetchContainer::ServableState::kNotServable:
case PrefetchContainer::ServableState::kShouldBlockUntilEligibilityGot:
case PrefetchContainer::ServableState::kShouldBlockUntilHeadReceived:
std::move(state->callback).Run({});
return;
case PrefetchContainer::ServableState::kServable:
break;
}
// Delay updating the prefetch with the probe result in case it becomes not
// servable.
state->reader.OnPrefetchProbeResult(state->probe_result.value());
PrefetchServingPageMetricsContainer* serving_page_metrics_container =
PrefetchServingPageMetricsContainerFromFrameTreeNodeId(
state->frame_tree_node_id);
if (serving_page_metrics_container) {
serving_page_metrics_container->SetPrefetchStatus(
state->reader.GetPrefetchStatus());
}
std::move(state->callback).Run(std::move(state->reader));
}
// COOKIE VALIDATION
void StartCookieValidation(std::unique_ptr<OnGotPrefetchToServeState>& state) {
WebContents* web_contents =
WebContents::FromFrameTreeNodeId(state->frame_tree_node_id);
if (!web_contents || !state->reader) {
// We can't confirm that the cookies matched. But probably everything is
// being torn down, anyway.
state->cookies_matched = false;
return;
}
if (!state->reader.VariesOnCookieIndices()) {
state->cookies_matched = true;
return;
}
network::mojom::CookieManager* cookie_manager =
web_contents->GetBrowserContext()
->GetDefaultStoragePartition()
->GetCookieManagerForBrowserProcess();
// Note: This currently relies on this being for main frame use only.
// The partitioning below needs to be adjusted if a subframe use were
// possible.
CHECK(FrameTreeNode::GloballyFindByID(state->frame_tree_node_id)
->IsMainFrame());
const GURL& url = state->reader.GetCurrentURLToServe();
net::SchemefulSite site(url);
cookie_manager->GetCookieList(
url, net::CookieOptions::MakeAllInclusive(),
net::CookiePartitionKeyCollection(
net::CookiePartitionKey::FromNetworkIsolationKey(
net::NetworkIsolationKey(site, site), net::SiteForCookies(site),
site, /*main_frame_navigation=*/true)),
base::BindOnce(&OnGotCookiesForValidation, std::move(state)));
}
void OnGotCookiesForValidation(
std::unique_ptr<OnGotPrefetchToServeState> state,
const std::vector<net::CookieWithAccessResult>& cookies,
const std::vector<net::CookieWithAccessResult>& excluded_cookies) {
std::vector<std::pair<std::string, std::string>> cookie_values;
cookie_values.reserve(cookies.size());
for (const net::CookieWithAccessResult& cookie : cookies) {
cookie_values.emplace_back(cookie.cookie.Name(), cookie.cookie.Value());
}
state->cookies_matched =
state->reader && state->reader.MatchesCookieIndices(cookie_values);
ContinueOnGotPrefetchToServe(std::move(state));
}
// ORIGIN PROBING
void StartProbe(std::unique_ptr<OnGotPrefetchToServeState>& state) {
// TODO(crbug.com/40274818): Should we check for existence of an
// `origin_prober` earlier instead of waiting until we have a matching
// prefetch?
PrefetchService* prefetch_service =
PrefetchService::GetFromFrameTreeNodeId(state->frame_tree_node_id);
if (!prefetch_service || !prefetch_service->GetPrefetchOriginProber()) {
return;
}
PrefetchOriginProber* prober = prefetch_service->GetPrefetchOriginProber();
if (!state->reader.IsIsolatedNetworkContextRequiredToServe() ||
!prober->ShouldProbeOrigins()) {
state->probe_result = PrefetchProbeResult::kNoProbing;
return;
}
GURL probe_url = url::SchemeHostPort(state->tentative_url).GetURL();
prober->Probe(probe_url,
base::BindOnce(&OnProbeComplete, std::move(state),
/*probe_start_time=*/base::TimeTicks::Now()));
}
// Called when the `PrefetchOriginProber` check is done (if performed).
// `probe_start_time` is used to calculate probe latency which is
// reported to the tab helper.
void OnProbeComplete(std::unique_ptr<OnGotPrefetchToServeState> state,
base::TimeTicks probe_start_time,
PrefetchProbeResult probe_result) {
state->probe_result = probe_result;
PrefetchServingPageMetricsContainer* serving_page_metrics_container =
PrefetchServingPageMetricsContainerFromFrameTreeNodeId(
state->frame_tree_node_id);
if (serving_page_metrics_container) {
serving_page_metrics_container->SetProbeLatency(base::TimeTicks::Now() -
probe_start_time);
}
if (!PrefetchProbeResultIsSuccess(probe_result) && state->reader) {
state->reader.OnPrefetchProbeResult(probe_result);
if (serving_page_metrics_container) {
serving_page_metrics_container->SetPrefetchStatus(
state->reader.GetPrefetchStatus());
}
}
ContinueOnGotPrefetchToServe(std::move(state));
}
// ISOLATED COOKIE COPYING
// Ensures that the cookies for prefetch are copied from its isolated
// network context to the default network context.
void EnsureCookiesCopied(std::unique_ptr<OnGotPrefetchToServeState>& state) {
PrefetchContainer::Reader& reader = state->reader;
// Start the cookie copy for the next redirect hop of |state->reader|.
if (reader && !reader.HasIsolatedCookieCopyStarted()) {
PrefetchService* prefetch_service =
PrefetchService::GetFromFrameTreeNodeId(state->frame_tree_node_id);
if (prefetch_service) {
prefetch_service->CopyIsolatedCookies(reader);
}
}
if (reader) {
reader.OnInterceptorCheckCookieCopy();
}
if (!reader || !reader.IsIsolatedCookieCopyInProgress()) {
RecordCookieWaitTime(base::TimeDelta());
state->cookie_copy_complete_if_required = true;
return;
}
reader.SetOnCookieCopyCompleteCallback(
base::BindOnce(&OnCookieCopyComplete, std::move(state),
/*cookie_copy_start_time=*/base::TimeTicks::Now()));
}
void OnCookieCopyComplete(std::unique_ptr<OnGotPrefetchToServeState> state,
base::TimeTicks cookie_copy_start_time) {
base::TimeDelta wait_time = base::TimeTicks::Now() - cookie_copy_start_time;
CHECK_GE(wait_time, base::TimeDelta());
RecordCookieWaitTime(wait_time);
state->cookie_copy_complete_if_required = true;
ContinueOnGotPrefetchToServe(std::move(state));
}
} // namespace
void OnGotPrefetchToServe(
FrameTreeNodeId frame_tree_node_id,
const GURL& tentative_resource_request_url,
base::OnceCallback<void(PrefetchContainer::Reader)> get_prefetch_callback,
PrefetchContainer::Reader reader) {
// TODO(crbug.com/40274818): With multiple prefetches matching, we should
// move some of the checks here in `PrefetchService::ReturnPrefetchToServe`.
// Why ? Because we might be able to serve a different prefetch if the
// prefetch in the `reader` cannot be served.
// The `tentative_resource_request_url` might be different from
// `GetCurrentURLToServe()` because of No-Vary-Search non-exact url match.
#if DCHECK_IS_ON()
if (reader) {
GURL::Replacements replacements;
replacements.ClearRef();
replacements.ClearQuery();
DCHECK_EQ(tentative_resource_request_url.ReplaceComponents(replacements),
reader.GetCurrentURLToServe().ReplaceComponents(replacements));
}
#endif
if (!reader) {
std::move(get_prefetch_callback).Run({});
return;
}
switch (reader.GetServableState(PrefetchCacheableDuration())) {
case PrefetchContainer::ServableState::kNotServable:
case PrefetchContainer::ServableState::kShouldBlockUntilEligibilityGot:
case PrefetchContainer::ServableState::kShouldBlockUntilHeadReceived:
std::move(get_prefetch_callback).Run({});
return;
case PrefetchContainer::ServableState::kServable:
break;
}
// We should not reach here if the cookies have changed. This should already
// have been checked in one of the call sites:
// 1) PrefetchService::ReturnPrefetchToServe (in which case |reader| should be
// empty)
// 2) PrefetchURLLoaderInterceptor::MaybeCreateLoader (before serving the next
// next redirect hop)
CHECK(!reader.HaveDefaultContextCookiesChanged());
// Asynchronous activity begins here.
// We allocate an explicit "coroutine state" for this and manage it manually.
// While slightly verbose, this avoids duplication of logic later on in
// control flow. This function will asynchronously call itself until it's
// done.
ContinueOnGotPrefetchToServe(base::WrapUnique(new OnGotPrefetchToServeState{
.frame_tree_node_id = frame_tree_node_id,
.tentative_url = tentative_resource_request_url,
.callback = std::move(get_prefetch_callback),
.reader = std::move(reader)}));
}
} // namespace content