| // 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/prefetch/search_prefetch/base_search_prefetch_request.h" |
| |
| #include <vector> |
| |
| #include "build/build_config.h" |
| #include "chrome/browser/prefetch/prefetch_headers.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/search_engines/template_url_service_factory.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/embedder_support/user_agent_utils.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/search_engines/template_url_service.h" |
| #include "components/variations/net/variations_http_headers.h" |
| #include "content/public/browser/client_hints.h" |
| #include "content/public/browser/frame_accept_header.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/url_loader_throttles.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/public/common/content_constants.h" |
| #include "net/base/load_flags.h" |
| #include "net/cookies/site_for_cookies.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/http/http_status_code.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/network/public/cpp/client_hints.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/mojom/fetch_api.mojom.h" |
| #include "third_party/blink/public/common/client_hints/client_hints.h" |
| #include "third_party/blink/public/common/loader/url_loader_throttle.h" |
| #include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h" |
| #include "url/origin.h" |
| |
| #if BUILDFLAG(IS_ANDROID) |
| #include "chrome/browser/android/omnibox/geolocation_header.h" |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| namespace { |
| |
| // A custom URLLoaderThrottle delegate that is very sensitive. Anything that |
| // would delay or cancel the request is treated the same, which would prevent |
| // the prefetch request. |
| class CheckForCancelledOrPausedDelegate |
| : public blink::URLLoaderThrottle::Delegate { |
| public: |
| CheckForCancelledOrPausedDelegate() = default; |
| ~CheckForCancelledOrPausedDelegate() override = default; |
| |
| CheckForCancelledOrPausedDelegate(const CheckForCancelledOrPausedDelegate&) = |
| delete; |
| CheckForCancelledOrPausedDelegate& operator=( |
| const CheckForCancelledOrPausedDelegate&) = delete; |
| |
| // URLLoaderThrottle::Delegate: |
| void CancelWithError(int error_code, |
| base::StringPiece custom_reason) override { |
| cancelled_or_paused_ = true; |
| } |
| |
| void Resume() override {} |
| |
| void PauseReadingBodyFromNet() override { cancelled_or_paused_ = true; } |
| |
| void RestartWithFlags(int additional_load_flags) override { |
| cancelled_or_paused_ = true; |
| } |
| |
| void RestartWithURLResetAndFlags(int additional_load_flags) override { |
| cancelled_or_paused_ = true; |
| } |
| |
| void RestartWithURLResetAndFlagsNow(int additional_load_flags) override { |
| cancelled_or_paused_ = true; |
| } |
| |
| bool cancelled_or_paused() const { return cancelled_or_paused_; } |
| |
| private: |
| bool cancelled_or_paused_ = false; |
| }; |
| |
| bool DoesHeaderContainClientHint( |
| const net::HttpRequestHeaders& headers, |
| const network::mojom::WebClientHintsType hint) { |
| const std::string& header = network::GetClientHintToNameMap().at(hint); |
| std::string value; |
| return headers.GetHeader(header, &value) && value == "?1"; |
| } |
| |
| // Computes the user agent value that should set for the User-Agent header. |
| std::string GetUserAgentValue(const net::HttpRequestHeaders& headers) { |
| // If Sec-CH-UA-Full is set on the headers, it means that the token for the |
| // SendFullUserAgentAfterReduction Origin Trial has been validated and we |
| // should send a reduced UA string on the request. Then check if |
| // Sec-CH-UA-Reduced is set on the headers, it means that the token for the |
| // UserAgentReduction Origin Trial has been validated and we |
| // should send a reduced UA string on the request. |
| const bool ua_reduced = DoesHeaderContainClientHint( |
| headers, network::mojom::WebClientHintsType::kUAReduced); |
| const bool ua_full = DoesHeaderContainClientHint( |
| headers, network::mojom::WebClientHintsType::kFullUserAgent); |
| return ua_full ? embedder_support::GetUserAgent() |
| : (ua_reduced ? embedder_support::GetReducedUserAgent() |
| : embedder_support::GetUserAgent()); |
| } |
| |
| } // namespace |
| |
| BaseSearchPrefetchRequest::BaseSearchPrefetchRequest( |
| const GURL& prefetch_url, |
| base::OnceClosure report_error_callback) |
| : prefetch_url_(prefetch_url), |
| report_error_callback_(std::move(report_error_callback)) {} |
| |
| BaseSearchPrefetchRequest::~BaseSearchPrefetchRequest() = default; |
| |
| // static |
| net::NetworkTrafficAnnotationTag |
| BaseSearchPrefetchRequest::NetworkAnnotationForPrefetch() { |
| return net::DefineNetworkTrafficAnnotation("search_prefetch_service", R"( |
| semantics { |
| sender: "Search Prefetch Service" |
| description: |
| "Prefetches search results page (HTML) based on omnibox hints " |
| "provided by the user's default search engine. This allows the " |
| "prefetched content to be served when the user navigates to the " |
| "omnibox hint." |
| trigger: |
| "User typing in the omnibox and the default search provider " |
| "indicates the provided omnibox hint entry is likely to be " |
| "navigated which would result in loading a search results page for " |
| "that hint." |
| data: "Credentials if user is signed in." |
| destination: OTHER |
| destination_other: "The user's default search engine." |
| } |
| policy { |
| cookies_allowed: YES |
| cookies_store: "user" |
| setting: |
| "Users can control this feature by opting out of 'Preload pages " |
| "for faster browsing and searching'" |
| chrome_policy { |
| DefaultSearchProviderEnabled { |
| policy_options {mode: MANDATORY} |
| DefaultSearchProviderEnabled: false |
| } |
| NetworkPredictionOptions { |
| NetworkPredictionOptions: 2 |
| } |
| } |
| })"); |
| } |
| |
| bool BaseSearchPrefetchRequest::StartPrefetchRequest(Profile* profile) { |
| net::NetworkTrafficAnnotationTag network_traffic_annotation = |
| NetworkAnnotationForPrefetch(); |
| |
| url::Origin prefetch_origin = url::Origin::Create(prefetch_url_); |
| |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| // This prefetch is not as high priority as navigation, but due to its |
| // navigation speeding and relatively high likelihood of being served to a |
| // navigation, the request is relatively high priority. |
| resource_request->priority = net::MEDIUM; |
| resource_request->url = prefetch_url_; |
| resource_request->credentials_mode = |
| network::mojom::CredentialsMode::kInclude; |
| resource_request->method = "GET"; |
| resource_request->mode = network::mojom::RequestMode::kNavigate; |
| resource_request->site_for_cookies = |
| net::SiteForCookies::FromUrl(prefetch_url_); |
| resource_request->destination = network::mojom::RequestDestination::kDocument; |
| resource_request->resource_type = |
| static_cast<int>(blink::mojom::ResourceType::kMainFrame); |
| resource_request->trusted_params = network::ResourceRequest::TrustedParams(); |
| // We don't handle redirects, so |kOther| makes sense here. |
| resource_request->trusted_params->isolation_info = net::IsolationInfo::Create( |
| net::IsolationInfo::RequestType::kOther, prefetch_origin, prefetch_origin, |
| resource_request->site_for_cookies); |
| resource_request->referrer_policy = net::ReferrerPolicy::NO_REFERRER; |
| |
| bool js_enabled = profile->GetPrefs() && profile->GetPrefs()->GetBoolean( |
| prefs::kWebKitJavascriptEnabled); |
| |
| AddClientHintsHeadersToPrefetchNavigation( |
| prefetch_origin, &(resource_request->headers), profile, |
| profile->GetClientHintsControllerDelegate(), |
| /*is_ua_override_on=*/false, js_enabled); |
| |
| // Tack an 'Upgrade-Insecure-Requests' header to outgoing navigational |
| // requests, as described in |
| // https://w3c.github.io/webappsec/specs/upgrade/#feature-detect |
| resource_request->headers.SetHeader("Upgrade-Insecure-Requests", "1"); |
| |
| resource_request->headers.SetHeader( |
| net::HttpRequestHeaders::kUserAgent, |
| GetUserAgentValue(resource_request->headers)); |
| resource_request->headers.SetHeader(content::kCorsExemptPurposeHeaderName, |
| "prefetch"); |
| resource_request->headers.SetHeader( |
| prefetch::headers::kSecPurposeHeaderName, |
| prefetch::headers::kSecPurposePrefetchHeaderValue); |
| resource_request->headers.SetHeader( |
| net::HttpRequestHeaders::kAccept, |
| content::FrameAcceptHeaderValue(/*allow_sxg_responses=*/true, profile)); |
| |
| #if BUILDFLAG(IS_ANDROID) |
| absl::optional<std::string> geo_header = |
| GetGeolocationHeaderIfAllowed(resource_request->url, profile); |
| if (geo_header) { |
| resource_request->headers.AddHeaderFromString(geo_header.value()); |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| |
| // Before sending out the request, allow throttles to modify the request (not |
| // the URL). The rest of the URL Loader throttle calls are captured in the |
| // navigation stack. Headers can be added by throttles at this point, which we |
| // want to capture. |
| auto wc_getter = |
| base::BindRepeating([]() -> content::WebContents* { return nullptr; }); |
| std::vector<std::unique_ptr<blink::URLLoaderThrottle>> throttles = |
| content::CreateContentBrowserURLLoaderThrottles( |
| *resource_request, profile, std::move(wc_getter), |
| /*navigation_ui_data=*/nullptr, |
| content::RenderFrameHost::kNoFrameTreeNodeId); |
| |
| auto* template_url_service = |
| TemplateURLServiceFactory::GetForProfile(profile); |
| DCHECK(template_url_service); |
| auto* default_search = template_url_service->GetDefaultSearchProvider(); |
| DCHECK(default_search); |
| |
| std::u16string prefetch_url_search_terms; |
| |
| default_search->ExtractSearchTermsFromURL( |
| prefetch_url_, template_url_service->search_terms_data(), |
| &prefetch_url_search_terms); |
| |
| bool should_defer = false; |
| for (auto& throttle : throttles) { |
| CheckForCancelledOrPausedDelegate cancel_or_pause_delegate; |
| throttle->set_delegate(&cancel_or_pause_delegate); |
| throttle->WillStartRequest(resource_request.get(), &should_defer); |
| // Make sure throttles are deleted before |cancel_or_pause_delegate| in case |
| // they call into the delegate in the destructor. |
| throttle.reset(); |
| |
| std::u16string new_url_search_terms; |
| |
| // Check that search terms still match. Google URLs can be changed by |
| // by safe search (and other features as well) Make sure the URL still has |
| // the same search terms for the DSE. |
| default_search->ExtractSearchTermsFromURL( |
| resource_request->url, template_url_service->search_terms_data(), |
| &new_url_search_terms); |
| |
| if (should_defer || new_url_search_terms != prefetch_url_search_terms || |
| cancel_or_pause_delegate.cancelled_or_paused()) { |
| return false; |
| } |
| } |
| |
| prefetch_url_ = resource_request->url; |
| |
| current_status_ = SearchPrefetchStatus::kInFlight; |
| |
| StartPrefetchRequestInternal(profile, std::move(resource_request), |
| network_traffic_annotation); |
| return true; |
| } |
| |
| void BaseSearchPrefetchRequest::CancelPrefetch() { |
| DCHECK(current_status_ == SearchPrefetchStatus::kInFlight || |
| current_status_ == SearchPrefetchStatus::kCanBeServed); |
| current_status_ = SearchPrefetchStatus::kRequestCancelled; |
| StopPrefetch(); |
| } |
| |
| void BaseSearchPrefetchRequest::ErrorEncountered() { |
| DCHECK(!report_error_callback_.is_null()); |
| DCHECK(current_status_ == SearchPrefetchStatus::kInFlight || |
| current_status_ == SearchPrefetchStatus::kCanBeServed || |
| current_status_ == SearchPrefetchStatus::kCanBeServedAndUserClicked); |
| current_status_ = SearchPrefetchStatus::kRequestFailed; |
| std::move(report_error_callback_).Run(); |
| StopPrefetch(); |
| } |
| |
| void BaseSearchPrefetchRequest::MarkPrefetchAsServable() { |
| DCHECK(current_status_ == SearchPrefetchStatus::kInFlight); |
| current_status_ = SearchPrefetchStatus::kCanBeServed; |
| } |
| |
| void BaseSearchPrefetchRequest::MarkPrefetchAsComplete() { |
| DCHECK(current_status_ == SearchPrefetchStatus::kInFlight || |
| current_status_ == SearchPrefetchStatus::kCanBeServed || |
| current_status_ == SearchPrefetchStatus::kCanBeServedAndUserClicked); |
| current_status_ = SearchPrefetchStatus::kComplete; |
| } |
| |
| void BaseSearchPrefetchRequest::MarkPrefetchAsClicked() { |
| DCHECK(current_status_ == SearchPrefetchStatus::kCanBeServed); |
| current_status_ = SearchPrefetchStatus::kCanBeServedAndUserClicked; |
| } |
| |
| bool BaseSearchPrefetchRequest::CanServePrefetchRequest( |
| const scoped_refptr<net::HttpResponseHeaders> headers) { |
| if (!headers) |
| return false; |
| |
| // Any 200 response can be served. |
| if (headers->response_code() >= net::HTTP_OK && |
| headers->response_code() < net::HTTP_MULTIPLE_CHOICES) { |
| return true; |
| } |
| |
| return false; |
| } |