// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/browser/extension_protocols.h"

#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/base64.h"
#include "base/callback_list.h"
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/format_macros.h"
#include "base/functional/bind.h"
#include "base/hash/sha1.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/self_deleting.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/no_destructor.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/cancelable_task_tracker.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "base/timer/elapsed_timer.h"
#include "base/types/optional_util.h"
#include "build/build_config.h"
#include "components/guest_view/buildflags/buildflags.h"
#include "components/keyed_service/content/browser_context_keyed_service_shutdown_notifier_factory.h"
#include "components/keyed_service/core/keyed_service_shutdown_notifier.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_host.h"
#include "content/public/browser/file_url_loader.h"
#include "content/public/browser/navigation_ui_data.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/common/child_process_id.h"
#include "content/public/common/content_features.h"
#include "extensions/browser/component_extension_resource_manager.h"
#include "extensions/browser/content_verifier/content_verifier.h"
#include "extensions/browser/content_verifier/content_verify_job.h"
#include "extensions/browser/extension_navigation_ui_data.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_registry_factory.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/process_map.h"
#include "extensions/browser/process_map_factory.h"
#include "extensions/browser/url_request_util.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/extension_resource.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_handlers/background_info.h"
#include "extensions/common/manifest_handlers/cross_origin_isolation_info.h"
#include "extensions/common/manifest_handlers/csp_info.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/common/manifest_handlers/incognito_info.h"
#include "extensions/common/manifest_handlers/shared_module_info.h"
#include "extensions/common/manifest_handlers/trial_tokens_handler.h"
#include "extensions/common/manifest_handlers/web_accessible_resources_info.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/base/filename_util.h"
#include "net/base/io_buffer.h"
#include "net/base/mime_util.h"
#include "net/base/net_errors.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_response_info.h"
#include "net/http/http_status_code.h"
#include "pdf/buildflags.h"
#include "services/network/public/cpp/record_ontransfersizeupdate_utils.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/self_deleting_url_loader_factory.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/early_hints.mojom.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/blink/public/common/loader/resource_type_util.h"
#include "url/origin.h"
#include "url/url_util.h"

#if BUILDFLAG(ENABLE_GUEST_VIEW)
#include "extensions/browser/guest_view/web_view/web_view_guest.h"
#endif

#if BUILDFLAG(ENABLE_PDF)
#include "pdf/pdf_features.h"
#endif  // BUILDFLAG(ENABLE_PDF)

using content::BrowserContext;
using extensions::Extension;
using extensions::SharedModuleInfo;

namespace extensions {
namespace {

ExtensionProtocolTestHandler* g_test_handler = nullptr;

// Stores relevant info about an ExtensionResource, namely: its file path, last
// modified time and file size.
struct ResourceInfo {
  ResourceInfo(base::FilePath file_path,
               base::Time last_modified_time,
               int64_t size)
      : file_path(std::move(file_path)),
        last_modified_time(last_modified_time),
        size(size) {}

  base::FilePath file_path;
  base::Time last_modified_time;
  int64_t size = 0;
};

void GenerateBackgroundPageContents(const Extension* extension,
                                    std::string* mime_type,
                                    std::string* charset,
                                    std::string* data) {
  DCHECK(extension);
  *mime_type = "text/html";
  *charset = "utf-8";
  *data = "<!DOCTYPE html>\n<body>\n";
  for (const auto& script : BackgroundInfo::GetBackgroundScripts(extension)) {
    *data += "<script src=\"";
    *data += script.relative_path().AsUTF8Unsafe();
    *data += "\"></script>\n";
  }
}

ResourceInfo ReadResourceInfo(const extensions::ExtensionResource& resource,
                              const base::FilePath& directory) {
  // NOTE: ExtensionResource::GetFilePath() must be called on a sequence which
  // tolerates blocking operations.
  base::FilePath file_path = resource.GetFilePath();

  base::Time last_modified_time;
  int64_t size = 0;
  if (base::PathExists(file_path)) {
    base::File::Info info;
    if (base::GetFileInfo(file_path, &info)) {
      last_modified_time = info.last_modified;
      size = info.size;
    }
  }

  return ResourceInfo(file_path, last_modified_time, size);
}

bool ExtensionCanLoadInIncognito(bool is_main_frame,
                                 const Extension* extension,
                                 bool extension_enabled_in_incognito) {
  if (!extension || !extension_enabled_in_incognito) {
    return false;
  }
  if (!is_main_frame || extension->is_login_screen_extension()) {
    return true;
  }

  // Only allow incognito toplevel navigations to extension resources in
  // split mode. In spanning mode, the extension must run in a single process,
  // and an incognito tab prevents that.
  return IncognitoInfo::IsSplitMode(extension);
}

// Returns true if an chrome-extension:// resource should be allowed to load.
// Pass true for |is_incognito| only for incognito profiles and not Chrome OS
// guest mode profiles.
//
// Called on the UI thread.
bool AllowExtensionResourceLoad(const network::ResourceRequest& request,
                                network::mojom::RequestDestination destination,
                                ui::PageTransition page_transition,
                                content::ChildProcessId child_id,
                                bool is_incognito,
                                const Extension* extension,
                                bool extension_enabled_in_incognito,
                                const ExtensionSet& extensions,
                                const ProcessMap& process_map,
                                const GURL& upstream_url) {
  const bool is_main_frame =
      destination == network::mojom::RequestDestination::kDocument;
  if (is_incognito &&
      !ExtensionCanLoadInIncognito(is_main_frame, extension,
                                   extension_enabled_in_incognito)) {
    return false;
  }

  // The following checks are meant to replicate similar set of checks in the
  // renderer process, performed by ResourceRequestPolicy::CanRequestResource.
  // These are not exactly equivalent, because we don't have the same bits of
  // information. The two checks need to be kept in sync as much as possible, as
  // an exploited renderer can bypass the checks in ResourceRequestPolicy.

  // Check if the extension for which this request is made is indeed loaded in
  // the process sending the request. If not, we need to explicitly check if
  // the resource is explicitly accessible or fits in a set of exception cases.
  // Note: This allows a case where two extensions execute in the same renderer
  // process to request each other's resources. We can't do a more precise
  // check, since the renderer can lie about which extension has made the
  // request.
  if (process_map.Contains(request.url.GetHost(), child_id)) {
    return true;
  }

  // Frame navigations to extensions have already been checked in
  // the ExtensionNavigationThrottle.
  // Dedicated Worker and Shared Worker main scripts can be loaded with
  // extension URLs in browser process. Service Worker and the imported scripts
  // can be loaded with extension URLs in browser process when PlzServiceWorker
  // is enabled or during update check.
  if (child_id.is_null() &&
      (blink::IsRequestDestinationFrame(destination) ||
       destination == network::mojom::RequestDestination::kWorker ||
       destination == network::mojom::RequestDestination::kSharedWorker ||
       destination == network::mojom::RequestDestination::kScript ||
       destination == network::mojom::RequestDestination::kServiceWorker)) {
    // If a feature-guarded same-origin check is required (e.g. for worker
    // main scripts), verify that the request URL matches the initiator origin
    // stored in `upstream_url`.
    //
    // We specifically exclude frame navigations from this check to avoid
    // breaking existing functionality like Declarative Net Request redirects,
    // which are already handled by other security mechanisms like
    // ExtensionNavigationThrottle.
    if (base::FeatureList::IsEnabled(
            features::kEnforceDedicatedWorkerSameOriginCheck) &&
        !upstream_url.is_empty() &&
        !blink::IsRequestDestinationFrame(destination)) {
      url::Origin upstream_origin = url::Origin::Create(upstream_url);
      url::Origin target_origin = url::Origin::Create(request.url);
      if (upstream_origin != target_origin) {
        // Only enforce the check if either side is an IWA or Extension.
        //
        // We use a hardcoded scheme name for IWA here to avoid a dependency
        // from the extensions layer.
        constexpr char kIsolatedAppScheme[] = "isolated-app";
        if (upstream_origin.scheme() == kIsolatedAppScheme ||
            upstream_origin.scheme() == kExtensionScheme ||
            target_origin.scheme() == kIsolatedAppScheme ||
            target_origin.scheme() == kExtensionScheme) {
          return false;
        }
      }
    }
    return true;
  }

  // Allow the extension module embedder to grant permission for loads.
  if (ExtensionsBrowserClient::Get()->AllowCrossRendererResourceLoad(
          request, destination, page_transition, child_id, is_incognito,
          extension, extensions, process_map, upstream_url)) {
    return true;
  }

  // No special exceptions for cross-process loading. Block the load.
  return false;
}

// Returns true if the given URL references an icon in the given extension.
bool URLIsForExtensionIcon(const GURL& url, const Extension* extension) {
  DCHECK(url.SchemeIs(extensions::kExtensionScheme));
  if (!extension) {
    return false;
  }

  DCHECK_EQ(url.GetHost(), extension->id());
  std::string_view path = url.path();
  DCHECK(path.length() > 0 && path[0] == '/');
  std::string_view path_without_slash = path.substr(1);
  return IconsInfo::GetIcons(extension).ContainsPath(path_without_slash);
}

// Retrieves the path corresponding to an extension on disk. Returns |true| on
// success and populates |*path|; otherwise returns |false|.
bool GetDirectoryForExtensionURL(const GURL& url,
                                 const ExtensionId& extension_id,
                                 const Extension* extension,
                                 const ExtensionSet& disabled_extensions,
                                 base::FilePath* out_path) {
  base::FilePath path;
  if (extension) {
    path = extension->path();
  }
  const Extension* disabled_extension =
      disabled_extensions.GetByID(extension_id);
  if (path.empty()) {
    // For disabled extensions, we only resolve the directory path to service
    // extension icon URL requests.
    if (URLIsForExtensionIcon(url, disabled_extension)) {
      path = disabled_extension->path();
    }
  }

  if (!path.empty()) {
    *out_path = path;
    return true;
  }

  DLOG_IF(WARNING, !disabled_extension)
      << "Failed to get directory for extension " << extension_id;

  return false;
}

void GetSecurityPolicyForURL(const network::ResourceRequest& request,
                             const Extension& extension,
                             bool is_web_view_request,
                             std::string* content_security_policy,
                             const std::string** cross_origin_embedder_policy,
                             const std::string** cross_origin_opener_policy,
                             bool* send_cors_header,
                             bool* follow_symlinks_anywhere) {
  std::string resource_path = request.url.GetPath();

  // Use default CSP for <webview>.
  if (!is_web_view_request) {
    *content_security_policy =
        CSPInfo::GetResourceContentSecurityPolicy(&extension, resource_path);
  }

  *cross_origin_embedder_policy =
      CrossOriginIsolationHeader::GetCrossOriginEmbedderPolicy(extension);
  *cross_origin_opener_policy =
      CrossOriginIsolationHeader::GetCrossOriginOpenerPolicy(extension);

  bool should_pdf_resource_send_cors_header = false;
#if BUILDFLAG(ENABLE_PDF)
  // The CORS headers are needed in the OOPIF PDF extension's index.html if the
  // original PDF has a COEP: require-corp header.
  const auto origin = extension.origin();
  should_pdf_resource_send_cors_header =
      chrome_pdf::features::IsOopifPdfEnabled() &&
      origin.scheme() == extensions::kExtensionScheme &&
      origin.host() == extension_misc::kPdfExtensionId &&
      resource_path == "/index.html";
#endif  // BUILDFLAG(ENABLE_PDF)

  if (should_pdf_resource_send_cors_header ||
      WebAccessibleResourcesInfo::IsResourceWebAccessible(
          &extension, resource_path,
          base::OptionalToPtr(request.request_initiator))) {
    *send_cors_header = true;
  }

  *follow_symlinks_anywhere =
      (extension.creation_flags() & Extension::FOLLOW_SYMLINKS_ANYWHERE) != 0;
}

bool IsPathEqualTo(const GURL& url, std::string_view test) {
  std::string_view path_piece = url.path();
  return path_piece.size() > 1 && path_piece.substr(1) == test;
}

bool IsFaviconURL(const GURL& url) {
  return IsPathEqualTo(url, kFaviconSourcePath) ||
         IsPathEqualTo(url, base::StrCat({kFaviconSourcePath, "/"}));
}

bool IsBackgroundPageURL(const GURL& url) {
  return IsPathEqualTo(url, kGeneratedBackgroundPageFilename);
}

bool IsBackgroundServiceWorker(const Extension& extension,
                               const network::ResourceRequest& request) {
  return request.destination ==
             network::mojom::RequestDestination::kServiceWorker &&
         BackgroundInfo::IsServiceWorkerBased(&extension) &&
         request.url ==
             BackgroundInfo::GetBackgroundServiceWorkerScriptURL(&extension);
}

bool IsExtensionDocument(const Extension& extension,
                         const network::ResourceRequest& request) {
  const network::mojom::RequestDestination destination = request.destination;
  return destination == network::mojom::RequestDestination::kDocument ||
         destination == network::mojom::RequestDestination::kIframe ||
         destination == network::mojom::RequestDestination::kFrame ||
         destination == network::mojom::RequestDestination::kFencedframe;
}

scoped_refptr<net::HttpResponseHeaders> BuildHttpHeaders(
    const std::string& content_security_policy,
    const std::string* cross_origin_embedder_policy,
    const std::string* cross_origin_opener_policy,
    const std::set<std::string>* origin_trial_tokens,
    bool send_cors_header,
    bool include_allow_service_worker_header) {
  std::string raw_headers;
  raw_headers.append("HTTP/1.1 200 OK");
  if (!content_security_policy.empty()) {
    raw_headers.append(1, '\0');
    raw_headers.append("Content-Security-Policy: ");
    raw_headers.append(content_security_policy);
  }

  if (cross_origin_embedder_policy) {
    DCHECK(!cross_origin_embedder_policy->empty());
    raw_headers.append(1, '\0');
    raw_headers.append("Cross-Origin-Embedder-Policy: ");
    raw_headers.append(*cross_origin_embedder_policy);
  }

  if (cross_origin_opener_policy) {
    DCHECK(!cross_origin_opener_policy->empty());
    raw_headers.append(1, '\0');
    raw_headers.append("Cross-Origin-Opener-Policy: ");
    raw_headers.append(*cross_origin_opener_policy);
  }

  if (origin_trial_tokens) {
    // TrialTokens::GetTrialTokens() never returns an empty set.
    DCHECK(!origin_trial_tokens->empty());
    std::set<std::string>::iterator token_it = origin_trial_tokens->begin();
    raw_headers.append(1, '\0');
    raw_headers.append("Origin-Trial: ");
    raw_headers.append(*token_it);
    token_it++;
    while (token_it != origin_trial_tokens->end()) {
      raw_headers.append(", ");
      raw_headers.append(*token_it);
      token_it++;
    }
  }

  if (send_cors_header) {
    raw_headers.append(1, '\0');
    raw_headers.append("Access-Control-Allow-Origin: *");
    raw_headers.append(1, '\0');
    raw_headers.append("Cross-Origin-Resource-Policy: cross-origin");
  }

  if (include_allow_service_worker_header) {
    raw_headers.append(1, '\0');
    raw_headers.append("Service-Worker-Allowed: /");
  }

  raw_headers.append(2, '\0');
  return base::MakeRefCounted<net::HttpResponseHeaders>(raw_headers);
}

void AddCacheHeaders(net::HttpResponseHeaders& headers,
                     base::Time last_modified_time) {
  // On Fuchsia, some resources are served from read-only filesystems which
  // don't manage creation timestamps. Cache-control headers should still
  // be generated for those resources.
  if (last_modified_time.is_null()) {
    return;
  }

  // Hash the time and make an etag to avoid exposing the exact
  // user installation time of the extension.
  std::string hash =
      base::StringPrintf("%" PRId64, last_modified_time.ToInternalValue());
  hash = base::SHA1HashString(hash);
  headers.SetHeader(
      "ETag", base::StringPrintf(R"("%s")", base::Base64Encode(hash).c_str()));

  // Also force revalidation.
  headers.SetHeader("cache-control", "no-cache");
}

void AddMimeTypeHeaders(net::HttpResponseHeaders& headers,
                        const base::FilePath& file_path) {
  std::string mime_type;
  if (net::GetWellKnownMimeTypeFromFile(file_path, &mime_type)) {
    headers.SetHeader(net::HttpRequestHeaders::kContentType, mime_type);
  } else {
    headers.SetHeader(net::HttpRequestHeaders::kContentType,
                      "application/octet-stream");
  }
}

class FileLoaderObserver : public content::FileURLLoaderObserver {
 public:
  explicit FileLoaderObserver(scoped_refptr<ContentVerifyJob> verify_job)
      : verify_job_(std::move(verify_job)) {}

  FileLoaderObserver(const FileLoaderObserver&) = delete;
  FileLoaderObserver& operator=(const FileLoaderObserver&) = delete;

  void OnSeekComplete(int64_t result) override {
    DCHECK_EQ(seek_position_, 0);
    base::AutoLock auto_lock(lock_);
    seek_position_ = result;
    // TODO(crbug.com/410916670) Add proper support for range headers.
    const bool is_seek_contiguous = result == bytes_read_;
    if (result > 0 && verify_job_.get() && !is_seek_contiguous) {
      verify_job_ = nullptr;
    }
  }

  void OnRead(base::span<char> buffer,
              mojo::DataPipeProducer::DataSource::ReadResult* result) override {
    DCHECK(result);
    {
      base::AutoLock auto_lock(lock_);
      bytes_read_ += result->bytes_read;
      if (verify_job_) {
        // Note: We still pass the data to |verify_job_|, even if there was a
        // read error, because some errors are ignorable. See
        // ContentVerifyJob::BytesRead() for more details.
        verify_job_->BytesRead(
            buffer.first(static_cast<size_t>(result->bytes_read)),
            result->result);
      }
    }
  }

  void OnDone() override {
    base::AutoLock auto_lock(lock_);
    if (verify_job_.get()) {
      verify_job_->DoneReading();
    }
  }

 private:
  int64_t bytes_read_ = 0;
  int64_t seek_position_ = 0;
  scoped_refptr<ContentVerifyJob> verify_job_;
  // To synchronize access to all members.
  base::Lock lock_;
};

class ExtensionURLLoaderFactory;

class ExtensionProtocolShutdownNotifierFactory
    : public BrowserContextKeyedServiceShutdownNotifierFactory {
 public:
  static ExtensionProtocolShutdownNotifierFactory* GetInstance() {
    static base::NoDestructor<ExtensionProtocolShutdownNotifierFactory>
        s_factory;
    return s_factory.get();
  }

  ExtensionProtocolShutdownNotifierFactory(
      const ExtensionProtocolShutdownNotifierFactory&) = delete;
  ExtensionProtocolShutdownNotifierFactory& operator=(
      const ExtensionProtocolShutdownNotifierFactory&) = delete;

 private:
  friend class base::NoDestructor<ExtensionProtocolShutdownNotifierFactory>;
  ExtensionProtocolShutdownNotifierFactory()
      : BrowserContextKeyedServiceShutdownNotifierFactory(
            "ExtensionProtocolShutdownNotifierFactory") {
    DependsOn(ExtensionRegistryFactory::GetInstance());
    DependsOn(ProcessMapFactory::GetInstance());
  }

  content::BrowserContext* GetBrowserContextToUse(
      content::BrowserContext* context) const override {
    return ExtensionsBrowserClient::Get()->GetContextOwnInstance(context);
  }
};

class ExtensionURLLoader : public network::mojom::URLLoader {
 public:
  static void CreateAndStart(
      mojo::PendingReceiver<network::mojom::URLLoader> loader,
      mojo::PendingRemote<network::mojom::URLLoaderClient> client,
      const network::ResourceRequest& request,
      bool is_web_view_request,
      content::ChildProcessId render_process_id,
      content::BrowserContext* browser_context,
      const std::optional<url::Origin>& initiator_origin) {
    DCHECK(browser_context);
    // A raw `new` is okay because `ExtensionURLLoader` is "self-owned". It
    // will delete itself when needed (when the request is completed, or when
    // the URLLoader or the URLLoaderClient connection gets dropped).
    auto* url_loader = new ExtensionURLLoader(
        std::move(loader), std::move(client), request, is_web_view_request,
        render_process_id, browser_context, initiator_origin);
    url_loader->Start();
  }

  ExtensionURLLoader(const ExtensionURLLoader&) = delete;
  ExtensionURLLoader& operator=(const ExtensionURLLoader&) = delete;

  // network::mojom::URLLoader:
  void FollowRedirect(
      network::HttpRequestHeadersUpdateParams headers_update_params,
      const std::optional<GURL>& new_url) override {
    // new_url isn't expected to have a value, but prefer it if it's populated.
    if (new_url.has_value()) {
      request_.url = new_url.value();
    }

    Start();
  }

  void OnBrowserContextDestroyed() { browser_context_ = nullptr; }

  void SetPriority(net::RequestPriority priority,
                   int32_t intra_priority_value) override {}

 private:
  ~ExtensionURLLoader() override = default;
  ExtensionURLLoader(
      mojo::PendingReceiver<network::mojom::URLLoader> loader,
      mojo::PendingRemote<network::mojom::URLLoaderClient> client,
      const network::ResourceRequest& request,
      bool is_web_view_request,
      content::ChildProcessId render_process_id,
      content::BrowserContext* browser_context,
      const std::optional<url::Origin>& initiator_origin)
      : request_(request),
        browser_context_(browser_context),
        is_web_view_request_(is_web_view_request),
        render_process_id_(render_process_id) {
    if (base::FeatureList::IsEnabled(
            features::kEnforceDedicatedWorkerSameOriginCheck)) {
      if (initiator_origin && initiator_origin->scheme() == kExtensionScheme) {
        upstream_url_ = initiator_origin->GetURL();
      }
    }
    client_.Bind(std::move(client));
    loader_.Bind(std::move(loader));
    loader_.set_disconnect_handler(base::BindOnce(
        &ExtensionURLLoader::OnMojoDisconnect, weak_ptr_factory_.GetWeakPtr()));
    shutdown_subscription_ =
        ExtensionProtocolShutdownNotifierFactory::GetInstance()
            ->Get(browser_context)
            ->Subscribe(base::BindRepeating(
                &ExtensionURLLoader::OnBrowserContextDestroyed,
                base::Unretained(this)));
  }

  // `this` instance should only be `delete`ed after completing handling of the
  // `request_` (e.g. after sending the response back to the `client_` or after
  // encountering an error and communicating the error to the the `client_`).
  void DeleteThis() { delete this; }

  void CompleteRequestAndDeleteThis(int status) {
    client_->OnComplete(network::URLLoaderCompletionStatus(status));
    DeleteThis();
  }

  void Start() {
    // Owner of BrowserContext should ensure that all WebContents are closed
    // before starting BrowserContext destruction, but this doesn't stop
    // incoming IPCs which may still be in-flight. Both
    // ExtensionURLLoaderFactory and ExtensionURLLoader implement
    // OnBrowserContextDestroyed to get notified of the BrowserContext
    // destruction and stop the execution of in-flight calls.
    if (!browser_context_) {
      CompleteRequestAndDeleteThis(net::ERR_FAILED);
      return;
    }

    const ExtensionId extension_id = request_.url.GetHost();
    ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context_);
    scoped_refptr<const Extension> extension =
        registry->GenerateInstalledExtensionsSet().GetByIDorGUID(extension_id);
    const ExtensionSet& enabled_extensions = registry->enabled_extensions();
    const ProcessMap* process_map = ProcessMap::Get(browser_context_);
    bool incognito_enabled =
        extensions::util::IsIncognitoEnabled(extension_id, browser_context_);

    // Redirect guid to id.
    if (extension && request_.url.GetHost() == extension->guid()) {
      GURL::Replacements replace_host;
      replace_host.SetHostStr(extension->id());
      upstream_url_ = request_.url;
      GURL new_url = request_.url.ReplaceComponents(replace_host);
      request_.url = new_url;
      net::RedirectInfo redirect_info;
      redirect_info.new_method = request_.method,
      redirect_info.new_url = request_.url;
      redirect_info.status_code = net::HTTP_TEMPORARY_REDIRECT;
      network::mojom::URLResponseHeadPtr response_head(
          ::network::mojom::URLResponseHead::New());
      client_->OnReceiveRedirect(redirect_info, std::move(response_head));
      return;
    }

    if (!AllowExtensionResourceLoad(
            request_, request_.destination,
            static_cast<ui::PageTransition>(request_.transition_type),
            render_process_id_, browser_context_->IsOffTheRecord(),
            extension.get(), incognito_enabled, enabled_extensions,
            *process_map, upstream_url_)) {
      CompleteRequestAndDeleteThis(net::ERR_BLOCKED_BY_CLIENT);
      return;
    }

    base::FilePath directory_path;
    if (!GetDirectoryForExtensionURL(
            request_.url, extension_id, extension.get(),
            registry->disabled_extensions(), &directory_path)) {
      CompleteRequestAndDeleteThis(net::ERR_FAILED);
      return;
    }

    LoadExtension(extension, std::move(directory_path));
  }

  void OnResourceInfoRead(const extensions::ExtensionResource& resource,
                          const base::Version& extension_version,
                          scoped_refptr<net::HttpResponseHeaders> headers,
                          scoped_refptr<ContentVerifier> content_verifier,
                          const ResourceInfo& resource_info) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

    // If the BrowserContext is shutting down, keyed services (like
    // ExtensionRegistry) may already be destroyed. Abort the request to avoid
    // crashing.
    if (!browser_context_) {
      CompleteRequestAndDeleteThis(net::ERR_FAILED);
      return;
    }

    const auto& read_file_path = resource_info.file_path;
    const auto& last_modified_time = resource_info.last_modified_time;
    request_.url = net::FilePathToFileURL(read_file_path);

    AddCacheHeaders(*headers, last_modified_time);
    AddMimeTypeHeaders(*headers, read_file_path);

    // TODO(crbug.com/416484593): If the file is missing, we might want to check
    // if the extension has moved to a new directory (e.g. _N suffix) with the
    // same version, and reroute the request there to avoid FILE_NOT_FOUND. For
    // example, in the case of request for an extension resource coming in
    // while a corruption repair that starts when an extension goes idle.

    bool should_verify_content =
        ShouldVerifyContent(resource, extension_version, resource_info);

    scoped_refptr<ContentVerifyJob> verify_job;
    if (content_verifier && should_verify_content) {
      verify_job = ContentVerifier::CreateAndStartJobFor(
          resource.extension_id(), resource.extension_root(), extension_version,
          resource.relative_path(), content_verifier);
    }

    content::CreateFileURLLoaderBypassingSecurityChecks(
        std::move(request_), loader_.Unbind(), client_.Unbind(),
        std::make_unique<FileLoaderObserver>(std::move(verify_job)),
        /*allow_directory_listing=*/false, std::move(headers));

    DeleteThis();
  }

  bool ShouldVerifyContent(const extensions::ExtensionResource& resource,
                           const base::Version& extension_version,
                           const ResourceInfo& resource_info) {
    // TODO(crbug.com/405286894, crbug.com/410916670): Properly implement
    // content verification for range headers which return a subset of the
    // extension's file. Currently end headers may trigger unintentional
    // corruptions.
    if (std::optional<std::string> range_header =
            request_.headers.GetHeader(net::HttpRequestHeaders::kRange);
        range_header) {
      std::vector<net::HttpByteRange> ranges;
      if (net::HttpUtil::ParseRangeHeader(*range_header, &ranges) &&
          ranges.size() == 1) {
        // For now, skip content verification if the file will be read before
        // its end.
        if (ranges[0].HasLastBytePosition() &&
            ranges[0].last_byte_position() != resource_info.size - 1) {
          return false;
        }
      } else {
        // Malformed range header or multiple ranges detected. The FileURLLoader
        // will also detect this and return an error.
        return false;
      }
    }

    ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context_);
    CHECK(registry);
    auto* extension =
        registry->enabled_extensions().GetByID(resource.extension_id());
    // The extension might have been re-installed since the moment the
    // resource read was issued. In this case, this task was requested for
    // an extension that is now either disabled, or installed in a different
    // folder or with a different version.
    if (!extension || extension->path() != resource.extension_root() ||
        extension->version() != extension_version) {
      return false;
    }

    return true;
  }

  void OnFaviconRetrieved(mojo::StructPtr<network::mojom::URLResponseHead> head,
                          scoped_refptr<base::RefCountedMemory> bitmap_data) {
    if (bitmap_data) {
      head->mime_type = "image/bmp";
      WriteData(std::move(head), base::as_byte_span(*bitmap_data));
    } else {
      CompleteRequestAndDeleteThis(net::ERR_FAILED);
    }
  }

  void WriteData(mojo::StructPtr<network::mojom::URLResponseHead> head,
                 base::span<const uint8_t> contents) {
    DCHECK(contents.data());
    mojo::ScopedDataPipeProducerHandle producer_handle;
    mojo::ScopedDataPipeConsumerHandle consumer_handle;
    if (mojo::CreateDataPipe(contents.size(), producer_handle,
                             consumer_handle) != MOJO_RESULT_OK) {
      CompleteRequestAndDeleteThis(net::ERR_FAILED);
      return;
    }

    size_t actually_written_bytes = 0;
    MojoResult result = producer_handle->WriteData(
        contents, MOJO_WRITE_DATA_FLAG_NONE, actually_written_bytes);
    if (result != MOJO_RESULT_OK || actually_written_bytes < contents.size()) {
      CompleteRequestAndDeleteThis(net::ERR_FAILED);
      return;
    }

    client_->OnReceiveResponse(std::move(head), std::move(consumer_handle),
                               std::nullopt);

    CompleteRequestAndDeleteThis(net::OK);
  }

  void LoadExtension(scoped_refptr<const Extension> extension,
                     base::FilePath directory_path) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    std::string content_security_policy;
    const std::string* cross_origin_embedder_policy = nullptr;
    const std::string* cross_origin_opener_policy = nullptr;
    const std::set<std::string>* origin_trial_tokens = nullptr;
    bool send_cors_header = false;
    bool follow_symlinks_anywhere = false;
    bool include_allow_service_worker_header = false;

    if (extension) {
      GetSecurityPolicyForURL(
          request_, *extension, is_web_view_request_, &content_security_policy,
          &cross_origin_embedder_policy, &cross_origin_opener_policy,
          &send_cors_header, &follow_symlinks_anywhere);
      if (IsBackgroundServiceWorker(*extension, request_)) {
        // Manifest version 3-style background service workers need
        // "Service-Worker-Allowed" and "Origin-Trial" headers.
        include_allow_service_worker_header = true;
        origin_trial_tokens = TrialTokens::GetTrialTokens(*extension);
      } else if (IsExtensionDocument(*extension, request_)) {
        // Any top-level or embedded document can receive the tokens.
        origin_trial_tokens = TrialTokens::GetTrialTokens(*extension);
      }
    }

    const bool is_background_page_url = IsBackgroundPageURL(request_.url);
    const bool is_favicon_url = IsFaviconURL(request_.url);
    auto* resource_manager =
        ExtensionsBrowserClient::Get()->GetComponentExtensionResourceManager();
    const bool is_dynamic_resource =
        extension &&
        mojom::ManifestLocation::kComponent == extension->location() &&
        resource_manager &&
        resource_manager->IsDynamicComponentExtensionResource(
            extension->id(), request_.url.GetPath(), browser_context_);
    if (is_background_page_url || is_favicon_url || is_dynamic_resource) {
      // Handle background page requests immediately with a simple generated
      // chunk of HTML.

      // Leave cache headers out of generated background page jobs.
      auto head = network::mojom::URLResponseHead::New();
      head->headers = BuildHttpHeaders(
          content_security_policy, cross_origin_embedder_policy,
          cross_origin_opener_policy, origin_trial_tokens,
          false /* send_cors_headers */, include_allow_service_worker_header);
      if (is_background_page_url) {
        std::string contents;
        GenerateBackgroundPageContents(extension.get(), &head->mime_type,
                                       &head->charset, &contents);
        WriteData(std::move(head), base::as_byte_span(contents));
      } else if (is_favicon_url) {
        tracker_ = std::make_unique<base::CancelableTaskTracker>();
        ExtensionsBrowserClient::Get()->GetFavicon(
            browser_context_, extension.get(), request_.url, tracker_.get(),
            base::BindOnce(&ExtensionURLLoader::OnFaviconRetrieved,
                           weak_ptr_factory_.GetWeakPtr(), std::move(head)));
      } else if (is_dynamic_resource) {
        std::string contents = resource_manager->GetDynamicResourceContent(
            extension->id(), request_.url.GetPath(), browser_context_);
        head->mime_type = "text/javascript";
        head->charset = "utf-8";
        head->headers->SetHeader("Content-Type",
                                 "text/javascript; charset=utf-8");
        WriteData(std::move(head), base::as_byte_span(contents));
      }
      return;
    }

    auto headers =
        BuildHttpHeaders(content_security_policy, cross_origin_embedder_policy,
                         cross_origin_opener_policy, origin_trial_tokens,
                         send_cors_header, include_allow_service_worker_header);
    // Component extension resources may be part of the embedder's resource
    // files, for example component_extension_resources.pak in Chrome.
    int resource_id = 0;
    const base::FilePath bundle_resource_path =
        ExtensionsBrowserClient::Get()->GetBundleResourcePath(
            request_, directory_path, &resource_id);
    if (!bundle_resource_path.empty()) {
      ExtensionsBrowserClient::Get()->LoadResourceFromResourceBundle(
          request_, loader_.Unbind(), bundle_resource_path, resource_id,
          std::move(headers), client_.Unbind(), browser_context_);
      DeleteThis();
      return;
    }

    base::FilePath relative_path =
        file_util::ExtensionURLToRelativeFilePath(request_.url);

    // Do not allow requests for resources in the _metadata folder, since any
    // files there are internal implementation details that should not be
    // considered part of the extension.
    if (base::FilePath(kMetadataFolder).IsParent(relative_path)) {
      CompleteRequestAndDeleteThis(net::ERR_FILE_NOT_FOUND);
      return;
    }

    // Handle shared resources (extension A loading resources out of extension
    // B).
    ExtensionId extension_id = extension->id();
    std::string path = request_.url.GetPath();
    if (SharedModuleInfo::IsImportedPath(path)) {
      std::string new_extension_id;
      std::string new_relative_path;
      SharedModuleInfo::ParseImportedPath(path, &new_extension_id,
                                          &new_relative_path);
      ExtensionRegistry* registry = ExtensionRegistry::Get(browser_context_);
      const Extension* new_extension =
          registry->enabled_extensions().GetByID(new_extension_id);
      if (SharedModuleInfo::ImportsExtensionById(extension.get(),
                                                 new_extension_id) &&
          new_extension) {
        directory_path = new_extension->path();
        extension_id = new_extension_id;
        relative_path = base::FilePath::FromUTF8Unsafe(new_relative_path);
      } else {
        CompleteRequestAndDeleteThis(net::ERR_BLOCKED_BY_CLIENT);
        return;
      }
    }

    if (g_test_handler) {
      g_test_handler->Run(&directory_path, &relative_path);
    }

    extensions::ExtensionResource resource(extension_id, directory_path,
                                           relative_path);
    if (follow_symlinks_anywhere) {
      resource.set_follow_symlinks_anywhere();
    }

    scoped_refptr<ContentVerifier> content_verifier =
        extensions::ExtensionSystem::Get(browser_context_)->content_verifier();

    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::MayBlock()},
        base::BindOnce(&ReadResourceInfo, resource, directory_path),
        base::BindOnce(&ExtensionURLLoader::OnResourceInfoRead,
                       weak_ptr_factory_.GetWeakPtr(), resource,
                       extension->version(), std::move(headers),
                       std::move(content_verifier)));
  }

  void OnMojoDisconnect() { DeleteThis(); }

  mojo::Receiver<network::mojom::URLLoader> loader_{this};
  mojo::Remote<network::mojom::URLLoaderClient> client_;
  network::ResourceRequest request_;
  raw_ptr<content::BrowserContext, AcrossTasksDanglingUntriaged>
      browser_context_;
  const bool is_web_view_request_;

  // We store the ID and get RenderProcessHost each time it's needed. This is to
  // avoid holding on to stale pointers if we get requests past the lifetime of
  // the objects.
  const content::ChildProcessId render_process_id_;

  // Tracker for favicon callback.
  std::unique_ptr<base::CancelableTaskTracker> tracker_;

  // Used for determining if `target_url` is allowed to be requested.
  GURL upstream_url_;

  base::CallbackListSubscription shutdown_subscription_;

  base::WeakPtrFactory<ExtensionURLLoader> weak_ptr_factory_{this};
};

class ExtensionURLLoaderFactory : public network::SelfDeletingURLLoaderFactory {
 public:
  ExtensionURLLoaderFactory(const ExtensionURLLoaderFactory&) = delete;
  ExtensionURLLoaderFactory& operator=(const ExtensionURLLoaderFactory&) =
      delete;

  static mojo::PendingRemote<network::mojom::URLLoaderFactory> Create(
      content::BrowserContext* browser_context,
      bool is_web_view_request,
      content::ChildProcessId render_process_id,
      std::optional<url::Origin> initiator_origin = std::nullopt) {
    DCHECK(browser_context);

    mojo::PendingRemote<network::mojom::URLLoaderFactory> pending_remote;

    // Return an unbound |pending_remote| if the |browser_context| has already
    // started shutting down.
    //
    // TODO(crbug.com/40243371): This should be a DCHECK or a CHECK
    // (no new ExtensionURLLoaderFactory should be created after BrowserContext
    // shutdown has started *if* all WebContents got closed before starting the
    // shutdown).
    if (browser_context->ShutdownStarted()) {
      return pending_remote;
    }

    base::MakeSelfDeleting<ExtensionURLLoaderFactory>(
        browser_context, is_web_view_request, render_process_id,
        std::move(initiator_origin),
        pending_remote.InitWithNewPipeAndPassReceiver());

    return pending_remote;
  }

  // Constructs ExtensionURLLoaderFactory bound to the |factory_receiver|.
  //
  // The factory is self-owned - it will delete itself once there are no more
  // receivers (including the receiver associated with the returned
  // mojo::PendingRemote and the receivers bound by the Clone method).  See also
  // the network::SelfDeletingURLLoaderFactory::OnDisconnect method.
  ExtensionURLLoaderFactory(
      content::BrowserContext* browser_context,
      bool is_web_view_request,
      content::ChildProcessId render_process_id,
      std::optional<url::Origin> initiator_origin,
      mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory_receiver,
      base::SelfDeletingPassKey key)
      : network::SelfDeletingURLLoaderFactory(std::move(factory_receiver), key),
        browser_context_(browser_context),
        is_web_view_request_(is_web_view_request),
        render_process_id_(render_process_id),
        initiator_origin_(std::move(initiator_origin)) {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

    // base::Unretained is safe below, because lifetime of
    // |browser_context_shutdown_subscription_| guarantees that
    // OnBrowserContextDestroyed won't be called after |this| is destroyed.
    browser_context_shutdown_subscription_ =
        ExtensionProtocolShutdownNotifierFactory::GetInstance()
            ->Get(browser_context)
            ->Subscribe(base::BindRepeating(
                &ExtensionURLLoaderFactory::OnBrowserContextDestroyed,
                base::Unretained(this)));
  }

  static void EnsureShutdownNotifierFactoryBuilt() {
    ExtensionProtocolShutdownNotifierFactory::GetInstance();
  }

 private:
  ~ExtensionURLLoaderFactory() override = default;

  // network::mojom::URLLoaderFactory:
  void CreateLoaderAndStart(
      mojo::PendingReceiver<network::mojom::URLLoader> loader,
      int32_t request_id,
      uint32_t options,
      const network::ResourceRequest& request,
      mojo::PendingRemote<network::mojom::URLLoaderClient> client,
      const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
      override {
    DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
    DCHECK_EQ(kExtensionScheme, request.url.GetScheme());
    ExtensionURLLoader::CreateAndStart(
        std::move(loader), std::move(client), request, is_web_view_request_,
        render_process_id_, browser_context_, initiator_origin_);
  }

  void OnBrowserContextDestroyed() {
    // When |browser_context_| gets destroyed, |this| factory is not able to
    // serve any more requests.
    DisconnectReceiversAndDestroy();
  }

  raw_ptr<content::BrowserContext> browser_context_;
  bool is_web_view_request_;

  // We store the ID and get RenderProcessHost each time it's needed. This is to
  // avoid holding on to stale pointers if we get requests past the lifetime of
  // the objects.
  const content::ChildProcessId render_process_id_;

  const std::optional<url::Origin> initiator_origin_;

  base::CallbackListSubscription browser_context_shutdown_subscription_;
};

}  // namespace

void SetExtensionProtocolTestHandler(ExtensionProtocolTestHandler* handler) {
  g_test_handler = handler;
}

mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateExtensionNavigationURLLoaderFactory(
    content::BrowserContext* browser_context,
    bool is_web_view_request) {
  return ExtensionURLLoaderFactory::Create(browser_context, is_web_view_request,
                                           content::ChildProcessId());
}

mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateExtensionWorkerMainResourceURLLoaderFactory(
    content::BrowserContext* browser_context,
    const std::optional<url::Origin>& request_initiator) {
  return ExtensionURLLoaderFactory::Create(browser_context,
                                           /*is_web_view_request=*/false,
                                           content::ChildProcessId(),
                                           request_initiator);
}

mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateExtensionServiceWorkerScriptURLLoaderFactory(
    content::BrowserContext* browser_context) {
  return ExtensionURLLoaderFactory::Create(browser_context,
                                           /*is_web_view_request=*/false,
                                           content::ChildProcessId());
}

mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateExtensionURLLoaderFactory(content::ChildProcessId render_process_id,
                                int render_frame_id) {
  content::RenderProcessHost* process_host =
      content::RenderProcessHost::FromID(render_process_id);
  content::BrowserContext* browser_context = process_host->GetBrowserContext();
  bool is_web_view_request = false;

#if BUILDFLAG(ENABLE_GUEST_VIEW)
  content::RenderFrameHost* render_frame_host =
      content::RenderFrameHost::FromID(render_process_id, render_frame_id);
  is_web_view_request =
      WebViewGuest::FromRenderFrameHost(render_frame_host) != nullptr;
#endif

  return ExtensionURLLoaderFactory::Create(browser_context, is_web_view_request,
                                           render_process_id);
}

void EnsureExtensionURLLoaderFactoryShutdownNotifierFactoryBuilt() {
  ExtensionURLLoaderFactory::EnsureShutdownNotifierFactoryBuilt();
}

}  // namespace extensions
