blob: 9307d211f19a198fac88957bf9574e43920d26ba [file] [log] [blame]
// Copyright 2022 The Chromium Authors. All rights reserved.
// 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_service.h"
#include <memory>
#include <utility>
#include "base/barrier_closure.h"
#include "base/feature_list.h"
#include "base/location.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/timer/timer.h"
#include "content/browser/preloading/prefetch/prefetch_document_manager.h"
#include "content/browser/preloading/prefetch/prefetch_features.h"
#include "content/browser/preloading/prefetch/prefetch_network_context.h"
#include "content/browser/preloading/prefetch/prefetch_params.h"
#include "content/browser/preloading/prefetch/prefetch_proxy_configurator.h"
#include "content/browser/preloading/prefetch/prefetched_mainframe_response_container.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/prefetch_service_delegate.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/visibility.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_constants.h"
#include "net/base/isolation_info.h"
#include "net/base/load_flags.h"
#include "net/base/url_util.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_partition_key_collection.h"
#include "net/cookies/site_for_cookies.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "services/network/public/mojom/cookie_partition_key.mojom.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_constants.h"
namespace content {
namespace {
static ServiceWorkerContext* g_service_worker_context_for_testing = nullptr;
bool (*g_host_non_unique_filter)(base::StringPiece) = nullptr;
static network::mojom::URLLoaderFactory* g_url_loader_factory_for_testing =
nullptr;
bool ShouldConsiderDecoyRequestForStatus(PrefetchStatus status) {
switch (status) {
case PrefetchStatus::kPrefetchNotEligibleUserHasCookies:
case PrefetchStatus::kPrefetchNotEligibleUserHasServiceWorker:
// If the prefetch is not eligible because of cookie or a service worker,
// then maybe send a decoy.
return true;
case PrefetchStatus::kPrefetchNotEligibleGoogleDomain:
case PrefetchStatus::kPrefetchNotEligibleSchemeIsNotHttps:
case PrefetchStatus::kPrefetchNotEligibleNonDefaultStoragePartition:
case PrefetchStatus::kPrefetchPositionIneligible:
case PrefetchStatus::kPrefetchIneligibleRetryAfter:
case PrefetchStatus::kPrefetchProxyNotAvailable:
case PrefetchStatus::kPrefetchNotEligibleHostIsNonUnique:
case PrefetchStatus::kPrefetchNotEligibleDataSaverEnabled:
// These statuses don't relate to any user state, so don't send a decoy
// request.
return false;
case PrefetchStatus::kPrefetchUsedNoProbe:
case PrefetchStatus::kPrefetchUsedProbeSuccess:
case PrefetchStatus::kPrefetchNotUsedProbeFailed:
case PrefetchStatus::kPrefetchNotStarted:
case PrefetchStatus::kPrefetchNotFinishedInTime:
case PrefetchStatus::kPrefetchFailedNetError:
case PrefetchStatus::kPrefetchFailedNon2XX:
case PrefetchStatus::kPrefetchFailedMIMENotSupported:
case PrefetchStatus::kPrefetchSuccessful:
case PrefetchStatus::kNavigatedToLinkNotOnSRP:
case PrefetchStatus::kSubresourceThrottled:
case PrefetchStatus::kPrefetchUsedNoProbeWithNSP:
case PrefetchStatus::kPrefetchUsedProbeSuccessWithNSP:
case PrefetchStatus::kPrefetchNotUsedProbeFailedWithNSP:
case PrefetchStatus::kPrefetchUsedNoProbeNSPAttemptDenied:
case PrefetchStatus::kPrefetchUsedProbeSuccessNSPAttemptDenied:
case PrefetchStatus::kPrefetchNotUsedProbeFailedNSPAttemptDenied:
case PrefetchStatus::kPrefetchUsedNoProbeNSPNotStarted:
case PrefetchStatus::kPrefetchUsedProbeSuccessNSPNotStarted:
case PrefetchStatus::kPrefetchNotUsedProbeFailedNSPNotStarted:
case PrefetchStatus::kPrefetchIsPrivacyDecoy:
case PrefetchStatus::kPrefetchIsStale:
case PrefetchStatus::kPrefetchIsStaleWithNSP:
case PrefetchStatus::kPrefetchIsStaleNSPAttemptDenied:
case PrefetchStatus::kPrefetchIsStaleNSPNotStarted:
case PrefetchStatus::kPrefetchNotUsedCookiesChanged:
case PrefetchStatus::kPrefetchFailedRedirectsDisabled:
// These statuses should not be returned by the eligibility checks, and
// thus not be passed in here.
NOTREACHED();
return false;
}
}
bool ShouldStartSpareRenderer() {
for (RenderProcessHost::iterator iter(RenderProcessHost::AllHostsIterator());
!iter.IsAtEnd(); iter.Advance()) {
if (iter.GetCurrentValue()->IsUnused()) {
// There is already a spare renderer.
return false;
}
}
return true;
}
absl::optional<base::TimeDelta> GetTotalPrefetchTime(
network::mojom::URLResponseHead* head) {
DCHECK(head);
base::Time start = head->request_time;
base::Time end = head->response_time;
if (start.is_null() || end.is_null())
return absl::nullopt;
return end - start;
}
absl::optional<base::TimeDelta> GetPrefetchConnectTime(
network::mojom::URLResponseHead* head) {
DCHECK(head);
base::TimeTicks start = head->load_timing.connect_timing.connect_start;
base::TimeTicks end = head->load_timing.connect_timing.connect_end;
if (start.is_null() || end.is_null())
return absl::nullopt;
return end - start;
}
void RecordPrefetchProxyPrefetchMainframeCookiesToCopy(
size_t cookie_list_size) {
UMA_HISTOGRAM_COUNTS_100("PrefetchProxy.Prefetch.Mainframe.CookiesToCopy",
cookie_list_size);
}
void CookieSetHelper(base::RepeatingClosure closure,
net::CookieAccessResult access_result) {
closure.Run();
}
} // namespace
// static
std::unique_ptr<PrefetchService> PrefetchService::CreateIfPossible(
BrowserContext* browser_context) {
if (!base::FeatureList::IsEnabled(features::kPrefetchUseContentRefactor))
return nullptr;
return std::make_unique<PrefetchService>(browser_context);
}
PrefetchService::PrefetchService(BrowserContext* browser_context)
: browser_context_(browser_context),
delegate_(GetContentClient()->browser()->CreatePrefetchServiceDelegate(
browser_context_)),
prefetch_proxy_configurator_(
PrefetchProxyConfigurator::MaybeCreatePrefetchProxyConfigurator(
PrefetchProxyHost(delegate_
? delegate_->GetDefaultPrefetchProxyHost()
: GURL("")),
delegate_ ? delegate_->GetAPIKey() : "")) {}
PrefetchService::~PrefetchService() = default;
void PrefetchService::PrefetchUrl(
base::WeakPtr<PrefetchContainer> prefetch_container) {
DCHECK(prefetch_container);
auto prefetch_container_key = prefetch_container->GetPrefetchContainerKey();
// If the user has disabled pre* actions, then don't prefetch.
if (delegate_ && !delegate_->IsSomePreloadingEnabled()) {
return;
}
if (delegate_) {
bool allow_all_domains = PrefetchAllowAllDomains() ||
(PrefetchAllowAllDomainsForExtendedPreloading() &&
delegate_->IsExtendedPreloadingEnabled());
if (!allow_all_domains &&
!delegate_->IsDomainInPrefetchAllowList(
RenderFrameHost::FromID(
prefetch_container->GetReferringRenderFrameHostId())
->GetLastCommittedURL())) {
return;
}
}
RecordExistingPrefetchWithMatchingURL(prefetch_container);
DCHECK(all_prefetches_.find(prefetch_container_key) == all_prefetches_.end());
all_prefetches_[prefetch_container_key] = prefetch_container;
CheckEligibilityOfPrefetch(
prefetch_container,
base::BindOnce(&PrefetchService::OnGotEligibilityResult,
weak_method_factory_.GetWeakPtr()));
}
void PrefetchService::CheckEligibilityOfPrefetch(
base::WeakPtr<PrefetchContainer> prefetch_container,
OnEligibilityResultCallback result_callback) const {
DCHECK(prefetch_container);
// TODO(https://crbug.com/1299059): Clean up the following checks by: 1)
// moving each check to a separate function, and 2) requiring that failed
// checks provide a PrefetchStatus related to the check.
if (browser_context_->IsOffTheRecord()) {
std::move(result_callback).Run(prefetch_container, false, absl::nullopt);
return;
}
if (GetContentClient()->browser()->IsDataSaverEnabled(browser_context_)) {
std::move(result_callback)
.Run(prefetch_container, false,
PrefetchStatus::kPrefetchNotEligibleDataSaverEnabled);
return;
}
// While a registry-controlled domain could still resolve to a non-publicly
// routable IP, this allows hosts which are very unlikely to work via the
// proxy to be discarded immediately.
bool is_host_non_unique =
g_host_non_unique_filter
? g_host_non_unique_filter(
prefetch_container->GetURL().HostNoBrackets())
: net::IsHostnameNonUnique(
prefetch_container->GetURL().HostNoBrackets());
if (prefetch_container->GetPrefetchType().IsProxyRequired() &&
is_host_non_unique) {
std::move(result_callback)
.Run(prefetch_container, false,
PrefetchStatus::kPrefetchNotEligibleHostIsNonUnique);
return;
}
// Only HTTP(S) URLs which are believed to be secure are eligible.
// For proxied prefetches, we only want HTTPS URLs.
// For non-proxied prefetches, other URLs (notably localhost HTTP) is also
// acceptable. This is common during development.
const bool is_secure_http =
prefetch_container->GetPrefetchType().IsProxyRequired()
? prefetch_container->GetURL().SchemeIs(url::kHttpsScheme)
: (prefetch_container->GetURL().SchemeIsHTTPOrHTTPS() &&
network::IsUrlPotentiallyTrustworthy(
prefetch_container->GetURL()));
if (!is_secure_http) {
std::move(result_callback)
.Run(prefetch_container, false,
PrefetchStatus::kPrefetchNotEligibleSchemeIsNotHttps);
return;
}
if (prefetch_container->GetPrefetchType().IsProxyRequired() &&
(!prefetch_proxy_configurator_ ||
!prefetch_proxy_configurator_->IsPrefetchProxyAvailable())) {
std::move(result_callback)
.Run(prefetch_container, false,
PrefetchStatus::kPrefetchProxyNotAvailable);
return;
}
// Only the default storage partition is supported since that is where we
// check for service workers and existing cookies.
StoragePartition* default_storage_partition =
browser_context_->GetDefaultStoragePartition();
if (default_storage_partition !=
browser_context_->GetStoragePartitionForUrl(prefetch_container->GetURL(),
/*can_create=*/false)) {
std::move(result_callback)
.Run(prefetch_container, false,
PrefetchStatus::kPrefetchNotEligibleNonDefaultStoragePartition);
return;
}
// If we have recently received a "retry-after" for the origin, then don't
// send new prefetches.
if (delegate_ && !delegate_->IsOriginOutsideRetryAfterWindow(
prefetch_container->GetURL())) {
std::move(result_callback)
.Run(prefetch_container, false,
PrefetchStatus::kPrefetchIneligibleRetryAfter);
return;
}
// This service worker check assumes that the prefetch will only ever be
// performed in a first-party context (main frame prefetch). At the moment
// that is true but if it ever changes then the StorageKey will need to be
// constructed with the top-level site to ensure correct partitioning.
ServiceWorkerContext* service_worker_context =
g_service_worker_context_for_testing
? g_service_worker_context_for_testing
: browser_context_->GetDefaultStoragePartition()
->GetServiceWorkerContext();
bool site_has_service_worker =
service_worker_context->MaybeHasRegistrationForStorageKey(
blink::StorageKey(url::Origin::Create(prefetch_container->GetURL())));
if (site_has_service_worker) {
std::move(result_callback)
.Run(prefetch_container, false,
PrefetchStatus::kPrefetchNotEligibleUserHasServiceWorker);
return;
}
// We do not need to check the cookies of prefetches that do not need an
// isolated network context.
if (!prefetch_container->GetPrefetchType()
.IsIsolatedNetworkContextRequired()) {
std::move(result_callback).Run(prefetch_container, true, absl::nullopt);
return;
}
net::CookieOptions options = net::CookieOptions::MakeAllInclusive();
options.set_return_excluded_cookies();
default_storage_partition->GetCookieManagerForBrowserProcess()->GetCookieList(
prefetch_container->GetURL(), options,
net::CookiePartitionKeyCollection::Todo(),
base::BindOnce(&PrefetchService::OnGotCookiesForEligibilityCheck,
weak_method_factory_.GetWeakPtr(), prefetch_container,
std::move(result_callback)));
}
void PrefetchService::OnGotCookiesForEligibilityCheck(
base::WeakPtr<PrefetchContainer> prefetch_container,
OnEligibilityResultCallback result_callback,
const net::CookieAccessResultList& cookie_list,
const net::CookieAccessResultList& excluded_cookies) const {
if (!prefetch_container) {
std::move(result_callback).Run(prefetch_container, false, absl::nullopt);
return;
}
if (!cookie_list.empty()) {
std::move(result_callback)
.Run(prefetch_container, false,
PrefetchStatus::kPrefetchNotEligibleUserHasCookies);
return;
}
// Cookies are tricky because cookies for different paths or a higher level
// domain (e.g.: m.foo.com and foo.com) may not show up in |cookie_list|, but
// they will show up in |excluded_cookies|. To check for any cookies for a
// domain, compare the domains of the prefetched |url| and the domains of all
// the returned cookies.
bool excluded_cookie_has_tld = false;
for (const auto& cookie_result : excluded_cookies) {
if (cookie_result.cookie.IsExpired(base::Time::Now())) {
// Expired cookies don't count.
continue;
}
if (prefetch_container->GetURL().DomainIs(
cookie_result.cookie.DomainWithoutDot())) {
excluded_cookie_has_tld = true;
break;
}
}
if (excluded_cookie_has_tld) {
std::move(result_callback)
.Run(prefetch_container, false,
PrefetchStatus::kPrefetchNotEligibleUserHasCookies);
return;
}
std::move(result_callback).Run(prefetch_container, true, absl::nullopt);
}
void PrefetchService::OnGotEligibilityResult(
base::WeakPtr<PrefetchContainer> prefetch_container,
bool eligible,
absl::optional<PrefetchStatus> status) {
if (!eligible || !prefetch_container) {
if (status && prefetch_container) {
prefetch_container->SetPrefetchStatus(status.value());
if (prefetch_container->GetPrefetchType().IsProxyRequired() &&
ShouldConsiderDecoyRequestForStatus(
prefetch_container->GetPrefetchStatus()) &&
PrefetchServiceSendDecoyRequestForIneligblePrefetch(
delegate_ ? delegate_->DisableDecoysBasedOnUserSettings()
: false)) {
prefetch_container->SetIsDecoy(true);
prefetch_queue_.push_back(prefetch_container);
prefetch_container->SetPrefetchStatus(
PrefetchStatus::kPrefetchIsPrivacyDecoy);
Prefetch();
}
}
return;
}
prefetch_container->SetPrefetchStatus(PrefetchStatus::kPrefetchNotStarted);
prefetch_queue_.push_back(prefetch_container);
Prefetch();
// Registers a cookie listener for this prefetch if it is using an isolated
// network context. If the cookies in the default partition associated with
// this URL change after this point, then the prefetched resources should not
// be served.
if (prefetch_container->GetPrefetchType()
.IsIsolatedNetworkContextRequired()) {
prefetch_container->RegisterCookieListener(
browser_context_->GetDefaultStoragePartition()
->GetCookieManagerForBrowserProcess());
}
}
void PrefetchService::Prefetch() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (PrefetchCloseIdleSockets()) {
for (const auto& iter : all_prefetches_) {
if (iter.second && iter.second->GetNetworkContext()) {
iter.second->GetNetworkContext()->CloseIdleConnections();
}
}
}
base::WeakPtr<PrefetchContainer> next_prefetch = nullptr;
while ((next_prefetch = PopNextPrefetchContainer()) != nullptr) {
StartSinglePrefetch(next_prefetch);
}
}
base::WeakPtr<PrefetchContainer> PrefetchService::PopNextPrefetchContainer() {
// Remove all prefetches from queue that no longer exist.
auto new_end = std::remove_if(
prefetch_queue_.begin(), prefetch_queue_.end(),
[](const base::WeakPtr<PrefetchContainer>& prefetch_container) {
return !prefetch_container;
});
prefetch_queue_.erase(new_end, prefetch_queue_.end());
// TODO(https://crbug.com/1299059): Remove prefetches from queue once the
// number of prefetches started by its referring render frame host exceeds
// some maximum limit.
// Don't start any new prefetches if we are currently at or beyond the limit
// for the number of concurrent prefetches.
DCHECK(num_active_prefetches_ >= 0);
DCHECK(PrefetchServiceMaximumNumberOfConcurrentPrefetches() >= 0);
if (num_active_prefetches_ >=
PrefetchServiceMaximumNumberOfConcurrentPrefetches()) {
return nullptr;
}
// Get the first prefetch that is from an active render frame host and in a
// visible WebContents.
auto prefetch_iter = std::find_if(
prefetch_queue_.begin(), prefetch_queue_.end(),
[](const base::WeakPtr<PrefetchContainer>& prefetch_container) {
RenderFrameHost* rfh = RenderFrameHost::FromID(
prefetch_container->GetReferringRenderFrameHostId());
return rfh->IsActive() && rfh->GetPage().IsPrimary() &&
WebContents::FromRenderFrameHost(rfh)->GetVisibility() ==
Visibility::VISIBLE;
});
if (prefetch_iter == prefetch_queue_.end()) {
return nullptr;
}
base::WeakPtr<PrefetchContainer> next_prefetch_container = *prefetch_iter;
prefetch_queue_.erase(prefetch_iter);
return next_prefetch_container;
}
void PrefetchService::TakeOwnershipOfPrefetch(
base::WeakPtr<PrefetchContainer> prefetch_container) {
DCHECK(prefetch_container);
// Take ownership of the |PrefetchContainer| from the
// |PrefetchDocumentManager|.
PrefetchDocumentManager* prefetch_document_manager =
prefetch_container->GetPrefetchDocumentManager();
DCHECK(prefetch_document_manager);
std::unique_ptr<PrefetchContainer> owned_prefetch_container =
prefetch_document_manager->ReleasePrefetchContainer(
prefetch_container->GetURL());
DCHECK(owned_prefetch_container.get() == prefetch_container.get());
// Create callback to delete the prefetch container after
// |PrefetchContainerLifetimeInPrefetchService|.
base::TimeDelta reset_delta = PrefetchContainerLifetimeInPrefetchService();
std::unique_ptr<base::OneShotTimer> reset_callback = nullptr;
if (reset_delta.is_positive()) {
reset_callback = std::make_unique<base::OneShotTimer>();
reset_callback->Start(
FROM_HERE, PrefetchContainerLifetimeInPrefetchService(),
base::BindOnce(&PrefetchService::ResetPrefetch, base::Unretained(this),
prefetch_container));
}
// Store prefetch and callback to delete prefetch.
owned_prefetches_[prefetch_container->GetPrefetchContainerKey()] =
std::make_pair(std::move(owned_prefetch_container),
std::move(reset_callback));
}
void PrefetchService::ResetPrefetch(
base::WeakPtr<PrefetchContainer> prefetch_container) {
DCHECK(prefetch_container);
DCHECK(
owned_prefetches_.find(prefetch_container->GetPrefetchContainerKey()) !=
owned_prefetches_.end());
owned_prefetches_.erase(
owned_prefetches_.find(prefetch_container->GetPrefetchContainerKey()));
auto prefetches_ready_to_serve_iter =
prefetches_ready_to_serve_.find(prefetch_container->GetURL());
if (prefetches_ready_to_serve_iter != prefetches_ready_to_serve_.end() &&
prefetches_ready_to_serve_iter->second->GetPrefetchContainerKey() ==
prefetch_container->GetPrefetchContainerKey()) {
prefetches_ready_to_serve_.erase(prefetches_ready_to_serve_iter);
}
}
void PrefetchService::StartSinglePrefetch(
base::WeakPtr<PrefetchContainer> prefetch_container) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(prefetch_container);
TakeOwnershipOfPrefetch(prefetch_container);
if (!prefetch_container->IsDecoy()) {
// The status is updated to be successful or failed when it finishes.
prefetch_container->SetPrefetchStatus(
PrefetchStatus::kPrefetchNotFinishedInTime);
}
url::Origin origin = url::Origin::Create(prefetch_container->GetURL());
net::IsolationInfo isolation_info = net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, origin, origin,
net::SiteForCookies::FromOrigin(origin));
network::ResourceRequest::TrustedParams trusted_params;
trusted_params.isolation_info = isolation_info;
std::unique_ptr<network::ResourceRequest> request =
std::make_unique<network::ResourceRequest>();
request->url = prefetch_container->GetURL();
request->method = "GET";
request->enable_load_timing = true;
// TODO(https://crbug.com/1317756): Investigate if we need to include the
// net::LOAD_DISABLE_CACHE flag.
request->load_flags = net::LOAD_DISABLE_CACHE | net::LOAD_PREFETCH;
request->credentials_mode = network::mojom::CredentialsMode::kInclude;
request->headers.SetHeader(kCorsExemptPurposeHeaderName, "prefetch");
request->headers.SetHeader(
"Sec-Purpose", prefetch_container->GetPrefetchType().IsProxyRequired()
? "prefetch;anonymous-client-ip"
: "prefetch");
// Remove the user agent header if it was set so that the network context's
// default is used.
request->headers.RemoveHeader("User-Agent");
request->trusted_params = trusted_params;
request->site_for_cookies = trusted_params.isolation_info.site_for_cookies();
const auto& devtools_observer = prefetch_container->GetDevToolsObserver();
if (devtools_observer && !prefetch_container->IsDecoy()) {
devtools_observer->OnStartSinglePrefetch(prefetch_container->RequestId(),
*request);
}
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("speculation_rules_prefetch",
R"(
semantics {
sender: "Speculation Rules Prefetch Loader"
description:
"Prefetches the mainframe HTML of a page specified via "
"speculation rules. This is done out-of-band of normal "
"prefetches to allow total isolation of this request from the "
"rest of browser traffic and user state like cookies and cache."
trigger:
"Used only when this feature and speculation rules feature are "
"enabled."
data: "None."
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting:
"Users can control this via a setting specific to each content "
"embedder."
policy_exception_justification: "Not implemented."
})");
std::unique_ptr<network::SimpleURLLoader> loader =
network::SimpleURLLoader::Create(std::move(request), traffic_annotation);
loader->SetOnRedirectCallback(
base::BindRepeating(&PrefetchService::OnPrefetchRedirect,
base::Unretained(this), prefetch_container));
loader->SetAllowHttpErrorResults(true);
loader->SetTimeoutDuration(PrefetchTimeoutDuration());
loader->SetURLLoaderFactoryOptions(
network::mojom::kURLLoadOptionSendSSLInfoWithResponse |
network::mojom::kURLLoadOptionSniffMimeType |
network::mojom::kURLLoadOptionSendSSLInfoForCertificateError);
loader->DownloadToString(GetURLLoaderFactory(prefetch_container),
base::BindOnce(&PrefetchService::OnPrefetchComplete,
base::Unretained(this),
prefetch_container, isolation_info),
PrefetchMainframeBodyLengthLimit());
prefetch_container->TakeURLLoader(std::move(loader));
num_active_prefetches_++;
// TODO(https://crbug.com/1299059): Run canary checks if needed.
// Start a spare renderer now so that it will be ready by the time it is
// useful to have.
if (ShouldStartSpareRenderer()) {
RenderProcessHost::WarmupSpareRenderProcessHost(browser_context_);
}
}
network::mojom::URLLoaderFactory* PrefetchService::GetURLLoaderFactory(
base::WeakPtr<PrefetchContainer> prefetch_container) {
DCHECK(prefetch_container);
if (g_url_loader_factory_for_testing) {
return g_url_loader_factory_for_testing;
}
return prefetch_container->GetOrCreateNetworkContext(this)
->GetURLLoaderFactory();
}
void PrefetchService::OnPrefetchRedirect(
base::WeakPtr<PrefetchContainer> prefetch_container,
const net::RedirectInfo& redirect_info,
const network::mojom::URLResponseHead& response_head,
std::vector<std::string>* removed_headers) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
num_active_prefetches_--;
if (!prefetch_container)
return;
// Currently all redirects are disabled. See https://crbug.com/1266876 for
// more details.
prefetch_container->SetPrefetchStatus(
PrefetchStatus::kPrefetchFailedRedirectsDisabled);
// Cancels current request.
prefetch_container->ResetURLLoader();
// Send DevTools event
const auto& devtools_observer = prefetch_container->GetDevToolsObserver();
if (devtools_observer) {
devtools_observer->OnPrefetchResponseReceived(
prefetch_container->GetURL(), prefetch_container->RequestId(),
response_head);
devtools_observer->OnPrefetchRequestComplete(
prefetch_container->RequestId(),
network::URLLoaderCompletionStatus{net::ERR_NOT_IMPLEMENTED});
}
// Continue prefetching other URLs.
Prefetch();
}
void PrefetchService::OnPrefetchComplete(
base::WeakPtr<PrefetchContainer> prefetch_container,
const net::IsolationInfo& isolation_info,
std::unique_ptr<std::string> body) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
num_active_prefetches_--;
if (!prefetch_container)
return;
// TODO(https://crbug.com/1299059): Store relevant metrics based on the status
// of the completed prefetch.
if (prefetch_container->IsDecoy()) {
// Since this prefetch was a decoy, we don't cache the response.
prefetch_container->ResetURLLoader();
Prefetch();
return;
}
base::UmaHistogramSparse(
"PrefetchProxy.Prefetch.Mainframe.NetError",
std::abs(prefetch_container->GetLoader()->NetError()));
const auto& devtools_observer = prefetch_container->GetDevToolsObserver();
if (devtools_observer) {
if (prefetch_container->GetLoader()->ResponseInfo()) {
devtools_observer->OnPrefetchResponseReceived(
prefetch_container->GetURL(), prefetch_container->RequestId(),
*prefetch_container->GetLoader()->ResponseInfo());
}
devtools_observer->OnPrefetchRequestComplete(
prefetch_container->RequestId(),
prefetch_container->GetLoader()->CompletionStatus().value_or(
network::URLLoaderCompletionStatus(
prefetch_container->GetLoader()->NetError())));
}
if (prefetch_container->GetLoader()->NetError() != net::OK) {
prefetch_container->SetPrefetchStatus(
PrefetchStatus::kPrefetchFailedNetError);
}
if (prefetch_container->GetLoader()->NetError() == net::OK && body &&
prefetch_container->GetLoader()->ResponseInfo()) {
network::mojom::URLResponseHeadPtr head =
prefetch_container->GetLoader()->ResponseInfo()->Clone();
// Verifies that the request was made using the prefetch proxy if required,
// or made directly if the proxy was not required.
DCHECK(!head->proxy_server.is_direct() ==
prefetch_container->GetPrefetchType().IsProxyRequired());
HandlePrefetchedResponse(prefetch_container, isolation_info,
std::move(head), std::move(body));
}
prefetch_container->ResetURLLoader();
Prefetch();
}
void PrefetchService::HandlePrefetchedResponse(
base::WeakPtr<PrefetchContainer> prefetch_container,
const net::IsolationInfo& isolation_info,
network::mojom::URLResponseHeadPtr head,
std::unique_ptr<std::string> body) {
DCHECK(prefetch_container);
DCHECK(!head->was_fetched_via_cache);
if (!head->headers)
return;
UMA_HISTOGRAM_COUNTS_10M("PrefetchProxy.Prefetch.Mainframe.BodyLength",
body->size());
absl::optional<base::TimeDelta> total_time = GetTotalPrefetchTime(head.get());
if (total_time) {
UMA_HISTOGRAM_CUSTOM_TIMES("PrefetchProxy.Prefetch.Mainframe.TotalTime",
*total_time, base::Milliseconds(10),
base::Seconds(30), 100);
}
absl::optional<base::TimeDelta> connect_time =
GetPrefetchConnectTime(head.get());
if (connect_time) {
UMA_HISTOGRAM_TIMES("PrefetchProxy.Prefetch.Mainframe.ConnectTime",
*connect_time);
}
int response_code = head->headers->response_code();
base::UmaHistogramSparse("PrefetchProxy.Prefetch.Mainframe.RespCode",
response_code);
if (response_code < 200 | response_code >= 300) {
prefetch_container->SetPrefetchStatus(
PrefetchStatus::kPrefetchFailedNon2XX);
if (response_code == net::HTTP_SERVICE_UNAVAILABLE) {
base::TimeDelta retry_after;
std::string retry_after_string;
if (head->headers->EnumerateHeader(nullptr, "Retry-After",
&retry_after_string) &&
net::HttpUtil::ParseRetryAfterHeader(
retry_after_string, base::Time::Now(), &retry_after) &&
delegate_) {
delegate_->ReportOriginRetryAfter(prefetch_container->GetURL(),
retry_after);
}
}
return;
}
if (PrefetchServiceHTMLOnly() && head->mime_type != "text/html") {
prefetch_container->SetPrefetchStatus(
PrefetchStatus::kPrefetchFailedMIMENotSupported);
return;
}
prefetch_container->TakePrefetchedResponse(
std::make_unique<PrefetchedMainframeResponseContainer>(
isolation_info, std::move(head), std::move(body)));
prefetch_container->SetPrefetchStatus(PrefetchStatus::kPrefetchSuccessful);
}
void PrefetchService::PrepareToServe(
base::WeakPtr<PrefetchContainer> prefetch_container) {
// Ensure |this| has this prefetch.
if (all_prefetches_.find(prefetch_container->GetPrefetchContainerKey()) ==
all_prefetches_.end())
return;
// If the prefetch isn't ready to be served, then stop.
if (prefetch_container->HaveDefaultContextCookiesChanged() ||
!prefetch_container->HasValidPrefetchedResponse(
PrefetchCacheableDuration()))
return;
// If the prefetch has a valid response, then it must be in
// |owned_prefetches_|.
DCHECK(
owned_prefetches_.find(prefetch_container->GetPrefetchContainerKey()) !=
owned_prefetches_.end());
// If there is already a prefetch with the same URL as |prefetch_container| in
// |prefetches_ready_to_serve_|, then don't do anything.
if (prefetches_ready_to_serve_.find(prefetch_container->GetURL()) !=
prefetches_ready_to_serve_.end())
return;
// Move prefetch into |prefetches_ready_to_serve_|.
prefetches_ready_to_serve_[prefetch_container->GetURL()] = prefetch_container;
// Start the process of copying cookies from the isolated network context used
// to make the prefetch to the default network context.
CopyIsolatedCookies(prefetch_container);
}
void PrefetchService::CopyIsolatedCookies(
base::WeakPtr<PrefetchContainer> prefetch_container) {
DCHECK(prefetch_container);
if (!prefetch_container->GetNetworkContext()) {
// Not set in unit tests.
return;
}
// We only need to copy cookies if the prefetch used an isolated network
// context.
if (!prefetch_container->GetPrefetchType()
.IsIsolatedNetworkContextRequired()) {
return;
}
prefetch_container->OnIsolatedCookieCopyStart();
net::CookieOptions options = net::CookieOptions::MakeAllInclusive();
prefetch_container->GetNetworkContext()->GetCookieManager()->GetCookieList(
prefetch_container->GetURL(), options,
net::CookiePartitionKeyCollection::Todo(),
base::BindOnce(&PrefetchService::OnGotIsolatedCookiesForCopy,
weak_method_factory_.GetWeakPtr(), prefetch_container));
}
void PrefetchService::OnGotIsolatedCookiesForCopy(
base::WeakPtr<PrefetchContainer> prefetch_container,
const net::CookieAccessResultList& cookie_list,
const net::CookieAccessResultList& excluded_cookies) {
RecordPrefetchProxyPrefetchMainframeCookiesToCopy(cookie_list.size());
if (cookie_list.empty()) {
prefetch_container->OnIsolatedCookieCopyComplete();
return;
}
base::RepeatingClosure barrier = base::BarrierClosure(
cookie_list.size(),
base::BindOnce(&PrefetchContainer::OnIsolatedCookieCopyComplete,
prefetch_container));
net::CookieOptions options = net::CookieOptions::MakeAllInclusive();
for (const net::CookieWithAccessResult& cookie : cookie_list) {
browser_context_->GetDefaultStoragePartition()
->GetCookieManagerForBrowserProcess()
->SetCanonicalCookie(cookie.cookie, prefetch_container->GetURL(),
options,
base::BindOnce(&CookieSetHelper, barrier));
}
}
base::WeakPtr<PrefetchContainer> PrefetchService::GetPrefetchToServe(
const GURL& url) const {
auto prefetch_iter = prefetches_ready_to_serve_.find(url);
if (prefetch_iter == prefetches_ready_to_serve_.end())
return nullptr;
return prefetch_iter->second;
}
// static
void PrefetchService::SetServiceWorkerContextForTesting(
ServiceWorkerContext* context) {
g_service_worker_context_for_testing = context;
}
// static
void PrefetchService::SetHostNonUniqueFilterForTesting(
bool (*filter)(base::StringPiece)) {
g_host_non_unique_filter = filter;
}
// static
void PrefetchService::SetURLLoaderFactoryForTesting(
network::mojom::URLLoaderFactory* url_loader_factory) {
g_url_loader_factory_for_testing = url_loader_factory;
}
void PrefetchService::RecordExistingPrefetchWithMatchingURL(
base::WeakPtr<PrefetchContainer> prefetch_container) const {
bool matching_prefetch = false;
for (const auto& prefetch_iter : all_prefetches_) {
if (prefetch_iter.second &&
prefetch_iter.second->GetURL() == prefetch_container->GetURL() &&
prefetch_iter.second->GetReferringRenderFrameHostId() !=
prefetch_container->GetReferringRenderFrameHostId()) {
matching_prefetch = true;
break;
}
}
base::UmaHistogramBoolean(
"PrefetchProxy.Prefetch.ExistingPrefetchWithMatchingURL",
matching_prefetch);
}
} // namespace content