blob: 9e59d70a933e7f4d6b12159ef3958e7b1f4d826d [file] [log] [blame]
// 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 "chrome/browser/ssl/https_upgrades_navigation_throttle.h"
#include <utility>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/https_first_mode_settings_tracker.h"
#include "chrome/browser/ssl/https_only_mode_tab_helper.h"
#include "chrome/browser/ssl/https_upgrades_navigation_throttle.h"
#include "chrome/browser/ssl/https_upgrades_util.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/security_interstitials/content/security_interstitial_tab_helper.h"
#include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h"
#include "components/security_interstitials/core/features.h"
#include "components/security_interstitials/core/https_only_mode_metrics.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/navigation_throttle.h"
#include "content/public/browser/web_contents.h"
#include "net/base/net_errors.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "ui/base/page_transition_types.h"
using security_interstitials::https_only_mode::Event;
namespace {
// Time that the throttle will wait before canceling the upgraded navigation and
// showing the HTTPS-First Mode interstitial.
base::TimeDelta g_fallback_delay = base::Seconds(3);
// TODO(crbug.com/351990829): Consider setting the page title and favicon to be
// more like interstitials.
inline constexpr char kBlankPageHtml[] = "<html></html>";
} // namespace
// static
void HttpsUpgradesNavigationThrottle::MaybeCreateAndAdd(
content::NavigationThrottleRegistry& registry,
std::unique_ptr<SecurityBlockingPageFactory> blocking_page_factory,
Profile* profile) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// HTTPS-First Mode is only relevant for primary main-frame HTTP(S)
// navigations.
content::NavigationHandle& handle = registry.GetNavigationHandle();
if (!registry.IsHTTPOrHTTPS() || !handle.IsInPrimaryMainFrame() ||
handle.IsSameDocument()) {
return;
}
security_interstitials::https_only_mode::HttpInterstitialState
interstitial_state =
ComputeInterstitialState(handle.GetWebContents(), handle.GetURL());
HttpsFirstModeService* hfm_service =
HttpsFirstModeServiceFactory::GetForProfile(profile);
if (hfm_service) {
// Can be null in some cases, e.g. when using Ash sign-in profile.
hfm_service->IncrementRecentNavigationCount();
}
bool https_upgrades_enabled =
interstitial_state.enabled_by_pref ||
base::FeatureList::IsEnabled(features::kHttpsUpgrades);
if (!https_upgrades_enabled) {
return;
}
// Ensure that the HttpsOnlyModeTabHelper has been created (this does nothing
// if it has already been created for the WebContents). There are cases where
// the tab helper won't get created by the initialization in
// chrome/browser/ui/tab_helpers.cc but the criteria for adding the throttle
// are still met (see crbug.com/1233889 for one example).
HttpsOnlyModeTabHelper::CreateForWebContents(handle.GetWebContents());
registry.AddThrottle(std::make_unique<HttpsUpgradesNavigationThrottle>(
registry, profile, std::move(blocking_page_factory), interstitial_state));
}
HttpsUpgradesNavigationThrottle::HttpsUpgradesNavigationThrottle(
content::NavigationThrottleRegistry& registry,
Profile* profile,
std::unique_ptr<SecurityBlockingPageFactory> blocking_page_factory,
security_interstitials::https_only_mode::HttpInterstitialState
interstitial_state)
: content::NavigationThrottle(registry),
profile_(profile),
blocking_page_factory_(std::move(blocking_page_factory)),
interstitial_state_(interstitial_state) {}
HttpsUpgradesNavigationThrottle::~HttpsUpgradesNavigationThrottle() = default;
content::NavigationThrottle::ThrottleCheckResult
HttpsUpgradesNavigationThrottle::WillStartRequest() {
// If the navigation is fallback to HTTP, trigger the HTTP interstitial (if
// enabled). The interceptor creates a redirect for the fallback navigation,
// which will trigger MaybeCreateLoader() in the interceptor for the redirect
// but *doesn't* trigger WillStartRequest() because it's all part of the same
// request. Here, we skip directly to showing the HTTP interstitial if this
// is:
// (1) a back/forward navigation, and
// (2) the URL already failed upgrades before.
// This lets us avoid triggering the Interceptor during a back/forward
// navigation (which breaks history state) and acts like the browser
// "remembering" the state of the tab as being on the interstitial for that
// URL.
//
// Other cases for starting a navigation to a URL that previously failed
// to be upgraded should go through the full upgrade flow -- better to assume
// that something may have changed in the time since. For example: a user
// reloading the tab showing the interstitial should re-try the upgrade.
auto* handle = navigation_handle();
auto* contents = handle->GetWebContents();
auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(contents);
if ((handle->GetPageTransition() & ui::PAGE_TRANSITION_FORWARD_BACK &&
tab_helper->has_failed_upgrade(handle->GetURL())) &&
!handle->GetURL().SchemeIsCryptographic()) {
if (IsInterstitialEnabled(interstitial_state_)) {
security_interstitials::https_only_mode::RecordInterstitialReason(
interstitial_state_);
// Mark this as a fallback HTTP navigation and trigger the interstitial.
tab_helper->set_is_navigation_fallback(true);
if (base::FeatureList::IsEnabled(
security_interstitials::features::kHttpsFirstDialogUi)) {
// New dialog UI.
// Rewrite the response to a blank page. DidFinishNavigation will
// show the ABH dialog on top of this blank page.
return {content::NavigationThrottle::CANCEL, net::ERR_BLOCKED_BY_CLIENT,
kBlankPageHtml};
} else {
// Old full-page interstitial UI.
ukm::SourceId source_id =
ukm::ConvertToSourceId(navigation_handle()->GetNavigationId(),
ukm::SourceIdType::NAVIGATION_ID);
std::unique_ptr<security_interstitials::HttpsOnlyModeBlockingPage>
blocking_page =
blocking_page_factory_->CreateHttpsOnlyModeBlockingPage(
contents, handle->GetURL(), interstitial_state_,
/*url_type_param=*/std::nullopt,
base::BindRepeating(&RecordHttpsFirstModeUKM, source_id));
std::string interstitial_html = blocking_page->GetHTMLContents();
security_interstitials::SecurityInterstitialTabHelper::
AssociateBlockingPage(handle, std::move(blocking_page));
return content::NavigationThrottle::ThrottleCheckResult(
content::NavigationThrottle::CANCEL, net::ERR_BLOCKED_BY_CLIENT,
std::move(interstitial_html));
}
}
// Otherwise, just record metrics and continue.
// TODO(crbug.com/40904694): Record a separate histogram for Site Engagement
// heuristic.
}
// TODO(crbug.com/40064769): There are some cases where the navigation may
// "restart", such as if we encounter an exempted transient network error on
// the upgraded HTTPS URL, show a net error page, and then reload the tab. In
// these cases the navigation will proceed with the upgrade/fallback logic,
// but the navigation timeout will no longer be set. Currently, re-starting
// the timer here would trigger a DCHECK in NavigationRequest.
// Navigation is HTTPS or an initial HTTP navigation (which will get
// upgraded by the interceptor). Fallback HTTP navigations are handled in
// WillRedirectRequest().
return content::NavigationThrottle::ThrottleAction::PROCEED;
}
content::NavigationThrottle::ThrottleCheckResult
HttpsUpgradesNavigationThrottle::WillFailRequest() {
// Fallback to HTTP on navigation failure is handled by
// HttpsUpgradesInterceptor::MaybeCreateLoaderForResponse().
return content::NavigationThrottle::PROCEED;
}
content::NavigationThrottle::ThrottleCheckResult
HttpsUpgradesNavigationThrottle::WillRedirectRequest() {
// If the navigation is doing a fallback redirect to HTTP, trigger the HTTP
// interstitial (if enabled). The interceptor creates a redirect for the
// fallback navigation, which will trigger MaybeCreateLoader() in the
// interceptor for the redirect but *doesn't* trigger WillStartRequest()
// because it's all part of the same request.
auto* handle = navigation_handle();
auto* contents = handle->GetWebContents();
auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(contents);
// Slow loading fallback URLs should be handled by the net stack.
if (tab_helper->is_navigation_fallback()) {
handle->CancelNavigationTimeout();
}
ukm::SourceId source_id = ukm::ConvertToSourceId(
navigation_handle()->GetNavigationId(), ukm::SourceIdType::NAVIGATION_ID);
if (tab_helper->is_navigation_fallback() &&
!handle->GetURL().SchemeIsCryptographic() &&
IsInterstitialEnabled(interstitial_state_)) {
security_interstitials::https_only_mode::RecordInterstitialReason(
interstitial_state_);
if (base::FeatureList::IsEnabled(
security_interstitials::features::kHttpsFirstDialogUi)) {
// New dialog UI.
// Rewrite the response to a blank page and cancel the navigation.
// HttpsOnlyModeTabHelper::DidFinishNavigation() will
// show the ABH dialog on top of this blank page.
return {content::NavigationThrottle::CANCEL, net::ERR_BLOCKED_BY_CLIENT,
kBlankPageHtml};
} else {
std::unique_ptr<security_interstitials::HttpsOnlyModeBlockingPage>
blocking_page =
blocking_page_factory_->CreateHttpsOnlyModeBlockingPage(
contents, handle->GetURL(), interstitial_state_,
/*url_type_param=*/std::nullopt,
base::BindRepeating(&RecordHttpsFirstModeUKM, source_id));
std::string interstitial_html = blocking_page->GetHTMLContents();
security_interstitials::SecurityInterstitialTabHelper::
AssociateBlockingPage(handle, std::move(blocking_page));
return content::NavigationThrottle::ThrottleCheckResult(
content::NavigationThrottle::CANCEL, net::ERR_BLOCKED_BY_CLIENT,
std::move(interstitial_html));
}
}
// Otherwise, just record metrics and continue.
// TODO(crbug.com/40904694): Record a separate histogram for Site Engagement
// heuristic.
// If the navigation was upgraded by the Interceptor, then the Throttle's
// WillRedirectRequest() will get triggered by the artificial redirect to
// HTTPS. The HTTPS upgrade will always happen after the Throttle's
// WillStartRequest() (which only checks for fallback HTTP), so tracking
// upgraded requests is deferred to WillRedirectRequest() here. Which
// navigations to upgrade is determined by the Interceptor, not the Throttle.
//
// The navigation may get upgraded at various points during redirects:
// 1. The Interceptor serves an artificial redirect to HTTPS if the
// navigation is upgraded. This means the Throttle will see the upgraded
// navigation state for the first time here in WillRedirectRequest().
// 2. HTTPS->HTTP downgrades can occur later in the lifecycle of a
// navigation, and will also result in the Interceptor serving an
// artificial redirect to upgrade the navigation.
//
// The Interceptor logs the URLs that it sees and triggers fallback if it
// encounters a redirect loop. Any cases that might not be caught by the
// Interceptor should result in net::ERR_TOO_MANY_REDIRECTS, but in general
// redirect loops should hit the cache and not cost too much. If they go too
// long, the fallback timer will kick in. ERR_TOO_MANY_REDIRECTS should result
// in the request failing and triggering fallback.
if (tab_helper->is_navigation_upgraded()) {
// Check if the timer is already started, as there may be additional
// redirects on the navigation after the artificial upgrade redirect.
bool timer_started =
navigation_handle()->SetNavigationTimeout(g_fallback_delay);
if (timer_started) {
RecordHttpsFirstModeNavigation(Event::kUpgradeAttempted,
interstitial_state_);
}
}
return content::NavigationThrottle::PROCEED;
}
content::NavigationThrottle::ThrottleCheckResult
HttpsUpgradesNavigationThrottle::WillProcessResponse() {
// Clear the status for this navigation as it will successfully commit.
auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(
navigation_handle()->GetWebContents());
if (tab_helper->is_navigation_upgraded()) {
RecordHttpsFirstModeNavigation(Event::kUpgradeSucceeded,
interstitial_state_);
tab_helper->set_is_navigation_upgraded(false);
}
// Clear the fallback flag, if set.
tab_helper->set_is_navigation_fallback(false);
return content::NavigationThrottle::PROCEED;
}
const char* HttpsUpgradesNavigationThrottle::GetNameForLogging() {
return "HttpsUpgradesNavigationThrottle";
}
// static
void HttpsUpgradesNavigationThrottle::set_timeout_for_testing(
base::TimeDelta timeout) {
g_fallback_delay = timeout;
}