| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/content_verifier.h" |
| |
| #include <algorithm> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/containers/contains.h" |
| #include "base/files/file_path.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_util.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "extensions/browser/content_hash_fetcher.h" |
| #include "extensions/browser/content_hash_reader.h" |
| #include "extensions/browser/content_verifier/content_verifier_utils.h" |
| #include "extensions/browser/content_verifier_delegate.h" |
| #include "extensions/browser/extension_file_task_runner.h" |
| #include "extensions/common/api/declarative_net_request/dnr_manifest_data.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/extension_l10n_util.h" |
| #include "extensions/common/file_util.h" |
| #include "extensions/common/manifest_handlers/background_info.h" |
| #include "extensions/common/manifest_handlers/content_scripts_handler.h" |
| #include "extensions/common/utils/base_string.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| ContentVerifier::TestObserver* g_content_verifier_test_observer = nullptr; |
| using content_verifier_utils::CanonicalRelativePath; |
| |
| // This function converts paths like "//foo/bar", "./foo/bar", and |
| // "/foo/bar" to "foo/bar". It also converts path separators to "/". |
| base::FilePath NormalizeRelativePath(const base::FilePath& path) { |
| if (path.ReferencesParent()) |
| return base::FilePath(); |
| |
| std::vector<base::FilePath::StringType> parts; |
| path.GetComponents(&parts); |
| if (parts.empty()) |
| return base::FilePath(); |
| |
| // Remove the first component if it is '.' or '/' or '//'. |
| const base::FilePath::StringType separators( |
| base::FilePath::kSeparators, base::FilePath::kSeparatorsLength); |
| if (!parts[0].empty() && |
| (parts[0] == base::FilePath::kCurrentDirectory || |
| parts[0].find_first_not_of(separators) == std::string::npos)) |
| parts.erase(parts.begin()); |
| |
| // Note that elsewhere we always normalize path separators to '/' so this |
| // should work for all platforms. |
| base::FilePath::StringType normalized_relative_path = |
| base::JoinString(parts, base::FilePath::StringType(1, '/')); |
| // Preserve trailing separator, if present. |
| if (path.EndsWithSeparator()) |
| normalized_relative_path.append(1, '/'); |
| return base::FilePath(normalized_relative_path); |
| } |
| |
| bool HasScriptFileExt(const base::FilePath& requested_path) { |
| return requested_path.MatchesExtension(FILE_PATH_LITERAL(".js")); |
| } |
| |
| bool HasPageFileExt(const base::FilePath& requested_path) { |
| base::FilePath::StringType file_extension = requested_path.Extension(); |
| return requested_path.MatchesExtension(FILE_PATH_LITERAL(".html")) || |
| requested_path.MatchesExtension(FILE_PATH_LITERAL(".htm")); |
| } |
| |
| std::unique_ptr<ContentVerifierIOData::ExtensionData> CreateIOData( |
| const Extension* extension, |
| ContentVerifierDelegate* delegate) { |
| ContentVerifierDelegate::VerifierSourceType source_type = |
| delegate->GetVerifierSourceType(*extension); |
| if (source_type == ContentVerifierDelegate::VerifierSourceType::NONE) |
| return nullptr; |
| |
| // The browser image paths from the extension may not be relative (eg |
| // they might have leading '/' or './'), so we strip those to make |
| // comparing to actual relative paths work later on. |
| std::set<base::FilePath> original_image_paths = |
| delegate->GetBrowserImagePaths(extension); |
| auto canonicalize_path = [](const base::FilePath& relative_path) { |
| return content_verifier_utils::CanonicalizeRelativePath( |
| NormalizeRelativePath(relative_path)); |
| }; |
| |
| auto image_paths = std::make_unique<std::set<CanonicalRelativePath>>(); |
| for (const auto& path : original_image_paths) { |
| image_paths->insert(canonicalize_path(path)); |
| } |
| |
| auto background_or_content_paths = |
| std::make_unique<std::set<CanonicalRelativePath>>(); |
| for (const std::string& script : |
| BackgroundInfo::GetBackgroundScripts(extension)) { |
| background_or_content_paths->insert( |
| canonicalize_path(extension->GetResource(script).relative_path())); |
| } |
| if (BackgroundInfo::HasBackgroundPage(extension)) { |
| background_or_content_paths->insert( |
| canonicalize_path(extensions::file_util::ExtensionURLToRelativeFilePath( |
| BackgroundInfo::GetBackgroundURL(extension)))); |
| } |
| for (const std::unique_ptr<UserScript>& script : |
| ContentScriptsInfo::GetContentScripts(extension)) { |
| for (const std::unique_ptr<UserScript::File>& js_file : |
| script->js_scripts()) { |
| background_or_content_paths->insert( |
| canonicalize_path(js_file->relative_path())); |
| } |
| } |
| |
| auto indexed_ruleset_paths = |
| std::make_unique<std::set<CanonicalRelativePath>>(); |
| using DNRManifestData = declarative_net_request::DNRManifestData; |
| for (const DNRManifestData::RulesetInfo& info : |
| DNRManifestData::GetRulesets(*extension)) { |
| indexed_ruleset_paths->insert(canonicalize_path( |
| file_util::GetIndexedRulesetRelativePath(info.id.value()))); |
| } |
| |
| return std::make_unique<ContentVerifierIOData::ExtensionData>( |
| std::move(image_paths), std::move(background_or_content_paths), |
| std::move(indexed_ruleset_paths), extension->version(), source_type); |
| } |
| |
| // Returns all locales. |
| std::set<std::string> GetAllLocaleCandidates() { |
| std::set<std::string> all_locales; |
| // TODO(asargent) - see if we can cache this list longer to avoid |
| // having to fetch it more than once for a given run of the |
| // browser. Maybe it can never change at runtime? (Or if it can, maybe |
| // there is an event we can listen for to know to drop our cache). |
| extension_l10n_util::GetAllLocales(&all_locales); |
| return all_locales; |
| } |
| } // namespace |
| |
| struct ContentVerifier::CacheKey { |
| CacheKey(const ExtensionId& extension_id, |
| const base::Version& version, |
| bool needs_force_missing_computed_hashes_creation) |
| : extension_id(extension_id), |
| version(version), |
| needs_force_missing_computed_hashes_creation( |
| needs_force_missing_computed_hashes_creation) {} |
| |
| bool operator<(const CacheKey& other) const { |
| return std::tie(extension_id, version, |
| needs_force_missing_computed_hashes_creation) < |
| std::tie(other.extension_id, other.version, |
| other.needs_force_missing_computed_hashes_creation); |
| } |
| |
| ExtensionId extension_id; |
| base::Version version; |
| // TODO(lazyboy): This shouldn't be necessary as key. For the common |
| // case, we'd only want to cache successful ContentHash instances regardless |
| // of whether force creation was requested. |
| bool needs_force_missing_computed_hashes_creation = false; |
| }; |
| |
| // A class to retrieve ContentHash for ContentVerifier. |
| // |
| // All public calls originate and terminate on IO, making it suitable for |
| // ContentVerifier to cache ContentHash instances easily. |
| // |
| // This class makes sure we do not have more than one ContentHash request in |
| // flight for a particular version of an extension. If a call to retrieve an |
| // extensions's ContentHash is made while another retieval for the same |
| // version of the extension is in flight, this class will queue up the |
| // callback(s) and respond to all of them when ContentHash is available. |
| class ContentVerifier::HashHelper { |
| public: |
| explicit HashHelper(ContentVerifier* content_verifier) |
| : content_verifier_(content_verifier) {} |
| ~HashHelper() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| // TODO(lazyboy): Do we need to Cancel() the callacks? |
| } |
| |
| // Cancels any ongoing computed_hashes.json disk write for an extension. |
| void Cancel(const ExtensionId& extension_id, |
| const base::Version& extension_version) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| auto callback_key = std::make_pair(extension_id, extension_version); |
| auto iter = callback_infos_.find(callback_key); |
| if (iter == callback_infos_.end()) |
| return; |
| iter->second.Cancel(); |
| callback_infos_.erase(iter); |
| } |
| |
| // Retrieves the ContentHash of an extension and responds via |callback|. |
| // |
| // Must be called on IO thread. The method responds through |callback| on IO |
| // thread. |
| void GetContentHash(ContentHash::FetchKey fetch_key, |
| ContentVerifierDelegate::VerifierSourceType source_type, |
| bool force_missing_computed_hashes_creation, |
| ContentHashCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| auto callback_key = |
| std::make_pair(fetch_key.extension_id, fetch_key.extension_version); |
| auto iter = callback_infos_.find(callback_key); |
| if (iter != callback_infos_.end()) { |
| iter->second.callbacks.push_back(std::move(callback)); |
| iter->second.force_missing_computed_hashes_creation |= |
| force_missing_computed_hashes_creation; |
| return; |
| } |
| scoped_refptr<IsCancelledChecker> checker = |
| base::MakeRefCounted<IsCancelledChecker>(); |
| auto iter_pair = callback_infos_.emplace( |
| callback_key, CallbackInfo(checker, std::move(callback))); |
| DCHECK(iter_pair.second); |
| iter_pair.first->second.force_missing_computed_hashes_creation |= |
| force_missing_computed_hashes_creation; |
| |
| GetExtensionFileTaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &HashHelper::ReadHashOnFileTaskRunner, std::move(fetch_key), |
| source_type, |
| base::BindRepeating(&IsCancelledChecker::IsCancelled, checker), |
| base::BindOnce(&HashHelper::DidReadHash, weak_factory_.GetWeakPtr(), |
| callback_key, checker))); |
| } |
| |
| private: |
| using CallbackKey = std::pair<ExtensionId, base::Version>; |
| |
| class IsCancelledChecker |
| : public base::RefCountedThreadSafe<IsCancelledChecker> { |
| public: |
| IsCancelledChecker() {} |
| |
| // Safe to call from any thread. |
| void Cancel() { |
| base::AutoLock autolock(cancelled_lock_); |
| cancelled_ = true; |
| } |
| // Safe to call from any thread. |
| bool IsCancelled() { |
| base::AutoLock autolock(cancelled_lock_); |
| return cancelled_; |
| } |
| |
| private: |
| friend class base::RefCountedThreadSafe<IsCancelledChecker>; |
| |
| ~IsCancelledChecker() {} |
| |
| // Note: this may be accessed from multiple threads, so all access should |
| // be protected by |cancelled_lock_|. |
| bool cancelled_ = false; |
| |
| // A lock for synchronizing access to |cancelled_|. |
| base::Lock cancelled_lock_; |
| |
| DISALLOW_COPY_AND_ASSIGN(IsCancelledChecker); |
| }; |
| |
| // Holds information about each call to HashHelper::GetContentHash(), for a |
| // particular extension (id and version). |
| // |
| // |callbacks| are the callbacks that callers to GetContentHash() passed us. |
| // |cancelled_checker| is used to cancel an extension's task from any thread. |
| // |force_missing_computed_hashes_creation| is true if any callback (from |
| // ContentVerifyJob) requested to recompute computed_hashes.json file in |
| // case the file is missing or cannot be read. |
| struct CallbackInfo { |
| CallbackInfo(const scoped_refptr<IsCancelledChecker>& cancelled_checker, |
| ContentHashCallback callback) |
| : cancelled_checker(cancelled_checker) { |
| callbacks.push_back(std::move(callback)); |
| } |
| |
| void Cancel() { cancelled_checker->Cancel(); } |
| |
| base::TimeDelta elapsed() const { return elapsed_timer.Elapsed(); } |
| |
| scoped_refptr<IsCancelledChecker> cancelled_checker; |
| // TODO(lazyboy): Use std::list? |
| std::vector<ContentHashCallback> callbacks; |
| bool force_missing_computed_hashes_creation = false; |
| base::ElapsedTimer elapsed_timer; |
| }; |
| |
| using IsCancelledCallback = base::RepeatingCallback<bool(void)>; |
| |
| static void ForwardToIO(ContentHash::CreatedCallback callback, |
| scoped_refptr<ContentHash> content_hash, |
| bool was_cancelled) { |
| // If the request was cancelled, then we don't have a corresponding entry |
| // for the request in |callback_infos_| anymore. |
| if (was_cancelled) |
| return; |
| |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce(std::move(callback), content_hash, was_cancelled)); |
| } |
| |
| static void ReadHashOnFileTaskRunner( |
| ContentHash::FetchKey fetch_key, |
| ContentVerifierDelegate::VerifierSourceType source_type, |
| const IsCancelledCallback& is_cancelled, |
| ContentHash::CreatedCallback created_callback) { |
| ContentHash::Create( |
| std::move(fetch_key), source_type, is_cancelled, |
| base::BindOnce(&HashHelper::ForwardToIO, std::move(created_callback))); |
| } |
| |
| static void ForceBuildComputedHashesOnFileTaskRuner( |
| const scoped_refptr<ContentHash> content_hash, |
| const IsCancelledCallback& is_cancelled, |
| ContentHash::CreatedCallback created_callback) { |
| content_hash->ForceBuildComputedHashes( |
| is_cancelled, |
| base::BindOnce(&HashHelper::ForwardToIO, std::move(created_callback))); |
| } |
| |
| void DidReadHash(const CallbackKey& key, |
| const scoped_refptr<IsCancelledChecker>& checker, |
| scoped_refptr<ContentHash> content_hash, |
| bool was_cancelled) { |
| DCHECK(checker); |
| if (was_cancelled || |
| // The request might have been cancelled on IO after |content_hash| was |
| // built. |
| // TODO(lazyboy): Add a specific test case for this. See |
| // https://crbug.com/825470 for a likely example of this. |
| checker->IsCancelled()) { |
| return; |
| } |
| |
| auto iter = callback_infos_.find(key); |
| DCHECK(iter != callback_infos_.end()); |
| auto& callback_info = iter->second; |
| |
| // Force creation of computed_hashes.json if all of the following are true: |
| // - any caller(s) has explicitly requested it. |
| // - hash retrieval failed due to invalid computed_hashes.json and |
| // re-creating the file might make the hash retrieval successful. |
| if (callback_info.force_missing_computed_hashes_creation && |
| content_hash->might_require_computed_hashes_force_creation()) { |
| GetExtensionFileTaskRunner()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&HashHelper::ForceBuildComputedHashesOnFileTaskRuner, |
| content_hash, |
| base::BindRepeating(&IsCancelledChecker::IsCancelled, |
| callback_info.cancelled_checker), |
| base::BindOnce(&HashHelper::CompleteDidReadHash, |
| weak_factory_.GetWeakPtr(), key, |
| callback_info.cancelled_checker))); |
| return; |
| } |
| |
| CompleteDidReadHash(key, callback_info.cancelled_checker, |
| std::move(content_hash), was_cancelled); |
| } |
| |
| void CompleteDidReadHash(const CallbackKey& key, |
| const scoped_refptr<IsCancelledChecker>& checker, |
| scoped_refptr<ContentHash> content_hash, |
| bool was_cancelled) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| DCHECK(checker); |
| if (was_cancelled || |
| // The request might have been cancelled on IO after |content_hash| was |
| // built. |
| checker->IsCancelled()) { |
| return; |
| } |
| |
| auto iter = callback_infos_.find(key); |
| DCHECK(iter != callback_infos_.end()); |
| auto& callback_info = iter->second; |
| UMA_HISTOGRAM_TIMES("Extensions.ContentVerification.ReadContentHashTime", |
| callback_info.elapsed()); |
| |
| for (auto& callback : callback_info.callbacks) |
| std::move(callback).Run(content_hash); |
| callback_infos_.erase(iter); |
| |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| |
| // OnFetchComplete will check content_hash->hash_mismatch_unix_paths(): |
| content_verifier_->OnFetchComplete(content_hash); |
| } |
| |
| // List of pending callbacks of GetContentHash(). |
| std::map<CallbackKey, CallbackInfo> callback_infos_; |
| |
| ContentVerifier* const content_verifier_ = nullptr; |
| |
| base::WeakPtrFactory<HashHelper> weak_factory_{this}; |
| |
| DISALLOW_COPY_AND_ASSIGN(HashHelper); |
| }; |
| |
| // static |
| void ContentVerifier::SetObserverForTests(TestObserver* observer) { |
| g_content_verifier_test_observer = observer; |
| } |
| |
| ContentVerifier::ContentVerifier( |
| content::BrowserContext* context, |
| std::unique_ptr<ContentVerifierDelegate> delegate) |
| : context_(context), delegate_(std::move(delegate)) {} |
| |
| ContentVerifier::~ContentVerifier() { |
| } |
| |
| void ContentVerifier::Start() { |
| ExtensionRegistry* registry = ExtensionRegistry::Get(context_); |
| observation_.Observe(registry); |
| } |
| |
| void ContentVerifier::Shutdown() { |
| shutdown_on_ui_ = true; |
| delegate_->Shutdown(); |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&ContentVerifier::ShutdownOnIO, this)); |
| observation_.Reset(); |
| } |
| |
| void ContentVerifier::ShutdownOnIO() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| shutdown_on_io_ = true; |
| io_data_.Clear(); |
| hash_helper_.reset(); |
| } |
| |
| scoped_refptr<ContentVerifyJob> ContentVerifier::CreateAndStartJobFor( |
| const std::string& extension_id, |
| const base::FilePath& extension_root, |
| const base::FilePath& relative_path) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| |
| const ContentVerifierIOData::ExtensionData* data = |
| io_data_.GetData(extension_id); |
| // The absence of |data| generally means that we don't have to verify the |
| // extension resource. However, it could also mean that |
| // OnExtensionLoadedOnIO didn't get a chance to fire yet. |
| // See https://crbug.com/826584 for an example of how this can happen from |
| // ExtensionUserScriptLoader. Currently, ExtensionUserScriptLoader performs a |
| // thread hopping to work around this problem. |
| // TODO(lazyboy): Prefer queueing up jobs in these case instead of the thread |
| // hopping solution, but that requires a substantial change in |
| // ContnetVerifier/ContentVerifyJob. |
| if (!data) |
| return nullptr; |
| |
| base::FilePath normalized_unix_path = NormalizeRelativePath(relative_path); |
| |
| if (!ShouldVerifyAnyPaths(extension_id, extension_root, |
| {normalized_unix_path})) { |
| return nullptr; |
| } |
| |
| // TODO(asargent) - we can probably get some good performance wins by having |
| // a cache of ContentHashReader's that we hold onto past the end of each job. |
| scoped_refptr<ContentVerifyJob> job = base::MakeRefCounted<ContentVerifyJob>( |
| extension_id, data->version, extension_root, normalized_unix_path, |
| base::BindOnce(&ContentVerifier::VerifyFailed, this, extension_id)); |
| job->Start(this); |
| return job; |
| } |
| |
| void ContentVerifier::GetContentHash( |
| const ExtensionId& extension_id, |
| const base::FilePath& extension_root, |
| const base::Version& extension_version, |
| bool force_missing_computed_hashes_creation, |
| ContentHashCallback callback) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| if (shutdown_on_io_) { |
| // NOTE: Release |callback| asynchronously, so that we don't release ref of |
| // ContentVerifyJob and possibly destroy it synchronously here while |
| // ContentVerifyJob is holding a lock. The lock destroyer would fail DCHECK |
| // in that case. |
| // TODO(lazyboy): Make CreateJobFor return a scoped_refptr instead of raw |
| // pointer to fix this. Also add unit test to exercise this code path |
| // explicitly. |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(base::DoNothing::Once<ContentHashCallback>(), |
| std::move(callback))); |
| return; |
| } |
| |
| CacheKey cache_key(extension_id, extension_version, |
| force_missing_computed_hashes_creation); |
| auto cache_iter = cache_.find(cache_key); |
| if (cache_iter != cache_.end()) { |
| // Currently, we expect |callback| to be called asynchronously. |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), cache_iter->second)); |
| return; |
| } |
| |
| const ContentVerifierIOData::ExtensionData* data = |
| io_data_.GetData(extension_id); |
| DCHECK(data); |
| ContentHash::FetchKey fetch_key = |
| GetFetchKey(extension_id, extension_root, extension_version); |
| // Since |shutdown_on_io_| = false, GetOrCreateHashHelper() must return |
| // non-nullptr instance of HashHelper. |
| GetOrCreateHashHelper()->GetContentHash( |
| std::move(fetch_key), data->source_type, |
| force_missing_computed_hashes_creation, |
| base::BindOnce(&ContentVerifier::DidGetContentHash, this, cache_key, |
| std::move(callback))); |
| } |
| |
| bool ContentVerifier::ShouldComputeHashesOnInstall(const Extension& extension) { |
| return delegate_->GetVerifierSourceType(extension) == |
| ContentVerifierDelegate::VerifierSourceType::UNSIGNED_HASHES; |
| } |
| |
| void ContentVerifier::VerifyFailed(const ExtensionId& extension_id, |
| ContentVerifyJob::FailureReason reason) { |
| if (!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)) { |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&ContentVerifier::VerifyFailed, this, |
| extension_id, reason)); |
| return; |
| } |
| if (shutdown_on_ui_) |
| return; |
| |
| VLOG(1) << "VerifyFailed " << extension_id << " reason:" << reason; |
| DCHECK_NE(ContentVerifyJob::NONE, reason); |
| |
| delegate_->VerifyFailed(extension_id, reason); |
| } |
| |
| void ContentVerifier::OnExtensionLoaded( |
| content::BrowserContext* browser_context, |
| const Extension* extension) { |
| if (shutdown_on_ui_) |
| return; |
| |
| std::unique_ptr<ContentVerifierIOData::ExtensionData> io_data = |
| CreateIOData(extension, delegate_.get()); |
| if (io_data) { |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&ContentVerifier::OnExtensionLoadedOnIO, this, |
| extension->id(), extension->path(), |
| extension->version(), std::move(io_data))); |
| } |
| } |
| |
| void ContentVerifier::OnExtensionLoadedOnIO( |
| const ExtensionId& extension_id, |
| const base::FilePath& extension_root, |
| const base::Version& extension_version, |
| std::unique_ptr<ContentVerifierIOData::ExtensionData> data) { |
| if (shutdown_on_io_) |
| return; |
| |
| io_data_.AddData(extension_id, std::move(data)); |
| GetContentHash(extension_id, extension_root, extension_version, |
| false /* force_missing_computed_hashes_creation */, |
| // HashHelper will respond directly to OnFetchComplete(). |
| base::DoNothing()); |
| } |
| |
| void ContentVerifier::OnExtensionUnloaded( |
| content::BrowserContext* browser_context, |
| const Extension* extension, |
| UnloadedExtensionReason reason) { |
| if (shutdown_on_ui_) |
| return; |
| content::GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&ContentVerifier::OnExtensionUnloadedOnIO, this, |
| extension->id(), extension->version())); |
| } |
| |
| ContentVerifierKey ContentVerifier::GetContentVerifierKey() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| return delegate_->GetPublicKey(); |
| } |
| |
| GURL ContentVerifier::GetSignatureFetchUrlForTest( |
| const ExtensionId& extension_id, |
| const base::Version& extension_version) { |
| return delegate_->GetSignatureFetchUrl(extension_id, extension_version); |
| } |
| |
| void ContentVerifier::VerifyFailedForTest( |
| const ExtensionId& extension_id, |
| ContentVerifyJob::FailureReason reason) { |
| VerifyFailed(extension_id, reason); |
| } |
| |
| void ContentVerifier::ClearCacheForTesting() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| cache_.clear(); |
| } |
| |
| void ContentVerifier::OnExtensionUnloadedOnIO( |
| const ExtensionId& extension_id, |
| const base::Version& extension_version) { |
| if (shutdown_on_io_) |
| return; |
| io_data_.RemoveData(extension_id); |
| |
| // Remove all possible cache entries for this extension version. |
| cache_.erase(CacheKey(extension_id, extension_version, true)); |
| cache_.erase(CacheKey(extension_id, extension_version, false)); |
| |
| HashHelper* hash_helper = GetOrCreateHashHelper(); |
| if (hash_helper) |
| hash_helper->Cancel(extension_id, extension_version); |
| } |
| |
| void ContentVerifier::OnFetchComplete( |
| const scoped_refptr<const ContentHash>& content_hash) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| ExtensionId extension_id = content_hash->extension_id(); |
| VLOG(1) << "OnFetchComplete " << extension_id |
| << " success:" << content_hash->succeeded(); |
| |
| const bool did_hash_mismatch = |
| ShouldVerifyAnyPaths(extension_id, content_hash->extension_root(), |
| content_hash->hash_mismatch_unix_paths()); |
| if (g_content_verifier_test_observer) { |
| g_content_verifier_test_observer->OnFetchComplete(content_hash, |
| did_hash_mismatch); |
| } |
| |
| if (!did_hash_mismatch) |
| return; |
| |
| VerifyFailed(extension_id, ContentVerifyJob::HASH_MISMATCH); |
| } |
| |
| ContentHash::FetchKey ContentVerifier::GetFetchKey( |
| const ExtensionId& extension_id, |
| const base::FilePath& extension_root, |
| const base::Version& extension_version) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| |
| const ContentVerifierIOData::ExtensionData* data = |
| io_data_.GetData(extension_id); |
| DCHECK(data); |
| if (data->source_type == |
| ContentVerifierDelegate::VerifierSourceType::UNSIGNED_HASHES) { |
| return ContentHash::FetchKey(extension_id, extension_root, |
| extension_version, mojo::NullRemote(), |
| GURL::EmptyGURL(), ContentVerifierKey()); |
| } |
| |
| // Create a new mojo pipe. It's safe to pass this around and use immediately, |
| // even though it needs to finish initialization on the UI thread. |
| mojo::PendingRemote<network::mojom::URLLoaderFactory> |
| url_loader_factory_remote; |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &ContentVerifier::BindURLLoaderFactoryReceiverOnUIThread, this, |
| url_loader_factory_remote.InitWithNewPipeAndPassReceiver())); |
| return ContentHash::FetchKey( |
| extension_id, extension_root, extension_version, |
| std::move(url_loader_factory_remote), |
| delegate_->GetSignatureFetchUrl(extension_id, extension_version), |
| delegate_->GetPublicKey()); |
| } |
| |
| void ContentVerifier::DidGetContentHash( |
| const CacheKey& cache_key, |
| ContentHashCallback original_callback, |
| scoped_refptr<const ContentHash> content_hash) { |
| cache_[cache_key] = content_hash; |
| std::move(original_callback).Run(content_hash); |
| } |
| |
| void ContentVerifier::BindURLLoaderFactoryReceiverOnUIThread( |
| mojo::PendingReceiver<network::mojom::URLLoaderFactory> |
| url_loader_factory_receiver) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (shutdown_on_ui_) |
| return; |
| |
| context_->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess() |
| ->Clone(std::move(url_loader_factory_receiver)); |
| } |
| |
| bool ContentVerifier::ShouldVerifyAnyPaths( |
| const std::string& extension_id, |
| const base::FilePath& extension_root, |
| const std::set<base::FilePath>& relative_unix_paths) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| const ContentVerifierIOData::ExtensionData* data = |
| io_data_.GetData(extension_id); |
| if (!data) |
| return false; |
| |
| const std::set<CanonicalRelativePath>& browser_images = |
| *(data->canonical_browser_image_paths); |
| const std::set<CanonicalRelativePath>& background_or_content_paths = |
| *(data->canonical_background_or_content_paths); |
| const std::set<CanonicalRelativePath>& indexed_ruleset_paths = |
| *(data->canonical_indexed_ruleset_paths); |
| |
| absl::optional<std::set<std::string>> all_locale_candidates; |
| |
| const CanonicalRelativePath manifest_file = |
| content_verifier_utils::CanonicalizeRelativePath( |
| base::FilePath(kManifestFilename)); |
| const base::FilePath messages_file(kMessagesFilename); |
| const base::FilePath locales_relative_dir(kLocaleFolder); |
| for (const base::FilePath& relative_unix_path : relative_unix_paths) { |
| if (relative_unix_path.empty()) |
| continue; |
| |
| CanonicalRelativePath canonical_path_value = |
| content_verifier_utils::CanonicalizeRelativePath(relative_unix_path); |
| |
| if (canonical_path_value == manifest_file) |
| continue; |
| |
| // JavaScript and HTML files should always be verified. |
| if (HasScriptFileExt(relative_unix_path) || |
| HasPageFileExt(relative_unix_path)) { |
| return true; |
| } |
| |
| // Background pages, scripts and content scripts should always be verified |
| // regardless of their file type. |
| if (base::Contains(background_or_content_paths, canonical_path_value)) |
| return true; |
| |
| if (base::Contains(browser_images, canonical_path_value)) |
| continue; |
| |
| // Skip indexed rulesets since these are generated. |
| if (base::Contains(indexed_ruleset_paths, canonical_path_value)) |
| continue; |
| |
| const base::FilePath canonical_path(canonical_path_value.value()); |
| if (locales_relative_dir.IsParent(canonical_path)) { |
| if (!all_locale_candidates) |
| all_locale_candidates = GetAllLocaleCandidates(); |
| |
| // Since message catalogs get transcoded during installation, we want |
| // to skip those paths. See if this path looks like |
| // _locales/<some locale>/messages.json - if so then skip it. |
| if (canonical_path.BaseName() == messages_file && |
| canonical_path.DirName().DirName() == locales_relative_dir && |
| ContainsStringIgnoreCaseASCII( |
| *all_locale_candidates, |
| canonical_path.DirName().BaseName().MaybeAsASCII())) { |
| continue; |
| } |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| ContentVerifier::HashHelper* ContentVerifier::GetOrCreateHashHelper() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::IO); |
| DCHECK(!shutdown_on_io_) << "Creating HashHelper after IO shutdown"; |
| // Just checking |hash_helper_| against nullptr isn't enough because we reset |
| // hash_helper_ in Shutdown(), and we shouldn't be re-creating it in that |
| // case. |
| if (!hash_helper_created_) { |
| DCHECK(!hash_helper_); |
| hash_helper_ = |
| std::unique_ptr<HashHelper, content::BrowserThread::DeleteOnIOThread>( |
| new HashHelper(this)); |
| hash_helper_created_ = true; |
| } |
| return hash_helper_.get(); |
| } |
| |
| void ContentVerifier::ResetIODataForTesting(const Extension* extension) { |
| io_data_.AddData(extension->id(), CreateIOData(extension, delegate_.get())); |
| } |
| |
| base::FilePath ContentVerifier::NormalizeRelativePathForTesting( |
| const base::FilePath& path) { |
| return NormalizeRelativePath(path); |
| } |
| |
| bool ContentVerifier::ShouldVerifyAnyPathsForTesting( |
| const std::string& extension_id, |
| const base::FilePath& extension_root, |
| const std::set<base::FilePath>& relative_unix_paths) { |
| return ShouldVerifyAnyPaths(extension_id, extension_root, |
| relative_unix_paths); |
| } |
| |
| void ContentVerifier::OverrideDelegateForTesting( |
| std::unique_ptr<ContentVerifierDelegate> delegate) { |
| delegate_ = std::move(delegate); |
| } |
| |
| } // namespace extensions |