| // 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 |