| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/predictors/prefetch_manager.h" |
| |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "chrome/browser/predictors/predictors_features.h" |
| #include "chrome/browser/predictors/predictors_switches.h" |
| #include "chrome/browser/predictors/resource_prefetch_predictor.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/global_request_id.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/browser/url_loader_throttles.h" |
| #include "net/base/load_flags.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/network/public/cpp/empty_url_loader_client.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/mojom/fetch_api.mojom.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "third_party/blink/public/common/loader/throttling_url_loader.h" |
| #include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h" |
| |
| namespace predictors { |
| |
| namespace { |
| |
| const net::NetworkTrafficAnnotationTag kPrefetchTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("predictive_prefetch", |
| R"( |
| semantics { |
| sender: "Loading Predictor" |
| description: |
| "This request is issued near the start of a navigation to " |
| "speculatively fetch resources that resulting page is predicted to " |
| "request." |
| trigger: |
| "Navigating Chrome (by clicking on a link, bookmark, history item, " |
| "using session restore, etc)." |
| data: |
| "Arbitrary site-controlled data can be included in the URL." |
| "Requests may include cookies and site-specific credentials." |
| destination: WEBSITE |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: |
| "There are a number of ways to prevent this request:" |
| "A) Disable predictive operations under Settings > Advanced > " |
| " Privacy > Preload pages for faster browsing and searching," |
| "B) Disable Lite Mode under Settings > Advanced > Lite mode, or " |
| "C) Disable 'Make searches and browsing better' under Settings > " |
| " Sync and Google services > Make searches and browsing better" |
| chrome_policy { |
| URLBlacklist { |
| URLBlacklist: { entries: '*' } |
| } |
| } |
| chrome_policy { |
| URLWhitelist { |
| URLWhitelist { } |
| } |
| } |
| } |
| comments: |
| "This feature can be safely disabled, but enabling it may result in " |
| "faster page loads. Using either URLBlacklist or URLWhitelist policies " |
| "(or a combination of both) limits the scope of these requests." |
| )"); |
| |
| } // namespace |
| |
| // Stores the status of all prefetches associated with a given |url|. |
| struct PrefetchInfo { |
| PrefetchInfo(const GURL& url, PrefetchManager& manager) |
| : url(url), |
| stats(std::make_unique<PrefetchStats>(url)), |
| manager(&manager) { |
| DCHECK(url.is_valid()); |
| DCHECK(url.SchemeIsHTTPOrHTTPS()); |
| } |
| |
| ~PrefetchInfo() = default; |
| |
| PrefetchInfo(const PrefetchInfo&) = delete; |
| PrefetchInfo& operator=(const PrefetchInfo&) = delete; |
| |
| void OnJobCreated() { job_count++; } |
| |
| void OnJobDestroyed() { |
| job_count--; |
| if (is_done()) { |
| // Destroys |this|. |
| manager->AllPrefetchJobsForUrlFinished(*this); |
| } |
| } |
| |
| bool is_done() const { return job_count == 0; } |
| |
| GURL url; |
| size_t job_count = 0; |
| bool was_canceled = false; |
| std::unique_ptr<PrefetchStats> stats; |
| // Owns |this|. |
| PrefetchManager* const manager; |
| |
| base::WeakPtrFactory<PrefetchInfo> weak_factory{this}; |
| }; |
| |
| // Stores all data need for running a prefetch to a |url|. |
| struct PrefetchJob { |
| PrefetchJob(PrefetchRequest prefetch_request, PrefetchInfo& info) |
| : url(prefetch_request.url), |
| network_isolation_key( |
| std::move(prefetch_request.network_isolation_key)), |
| destination(prefetch_request.destination), |
| info(info.weak_factory.GetWeakPtr()) { |
| DCHECK(url.is_valid()); |
| DCHECK(url.SchemeIsHTTPOrHTTPS()); |
| DCHECK(network_isolation_key.IsFullyPopulated()); |
| info.OnJobCreated(); |
| } |
| |
| ~PrefetchJob() { |
| if (info) |
| info->OnJobDestroyed(); |
| } |
| |
| PrefetchJob(const PrefetchJob&) = delete; |
| PrefetchJob& operator=(const PrefetchJob&) = delete; |
| |
| GURL url; |
| net::NetworkIsolationKey network_isolation_key; |
| network::mojom::RequestDestination destination; |
| |
| // PrefetchJob lives until the URL load completes, so it can outlive the |
| // PrefetchManager and therefore the PrefetchInfo. |
| base::WeakPtr<PrefetchInfo> info; |
| }; |
| |
| PrefetchStats::PrefetchStats(const GURL& url) |
| : url(url), start_time(base::TimeTicks::Now()) {} |
| PrefetchStats::~PrefetchStats() = default; |
| |
| PrefetchManager::PrefetchManager(base::WeakPtr<Delegate> delegate, |
| Profile* profile) |
| : delegate_(std::move(delegate)), profile_(profile) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(profile_); |
| } |
| |
| PrefetchManager::~PrefetchManager() = default; |
| |
| void PrefetchManager::Start(const GURL& url, |
| std::vector<PrefetchRequest> requests) { |
| DCHECK(base::FeatureList::IsEnabled(features::kLoadingPredictorPrefetch)); |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| PrefetchInfo* info; |
| if (prefetch_info_.find(url) == prefetch_info_.end()) { |
| auto iterator_and_whether_inserted = |
| prefetch_info_.emplace(url, std::make_unique<PrefetchInfo>(url, *this)); |
| info = iterator_and_whether_inserted.first->second.get(); |
| } else { |
| info = prefetch_info_.find(url)->second.get(); |
| } |
| |
| for (auto& request : requests) { |
| queued_jobs_.emplace_back( |
| std::make_unique<PrefetchJob>(std::move(request), *info)); |
| } |
| |
| TryToLaunchPrefetchJobs(); |
| } |
| |
| void PrefetchManager::Stop(const GURL& url) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| auto it = prefetch_info_.find(url); |
| if (it == prefetch_info_.end()) |
| return; |
| it->second->was_canceled = true; |
| } |
| |
| blink::mojom::ResourceType GetResourceType( |
| network::mojom::RequestDestination destination) { |
| switch (destination) { |
| case network::mojom::RequestDestination::kEmpty: |
| return blink::mojom::ResourceType::kSubResource; |
| case network::mojom::RequestDestination::kScript: |
| return blink::mojom::ResourceType::kScript; |
| case network::mojom::RequestDestination::kStyle: |
| return blink::mojom::ResourceType::kStylesheet; |
| default: |
| NOTREACHED() << destination; |
| } |
| return blink::mojom::ResourceType::kSubResource; |
| } |
| |
| void PrefetchManager::PrefetchUrl( |
| std::unique_ptr<PrefetchJob> job, |
| scoped_refptr<network::SharedURLLoaderFactory> factory) { |
| DCHECK(job); |
| DCHECK(job->info); |
| |
| PrefetchInfo& info = *job->info; |
| url::Origin top_frame_origin = url::Origin::Create(info.url); |
| |
| network::ResourceRequest request; |
| request.method = "GET"; |
| request.url = job->url; |
| request.site_for_cookies = net::SiteForCookies::FromUrl(info.url); |
| request.request_initiator = top_frame_origin; |
| |
| // The prefetch can happen before the referrer policy is known, so use a |
| // conservative one (no-referrer) by default. |
| request.referrer_policy = net::ReferrerPolicy::NO_REFERRER; |
| |
| request.headers.SetHeader("Purpose", "prefetch"); |
| |
| request.load_flags = net::LOAD_PREFETCH; |
| request.destination = job->destination; |
| request.resource_type = |
| static_cast<int>(GetResourceType(request.destination)); |
| |
| // TODO(falken): Support CORS? |
| request.mode = network::mojom::RequestMode::kNoCors; |
| |
| // The hints are only for requests made from the top frame, |
| // so frame_origin is the same as top_frame_origin. |
| auto frame_origin = top_frame_origin; |
| |
| request.trusted_params = network::ResourceRequest::TrustedParams(); |
| request.trusted_params->isolation_info = net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kOther, top_frame_origin, frame_origin, |
| net::SiteForCookies::FromUrl(info.url)); |
| |
| // TODO(crbug.com/1092329): Ensure the request is seen by extensions. |
| |
| // Set up throttles. Use null values for frame/navigation-related params, for |
| // now, since this is just the browser prefetching resources and the requests |
| // don't need to appear to come from a frame. |
| // TODO(falken): Clarify the API of CreateURLLoaderThrottles() for prefetching |
| // and subresources. |
| auto wc_getter = |
| base::BindRepeating([]() -> content::WebContents* { return nullptr; }); |
| std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles = |
| content::CreateContentBrowserURLLoaderThrottles( |
| request, profile_, std::move(wc_getter), |
| /*navigation_ui_data=*/nullptr, |
| content::RenderFrameHost::kNoFrameTreeNodeId); |
| |
| auto client = std::make_unique<network::EmptyURLLoaderClient>(); |
| |
| ++inflight_jobs_count_; |
| |
| // Since the CORS-RFC1918 check is skipped when the client security state is |
| // unknown, just block any local request to be safe for now. |
| int options = base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kLoadingPredictorAllowLocalRequestForTesting) |
| ? network::mojom::kURLLoadOptionNone |
| : network::mojom::kURLLoadOptionBlockLocalRequest; |
| |
| std::unique_ptr<blink::ThrottlingURLLoader> loader = |
| blink::ThrottlingURLLoader::CreateLoaderAndStart( |
| std::move(factory), std::move(throttles), |
| /*routing_id is not needed*/ -1, |
| content::GlobalRequestID::MakeBrowserInitiated().request_id, options, |
| &request, client.get(), kPrefetchTrafficAnnotation, |
| base::ThreadTaskRunnerHandle::Get(), |
| /*cors_exempt_header_list=*/base::nullopt); |
| |
| delegate_->PrefetchInitiated(info.url, job->url); |
| |
| // The idea of prefetching is for the network service to put the response in |
| // the http cache. So from the prefetching layer, nothing needs to be done |
| // with the response, so just drain it. |
| auto* raw_client = client.get(); |
| raw_client->Drain(base::BindOnce(&PrefetchManager::OnPrefetchFinished, |
| weak_factory_.GetWeakPtr(), std::move(job), |
| std::move(loader), std::move(client))); |
| } |
| |
| // Some params are unused but bound to this function to keep them alive until |
| // the load finishes. |
| void PrefetchManager::OnPrefetchFinished( |
| std::unique_ptr<PrefetchJob> job, |
| std::unique_ptr<blink::ThrottlingURLLoader> loader, |
| std::unique_ptr<network::mojom::URLLoaderClient> client, |
| const network::URLLoaderCompletionStatus& status) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| PrefetchInfo& info = *job->info; |
| if (observer_for_testing_) |
| observer_for_testing_->OnPrefetchFinished(info.url, job->url, status); |
| |
| loader.reset(); |
| client.reset(); |
| job.reset(); |
| |
| --inflight_jobs_count_; |
| TryToLaunchPrefetchJobs(); |
| } |
| |
| void PrefetchManager::TryToLaunchPrefetchJobs() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (queued_jobs_.empty() || |
| inflight_jobs_count_ >= features::GetMaxInflightPrefetches()) { |
| return; |
| } |
| |
| // TODO(falken): Is it ok to assume the default partition? Try to plumb the |
| // partition here, e.g., from WebContentsObserver. And make a similar change |
| // in PreconnectManager. |
| content::StoragePartition* storage_partition = |
| content::BrowserContext::GetDefaultStoragePartition(profile_); |
| scoped_refptr<network::SharedURLLoaderFactory> factory = |
| storage_partition->GetURLLoaderFactoryForBrowserProcess(); |
| |
| while (!queued_jobs_.empty() && |
| inflight_jobs_count_ < features::GetMaxInflightPrefetches()) { |
| std::unique_ptr<PrefetchJob> job = std::move(queued_jobs_.front()); |
| queued_jobs_.pop_front(); |
| base::WeakPtr<PrefetchInfo> info = job->info; |
| // |this| owns all infos. |
| DCHECK(info); |
| |
| if (job->url.is_valid() && factory && !info->was_canceled) |
| PrefetchUrl(std::move(job), factory); |
| } |
| } |
| |
| void PrefetchManager::AllPrefetchJobsForUrlFinished(PrefetchInfo& info) { |
| DCHECK(info.is_done()); |
| auto it = prefetch_info_.find(info.url); |
| DCHECK(it != prefetch_info_.end()); |
| DCHECK(&info == it->second.get()); |
| |
| if (delegate_) |
| delegate_->PrefetchFinished(std::move(info.stats)); |
| if (observer_for_testing_) |
| observer_for_testing_->OnAllPrefetchesFinished(info.url); |
| prefetch_info_.erase(it); |
| } |
| |
| } // namespace predictors |