blob: 75bf49784d0883adcfbd322619382a46b3d7722c [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_interceptor.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ssl/https_only_mode_tab_helper.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h"
#include "components/security_interstitials/core/https_only_mode_metrics.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/url_loader_request_interceptor.h"
#include "content/public/browser/web_contents.h"
#include "extensions/buildflags/buildflags.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "net/base/url_util.h"
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
#include "url/url_constants.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "components/guest_view/browser/guest_view_base.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
namespace {
// Used to handle upgrading/fallback for tests using EmbeddedTestServer which
// uses random ports.
int g_https_port_for_testing = 0;
int g_http_port_for_testing = 0;
// Updates a URL to HTTPS. URLs with the default port will result in the HTTPS
// URL using the default port 443. URLs with non-default ports won't have the
// port changed. For tests, the HTTPS port used can be overridden with
// HttpsUpgradesInterceptor::SetHttpsPortForTesting().
GURL UpgradeUrlToHttps(const GURL& url) {
DCHECK(!url.SchemeIsCryptographic());
// Replace scheme with HTTPS.
GURL::Replacements upgrade_url;
upgrade_url.SetSchemeStr(url::kHttpsScheme);
// For tests that use the EmbeddedTestServer, the server's port needs to be
// specified as it can't use the default ports.
int https_port_for_testing =
HttpsUpgradesInterceptor::GetHttpsPortForTesting();
// `port_str` must be in scope for the call to ReplaceComponents() below.
const std::string port_str = base::NumberToString(https_port_for_testing);
if (https_port_for_testing) {
// Only reached in testing, where the original URL will always have a
// non-default port.
DCHECK(!url.port().empty());
upgrade_url.SetPortStr(port_str);
}
return url.ReplaceComponents(upgrade_url);
}
// Only serve upgrade redirects for main frame, GET requests to HTTP URLs. This
// excludes "localhost" (and loopback addresses) as they do not expose traffic
// over the network.
// TODO(crbug.com/1394910): Extend the exemption list for HTTPS-Upgrades
// beyond just localhost.
bool ShouldCreateLoader(const network::ResourceRequest& resource_request,
HttpsOnlyModeTabHelper* tab_helper) {
if (resource_request.is_outermost_main_frame &&
resource_request.method == "GET" &&
!net::IsLocalhost(resource_request.url) &&
resource_request.url.SchemeIs(url::kHttpScheme) &&
!tab_helper->is_navigation_fallback()) {
return true;
}
return false;
}
// Helper to record an HTTPS-First Mode navigation event.
// TODO(crbug.com/1394910): Rename these metrics now that they apply to both
// HTTPS-First Mode and HTTPS Upgrades.
void RecordHttpsFirstModeNavigation(
security_interstitials::https_only_mode::Event event) {
base::UmaHistogramEnumeration(
security_interstitials::https_only_mode::kEventHistogram, event);
}
// Helper to configure an artificial redirect to `new_url`. This configures
// `response_head` and returns a computed RedirectInfo so both can be passed to
// URLLoaderClient::OnReceiveRedirect() to trigger the redirect.
net::RedirectInfo SetupRedirect(
const network::ResourceRequest& request,
const GURL& new_url,
network::mojom::URLResponseHead* response_head) {
response_head->encoded_data_length = 0;
response_head->request_start = base::TimeTicks::Now();
response_head->response_start = response_head->request_start;
std::string header_string = base::StringPrintf(
"HTTP/1.1 %i Temporary Redirect\n"
"Location: %s\n",
net::HTTP_TEMPORARY_REDIRECT, new_url.spec().c_str());
response_head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(header_string));
net::RedirectInfo redirect_info = net::RedirectInfo::ComputeRedirectInfo(
request.method, request.url, request.site_for_cookies,
request.update_first_party_url_on_redirect
? net::RedirectInfo::FirstPartyURLPolicy::UPDATE_URL_ON_REDIRECT
: net::RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL,
request.referrer_policy, request.referrer.spec(),
net::HTTP_TEMPORARY_REDIRECT, new_url,
/*referrer_policy_header=*/absl::nullopt,
/*insecure_scheme_was_upgraded=*/false);
return redirect_info;
}
} // namespace
using RequestHandler = HttpsUpgradesInterceptor::RequestHandler;
using security_interstitials::https_only_mode::Event;
// static
std::unique_ptr<HttpsUpgradesInterceptor>
HttpsUpgradesInterceptor::MaybeCreateInterceptor(int frame_tree_node_id) {
auto* web_contents =
content::WebContents::FromFrameTreeNodeId(frame_tree_node_id);
// Could be null if the FrameTreeNode's RenderFrameHost is shutting down.
if (!web_contents) {
return nullptr;
}
// If there isn't a BrowserContext/Profile for this, then just allow it.
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
if (!profile ||
!g_browser_process->profile_manager()->IsValidProfile(profile)) {
return nullptr;
}
PrefService* prefs = profile->GetPrefs();
// Both HTTPS-First Mode and HTTPS-Upgrades are forms of upgrading all HTTP
// navigations to HTTPS, with HTTPS-First Mode additionally enabling the
// HTTP interstitial on fallback.
bool https_first_mode_enabled =
base::FeatureList::IsEnabled(features::kHttpsFirstModeV2) && prefs &&
prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled);
bool https_upgrades_enabled =
https_first_mode_enabled ||
base::FeatureList::IsEnabled(features::kHttpsUpgrades);
if (!https_upgrades_enabled) {
return nullptr;
}
return std::make_unique<HttpsUpgradesInterceptor>(frame_tree_node_id,
https_first_mode_enabled);
}
HttpsUpgradesInterceptor::HttpsUpgradesInterceptor(
int frame_tree_node_id,
bool http_interstitial_enabled)
: frame_tree_node_id_(frame_tree_node_id),
http_interstitial_enabled_(http_interstitial_enabled) {}
HttpsUpgradesInterceptor::~HttpsUpgradesInterceptor() = default;
void HttpsUpgradesInterceptor::MaybeCreateLoader(
const network::ResourceRequest& tentative_resource_request,
content::BrowserContext* browser_context,
content::URLLoaderRequestInterceptor::LoaderCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Note: Redirects cause a restarted request with a new call to
// MaybeCreateLoader().
// If there isn't a BrowserContext/Profile for this, then just allow it.
Profile* profile = Profile::FromBrowserContext(browser_context);
if (!profile ||
!g_browser_process->profile_manager()->IsValidProfile(profile)) {
std::move(callback).Run({});
return;
}
// TODO(crbug.com/1394910): Check for HttpsUpgrades and HttpsAllowlist
// enterprise policies as well. It might be best to consolidate these checks
// into the HttpsUpgradesNavigationThrottle which sees the navigation first.
auto* prefs = profile->GetPrefs();
bool https_first_mode_enabled =
base::FeatureList::IsEnabled(features::kHttpsFirstModeV2) && prefs &&
prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled);
bool https_upgrades_enabled =
base::FeatureList::IsEnabled(features::kHttpsUpgrades) ||
https_first_mode_enabled;
if (!https_upgrades_enabled) {
// Don't upgrade the request and let the default loader continue.
std::move(callback).Run({});
return;
}
auto* web_contents =
content::WebContents::FromFrameTreeNodeId(frame_tree_node_id_);
// Could be null if the FrameTreeNode's RenderFrameHost is shutting down.
if (!web_contents) {
std::move(callback).Run({});
return;
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
// If this is a GuestView (e.g., Chrome Apps <webview>) then HTTPS-First Mode
// should not apply. See crbug.com/1233889 for more details.
if (guest_view::GuestViewBase::IsGuest(web_contents)) {
std::move(callback).Run({});
return;
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(web_contents);
if (!tab_helper) {
HttpsOnlyModeTabHelper::CreateForWebContents(web_contents);
tab_helper = HttpsOnlyModeTabHelper::FromWebContents(web_contents);
}
// Don't upgrade navigation if it is allowlisted.
// TODO(crbug.com/1394910): Distinguish HTTPS-First Mode and HTTPS-Upgrades
// allowlist entries, and ensure that HTTPS-Upgrades allowlist entries don't
// downgrade Page Info.
// TODO(crbug.com/1394910): Move this to a helper function `IsAllowlisted()`,
// especially once this gets more complicated for HFM vs. Upgrades.
StatefulSSLHostStateDelegate* state =
static_cast<StatefulSSLHostStateDelegate*>(
profile->GetSSLHostStateDelegate());
// StatefulSSLHostStateDelegate can be null during tests.
auto* storage_partition =
web_contents->GetPrimaryMainFrame()->GetStoragePartition();
if (state && state->IsHttpAllowedForHost(
tentative_resource_request.url.host(), storage_partition)) {
// Renew the allowlist expiration for this host as the user is still
// actively using it. This means that the allowlist entry will stay
// valid until the user stops visiting this host for the entire
// expiration period (one week).
state->AllowHttpForHost(tentative_resource_request.url.host(),
storage_partition);
std::move(callback).Run({});
return;
}
if (!ShouldCreateLoader(tentative_resource_request, tab_helper)) {
std::move(callback).Run({});
return;
}
// Check whether this host would be upgraded to HTTPS by HSTS. This requires a
// Mojo call to the network service, so set up a callback to continue the rest
// of the MaybeCreateLoader() logic (passing along the necessary state). The
// HSTS status will be passed as a boolean to
// MaybeCreateLoaderOnHstsQueryCompleted(). If the Mojo call fails, this will
// default to passing `false` and continuing as though the host does not have
// HSTS (i.e., it will proceed with the HTTPS-First Mode logic).
// TODO(crbug.com/1394910): Consider caching this result, at least within the
// same navigation.
auto query_complete_callback = base::BindOnce(
&HttpsUpgradesInterceptor::MaybeCreateLoaderOnHstsQueryCompleted,
weak_factory_.GetWeakPtr(), tentative_resource_request,
std::move(callback), tab_helper);
network::mojom::NetworkContext* network_context =
profile->GetDefaultStoragePartition()->GetNetworkContext();
network_context->IsHSTSActiveForHost(
tentative_resource_request.url.host(),
mojo::WrapCallbackWithDefaultInvokeIfNotRun(
std::move(query_complete_callback),
/*is_hsts_active_for_host=*/false));
}
void HttpsUpgradesInterceptor::MaybeCreateLoaderOnHstsQueryCompleted(
const network::ResourceRequest& tentative_resource_request,
content::URLLoaderRequestInterceptor::LoaderCallback callback,
HttpsOnlyModeTabHelper* tab_helper,
bool is_hsts_active_for_host) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Don't upgrade this request if HSTS is active for this host.
if (is_hsts_active_for_host) {
std::move(callback).Run({});
return;
}
// Mark navigation as upgraded.
tab_helper->set_is_navigation_upgraded(true);
tab_helper->set_fallback_url(tentative_resource_request.url);
GURL https_url = UpgradeUrlToHttps(tentative_resource_request.url);
std::move(callback).Run(CreateRedirectHandler(https_url));
}
bool HttpsUpgradesInterceptor::MaybeCreateLoaderForResponse(
const network::URLLoaderCompletionStatus& status,
const network::ResourceRequest& request,
network::mojom::URLResponseHeadPtr* response_head,
mojo::ScopedDataPipeConsumerHandle* response_body,
mojo::PendingRemote<network::mojom::URLLoader>* loader,
mojo::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver,
blink::ThrottlingURLLoader* url_loader,
bool* skip_other_interceptors,
bool* will_return_unsafe_redirect) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// When an upgraded navigation fails, this method creates a loader to trigger
// the fallback to HTTP.
//
// Note: MaybeCreateLoaderForResponse() is called for all navigation
// responses and failures, but not for things like a NavigationThrottle
// cancelling or blocking the navigation.
// Only intercept if the navigation failed.
if (status.error_code == net::OK) {
return false;
}
auto* web_contents =
content::WebContents::FromFrameTreeNodeId(frame_tree_node_id_);
if (!web_contents) {
// `web_contents` can be null if the tab is being closed. Skip handling
// failure in that case since the page is going away anyway.
return false;
}
auto* tab_helper = HttpsOnlyModeTabHelper::FromWebContents(web_contents);
if (!tab_helper->is_navigation_upgraded()) {
return false;
}
// Record failure type metrics for upgraded navigations.
RecordHttpsFirstModeNavigation(Event::kUpgradeFailed);
if (net::IsCertificateError(status.error_code)) {
RecordHttpsFirstModeNavigation(Event::kUpgradeCertError);
} else if (status.error_code == net::ERR_TIMED_OUT) {
RecordHttpsFirstModeNavigation(Event::kUpgradeTimedOut);
} else {
RecordHttpsFirstModeNavigation(Event::kUpgradeNetError);
}
// If HTTPS-First Mode is not enabled (so no interstitial will be shown),
// add the hostname to the allowlist now before triggering fallback.
// HTTPS-First Mode handles this on the user proceeding through the
// interstitial only.
// TODO(crbug.com/1394910): Distinguish HTTPS-First Mode and HTTPS-Upgrades
// allowlist entries, and ensure that HTTPS-Upgrades allowlist entries don't
// downgrade Page Info.
// TODO(crbug.com/1394910): Move this to a helper function
// `AddUrlToAllowlist()`, especially once this gets more complicated for
// HFM vs. Upgrades.
if (!http_interstitial_enabled_) {
Profile* profile =
Profile::FromBrowserContext(web_contents->GetBrowserContext());
StatefulSSLHostStateDelegate* state =
static_cast<StatefulSSLHostStateDelegate*>(
profile->GetSSLHostStateDelegate());
// StatefulSSLHostStateDelegate can be null during tests.
if (state) {
state->AllowHttpForHost(
request.url.host(),
web_contents->GetPrimaryMainFrame()->GetStoragePartition());
}
}
tab_helper->set_is_navigation_upgraded(false);
tab_helper->set_is_navigation_fallback(true);
// `client_` may have been previously boudn from handling the initial upgrade
// in MaybeCreateLoader(), so reset it before re-binding it to handle this
// response.
client_.reset();
*client_receiver = client_.BindNewPipeAndPassReceiver();
// Create an artificial redirect back to the fallback URL.
auto new_response_head = network::mojom::URLResponseHead::New();
net::RedirectInfo redirect_info = SetupRedirect(
request, tab_helper->fallback_url(), new_response_head.get());
client_->OnReceiveRedirect(redirect_info, std::move(new_response_head));
return true;
}
// static
void HttpsUpgradesInterceptor::SetHttpsPortForTesting(int port) {
g_https_port_for_testing = port;
}
// static
void HttpsUpgradesInterceptor::SetHttpPortForTesting(int port) {
g_http_port_for_testing = port;
}
// static
int HttpsUpgradesInterceptor::GetHttpsPortForTesting() {
return g_https_port_for_testing;
}
// static
int HttpsUpgradesInterceptor::GetHttpPortForTesting() {
return g_http_port_for_testing;
}
RequestHandler HttpsUpgradesInterceptor::CreateRedirectHandler(
const GURL& new_url) {
return base::BindOnce(&HttpsUpgradesInterceptor::RedirectHandler,
weak_factory_.GetWeakPtr(), new_url);
}
void HttpsUpgradesInterceptor::RedirectHandler(
const GURL& new_url,
const network::ResourceRequest& request,
mojo::PendingReceiver<network::mojom::URLLoader> receiver,
mojo::PendingRemote<network::mojom::URLLoaderClient> client) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!receiver_.is_bound());
DCHECK(!client_.is_bound());
// Set up Mojo connection and initiate the redirect.
receiver_.Bind(std::move(receiver));
receiver_.set_disconnect_handler(
base::BindOnce(&HttpsUpgradesInterceptor::OnConnectionClosed,
weak_factory_.GetWeakPtr()));
client_.Bind(std::move(client));
// Create redirect.
auto response_head = network::mojom::URLResponseHead::New();
net::RedirectInfo redirect_info =
SetupRedirect(request, new_url, response_head.get());
client_->OnReceiveRedirect(redirect_info, std::move(response_head));
}
void HttpsUpgradesInterceptor::OnConnectionClosed() {
// This happens when content cancels the navigation. Reset the receiver and
// client handle.
receiver_.reset();
client_.reset();
}