// 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 "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/renderer_host/chrome_navigation_ui_data.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_util.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/captive_portal/core/buildflags.h"
#include "components/content_settings/core/browser/content_settings_registry.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/prefs/pref_service.h"
#include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.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/is_potentially_trustworthy.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 "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/url_constants.h"

#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
#include "components/captive_portal/content/captive_portal_tab_helper.h"
#endif

#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "components/guest_view/browser/guest_view_base.h"
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

using security_interstitials::https_only_mode::RecordHttpsFirstModeNavigation;
using security_interstitials::https_only_mode::
    RecordNavigationRequestSecurityLevel;

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.GetPort().empty());
    upgrade_url.SetPortStr(port_str);
  }

  return url.ReplaceComponents(upgrade_url);
}

// 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"
      "Non-Authoritative-Reason: HttpsUpgrades\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(),
      request.request_initiator, net::HTTP_TEMPORARY_REDIRECT, new_url,
      /*referrer_policy_header=*/std::nullopt,
      /*insecure_scheme_was_upgraded=*/false);
  return redirect_info;
}

// Check whether the HTTP or HTTPS versions of the URL has "Insecure
// Content" allowed in content settings. A user can manually specify hosts
// or hostname patterns (e.g., [*.]example.com) in site settings.
bool DoesInsecureContentSettingDisableUpgrading(const GURL& url,
                                                Profile* profile) {
  // Mixed content isn't an overridable content setting on Android.
#if BUILDFLAG(IS_ANDROID)
  return false;
#else
  HostContentSettingsMap* content_settings =
      HostContentSettingsMapFactory::GetForProfile(profile);

  if (!content_settings) {
    return false;
  }

  if (content_settings->GetContentSetting(url, GURL(),
                                          ContentSettingsType::MIXEDSCRIPT) ==
      CONTENT_SETTING_ALLOW) {
    return true;
  }

  // Also check for the HTTPS version of the URL -- if an upgraded page is
  // broken and the user goes through Page Info -> Site Settings and sets
  // "Insecure Content" to be allowed, this will store a site setting only for
  // the HTTPS version of the site.
  GURL https_url = url.SchemeIsCryptographic() ? url : UpgradeUrlToHttps(url);
  if (content_settings->GetContentSetting(https_url, GURL(),
                                          ContentSettingsType::MIXEDSCRIPT) ==
      CONTENT_SETTING_ALLOW) {
    return true;
  }
  return false;
#endif
}

// Check for net errors that should not result in an HTTPS-First Mode
// interstitial. These cover cases where the error is most likely related to the
// local network conditions or a transient error, where it is more useful to
// show the user a network error page than the HTTPS-First Mode interstitial.
// (Or, in the case of HTTPS Upgrades, we don't want to fallback and allowlist
// the hostname yet -- instead we want to show the network error page and then
// retry the HTTPS upgrade again later.)
bool IsHttpsFirstModeExemptedError(int error) {
  return net::IsHostnameResolutionError(error) ||
         error == net::ERR_NETWORK_CHANGED ||
         error == net::ERR_INTERNET_DISCONNECTED ||
         error == net::ERR_ADDRESS_UNREACHABLE;
}

}  // namespace

using RequestHandler = HttpsUpgradesInterceptor::RequestHandler;
using security_interstitials::https_only_mode::Event;
using security_interstitials::https_only_mode::NavigationRequestSecurityLevel;

// static
std::unique_ptr<HttpsUpgradesInterceptor>
HttpsUpgradesInterceptor::MaybeCreateInterceptor(
    content::FrameTreeNodeId frame_tree_node_id,
    content::NavigationUIData* navigation_ui_data) {
  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;
  }

  return std::make_unique<HttpsUpgradesInterceptor>(frame_tree_node_id,
                                                    navigation_ui_data);
}

HttpsUpgradesInterceptor::HttpsUpgradesInterceptor(
    content::FrameTreeNodeId frame_tree_node_id,
    content::NavigationUIData* navigation_ui_data)
    : frame_tree_node_id_(frame_tree_node_id),
      navigation_ui_data_(navigation_ui_data) {}

HttpsUpgradesInterceptor::~HttpsUpgradesInterceptor() = default;

bool ShouldExcludeNavigationFromUpgrades(
    content::NavigationUIData* navigation_ui_data,
    content::WebContents* contents) {
  // If the URL was typed with an explicit http:// URL or is captive portal
  // login URL, it is opted-out from upgrades.
  ChromeNavigationUIData* chrome_navigation_ui_data =
      static_cast<ChromeNavigationUIData*>(navigation_ui_data);
  if (!chrome_navigation_ui_data) {
    return false;
  }
  if (!chrome_navigation_ui_data->force_no_https_upgrade()) {
    return false;
  }
  NavigationRequestSecurityLevel level =
      NavigationRequestSecurityLevel::kExplicitHttpScheme;
#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
  captive_portal::CaptivePortalTabHelper* captive_portal_tab_helper =
      captive_portal::CaptivePortalTabHelper::FromWebContents(contents);
  if (captive_portal_tab_helper->is_captive_portal_tab() ||
      captive_portal_tab_helper->is_captive_portal_window()) {
    level = NavigationRequestSecurityLevel::kCaptivePortalLogin;
  }
#endif

  RecordNavigationRequestSecurityLevel(level);
  return true;
}

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;
  }

  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/40781148 for more details.
  if (guest_view::GuestViewBase::IsGuest(frame_tree_node_id_)) {
    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);
  }

  StatefulSSLHostStateDelegate* state =
      static_cast<StatefulSSLHostStateDelegate*>(
          profile->GetSSLHostStateDelegate());
  auto* storage_partition =
      web_contents->GetPrimaryMainFrame()->GetStoragePartition();

  // Set up the interstitial state before checking any exclusions to upgrades,
  // as some may depend on this being configured.
  interstitial_state_ = std::make_unique<
      security_interstitials::https_only_mode::HttpInterstitialState>(
      ComputeInterstitialState(web_contents, tentative_resource_request.url));

  // Exclude HTTPS URLs.
  if (tentative_resource_request.url.SchemeIs(url::kHttpsScheme)) {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kSecure);
    std::move(callback).Run({});
    return;
  }

  // Exclude all other schemes other than HTTP.
  if (!tentative_resource_request.url.SchemeIs(url::kHttpScheme)) {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kOtherScheme);
    std::move(callback).Run({});
    return;
  }

  // Exclude "localhost" (and loopback addresses) as they do not expose traffic
  // over the network.
  if (net::IsLocalhost(tentative_resource_request.url)) {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kLocalhost);
    std::move(callback).Run({});
    return;
  }

  // For non-strict modes (and Incognito), skip attempting to upgrade non-unique
  // hostnames as they can't get publicly-trusted certificates.
  //
  // HTTPS-First Strict Mode does not exempt these hosts in order to ensure that
  // Chrome shows the HTTP interstitial before navigation to them. Potentially,
  // these could fast-fail instead and skip directly to the interstitial.
  if (net::IsHostnameNonUnique(tentative_resource_request.url.GetHost())) {
    if (ShouldExemptNonUniqueHostnames(*interstitial_state_)) {
      RecordNavigationRequestSecurityLevel(
          NavigationRequestSecurityLevel::kNonUniqueHostname);
      std::move(callback).Run({});
      return;
    }
  }

  // For non-strict modes (and Incognito), skip attempting to upgrade
  // single-label hostnames. After crrev.com/c/5507613, single-label hostname
  // are not guaranteed to be considered non-unique, but they are very unlikely
  // to have publicly-trusted certificates. Similarly to non-unique hostnames,
  // strict mode does not exempt these in order to ensure that Chrome shows the
  // HTTP interstitial before navigation to them.
  if (net::GetSuperdomain(tentative_resource_request.url.GetHost()).empty()) {
    // Record this as a fallback event so that we don't auto-enable HFM due to
    // the typically secure user heuristic and start showing interstitials on
    // it.
    HttpsFirstModeService* hfm_service =
        HttpsFirstModeServiceFactory::GetForProfile(profile);
    if (hfm_service) {
      hfm_service->RecordHttpsUpgradeFallbackEvent();
    }

    if (ShouldExemptNonUniqueHostnames(*interstitial_state_)) {
      RecordNavigationRequestSecurityLevel(
          NavigationRequestSecurityLevel::kSingleLabelHostname);
      std::move(callback).Run({});
      return;
    }
  }

  // Captive portals and manually-entered http:// navigations are excluded from
  // upgrades and we shouldn't warn on them when strict mode isn't enabled, so
  // allowlist those http:// connections instead.
  // TODO(crbug.com/363205521): Consider whether we want to allowlist captive
  // portal hostnames.
  if (!IsStrictInterstitialEnabled(*interstitial_state_) &&
      ShouldExcludeNavigationFromUpgrades(navigation_ui_data_, web_contents)) {
    // Only allowlist the initial host of the navigation. Server-side redirect
    // targets are not chosen by the user and shouldn't be persistently
    // allowlisted.
    if (state &&
        tentative_resource_request.navigation_redirect_chain.size() <= 1) {
      state->AllowHttpForHost(tentative_resource_request.url.GetHost(),
                              storage_partition);
    }
    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/40248833): Consider caching this result, at least within the
  // same navigation.
  auto query_complete_callback = base::BindOnce(
      &HttpsUpgradesInterceptor::MaybeCreateLoaderOnHstsQueryCompleted,
      weak_factory_.GetWeakPtr(), tentative_resource_request.url,
      tentative_resource_request.is_outermost_main_frame,
      tentative_resource_request.method,
      tentative_resource_request.transition_type, std::move(callback));
  network::mojom::NetworkContext* network_context =
      profile->GetDefaultStoragePartition()->GetNetworkContext();

  CHECK(tentative_resource_request.trusted_params);
  network_context->IsHSTSActiveForHost(
      tentative_resource_request.url.GetHost(),
      tentative_resource_request.trusted_params->isolation_info
          .IsOutermostMainFrameRequest(),
      mojo::WrapCallbackWithDefaultInvokeIfNotRun(
          std::move(query_complete_callback),
          /*is_hsts_active_for_host=*/false));
}

void HttpsUpgradesInterceptor::MaybeCreateLoaderOnHstsQueryCompleted(
    GURL url,
    bool is_outermost_main_frame,
    std::string method,
    int transition_type,
    content::URLLoaderRequestInterceptor::LoaderCallback callback,
    bool is_hsts_active_for_host) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Reconstruct objects here instead of binding them as parameters to this
  // callback method.
  //
  // It's possible for the WebContents to be destroyed during the
  // asynchronous HSTS query call, before this callback is run. If it no longer
  // exists, don't upgrade and return. (See crbug.com/40076177.)
  content::WebContents* web_contents =
      content::WebContents::FromFrameTreeNodeId(frame_tree_node_id_);
  if (!web_contents) {
    std::move(callback).Run({});
    return;
  }
  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());
  HttpsOnlyModeTabHelper* tab_helper =
      HttpsOnlyModeTabHelper::FromWebContents(web_contents);
  CHECK(profile);
  CHECK(tab_helper);

  // Don't upgrade this request if HSTS is active for this host.
  if (is_hsts_active_for_host) {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kHstsUpgraded);
    std::move(callback).Run({});
    return;
  }

  // Only serve upgrade redirects for main frame, GET requests.
  if (!is_outermost_main_frame || method != "GET") {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kInsecure);
    std::move(callback).Run({});
    return;
  }

  // For non-strict modes, skip attempting to upgrade URLs with non-default
  // ports, as these are unlikely to succeed (the server needs to support HTTP
  // and HTTPS on the same port, or the URL needs to be incorrectly have an
  // HTTP scheme but with the server's HTTPS port).
  //
  // For non-default ports, we explicitly don't allowlist the hostname, as that
  // would prevent upgrades/warnings on the entire hostname (even if the user
  // later tried to visit using default ports). This also prevents this from
  // being useful for downgrade attacks (e.g., a malicious site first calling
  // `window.open("http://example.com:5678")` before calling
  // `window.open("http://example.com")` will still result in the second
  // navigation getting upgraded/warning-on-fallback).
  //
  // For testing, treat the "HTTP port for testing" (if set) as the default.
  //
  // TODO(crbug.com/349860796): If this check is placed in `MaybeCreateLoader()`
  // above (before the async HSTS check), a few tests fail. Once the underlying
  // test issues are determined we can freely re-order this exemption check.
  if (url.has_port() && url.IntPort() != GetHttpPortForTesting()) {
    // Record this as a fallback event so that we don't auto-enable HFM due to
    // the typically secure user heuristic and start showing interstitials on
    // it.
    HttpsFirstModeService* hfm_service =
        HttpsFirstModeServiceFactory::GetForProfile(profile);
    if (hfm_service) {
      hfm_service->RecordHttpsUpgradeFallbackEvent();
    }

    // Strict is true for HFM+SE if it applies to the hostname.
    if (!IsStrictInterstitialEnabled(*interstitial_state_)) {
      // All feature variations should record the navigation metric.
      RecordNavigationRequestSecurityLevel(
          NavigationRequestSecurityLevel::kNonDefaultPorts);
      std::move(callback).Run({});
      return;
    }
  }

  // Don't upgrade navigation if it is allowlisted.
  // First, check the enterprise policy HTTP allowlist.
  PrefService* prefs = profile->GetPrefs();
  if (IsHostnameInHttpAllowlist(url, profile->GetPrefs())) {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kAllowlisted);
    std::move(callback).Run({});
    return;
  }

  // Check if the origin is specified in the
  // `--unsafely-treat-insecure-origin-as-secure` command-line flag or
  // `OverrideSecurityRestrictionsOnInsecureOrigin` policy.
  if (network::SecureOriginAllowlist::GetInstance().IsOriginAllowlisted(
          url::Origin::Create(url))) {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kAllowlisted);
    std::move(callback).Run({});
    return;
  }

  // Next check whether the HTTP or HTTPS versions of the URL has "Insecure
  // Content" allowed in content settings. We treat this as a sign to not do
  // silent HTTPS Upgrades for the site overall and not show an HTTPS-First Mode
  // interstitial for Engaged Sites. Strict HTTPS-First Mode ignores this
  // setting.
  if (!IsStrictInterstitialEnabled(*interstitial_state_) &&
      DoesInsecureContentSettingDisableUpgrading(url, profile)) {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kAllowlisted);
    std::move(callback).Run({});
    return;
  }

  // Then check whether the host has been allowlisted by the user (or by a
  // previous upgrade attempt failing).
  // TODO(crbug.com/40248833): Distinguish HTTPS-First Mode and HTTPS-Upgrades
  // allowlist entries.
  // TODO(crbug.com/40248833): 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(url.GetHost(), 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(url.GetHost(), storage_partition);

    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kInsecure);
    std::move(callback).Run({});
    return;
  }

  // If this is a back/forward navigation to a failed upgrade, then don't
  // intercept to upgrade the navigation. Other forms of re-visiting a URL
  // that previously failed to be upgraded to HTTPS *should* be intercepted so
  // the upgrade can be attempted again (e.g., the user reloading the tab, the
  // user navigating around and ending back on this URL in the same tab, etc.).
  //
  // This effectively "caches" the HTTPS-First Mode interstitial for the
  // history entry of a failed upgrade for the lifetime of the tab. This means
  // that it is possible for a user to come back much later (say, a week later),
  // after a site has fixed its HTTPS configuration, and still see the
  // interstitial for that URL.
  //
  // Without this check, resetting the HTTPS-Upgrades flags in
  // HttpsOnlyModeTabHelper::DidStartNavigation() means the Interceptor would
  // fire on back/forward navigation to the interstitial, which causes an
  // "extra" interstitial entry to be added to the history list and lose other
  // entries.
  auto* entry = web_contents->GetController().GetPendingEntry();
  if (entry && entry->GetTransitionType() & ui::PAGE_TRANSITION_FORWARD_BACK &&
      tab_helper->has_failed_upgrade(url)) {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kInsecure);
    std::move(callback).Run({});
    return;
  }

  // The `HttpsUpgradesEnabled` enterprise policy can be set to `false` to
  // disable HTTPS-Upgrades entirely. Abort if HFM is disabled and the
  // enterprise policy is set.
  if (!prefs->GetBoolean(prefs::kHttpsUpgradesEnabled) &&
      !IsInterstitialEnabled(*interstitial_state_)) {
    RecordHttpsFirstModeNavigation(Event::kUpgradeNotAttempted,
                                   *interstitial_state_);
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kAllowlisted);
    std::move(callback).Run({});
    return;
  }

  // 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.
  if (!base::FeatureList::IsEnabled(features::kHttpsUpgrades) &&
      !IsInterstitialEnabled(*interstitial_state_)) {
    // Don't upgrade the request and let the default loader continue, but record
    // that the request *would have* upgraded, had upgrading been enabled.
    RecordHttpsFirstModeNavigation(Event::kUpgradeNotAttempted,
                                   *interstitial_state_);
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kInsecure);
    std::move(callback).Run({});
    return;
  }

  if (state && state->IsHttpsEnforcedForUrl(url, storage_partition) &&
      !MustDisableSiteEngagementHeuristic(profile)) {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kHttpsEnforcedOnHostname);
  }

  // If the request URL is in the set of URLs that HttpsUpgradesInterceptor has
  // already processed, skip upgrading and trigger fallback to HTTP to avoid a
  // redirect loop.
  if (urls_seen_.contains(url)) {
    // Record failure type metrics for upgraded navigations.
    RecordHttpsFirstModeNavigation(Event::kUpgradeFailed, *interstitial_state_);
    RecordHttpsFirstModeNavigation(Event::kUpgradeRedirectLoop,
                                   *interstitial_state_);

    // If HTTPS-First Mode is not enabled (so no interstitial will be shown),
    // add the fallback hostname to the allowlist now before triggering
    // fallback. HTTPS-First Mode handles this on the user proceeding through
    // the interstitial only.
    // TODO(crbug.com/40912859): Distinguish HTTPS-First Mode and HTTPS-Upgrades
    // allowlist entries, and ensure that HTTPS-Upgrades allowlist entries don't
    // downgrade Page Info.
    // TODO(crbug.com/40248833): Move this to a helper function
    // `AddUrlToAllowlist()`, especially once this gets more complicated for
    // HFM vs. Upgrades.
    if (!IsInterstitialEnabled(*interstitial_state_)) {
      // StatefulSSLHostStateDelegate can be null during tests.
      if (state) {
        state->AllowHttpForHost(
            tab_helper->fallback_url().GetHost(),
            web_contents->GetPrimaryMainFrame()->GetStoragePartition());
      }
      // Also record this fallback event so that we can auto-enable HFM based on
      // browsing patterns later on.
      HttpsFirstModeService* hfm_service =
          HttpsFirstModeServiceFactory::GetForProfile(profile);
      // HttpsFirstModeService can be null in tests.
      if (hfm_service) {
        hfm_service->RecordHttpsUpgradeFallbackEvent();
      }
    }

    tab_helper->set_is_navigation_upgraded(false);
    tab_helper->set_is_navigation_fallback(true);
    tab_helper->set_fallback_reason(
        security_interstitials::https_only_mode::FallbackReason::kRedirectLoop);
    tab_helper->add_failed_upgrade(
        tab_helper->fallback_url(),
        security_interstitials::https_only_mode::FallbackReason::kRedirectLoop);

    // Note: If `fallback_url` is the same as the request URL, this
    // could skip doing an additional redirect, but then the NavigationThrottle
    // doesn't have the ability to act on this navigation request and apply
    // the HTTPS-First Mode interstitial. If we add a better way to "fast fail"
    // navigations directly to the interstitial, then we could probably use that
    // here as well as an optimization.
    std::move(callback).Run(CreateRedirectHandler(tab_helper->fallback_url()));
    return;
  }
  // Not a redirect loop. Add the current request URL to the set of URLs seen.
  urls_seen_.insert(url);

  ChromeNavigationUIData* chrome_navigation_ui_data =
      static_cast<ChromeNavigationUIData*>(navigation_ui_data_);
  bool is_explicit_http = chrome_navigation_ui_data &&
                          chrome_navigation_ui_data->force_no_https_upgrade();
  bool is_from_address_bar =
      (transition_type & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) != 0;
  bool is_typed_schemeless_upgrade =
      is_from_address_bar && !is_explicit_http &&
      base::FeatureList::IsEnabled(
          features::kHttpsUpgradesTypedSchemelessNavigationNoTimeoutFallback);

  if (is_typed_schemeless_upgrade) {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kTypedSchemelessUpgraded);
  } else {
    RecordNavigationRequestSecurityLevel(
        NavigationRequestSecurityLevel::kUpgraded);
  }

  // Mark navigation as upgraded.
  tab_helper->set_is_navigation_upgraded(true);
  if (is_typed_schemeless_upgrade) {
    tab_helper->set_is_typed_schemeless_upgrade(true);
  }
  tab_helper->set_fallback_url(url);

  GURL https_url = UpgradeUrlToHttps(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::PendingReceiver<network::mojom::URLLoaderClient>* client_receiver,
    blink::ThrottlingURLLoader* url_loader) {
  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;
  }

  // If the navigation was blocked by a client-side feature (e.g. Safe Browsing
  // or an extension), do not attempt to fallback to HTTP. This is a local
  // block, not a server-side HTTPS support failure.
  if (status.error_code == net::ERR_BLOCKED_BY_CLIENT) {
    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 || !tab_helper->is_navigation_upgraded()) {
    return false;
  }

  // interstitial_state_ is only created after the initial checks in
  // MaybeCreateLoader(). If it wasn't created, the load wasn't eligible for
  // upgrades, so ignore the load here as well. Also explicitly ignore non-main
  // frame loads because we don't want to trigger a fallback navigation in
  // non-main frames.
  // This is a fix for crbug.com/40909795.
  if (!interstitial_state_ || !request.is_outermost_main_frame) {
    return false;
  }

  // If the navigation resulted in an exempted transient network error, don't
  // intercept the failure and allow the normal net error page to trigger. Set
  // the exempt-error flag so if the net error page is reloaded we can try
  // continuing with the upgrade (and fallback to the original URL if the
  // transient network error goes away and the navigation fails).
  // Currently this is only done if the HTTPS-First Mode interstitial is
  // enabled, because otherwise these would cause the interstitial to be shown
  // which is confusing. HTTPS-Upgrades will silently fall back to HTTP for
  // these errors.
  //
  // However, if this is a request to a non-unique hostname, don't prefer
  // net error as it is likely non-recoverable -- we want to fallback to HTTP
  // and the HTTPS-First Mode interstitial in this case. (In particular, this
  // avoids breaking corporate single-label hostnames.) Treat all single-label
  // hostnames as if they were non-unique, since while unique single-label hosts
  // (i.e. TLDs on the PSL) could get a cert, it's more likely they're being
  // used as a corporate hostname. These domains are safe to exclude since this
  // only results in potentially show an extra HFM warning before the net error.
  if (IsInterstitialEnabled(*interstitial_state_) &&
      IsHttpsFirstModeExemptedError(status.error_code) &&
      !net::IsHostnameNonUnique(request.url.GetHost()) &&
      !net::GetSuperdomain(request.url.GetHost()).empty()) {
    tab_helper->set_is_exempt_error(true);
    return false;
  }

  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());
  StatefulSSLHostStateDelegate* state =
      static_cast<StatefulSSLHostStateDelegate*>(
          profile->GetSSLHostStateDelegate());

  // Record failure type metrics for upgraded navigations.
  RecordHttpsFirstModeNavigation(Event::kUpgradeFailed, *interstitial_state_);
  security_interstitials::https_only_mode::FallbackReason fallback_reason =
      security_interstitials::https_only_mode::FallbackReason::kNetError;
  if (net::IsCertificateError(status.error_code)) {
    RecordHttpsFirstModeNavigation(Event::kUpgradeCertError,
                                   *interstitial_state_);
    fallback_reason =
        security_interstitials::https_only_mode::FallbackReason::kCertError;
  } else if (status.error_code == net::ERR_TIMED_OUT) {
    RecordHttpsFirstModeNavigation(Event::kUpgradeTimedOut,
                                   *interstitial_state_);
    fallback_reason =
        security_interstitials::https_only_mode::FallbackReason::kTimerFired;
  } else {
    RecordHttpsFirstModeNavigation(Event::kUpgradeNetError,
                                   *interstitial_state_);
  }

  if (tab_helper->is_typed_schemeless_upgrade() &&
      status.error_code == net::ERR_TIMED_OUT) {
    RecordHttpsFirstModeNavigation(Event::kTypedSchemelessUpgradeTimedOut,
                                   *interstitial_state_);
    tab_helper->set_is_navigation_upgraded(false);
    tab_helper->set_is_typed_schemeless_upgrade(false);
    return false;
  }

  // If no interstitial will be shown, add the fallback hostname to the
  // allowlist now before triggering fallback. The interstitial handles this on
  // the user proceeding through the interstitial only.
  if (!IsInterstitialEnabled(*interstitial_state_)) {
    // StatefulSSLHostStateDelegate can be null during tests.
    if (state) {
      state->AllowHttpForHost(
          tab_helper->fallback_url().GetHost(),
          web_contents->GetPrimaryMainFrame()->GetStoragePartition());
    }

    // Also record this fallback event so that we can auto-enable HFM based on
    // browsing patterns later on.
    HttpsFirstModeService* hfm_service =
        HttpsFirstModeServiceFactory::GetForProfile(profile);
    // HttpsFirstModeService can be null in tests.
    if (hfm_service) {
      hfm_service->RecordHttpsUpgradeFallbackEvent();
    }
  }

  tab_helper->set_is_navigation_upgraded(false);
  tab_helper->set_is_typed_schemeless_upgrade(false);
  tab_helper->set_is_navigation_fallback(true);
  tab_helper->set_fallback_reason(fallback_reason);
  tab_helper->add_failed_upgrade(tab_helper->fallback_url(), fallback_reason);

  // `client_` may have been previously bound 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_);

  // Set up Mojo connection and initiate the redirect. `client_` and `receiver_`
  // may have been previously bound from handling a previous upgrade earlier in
  // the same navigation, so reset them before re-binding them to handle a new
  // redirect.
  receiver_.reset();
  client_.reset();
  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();
}
