blob: 5e2f23af5b5610a5ecbe7182769a21dbd6a69538 [file] [log] [blame]
// 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/prerender/isolated/isolated_prerender_tab_helper.h"
#include <string>
#include "base/bind.h"
#include "base/feature_list.h"
#include "chrome/browser/navigation_predictor/navigation_predictor_keyed_service_factory.h"
#include "chrome/browser/net/prediction_options.h"
#include "chrome/browser/prerender/isolated/isolated_prerender_features.h"
#include "chrome/browser/prerender/isolated/isolated_prerender_params.h"
#include "chrome/browser/prerender/isolated/isolated_prerender_proxy_configurator.h"
#include "chrome/browser/prerender/isolated/isolated_prerender_service.h"
#include "chrome/browser/prerender/isolated/isolated_prerender_service_factory.h"
#include "chrome/browser/prerender/isolated/isolated_prerender_service_workers_observer.h"
#include "chrome/browser/profiles/profile.h"
#include "components/data_reduction_proxy/core/browser/data_reduction_proxy_settings.h"
#include "components/google/core/common/google_util.h"
#include "components/search_engines/template_url_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_constants.h"
#include "content/public/common/user_agent.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/base/network_isolation_key.h"
#include "net/cookies/cookie_store.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "services/network/public/mojom/network_service.mojom.h"
#include "url/origin.h"
IsolatedPrerenderTabHelper::CurrentPageLoad::CurrentPageLoad() = default;
IsolatedPrerenderTabHelper::CurrentPageLoad::~CurrentPageLoad() = default;
IsolatedPrerenderTabHelper::IsolatedPrerenderTabHelper(
content::WebContents* web_contents)
: content::WebContentsObserver(web_contents) {
page_ = std::make_unique<CurrentPageLoad>();
profile_ = Profile::FromBrowserContext(web_contents->GetBrowserContext());
NavigationPredictorKeyedService* navigation_predictor_service =
NavigationPredictorKeyedServiceFactory::GetForProfile(profile_);
if (navigation_predictor_service) {
navigation_predictor_service->AddObserver(this);
}
}
IsolatedPrerenderTabHelper::~IsolatedPrerenderTabHelper() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
NavigationPredictorKeyedService* navigation_predictor_service =
NavigationPredictorKeyedServiceFactory::GetForProfile(profile_);
if (navigation_predictor_service) {
navigation_predictor_service->RemoveObserver(this);
}
}
void IsolatedPrerenderTabHelper::DidStartNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!navigation_handle->IsInMainFrame()) {
return;
}
if (navigation_handle->IsSameDocument()) {
return;
}
// User is navigating, don't bother prefetching further.
page_->url_loader.reset();
}
void IsolatedPrerenderTabHelper::DidFinishNavigation(
content::NavigationHandle* navigation_handle) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!navigation_handle->IsInMainFrame()) {
return;
}
if (navigation_handle->IsSameDocument()) {
return;
}
if (!navigation_handle->HasCommitted()) {
return;
}
DCHECK(!PrefetchingActive());
page_ = std::make_unique<CurrentPageLoad>();
}
void IsolatedPrerenderTabHelper::OnVisibilityChanged(
content::Visibility visibility) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!base::FeatureList::IsEnabled(
features::kPrefetchSRPNavigationPredictions_HTMLOnly)) {
return;
}
// Start prefetching if the tab has become visible and prefetching is
// inactive. Hidden and occluded visibility is ignored here so that pending
// prefetches can finish.
if (visibility == content::Visibility::VISIBLE && !PrefetchingActive())
Prefetch();
}
std::unique_ptr<PrefetchedMainframeResponseContainer>
IsolatedPrerenderTabHelper::TakePrefetchResponse(const GURL& url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto it = page_->prefetched_responses.find(url);
if (it == page_->prefetched_responses.end())
return nullptr;
std::unique_ptr<PrefetchedMainframeResponseContainer> response =
std::move(it->second);
page_->prefetched_responses.erase(it);
return response;
}
bool IsolatedPrerenderTabHelper::PrefetchingActive() const {
return page_ && page_->url_loader;
}
void IsolatedPrerenderTabHelper::Prefetch() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(base::FeatureList::IsEnabled(
features::kPrefetchSRPNavigationPredictions_HTMLOnly));
page_->url_loader.reset();
if (page_->urls_to_prefetch.empty()) {
return;
}
if (IsolatedPrerenderMaximumNumberOfPrefetches().has_value() &&
page_->num_prefetches_attempted >=
IsolatedPrerenderMaximumNumberOfPrefetches().value()) {
return;
}
if (web_contents()->GetVisibility() != content::Visibility::VISIBLE) {
// |OnVisibilityChanged| will restart prefetching when the tab becomes
// visible again.
return;
}
page_->num_prefetches_attempted++;
GURL url = page_->urls_to_prefetch[0];
page_->urls_to_prefetch.erase(page_->urls_to_prefetch.begin());
net::NetworkIsolationKey key =
net::NetworkIsolationKey::CreateOpaqueAndNonTransient();
network::ResourceRequest::TrustedParams trusted_params;
trusted_params.network_isolation_key = key;
std::unique_ptr<network::ResourceRequest> request =
std::make_unique<network::ResourceRequest>();
request->url = url;
request->method = "GET";
request->load_flags = net::LOAD_DISABLE_CACHE | net::LOAD_PREFETCH;
request->credentials_mode = network::mojom::CredentialsMode::kOmit;
request->headers.SetHeader(content::kCorsExemptPurposeHeaderName, "prefetch");
request->trusted_params = trusted_params;
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("navigation_predictor_srp_prefetch",
R"(
semantics {
sender: "Navigation Predictor SRP Prefetch Loader"
description:
"Prefetches the mainframe HTML of a page linked from a Google "
"Search Result Page (SRP). 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 for sites off of Google SRPs (Search Result Pages) only "
"for Lite mode users when the feature is enabled."
data: "None."
destination: WEBSITE
}
policy {
cookies_allowed: NO
setting:
"Users can control Lite mode on Android via the settings menu. "
"Lite mode is not available on iOS, and on desktop only for "
"developer testing."
policy_exception_justification: "Not implemented."
})");
// TODO(crbug/1023485): Disallow auth challenges.
page_->url_loader =
network::SimpleURLLoader::Create(std::move(request), traffic_annotation);
// base::Unretained is safe because |page_->url_loader| is owned by |this|.
page_->url_loader->SetOnRedirectCallback(base::BindRepeating(
&IsolatedPrerenderTabHelper::OnPrefetchRedirect, base::Unretained(this)));
page_->url_loader->SetAllowHttpErrorResults(true);
page_->url_loader->DownloadToString(
GetURLLoaderFactory(),
base::BindOnce(&IsolatedPrerenderTabHelper::OnPrefetchComplete,
base::Unretained(this), url, key),
1024 * 1024 * 5 /* 5MB */);
}
void IsolatedPrerenderTabHelper::OnPrefetchRedirect(
const net::RedirectInfo& redirect_info,
const network::mojom::URLResponseHead& response_head,
std::vector<std::string>* removed_headers) {
DCHECK(PrefetchingActive());
// Run the new URL through all the eligibility checks. In the mean time,
// continue on with other Prefetches.
if (CheckAndMaybePrefetchURL(redirect_info.new_url)) {
// The redirect shouldn't count against our prefetch limit if the redirect
// was followed.
page_->num_prefetches_attempted--;
}
// Cancels the current request.
Prefetch();
}
void IsolatedPrerenderTabHelper::OnPrefetchComplete(
const GURL& url,
const net::NetworkIsolationKey& key,
std::unique_ptr<std::string> body) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(PrefetchingActive());
if (page_->url_loader->NetError() == net::OK && body &&
page_->url_loader->ResponseInfo()) {
network::mojom::URLResponseHeadPtr head =
page_->url_loader->ResponseInfo()->Clone();
HandlePrefetchResponse(url, key, std::move(head), std::move(body));
}
Prefetch();
}
void IsolatedPrerenderTabHelper::HandlePrefetchResponse(
const GURL& url,
const net::NetworkIsolationKey& key,
network::mojom::URLResponseHeadPtr head,
std::unique_ptr<std::string> body) {
DCHECK(!head->was_fetched_via_cache);
DCHECK(PrefetchingActive());
int response_code = head->headers->response_code();
if (response_code < 200 || response_code >= 300) {
return;
}
if (head->mime_type != "text/html") {
return;
}
std::unique_ptr<PrefetchedMainframeResponseContainer> response =
std::make_unique<PrefetchedMainframeResponseContainer>(
key, std::move(head), std::move(body));
page_->prefetched_responses.emplace(url, std::move(response));
}
void IsolatedPrerenderTabHelper::OnPredictionUpdated(
const base::Optional<NavigationPredictorKeyedService::Prediction>&
prediction) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!base::FeatureList::IsEnabled(
features::kPrefetchSRPNavigationPredictions_HTMLOnly)) {
return;
}
// DataSaver must be enabled by the user to use this feature.
if (!data_reduction_proxy::DataReductionProxySettings::
IsDataSaverEnabledByUser(profile_->IsOffTheRecord(),
profile_->GetPrefs())) {
return;
}
// This checks whether the user has enabled pre* actions in the settings UI.
if (!chrome_browser_net::CanPreresolveAndPreconnectUI(profile_->GetPrefs())) {
return;
}
// This is also checked before prefetching from the network, but checking
// again here allows us to skip querying for cookies if we won't be
// prefetching the url anyways.
if (IsolatedPrerenderMaximumNumberOfPrefetches().has_value() &&
page_->num_prefetches_attempted >=
IsolatedPrerenderMaximumNumberOfPrefetches().value()) {
return;
}
if (!prediction.has_value()) {
return;
}
if (prediction.value().web_contents() != web_contents()) {
// We only care about predictions in this tab.
return;
}
if (!google_util::IsGoogleSearchUrl(
prediction.value().source_document_url())) {
return;
}
for (const GURL& url : prediction.value().sorted_predicted_urls()) {
CheckAndMaybePrefetchURL(url);
}
}
bool IsolatedPrerenderTabHelper::CheckAndMaybePrefetchURL(const GURL& url) {
DCHECK(data_reduction_proxy::DataReductionProxySettings::
IsDataSaverEnabledByUser(profile_->IsOffTheRecord(),
profile_->GetPrefs()));
if (google_util::IsGoogleAssociatedDomainUrl(url)) {
return false;
}
if (url.HostIsIPAddress()) {
return false;
}
if (!url.SchemeIs(url::kHttpsScheme)) {
return false;
}
content::StoragePartition* default_storage_partition =
content::BrowserContext::GetDefaultStoragePartition(profile_);
// Only the default storage partition is supported since that is the only
// place where service workers are observed by
// |IsolatedPrerenderServiceWorkersObserver|.
if (default_storage_partition !=
content::BrowserContext::GetStoragePartitionForSite(
profile_, url, /*can_create=*/false)) {
return false;
}
IsolatedPrerenderService* isolated_prerender_service =
IsolatedPrerenderServiceFactory::GetForProfile(profile_);
if (!isolated_prerender_service) {
return false;
}
base::Optional<bool> site_has_service_worker =
isolated_prerender_service->service_workers_observer()
->IsServiceWorkerRegisteredForOrigin(url::Origin::Create(url));
if (!site_has_service_worker.has_value() || site_has_service_worker.value()) {
return false;
}
net::CookieOptions options = net::CookieOptions::MakeAllInclusive();
default_storage_partition->GetCookieManagerForBrowserProcess()->GetCookieList(
url, options,
base::BindOnce(&IsolatedPrerenderTabHelper::OnGotCookieList,
weak_factory_.GetWeakPtr(), url));
return true;
}
void IsolatedPrerenderTabHelper::OnGotCookieList(
const GURL& url,
const net::CookieStatusList& cookie_with_status_list,
const net::CookieStatusList& excluded_cookies) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!cookie_with_status_list.empty())
return;
// TODO(robertogden): Consider adding redirect URLs to the front of the list.
page_->urls_to_prefetch.push_back(url);
if (!PrefetchingActive()) {
Prefetch();
}
}
network::mojom::URLLoaderFactory*
IsolatedPrerenderTabHelper::GetURLLoaderFactory() {
if (!page_->isolated_url_loader_factory) {
CreateIsolatedURLLoaderFactory();
}
DCHECK(page_->isolated_url_loader_factory);
return page_->isolated_url_loader_factory.get();
}
void IsolatedPrerenderTabHelper::CreateIsolatedURLLoaderFactory() {
page_->isolated_network_context.reset();
page_->isolated_url_loader_factory.reset();
IsolatedPrerenderService* isolated_prerender_service =
IsolatedPrerenderServiceFactory::GetForProfile(profile_);
auto context_params = network::mojom::NetworkContextParams::New();
context_params->user_agent = content::GetFrozenUserAgent(true).as_string();
context_params->initial_custom_proxy_config =
isolated_prerender_service->proxy_configurator()
->CreateCustomProxyConfig();
// Also register a client config receiver so that updates to the set of proxy
// hosts or proxy headers will be updated.
mojo::Remote<network::mojom::CustomProxyConfigClient> config_client;
context_params->custom_proxy_config_client_receiver =
config_client.BindNewPipeAndPassReceiver();
isolated_prerender_service->proxy_configurator()->AddCustomProxyConfigClient(
std::move(config_client));
content::GetNetworkService()->CreateNetworkContext(
page_->isolated_network_context.BindNewPipeAndPassReceiver(),
std::move(context_params));
auto factory_params = network::mojom::URLLoaderFactoryParams::New();
factory_params->process_id = network::mojom::kBrowserProcessId;
factory_params->is_trusted = true;
factory_params->is_corb_enabled = false;
page_->isolated_network_context->CreateURLLoaderFactory(
page_->isolated_url_loader_factory.BindNewPipeAndPassReceiver(),
std::move(factory_params));
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(IsolatedPrerenderTabHelper)