// 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 "chrome/browser/extensions/updater/local_extension_cache.h" #include "base/bind.h" #include "base/files/file_enumerator.h" #include "base/files/file_util.h" #include "base/sequenced_task_runner.h" #include "base/strings/string_util.h" #include "base/sys_info.h" #include "base/version.h" #include "components/crx_file/id_util.h" #include "content/public/browser/browser_thread.h" namespace extensions { namespace { // File name extension for CRX files (not case sensitive). const char kCRXFileExtension[] = ".crx"; // Delay between checks for flag file presence when waiting for the cache to // become ready. const int64_t kCacheStatusPollingDelayMs = 1000; } // namespace const char LocalExtensionCache::kCacheReadyFlagFileName[] = ".initialized"; LocalExtensionCache::LocalExtensionCache( const base::FilePath& cache_dir, uint64_t max_cache_size, const base::TimeDelta& max_cache_age, const scoped_refptr<base::SequencedTaskRunner>& backend_task_runner) : cache_dir_(cache_dir), max_cache_size_(max_cache_size), min_cache_age_(base::Time::Now() - max_cache_age), backend_task_runner_(backend_task_runner), state_(kUninitialized), cache_status_polling_delay_( base::TimeDelta::FromMilliseconds(kCacheStatusPollingDelayMs)), weak_ptr_factory_(this) {} LocalExtensionCache::~LocalExtensionCache() { if (state_ == kReady) CleanUp(); } void LocalExtensionCache::Init(bool wait_for_cache_initialization, const base::Closure& callback) { DCHECK_EQ(state_, kUninitialized); state_ = kWaitInitialization; if (wait_for_cache_initialization) CheckCacheStatus(callback); else CheckCacheContents(callback); } void LocalExtensionCache::Shutdown(const base::Closure& callback) { DCHECK_NE(state_, kShutdown); if (state_ == kReady) CleanUp(); state_ = kShutdown; backend_task_runner_->PostTaskAndReply(FROM_HERE, base::Bind(&base::DoNothing), callback); } // static LocalExtensionCache::CacheMap::iterator LocalExtensionCache::FindExtension( CacheMap& cache, const std::string& id, const std::string& expected_hash) { CacheHit hit = cache.equal_range(id); CacheMap::iterator empty_hash = cache.end(); std::string hash = base::ToLowerASCII(expected_hash); for (CacheMap::iterator it = hit.first; it != hit.second; ++it) { if (expected_hash.empty() || it->second.expected_hash == hash) { return it; } if (it->second.expected_hash.empty()) { empty_hash = it; } } return empty_hash; } bool LocalExtensionCache::GetExtension(const std::string& id, const std::string& expected_hash, base::FilePath* file_path, std::string* version) { if (state_ != kReady) return false; CacheMap::iterator it = FindExtension(cached_extensions_, id, expected_hash); if (it == cached_extensions_.end()) return false; if (file_path) { *file_path = it->second.file_path; // If caller is not interested in file_path, extension is not used. base::Time now = base::Time::Now(); backend_task_runner_->PostTask(FROM_HERE, base::Bind(&LocalExtensionCache::BackendMarkFileUsed, it->second.file_path, now)); it->second.last_used = now; } if (version) *version = it->second.version; return true; } bool LocalExtensionCache::ShouldRetryDownload( const std::string& id, const std::string& expected_hash) { if (state_ != kReady) return false; CacheMap::iterator it = FindExtension(cached_extensions_, id, expected_hash); if (it == cached_extensions_.end()) return false; return (!expected_hash.empty() && it->second.expected_hash.empty()); } // static bool LocalExtensionCache::NewerOrSame(const CacheMap::iterator& entry, const std::string& version, const std::string& expected_hash, int* compare) { Version new_version(version); Version prev_version(entry->second.version); int cmp = new_version.CompareTo(prev_version); if (compare) *compare = cmp; // Cache entry is newer if its version is greater or same, and in the latter // case we will prefer the existing one if we are trying to add an // unhashed file, or we already have a hashed file in cache. return (cmp < 0 || (cmp == 0 && (expected_hash.empty() || !entry->second.expected_hash.empty()))); } void LocalExtensionCache::PutExtension(const std::string& id, const std::string& expected_hash, const base::FilePath& file_path, const std::string& version, const PutExtensionCallback& callback) { if (state_ != kReady) { callback.Run(file_path, true); return; } Version version_validator(version); if (!version_validator.IsValid()) { LOG(ERROR) << "Extension " << id << " has bad version " << version; callback.Run(file_path, true); return; } CacheMap::iterator it = FindExtension(cached_extensions_, id, expected_hash); if (it != cached_extensions_.end() && NewerOrSame(it, version, expected_hash, NULL)) { LOG(WARNING) << "Cache contains newer or the same version " << it->second.version << " for extension " << id << " version " << version; callback.Run(file_path, true); return; } backend_task_runner_->PostTask( FROM_HERE, base::Bind(&LocalExtensionCache::BackendInstallCacheEntry, weak_ptr_factory_.GetWeakPtr(), cache_dir_, id, expected_hash, file_path, version, callback)); } bool LocalExtensionCache::RemoveExtensionAt(const CacheMap::iterator& it, bool match_hash) { if (state_ != kReady || it == cached_extensions_.end()) return false; std::string hash = match_hash ? it->second.expected_hash : std::string(); backend_task_runner_->PostTask( FROM_HERE, base::Bind(&LocalExtensionCache::BackendRemoveCacheEntry, cache_dir_, it->first, hash)); cached_extensions_.erase(it); return true; } bool LocalExtensionCache::RemoveExtension(const std::string& id, const std::string& expected_hash) { if (state_ != kReady) return false; CacheMap::iterator it = FindExtension(cached_extensions_, id, expected_hash); if (it == cached_extensions_.end()) return false; while (it != cached_extensions_.end()) { RemoveExtensionAt(it, !expected_hash.empty()); // For empty |expected_hash| this will iteratively return any cached file. // For any specific |expected_hash| this will only be able to find the // matching entry once. it = FindExtension(cached_extensions_, id, expected_hash); } return true; } bool LocalExtensionCache::GetStatistics(uint64_t* cache_size, size_t* extensions_count) { if (state_ != kReady) return false; *cache_size = 0; for (CacheMap::iterator it = cached_extensions_.begin(); it != cached_extensions_.end(); ++it) { *cache_size += it->second.size; } *extensions_count = cached_extensions_.size(); return true; } void LocalExtensionCache::SetCacheStatusPollingDelayForTests( const base::TimeDelta& delay) { cache_status_polling_delay_ = delay; } void LocalExtensionCache::CheckCacheStatus(const base::Closure& callback) { if (state_ == kShutdown) { callback.Run(); return; } backend_task_runner_->PostTask( FROM_HERE, base::Bind(&LocalExtensionCache::BackendCheckCacheStatus, weak_ptr_factory_.GetWeakPtr(), cache_dir_, callback)); } // static void LocalExtensionCache::BackendCheckCacheStatus( base::WeakPtr<LocalExtensionCache> local_cache, const base::FilePath& cache_dir, const base::Closure& callback) { const bool exists = base::PathExists(cache_dir.AppendASCII(kCacheReadyFlagFileName)); static bool first_check = true; if (first_check && !exists && !base::SysInfo::IsRunningOnChromeOS()) { LOG(WARNING) << "Extensions will not be installed from update URLs until " << cache_dir.AppendASCII(kCacheReadyFlagFileName).value() << " exists."; } first_check = false; content::BrowserThread::PostTask( content::BrowserThread::UI, FROM_HERE, base::Bind(&LocalExtensionCache::OnCacheStatusChecked, local_cache, exists, callback)); } void LocalExtensionCache::OnCacheStatusChecked(bool ready, const base::Closure& callback) { if (state_ == kShutdown) { callback.Run(); return; } if (ready) { CheckCacheContents(callback); } else { content::BrowserThread::PostDelayedTask( content::BrowserThread::UI, FROM_HERE, base::Bind(&LocalExtensionCache::CheckCacheStatus, weak_ptr_factory_.GetWeakPtr(), callback), cache_status_polling_delay_); } } void LocalExtensionCache::CheckCacheContents(const base::Closure& callback) { DCHECK_EQ(state_, kWaitInitialization); backend_task_runner_->PostTask( FROM_HERE, base::Bind(&LocalExtensionCache::BackendCheckCacheContents, weak_ptr_factory_.GetWeakPtr(), cache_dir_, callback)); } // static void LocalExtensionCache::BackendCheckCacheContents( base::WeakPtr<LocalExtensionCache> local_cache, const base::FilePath& cache_dir, const base::Closure& callback) { scoped_ptr<CacheMap> cache_content(new CacheMap); BackendCheckCacheContentsInternal(cache_dir, cache_content.get()); content::BrowserThread::PostTask( content::BrowserThread::UI, FROM_HERE, base::Bind(&LocalExtensionCache::OnCacheContentsChecked, local_cache, base::Passed(&cache_content), callback)); } // static LocalExtensionCache::CacheMap::iterator LocalExtensionCache::InsertCacheEntry( CacheMap& cache, const std::string& id, const CacheItemInfo& info, const bool delete_files) { bool keep = true; std::string any_hash; // FindExtension with empty hash will always return the first one CacheMap::iterator it = FindExtension(cache, id, any_hash); if (it != cache.end()) { // |cache_content| already has some version for this ID. Remove older ones. // If we loook at the first cache entry, it may be: // 1. an older version (in which case we should remove all its instances) // 2. a newer version (in which case we should skip current file) // 3. the same version without hash (skip if our hash is empty, // 4. remove if our hash in not empty), // 5. the same version with hash (skip if our hash is empty, // 6. skip if there is already an entry with the same hash, // otherwise add a new entry). int cmp = 0; if (!NewerOrSame(it, info.version, info.expected_hash, &cmp)) { // Case #1 or #4, remove all instances from cache. while ((it != cache.end()) && (it->first == id)) { if (delete_files) { base::DeleteFile(base::FilePath(it->second.file_path), true /* recursive */); VLOG(1) << "Remove older version " << it->second.version << " for extension id " << id; } it = cache.erase(it); } } else if ((cmp < 0) || (cmp == 0 && info.expected_hash.empty())) { // Case #2, #3 or #5 keep = false; } else if (cmp == 0) { // Same version, both hashes are not empty, try to find the same hash. while (keep && (it != cache.end()) && (it->first == id)) { if (it->second.expected_hash == info.expected_hash) { // Case #6 keep = false; } ++it; } } } if (keep) { it = cache.insert(std::make_pair(id, info)); } else { if (delete_files) { base::DeleteFile(info.file_path, true /* recursive */); VLOG(1) << "Remove older version " << info.version << " for extension id " << id; } it = cache.end(); } return it; } // static void LocalExtensionCache::BackendCheckCacheContentsInternal( const base::FilePath& cache_dir, CacheMap* cache_content) { // Start by verifying that the cache_dir exists. if (!base::DirectoryExists(cache_dir)) { // Create it now. if (!base::CreateDirectory(cache_dir)) { LOG(ERROR) << "Failed to create cache directory at " << cache_dir.value(); } // Nothing else to do. Cache is empty. return; } // Enumerate all the files in the cache |cache_dir|, including directories // and symlinks. Each unrecognized file will be erased. int types = base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES; base::FileEnumerator enumerator(cache_dir, false /* recursive */, types); for (base::FilePath path = enumerator.Next(); !path.empty(); path = enumerator.Next()) { base::FileEnumerator::FileInfo info = enumerator.GetInfo(); std::string basename = path.BaseName().value(); if (info.IsDirectory() || base::IsLink(info.GetName())) { LOG(ERROR) << "Erasing bad file in cache directory: " << basename; base::DeleteFile(path, true /* recursive */); continue; } // Skip flag file that indicates that cache is ready. if (basename == kCacheReadyFlagFileName) continue; // crx files in the cache are named // <extension-id>-<version>[-<expected_hash>].crx. std::string id; std::string version; std::string expected_hash; if (base::EndsWith(basename, kCRXFileExtension, base::CompareCase::INSENSITIVE_ASCII)) { size_t n = basename.find('-'); if (n != std::string::npos && n + 1 < basename.size() - 4) { id = basename.substr(0, n); // Size of |version| = total size - "<id>" - "-" - ".crx" version = basename.substr(n + 1, basename.size() - 5 - id.size()); n = version.find('-'); if (n != std::string::npos && n + 1 < version.size()) { expected_hash = version.substr(n + 1, version.size() - n - 1); version.resize(n); } } } // Enforce a lower-case id. id = base::ToLowerASCII(id); if (!crx_file::id_util::IdIsValid(id)) { LOG(ERROR) << "Bad extension id in cache: " << id; id.clear(); } if (!Version(version).IsValid()) { LOG(ERROR) << "Bad extension version in cache: " << version; version.clear(); } if (id.empty() || version.empty()) { LOG(ERROR) << "Invalid file in cache, erasing: " << basename; base::DeleteFile(path, true /* recursive */); continue; } VLOG(1) << "Found cached version " << version << " for extension id " << id; InsertCacheEntry( *cache_content, id, CacheItemInfo(version, expected_hash, info.GetLastModifiedTime(), info.GetSize(), path), true); } } void LocalExtensionCache::OnCacheContentsChecked( scoped_ptr<CacheMap> cache_content, const base::Closure& callback) { cache_content->swap(cached_extensions_); state_ = kReady; callback.Run(); } // static void LocalExtensionCache::BackendMarkFileUsed(const base::FilePath& file_path, const base::Time& time) { base::TouchFile(file_path, time, time); } // static std::string LocalExtensionCache::ExtensionFileName( const std::string& id, const std::string& version, const std::string& expected_hash) { std::string filename = id + "-" + version; if (!expected_hash.empty()) filename += "-" + base::ToLowerASCII(expected_hash); filename += kCRXFileExtension; return filename; } // static void LocalExtensionCache::BackendInstallCacheEntry( base::WeakPtr<LocalExtensionCache> local_cache, const base::FilePath& cache_dir, const std::string& id, const std::string& expected_hash, const base::FilePath& file_path, const std::string& version, const PutExtensionCallback& callback) { std::string basename = ExtensionFileName(id, version, expected_hash); base::FilePath cached_crx_path = cache_dir.AppendASCII(basename); bool was_error = false; if (base::PathExists(cached_crx_path)) { LOG(ERROR) << "File already exists " << file_path.value(); cached_crx_path = file_path; was_error = true; } base::File::Info info; if (!was_error) { if (!base::Move(file_path, cached_crx_path)) { LOG(ERROR) << "Failed to copy from " << file_path.value() << " to " << cached_crx_path.value(); cached_crx_path = file_path; was_error = true; } else { was_error = !base::GetFileInfo(cached_crx_path, &info); VLOG(1) << "Cache entry installed for extension id " << id << " version " << version; } } content::BrowserThread::PostTask( content::BrowserThread::UI, FROM_HERE, base::Bind(&LocalExtensionCache::OnCacheEntryInstalled, local_cache, id, CacheItemInfo(version, expected_hash, info.last_modified, info.size, cached_crx_path), was_error, callback)); } void LocalExtensionCache::OnCacheEntryInstalled( const std::string& id, const CacheItemInfo& info, bool was_error, const PutExtensionCallback& callback) { if (state_ == kShutdown || was_error) { // If |was_error| is true, it means that the |info.file_path| refers to the // original downloaded file, otherwise it refers to a file in cache, which // should not be deleted by CrxInstaller. callback.Run(info.file_path, was_error); return; } CacheMap::iterator it = InsertCacheEntry(cached_extensions_, id, info, false); if (it == cached_extensions_.end()) { DCHECK(0) << "Cache contains newer or the same version"; callback.Run(info.file_path, true); return; } // Time from file system can have lower precision so use precise "now". it->second.last_used = base::Time::Now(); callback.Run(info.file_path, false); } // static void LocalExtensionCache::BackendRemoveCacheEntry( const base::FilePath& cache_dir, const std::string& id, const std::string& expected_hash) { std::string file_pattern = ExtensionFileName(id, "*", expected_hash); base::FileEnumerator enumerator(cache_dir, false /* not recursive */, base::FileEnumerator::FILES, file_pattern); for (base::FilePath path = enumerator.Next(); !path.empty(); path = enumerator.Next()) { base::DeleteFile(path, false); VLOG(1) << "Removed cached file " << path.value(); } } // static bool LocalExtensionCache::CompareCacheItemsAge(const CacheMap::iterator& lhs, const CacheMap::iterator& rhs) { return lhs->second.last_used < rhs->second.last_used; } void LocalExtensionCache::CleanUp() { DCHECK_EQ(state_, kReady); std::vector<CacheMap::iterator> items; items.reserve(cached_extensions_.size()); uint64_t total_size = 0; for (CacheMap::iterator it = cached_extensions_.begin(); it != cached_extensions_.end(); ++it) { items.push_back(it); total_size += it->second.size; } std::sort(items.begin(), items.end(), CompareCacheItemsAge); for (std::vector<CacheMap::iterator>::iterator it = items.begin(); it != items.end(); ++it) { if ((*it)->second.last_used < min_cache_age_ || (max_cache_size_ && total_size > max_cache_size_)) { total_size -= (*it)->second.size; VLOG(1) << "Clean up cached extension id " << (*it)->first; RemoveExtensionAt(*it, true); } } } LocalExtensionCache::CacheItemInfo::CacheItemInfo( const std::string& version, const std::string& expected_hash, const base::Time& last_used, uint64_t size, const base::FilePath& file_path) : version(version), expected_hash(base::ToLowerASCII(expected_hash)), last_used(last_used), size(size), file_path(file_path) {} LocalExtensionCache::CacheItemInfo::~CacheItemInfo() { } } // namespace extensions