| // Copyright 2022 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_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/ranges/algorithm.h" |
| #include "base/timer/timer.h" |
| #include "content/browser/browser_context_impl.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_origin_prober.h" |
| #include "content/browser/preloading/prefetch/prefetch_params.h" |
| #include "content/browser/preloading/prefetch/prefetch_proxy_configurator.h" |
| #include "content/browser/preloading/prefetch/prefetch_status.h" |
| #include "content/browser/preloading/prefetch/prefetch_streaming_url_loader.h" |
| #include "content/browser/preloading/prefetch/proxy_lookup_client_impl.h" |
| #include "content/browser/preloading/preloading_attempt_impl.h" |
| #include "content/browser/renderer_host/frame_tree_node.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/frame_accept_header.h" |
| #include "content/public/browser/prefetch_service_delegate.h" |
| #include "content/public/browser/preloading.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/url_response_head.mojom-shared.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "third_party/blink/public/common/loader/referrer_utils.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; |
| |
| static network::mojom::NetworkContext* |
| g_network_context_for_proxy_lookup_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::kPrefetchNotEligibleSchemeIsNotHttps: |
| case PrefetchStatus::kPrefetchNotEligibleNonDefaultStoragePartition: |
| case PrefetchStatus::kPrefetchIneligibleRetryAfter: |
| case PrefetchStatus::kPrefetchProxyNotAvailable: |
| case PrefetchStatus::kPrefetchNotEligibleHostIsNonUnique: |
| case PrefetchStatus::kPrefetchNotEligibleDataSaverEnabled: |
| case PrefetchStatus::kPrefetchNotEligibleBatterySaverEnabled: |
| case PrefetchStatus::kPrefetchNotEligiblePreloadingDisabled: |
| case PrefetchStatus::kPrefetchNotEligibleExistingProxy: |
| case PrefetchStatus::kPrefetchNotEligibleBrowserContextOffTheRecord: |
| case PrefetchStatus:: |
| kPrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy: |
| // These statuses don't relate to any user state, so don't send a decoy |
| // request. |
| return false; |
| case PrefetchStatus::kPrefetchNotUsedProbeFailed: |
| case PrefetchStatus::kPrefetchNotStarted: |
| case PrefetchStatus::kPrefetchNotFinishedInTime: |
| case PrefetchStatus::kPrefetchFailedNetError: |
| case PrefetchStatus::kPrefetchFailedNon2XX: |
| case PrefetchStatus::kPrefetchFailedMIMENotSupported: |
| case PrefetchStatus::kPrefetchSuccessful: |
| case PrefetchStatus::kPrefetchIsPrivacyDecoy: |
| case PrefetchStatus::kPrefetchIsStale: |
| case PrefetchStatus::kPrefetchNotUsedCookiesChanged: |
| case PrefetchStatus::kPrefetchResponseUsed: |
| case PrefetchStatus::kPrefetchHeldback: |
| case PrefetchStatus::kPrefetchAllowed: |
| case PrefetchStatus::kPrefetchFailedInvalidRedirect: |
| case PrefetchStatus::kPrefetchFailedIneligibleRedirect: |
| case PrefetchStatus::kPrefetchFailedPerPageLimitExceeded: |
| case PrefetchStatus::kPrefetchEvicted: |
| // These statuses should not be returned by the eligibility checks, and |
| // thus not be passed in here. |
| NOTREACHED(); |
| return false; |
| } |
| } |
| |
| bool ShouldStartSpareRenderer() { |
| if (!PrefetchStartsSpareRenderer()) { |
| return false; |
| } |
| |
| for (RenderProcessHost::iterator iter(RenderProcessHost::AllHostsIterator()); |
| !iter.IsAtEnd(); iter.Advance()) { |
| if (iter.GetCurrentValue()->IsUnused()) { |
| // There is already a spare renderer. |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void RecordPrefetchProxyPrefetchMainframeTotalTime( |
| 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; |
| } |
| |
| UMA_HISTOGRAM_CUSTOM_TIMES("PrefetchProxy.Prefetch.Mainframe.TotalTime", |
| end - start, base::Milliseconds(10), |
| base::Seconds(30), 100); |
| } |
| |
| void RecordPrefetchProxyPrefetchMainframeConnectTime( |
| 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; |
| } |
| |
| UMA_HISTOGRAM_TIMES("PrefetchProxy.Prefetch.Mainframe.ConnectTime", |
| end - start); |
| } |
| |
| void RecordPrefetchProxyPrefetchMainframeRespCode(int response_code) { |
| base::UmaHistogramSparse("PrefetchProxy.Prefetch.Mainframe.RespCode", |
| response_code); |
| } |
| |
| void RecordPrefetchProxyPrefetchMainframeNetError(int net_error) { |
| base::UmaHistogramSparse("PrefetchProxy.Prefetch.Mainframe.NetError", |
| std::abs(net_error)); |
| } |
| |
| void RecordPrefetchProxyPrefetchMainframeBodyLength(int64_t body_length) { |
| UMA_HISTOGRAM_COUNTS_10M("PrefetchProxy.Prefetch.Mainframe.BodyLength", |
| body_length); |
| } |
| |
| 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(); |
| } |
| |
| // Returns true if the prefetch is heldback, and set the holdback status |
| // correspondingly. |
| bool CheckAndSetPrefetchHoldbackStatus( |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| if (!prefetch_container->HasPreloadingAttempt()) { |
| return false; |
| } |
| // In addition to the globally-controlled preloading config, check for the |
| // feature-specific holdback. We disable the feature if the user is in either |
| // of those holdbacks. |
| if (IsContentPrefetchHoldback()) { |
| prefetch_container->preloading_attempt()->SetHoldbackStatus( |
| PreloadingHoldbackStatus::kHoldback); |
| } |
| if (prefetch_container->preloading_attempt()->ShouldHoldback()) { |
| prefetch_container->SetPrefetchStatus(PrefetchStatus::kPrefetchHeldback); |
| return true; |
| } |
| return false; |
| } |
| |
| BrowserContext* BrowserContextFromFrameTreeNodeId(int frame_tree_node_id) { |
| WebContents* web_content = |
| WebContents::FromFrameTreeNodeId(frame_tree_node_id); |
| if (!web_content) { |
| return nullptr; |
| } |
| return web_content->GetBrowserContext(); |
| } |
| |
| void RecordRedirectResult(PrefetchRedirectResult result) { |
| UMA_HISTOGRAM_ENUMERATION("PrefetchProxy.Redirect.Result", result); |
| } |
| |
| void RecordRedirectNetworkContextTransition( |
| bool previous_requires_isolated_network_context, |
| bool redirect_requires_isolated_network_context) { |
| PrefetchRedirectNetworkContextTransition transition; |
| if (!previous_requires_isolated_network_context && |
| !redirect_requires_isolated_network_context) { |
| transition = PrefetchRedirectNetworkContextTransition::kDefaultToDefault; |
| } |
| if (!previous_requires_isolated_network_context && |
| redirect_requires_isolated_network_context) { |
| transition = PrefetchRedirectNetworkContextTransition::kDefaultToIsolated; |
| } |
| if (previous_requires_isolated_network_context && |
| !redirect_requires_isolated_network_context) { |
| transition = PrefetchRedirectNetworkContextTransition::kIsolatedToDefault; |
| } |
| if (previous_requires_isolated_network_context && |
| redirect_requires_isolated_network_context) { |
| transition = PrefetchRedirectNetworkContextTransition::kIsolatedToIsolated; |
| } |
| |
| UMA_HISTOGRAM_ENUMERATION( |
| "PrefetchProxy.Redirect.NetworkContextStateTransition", transition); |
| } |
| |
| void OnIsolatedCookieCopyComplete( |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| if (prefetch_container) { |
| prefetch_container->GetReader().OnIsolatedCookieCopyComplete(); |
| } |
| } |
| |
| void BlockUntilHeadTimeoutHelper( |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| VLOG(0) << "PS::BlockUntilHeadTimeoutHelper"; |
| |
| if (!prefetch_container || !prefetch_container->GetLastStreamingURLLoader()) { |
| return; |
| } |
| |
| // Takes the on_received_head_callback |
| base::OnceClosure on_received_head_callback = |
| prefetch_container->GetLastStreamingURLLoader() |
| ->ReleaseOnReceivedHeadCallback(); |
| if (on_received_head_callback) { |
| std::move(on_received_head_callback).Run(); |
| } |
| } |
| |
| bool IsReferrerPolicySufficientlyStrict( |
| const network::mojom::ReferrerPolicy& referrer_policy) { |
| // https://github.com/WICG/nav-speculation/blob/main/prefetch.bs#L606 |
| // "", "`strict-origin-when-cross-origin`", "`strict-origin`", |
| // "`same-origin`", "`no-referrer`". |
| switch (referrer_policy) { |
| case network::mojom::ReferrerPolicy::kDefault: |
| case network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin: |
| case network::mojom::ReferrerPolicy::kSameOrigin: |
| case network::mojom::ReferrerPolicy::kStrictOrigin: |
| return true; |
| case network::mojom::ReferrerPolicy::kAlways: |
| case network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade: |
| case network::mojom::ReferrerPolicy::kNever: |
| case network::mojom::ReferrerPolicy::kOrigin: |
| case network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin: |
| return false; |
| } |
| } |
| |
| } // 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); |
| } |
| |
| // static |
| PrefetchService* PrefetchService::GetFromFrameTreeNodeId( |
| int frame_tree_node_id) { |
| BrowserContext* browser_context = |
| BrowserContextFromFrameTreeNodeId(frame_tree_node_id); |
| if (!browser_context) { |
| return nullptr; |
| } |
| return BrowserContextImpl::From(browser_context)->GetPrefetchService(); |
| } |
| |
| void PrefetchService::SetFromFrameTreeNodeIdForTesting( |
| int frame_tree_node_id, |
| std::unique_ptr<PrefetchService> prefetch_service) { |
| BrowserContext* browser_context = |
| BrowserContextFromFrameTreeNodeId(frame_tree_node_id); |
| CHECK(browser_context); |
| return BrowserContextImpl::From(browser_context) |
| ->SetPrefetchServiceForTesting(std::move(prefetch_service)); // IN-TEST |
| } |
| |
| 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() : "")), |
| origin_prober_(std::make_unique<PrefetchOriginProber>( |
| browser_context_, |
| PrefetchDNSCanaryCheckURL( |
| delegate_ ? delegate_->GetDefaultDNSCanaryCheckURL() : GURL("")), |
| PrefetchTLSCanaryCheckURL( |
| delegate_ ? delegate_->GetDefaultTLSCanaryCheckURL() |
| : GURL("")))) {} |
| |
| PrefetchService::~PrefetchService() = default; |
| |
| PrefetchOriginProber* PrefetchService::GetPrefetchOriginProber() const { |
| return origin_prober_.get(); |
| } |
| |
| void PrefetchService::PrefetchUrl( |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| DCHECK(prefetch_container); |
| auto prefetch_container_key = prefetch_container->GetPrefetchContainerKey(); |
| |
| if (delegate_) { |
| // If pre* actions are disabled then don't prefetch. |
| switch (delegate_->IsSomePreloadingEnabled()) { |
| case PreloadingEligibility::kEligible: |
| break; |
| case PreloadingEligibility::kDataSaverEnabled: |
| OnGotEligibilityResult( |
| prefetch_container, false, |
| PrefetchStatus::kPrefetchNotEligibleDataSaverEnabled); |
| return; |
| case PreloadingEligibility::kBatterySaverEnabled: |
| OnGotEligibilityResult( |
| prefetch_container, false, |
| PrefetchStatus::kPrefetchNotEligibleBatterySaverEnabled); |
| return; |
| case PreloadingEligibility::kPreloadingDisabled: |
| OnGotEligibilityResult( |
| prefetch_container, false, |
| PrefetchStatus::kPrefetchNotEligiblePreloadingDisabled); |
| return; |
| default: |
| DVLOG(1) << *prefetch_container |
| << ": not prefetched (PrefetchServiceDelegate)"; |
| return; |
| } |
| |
| const auto& prefetch_type = prefetch_container->GetPrefetchType(); |
| if (prefetch_type.IsProxyRequiredWhenCrossOrigin() && |
| !prefetch_type.IsProxyBypassedForTesting()) { |
| bool allow_all_domains = |
| PrefetchAllowAllDomains() || |
| (PrefetchAllowAllDomainsForExtendedPreloading() && |
| delegate_->IsExtendedPreloadingEnabled()); |
| if (!allow_all_domains && |
| !delegate_->IsDomainInPrefetchAllowList( |
| RenderFrameHost::FromID( |
| prefetch_container->GetReferringRenderFrameHostId()) |
| ->GetLastCommittedURL())) { |
| DVLOG(1) << *prefetch_container |
| << ": not prefetched (not in allow list)"; |
| return; |
| } |
| } |
| |
| delegate_->OnPrefetchLikely(WebContents::FromRenderFrameHost( |
| &prefetch_container->GetPrefetchDocumentManager() |
| ->render_frame_host())); |
| } |
| |
| RecordExistingPrefetchWithMatchingURL(prefetch_container); |
| |
| // A newly submitted prefetch could already be in |all_prefetches_| if and |
| // only if: |
| // 1) There was a same origin navigaition that used the same renderer. |
| // 2) Both pages requested a prefetch for the same URL. |
| // 3) The prefetch from the first page had at least started its network |
| // request (which would mean that it is in |owned_prefetches_| and owned |
| // by the prefetch service). |
| // If this happens, then we just delete the old prefetch and add the new |
| // prefetch to |all_prefetches_|. |
| auto prefetch_iter = all_prefetches_.find(prefetch_container_key); |
| if (prefetch_iter != all_prefetches_.end() && prefetch_iter->second) { |
| ResetPrefetch(prefetch_iter->second); |
| } |
| all_prefetches_[prefetch_container_key] = prefetch_container; |
| |
| CheckEligibilityOfPrefetch( |
| prefetch_container->GetURL(), prefetch_container, |
| base::BindOnce(&PrefetchService::OnGotEligibilityResult, |
| weak_method_factory_.GetWeakPtr())); |
| } |
| |
| void PrefetchService::CheckEligibilityOfPrefetch( |
| const GURL& url, |
| 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, |
| PrefetchStatus::kPrefetchNotEligibleBrowserContextOffTheRecord); |
| 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(url.HostNoBrackets()) |
| : net::IsHostnameNonUnique(url.HostNoBrackets()); |
| if (!prefetch_container->GetPrefetchType().IsProxyBypassedForTesting() && |
| prefetch_container->IsProxyRequiredForURL(url) && 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->IsProxyRequiredForURL(url) |
| ? url.SchemeIs(url::kHttpsScheme) |
| : (url.SchemeIsHTTPOrHTTPS() && |
| network::IsUrlPotentiallyTrustworthy(url)); |
| if (!is_secure_http) { |
| std::move(result_callback) |
| .Run(prefetch_container, false, |
| PrefetchStatus::kPrefetchNotEligibleSchemeIsNotHttps); |
| return; |
| } |
| |
| if (prefetch_container->IsProxyRequiredForURL(url) && |
| !prefetch_container->GetPrefetchType().IsProxyBypassedForTesting() && |
| (!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(url, |
| /*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(url)) { |
| 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::CreateFirstParty(url::Origin::Create(url))); |
| if (site_has_service_worker) { |
| std::move(result_callback) |
| .Run(prefetch_container, false, |
| PrefetchStatus::kPrefetchNotEligibleUserHasServiceWorker); |
| return; |
| } |
| |
| // This blocks same-site cross-origin prefetches that require the prefetch |
| // proxy. Same-site prefetches are made using the default network context, and |
| // the prefetch request cannot be configured to use the proxy in that network |
| // context. |
| // TODO(https://crbug.com/1439986): Allow same-site cross-origin prefetches |
| // that require the prefetch proxy to be made. |
| if (prefetch_container->IsProxyRequiredForURL(url) && |
| !prefetch_container |
| ->IsIsolatedNetworkContextRequiredForCurrentPrefetch()) { |
| std::move(result_callback) |
| .Run(prefetch_container, false, |
| PrefetchStatus:: |
| kPrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy); |
| return; |
| } |
| |
| // We do not need to check the cookies of prefetches that do not need an |
| // isolated network context. |
| if (!prefetch_container |
| ->IsIsolatedNetworkContextRequiredForCurrentPrefetch()) { |
| 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( |
| url, options, net::CookiePartitionKeyCollection::Todo(), |
| base::BindOnce(&PrefetchService::OnGotCookiesForEligibilityCheck, |
| weak_method_factory_.GetWeakPtr(), url, prefetch_container, |
| std::move(result_callback))); |
| } |
| |
| void PrefetchService::OnGotCookiesForEligibilityCheck( |
| const GURL& url, |
| 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 (url.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; |
| } |
| |
| StartProxyLookupCheck(url, prefetch_container, std::move(result_callback)); |
| } |
| |
| void PrefetchService::StartProxyLookupCheck( |
| const GURL& url, |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| OnEligibilityResultCallback result_callback) const { |
| // Same origin prefetches (which use the default network context and cannot |
| // use the prefetch proxy) can use the existing proxy settings. |
| // TODO(https://crbug.com/1343903): Copy proxy settings over to the isolated |
| // network context for the prefetch in order to allow non-private cross origin |
| // prefetches to be made using the existing proxy settings. |
| if (!prefetch_container |
| ->IsIsolatedNetworkContextRequiredForCurrentPrefetch()) { |
| std::move(result_callback).Run(prefetch_container, true, absl::nullopt); |
| return; |
| } |
| |
| // Start proxy check for this prefetch, and give ownership of the |
| // |ProxyLookupClientImpl| to |prefetch_container|. |
| auto network_anonymization_key = |
| net::NetworkAnonymizationKey::CreateSameSite(net::SchemefulSite(url)); |
| prefetch_container->TakeProxyLookupClient( |
| std::make_unique<ProxyLookupClientImpl>( |
| url, network_anonymization_key, |
| base::BindOnce(&PrefetchService::OnGotProxyLookupResult, |
| weak_method_factory_.GetWeakPtr(), prefetch_container, |
| std::move(result_callback)), |
| g_network_context_for_proxy_lookup_for_testing |
| ? g_network_context_for_proxy_lookup_for_testing |
| : browser_context_->GetDefaultStoragePartition() |
| ->GetNetworkContext())); |
| } |
| |
| void PrefetchService::OnGotProxyLookupResult( |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| OnEligibilityResultCallback result_callback, |
| bool has_proxy) const { |
| if (!prefetch_container) { |
| std::move(result_callback).Run(prefetch_container, false, absl::nullopt); |
| return; |
| } |
| |
| prefetch_container->ReleaseProxyLookupClient(); |
| if (has_proxy) { |
| std::move(result_callback) |
| .Run(prefetch_container, false, |
| PrefetchStatus::kPrefetchNotEligibleExistingProxy); |
| 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 (!prefetch_container) { |
| return; |
| } |
| |
| bool is_decoy = false; |
| if (!eligible) { |
| // Expect a status if the container is alive but prefetch not eligible. |
| DCHECK(status.has_value()); |
| is_decoy = |
| prefetch_container->IsProxyRequiredForURL( |
| prefetch_container->GetURL()) && |
| ShouldConsiderDecoyRequestForStatus(status.value()) && |
| PrefetchServiceSendDecoyRequestForIneligblePrefetch( |
| delegate_ ? delegate_->DisableDecoysBasedOnUserSettings() : false); |
| } |
| // The prefetch decoy is pushed onto the queue and the network request will be |
| // dispatched, but the response will not be used. Thus it is eligible but a |
| // failure. |
| prefetch_container->SetIsDecoy(is_decoy); |
| if (is_decoy) { |
| prefetch_container->OnEligibilityCheckComplete(true, absl::nullopt); |
| } else { |
| prefetch_container->OnEligibilityCheckComplete(eligible, status); |
| } |
| |
| if (!eligible && !is_decoy) { |
| DVLOG(1) << *prefetch_container |
| << ": not prefetched (not eligible nor decoy. PrefetchStatus=" |
| << static_cast<int>(*status) << ")"; |
| return; |
| } |
| |
| if (!is_decoy) { |
| prefetch_container->SetPrefetchStatus(PrefetchStatus::kPrefetchNotStarted); |
| |
| // 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 |
| ->IsIsolatedNetworkContextRequiredForCurrentPrefetch()) { |
| prefetch_container->RegisterCookieListener( |
| browser_context_->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess()); |
| } |
| } |
| prefetch_queue_.push_back(prefetch_container); |
| |
| // Calling |Prefetch| could result in a prefetch being deleted, so |
| // |prefetch_cotnainer| should not be used after this call. |
| Prefetch(); |
| } |
| |
| void PrefetchService::OnGotEligibilityResultForRedirect( |
| const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr redirect_head, |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| bool eligible, |
| absl::optional<PrefetchStatus> status) { |
| if (!prefetch_container) { |
| return; |
| } |
| |
| RecordRedirectResult(eligible |
| ? PrefetchRedirectResult::kSuccessRedirectFollowed |
| : PrefetchRedirectResult::kFailedIneligible); |
| |
| // If the redirect is ineligible, the prefetch may change into a decoy. |
| bool is_decoy = false; |
| if (!eligible) { |
| // Expect a status if the container is alive but prefetch not eligible. |
| DCHECK(status.has_value()); |
| is_decoy = |
| prefetch_container->IsProxyRequiredForURL(redirect_info.new_url) && |
| ShouldConsiderDecoyRequestForStatus(status.value()) && |
| PrefetchServiceSendDecoyRequestForIneligblePrefetch( |
| delegate_ ? delegate_->DisableDecoysBasedOnUserSettings() : false); |
| } |
| prefetch_container->SetIsDecoy(prefetch_container->IsDecoy() || is_decoy); |
| |
| // Inform the prefetch container of the result of the eligibility check |
| if (prefetch_container->IsDecoy()) { |
| prefetch_container->OnEligibilityCheckComplete(true, absl::nullopt); |
| } else { |
| prefetch_container->OnEligibilityCheckComplete(eligible, status); |
| if (eligible && |
| prefetch_container |
| ->IsIsolatedNetworkContextRequiredForCurrentPrefetch()) { |
| prefetch_container->RegisterCookieListener( |
| browser_context_->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess()); |
| } |
| } |
| |
| // If the redirect is not eligible and the prefetch is not a decoy, then stop |
| // the prefetch. |
| if (!eligible && !prefetch_container->IsDecoy()) { |
| active_prefetches_.erase(prefetch_container->GetPrefetchContainerKey()); |
| prefetch_container->GetLastStreamingURLLoader()->HandleRedirect( |
| PrefetchStreamingURLLoaderStatus::kFailedInvalidRedirect, redirect_info, |
| std::move(redirect_head)); |
| prefetch_container->ResetAllStreamingURLLoaders(); |
| |
| Prefetch(); |
| |
| return; |
| } |
| |
| // If the redirect requires a change in network contexts, then stop the |
| // current streaming URL loader and start a new streaming URL loader for the |
| // redirect URL. |
| if (prefetch_container |
| ->IsIsolatedNetworkContextRequiredForCurrentPrefetch() != |
| prefetch_container |
| ->IsIsolatedNetworkContextRequiredForPreviousRedirectHop()) { |
| prefetch_container->GetLastStreamingURLLoader()->HandleRedirect( |
| PrefetchStreamingURLLoaderStatus:: |
| kStopSwitchInNetworkContextForRedirect, |
| redirect_info, std::move(redirect_head)); |
| MakePrefetchRequest(prefetch_container, redirect_info.new_url); |
| |
| return; |
| } |
| |
| // Otherwise, follow the redirect in the same streaming URL loader. |
| prefetch_container->GetLastStreamingURLLoader()->HandleRedirect( |
| PrefetchStreamingURLLoaderStatus::kFollowRedirect, redirect_info, |
| std::move(redirect_head)); |
| } |
| |
| void PrefetchService::Prefetch() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (PrefetchCloseIdleSockets()) { |
| for (const auto& iter : all_prefetches_) { |
| if (iter.second) { |
| iter.second->CloseIdleConnections(); |
| } |
| } |
| } |
| |
| base::WeakPtr<PrefetchContainer> next_prefetch = nullptr; |
| while ((next_prefetch = PopNextPrefetchContainer()) != nullptr) { |
| StartSinglePrefetch(next_prefetch); |
| } |
| } |
| |
| base::WeakPtr<PrefetchContainer> PrefetchService::PopNextPrefetchContainer() { |
| auto new_end = std::remove_if( |
| prefetch_queue_.begin(), prefetch_queue_.end(), |
| [&](const base::WeakPtr<PrefetchContainer>& prefetch_container) { |
| // Remove all prefetches from queue that no longer exist. |
| return !prefetch_container; |
| }); |
| prefetch_queue_.erase(new_end, prefetch_queue_.end()); |
| |
| // Don't start any new prefetches if we are currently at or beyond the limit |
| // for the number of concurrent prefetches. |
| DCHECK(PrefetchServiceMaximumNumberOfConcurrentPrefetches() >= 0); |
| if (active_prefetches_.size() >= |
| PrefetchServiceMaximumNumberOfConcurrentPrefetches()) { |
| return nullptr; |
| } |
| |
| // Get the first prefetch that is from an active RenderFrameHost and in a |
| // visible WebContents. |
| auto prefetch_iter = base::ranges::find_if( |
| prefetch_queue_, |
| [](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()); |
| |
| RemovePrefetch(prefetch_container->GetPrefetchContainerKey()); |
| |
| auto active_prefetch_iter = |
| active_prefetches_.find(prefetch_container->GetPrefetchContainerKey()); |
| if (active_prefetch_iter != active_prefetches_.end()) { |
| active_prefetches_.erase(active_prefetch_iter); |
| } |
| |
| auto prefetches_ready_to_serve_iter = prefetches_ready_to_serve_.find( |
| prefetch_container->GetPrefetchContainerKey()); |
| 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); |
| } |
| |
| owned_prefetches_.erase( |
| owned_prefetches_.find(prefetch_container->GetPrefetchContainerKey())); |
| } |
| |
| void PrefetchService::RemovePrefetch( |
| const PrefetchContainer::Key& prefetch_container_key) { |
| const auto prefetch_iter = all_prefetches_.find(prefetch_container_key); |
| if (prefetch_iter != all_prefetches_.end()) { |
| all_prefetches_.erase(prefetch_iter); |
| } |
| } |
| |
| void PrefetchService::EvictPrefetch( |
| const PrefetchContainer::Key& prefetch_container_key) { |
| DCHECK(base::Contains(owned_prefetches_, prefetch_container_key)); |
| base::WeakPtr<PrefetchContainer> prefetch_container = |
| owned_prefetches_[prefetch_container_key].first->GetWeakPtr(); |
| DCHECK(prefetch_container); |
| prefetch_container->SetPrefetchStatus(PrefetchStatus::kPrefetchEvicted); |
| ResetPrefetch(prefetch_container); |
| if (active_prefetches_.size() < |
| PrefetchServiceMaximumNumberOfConcurrentPrefetches()) { |
| Prefetch(); |
| } |
| } |
| |
| void PrefetchService::StartSinglePrefetch( |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(prefetch_container); |
| |
| // Do not prefetch for a Holdback control group. Called after the checks in |
| // `PopNextPrefetchContainer` because we want to compare against the |
| // prefetches that would have been dispatched. |
| if (CheckAndSetPrefetchHoldbackStatus(prefetch_container)) { |
| DVLOG(1) << *prefetch_container |
| << ": not prefetched (holdback control group)"; |
| return; |
| } |
| |
| TakeOwnershipOfPrefetch(prefetch_container); |
| |
| // Note: This must be called before CanPrefetchNow() below to prevent |
| // re-entrancy. |
| active_prefetches_.insert(prefetch_container->GetPrefetchContainerKey()); |
| |
| const bool is_above_limit = |
| (PrefetchNewLimitsEnabled() && |
| !prefetch_container->GetPrefetchDocumentManager()->CanPrefetchNow( |
| prefetch_container.get())) || |
| (!PrefetchNewLimitsEnabled() && |
| prefetch_container->GetPrefetchDocumentManager() |
| ->GetNumberOfPrefetchRequestAttempted() >= |
| PrefetchServiceMaximumNumberOfPrefetchesPerPage().value_or( |
| std::numeric_limits<int>::max())); |
| if (is_above_limit) { |
| // TODO(crbug.com/1445086): We shouldn't be cancelling this and should |
| // just keep it in the queue when PrefetchNewLimits is enabled (move this |
| // check to PopNextPrefetchContainer()). |
| prefetch_container->SetPrefetchStatus( |
| PrefetchStatus::kPrefetchFailedPerPageLimitExceeded); |
| ResetPrefetch(prefetch_container); |
| return; |
| } |
| |
| prefetch_container->GetPrefetchDocumentManager() |
| ->OnPrefetchRequestAttempted(); |
| |
| if (!prefetch_container->IsDecoy()) { |
| // The status is updated to be successful or failed when it finishes. |
| prefetch_container->SetPrefetchStatus( |
| PrefetchStatus::kPrefetchNotFinishedInTime); |
| } |
| |
| MakePrefetchRequest(prefetch_container, prefetch_container->GetURL()); |
| |
| PrefetchDocumentManager* prefetch_document_manager = |
| prefetch_container->GetPrefetchDocumentManager(); |
| if (prefetch_container->GetPrefetchType().IsProxyRequiredWhenCrossOrigin() && |
| !prefetch_container->IsDecoy() && |
| (!prefetch_document_manager || |
| !prefetch_document_manager->HaveCanaryChecksStarted())) { |
| // Make sure canary checks have run so we know the result by the time we |
| // want to use the prefetch. Checking the canary cache can be a slow and |
| // blocking operation (see crbug.com/1266018), so we only do this for the |
| // first non-decoy prefetch we make on the page. |
| // TODO(crbug.com/1266018): once this bug is fixed, fire off canary check |
| // regardless of whether the request is a decoy or not. |
| origin_prober_->RunCanaryChecksIfNeeded(); |
| |
| if (prefetch_document_manager) { |
| prefetch_document_manager->OnCanaryChecksStarted(); |
| } |
| } |
| |
| // 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_); |
| } |
| } |
| |
| void PrefetchService::MakePrefetchRequest( |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| const GURL& url) { |
| url::Origin origin = url::Origin::Create(url); |
| 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 = url; |
| request->method = "GET"; |
| request->referrer = prefetch_container->GetReferrer().url; |
| request->referrer_policy = Referrer::ReferrerPolicyForUrlRequest( |
| prefetch_container->GetReferrer().policy); |
| 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->IsProxyRequiredForURL(url) |
| ? "prefetch;anonymous-client-ip" |
| : "prefetch"); |
| request->headers.SetHeader( |
| net::HttpRequestHeaders::kAccept, |
| FrameAcceptHeaderValue(/*allow_sxg_responses=*/true, browser_context_)); |
| request->headers.SetHeader("Upgrade-Insecure-Requests", "1"); |
| |
| // 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(); |
| request->devtools_request_id = prefetch_container->RequestId(); |
| |
| const auto& devtools_observer = prefetch_container->GetDevToolsObserver(); |
| if (devtools_observer && !prefetch_container->IsDecoy()) { |
| request->trusted_params->devtools_observer = |
| devtools_observer->MakeSelfOwnedNetworkServiceDevToolsObserver(); |
| 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<PrefetchStreamingURLLoader> streaming_loader = |
| std::make_unique<PrefetchStreamingURLLoader>( |
| GetURLLoaderFactoryForCurrentPrefetch(prefetch_container), |
| std::move(request), traffic_annotation, PrefetchTimeoutDuration(), |
| base::BindOnce(&PrefetchService::OnPrefetchResponseStarted, |
| base::Unretained(this), prefetch_container), |
| base::BindOnce(&PrefetchService::OnPrefetchResponseCompleted, |
| base::Unretained(this), prefetch_container), |
| base::BindRepeating(&PrefetchService::OnPrefetchRedirect, |
| base::Unretained(this), prefetch_container)); |
| |
| prefetch_container->TakeStreamingURLLoader(std::move(streaming_loader)); |
| |
| DVLOG(1) << *prefetch_container << ": PrefetchStreamingURLLoader is created."; |
| } |
| |
| network::mojom::URLLoaderFactory* |
| PrefetchService::GetURLLoaderFactoryForCurrentPrefetch( |
| 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->GetOrCreateNetworkContextForCurrentPrefetch(this) |
| ->GetURLLoaderFactory(); |
| } |
| |
| void PrefetchService::OnPrefetchRedirect( |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr redirect_head) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!prefetch_container) { |
| RecordRedirectResult(PrefetchRedirectResult::kFailedNullPrefetch); |
| return; |
| } |
| |
| DCHECK( |
| active_prefetches_.find(prefetch_container->GetPrefetchContainerKey()) != |
| active_prefetches_.end()); |
| |
| prefetch_container->AddRedirectHop(redirect_info.new_url); |
| |
| // Update the prefetch's referrer in case a redirect requires a change in |
| // network context and a new request needs to be started. |
| prefetch_container->UpdateReferrer( |
| GURL(redirect_info.new_referrer), |
| blink::ReferrerUtils::NetToMojoReferrerPolicy( |
| redirect_info.new_referrer_policy)); |
| |
| // Check that the prefetch's referrer policy is sufficiently strict to allow |
| // for the redirect to be followed. |
| net::SchemefulSite previous_site = |
| prefetch_container->GetSiteForPreviousRedirectHop(redirect_info.new_url); |
| net::SchemefulSite redirect_site(redirect_info.new_url); |
| bool is_referrer_policy_sufficiently_strict = |
| IsReferrerPolicySufficientlyStrict( |
| prefetch_container->GetReferrer().policy); |
| |
| absl::optional<PrefetchRedirectResult> failure; |
| |
| if (!base::FeatureList::IsEnabled(features::kPrefetchRedirects)) { |
| failure = PrefetchRedirectResult::kFailedRedirectsDisabled; |
| } else if (redirect_info.new_method != "GET") { |
| failure = PrefetchRedirectResult::kFailedInvalidMethod; |
| } else if (!redirect_head->headers || |
| redirect_head->headers->response_code() < 300 || |
| redirect_head->headers->response_code() >= 400) { |
| failure = PrefetchRedirectResult::kFailedInvalidResponseCode; |
| } else if (previous_site != redirect_site && |
| !is_referrer_policy_sufficiently_strict) { |
| failure = PrefetchRedirectResult::kFailedInsufficientReferrerPolicy; |
| } |
| |
| if (failure) { |
| active_prefetches_.erase(prefetch_container->GetPrefetchContainerKey()); |
| prefetch_container->SetPrefetchStatus( |
| PrefetchStatus::kPrefetchFailedInvalidRedirect); |
| prefetch_container->GetLastStreamingURLLoader()->HandleRedirect( |
| PrefetchStreamingURLLoaderStatus::kFailedInvalidRedirect, redirect_info, |
| std::move(redirect_head)); |
| prefetch_container->ResetAllStreamingURLLoaders(); |
| |
| Prefetch(); |
| RecordRedirectResult(*failure); |
| return; |
| } |
| |
| RecordRedirectNetworkContextTransition( |
| prefetch_container |
| ->IsIsolatedNetworkContextRequiredForPreviousRedirectHop(), |
| prefetch_container->IsIsolatedNetworkContextRequiredForCurrentPrefetch()); |
| |
| CheckEligibilityOfPrefetch( |
| redirect_info.new_url, prefetch_container, |
| base::BindOnce(&PrefetchService::OnGotEligibilityResultForRedirect, |
| base::Unretained(this), redirect_info, |
| std::move(redirect_head))); |
| } |
| |
| PrefetchStreamingURLLoaderStatus PrefetchService::OnPrefetchResponseStarted( |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| network::mojom::URLResponseHead* head) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!prefetch_container || prefetch_container->IsDecoy()) { |
| return PrefetchStreamingURLLoaderStatus::kPrefetchWasDecoy; |
| } |
| |
| if (!head) { |
| return PrefetchStreamingURLLoaderStatus::kFailedInvalidHead; |
| } |
| |
| const auto& devtools_observer = prefetch_container->GetDevToolsObserver(); |
| if (devtools_observer) { |
| devtools_observer->OnPrefetchResponseReceived( |
| prefetch_container->GetURL(), prefetch_container->RequestId(), *head); |
| } |
| |
| if (!head->headers) { |
| return PrefetchStreamingURLLoaderStatus::kFailedInvalidHeaders; |
| } |
| |
| RecordPrefetchProxyPrefetchMainframeTotalTime(head); |
| RecordPrefetchProxyPrefetchMainframeConnectTime(head); |
| |
| int response_code = head->headers->response_code(); |
| RecordPrefetchProxyPrefetchMainframeRespCode(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_) { |
| // Cap the retry after value to a maximum. |
| if (retry_after > PrefetchMaximumRetryAfterDelta()) { |
| retry_after = PrefetchMaximumRetryAfterDelta(); |
| } |
| |
| delegate_->ReportOriginRetryAfter(prefetch_container->GetURL(), |
| retry_after); |
| } |
| } |
| return PrefetchStreamingURLLoaderStatus::kFailedNon2XX; |
| } |
| |
| if (PrefetchServiceHTMLOnly() && head->mime_type != "text/html") { |
| prefetch_container->SetPrefetchStatus( |
| PrefetchStatus::kPrefetchFailedMIMENotSupported); |
| return PrefetchStreamingURLLoaderStatus::kFailedMIMENotSupported; |
| } |
| |
| prefetch_container->OnPrefetchedResponseHeadReceived(); |
| return PrefetchStreamingURLLoaderStatus::kHeadReceivedWaitingOnBody; |
| } |
| |
| void PrefetchService::OnPrefetchResponseCompleted( |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| const network::URLLoaderCompletionStatus& completion_status) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!prefetch_container) { |
| return; |
| } |
| |
| DCHECK( |
| active_prefetches_.find(prefetch_container->GetPrefetchContainerKey()) != |
| active_prefetches_.end()); |
| active_prefetches_.erase(prefetch_container->GetPrefetchContainerKey()); |
| |
| prefetch_container->OnPrefetchComplete(); |
| |
| if (prefetch_container->IsDecoy()) { |
| prefetch_container->SetPrefetchStatus( |
| PrefetchStatus::kPrefetchIsPrivacyDecoy); |
| prefetch_container->ResetAllStreamingURLLoaders(); |
| Prefetch(); |
| return; |
| } |
| |
| // TODO(https://crbug.com/1399956): Call |
| // SpeculationHostDevToolsObserver::OnPrefetchBodyDataReceived with body of |
| // the response. |
| const auto& devtools_observer = prefetch_container->GetDevToolsObserver(); |
| if (devtools_observer) { |
| devtools_observer->OnPrefetchRequestComplete( |
| prefetch_container->RequestId(), completion_status); |
| } |
| |
| int net_error = completion_status.error_code; |
| int64_t body_length = completion_status.decoded_body_length; |
| |
| RecordPrefetchProxyPrefetchMainframeNetError(net_error); |
| |
| // Updates the prefetch's status if it hasn't been updated since the request |
| // first started. For the prefetch to reach the network stack, it must have |
| // `PrefetchStatus::kPrefetchAllowed` or beyond. |
| DCHECK(prefetch_container->HasPrefetchStatus()); |
| if (prefetch_container->GetPrefetchStatus() == |
| PrefetchStatus::kPrefetchNotFinishedInTime) { |
| prefetch_container->SetPrefetchStatus( |
| net_error == net::OK ? PrefetchStatus::kPrefetchSuccessful |
| : PrefetchStatus::kPrefetchFailedNetError); |
| prefetch_container->UpdateServingPageMetrics(); |
| } |
| |
| if (net_error == net::OK) { |
| RecordPrefetchProxyPrefetchMainframeBodyLength(body_length); |
| } |
| |
| if (prefetch_container->GetLastStreamingURLLoader()) { |
| // If the prefetch from the streaming URL loader cannot be served at this |
| // point, then it can be discarded. |
| if (!prefetch_container->GetLastStreamingURLLoader()->Servable( |
| PrefetchCacheableDuration())) { |
| prefetch_container->ResetAllStreamingURLLoaders(); |
| } else { |
| PrefetchDocumentManager* prefetch_document_manager = |
| prefetch_container->GetPrefetchDocumentManager(); |
| if (prefetch_document_manager) { |
| prefetch_document_manager->OnPrefetchSuccessful( |
| prefetch_container.get()); |
| } |
| } |
| } |
| |
| Prefetch(); |
| } |
| |
| void PrefetchService::PrepareToServe( |
| const GURL& url, |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| // Ensure |this| has this prefetch. |
| if (all_prefetches_.find(prefetch_container->GetPrefetchContainerKey()) == |
| all_prefetches_.end()) { |
| DVLOG(1) << *prefetch_container |
| << ": didn't promote to ready (not in all_prefetches_)"; |
| return; |
| } |
| |
| bool is_servable = |
| prefetch_container->IsPrefetchServable(PrefetchCacheableDuration()); |
| |
| // `url` might be different from |
| // `prefetch_container->GetPrefetchContainerKey().second` due to |
| // No-Vary-Search. |
| PrefetchContainer::Key ready_key( |
| prefetch_container->GetPrefetchContainerKey().first, url); |
| |
| // 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(ready_key) != |
| prefetches_ready_to_serve_.end()) { |
| DVLOG(1) << *prefetch_container |
| << ": didn't promote to ready (another ready prefetch)"; |
| return; |
| } |
| |
| // Move prefetch into |prefetches_ready_to_serve_|. |
| DVLOG(1) << *prefetch_container << ": promoted to ready"; |
| prefetches_ready_to_serve_[ready_key] = prefetch_container; |
| |
| if (is_servable) { |
| // For prefetches that are already servable, 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->GetReader().GetCurrentNetworkContextToServe()) { |
| // Not set in unit tests. |
| return; |
| } |
| |
| // We only need to copy cookies if the prefetch used an isolated network |
| // context. |
| if (!prefetch_container->GetReader() |
| .IsIsolatedNetworkContextRequiredToServe()) { |
| return; |
| } |
| |
| prefetch_container->GetReader().OnIsolatedCookieCopyStart(); |
| net::CookieOptions options = net::CookieOptions::MakeAllInclusive(); |
| prefetch_container->GetReader() |
| .GetCurrentNetworkContextToServe() |
| ->GetCookieManager() |
| ->GetCookieList( |
| prefetch_container->GetReader().GetCurrentURLToServe(), 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) { |
| prefetch_container->GetReader().OnIsolatedCookiesReadCompleteAndWriteStart(); |
| RecordPrefetchProxyPrefetchMainframeCookiesToCopy(cookie_list.size()); |
| |
| if (cookie_list.empty()) { |
| prefetch_container->GetReader().OnIsolatedCookieCopyComplete(); |
| return; |
| } |
| |
| base::RepeatingClosure barrier = base::BarrierClosure( |
| cookie_list.size(), |
| base::BindOnce(&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->GetReader().GetCurrentURLToServe(), options, |
| base::BindOnce(&CookieSetHelper, barrier)); |
| } |
| } |
| |
| void PrefetchService::DumpPrefetchesForDebug() const { |
| #if DCHECK_IS_ON() |
| std::ostringstream ss; |
| ss << "PrefetchService[" << this << "]:" << std::endl; |
| |
| ss << "Owned:" << std::endl; |
| for (const auto& entry : owned_prefetches_) { |
| ss << *entry.second.first << std::endl; |
| } |
| |
| ss << "Ready to serve:" << std::endl; |
| for (const auto& entry : prefetches_ready_to_serve_) { |
| if (PrefetchContainer* prefetch_container = entry.second.get()) { |
| ss << *prefetch_container << std::endl; |
| } |
| } |
| DVLOG(1) << ss.str(); |
| #endif // DCHECK_IS_ON() |
| } |
| |
| PrefetchContainer* PrefetchService::FindPrefetchContainerToServe( |
| const PrefetchContainer::Key& key) { |
| // Search for an exact match first. If one is found and not deleted, produce |
| // it. |
| auto it = prefetches_ready_to_serve_.find(key); |
| if (it != prefetches_ready_to_serve_.end()) { |
| PrefetchContainer* prefetch = it->second.get(); |
| prefetches_ready_to_serve_.erase(it); |
| if (prefetch && !prefetch->HasPrefetchBeenConsideredToServe()) { |
| return prefetch; |
| } |
| } |
| |
| // Search for an inexact match using the No-Vary-Search hint. |
| // It must either be servable now or potentially servable soon. |
| const auto frame_host_id = key.first; |
| const GURL& nav_url = key.second; |
| for (const auto& active_prefetch : active_prefetches_) { |
| if (active_prefetch.first != frame_host_id) { |
| continue; |
| } |
| PrefetchContainer* prefetch = all_prefetches_[active_prefetch].get(); |
| if (!prefetch || prefetch->HasPrefetchBeenConsideredToServe()) { |
| continue; |
| } |
| const auto& nvs_expected = prefetch->GetNoVarySearchHint(); |
| if (!nvs_expected || |
| !nvs_expected->AreEquivalent(nav_url, prefetch->GetURL())) { |
| continue; |
| } |
| if (prefetch->IsPrefetchServable(PrefetchCacheableDuration()) || |
| prefetch->ShouldBlockUntilHeadReceived()) { |
| return prefetch; |
| } |
| } |
| return nullptr; |
| } |
| |
| void PrefetchService::GetPrefetchToServe( |
| const PrefetchContainer::Key& key, |
| OnPrefetchToServeReady on_prefetch_to_serve_ready) { |
| DumpPrefetchesForDebug(); |
| const GURL& url = key.second; |
| |
| PrefetchContainer* prefetch_container = FindPrefetchContainerToServe(key); |
| if (!prefetch_container) { |
| DVLOG(1) |
| << "PrefetchService::GetPrefetchToServe(" << url |
| << "): PrefetchContainer is null or no matching prefetch was found"; |
| std::move(on_prefetch_to_serve_ready).Run(nullptr); |
| return; |
| } |
| |
| if (prefetch_container->GetRedirectChainSize() > 1 && |
| !base::FeatureList::IsEnabled(features::kPrefetchRedirects)) { |
| std::move(on_prefetch_to_serve_ready).Run(nullptr); |
| return; |
| } |
| |
| if (prefetch_container->IsPrefetchServable(PrefetchCacheableDuration())) { |
| DVLOG(1) << "PrefetchService::GetPrefetchToServe(" << url |
| << "): PrefetchContainer is servable"; |
| prefetch_container->OnGetPrefetchToServe(/*blocked_until_head=*/false); |
| ReturnPrefetchToServe(prefetch_container->GetWeakPtr(), |
| std::move(on_prefetch_to_serve_ready)); |
| return; |
| } |
| |
| if (prefetch_container->ShouldBlockUntilHeadReceived()) { |
| DVLOG(1) << "PrefetchService::GetPrefetchToServe(" << url |
| << "): PrefetchContainer is blocked until head"; |
| prefetch_container->OnGetPrefetchToServe(/*blocked_until_head=*/true); |
| prefetch_container->GetLastStreamingURLLoader()->SetOnReceivedHeadCallback( |
| base::BindOnce(&PrefetchService::WaitOnPrefetchToServeHead, |
| weak_method_factory_.GetWeakPtr(), key, |
| prefetch_container->GetWeakPtr(), |
| std::move(on_prefetch_to_serve_ready))); |
| |
| base::TimeDelta block_until_head_timeout = PrefetchBlockUntilHeadTimeout( |
| prefetch_container->GetPrefetchType().GetEagerness()); |
| VLOG(0) << "PS::GetPrefetchToServe; block_until_head_timeout = " |
| << block_until_head_timeout; |
| if (block_until_head_timeout.is_positive()) { |
| std::unique_ptr<base::OneShotTimer> block_until_head_timer = |
| std::make_unique<base::OneShotTimer>(); |
| block_until_head_timer->Start( |
| FROM_HERE, block_until_head_timeout, |
| base::BindOnce(&BlockUntilHeadTimeoutHelper, |
| prefetch_container->GetWeakPtr())); |
| prefetch_container->TakeBlockUntilHeadTimer( |
| std::move(block_until_head_timer)); |
| } |
| |
| return; |
| } |
| |
| DVLOG(1) << "PrefetchService::GetPrefetchToServe(" << url |
| << "): PrefetchContainer is not servable"; |
| prefetch_container->OnReturnPrefetchToServe(/*served=*/false); |
| std::move(on_prefetch_to_serve_ready).Run(nullptr); |
| } |
| |
| void PrefetchService::WaitOnPrefetchToServeHead( |
| const PrefetchContainer::Key& key, |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| OnPrefetchToServeReady on_prefetch_to_serve_ready) { |
| const GURL& nav_url = key.second; |
| if (!prefetch_container) { |
| ReturnPrefetchToServe(nullptr, std::move(on_prefetch_to_serve_ready)); |
| return; |
| } |
| |
| prefetch_container->ResetBlockUntilHeadTimer(); |
| |
| if (!prefetch_container->IsPrefetchServable(PrefetchCacheableDuration())) { |
| prefetch_container->OnReturnPrefetchToServe(/*served=*/false); |
| ReturnPrefetchToServe(nullptr, std::move(on_prefetch_to_serve_ready)); |
| return; |
| } |
| |
| if (nav_url == prefetch_container->GetURL()) { |
| PrepareToServe(nav_url, prefetch_container); |
| GetPrefetchToServe(key, std::move(on_prefetch_to_serve_ready)); |
| return; |
| } |
| |
| if (const auto* head = prefetch_container->GetHead()) { |
| if (!head->parsed_headers || |
| !head->parsed_headers->no_vary_search_with_parse_error || |
| head->parsed_headers->no_vary_search_with_parse_error |
| ->is_parse_error()) { |
| // is_parse_error() == true includes the case where the header is |
| // not there (kOk) and the case where the header is equivalent |
| // to default behavior (exactly match URL - kDefaultValue) |
| prefetch_container->OnReturnPrefetchToServe(/*served=*/false); |
| prefetch_container->UpdateServingPageMetrics(); |
| ReturnPrefetchToServe(nullptr, std::move(on_prefetch_to_serve_ready)); |
| return; |
| } |
| auto no_vary_search_data = |
| no_vary_search::ParseHttpNoVarySearchDataFromMojom( |
| head->parsed_headers->no_vary_search_with_parse_error |
| ->get_no_vary_search()); |
| if (!no_vary_search_data.AreEquivalent(nav_url, |
| prefetch_container->GetURL())) { |
| prefetch_container->OnReturnPrefetchToServe(/*served=*/false); |
| prefetch_container->UpdateServingPageMetrics(); |
| ReturnPrefetchToServe(nullptr, std::move(on_prefetch_to_serve_ready)); |
| return; |
| } |
| DVLOG(1) << "PrefetchService::WaitOnPrefetchToServeHead::" |
| << "url = " << nav_url << "::" |
| << "matches by NVS header the prefetch " |
| << prefetch_container->GetURL(); |
| if (auto attempt = prefetch_container->preloading_attempt()) { |
| // Before No-Vary-Search hint, the decision to use a prefetched response |
| // was made in `DidStartNavigation`. `SetIsAccurateTriggering` is called |
| // by `PreloadingDataImpl::DidStartNavigation`. With No-Vary-Search |
| // hint the decision to use an in-flight prefetched response is |
| // delayed until the headers are received from the server. This |
| // happens after `DidStartNavigation`. At this point in the code we |
| // have already decided we are going to use the prefetch, so we can |
| // safely call `SetIsAccurateTriggering`. |
| static_cast<PreloadingAttemptImpl*>(attempt.get()) |
| ->SetIsAccurateTriggering(nav_url); |
| } |
| PrepareToServe(nav_url, prefetch_container); |
| GetPrefetchToServe(key, std::move(on_prefetch_to_serve_ready)); |
| } |
| } |
| |
| void PrefetchService::ReturnPrefetchToServe( |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| OnPrefetchToServeReady on_prefetch_to_serve_ready) { |
| if (prefetch_container) { |
| prefetch_container->UpdateServingPageMetrics(); |
| } |
| |
| if (!prefetch_container || |
| !prefetch_container->IsPrefetchServable(PrefetchCacheableDuration())) { |
| if (prefetch_container) { |
| prefetch_container->OnReturnPrefetchToServe(/*served=*/false); |
| } |
| std::move(on_prefetch_to_serve_ready).Run(nullptr); |
| return; |
| } |
| |
| if (prefetch_container->GetReader().HaveDefaultContextCookiesChanged()) { |
| prefetch_container->SetPrefetchStatus( |
| PrefetchStatus::kPrefetchNotUsedCookiesChanged); |
| prefetch_container->UpdateServingPageMetrics(); |
| prefetch_container->OnReturnPrefetchToServe(/*served=*/false); |
| std::move(on_prefetch_to_serve_ready).Run(nullptr); |
| return; |
| } |
| |
| if (!prefetch_container->GetReader().HasIsolatedCookieCopyStarted()) { |
| CopyIsolatedCookies(prefetch_container); |
| } |
| |
| prefetch_container->OnReturnPrefetchToServe(/*served=*/true); |
| std::move(on_prefetch_to_serve_ready).Run(prefetch_container); |
| return; |
| } |
| |
| // 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; |
| } |
| |
| // static |
| void PrefetchService::SetNetworkContextForProxyLookupForTesting( |
| network::mojom::NetworkContext* network_context) { |
| g_network_context_for_proxy_lookup_for_testing = network_context; |
| } |
| |
| void PrefetchService::RecordExistingPrefetchWithMatchingURL( |
| base::WeakPtr<PrefetchContainer> prefetch_container) const { |
| bool matching_prefetch = false; |
| int num_matching_prefetches = 0; |
| |
| int num_matching_eligible_prefetch = 0; |
| int num_matching_servable_prefetch = 0; |
| int num_matching_prefetch_same_referrer = 0; |
| int num_matching_prefetch_same_rfh = 0; |
| |
| for (const auto& prefetch_iter : all_prefetches_) { |
| if (prefetch_iter.second && |
| prefetch_iter.second->GetURL() == prefetch_container->GetURL()) { |
| matching_prefetch = true; |
| num_matching_prefetches++; |
| |
| if (prefetch_iter.second->IsInitialPrefetchEligible()) { |
| num_matching_eligible_prefetch++; |
| } |
| |
| if (prefetch_iter.second->IsPrefetchServable( |
| PrefetchCacheableDuration()) && |
| !prefetch_iter.second->HasPrefetchBeenConsideredToServe()) { |
| num_matching_servable_prefetch++; |
| } |
| |
| if (prefetch_iter.second->GetReferrer().url == |
| prefetch_container->GetReferrer().url) { |
| num_matching_prefetch_same_referrer++; |
| } |
| |
| if (prefetch_iter.second->GetReferringRenderFrameHostId() == |
| prefetch_container->GetReferringRenderFrameHostId()) { |
| num_matching_prefetch_same_rfh++; |
| } |
| } |
| } |
| |
| base::UmaHistogramBoolean( |
| "PrefetchProxy.Prefetch.ExistingPrefetchWithMatchingURL", |
| matching_prefetch); |
| base::UmaHistogramCounts100( |
| "PrefetchProxy.Prefetch.NumExistingPrefetchWithMatchingURL", |
| num_matching_prefetches); |
| |
| if (matching_prefetch) { |
| base::UmaHistogramCounts100( |
| "PrefetchProxy.Prefetch.NumExistingEligiblePrefetchWithMatchingURL", |
| num_matching_eligible_prefetch); |
| base::UmaHistogramCounts100( |
| "PrefetchProxy.Prefetch.NumExistingServablePrefetchWithMatchingURL", |
| num_matching_servable_prefetch); |
| base::UmaHistogramCounts100( |
| "PrefetchProxy.Prefetch.NumExistingPrefetchWithMatchingURLAndReferrer", |
| num_matching_prefetch_same_referrer); |
| base::UmaHistogramCounts100( |
| "PrefetchProxy.Prefetch." |
| "NumExistingPrefetchWithMatchingURLAndRenderFrameHost", |
| num_matching_prefetch_same_rfh); |
| } |
| } |
| |
| } // namespace content |