// 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/renderer_host/isolated_web_app_throttle.h"

#include "base/feature_list.h"
#include "content/browser/renderer_host/frame_tree_node.h"
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/web_exposed_isolation_info.h"
#include "content/common/navigation_params_utils.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/isolated_web_apps_policy.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/web_contents_user_data.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/page_type.h"
#include "url/origin.h"
#include "url/scheme_host_port.h"

namespace content {

namespace {

// Stores the origin of the Isolated Web App that a WebContents is bound to.
// Every WebContents will have an instance of this class assigned to it during
// its first navigation, which will determine whether the WebContents hosts an
// Isolated Web App for its lifetime. Note that activating an alternative frame
// tree (e.g. preloading or portals) will NOT override this state.
class WebContentsIsolationInfo
    : public WebContentsUserData<WebContentsIsolationInfo> {
 public:
  ~WebContentsIsolationInfo() override = default;

  bool is_isolated_application() { return isolated_origin_.has_value(); }

  const url::Origin& origin() {
    DCHECK(is_isolated_application());
    return isolated_origin_.value();
  }

 private:
  friend class WebContentsUserData<WebContentsIsolationInfo>;
  explicit WebContentsIsolationInfo(WebContents* web_contents,
                                    absl::optional<url::Origin> isolated_origin)
      : WebContentsUserData<WebContentsIsolationInfo>(*web_contents),
        isolated_origin_(isolated_origin) {}

  absl::optional<url::Origin> isolated_origin_;

  WEB_CONTENTS_USER_DATA_KEY_DECL();
};
WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsIsolationInfo);

absl::optional<url::SchemeHostPort> GetTupleFromOptionalOrigin(
    const absl::optional<url::Origin>& origin) {
  if (origin.has_value()) {
    return origin->GetTupleOrPrecursorTupleIfOpaque();
  }
  return absl::nullopt;
}

}  // namespace

// static
std::unique_ptr<IsolatedWebAppThrottle>
IsolatedWebAppThrottle::MaybeCreateThrottleFor(NavigationHandle* handle) {
  BrowserContext* browser_context = NavigationRequest::From(handle)
                                        ->frame_tree_node()
                                        ->navigator()
                                        .controller()
                                        .GetBrowserContext();

  if (IsolatedWebAppsPolicy::AreIsolatedWebAppsEnabled(browser_context)) {
    return std::make_unique<IsolatedWebAppThrottle>(handle);
  }
  return nullptr;
}

IsolatedWebAppThrottle::IsolatedWebAppThrottle(
    NavigationHandle* navigation_handle)
    : NavigationThrottle(navigation_handle) {}

IsolatedWebAppThrottle::~IsolatedWebAppThrottle() = default;

NavigationThrottle::ThrottleCheckResult
IsolatedWebAppThrottle::WillStartRequest() {
  bool requests_app_isolation = embedder_requests_app_isolation();
  auto* navigation_request = NavigationRequest::From(navigation_handle());
  dest_origin_ = navigation_request->GetTentativeOriginAtRequestTime();

  // If this is the first navigation in this WebContents, save the isolation
  // state to validate future navigations.
  auto* web_contents_isolation_info = WebContentsIsolationInfo::FromWebContents(
      navigation_handle()->GetWebContents());
  if (!web_contents_isolation_info) {
    WebContentsIsolationInfo::CreateForWebContents(
        navigation_handle()->GetWebContents(),
        requests_app_isolation ? absl::make_optional(dest_origin_)
                               : absl::nullopt);
  }

  FrameTreeNode* frame_tree_node = navigation_request->frame_tree_node();
  if (!frame_tree_node->is_on_initial_empty_document()) {
    prev_origin_ = frame_tree_node->current_origin();
  }

  return DoThrottle(requests_app_isolation, NavigationThrottle::BLOCK_REQUEST);
}

NavigationThrottle::ThrottleCheckResult
IsolatedWebAppThrottle::WillRedirectRequest() {
  // On redirects, the old destination origin becomes the new previous origin.
  auto* navigation_request = NavigationRequest::From(navigation_handle());
  prev_origin_ = dest_origin_;
  dest_origin_ = navigation_request->GetTentativeOriginAtRequestTime();

  return DoThrottle(embedder_requests_app_isolation(),
                    NavigationThrottle::BLOCK_REQUEST);
}

NavigationThrottle::ThrottleCheckResult
IsolatedWebAppThrottle::WillProcessResponse() {
  auto* navigation_request = NavigationRequest::From(navigation_handle());
  auto* assigned_rfh = static_cast<RenderFrameHostImpl*>(
      navigation_request->GetRenderFrameHost());
  // Allow downloads and 204s (for these GetOriginToCommit returns nullopt).
  if (!assigned_rfh) {
    return NavigationThrottle::PROCEED;
  }

  // Update |dest_origin_| to point to the final origin, which may have changed
  // since the last WillStartRequest/WillRedirectRequest call.
  dest_origin_ = navigation_request->GetOriginToCommit().value();
  const WebExposedIsolationInfo& assigned_isolation_info =
      assigned_rfh->GetSiteInstance()->GetWebExposedIsolationInfo();
  return DoThrottle(assigned_isolation_info.is_isolated_application(),
                    NavigationThrottle::BLOCK_RESPONSE);
}

bool IsolatedWebAppThrottle::OpenUrlExternal(const GURL& url) {
  NavigationRequest* navigation_request =
      NavigationRequest::From(navigation_handle());
  const FrameTreeNode* frame_tree_node = navigation_request->frame_tree_node();
  mojo::PendingRemote<network::mojom::URLLoaderFactory> loader_factory;
  return GetContentClient()->browser()->HandleExternalProtocol(
      url,
      base::BindRepeating(
          [](const int frame_tree_node_id) {
            return WebContents::FromFrameTreeNodeId(frame_tree_node_id);
          },
          frame_tree_node->frame_tree_node_id()),
      frame_tree_node->frame_tree_node_id(),
      navigation_request->GetNavigationUIData(),
      /*is_primary_main_frame=*/true, /*is_in_fenced_frame_tree=*/false,
      network::mojom::WebSandboxFlags::kNone,
      (navigation_handle()->GetRedirectChain().size() > 1)
          ? ui::PageTransition::PAGE_TRANSITION_SERVER_REDIRECT
          : ui::PageTransition::PAGE_TRANSITION_LINK,
      navigation_request->HasUserGesture(),
      /*initiating_origin=*/absl::nullopt,
      /*initiator_document=*/nullptr, &loader_factory);
}

NavigationThrottle::ThrottleCheckResult IsolatedWebAppThrottle::DoThrottle(
    bool needs_app_isolation,
    NavigationThrottle::ThrottleAction block_action) {
  auto* web_contents_isolation_info = WebContentsIsolationInfo::FromWebContents(
      navigation_handle()->GetWebContents());
  DCHECK(web_contents_isolation_info);

  // Block navigations into Isolated Web Apps (IWA) from non-IWA contexts.
  if (!web_contents_isolation_info->is_isolated_application()) {
    return needs_app_isolation ? block_action : NavigationThrottle::PROCEED;
  }

  // We want the following origin checks to be a bit more permissive than
  // usual. In particular, if the isolation, previous, or destination origins
  // are opaque, we want to use their precursor tuple for "origin" comparisons.
  // This lets us allow navigations to/from data, error, or web bundle URLs
  // that originate from the same precursor URLs. Other rules may block these
  // navigations, but for the purpose of this throttle, these navigations are
  // valid.
  const url::SchemeHostPort& web_contents_isolation_tuple =
      web_contents_isolation_info->origin().GetTupleOrPrecursorTupleIfOpaque();
  const url::SchemeHostPort& dest_tuple =
      dest_origin_.GetTupleOrPrecursorTupleIfOpaque();
  DCHECK(web_contents_isolation_tuple.IsValid());

  // If the main frame tries to leave the app's origin, cancel the
  // navigation and open the URL in the systems' default application.
  // Iframes are allowed to leave the app's origin.
  if (dest_tuple != web_contents_isolation_tuple) {
    if (navigation_handle()->IsInMainFrame()) {
      OpenUrlExternal(navigation_handle()->GetURL());
      return NavigationThrottle::CANCEL;
    }
    return NavigationThrottle::PROCEED;
  }

  // Block renderer-initiated iframe navigations into the app that were
  // initiated by a non-app frame. This ensures that all iframe navigations into
  // the app come from the app itself.
  absl::optional<url::SchemeHostPort> prev_tuple =
      GetTupleFromOptionalOrigin(prev_origin_);
  if (prev_tuple.has_value() &&
      prev_tuple.value() != web_contents_isolation_tuple &&
      navigation_handle()->IsRendererInitiated()) {
    // Main frames shouldn't have been allowed to leave the app's origin.
    CHECK(!navigation_handle()->IsInMainFrame());

    // Allow the navigation if it was initiated by the app, meaning it has a
    // trusted destination URL. This only applies to the initial request, as
    // redirect locations come from outside the app.
    if (navigation_handle()->GetRedirectChain().size() == 1 &&
        navigation_handle()->GetInitiatorOrigin().has_value()) {
      const url::SchemeHostPort& initiator_tuple =
          navigation_handle()
              ->GetInitiatorOrigin()
              .value()
              .GetTupleOrPrecursorTupleIfOpaque();
      if (initiator_tuple == web_contents_isolation_tuple) {
        return NavigationThrottle::PROCEED;
      }
    }
    return block_action;
  }

  if (!navigation_handle()->IsInMainFrame()) {
    {
      // Block iframe navigations to the app's origin if the parent frame
      // doesn't belong to the app. This prevents non-app frames from having
      // access to an app frame.
      const url::SchemeHostPort& parent_tuple =
          navigation_handle()
              ->GetParentFrame()
              ->GetLastCommittedOrigin()
              .GetTupleOrPrecursorTupleIfOpaque();
      if (parent_tuple != web_contents_isolation_tuple) {
        return block_action;
      }
    }

    // Allow iframe same-origin navigations to blob: and data: URLs
    // (cross-origin iframe navigation are already allowed and handled further
    // up as part of the `dest_tuple != web_contents_isolation_tuple`
    // condition).
    if (navigation_handle()->GetURL().SchemeIs(url::kDataScheme) ||
        navigation_handle()->GetURL().SchemeIsBlob()) {
      return NavigationThrottle::PROCEED;
    }
  }

  // At this point we know the navigation is same-tuple within an Isolated Web
  // App. If the new page isn't isolated, block the navigation.
  if (!needs_app_isolation) {
    return block_action;
  }

  return NavigationThrottle::PROCEED;
}

bool IsolatedWebAppThrottle::embedder_requests_app_isolation() {
  BrowserContext* browser_context = NavigationRequest::From(navigation_handle())
                                        ->frame_tree_node()
                                        ->navigator()
                                        .controller()
                                        .GetBrowserContext();
  return SiteIsolationPolicy::ShouldUrlUseApplicationIsolationLevel(
      browser_context, navigation_handle()->GetURL());
}

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

}  // namespace content