// Copyright 2019 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/url_loader_factory_params_helper.h"

#include <optional>
#include <string_view>

#include "base/command_line.h"
#include "base/containers/lru_cache.h"
#include "base/feature_list.h"
#include "base/no_destructor.h"
#include "base/not_fatal_until.h"
#include "content/browser/devtools/network_service_devtools_observer.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_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/runtime_feature_state/runtime_feature_state_document_data.h"
#include "content/public/common/child_process_id_util.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "net/base/features.h"
#include "net/base/isolation_info.h"
#include "net/cookies/cookie_setting_override.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "services/network/public/mojom/cross_origin_embedder_policy.mojom.h"
#include "services/network/public/mojom/device_bound_sessions.mojom.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/shared_dictionary_access_observer.mojom.h"
#include "services/network/public/mojom/url_loader.mojom-shared.h"
#include "third_party/blink/public/common/tokens/tokens.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"

namespace content {

namespace {

// Helper used by the public URLLoaderFactoryParamsHelper::Create... methods.
//
// |origin| is the origin that will use the URLLoaderFactory.
// |origin| is typically the same as the origin in
// network::ResourceRequest::request_initiator, except when
// |is_for_isolated_world|.  See also the doc comment for
// extensions::URLLoaderFactoryManager::CreateFactory.
network::mojom::URLLoaderFactoryParamsPtr CreateParams(
    RenderProcessHost* process,
    const url::Origin& origin,
    const url::Origin& request_initiator_origin_lock,
    bool is_trusted,
    const std::optional<blink::LocalFrameToken>& top_frame_token,
    const net::IsolationInfo& isolation_info,
    network::mojom::ClientSecurityStatePtr client_security_state,
    mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter>
        coep_reporter,
    mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter>
        dip_reporter,
    bool allow_universal_access_from_file_urls,
    bool is_for_isolated_world,
    mojo::PendingRemote<network::mojom::CookieAccessObserver> cookie_observer,
    mojo::PendingRemote<network::mojom::TrustTokenAccessObserver>
        trust_token_observer,
    mojo::PendingRemote<network::mojom::SharedDictionaryAccessObserver>
        shared_dictionary_observer,
    mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
        url_loader_network_observer,
    mojo::PendingRemote<network::mojom::DevToolsObserver> devtools_observer,
    mojo::PendingRemote<network::mojom::DeviceBoundSessionAccessObserver>
        device_bound_session_observer,
    network::mojom::TrustTokenOperationPolicyVerdict
        trust_token_issuance_policy,
    network::mojom::TrustTokenOperationPolicyVerdict
        trust_token_redemption_policy,
    net::CookieSettingOverrides cookie_setting_overrides,
    std::string_view debug_tag,
    bool require_cross_site_request_for_cookies,
    bool is_for_service_worker,
    const base::UnguessableToken& network_restrictions_id,
    bool has_effective_top_frame_for_storage_partitioning,
    bool is_outermost_main_frame = false) {
  DCHECK(process);

  network::mojom::URLLoaderFactoryParamsPtr params =
      network::mojom::URLLoaderFactoryParams::New();

  params->process_id = ToOriginatingProcessId(process->GetID());
  params->request_initiator_origin_lock = request_initiator_origin_lock;

  params->is_trusted = is_trusted;
  params->is_outermost_main_frame = is_outermost_main_frame;
  if (top_frame_token)
    params->top_frame_id = top_frame_token.value().value();

  CHECK(!network_restrictions_id.is_empty(), base::NotFatalUntil::M163);
  params->network_restrictions_id = network_restrictions_id;
  params->isolation_info = isolation_info;

  if (URLLoaderFactoryParamsHelper::ShouldPreferFactorySiteForCookies(
          has_effective_top_frame_for_storage_partitioning, isolation_info)) {
    params->prefer_factory_site_for_cookies = true;
  }

  params->disable_web_security =
      base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kDisableWebSecurity);
  params->client_security_state = std::move(client_security_state);
  params->coep_reporter = std::move(coep_reporter);
  params->dip_reporter = std::move(dip_reporter);

  if (params->disable_web_security) {
    // --disable-web-security also disables Opaque Response Blocking (ORB).
    params->is_orb_enabled = false;
  } else if (allow_universal_access_from_file_urls &&
             origin.scheme() == url::kFileScheme) {
    // allow_universal_access_from_file_urls disables ORB (via
    // `is_orb_enabled`) and CORS (via `disable_web_security`) for requests
    // made from a file: |origin|.
    params->is_orb_enabled = false;
    params->disable_web_security = true;
  } else {
    params->is_orb_enabled = true;
  }

  params->trust_token_issuance_policy = trust_token_issuance_policy;
  params->trust_token_redemption_policy = trust_token_redemption_policy;

  GetContentClient()->browser()->OverrideURLLoaderFactoryParams(
      process->GetBrowserContext(), origin, is_for_isolated_world,
      is_for_service_worker, params.get());

  params->cookie_observer = std::move(cookie_observer);
  params->trust_token_observer = std::move(trust_token_observer);
  params->shared_dictionary_observer = std::move(shared_dictionary_observer);
  params->url_loader_network_observer = std::move(url_loader_network_observer);
  params->devtools_observer = std::move(devtools_observer);
  params->device_bound_session_observer =
      std::move(device_bound_session_observer);

  params->cookie_setting_overrides = cookie_setting_overrides;

  params->debug_tag = std::string(debug_tag);

  params->require_cross_site_request_for_cookies =
      require_cross_site_request_for_cookies;

  if (URLLoaderFactoryParamsHelper::IsMainFrameOriginRecentlyAccessed(
          isolation_info)) {
    params->is_main_frame_origin_recently_accessed = true;
  }

  return params;
}

base::LRUCacheSet<url::Origin>& GetRecentlyAccessedOriginSet() {
  static base::NoDestructor<base::LRUCacheSet<url::Origin>> origin_set(
      net::features::kRecentlyAccessedOriginCacheSize.Get());
  return *origin_set;
}

}  // namespace

// static
network::mojom::URLLoaderFactoryParamsPtr
URLLoaderFactoryParamsHelper::CreateForFrame(
    RenderFrameHostImpl* frame,
    const url::Origin& frame_origin,
    const net::IsolationInfo& isolation_info,
    network::mojom::ClientSecurityStatePtr client_security_state,
    mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter>
        coep_reporter,
    mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter>
        dip_reporter,
    RenderProcessHost* process,
    network::mojom::TrustTokenOperationPolicyVerdict
        trust_token_issuance_policy,
    network::mojom::TrustTokenOperationPolicyVerdict
        trust_token_redemption_policy,
    net::CookieSettingOverrides cookie_setting_overrides,
    const base::UnguessableToken& network_restrictions_id,
    std::string_view debug_tag) {
  const bool has_effective_top_frame_for_storage_partitioning =
      GetContentClient()->browser()->GetEffectiveTopFrameForPartitioning(
          frame) != nullptr;
  return CreateParams(
      process,
      frame_origin,  // origin
      frame_origin,  // request_initiator_origin_lock
      false,         // is_trusted
      frame->GetTopFrameToken(), isolation_info,
      std::move(client_security_state), std::move(coep_reporter),
      std::move(dip_reporter),
      frame->GetOrCreateWebPreferences().allow_universal_access_from_file_urls,
      false,  // is_for_isolated_world
      frame->CreateCookieAccessObserver(
          CookieAccessDetails::Source::kNonNavigation),
      frame->CreateTrustTokenAccessObserver(),
      frame->CreateSharedDictionaryAccessObserver(),
      frame->CreateURLLoaderNetworkObserver(),
      NetworkServiceDevToolsObserver::MakeSelfOwned(frame->frame_tree_node()),
      frame->CreateDeviceBoundSessionObserver(), trust_token_issuance_policy,
      trust_token_redemption_policy, cookie_setting_overrides, debug_tag,
      /*require_cross_site_request_for_cookies=*/false,
      /*is_for_service_worker=*/false, network_restrictions_id,
      has_effective_top_frame_for_storage_partitioning,
      frame->IsOutermostMainFrame());
}

// static
network::mojom::URLLoaderFactoryParamsPtr
URLLoaderFactoryParamsHelper::CreateForIsolatedWorld(
    RenderFrameHostImpl* frame,
    const url::Origin& isolated_world_origin,
    const url::Origin& main_world_origin,
    const net::IsolationInfo& isolation_info,
    network::mojom::ClientSecurityStatePtr client_security_state,
    network::mojom::TrustTokenOperationPolicyVerdict
        trust_token_issuance_policy,
    network::mojom::TrustTokenOperationPolicyVerdict
        trust_token_redemption_policy,
    net::CookieSettingOverrides cookie_setting_overrides) {
  const bool has_effective_top_frame_for_storage_partitioning =
      GetContentClient()->browser()->GetEffectiveTopFrameForPartitioning(
          frame) != nullptr;
  return CreateParams(
      frame->GetProcess(),
      isolated_world_origin,  // origin
      main_world_origin,      // request_initiator_origin_lock
      false,                  // is_trusted
      frame->GetTopFrameToken(), isolation_info,
      std::move(client_security_state),
      mojo::NullRemote(),  // coep_reporter
      mojo::NullRemote(),  // dip_reporter
      frame->GetOrCreateWebPreferences().allow_universal_access_from_file_urls,
      true,  // is_for_isolated_world
      frame->CreateCookieAccessObserver(
          CookieAccessDetails::Source::kNonNavigation),
      frame->CreateTrustTokenAccessObserver(),
      frame->CreateSharedDictionaryAccessObserver(),
      frame->CreateURLLoaderNetworkObserver(),
      NetworkServiceDevToolsObserver::MakeSelfOwned(frame->frame_tree_node()),
      frame->CreateDeviceBoundSessionObserver(), trust_token_issuance_policy,
      trust_token_redemption_policy, cookie_setting_overrides,
      "ParamHelper::CreateForIsolatedWorld",
      /*require_cross_site_request_for_cookies=*/false,
      /*is_for_service_worker=*/false,
      // Extensions and isolated worlds are out of scope for
      // Connection-Allowlists.
      network::GetNoOpNetworkRestrictionsId(),
      has_effective_top_frame_for_storage_partitioning,
      frame->IsOutermostMainFrame());
}

network::mojom::URLLoaderFactoryParamsPtr
URLLoaderFactoryParamsHelper::CreateForPrefetch(
    RenderFrameHostImpl* frame,
    network::mojom::ClientSecurityStatePtr client_security_state,
    net::CookieSettingOverrides cookie_setting_overrides,
    const base::UnguessableToken& network_restrictions_id) {
  // The factory client |is_trusted| to control the |network_isolation_key| in
  // each separate request (rather than forcing the client to use the key
  // specified in URLLoaderFactoryParams).
  const url::Origin& frame_origin = frame->GetLastCommittedOrigin();
  return CreateParams(
      frame->GetProcess(),
      frame_origin,  // origin
      frame_origin,  // request_initiator_origin_lock
      true,          // is_trusted
      frame->GetTopFrameToken(),
      net::IsolationInfo(),  // isolation_info
      std::move(client_security_state),
      mojo::NullRemote(),  // coep_reporter
      mojo::NullRemote(),  // dip_reporter
      frame->GetOrCreateWebPreferences().allow_universal_access_from_file_urls,
      false,  // is_for_isolated_world
      frame->CreateCookieAccessObserver(
          CookieAccessDetails::Source::kNonNavigation),
      frame->CreateTrustTokenAccessObserver(),
      frame->CreateSharedDictionaryAccessObserver(),
      frame->CreateURLLoaderNetworkObserver(),
      NetworkServiceDevToolsObserver::MakeSelfOwned(frame->frame_tree_node()),
      frame->CreateDeviceBoundSessionObserver(),
      network::mojom::TrustTokenOperationPolicyVerdict::kForbid,
      network::mojom::TrustTokenOperationPolicyVerdict::kForbid,
      cookie_setting_overrides, "ParamHelper::CreateForPrefetch",
      /*require_cross_site_request_for_cookies=*/false,
      /*is_for_service_worker=*/false, network_restrictions_id,
      // TODO(crbug.com/495538206): Revisit if prefetch from a frame with
      // an effective top frame for storage partitioning needs the same
      // browser-side `site_for_cookies` override.
      /*has_effective_top_frame_for_storage_partitioning=*/false,
      frame->IsOutermostMainFrame());
}

// static
// TODO(crbug.com/40190528): make sure client_security_state is no longer
// nullptr anywhere.
// TODO(crbug.com/40247160): Investigate whether to support cookie setting
// overrides (hardcoded empty set used for now).
network::mojom::URLLoaderFactoryParamsPtr
URLLoaderFactoryParamsHelper::CreateForWorker(
    RenderProcessHost* process,
    const url::Origin& request_initiator,
    const net::IsolationInfo& isolation_info,
    mojo::PendingRemote<network::mojom::CrossOriginEmbedderPolicyReporter>
        coep_reporter,
    mojo::PendingRemote<network::mojom::DocumentIsolationPolicyReporter>
        dip_reporter,
    mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
        url_loader_network_observer,
    mojo::PendingRemote<network::mojom::DevToolsObserver> devtools_observer,
    network::mojom::ClientSecurityStatePtr client_security_state,
    const base::UnguessableToken& network_restrictions_id,
    std::string_view debug_tag,
    bool require_cross_site_request_for_cookies,
    bool is_for_service_worker) {
  return CreateParams(
      process,
      request_initiator,  // origin
      request_initiator,  // request_initiator_origin_lock
      false,              // is_trusted
      std::nullopt,       // top_frame_token
      isolation_info, std::move(client_security_state),
      std::move(coep_reporter), std::move(dip_reporter),
      false,  // allow_universal_access_from_file_urls
      false,  // is_for_isolated_world
      static_cast<StoragePartitionImpl*>(process->GetStoragePartition())
          ->CreateCookieAccessObserverForServiceWorker(),
      static_cast<StoragePartitionImpl*>(process->GetStoragePartition())
          ->CreateTrustTokenAccessObserverForServiceWorker(),
      static_cast<StoragePartitionImpl*>(process->GetStoragePartition())
          ->CreateSharedDictionaryAccessObserverForServiceWorker(),
      std::move(url_loader_network_observer), std::move(devtools_observer),
      static_cast<StoragePartitionImpl*>(process->GetStoragePartition())
          ->CreateDeviceBoundSessionObserverForServiceWorker(),
      // Trust Token redemption and signing operations require the Permissions
      // Policy. It seems Permissions Policy in worker contexts
      // is currently an open issue (as of 06/21/2022):
      // https://github.com/w3c/webappsec-permissions-policy/issues/207.
      network::mojom::TrustTokenOperationPolicyVerdict::kPotentiallyPermit,
      network::mojom::TrustTokenOperationPolicyVerdict::kPotentiallyPermit,
      net::CookieSettingOverrides(), debug_tag,
      require_cross_site_request_for_cookies, is_for_service_worker,
      network_restrictions_id,
      // TODO(crbug.com/495538206): Revisit if workers attached to a
      // frame with an effective top frame for storage partitioning need
      // the same browser-side `site_for_cookies` override.
      /*has_effective_top_frame_for_storage_partitioning=*/false,
      /*is_outermost_main_frame=*/false);
}

// static
// TODO(crbug.com/40247160): Investigate whether to support cookie setting
// overrides (hardcoded empty set used for now).
network::mojom::URLLoaderFactoryParamsPtr
URLLoaderFactoryParamsHelper::CreateForEarlyHintsPreload(
    RenderProcessHost* process,
    const url::Origin& tentative_origin,
    NavigationRequest& navigation_request,
    const network::mojom::EarlyHints& early_hints,
    mojo::PendingRemote<network::mojom::CookieAccessObserver> cookie_observer,
    mojo::PendingRemote<network::mojom::TrustTokenAccessObserver>
        trust_token_observer,
    mojo::PendingRemote<network::mojom::SharedDictionaryAccessObserver>
        shared_dictionary_observer,
    mojo::PendingRemote<network::mojom::DeviceBoundSessionAccessObserver>
        device_bound_session_observer) {
  // TODO(crbug.com/40188470): Consider not using the speculative
  // RenderFrameHostImpl to create URLLoaderNetworkServiceObserver.
  // In general we should avoid using speculative RenderFrameHostImpl
  // to fill URLLoaderFactoryParams because some parameters can be calculated
  // only after the RenderFrameHostImpl is committed.
  // See also the design doc linked from the bug entry. It describes options
  // to create the observer without RenderFrameHostImpl.
  mojo::PendingRemote<network::mojom::URLLoaderNetworkServiceObserver>
      url_loader_network_observer = navigation_request.frame_tree_node()
                                        ->current_frame_host()
                                        ->CreateURLLoaderNetworkObserver();

  auto isolation_info = net::IsolationInfo::Create(
      net::IsolationInfo::RequestType::kOther,
      /*top_frame_origin=*/tentative_origin, /*frame_origin=*/tentative_origin,
      net::SiteForCookies::FromOrigin(tentative_origin));

  // TODO(https://issues.chromium.org/issues/336754077):
  // Support Document-Isolation-Policy in early hints headers instead of passing
  // a default DocumentIsolationPolicy.
  network::mojom::ClientSecurityStatePtr client_security_state =
      network::mojom::ClientSecurityState::New(
          early_hints.headers->cross_origin_embedder_policy,
          network::IsOriginPotentiallyTrustworthy(tentative_origin),
          early_hints.ip_address_space,
          network::mojom::LocalNetworkAccessRequestPolicy::kBlock,
          network::DocumentIsolationPolicy());

  // A NoOp network restrictions ID is used for Early Hints
  // URLLoaderFactoryParams because the connection allowlists check is done by
  // `NavigationEarlyHintsManager::HandleEarlyHints`. The check does not depend
  // on the network restrictions ID. Instead, the URL of the preload and
  // preconnect triggered by the Link header is checked against the connection
  // allowlists in the Early Hints response directly.
  return CreateParams(
      process, /*origin=*/tentative_origin,
      /*request_initiator_origin_lock=*/tentative_origin,
      /*is_trusted=*/false, /*top_frame_token=*/std::nullopt, isolation_info,
      std::move(client_security_state),
      /*coep_reporter=*/mojo::NullRemote(),
      /*dip_reporter=*/mojo::NullRemote(),
      /*allow_universal_access_from_file_urls=*/false,
      /*is_for_isolated_world=*/false, std::move(cookie_observer),
      std::move(trust_token_observer), std::move(shared_dictionary_observer),
      std::move(url_loader_network_observer),
      /*devtools_observer=*/mojo::NullRemote(),
      std::move(device_bound_session_observer),
      network::mojom::TrustTokenOperationPolicyVerdict::kForbid,
      network::mojom::TrustTokenOperationPolicyVerdict::kForbid,
      net::CookieSettingOverrides(), "ParamHelper::CreateForEarlyHintsPreload",
      /*require_cross_site_request_for_cookies=*/false,
      /*is_for_service_worker=*/false,
      /*network_restrictions_id=*/network::GetNoOpNetworkRestrictionsId(),
      // TODO(crbug.com/495538206): Revisit if early-hints preloads
      // initiated from a frame with an effective top frame for storage
      // partitioning need the same browser-side `site_for_cookies`
      // override.
      /*has_effective_top_frame_for_storage_partitioning=*/false,
      navigation_request.frame_tree_node()->IsOutermostMainFrame());
}

// static
void URLLoaderFactoryParamsHelper::OnMainFrameNavigation(url::Origin origin) {
  if (base::FeatureList::IsEnabled(
          net::features::kUpdateIsMainFrameOriginRecentlyAccessed)) {
    GetRecentlyAccessedOriginSet().Put(std::move(origin));
  }
}

// static
bool URLLoaderFactoryParamsHelper::IsMainFrameOriginRecentlyAccessed(
    const net::IsolationInfo& isolation_info) {
  if (!base::FeatureList::IsEnabled(
          net::features::kUpdateIsMainFrameOriginRecentlyAccessed)) {
    return false;
  }

  const std::optional<url::Origin> top_frame_origin =
      isolation_info.top_frame_origin();
  if (!top_frame_origin) {
    return false;
  }

  auto& origin_set = GetRecentlyAccessedOriginSet();
  return origin_set.Peek(top_frame_origin.value()) != origin_set.end();
}

// static
bool URLLoaderFactoryParamsHelper::ShouldPreferFactorySiteForCookies(
    bool has_effective_top_frame_for_storage_partitioning,
    const net::IsolationInfo& isolation_info) {
  // When the frame's effective top frame for storage partitioning matches
  // the actual top frame, the renderer-computed
  // `ResourceRequest::site_for_cookies` is already correct, so the flag
  // is redundant. The browser-side override in
  // `RenderFrameHostImpl::ComputeIsolationInfoInternal()` only diverges
  // from the renderer when the two top frames differ.
  if (!has_effective_top_frame_for_storage_partitioning) {
    return false;
  }
  const net::SiteForCookies& site_for_cookies =
      isolation_info.site_for_cookies();
  if (site_for_cookies.IsNull()) {
    return false;
  }
  return GetContentClient()->browser()->ShouldUseFirstPartyStorageKey(
      url::Origin::Create(site_for_cookies.RepresentativeUrl()));
}

}  // namespace content
