// 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 <optional>
#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 {

// If set, the fallback delay has been overridden for testing.
std::optional<base::TimeDelta> g_fallback_delay;

// 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.GetNavigationHandle().GetURL().SchemeIsHTTPOrHTTPS() ||
      !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 =
      IsInterstitialEnabled(interstitial_state) ||
      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/40781148 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);
        security_interstitials::https_only_mode::FallbackReason
            fallback_reason =
                tab_helper->get_failed_upgrade_reason(handle->GetURL());
        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,
                                        fallback_reason));
        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,
                                      tab_helper->fallback_reason()));
      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()) {
    base::TimeDelta default_delay =
        IsInterstitialEnabled(interstitial_state_)
            ? features::kHttpsUpgradesAskBeforeHttpFallbackDelay.Get()
            : features::kHttpsUpgradesFallbackDelay.Get();
    base::TimeDelta delay = g_fallback_delay.value_or(default_delay);
    // Only log the first time the timeout is set.
    bool timer_started = navigation_handle()->SetNavigationTimeout(delay);
    if (timer_started) {
      RecordHttpsFirstModeNavigation(Event::kUpgradeAttempted,
                                     interstitial_state_);
      if (tab_helper->is_typed_schemeless_upgrade()) {
        RecordHttpsFirstModeNavigation(Event::kTypedSchemelessUpgradeAttempted,
                                       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_);
    if (tab_helper->is_typed_schemeless_upgrade()) {
      RecordHttpsFirstModeNavigation(Event::kTypedSchemelessUpgradeSucceeded,
                                     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;
}
