| // 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 <optional> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/auto_reset.h" |
| #include "base/barrier_closure.h" |
| #include "base/containers/fixed_flat_set.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/devtools/render_frame_devtools_agent_host.h" |
| #include "content/browser/preloading/prefetch/prefetch_document_manager.h" |
| #include "content/browser/preloading/prefetch/prefetch_features.h" |
| #include "content/browser/preloading/prefetch/prefetch_match_resolver.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_request_headers.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/devtools_observer_util.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/devtools_observer.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(PreloadingEligibility eligibility) { |
| switch (eligibility) { |
| case PreloadingEligibility::kUserHasCookies: |
| case PreloadingEligibility::kUserHasServiceWorker: |
| // If the prefetch is not eligible because of cookie or a service worker, |
| // then maybe send a decoy. |
| return true; |
| case PreloadingEligibility::kBatterySaverEnabled: |
| case PreloadingEligibility::kBrowserContextOffTheRecord: |
| case PreloadingEligibility::kDataSaverEnabled: |
| case PreloadingEligibility::kExistingProxy: |
| case PreloadingEligibility::kHostIsNonUnique: |
| case PreloadingEligibility::kNonDefaultStoragePartition: |
| case PreloadingEligibility::kPrefetchProxyNotAvailable: |
| case PreloadingEligibility::kPreloadingDisabled: |
| case PreloadingEligibility::kRetryAfter: |
| case PreloadingEligibility::kSameSiteCrossOriginPrefetchRequiredProxy: |
| case PreloadingEligibility::kSchemeIsNotHttps: |
| // These statuses don't relate to any user state, so don't send a decoy |
| // request. |
| return false; |
| case PreloadingEligibility::kEligible: |
| default: |
| // Other ineligible cases are not used in `PrefetchService`. |
| 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 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; |
| } |
| |
| // Normally CheckIfShouldHoldback() computes the holdback status based on |
| // PreloadingConfig. In special cases, we call SetHoldbackOverride() to |
| // override that processing. |
| RenderFrameHostImpl* initiator_rfh = RenderFrameHostImpl::FromID( |
| prefetch_container->GetReferringRenderFrameHostId()); |
| bool devtools_client_exist = |
| initiator_rfh && |
| RenderFrameDevToolsAgentHost::GetFor(initiator_rfh) != nullptr; |
| if (devtools_client_exist) { |
| prefetch_container->preloading_attempt()->SetHoldbackStatus( |
| PreloadingHoldbackStatus::kAllowed); |
| } |
| |
| if (prefetch_container->preloading_attempt()->ShouldHoldback()) { |
| prefetch_container->SetLoadState( |
| PrefetchContainer::LoadState::kFailedHeldback); |
| 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(PrefetchContainer::Reader reader) { |
| if (reader) { |
| reader.OnIsolatedCookieCopyComplete(); |
| } |
| } |
| |
| void BlockUntilHeadTimeoutHelper( |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| base::WeakPtr<PrefetchMatchResolver> prefetch_match_resolver) { |
| if (!prefetch_match_resolver) { |
| // This means the navigation that was waiting for this prefetch_container |
| // has been aborted and we don't have to do anything. |
| return; |
| } |
| |
| if (!prefetch_container) { |
| return; |
| } |
| |
| // Takes the on_received_head_callback |
| base::OnceClosure on_received_head_callback = |
| prefetch_container->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 |
| 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; |
| |
| void PrefetchService::SetPrefetchServiceDelegateForTesting( |
| std::unique_ptr<PrefetchServiceDelegate> delegate) { |
| DCHECK(!delegate_); |
| delegate_ = std::move(delegate); |
| } |
| |
| PrefetchOriginProber* PrefetchService::GetPrefetchOriginProber() const { |
| return origin_prober_.get(); |
| } |
| |
| void PrefetchService::AddPrefetchContainerWithoutStartingPrefetch( |
| std::unique_ptr<PrefetchContainer> owned_prefetch_container) { |
| base::WeakPtr<PrefetchContainer> prefetch_container = |
| owned_prefetch_container->GetWeakPtr(); |
| DCHECK(prefetch_container); |
| auto prefetch_container_key = prefetch_container->GetPrefetchContainerKey(); |
| |
| RecordExistingPrefetchWithMatchingURL(prefetch_container); |
| |
| // A newly submitted prefetch could already be in |owned_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 |owned_prefetches_|. |
| auto prefetch_iter = owned_prefetches_.find(prefetch_container_key); |
| if (prefetch_iter != owned_prefetches_.end()) { |
| ResetPrefetch(prefetch_iter->second->GetWeakPtr()); |
| } |
| |
| owned_prefetches_[prefetch_container_key] = |
| std::move(owned_prefetch_container); |
| } |
| |
| void PrefetchService::AddPrefetchContainer( |
| std::unique_ptr<PrefetchContainer> owned_prefetch_container) { |
| base::WeakPtr<PrefetchContainer> prefetch_container = |
| owned_prefetch_container->GetWeakPtr(); |
| AddPrefetchContainerWithoutStartingPrefetch( |
| std::move(owned_prefetch_container)); |
| PrefetchUrl(std::move(prefetch_container)); |
| } |
| |
| void PrefetchService::PrefetchUrl( |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| if (delegate_) { |
| // If pre* actions are disabled then don't prefetch. |
| switch (delegate_->IsSomePreloadingEnabled()) { |
| case PreloadingEligibility::kEligible: |
| break; |
| case PreloadingEligibility::kDataSaverEnabled: |
| OnGotEligibilityResult(prefetch_container, |
| PreloadingEligibility::kDataSaverEnabled); |
| return; |
| case PreloadingEligibility::kBatterySaverEnabled: |
| OnGotEligibilityResult(prefetch_container, |
| PreloadingEligibility::kBatterySaverEnabled); |
| return; |
| case PreloadingEligibility::kPreloadingDisabled: |
| OnGotEligibilityResult(prefetch_container, |
| PreloadingEligibility::kPreloadingDisabled); |
| return; |
| default: |
| DVLOG(1) << *prefetch_container |
| << ": not prefetched (PrefetchServiceDelegate)"; |
| return; |
| } |
| |
| const auto& prefetch_type = prefetch_container->GetPrefetchType(); |
| if (prefetch_type.IsProxyRequiredWhenCrossOrigin()) { |
| bool allow_all_domains = |
| PrefetchAllowAllDomains() || |
| (PrefetchAllowAllDomainsForExtendedPreloading() && |
| delegate_->IsExtendedPreloadingEnabled()); |
| if (!allow_all_domains && |
| !delegate_->IsDomainInPrefetchAllowList( |
| prefetch_container->GetReferringOrigin().GetURL())) { |
| DVLOG(1) << *prefetch_container |
| << ": not prefetched (not in allow list)"; |
| return; |
| } |
| } |
| |
| delegate_->OnPrefetchLikely(WebContents::FromRenderFrameHost( |
| &prefetch_container->GetPrefetchDocumentManager() |
| ->render_frame_host())); |
| } |
| |
| 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 { |
| CHECK(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, |
| PreloadingEligibility::kBrowserContextOffTheRecord); |
| 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. |
| // |
| // Conditions on the outer-most if block: |
| // Host-uniqueness check is only applied to proxied prefetches, where that |
| // matters. Also, we bypass the check for the test hosts, since we run the |
| // test web servers on the localhost or private networks, where the check |
| // fails. |
| if (prefetch_container->IsProxyRequiredForURL(url) && |
| !ShouldPrefetchBypassProxyForTestHost(url.host())) { |
| bool is_host_non_unique = |
| g_host_non_unique_filter |
| ? g_host_non_unique_filter(url.HostNoBrackets()) |
| : net::IsHostnameNonUnique(url.HostNoBrackets()); |
| if (is_host_non_unique) { |
| std::move(result_callback) |
| .Run(prefetch_container, PreloadingEligibility::kHostIsNonUnique); |
| 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, PreloadingEligibility::kSchemeIsNotHttps); |
| return; |
| } |
| |
| // Fail the prefetch (or more precisely, PrefetchContainer::SinglePrefetch) |
| // early if it is going to go through a proxy, and we know that it is not |
| // available. |
| if (prefetch_container->IsProxyRequiredForURL(url) && |
| !ShouldPrefetchBypassProxyForTestHost(url.host()) && |
| (!prefetch_proxy_configurator_ || |
| !prefetch_proxy_configurator_->IsPrefetchProxyAvailable())) { |
| std::move(result_callback) |
| .Run(prefetch_container, |
| PreloadingEligibility::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, |
| PreloadingEligibility::kNonDefaultStoragePartition); |
| 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, PreloadingEligibility::kRetryAfter); |
| return; |
| } |
| |
| CheckHasServiceWorker(url, prefetch_container, std::move(result_callback)); |
| } |
| |
| void PrefetchService::CheckHasServiceWorker( |
| const GURL& url, |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| OnEligibilityResultCallback result_callback) const { |
| CHECK(prefetch_container); |
| // 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(); |
| CHECK(service_worker_context); |
| auto key = blink::StorageKey::CreateFirstParty(url::Origin::Create(url)); |
| // Check `MaybeHasRegistrationForStorageKey` first as it is much faster than |
| // calling `CheckHasServiceWorker`. |
| auto has_registration_for_storage_key = |
| service_worker_context->MaybeHasRegistrationForStorageKey(key); |
| if (prefetch_container->HasPreloadingAttempt()) { |
| auto* preloading_attempt = static_cast<PreloadingAttemptImpl*>( |
| prefetch_container->preloading_attempt().get()); |
| CHECK(preloading_attempt); |
| preloading_attempt->SetServiceWorkerRegisteredCheck( |
| has_registration_for_storage_key |
| ? PreloadingAttemptImpl::ServiceWorkerRegisteredCheck::kPath |
| : PreloadingAttemptImpl::ServiceWorkerRegisteredCheck::kOriginOnly); |
| } |
| if (!has_registration_for_storage_key) { |
| OnGotServiceWorkerResult(url, std::move(prefetch_container), |
| base::Time::Now(), std::move(result_callback), |
| ServiceWorkerCapability::NO_SERVICE_WORKER); |
| return; |
| } |
| // Start recording here the start of the check for Service Worker registration |
| // for url. |
| service_worker_context->CheckHasServiceWorker( |
| url, key, |
| base::BindOnce(&PrefetchService::OnGotServiceWorkerResult, |
| weak_method_factory_.GetWeakPtr(), url, |
| std::move(prefetch_container), base::Time::Now(), |
| std::move(result_callback))); |
| } |
| |
| void PrefetchService::OnGotServiceWorkerResult( |
| const GURL& url, |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| base::Time check_has_service_worker_start_time, |
| OnEligibilityResultCallback result_callback, |
| ServiceWorkerCapability service_worker_capability) const { |
| if (!prefetch_container) { |
| std::move(result_callback).Run(nullptr, PreloadingEligibility::kEligible); |
| return; |
| } |
| CHECK(prefetch_container); |
| if (prefetch_container->HasPreloadingAttempt()) { |
| const auto duration = |
| base::Time::Now() - check_has_service_worker_start_time; |
| auto* preloading_attempt = static_cast<PreloadingAttemptImpl*>( |
| prefetch_container->preloading_attempt().get()); |
| CHECK(preloading_attempt); |
| preloading_attempt->SetServiceWorkerRegisteredCheckDuration(duration); |
| } |
| switch (service_worker_capability) { |
| case ServiceWorkerCapability::NO_SERVICE_WORKER: |
| case ServiceWorkerCapability::SERVICE_WORKER_NO_FETCH_HANDLER: |
| break; |
| case ServiceWorkerCapability::SERVICE_WORKER_WITH_FETCH_HANDLER: |
| std::move(result_callback) |
| .Run(std::move(prefetch_container), |
| PreloadingEligibility::kUserHasServiceWorker); |
| 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(std::move(prefetch_container), |
| PreloadingEligibility::kSameSiteCrossOriginPrefetchRequiredProxy); |
| 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(std::move(prefetch_container), PreloadingEligibility::kEligible); |
| return; |
| } |
| |
| StoragePartition* default_storage_partition = |
| browser_context_->GetDefaultStoragePartition(); |
| CHECK(default_storage_partition); |
| 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, |
| std::move(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(nullptr, PreloadingEligibility::kEligible); |
| return; |
| } |
| |
| if (!cookie_list.empty()) { |
| std::move(result_callback) |
| .Run(prefetch_container, PreloadingEligibility::kUserHasCookies); |
| return; |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| features::kPrefetchStateContaminationMitigation)) { |
| // The cookie eligibility check just happened, and we might proceed anyway. |
| // We might therefore need to delay further processing to the extent |
| // required to obscure the outcome of this check from the current site. |
| auto* initiator_rfh = RenderFrameHost::FromID( |
| prefetch_container->GetReferringRenderFrameHostId()); |
| const bool is_contamination_exempt = |
| delegate_ && initiator_rfh && |
| delegate_->IsContaminationExempt(initiator_rfh->GetLastCommittedURL()); |
| if (!is_contamination_exempt) { |
| prefetch_container->MarkCrossSiteContaminated(); |
| } |
| } |
| |
| // 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, PreloadingEligibility::kUserHasCookies); |
| 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, PreloadingEligibility::kEligible); |
| 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(nullptr, PreloadingEligibility::kEligible); |
| return; |
| } |
| |
| prefetch_container->ReleaseProxyLookupClient(); |
| if (has_proxy) { |
| std::move(result_callback) |
| .Run(prefetch_container, PreloadingEligibility::kExistingProxy); |
| return; |
| } |
| std::move(result_callback) |
| .Run(prefetch_container, PreloadingEligibility::kEligible); |
| } |
| |
| void PrefetchService::OnGotEligibilityResult( |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| PreloadingEligibility eligibility) { |
| if (!prefetch_container) { |
| return; |
| } |
| |
| const bool eligible = eligibility == PreloadingEligibility::kEligible; |
| bool is_decoy = false; |
| if (!eligible) { |
| is_decoy = |
| prefetch_container->IsProxyRequiredForURL( |
| prefetch_container->GetURL()) && |
| ShouldConsiderDecoyRequestForStatus(eligibility) && |
| 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( |
| PreloadingEligibility::kEligible); |
| } else { |
| prefetch_container->OnEligibilityCheckComplete(eligibility); |
| } |
| |
| if (!eligible && !is_decoy) { |
| DVLOG(1) |
| << *prefetch_container |
| << ": not prefetched (not eligible nor decoy. PreloadingEligibility=" |
| << static_cast<int>(eligibility) << ")"; |
| 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_container| 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, |
| PreloadingEligibility eligibility) { |
| if (!prefetch_container) { |
| return; |
| } |
| |
| const bool eligible = eligibility == PreloadingEligibility::kEligible; |
| RecordRedirectResult(eligible |
| ? PrefetchRedirectResult::kSuccessRedirectFollowed |
| : PrefetchRedirectResult::kFailedIneligible); |
| |
| // If the redirect is ineligible, the prefetch may change into a decoy. |
| bool is_decoy = false; |
| if (!eligible) { |
| is_decoy = |
| prefetch_container->IsProxyRequiredForURL(redirect_info.new_url) && |
| ShouldConsiderDecoyRequestForStatus(eligibility) && |
| 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( |
| PreloadingEligibility::kEligible); |
| } else { |
| prefetch_container->OnEligibilityCheckComplete(eligibility); |
| 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->GetStreamingURLLoader()->HandleRedirect( |
| PrefetchRedirectStatus::kFail, redirect_info, std::move(redirect_head)); |
| |
| Prefetch(); |
| |
| return; |
| } |
| |
| const auto& devtools_observer = prefetch_container->GetDevToolsObserver(); |
| if (devtools_observer && !prefetch_container->IsDecoy()) { |
| GURL previous_url = prefetch_container->GetPreviousURL(); |
| auto redirect_head_info = network::ExtractDevToolsInfo(*redirect_head); |
| std::pair<const GURL&, const network::mojom::URLResponseHeadDevToolsInfo&> |
| redirect_info_for_devtools{previous_url, *redirect_head_info}; |
| devtools_observer->OnStartSinglePrefetch( |
| prefetch_container->RequestId(), |
| *prefetch_container->GetResourceRequest(), |
| std::move(redirect_info_for_devtools)); |
| } |
| |
| // 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->GetStreamingURLLoader()->HandleRedirect( |
| PrefetchRedirectStatus::kSwitchNetworkContext, redirect_info, |
| std::move(redirect_head)); |
| // The new ResponseReader is associated with the new streaming URL loader at |
| // the PrefetchStreamingURLLoader constructor. |
| SendPrefetchRequest(prefetch_container); |
| |
| return; |
| } |
| |
| // Otherwise, follow the redirect in the same streaming URL loader. |
| prefetch_container->GetStreamingURLLoader()->HandleRedirect( |
| PrefetchRedirectStatus::kFollow, redirect_info, std::move(redirect_head)); |
| // Associate the new ResponseReader with the current streaming URL loader. |
| prefetch_container->GetStreamingURLLoader()->SetResponseReader( |
| prefetch_container->GetResponseReaderForCurrentPrefetch()); |
| } |
| |
| void PrefetchService::Prefetch() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Asserts that re-entrancy doesn't happen. |
| #if DCHECK_IS_ON() |
| DCHECK(!prefetch_reentrancy_guard_); |
| base::AutoReset reset_guard(&prefetch_reentrancy_guard_, true); |
| #endif |
| |
| if (PrefetchCloseIdleSockets()) { |
| for (const auto& iter : owned_prefetches_) { |
| if (iter.second) { |
| iter.second->CloseIdleConnections(); |
| } |
| } |
| } |
| |
| base::WeakPtr<PrefetchContainer> next_prefetch = nullptr; |
| base::WeakPtr<PrefetchContainer> prefetch_to_evict = nullptr; |
| while ((std::tie(next_prefetch, prefetch_to_evict) = |
| PopNextPrefetchContainer()) != |
| std::make_tuple(nullptr, nullptr)) { |
| StartSinglePrefetch(next_prefetch, prefetch_to_evict); |
| } |
| } |
| |
| std::tuple<base::WeakPtr<PrefetchContainer>, 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()) { |
| DVLOG(1) << "PrefetchService::PopNextPrefetchContainer: maximum number of " |
| "concurrent prefetches reached! " |
| << "The max number of concurrent prefetches is: " |
| << PrefetchServiceMaximumNumberOfConcurrentPrefetches() << ". " |
| << "If you need to have more concurrent prefetches change " |
| "kPrefetchUseContentRefactor.max_concurrent_prefetches."; |
| return std::make_tuple(nullptr, nullptr); |
| } |
| |
| base::WeakPtr<PrefetchContainer> prefetch_to_evict; |
| // Get the first prefetch can be prefetched currently. This depends on the |
| // state of the initiating document, and the number of completed prefetches |
| // (this can also result in previously completed prefetches being evicted). |
| auto prefetch_iter = base::ranges::find_if( |
| prefetch_queue_, |
| [&](const base::WeakPtr<PrefetchContainer>& prefetch_container) { |
| bool can_prefetch_now = false; |
| std::tie(can_prefetch_now, prefetch_to_evict) = |
| prefetch_container->GetPrefetchDocumentManager()->CanPrefetchNow( |
| prefetch_container.get()); |
| // |prefetch_to_evict| should only be set if |can_prefetch_now| is true. |
| DCHECK(!prefetch_to_evict || can_prefetch_now); |
| return can_prefetch_now; |
| }); |
| if (prefetch_iter == prefetch_queue_.end()) { |
| return std::make_tuple(nullptr, nullptr); |
| } |
| |
| base::WeakPtr<PrefetchContainer> next_prefetch_container = *prefetch_iter; |
| prefetch_queue_.erase(prefetch_iter); |
| return std::make_tuple(next_prefetch_container, prefetch_to_evict); |
| } |
| |
| void PrefetchService::OnPrefetchTimeout( |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| prefetch_container->SetPrefetchStatus(PrefetchStatus::kPrefetchIsStale); |
| ResetPrefetch(prefetch_container); |
| |
| if (PrefetchNewLimitsEnabled() && |
| active_prefetches_.size() < |
| PrefetchServiceMaximumNumberOfConcurrentPrefetches()) { |
| Prefetch(); |
| } |
| } |
| |
| void PrefetchService::ResetPrefetch( |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| CHECK(prefetch_container); |
| auto it = |
| owned_prefetches_.find(prefetch_container->GetPrefetchContainerKey()); |
| CHECK(it != owned_prefetches_.end()); |
| CHECK_EQ(it->second.get(), prefetch_container.get()); |
| |
| auto active_prefetch_iter = |
| active_prefetches_.find(prefetch_container->GetPrefetchContainerKey()); |
| if (active_prefetch_iter != active_prefetches_.end()) { |
| active_prefetches_.erase(active_prefetch_iter); |
| } |
| |
| owned_prefetches_.erase(it); |
| } |
| |
| void PrefetchService::OnCandidatesUpdated() { |
| if (active_prefetches_.size() < |
| PrefetchServiceMaximumNumberOfConcurrentPrefetches()) { |
| Prefetch(); |
| } |
| } |
| |
| void PrefetchService::StartSinglePrefetch( |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| base::WeakPtr<PrefetchContainer> prefetch_to_evict) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(prefetch_container); |
| CHECK_EQ(prefetch_container->GetLoadState(), |
| PrefetchContainer::LoadState::kEligible); |
| |
| // 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; |
| } |
| |
| prefetch_container->SetLoadState(PrefetchContainer::LoadState::kStarted); |
| |
| // Start timer to release the prefetch container after |
| // |PrefetchContainerLifetimeInPrefetchService|. |
| base::TimeDelta reset_delta = PrefetchContainerLifetimeInPrefetchService(); |
| if (reset_delta.is_positive()) { |
| prefetch_container->StartTimeoutTimer( |
| PrefetchContainerLifetimeInPrefetchService(), |
| base::BindOnce(&PrefetchService::OnPrefetchTimeout, |
| weak_method_factory_.GetWeakPtr(), prefetch_container)); |
| } |
| |
| const bool is_above_limit = |
| !PrefetchNewLimitsEnabled() && |
| prefetch_container->GetPrefetchDocumentManager() |
| ->GetNumberOfPrefetchRequestAttempted() >= |
| PrefetchServiceMaximumNumberOfPrefetchesPerPage().value_or( |
| std::numeric_limits<int>::max()); |
| if (is_above_limit) { |
| prefetch_container->SetPrefetchStatus( |
| PrefetchStatus::kPrefetchFailedPerPageLimitExceeded); |
| ResetPrefetch(prefetch_container); |
| return; |
| } |
| |
| if (prefetch_to_evict) { |
| DCHECK(PrefetchNewLimitsEnabled()); |
| prefetch_to_evict->SetPrefetchStatus( |
| PrefetchStatus::kPrefetchEvictedForNewerPrefetch); |
| ResetPrefetch(prefetch_to_evict); |
| } |
| |
| active_prefetches_.insert(prefetch_container->GetPrefetchContainerKey()); |
| |
| 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); |
| } |
| |
| net::HttpRequestHeaders additional_headers; |
| additional_headers.SetHeader( |
| net::HttpRequestHeaders::kAccept, |
| FrameAcceptHeaderValue(/*allow_sxg_responses=*/true, browser_context_)); |
| prefetch_container->MakeResourceRequest(additional_headers); |
| |
| const auto& devtools_observer = prefetch_container->GetDevToolsObserver(); |
| if (devtools_observer && !prefetch_container->IsDecoy()) { |
| devtools_observer->OnStartSinglePrefetch( |
| prefetch_container->RequestId(), |
| *prefetch_container->GetResourceRequest(), std::nullopt); |
| } |
| |
| SendPrefetchRequest(prefetch_container); |
| |
| 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::SendPrefetchRequest( |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| 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." |
| })"); |
| |
| auto streaming_loader = PrefetchStreamingURLLoader::CreateAndStart( |
| GetURLLoaderFactoryForCurrentPrefetch(prefetch_container), |
| *prefetch_container->GetResourceRequest(), 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), |
| base::BindOnce(&PrefetchContainer::OnReceivedHead, prefetch_container), |
| prefetch_container->GetResponseReaderForCurrentPrefetch()); |
| prefetch_container->SetStreamingURLLoader(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() |
| ->GetURLLoaderFactory(this); |
| } |
| |
| 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()); |
| |
| // Update the prefetch's referrer in case a redirect requires a change in |
| // network context and a new request needs to be started. |
| const auto new_referrer_policy = |
| blink::ReferrerUtils::NetToMojoReferrerPolicy( |
| redirect_info.new_referrer_policy); |
| |
| std::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 (net::SchemefulSite(prefetch_container->GetCurrentURL()) != |
| net::SchemefulSite(redirect_info.new_url) && |
| !IsReferrerPolicySufficientlyStrict(new_referrer_policy)) { |
| // The new referrer policy is not sufficiently strict to allow cross-site |
| // redirects. |
| failure = PrefetchRedirectResult::kFailedInsufficientReferrerPolicy; |
| } |
| |
| if (failure) { |
| active_prefetches_.erase(prefetch_container->GetPrefetchContainerKey()); |
| prefetch_container->SetPrefetchStatus( |
| PrefetchStatus::kPrefetchFailedInvalidRedirect); |
| prefetch_container->GetStreamingURLLoader()->HandleRedirect( |
| PrefetchRedirectStatus::kFail, redirect_info, std::move(redirect_head)); |
| |
| Prefetch(); |
| RecordRedirectResult(*failure); |
| return; |
| } |
| |
| prefetch_container->AddRedirectHop(redirect_info); |
| prefetch_container->UpdateReferrer(GURL(redirect_info.new_referrer), |
| new_referrer_policy); |
| |
| 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))); |
| } |
| |
| std::optional<PrefetchErrorOnResponseReceived> |
| 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 PrefetchErrorOnResponseReceived::kPrefetchWasDecoy; |
| } |
| |
| if (!head) { |
| return PrefetchErrorOnResponseReceived::kFailedInvalidHead; |
| } |
| |
| if (prefetch_container && prefetch_container->IsCrossSiteContaminated()) { |
| head->is_prefetch_with_cross_site_contamination = true; |
| } |
| |
| const auto& devtools_observer = prefetch_container->GetDevToolsObserver(); |
| if (devtools_observer) { |
| devtools_observer->OnPrefetchResponseReceived( |
| prefetch_container->GetCurrentURL(), prefetch_container->RequestId(), |
| *head); |
| } |
| |
| if (!head->headers) { |
| return PrefetchErrorOnResponseReceived::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. |
| static constexpr base::TimeDelta max_retry_after = base::Days(7); |
| if (retry_after > max_retry_after) { |
| retry_after = max_retry_after; |
| } |
| |
| delegate_->ReportOriginRetryAfter(prefetch_container->GetURL(), |
| retry_after); |
| } |
| } |
| return PrefetchErrorOnResponseReceived::kFailedNon2XX; |
| } |
| |
| if (PrefetchServiceHTMLOnly() && head->mime_type != "text/html") { |
| prefetch_container->SetPrefetchStatus( |
| PrefetchStatus::kPrefetchFailedMIMENotSupported); |
| return PrefetchErrorOnResponseReceived::kFailedMIMENotSupported; |
| } |
| |
| return std::nullopt; |
| } |
| |
| void PrefetchService::OnPrefetchResponseCompleted( |
| base::WeakPtr<PrefetchContainer> prefetch_container, |
| const network::URLLoaderCompletionStatus& completion_status) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| DVLOG(1) << "PrefetchService::OnPrefetchResponseCompleted"; |
| if (!prefetch_container) { |
| return; |
| } |
| |
| DCHECK( |
| active_prefetches_.find(prefetch_container->GetPrefetchContainerKey()) != |
| active_prefetches_.end()); |
| active_prefetches_.erase(prefetch_container->GetPrefetchContainerKey()); |
| |
| prefetch_container->OnPrefetchComplete(completion_status); |
| |
| if (on_prefetch_response_completed_for_testing_) { |
| on_prefetch_response_completed_for_testing_.Run(prefetch_container); |
| } |
| |
| Prefetch(); |
| } |
| |
| void PrefetchService::CopyIsolatedCookies( |
| const PrefetchContainer::Reader& reader) { |
| DCHECK(reader); |
| |
| if (!reader.GetCurrentNetworkContextToServe()) { |
| // Not set in unit tests. |
| return; |
| } |
| |
| // We only need to copy cookies if the prefetch used an isolated network |
| // context. |
| if (!reader.IsIsolatedNetworkContextRequiredToServe()) { |
| return; |
| } |
| |
| reader.OnIsolatedCookieCopyStart(); |
| net::CookieOptions options = net::CookieOptions::MakeAllInclusive(); |
| reader.GetCurrentNetworkContextToServe()->GetCookieManager()->GetCookieList( |
| reader.GetCurrentURLToServe(), options, |
| net::CookiePartitionKeyCollection::Todo(), |
| base::BindOnce(&PrefetchService::OnGotIsolatedCookiesForCopy, |
| weak_method_factory_.GetWeakPtr(), reader.Clone())); |
| } |
| |
| void PrefetchService::OnGotIsolatedCookiesForCopy( |
| PrefetchContainer::Reader reader, |
| const net::CookieAccessResultList& cookie_list, |
| const net::CookieAccessResultList& excluded_cookies) { |
| reader.OnIsolatedCookiesReadCompleteAndWriteStart(); |
| RecordPrefetchProxyPrefetchMainframeCookiesToCopy(cookie_list.size()); |
| |
| if (cookie_list.empty()) { |
| reader.OnIsolatedCookieCopyComplete(); |
| return; |
| } |
| |
| const auto current_url = reader.GetCurrentURLToServe(); |
| |
| base::RepeatingClosure barrier = base::BarrierClosure( |
| cookie_list.size(), |
| base::BindOnce(&OnIsolatedCookieCopyComplete, std::move(reader))); |
| |
| net::CookieOptions options = net::CookieOptions::MakeAllInclusive(); |
| for (const net::CookieWithAccessResult& cookie : cookie_list) { |
| browser_context_->GetDefaultStoragePartition() |
| ->GetCookieManagerForBrowserProcess() |
| ->SetCanonicalCookie(cookie.cookie, current_url, 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 << std::endl; |
| } |
| |
| DVLOG(1) << ss.str(); |
| #endif // DCHECK_IS_ON() |
| } |
| |
| std::vector<PrefetchContainer*> PrefetchService::FindPrefetchContainerToServe( |
| const PrefetchContainer::Key& key, |
| base::WeakPtr<PrefetchServingPageMetricsContainer> |
| serving_page_metrics_container) { |
| std::vector<PrefetchContainer*> matches; |
| std::vector<PrefetchContainer*> hint_matches; |
| DVLOG(1) << "PrefetchService::FindPrefetchContainerToServe(" << key << ")"; |
| // Search for an exact or No-Vary-Search match first. |
| no_vary_search::IterateCandidates( |
| key, owned_prefetches_, |
| base::BindRepeating( |
| [](const PrefetchContainer::Key& key, |
| std::vector<PrefetchContainer*>* matches, |
| std::vector<PrefetchContainer*>* hint_matches, |
| const std::unique_ptr<PrefetchContainer>& prefetch_container, |
| no_vary_search::MatchType match_type) { |
| switch (match_type) { |
| case no_vary_search::MatchType::kExact: |
| case no_vary_search::MatchType::kNoVarySearch: |
| matches->push_back(prefetch_container.get()); |
| break; |
| case no_vary_search::MatchType::kOther: |
| if (const auto& nvs_expected = |
| prefetch_container->GetNoVarySearchHint()) { |
| // We cannot match based on the NVS hint once we have the |
| // response headers. If we have matching NVS header, |
| // the entry would have matched with kNoVarySearch above. |
| // We only match based on the hint if we have not yet |
| // received the headers. |
| if (prefetch_container->GetServableState( |
| PrefetchCacheableDuration()) == |
| PrefetchContainer::ServableState:: |
| kShouldBlockUntilHeadReceived && |
| nvs_expected->AreEquivalent( |
| key.prefetch_url(), prefetch_container->GetURL())) { |
| hint_matches->push_back(prefetch_container.get()); |
| } |
| } |
| break; |
| } |
| return no_vary_search::IterateCandidateResult::kContinue; |
| }, |
| key, base::Unretained(&matches), base::Unretained(&hint_matches))); |
| |
| // Insert the No-Vary-Search hint matches at the end of `matches`. |
| matches.insert(matches.end(), hint_matches.begin(), hint_matches.end()); |
| |
| for (PrefetchContainer* prefetch_container : matches) { |
| prefetch_container->SetServingPageMetrics(serving_page_metrics_container); |
| prefetch_container->UpdateServingPageMetrics(); |
| } |
| |
| std::erase_if(matches, [](const auto* prefetch_container) { |
| if (prefetch_container->HasPrefetchBeenConsideredToServe()) { |
| DVLOG(1) << "PrefetchService::FindPrefetchContainerToServe: skipped " |
| << "because already considered to serve: " |
| << *prefetch_container; |
| return true; |
| } |
| |
| switch (prefetch_container->GetServableState(PrefetchCacheableDuration())) { |
| case PrefetchContainer::ServableState::kNotServable: |
| DVLOG(1) << "PrefetchService::FindPrefetchContainerToServe: skipped " |
| "because not servable: " |
| << *prefetch_container; |
| return true; |
| case PrefetchContainer::ServableState::kServable: |
| case PrefetchContainer::ServableState::kShouldBlockUntilHeadReceived: |
| break; |
| } |
| |
| if (prefetch_container->IsDecoy()) { |
| DVLOG(1) |
| << "PrefetchService::FindPrefetchContainerToServe: skipped because " |
| "prefetch is a decoy: " |
| << *prefetch_container; |
| return true; |
| } |
| |
| // Note: This codepath is only be reached in practice if we create a |
| // second NavigationRequest to this prefetch's URL. The first |
| // NavigationRequest would call GetPrefetch, which might set this |
| // PrefetchContainer's status to kPrefetchNotUsedCookiesChanged. |
| CHECK(prefetch_container->HasPrefetchStatus()); |
| if (prefetch_container->GetPrefetchStatus() == |
| PrefetchStatus::kPrefetchNotUsedCookiesChanged) { |
| DVLOG(1) |
| << "PrefetchService::FindPrefetchContainerToServe: skipped because " |
| "cookies for url have changed since prefetch completed: " |
| << *prefetch_container; |
| return true; |
| } |
| |
| DVLOG(1) << "PrefetchService::FindPrefetchContainerToServe: matched: " |
| << *prefetch_container; |
| return false; |
| }); |
| |
| return matches; |
| } |
| |
| base::WeakPtr<PrefetchContainer> PrefetchService::MatchUrl( |
| const PrefetchContainer::Key& key) const { |
| return no_vary_search::MatchUrl(key, owned_prefetches_); |
| } |
| |
| std::vector<std::pair<GURL, base::WeakPtr<PrefetchContainer>>> |
| PrefetchService::GetAllForUrlWithoutRefAndQueryForTesting( |
| const PrefetchContainer::Key& key) const { |
| return no_vary_search::GetAllForUrlWithoutRefAndQueryForTesting( |
| key, owned_prefetches_); |
| } |
| |
| PrefetchService::HandlePrefetchContainerResult |
| PrefetchService::HandlePrefetchContainerToServe( |
| const PrefetchContainer::Key& key, |
| PrefetchContainer& prefetch_container, |
| PrefetchMatchResolver& prefetch_match_resolver) { |
| const GURL& url = key.prefetch_url(); |
| DVLOG(1) << "PrefetchService::HandlePrefetchContainerToServe(" |
| << prefetch_container << "): Start"; |
| |
| if (prefetch_container.HasPrefetchBeenConsideredToServe()) { |
| DVLOG(1) << "PrefetchService::HandlePrefetchContainerToServe(" |
| << prefetch_container << "): Skipped already considered to serve."; |
| return HandlePrefetchContainerResult::kNotUsable; |
| } |
| |
| // If prefetch is servable that means it already was matched to navigation |
| // url. Return it. |
| switch (prefetch_container.GetServableState(PrefetchCacheableDuration())) { |
| case PrefetchContainer::ServableState::kServable: { |
| DVLOG(1) << "PrefetchService::HandlePrefetchContainerToServe(" << url |
| << "): " << prefetch_container << " is servable"; |
| prefetch_container.OnGetPrefetchToServe(/*blocked_until_head=*/false); |
| return ReturnPrefetchToServe( |
| prefetch_container.GetURL(), prefetch_container.CreateReader(), |
| prefetch_match_resolver, |
| FallbackToRegularNavigationWhenPrefetchNotUsable(false)); |
| } |
| case PrefetchContainer::ServableState::kShouldBlockUntilHeadReceived: { |
| DVLOG(1) << "PrefetchService::HandlePrefetchContainerToServe(" << url |
| << "): " << prefetch_container << " is blocked until head"; |
| if (prefetch_match_resolver.IsWaitingForPrefetch(prefetch_container)) { |
| // TODO(crbug.com/1462206): Figure out if this path is actually |
| // possible. The reason I believe it is not possible is because the |
| // second time `GetPrefetchToServe` is called it executes |
| // HandlePrefetchContainerToServe first for the prefetch container |
| // that is marked as "ready". Since when we mark a prefetch as "ready" |
| // we immediately execute GetPrefetchToServe the container will be |
| // servable so we will return in the if above and serve that prefetch. |
| return HandlePrefetchContainerResult::kWaitForHead; |
| } |
| prefetch_container.OnGetPrefetchToServe(/*blocked_until_head=*/true); |
| prefetch_container.SetOnReceivedHeadCallback(base::BindOnce( |
| &PrefetchService::WaitOnPrefetchToServeHead, |
| weak_method_factory_.GetWeakPtr(), key, |
| prefetch_match_resolver.GetWeakPtr(), prefetch_container.GetURL(), |
| prefetch_container.GetWeakPtr())); |
| base::TimeDelta block_until_head_timeout = PrefetchBlockUntilHeadTimeout( |
| prefetch_container.GetPrefetchType().GetEagerness()); |
| 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_match_resolver.GetWeakPtr())); |
| prefetch_container.TakeBlockUntilHeadTimer( |
| std::move(block_until_head_timer)); |
| } |
| prefetch_match_resolver.WaitForPrefetch(prefetch_container); |
| return HandlePrefetchContainerResult::kWaitForHead; |
| } |
| case PrefetchContainer::ServableState::kNotServable: { |
| DVLOG(1) << "PrefetchService::HandlePrefetchContainerToServe(" << key |
| << "): " << prefetch_container << " is not servable"; |
| prefetch_container.OnReturnPrefetchToServe(/*served=*/false); |
| return HandlePrefetchContainerResult::kNotUsable; |
| } |
| } |
| } |
| |
| void PrefetchService::GetPrefetchToServe( |
| const PrefetchContainer::Key& key, |
| base::WeakPtr<PrefetchServingPageMetricsContainer> |
| serving_page_metrics_container, |
| PrefetchMatchResolver& prefetch_match_resolver) { |
| DumpPrefetchesForDebug(); |
| auto potential_matching_prefetches = FindPrefetchContainerToServe( |
| key, std::move(serving_page_metrics_container)); |
| DVLOG(1) << "PrefetchService::GetPrefetchToServe(" << key |
| << "): Potential matched with " |
| << potential_matching_prefetches.size() << " prefetch containers."; |
| bool waiting_on_prefetch_head = false; |
| for (auto* prefetch_container : potential_matching_prefetches) { |
| constexpr auto final_state_prefetch_container_results = |
| base::MakeFixedFlatSet<HandlePrefetchContainerResult>( |
| {HandlePrefetchContainerResult::kToBeServed, |
| HandlePrefetchContainerResult::kNotToBeServedCookiesChanged}); |
| CHECK(prefetch_container); |
| auto result = HandlePrefetchContainerToServe(key, *prefetch_container, |
| prefetch_match_resolver); |
| if (final_state_prefetch_container_results.contains(result)) { |
| return; |
| } |
| waiting_on_prefetch_head |= |
| (result == HandlePrefetchContainerResult::kWaitForHead); |
| } |
| // If there is at least one prefetch that we are waiting for the head then |
| // stop. |
| if (waiting_on_prefetch_head) { |
| return; |
| } |
| // If not waiting on any prefetches it means there is no match. Let the |
| // browser know to request url from the web server. |
| DVLOG(1) << "PrefetchService::GetPrefetchToServe(" << key |
| << "): No PrefetchContainer is servable"; |
| ReturnPrefetchToServe({}, {}, prefetch_match_resolver); |
| } |
| |
| void PrefetchService::WaitOnPrefetchToServeHead( |
| const PrefetchContainer::Key& key, |
| base::WeakPtr<PrefetchMatchResolver> prefetch_match_resolver, |
| const GURL& prefetch_url, |
| base::WeakPtr<PrefetchContainer> prefetch_container) { |
| DVLOG(1) << "PrefetchService::WaitOnPrefetchToServeHead:" |
| << "prefetch_url = " << prefetch_url; |
| if (!prefetch_match_resolver) { |
| // Since prefetch_match_resolver is a NavigationHandleUserData, |
| // if it is null it means the navigation has finished so there is nothing to |
| // do here. |
| return; |
| } |
| |
| // This method is called only for prefetches that we have started waiting on. |
| // We make sure that this is true by checking with prefetch_match_resolver. |
| CHECK(prefetch_match_resolver->IsWaitingForPrefetch(prefetch_url)); |
| // We need to make sure we are not waiting on this prefetch_url anymore. |
| prefetch_match_resolver->EndWaitForPrefetch(prefetch_url); |
| // Make sure we are not waiting on this prefetch_url anymore. |
| CHECK(!prefetch_match_resolver->IsWaitingForPrefetch(prefetch_url)); |
| |
| DVLOG(1) << "PrefetchService::WaitOnPrefetchToServeHead(" << key |
| << "): PrefetchContainer head received for " << prefetch_url << "!"; |
| // This method is registered with the prefetch_container as the |
| // ReceivedHeadCallback. We only call this method immediately after |
| // requesting the ReceivedHeadCallback from the prefetch_container. |
| // The prefetch_container must be alive. |
| CHECK(prefetch_container); |
| prefetch_container->ResetBlockUntilHeadTimer(); |
| |
| switch (prefetch_container->GetServableState(PrefetchCacheableDuration())) { |
| case PrefetchContainer::ServableState::kNotServable: |
| case PrefetchContainer::ServableState::kShouldBlockUntilHeadReceived: { |
| DVLOG(1) << "PrefetchService::WaitOnPrefetchToServeHead(" << key |
| << "): " << *prefetch_container << " not servable!"; |
| prefetch_container->OnReturnPrefetchToServe(/*served=*/false); |
| ReturnPrefetchToServe(prefetch_url, {}, *prefetch_match_resolver); |
| return; |
| } |
| case PrefetchContainer::ServableState::kServable: |
| break; |
| } |
| |
| const GURL& nav_url = key.prefetch_url(); |
| if (nav_url == prefetch_container->GetURL()) { |
| HandlePrefetchContainerToServe(key, *prefetch_container, |
| *prefetch_match_resolver); |
| return; |
| } |
| |
| // No-Vary-Search response header is already populated by |
| // PrefetchContainer::OnReceivedHead. |
| auto no_vary_search_data = prefetch_container->GetNoVarySearchData(); |
| if (!no_vary_search_data.has_value() || |
| !no_vary_search_data.value().AreEquivalent( |
| nav_url, prefetch_container->GetURL())) { |
| prefetch_container->OnReturnPrefetchToServe(/*served=*/false); |
| prefetch_container->UpdateServingPageMetrics(); |
| ReturnPrefetchToServe(prefetch_url, {}, *prefetch_match_resolver); |
| 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); |
| } |
| HandlePrefetchContainerToServe(key, *prefetch_container, |
| *prefetch_match_resolver); |
| } |
| |
| PrefetchService::HandlePrefetchContainerResult |
| PrefetchService::ReturnPrefetchToServe( |
| const GURL& prefetch_url, |
| PrefetchContainer::Reader reader, |
| PrefetchMatchResolver& prefetch_match_resolver, |
| FallbackToRegularNavigationWhenPrefetchNotUsable |
| when_prefetch_not_used_fallback_to_regular_navigation) { |
| if (prefetch_url.is_empty()) { |
| // In this case there is no matching prefetch. |
| if (when_prefetch_not_used_fallback_to_regular_navigation) { |
| prefetch_match_resolver.PrefetchNotAvailable(); |
| } |
| return HandlePrefetchContainerResult::kNotAvailable; |
| } |
| if (!reader) { |
| // In this case the prefetch cannot be used for some reason. |
| if (when_prefetch_not_used_fallback_to_regular_navigation) { |
| prefetch_match_resolver.PrefetchNotUsable(prefetch_url); |
| } |
| return HandlePrefetchContainerResult::kNotUsable; |
| } |
| |
| PrefetchContainer* prefetch_container = reader.GetPrefetchContainer(); |
| CHECK(prefetch_container); |
| // We know `prefetch_container` is servable when we get here. |
| CHECK_EQ(prefetch_container->GetServableState(PrefetchCacheableDuration()), |
| PrefetchContainer::ServableState::kServable); |
| prefetch_container->UpdateServingPageMetrics(); |
| if (reader.HaveDefaultContextCookiesChanged()) { |
| prefetch_match_resolver |
| .FallbackToRegularNavigationWhenMatchedPrefetchCookiesChanged( |
| *prefetch_container); |
| return HandlePrefetchContainerResult::kNotToBeServedCookiesChanged; |
| } |
| |
| if (!reader.HasIsolatedCookieCopyStarted()) { |
| CopyIsolatedCookies(reader); |
| } |
| |
| DVLOG(1) << "PrefetchService::ReturnPrefetchToServe" << *prefetch_container |
| << " is served!"; |
| prefetch_container->OnReturnPrefetchToServe( |
| /*served=*/true); |
| prefetch_match_resolver.PrefetchServed(std::move(reader)); |
| return HandlePrefetchContainerResult::kToBeServed; |
| } |
| |
| // 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; |
| } |
| |
| base::WeakPtr<PrefetchService> PrefetchService::GetWeakPtr() { |
| return weak_method_factory_.GetWeakPtr(); |
| } |
| |
| 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 : owned_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++; |
| } |
| |
| switch ( |
| prefetch_iter.second->GetServableState(PrefetchCacheableDuration())) { |
| case PrefetchContainer::ServableState::kNotServable: |
| case PrefetchContainer::ServableState::kShouldBlockUntilHeadReceived: |
| break; |
| case PrefetchContainer::ServableState::kServable: |
| if (!prefetch_iter.second->HasPrefetchBeenConsideredToServe()) { |
| num_matching_servable_prefetch++; |
| } |
| break; |
| } |
| |
| if (prefetch_iter.second->HasSameReferringURLForMetrics( |
| *prefetch_container)) { |
| 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 |