blob: f8350798a124329d3df2c98895043f6e7ce40649 [file] [log] [blame]
// 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/compiler_specific.h"
#include "base/containers/span.h"
#include "base/feature_list.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/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 "crypto/secure_hash.h"
#include "crypto/sha2.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 {
BASE_FEATURE(kOverrideExtensionFilesMimeTypes,
"OverrideExtensionFilesMimeTypes",
base::FEATURE_ENABLED_BY_DEFAULT);
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;
*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,
int 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.host(), 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 == content::ChildProcessHost::kInvalidUniqueID &&
(blink::IsRequestDestinationFrame(destination) ||
destination == network::mojom::RequestDestination::kWorker ||
destination == network::mojom::RequestDestination::kSharedWorker ||
destination == network::mojom::RequestDestination::kScript ||
destination == network::mojom::RequestDestination::kServiceWorker)) {
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.host(), extension->id());
std::string_view path = url.path_piece();
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.path();
// 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_piece();
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 ==
extension.GetResourceURL(
BackgroundInfo::GetBackgroundServiceWorkerScript(&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 !BUILDFLAG(IS_FUCHSIA)
if (last_modified_time.is_null()) {
return;
}
#endif // !BUILDFLAG(IS_FUCHSIA)
// 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 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,
int render_process_id,
content::BrowserContext* browser_context) {
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);
url_loader->Start();
}
ExtensionURLLoader(const ExtensionURLLoader&) = delete;
ExtensionURLLoader& operator=(const ExtensionURLLoader&) = delete;
// network::mojom::URLLoader:
void FollowRedirect(
const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
const net::HttpRequestHeaders& modified_cors_exempt_headers,
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 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,
int render_process_id,
content::BrowserContext* browser_context)
: request_(request),
browser_context_(browser_context),
is_web_view_request_(is_web_view_request),
render_process_id_(render_process_id) {
client_.Bind(std::move(client));
loader_.Bind(std::move(loader));
loader_.set_disconnect_handler(base::BindOnce(
&ExtensionURLLoader::OnMojoDisconnect, weak_ptr_factory_.GetWeakPtr()));
}
// `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 URLLoaderFactory IPCs which may still be in-flight until (as
// part of BrowserContext destruction sequence) OnBrowserContextDestroyed
// below is called (which will prevent future IPCs by calling
// DisconnectReceiversAndDestroy). Note that DisconnectReceiversAndDestroy
// will only stop future ExtensionURLLoaderFactory IPCs, but it won't stop
// future ExtensionURLLoader IPCs - this is okay, because the loader doesn't
// directly interact with the BrowserContext.
if (browser_context_->ShutdownStarted()) {
CompleteRequestAndDeleteThis(net::ERR_FAILED);
return;
}
const ExtensionId extension_id = request_.url.host();
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.host() == 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,
scoped_refptr<net::HttpResponseHeaders> headers,
scoped_refptr<ContentVerifier> content_verifier,
const ResourceInfo& resource_info) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const auto& read_file_path = resource_info.file_path;
const auto& last_modified_time = resource_info.last_modified_time;
const auto& file_size = resource_info.size;
request_.url = net::FilePathToFileURL(read_file_path);
AddCacheHeaders(*headers, last_modified_time);
// TODO(crbug.com/400647848): Remove this if-check and always override mime
// type headers in M139.
if (base::FeatureList::IsEnabled(kOverrideExtensionFilesMimeTypes)) {
AddMimeTypeHeaders(*headers, read_file_path);
}
// 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.
bool should_verify_content = true;
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.
should_verify_content = !ranges[0].HasLastBytePosition() ||
ranges[0].last_byte_position() == file_size - 1;
} else {
// Malformed range header or multiple ranges detected. The FileURLLoader
// will also detect this and return an error.
should_verify_content = false;
}
}
scoped_refptr<ContentVerifyJob> verify_job;
if (content_verifier && should_verify_content) {
verify_job = ContentVerifier::CreateAndStartJobFor(
resource.extension_id(), resource.extension_root(),
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();
}
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);
if (is_background_page_url || is_favicon_url) {
// 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)));
}
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());
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.path();
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,
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_;
const 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 int 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::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,
int render_process_id) {
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;
}
// Manages its own lifetime.
new ExtensionURLLoaderFactory(
browser_context, is_web_view_request, render_process_id,
pending_remote.InitWithNewPipeAndPassReceiver());
return pending_remote;
}
static void EnsureShutdownNotifierFactoryBuilt() {
BrowserContextShutdownNotifierFactory::GetInstance();
}
private:
// 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,
int render_process_id,
mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory_receiver)
: network::SelfDeletingURLLoaderFactory(std::move(factory_receiver)),
browser_context_(browser_context),
is_web_view_request_(is_web_view_request),
render_process_id_(render_process_id) {
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_ =
BrowserContextShutdownNotifierFactory::GetInstance()
->Get(browser_context)
->Subscribe(base::BindRepeating(
&ExtensionURLLoaderFactory::OnBrowserContextDestroyed,
base::Unretained(this)));
}
~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.scheme());
ExtensionURLLoader::CreateAndStart(std::move(loader), std::move(client),
request, is_web_view_request_,
render_process_id_, browser_context_);
}
void OnBrowserContextDestroyed() {
// When |browser_context_| gets destroyed, |this| factory is not able to
// serve any more requests.
DisconnectReceiversAndDestroy();
}
class BrowserContextShutdownNotifierFactory
: public BrowserContextKeyedServiceShutdownNotifierFactory {
public:
static BrowserContextShutdownNotifierFactory* GetInstance() {
static base::NoDestructor<BrowserContextShutdownNotifierFactory>
s_factory;
return s_factory.get();
}
// No copying.
BrowserContextShutdownNotifierFactory(
const BrowserContextShutdownNotifierFactory&) = delete;
BrowserContextShutdownNotifierFactory& operator=(
const BrowserContextShutdownNotifierFactory&) = delete;
private:
friend class base::NoDestructor<BrowserContextShutdownNotifierFactory>;
BrowserContextShutdownNotifierFactory()
: BrowserContextKeyedServiceShutdownNotifierFactory(
"ExtensionURLLoaderFactory::"
"BrowserContextShutdownNotifierFactory") {
DependsOn(ExtensionRegistryFactory::GetInstance());
DependsOn(ProcessMapFactory::GetInstance());
}
content::BrowserContext* GetBrowserContextToUse(
content::BrowserContext* context) const override {
return ExtensionsBrowserClient::Get()->GetContextOwnInstance(context);
}
};
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 int render_process_id_;
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::ChildProcessHost::kInvalidUniqueID);
}
mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateExtensionWorkerMainResourceURLLoaderFactory(
content::BrowserContext* browser_context) {
return ExtensionURLLoaderFactory::Create(
browser_context,
/*is_web_view_request=*/false,
content::ChildProcessHost::kInvalidUniqueID);
}
mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateExtensionServiceWorkerScriptURLLoaderFactory(
content::BrowserContext* browser_context) {
return ExtensionURLLoaderFactory::Create(
browser_context,
/*is_web_view_request=*/false,
content::ChildProcessHost::kInvalidUniqueID);
}
mojo::PendingRemote<network::mojom::URLLoaderFactory>
CreateExtensionURLLoaderFactory(int 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