910e62b5创建于 1月15日历史提交
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/preloading/prerender/prerender_navigation_throttle.h"

#include "base/memory/ptr_util.h"
#include "base/no_destructor.h"
#include "base/strings/string_split.h"
#include "content/browser/preloading/prerender/prerender_final_status.h"
#include "content/browser/preloading/prerender/prerender_host.h"
#include "content/browser/preloading/prerender/prerender_host_registry.h"
#include "content/browser/preloading/prerender/prerender_metrics.h"
#include "content/browser/preloading/prerender/prerender_navigation_utils.h"
#include "content/browser/renderer_host/frame_tree.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_delegate.h"
#include "content/public/browser/preloading_trigger_type.h"
#include "services/network/public/mojom/parsed_headers.mojom.h"
#include "url/origin.h"
#include "url/url_constants.h"

namespace content {

namespace {

// For the given two origins, analyze what kind of redirection happened.
void AnalyzeCrossOriginRedirection(const url::Origin& current_origin,
                                   const url::Origin& initial_origin,
                                   const std::string& histogram_suffix) {
  CHECK_NE(initial_origin, current_origin);
  CHECK(current_origin.GetURL().SchemeIsHTTPOrHTTPS());
  CHECK(initial_origin.GetURL().SchemeIsHTTPOrHTTPS());

  std::bitset<3> bits;
  bits[2] = current_origin.scheme() != initial_origin.scheme();
  bits[1] = current_origin.host() != initial_origin.host();
  bits[0] = current_origin.port() != initial_origin.port();
  CHECK(bits.any());
  auto mismatch_type =
      static_cast<PrerenderCrossOriginRedirectionMismatch>(bits.to_ulong());

  RecordPrerenderRedirectionMismatchType(mismatch_type, histogram_suffix);

  if (mismatch_type ==
      PrerenderCrossOriginRedirectionMismatch::kSchemePortMismatch) {
    RecordPrerenderRedirectionProtocolChange(
        current_origin.scheme() == url::kHttpsScheme
            ? PrerenderCrossOriginRedirectionProtocolChange::
                  kHttpProtocolUpgrade
            : PrerenderCrossOriginRedirectionProtocolChange::
                  kHttpProtocolDowngrade,
        histogram_suffix);
    return;
  }
}

}  // namespace

// static
void PrerenderNavigationThrottle::MaybeCreateAndAdd(
    NavigationThrottleRegistry& registry) {
  auto* navigation_request =
      NavigationRequest::From(&registry.GetNavigationHandle());
  FrameTreeNode* frame_tree_node = navigation_request->frame_tree_node();
  if (frame_tree_node->GetFrameType() == FrameType::kPrerenderMainFrame) {
    registry.AddThrottle(
        base::WrapUnique(new PrerenderNavigationThrottle(registry)));
  }
}

PrerenderNavigationThrottle::~PrerenderNavigationThrottle() = default;

const char* PrerenderNavigationThrottle::GetNameForLogging() {
  return "PrerenderNavigationThrottle";
}

NavigationThrottle::ThrottleCheckResult
PrerenderNavigationThrottle::WillStartRequest() {
  return WillStartOrRedirectRequest(/*is_redirection=*/false);
}

NavigationThrottle::ThrottleCheckResult
PrerenderNavigationThrottle::WillRedirectRequest() {
  return WillStartOrRedirectRequest(/*is_redirection=*/true);
}

PrerenderNavigationThrottle::PrerenderNavigationThrottle(
    NavigationThrottleRegistry& registry)
    : NavigationThrottle(registry),
      prerender_host_(
          PrerenderHost::GetFromFrameTreeNode(
              *NavigationRequest::From(&registry.GetNavigationHandle())
                   ->frame_tree_node())
              .GetWeakPtr()) {
  CHECK(prerender_host_);

  // This throttle is responsible for setting the initial navigation id on the
  // PrerenderHost, since the PrerenderHost obtains the NavigationRequest,
  // which has the ID, only after the navigation throttles run.
  if (prerender_host_->GetInitialNavigationId().has_value()) {
    // If the host already has an initial navigation id, this throttle
    // will later cancel the navigation in Will*Request(). Just do nothing
    // until then.
  } else {
    prerender_host_->SetInitialNavigation(
        NavigationRequest::From(&registry.GetNavigationHandle()));
  }
}

NavigationThrottle::ThrottleCheckResult
PrerenderNavigationThrottle::WillStartOrRedirectRequest(bool is_redirection) {
  if (!prerender_host_) {
    return CANCEL;
  }

  GURL navigation_url = navigation_handle()->GetURL();
  url::Origin navigation_origin = url::Origin::Create(navigation_url);
  url::Origin initial_prerendering_origin =
      url::Origin::Create(prerender_host_->GetInitialUrl());

  // Reset the flags that should be calculated every time redirction happens.
  is_same_site_cross_origin_prerender_ = false;
  same_site_cross_origin_prerender_did_redirect_ = false;

  // Allow only HTTP(S) schemes.
  // https://wicg.github.io/nav-speculation/prerendering.html#no-bad-navs
  if (!navigation_url.SchemeIsHTTPOrHTTPS()) {
    if (is_redirection) {
      CancelPrerendering(PrerenderFinalStatus::kInvalidSchemeRedirect);
    } else {
      // For non-redirected initial navigation, this should be checked in
      // PrerenderHostRegistry::CreateAndStartHost().
      CHECK(!IsInitialNavigation());
      CancelPrerendering(PrerenderFinalStatus::kInvalidSchemeNavigation);
    }
    return CANCEL;
  }

  // Disallow all pages that have an effective URL like hosted apps and NTP.
  auto* browser_context =
      navigation_handle()->GetStartingSiteInstance()->GetBrowserContext();
  if (SiteInstanceImpl::HasEffectiveURL(browser_context, navigation_url)) {
    CancelPrerendering(
        is_redirection
            ? PrerenderFinalStatus::kRedirectedPrerenderingUrlHasEffectiveUrl
            : PrerenderFinalStatus::kPrerenderingUrlHasEffectiveUrl);
    return CANCEL;
  }

  // Origin checks for the navigation (redirection), which varies depending on
  // whether the navigation is initial one or not.
  if (IsInitialNavigation()) {
    // Origin checks for initial prerendering navigation (redirection).
    //
    // For non-embedder triggered prerendering, compare the origin of the
    // initiator URL to the origin of navigation (redirection) URL.
    //
    // For embedder triggered prerendering, there is no initiator page, so
    // initial prerendering navigation doesn't check origins and instead initial
    // prerendering redirection compare the origin of initial prerendering URL
    // to the origin of redirection URL.

    if (prerender_host_->IsBrowserInitiated()) {
      // Cancel an embedder triggered prerendering if it is redirected to a URL
      // cross-site to the initial prerendering URL.
      if (prerender_navigation_utils::IsCrossSite(
              navigation_url, initial_prerendering_origin)) {
        AnalyzeCrossOriginRedirection(navigation_origin,
                                      initial_prerendering_origin,
                                      prerender_host_->GetHistogramSuffix());
        CancelPrerendering(
            PrerenderFinalStatus::kCrossSiteRedirectInInitialNavigation);
        return CANCEL;
      }

      // Skip the same-site check for non-redirected cases as the initiator
      // origin is nullopt for browser-initiated prerendering.
      CHECK(!prerender_host_->initiator_origin().has_value());
    } else if (prerender_navigation_utils::IsCrossSite(
                   navigation_url,
                   prerender_host_->initiator_origin().value())) {
      // TODO(crbug.com/40168192): Once cross-site prerendering is implemented,
      // we'll need to enforce strict referrer policies
      // (https://wicg.github.io/nav-speculation/prefetch.html#list-of-sufficiently-strict-speculative-navigation-referrer-policies).
      //
      // Cancel prerendering if this is cross-site prerendering, cross-site
      // redirection during prerendering, or cross-site navigation from a
      // prerendered page.
      CancelPrerendering(
          is_redirection
              ? PrerenderFinalStatus::kCrossSiteRedirectInInitialNavigation
              : PrerenderFinalStatus::kCrossSiteNavigationInInitialNavigation);
      return CANCEL;
    } else if (prerender_navigation_utils::IsSameSiteCrossOrigin(
                   navigation_url,
                   prerender_host_->initiator_origin().value())) {
      // Same-site cross-origin prerendering is allowed only when the opt-in
      // header is specified on response. This will be checked on
      // WillProcessResponse().
      is_same_site_cross_origin_prerender_ = true;
      same_site_cross_origin_prerender_did_redirect_ = is_redirection;
    }
  } else {
    // Origin checks for the main frame navigation (redirection) happens after
    // the initial prerendering navigation in a prerendered page. Compare the
    // origin of the initial prerendering URL to the origin of navigation
    // (redirection) URL.

    // Cross-site navigations after the initial prerendering navigation are
    // disallowed.
    if (prerender_navigation_utils::IsCrossSite(navigation_url,
                                                initial_prerendering_origin)) {
      CancelPrerendering(
          is_redirection
              ? PrerenderFinalStatus::kCrossSiteRedirectInMainFrameNavigation
              : PrerenderFinalStatus::
                    kCrossSiteNavigationInMainFrameNavigation);
      return CANCEL;
    }

    // Same-site cross-origin prerendering is allowed only when the opt-in
    // header is specified on response. This will be checked on
    // WillProcessResponse().
    if (prerender_navigation_utils::IsSameSiteCrossOrigin(
            navigation_url, initial_prerendering_origin)) {
      is_same_site_cross_origin_prerender_ = true;
      same_site_cross_origin_prerender_did_redirect_ = is_redirection;
    }
  }

  return PROCEED;
}

NavigationThrottle::ThrottleCheckResult
PrerenderNavigationThrottle::WillProcessResponse() {
  if (!prerender_host_) {
    return CANCEL;
  }

  auto* navigation_request = NavigationRequest::From(navigation_handle());

  // https://wicg.github.io/nav-speculation/prerendering.html#navigate-fetch-patch
  // "1. If browsingContext is a prerendering browsing context and
  // responseOrigin is not same origin with incumbentNavigationOrigin, then:"
  // "1.1. Let loadingModes be the result of getting the supported loading
  // modes for response."
  // "1.2. If loadingModes does not contain `credentialed-prerender`, then
  // set response to a network error."
  bool is_credentialed_prerender =
      navigation_request->response() &&
      navigation_request->response()->parsed_headers &&
      base::Contains(
          navigation_request->response()->parsed_headers->supports_loading_mode,
          network::mojom::LoadingMode::kCredentialedPrerender);
  // Cancel prerendering when this is same-site cross-origin navigation but the
  // opt-in header is not specified.
  if (is_same_site_cross_origin_prerender_ && !is_credentialed_prerender) {
    // Calculate the final status for cancellation.
    PrerenderFinalStatus final_status = PrerenderFinalStatus::kDestroyed;
    if (IsInitialNavigation()) {
      final_status =
          same_site_cross_origin_prerender_did_redirect_
              ? PrerenderFinalStatus::
                    kSameSiteCrossOriginRedirectNotOptInInInitialNavigation
              : PrerenderFinalStatus::
                    kSameSiteCrossOriginNavigationNotOptInInInitialNavigation;
    } else {
      final_status =
          same_site_cross_origin_prerender_did_redirect_
              ? PrerenderFinalStatus::
                    kSameSiteCrossOriginRedirectNotOptInInMainFrameNavigation
              : PrerenderFinalStatus::
                    kSameSiteCrossOriginNavigationNotOptInInMainFrameNavigation;
    }
    CancelPrerendering(final_status);
    return CANCEL;
  }

  std::optional<PrerenderFinalStatus> cancel_reason;

  // TODO(crbug.com/40222993): Delay until activation instead of cancellation.
  if (navigation_handle()->IsDownload()) {
    // Disallow downloads during prerendering and cancel the prerender.
    cancel_reason = PrerenderFinalStatus::kDownload;
  } else if (prerender_navigation_utils::IsDisallowedHttpResponseCode(
                 navigation_request->commit_params().http_response_code)) {
    // There's no point in trying to prerender failed navigations.
    cancel_reason = PrerenderFinalStatus::kNavigationBadHttpStatus;
  }

  if (cancel_reason.has_value()) {
    CancelPrerendering(cancel_reason.value());
    return CANCEL;
  }
  return PROCEED;
}

bool PrerenderNavigationThrottle::IsInitialNavigation() const {
  return prerender_host_ ? prerender_host_->IsInitialNavigation(
                               *NavigationRequest::From(navigation_handle()))
                         : false;
}

void PrerenderNavigationThrottle::CancelPrerendering(
    PrerenderFinalStatus final_status) {
  if (!prerender_host_) {
    return;
  }
  auto* navigation_request = NavigationRequest::From(navigation_handle());
  FrameTreeNode* frame_tree_node = navigation_request->frame_tree_node();
  CHECK_EQ(frame_tree_node->GetFrameType(), FrameType::kPrerenderMainFrame);
  PrerenderHostRegistry* prerender_host_registry =
      frame_tree_node->current_frame_host()
          ->delegate()
          ->GetPrerenderHostRegistry();
  prerender_host_registry->CancelHost(prerender_host_->frame_tree_node_id(),
                                      final_status);
}

}  // namespace content