| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/component_updater/cros_component_installer_chromeos.h" |
| |
| #include <algorithm> |
| #include <map> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/to_string.h" |
| #include "base/task/thread_pool.h" |
| #include "chrome/browser/ash/login/demo_mode/demo_mode_dimensions.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/component_updater/component_installer_errors.h" |
| #include "chrome/browser/component_updater/metadata_table_chromeos.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/pref_names.h" |
| #include "chromeos/ash/components/dbus/dbus_thread_manager.h" |
| #include "chromeos/ash/components/dbus/image_loader/image_loader_client.h" |
| #include "chromeos/ash/components/settings/cros_settings.h" |
| #include "chromeos/ash/components/settings/cros_settings_names.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "components/component_updater/component_updater_paths.h" |
| #include "components/component_updater/component_updater_switches.h" |
| #include "components/crx_file/id_util.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/version_info/version_info.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "crypto/sha2.h" |
| |
| namespace component_updater { |
| |
| // Root path where all components are stored. |
| constexpr char kComponentsRootPath[] = "cros-components"; |
| |
| namespace { |
| |
| // All downloadable Chrome OS components. |
| const ComponentConfig kConfigs[] = { |
| {"cros-termina", ComponentConfig::PolicyType::kEnvVersion, "980.1", |
| "e9d960f84f628e1f42d05de4046bb5b3154b6f1f65c08412c6af57a29aecaffb"}, |
| {"rtanalytics-full", ComponentConfig::PolicyType::kEnvVersion, "106.0", |
| "c93c3e1013c52100a20038b405ac854d69fa889f6dc4fa6f188267051e05e444"}, |
| {"demo-mode-resources", ComponentConfig::PolicyType::kEnvVersion, "1.0", |
| "93c093ebac788581389015e9c59c5af111d2fa5174d206eb795042e6376cbd10"}, |
| {"demo-mode-app", ComponentConfig::PolicyType::kDemoApp, nullptr, |
| "b6c5ce9f03b0ce830eb5f9f92ed3016cfdb7a2327330f0187adbe9a00ddfd34d"}, |
| {"growth-campaigns", ComponentConfig::PolicyType::kGrowthCampaigns, nullptr, |
| "36448796af5fb67380ec0180a8379ddd26fce20d3da6a231e0a60dfe2360407e"}, |
| }; |
| |
| const ComponentConfig* FindConfig(const std::string& name) { |
| const ComponentConfig* config = |
| std::ranges::find(kConfigs, name, &ComponentConfig::name); |
| if (config == std::end(kConfigs)) { |
| return nullptr; |
| } |
| return config; |
| } |
| |
| void LogCustomUninstall(std::optional<bool> result) {} |
| |
| void FinishCustomUninstallOnUIThread(const std::string& name) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| ash::ImageLoaderClient::Get()->UnmountComponent( |
| name, base::BindOnce(&LogCustomUninstall)); |
| } |
| |
| std::string GenerateId(const std::string& sha2hashstr) { |
| // kIdSize is the count of a pair of hex in the sha2hash array. |
| // In string representation of sha2hash, size is doubled since each hex is |
| // represented by a single char. |
| return crx_file::id_util::GenerateIdFromHex( |
| sha2hashstr.substr(0, crx_file::id_util::kIdSize * 2)); |
| } |
| |
| // Returns all installed components. |
| std::vector<ComponentConfig> GetInstalled() { |
| std::vector<ComponentConfig> configs; |
| base::FilePath root; |
| if (!base::PathService::Get(DIR_COMPONENT_USER, &root)) { |
| return configs; |
| } |
| |
| root = root.Append(kComponentsRootPath); |
| for (const ComponentConfig& config : kConfigs) { |
| base::FilePath component_path = root.Append(config.name); |
| if (base::PathExists(component_path)) { |
| configs.push_back(config); |
| } |
| } |
| return configs; |
| } |
| |
| // Report Error code. |
| void ReportError(ComponentManagerAsh::Error error) { |
| UMA_HISTOGRAM_ENUMERATION("ComponentUpdater.InstallResult", error); |
| } |
| |
| } // namespace |
| |
| CrOSComponentInstallerPolicy::CrOSComponentInstallerPolicy( |
| const ComponentConfig& config, |
| CrOSComponentInstaller* cros_component_installer) |
| : cros_component_installer_(cros_component_installer), name_(config.name) { |
| if (strlen(config.sha2hash) != crypto::kSHA256Length * 2) { |
| return; |
| } |
| |
| bool converted = base::HexStringToBytes(config.sha2hash, &sha2_hash_); |
| DCHECK(converted); |
| DCHECK_EQ(crypto::kSHA256Length, sha2_hash_.size()); |
| } |
| |
| CrOSComponentInstallerPolicy::~CrOSComponentInstallerPolicy() = default; |
| |
| bool CrOSComponentInstallerPolicy::SupportsGroupPolicyEnabledComponentUpdates() |
| const { |
| return true; |
| } |
| |
| bool CrOSComponentInstallerPolicy::RequiresNetworkEncryption() const { |
| return true; |
| } |
| |
| bool CrOSComponentInstallerPolicy::AllowUpdates() const { |
| return true; |
| } |
| |
| update_client::CrxInstaller::Result |
| CrOSComponentInstallerPolicy::OnCustomInstall( |
| const base::Value::Dict& manifest, |
| const base::FilePath& install_dir) { |
| cros_component_installer_->EmitInstalledSignal(GetName()); |
| |
| return update_client::CrxInstaller::Result(update_client::InstallError::NONE); |
| } |
| |
| void CrOSComponentInstallerPolicy::OnCustomUninstall() { |
| cros_component_installer_->UnregisterCompatiblePath(name_); |
| |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&FinishCustomUninstallOnUIThread, name_)); |
| } |
| |
| bool CrOSComponentInstallerPolicy::VerifyInstallation( |
| const base::Value::Dict& manifest, |
| const base::FilePath& install_dir) const { |
| return true; |
| } |
| |
| base::FilePath CrOSComponentInstallerPolicy::GetRelativeInstallDir() const { |
| base::FilePath path = base::FilePath(kComponentsRootPath); |
| return path.Append(name_); |
| } |
| |
| void CrOSComponentInstallerPolicy::GetHash(std::vector<uint8_t>* hash) const { |
| *hash = sha2_hash_; |
| } |
| |
| std::string CrOSComponentInstallerPolicy::GetName() const { |
| return name_; |
| } |
| |
| EnvVersionInstallerPolicy::EnvVersionInstallerPolicy( |
| const ComponentConfig& config, |
| CrOSComponentInstaller* cros_component_installer) |
| : CrOSComponentInstallerPolicy(config, cros_component_installer), |
| env_version_(config.env_version) { |
| DCHECK(!env_version_.empty()); |
| } |
| |
| EnvVersionInstallerPolicy::~EnvVersionInstallerPolicy() = default; |
| |
| void EnvVersionInstallerPolicy::ComponentReady(const base::Version& version, |
| const base::FilePath& path, |
| base::Value::Dict manifest) { |
| std::string* min_env_version = manifest.FindString("min_env_version"); |
| if (!min_env_version) { |
| return; |
| } |
| |
| if (!IsCompatible(env_version_, *min_env_version)) { |
| return; |
| } |
| |
| cros_component_installer_->RegisterCompatiblePath( |
| GetName(), CompatibleComponentInfo(path, version)); |
| } |
| |
| update_client::InstallerAttributes |
| EnvVersionInstallerPolicy::GetInstallerAttributes() const { |
| update_client::InstallerAttributes attrs; |
| attrs["_env_version"] = env_version_; |
| return attrs; |
| } |
| |
| // static |
| bool EnvVersionInstallerPolicy::IsCompatible( |
| const std::string& env_version_str, |
| const std::string& min_env_version_str) { |
| base::Version env_version(env_version_str); |
| base::Version min_env_version(min_env_version_str); |
| return env_version.IsValid() && min_env_version.IsValid() && |
| env_version.components()[0] == min_env_version.components()[0] && |
| env_version >= min_env_version; |
| } |
| |
| DemoAppInstallerPolicy::DemoAppInstallerPolicy( |
| const ComponentConfig& config, |
| CrOSComponentInstaller* cros_component_installer) |
| : CrOSComponentInstallerPolicy(config, cros_component_installer) {} |
| |
| DemoAppInstallerPolicy::~DemoAppInstallerPolicy() = default; |
| |
| void DemoAppInstallerPolicy::ComponentReady(const base::Version& version, |
| const base::FilePath& path, |
| base::Value::Dict manifest) { |
| cros_component_installer_->RegisterCompatiblePath( |
| GetName(), CompatibleComponentInfo(path, version)); |
| } |
| |
| update_client::InstallerAttributes |
| DemoAppInstallerPolicy::GetInstallerAttributes() const { |
| update_client::InstallerAttributes demo_app_installer_attributes; |
| demo_app_installer_attributes["retailer_id"] = ash::demo_mode::RetailerName(); |
| demo_app_installer_attributes["store_id"] = ash::demo_mode::StoreNumber(); |
| demo_app_installer_attributes["demo_country"] = ash::demo_mode::Country(); |
| demo_app_installer_attributes["is_cloud_gaming_device"] = |
| base::ToString(ash::demo_mode::IsCloudGamingDevice()); |
| demo_app_installer_attributes["is_feature_aware_device"] = |
| base::ToString(ash::demo_mode::IsFeatureAwareDevice()); |
| |
| auto* const cmdline = base::CommandLine::ForCurrentProcess(); |
| if (cmdline->HasSwitch(switches::kDemoModeTestTag)) { |
| demo_app_installer_attributes["tag"] = |
| cmdline->GetSwitchValueASCII(switches::kDemoModeTestTag); |
| } |
| return demo_app_installer_attributes; |
| } |
| |
| GrowthCampaignsInstallerPolicy::GrowthCampaignsInstallerPolicy( |
| const ComponentConfig& config, |
| CrOSComponentInstaller* cros_component_installer) |
| : CrOSComponentInstallerPolicy(config, cros_component_installer) {} |
| |
| GrowthCampaignsInstallerPolicy::~GrowthCampaignsInstallerPolicy() = default; |
| |
| void GrowthCampaignsInstallerPolicy::ComponentReady( |
| const base::Version& version, |
| const base::FilePath& path, |
| base::Value::Dict manifest) { |
| cros_component_installer_->RegisterCompatiblePath( |
| GetName(), CompatibleComponentInfo(path, version)); |
| } |
| |
| update_client::InstallerAttributes |
| GrowthCampaignsInstallerPolicy::GetInstallerAttributes() const { |
| update_client::InstallerAttributes attributes; |
| auto* const cmdline = base::CommandLine::ForCurrentProcess(); |
| if (cmdline->HasSwitch(switches::kCampaignsTestTag)) { |
| attributes["tag"] = |
| cmdline->GetSwitchValueASCII(switches::kCampaignsTestTag); |
| } |
| return attributes; |
| } |
| |
| CrOSComponentInstaller::CrOSComponentInstaller( |
| std::unique_ptr<MetadataTable> metadata_table, |
| ComponentUpdateService* component_updater) |
| : metadata_table_(std::move(metadata_table)), |
| component_updater_(component_updater) {} |
| |
| CrOSComponentInstaller::~CrOSComponentInstaller() = default; |
| |
| void CrOSComponentInstaller::SetDelegate(Delegate* delegate) { |
| delegate_ = delegate; |
| } |
| |
| void CrOSComponentInstaller::Load(const std::string& name, |
| MountPolicy mount_policy, |
| UpdatePolicy update_policy, |
| LoadCallback load_callback) { |
| if (!IsCompatible(name) || update_policy == UpdatePolicy::kForce) { |
| // A compatible component is not installed, or forced update is requested. |
| // Start registration and installation/update process. |
| Install(name, update_policy, mount_policy, std::move(load_callback)); |
| } else if (mount_policy == MountPolicy::kMount) { |
| // A compatible component is installed, load it. |
| LoadInternal(name, std::move(load_callback)); |
| } else { |
| // A compatible component is installed, do not load it. |
| static constexpr Error error = Error::NONE; |
| ReportError(error); |
| std::move(load_callback).Run(error, base::FilePath()); |
| } |
| } |
| |
| bool CrOSComponentInstaller::Unload(const std::string& name) { |
| DispatchFailedLoads(std::move(load_cache_[name].callbacks)); |
| load_cache_.erase(name); |
| |
| const ComponentConfig* config = FindConfig(name); |
| if (!config) { |
| // Component |name| does not exist. |
| return false; |
| } |
| const std::string id = GenerateId(config->sha2hash); |
| metadata_table_->DeleteComponentForCurrentUser(name); |
| return metadata_table_->HasComponentForAnyUser(name) || |
| component_updater_->UnregisterComponent(id); |
| } |
| |
| void CrOSComponentInstaller::GetVersion( |
| const std::string& name, |
| base::OnceCallback<void(const base::Version&)> version_callback) const { |
| if (!IsCompatible(name)) { |
| // `name` does not match to any component. |
| std::move(version_callback).Run(base::Version()); |
| return; |
| } |
| |
| auto component_iter = compatible_components_.find(name); |
| |
| // Path compatible to `name` must exist. |
| CHECK(component_iter != compatible_components_.end() && |
| !(component_iter->second.path.empty())); |
| if (component_iter->second.version.has_value()) { |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(std::move(version_callback), |
| component_iter->second.version.value())); |
| } else { |
| ash::ImageLoaderClient::Get()->RequestComponentVersion( |
| name, base::BindOnce(&CrOSComponentInstaller::FinishGetVersion, |
| weak_factory_.GetWeakPtr(), |
| std::move(version_callback))); |
| } |
| } |
| |
| void CrOSComponentInstaller::RegisterInstalled() { |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, {base::MayBlock()}, base::BindOnce(GetInstalled), |
| base::BindOnce(&CrOSComponentInstaller::RegisterN, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void CrOSComponentInstaller::RegisterCompatiblePath( |
| const std::string& name, |
| CompatibleComponentInfo info) { |
| compatible_components_[name] = std::move(info); |
| } |
| |
| void CrOSComponentInstaller::UnregisterCompatiblePath(const std::string& name) { |
| DispatchFailedLoads(std::move(load_cache_[name].callbacks)); |
| load_cache_.erase(name); |
| compatible_components_.erase(name); |
| } |
| |
| base::FilePath CrOSComponentInstaller::GetCompatiblePath( |
| const std::string& name) const { |
| const auto it = compatible_components_.find(name); |
| return it == compatible_components_.end() ? base::FilePath() |
| : it->second.path; |
| } |
| |
| void CrOSComponentInstaller::EmitInstalledSignal(const std::string& component) { |
| if (delegate_) { |
| delegate_->EmitInstalledSignal(component); |
| } |
| } |
| |
| CrOSComponentInstaller::LoadInfo::LoadInfo() = default; |
| CrOSComponentInstaller::LoadInfo::~LoadInfo() = default; |
| std::map<std::string, CrOSComponentInstaller::LoadInfo>& |
| CrOSComponentInstaller::GetLoadCacheForTesting() { |
| return load_cache_; |
| } |
| |
| void CrOSComponentInstaller::RemoveLoadCacheEntry( |
| const std::string& component_name) { |
| load_cache_.erase(component_name); |
| } |
| |
| bool CrOSComponentInstaller::IsRegisteredMayBlock(const std::string& name) { |
| base::FilePath root; |
| if (!base::PathService::Get(DIR_COMPONENT_USER, &root)) { |
| return false; |
| } |
| |
| return base::PathExists(root.Append(kComponentsRootPath).Append(name)); |
| } |
| |
| void CrOSComponentInstaller::Register(const ComponentConfig& config, |
| base::OnceClosure register_callback) { |
| std::unique_ptr<CrOSComponentInstallerPolicy> policy; |
| switch (config.policy_type) { |
| case ComponentConfig::PolicyType::kEnvVersion: |
| policy = std::make_unique<EnvVersionInstallerPolicy>(config, this); |
| break; |
| case ComponentConfig::PolicyType::kDemoApp: |
| policy = std::make_unique<DemoAppInstallerPolicy>(config, this); |
| break; |
| case ComponentConfig::PolicyType::kGrowthCampaigns: |
| policy = std::make_unique<GrowthCampaignsInstallerPolicy>(config, this); |
| break; |
| } |
| auto installer = base::MakeRefCounted<ComponentInstaller>(std::move(policy)); |
| installer->Register(component_updater_, std::move(register_callback)); |
| } |
| |
| void CrOSComponentInstaller::Install(const std::string& name, |
| UpdatePolicy update_policy, |
| MountPolicy mount_policy, |
| LoadCallback load_callback) { |
| const ComponentConfig* config = FindConfig(name); |
| if (!config) { |
| static constexpr Error error = Error::UNKNOWN_COMPONENT; |
| ReportError(error); |
| std::move(load_callback).Run(error, base::FilePath()); |
| return; |
| } |
| |
| Register( |
| *config, |
| base::BindOnce( |
| &CrOSComponentInstaller::StartInstall, weak_factory_.GetWeakPtr(), |
| name, GenerateId(config->sha2hash), update_policy, |
| base::BindOnce(&CrOSComponentInstaller::FinishInstall, |
| weak_factory_.GetWeakPtr(), name, mount_policy, |
| update_policy, std::move(load_callback)))); |
| } |
| |
| void CrOSComponentInstaller::StartInstall( |
| const std::string& name, |
| const std::string& id, |
| UpdatePolicy update_policy, |
| update_client::Callback install_callback) { |
| // Check whether an installed component was found during registration, and |
| // determine whether OnDemandUpdater should be started accordingly. |
| const bool is_compatible = IsCompatible(name); |
| if (update_policy == UpdatePolicy::kSkip || |
| (is_compatible && update_policy != UpdatePolicy::kForce)) { |
| std::move(install_callback).Run(update_client::Error::NONE); |
| return; |
| } |
| |
| const component_updater::OnDemandUpdater::Priority priority = |
| is_compatible ? component_updater::OnDemandUpdater::Priority::BACKGROUND |
| : component_updater::OnDemandUpdater::Priority::FOREGROUND; |
| component_updater_->GetOnDemandUpdater().OnDemandUpdate( |
| id, priority, std::move(install_callback)); |
| } |
| |
| void CrOSComponentInstaller::FinishInstall(const std::string& name, |
| MountPolicy mount_policy, |
| UpdatePolicy update_policy, |
| LoadCallback load_callback, |
| update_client::Error error) { |
| if (error != update_client::Error::NONE) { |
| Error err = Error::INSTALL_FAILURE; |
| if (error == update_client::Error::UPDATE_IN_PROGRESS) { |
| err = Error::UPDATE_IN_PROGRESS; |
| } |
| ReportError(err); |
| std::move(load_callback).Run(err, base::FilePath()); |
| } else if (!IsCompatible(name)) { |
| const Error err = update_policy == UpdatePolicy::kSkip |
| ? Error::NOT_FOUND |
| : Error::COMPATIBILITY_CHECK_FAILED; |
| ReportError(err); |
| std::move(load_callback).Run(err, base::FilePath()); |
| } else if (mount_policy == MountPolicy::kMount) { |
| LoadInternal(name, std::move(load_callback)); |
| } else { |
| static constexpr Error err = Error::NONE; |
| ReportError(err); |
| std::move(load_callback).Run(err, base::FilePath()); |
| } |
| } |
| |
| void CrOSComponentInstaller::LoadInternal(const std::string& name, |
| LoadCallback load_callback) { |
| // Use the cached value if it exists. |
| auto it = load_cache_.find(name); |
| if (it != load_cache_.end()) { |
| // If the request is ongoing, queue up a callback. |
| if (!it->second.success.has_value()) { |
| it->second.callbacks.push_back(std::move(load_callback)); |
| return; |
| } |
| // Otherwise immediately dispatch. |
| DispatchLoadCallback(std::move(load_callback), it->second.path, |
| it->second.success.value()); |
| return; |
| } |
| |
| // Update the cache to indicate the request is being queued. |
| load_cache_[name].success = std::nullopt; |
| |
| const base::FilePath path = GetCompatiblePath(name); |
| DCHECK(!path.empty()); |
| ash::ImageLoaderClient::Get()->LoadComponentAtPath( |
| name, path, |
| base::BindOnce(&CrOSComponentInstaller::FinishLoad, |
| weak_factory_.GetWeakPtr(), std::move(load_callback), |
| name)); |
| } |
| |
| void CrOSComponentInstaller::FinishLoad(LoadCallback load_callback, |
| const std::string& name, |
| std::optional<base::FilePath> result) { |
| // ImageLoader returns an empty path if mount failed. |
| bool success = result.has_value() && !result.value().empty(); |
| base::FilePath path; |
| if (success) { |
| path = result.value(); |
| } |
| |
| DispatchLoadCallback(std::move(load_callback), path, success); |
| |
| // Update the cache. |
| auto it = load_cache_.find(name); |
| if (it != load_cache_.end()) { |
| it->second.success = success; |
| it->second.path = path; |
| |
| // Dispatch queued up callbacks. |
| for (LoadCallback& queued_callback : it->second.callbacks) { |
| DispatchLoadCallback(std::move(queued_callback), path, success); |
| } |
| it->second.callbacks.clear(); |
| } |
| } |
| |
| void CrOSComponentInstaller::FinishGetVersion( |
| base::OnceCallback<void(const base::Version&)> version_callback, |
| std::optional<std::string> result) const { |
| std::move(version_callback).Run(base::Version(result.value_or(""))); |
| } |
| |
| void CrOSComponentInstaller::RegisterN( |
| const std::vector<ComponentConfig>& configs) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| for (const auto& config : configs) { |
| Register(config, base::OnceClosure()); |
| } |
| } |
| |
| bool CrOSComponentInstaller::IsCompatible(const std::string& name) const { |
| return compatible_components_.count(name) > 0; |
| } |
| |
| void CrOSComponentInstaller::DispatchLoadCallback(LoadCallback callback, |
| base::FilePath path, |
| bool success) { |
| Error error = success ? Error::NONE : Error::MOUNT_FAILURE; |
| ReportError(error); |
| std::move(callback).Run(error, std::move(path)); |
| } |
| |
| void CrOSComponentInstaller::DispatchFailedLoads( |
| std::vector<LoadCallback> callbacks) { |
| for (LoadCallback& callback : callbacks) { |
| DispatchLoadCallback(std::move(callback), base::FilePath(), |
| /*success=*/false); |
| } |
| } |
| |
| } // namespace component_updater |