| // Copyright 2020 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/policy/extension_force_install_mixin.h" |
| |
| #include <stdint.h> |
| |
| #include <atomic> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/important_file_writer.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/bind.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/notreached.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/values.h" |
| #include "base/version.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "components/crx_file/crx_verifier.h" |
| #include "components/crx_file/id_util.h" |
| #include "components/policy/core/common/cloud/cloud_policy_client.h" |
| #include "components/policy/core/common/mock_configuration_policy_provider.h" |
| #include "components/policy/core/common/policy_map.h" |
| #include "components/policy/core/common/policy_namespace.h" |
| #include "components/policy/core/common/policy_service.h" |
| #include "components/policy/core/common/policy_types.h" |
| #include "components/policy/policy_constants.h" |
| #include "components/prefs/pref_change_registrar.h" |
| #include "components/prefs/pref_service.h" |
| #include "extensions/browser/extension_creator.h" |
| #include "extensions/browser/extension_host_test_helper.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/pref_names.h" |
| #include "extensions/browser/test_extension_registry_observer.h" |
| #include "extensions/browser/updater/extension_downloader_test_helper.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_id.h" |
| #include "extensions/common/file_util.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/common/mojom/view_type.mojom.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "net/http/http_status_code.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_request.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/zlib/google/zip.h" |
| #include "url/gurl.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "chrome/browser/ash/login/test/device_state_mixin.h" |
| #include "chrome/browser/ash/login/test/scoped_policy_update.h" |
| #include "chrome/browser/ash/policy/core/device_policy_cros_browser_test.h" |
| #include "chrome/browser/ash/policy/test_support/embedded_policy_test_server_mixin.h" |
| #include "components/policy/proto/chrome_device_policy.pb.h" |
| #endif |
| |
| namespace { |
| |
| // Name of the directory whose contents are served by the embedded test |
| // server. |
| constexpr char kServedDirName[] = "served"; |
| // Hardcoded string value expected from the extension after extension is |
| // installed and it started executing. |
| constexpr char kReadyMessage[] = "ready"; |
| // Template for the file name of a served CRX file. |
| constexpr char kCrxFileNameTemplate[] = "%s-%s.crx"; |
| // Template for the file name of a served update manifest file. |
| constexpr char kUpdateManifestFileNameTemplate[] = "%s.xml"; |
| |
| // Implements waiting until the given extension appears in the |
| // force-installation pref. |
| class ForceInstallPrefObserver final { |
| public: |
| ForceInstallPrefObserver(Profile* profile, |
| const extensions::ExtensionId& extension_id); |
| ForceInstallPrefObserver(const ForceInstallPrefObserver&) = delete; |
| ForceInstallPrefObserver& operator=(const ForceInstallPrefObserver&) = delete; |
| ~ForceInstallPrefObserver(); |
| |
| void Wait(); |
| |
| private: |
| void OnPrefChanged(); |
| bool IsForceInstallPrefSet() const; |
| |
| const raw_ptr<PrefService> pref_service_; |
| const std::string pref_name_; |
| const extensions::ExtensionId extension_id_; |
| PrefChangeRegistrar pref_change_registrar_; |
| base::RunLoop run_loop_; |
| base::WeakPtrFactory<ForceInstallPrefObserver> weak_ptr_factory_{this}; |
| }; |
| |
| ForceInstallPrefObserver::ForceInstallPrefObserver( |
| Profile* profile, |
| const extensions::ExtensionId& extension_id) |
| : pref_service_(profile->GetPrefs()), |
| pref_name_(extensions::pref_names::kInstallForceList), |
| extension_id_(extension_id) { |
| pref_change_registrar_.Init(pref_service_); |
| pref_change_registrar_.Add( |
| pref_name_, base::BindRepeating(&ForceInstallPrefObserver::OnPrefChanged, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| ForceInstallPrefObserver::~ForceInstallPrefObserver() = default; |
| |
| void ForceInstallPrefObserver::Wait() { |
| if (IsForceInstallPrefSet()) |
| return; |
| run_loop_.Run(); |
| } |
| |
| void ForceInstallPrefObserver::OnPrefChanged() { |
| if (IsForceInstallPrefSet()) |
| run_loop_.Quit(); |
| } |
| |
| bool ForceInstallPrefObserver::IsForceInstallPrefSet() const { |
| const PrefService::Preference* const pref = |
| pref_service_->FindPreference(pref_name_); |
| if (!pref || !pref->IsManaged()) { |
| // Note that we intentionally ignore the pref if it's set but isn't managed, |
| // mimicking the real behavior of the Extensions system that only respects |
| // trusted (policy-set) values. Normally there's no "untrusted" value in |
| // these prefs, but in theory this is an attack that the Extensions system |
| // protects against, and there might be tests that simulate this scenario. |
| return false; |
| } |
| DCHECK_EQ(pref->GetType(), base::Value::Type::DICT); |
| return pref->GetValue()->GetDict().contains(extension_id_); |
| } |
| |
| // Implements waiting for the mixin's specified event. |
| class ForceInstallWaiter final { |
| public: |
| ForceInstallWaiter(ExtensionForceInstallMixin::WaitMode wait_mode, |
| const extensions::ExtensionId& extension_id, |
| Profile* profile); |
| ForceInstallWaiter(const ForceInstallWaiter&) = delete; |
| ForceInstallWaiter& operator=(const ForceInstallWaiter&) = delete; |
| ~ForceInstallWaiter(); |
| |
| // Waits until the event specified |wait_mode| gets satisfied. Returns false |
| // if the waiting timed out. |
| bool Wait(); |
| |
| private: |
| // Implementation of Wait(). Returns the result via |success| in order to be |
| // able to use ASSERT* macros inside. |
| void WaitImpl(bool* success); |
| |
| const ExtensionForceInstallMixin::WaitMode wait_mode_; |
| const extensions::ExtensionId extension_id_; |
| const raw_ptr<Profile> profile_; |
| std::unique_ptr<ForceInstallPrefObserver> force_install_pref_observer_; |
| std::unique_ptr<extensions::TestExtensionRegistryObserver> registry_observer_; |
| std::unique_ptr<extensions::ExtensionHostTestHelper> |
| background_page_first_load_observer_; |
| std::unique_ptr<ExtensionTestMessageListener> extension_message_listener_; |
| }; |
| |
| ForceInstallWaiter::ForceInstallWaiter( |
| ExtensionForceInstallMixin::WaitMode wait_mode, |
| const extensions::ExtensionId& extension_id, |
| Profile* profile) |
| : wait_mode_(wait_mode), extension_id_(extension_id), profile_(profile) { |
| DCHECK(crx_file::id_util::IdIsValid(extension_id_)); |
| if (!profile_ && wait_mode_ != ExtensionForceInstallMixin::WaitMode::kNone) { |
| ADD_FAILURE() << "No profile passed to the Init method"; |
| return; |
| } |
| switch (wait_mode_) { |
| case ExtensionForceInstallMixin::WaitMode::kNone: |
| break; |
| case ExtensionForceInstallMixin::WaitMode::kPrefSet: |
| force_install_pref_observer_ = |
| std::make_unique<ForceInstallPrefObserver>(profile_, extension_id_); |
| break; |
| case ExtensionForceInstallMixin::WaitMode::kLoad: |
| registry_observer_ = |
| std::make_unique<extensions::TestExtensionRegistryObserver>( |
| extensions::ExtensionRegistry::Get(profile_), extension_id_); |
| break; |
| case ExtensionForceInstallMixin::WaitMode::kBackgroundPageFirstLoad: |
| background_page_first_load_observer_ = |
| std::make_unique<extensions::ExtensionHostTestHelper>(profile_, |
| extension_id_); |
| background_page_first_load_observer_->RestrictToType( |
| extensions::mojom::ViewType::kExtensionBackgroundPage); |
| break; |
| case ExtensionForceInstallMixin::WaitMode::kReadyMessageReceived: |
| extension_message_listener_ = |
| std::make_unique<ExtensionTestMessageListener>(kReadyMessage); |
| extension_message_listener_->set_extension_id(extension_id_); |
| break; |
| } |
| } |
| |
| ForceInstallWaiter::~ForceInstallWaiter() = default; |
| |
| bool ForceInstallWaiter::Wait() { |
| bool success = false; |
| WaitImpl(&success); |
| return success; |
| } |
| |
| void ForceInstallWaiter::WaitImpl(bool* success) { |
| switch (wait_mode_) { |
| case ExtensionForceInstallMixin::WaitMode::kNone: |
| // No waiting needed. |
| *success = true; |
| break; |
| case ExtensionForceInstallMixin::WaitMode::kPrefSet: |
| // Wait and assert that the waiting run loop didn't time out. |
| ASSERT_NO_FATAL_FAILURE(force_install_pref_observer_->Wait()); |
| *success = true; |
| break; |
| case ExtensionForceInstallMixin::WaitMode::kLoad: |
| *success = registry_observer_->WaitForExtensionLoaded() != nullptr; |
| break; |
| case ExtensionForceInstallMixin::WaitMode::kBackgroundPageFirstLoad: |
| // Wait and assert that the waiting run loop didn't time out. |
| ASSERT_NO_FATAL_FAILURE(background_page_first_load_observer_ |
| ->WaitForHostCompletedFirstLoad()); |
| *success = true; |
| break; |
| case ExtensionForceInstallMixin::WaitMode::kReadyMessageReceived: |
| ASSERT_NO_FATAL_FAILURE(*success = extension_message_listener_ |
| ->WaitUntilSatisfied()); |
| break; |
| } |
| } |
| |
| std::string GetServedUpdateManifestFileName( |
| const extensions::ExtensionId& extension_id) { |
| return base::StringPrintf(kUpdateManifestFileNameTemplate, |
| extension_id.c_str()); |
| } |
| |
| std::string GetServedCrxFileName(const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version) { |
| return base::StringPrintf(kCrxFileNameTemplate, extension_id.c_str(), |
| extension_version.GetString().c_str()); |
| } |
| |
| std::string GenerateUpdateManifest(const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version, |
| const GURL& crx_url) { |
| return extensions::CreateUpdateManifest( |
| {extensions::UpdateManifestItem(extension_id) |
| .codebase(crx_url.spec()) |
| .version(extension_version.GetString())}); |
| } |
| |
| bool ParseExtensionManifestData(const base::FilePath& extension_dir_path, |
| base::Version* extension_version) { |
| std::string error_message; |
| std::optional<base::Value::Dict> extension_manifest; |
| { |
| base::ScopedAllowBlockingForTesting scoped_allow_blocking; |
| extension_manifest = |
| extensions::file_util::LoadManifest(extension_dir_path, &error_message); |
| } |
| if (!extension_manifest) { |
| ADD_FAILURE() << "Failed to load extension manifest from " |
| << extension_dir_path.value() << ": " << error_message; |
| return false; |
| } |
| const std::string* version_string = |
| extension_manifest->FindString(extensions::manifest_keys::kVersion); |
| if (!version_string) { |
| ADD_FAILURE() << "Failed to load extension version from " |
| << extension_dir_path.value() |
| << ": manifest key missing or has wrong type"; |
| return false; |
| } |
| *extension_version = base::Version(*version_string); |
| if (!extension_version->IsValid()) { |
| ADD_FAILURE() << "Failed to load extension version from " |
| << extension_dir_path.value() << ": bad format"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool ParseCrxOuterData(const base::FilePath& crx_path, |
| extensions::ExtensionId* extension_id) { |
| base::ScopedAllowBlockingForTesting scoped_allow_blocking; |
| std::string public_key; |
| const crx_file::VerifierResult crx_verifier_result = crx_file::Verify( |
| crx_path, crx_file::VerifierFormat::CRX3, |
| /*required_key_hashes=*/std::vector<std::vector<uint8_t>>(), |
| /*required_file_hash=*/std::vector<uint8_t>(), &public_key, extension_id, |
| /*compressed_verified_contents=*/nullptr); |
| if (crx_verifier_result != crx_file::VerifierResult::OK_FULL) { |
| ADD_FAILURE() << "Failed to read created CRX: verifier result " |
| << static_cast<int>(crx_verifier_result); |
| return false; |
| } |
| return true; |
| } |
| |
| bool ParseCrxInnerData(const base::FilePath& crx_path, |
| base::Version* extension_version) { |
| base::ScopedAllowBlockingForTesting scoped_allow_blocking; |
| base::ScopedTempDir temp_dir; |
| if (!temp_dir.CreateUniqueTempDir()) { |
| ADD_FAILURE() << "Failed to create temp directory"; |
| return false; |
| } |
| if (!zip::Unzip(crx_path, temp_dir.GetPath())) { |
| ADD_FAILURE() << "Failed to unpack CRX from " << crx_path.value(); |
| return false; |
| } |
| return ParseExtensionManifestData(temp_dir.GetPath(), extension_version); |
| } |
| |
| std::string MakeForceInstallPolicyItemValue( |
| const extensions::ExtensionId& extension_id, |
| const GURL& update_manifest_url) { |
| if (update_manifest_url.is_empty()) |
| return extension_id; |
| return base::StringPrintf("%s;%s", extension_id.c_str(), |
| update_manifest_url.spec().c_str()); |
| } |
| |
| void UpdatePolicyViaMockPolicyProvider( |
| const extensions::ExtensionId& extension_id, |
| const GURL& update_manifest_url, |
| policy::MockConfigurationPolicyProvider* mock_policy_provider) { |
| const std::string policy_item_value = |
| MakeForceInstallPolicyItemValue(extension_id, update_manifest_url); |
| policy::PolicyMap policy_map = |
| mock_policy_provider->policies() |
| .Get(policy::PolicyNamespace(policy::POLICY_DOMAIN_CHROME, |
| /*component_id=*/std::string())) |
| .Clone(); |
| policy::PolicyMap::Entry* const existing_entry = |
| policy_map.GetMutable(policy::key::kExtensionInstallForcelist); |
| if (existing_entry && existing_entry->value(base::Value::Type::LIST)) { |
| // Append to the existing policy. |
| existing_entry->value(base::Value::Type::LIST) |
| ->GetList() |
| .Append(policy_item_value); |
| } else { |
| // Set the new policy value. |
| base::Value::List policy_value; |
| policy_value.Append(policy_item_value); |
| policy_map.Set(policy::key::kExtensionInstallForcelist, |
| policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER, |
| policy::POLICY_SOURCE_CLOUD, |
| base::Value(std::move(policy_value)), |
| /*external_data_fetcher=*/nullptr); |
| } |
| mock_policy_provider->UpdateChromePolicy(policy_map); |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| |
| void UpdatePolicyViaDeviceStateMixin( |
| const extensions::ExtensionId& extension_id, |
| const GURL& update_manifest_url, |
| ash::DeviceStateMixin* device_state_mixin) { |
| device_state_mixin->RequestDevicePolicyUpdate() |
| ->policy_payload() |
| ->mutable_device_login_screen_extensions() |
| ->add_device_login_screen_extensions( |
| MakeForceInstallPolicyItemValue(extension_id, update_manifest_url)); |
| } |
| |
| void UpdatePolicyViaDevicePolicyCrosTestHelper( |
| const extensions::ExtensionId& extension_id, |
| const GURL& update_manifest_url, |
| policy::DevicePolicyCrosTestHelper* device_policy_cros_test_helper) { |
| device_policy_cros_test_helper->device_policy() |
| ->payload() |
| .mutable_device_login_screen_extensions() |
| ->add_device_login_screen_extensions( |
| MakeForceInstallPolicyItemValue(extension_id, update_manifest_url)); |
| device_policy_cros_test_helper->RefreshDevicePolicy(); |
| } |
| |
| void UpdatePolicyViaEmbeddedPolicyMixin( |
| const extensions::ExtensionId& extension_id, |
| const GURL& update_manifest_url, |
| ash::EmbeddedPolicyTestServerMixin* policy_test_server_mixin, |
| policy::UserPolicyBuilder* user_policy_builder, |
| const std::string& account_id, |
| const std::string& policy_type, |
| bool* success) { |
| user_policy_builder->payload() |
| .mutable_extensioninstallforcelist() |
| ->mutable_value() |
| ->add_entries( |
| MakeForceInstallPolicyItemValue(extension_id, update_manifest_url)); |
| user_policy_builder->Build(); |
| |
| policy_test_server_mixin->UpdatePolicy( |
| policy_type, account_id, |
| user_policy_builder->payload().SerializeAsString()); |
| |
| base::RunLoop run_loop; |
| g_browser_process->policy_service()->RefreshPolicies( |
| run_loop.QuitClosure(), policy::PolicyFetchReason::kTest); |
| ASSERT_NO_FATAL_FAILURE(run_loop.Run()); |
| |
| // Report the outcome via an output argument instead of the return value, |
| // since ASSERT_NO_FATAL_FAILURE() only works in void functions. |
| *success = true; |
| } |
| |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| // Simulates a server error according to the current error mode, or returns no |
| // response when no error is configured. Note that this function is called on |
| // the IO thread. |
| std::unique_ptr<net::test_server::HttpResponse> ErrorSimulatingRequestHandler( |
| std::atomic<ExtensionForceInstallMixin::ServerErrorMode>* server_error_mode, |
| const net::test_server::HttpRequest& /*request*/) { |
| switch (server_error_mode->load()) { |
| case ExtensionForceInstallMixin::ServerErrorMode::kNone: { |
| return nullptr; |
| } |
| case ExtensionForceInstallMixin::ServerErrorMode::kHung: { |
| return std::make_unique<net::test_server::HungResponse>(); |
| } |
| case ExtensionForceInstallMixin::ServerErrorMode::kInternalError: { |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_INTERNAL_SERVER_ERROR); |
| return response; |
| } |
| } |
| } |
| |
| } // namespace |
| |
| ExtensionForceInstallMixin::ExtensionForceInstallMixin( |
| InProcessBrowserTestMixinHost* host) |
| : InProcessBrowserTestMixin(host) {} |
| |
| ExtensionForceInstallMixin::~ExtensionForceInstallMixin() = default; |
| |
| void ExtensionForceInstallMixin::InitWithMockPolicyProvider( |
| Profile* profile, |
| policy::MockConfigurationPolicyProvider* mock_policy_provider) { |
| DCHECK(mock_policy_provider); |
| DCHECK(!initialized_) << "Init already called"; |
| DCHECK(!profile_); |
| DCHECK(!mock_policy_provider_); |
| initialized_ = true; |
| profile_ = profile; |
| mock_policy_provider_ = mock_policy_provider; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| |
| void ExtensionForceInstallMixin::InitWithDeviceStateMixin( |
| Profile* profile, |
| ash::DeviceStateMixin* device_state_mixin) { |
| DCHECK(device_state_mixin); |
| DCHECK(!initialized_) << "Init already called"; |
| DCHECK(!profile_); |
| DCHECK(!device_state_mixin_); |
| initialized_ = true; |
| profile_ = profile; |
| device_state_mixin_ = device_state_mixin; |
| } |
| |
| void ExtensionForceInstallMixin::InitWithDevicePolicyCrosTestHelper( |
| Profile* profile, |
| policy::DevicePolicyCrosTestHelper* device_policy_cros_test_helper) { |
| DCHECK(device_policy_cros_test_helper); |
| DCHECK(!initialized_) << "Init already called"; |
| DCHECK(!profile_); |
| DCHECK(!device_policy_cros_test_helper_); |
| initialized_ = true; |
| profile_ = profile; |
| device_policy_cros_test_helper_ = device_policy_cros_test_helper; |
| } |
| |
| void ExtensionForceInstallMixin::InitWithEmbeddedPolicyMixin( |
| Profile* profile, |
| ash::EmbeddedPolicyTestServerMixin* policy_test_server_mixin, |
| policy::UserPolicyBuilder* user_policy_builder, |
| const std::string& account_id, |
| const std::string& policy_type) { |
| DCHECK(policy_test_server_mixin); |
| DCHECK(user_policy_builder); |
| DCHECK(!account_id.empty()); |
| DCHECK(!policy_type.empty()); |
| DCHECK(!initialized_) << "Init already called"; |
| DCHECK(!profile_); |
| DCHECK(!policy_test_server_mixin_); |
| DCHECK(!user_policy_builder_); |
| DCHECK(account_id_.empty()); |
| DCHECK(policy_type_.empty()); |
| initialized_ = true; |
| profile_ = profile; |
| policy_test_server_mixin_ = policy_test_server_mixin; |
| user_policy_builder_ = user_policy_builder; |
| account_id_ = account_id; |
| policy_type_ = policy_type; |
| } |
| |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| bool ExtensionForceInstallMixin::ForceInstallFromCrx( |
| const base::FilePath& crx_path, |
| WaitMode wait_mode, |
| extensions::ExtensionId* extension_id, |
| base::Version* extension_version) { |
| DCHECK(initialized_) << "Init not called"; |
| DCHECK(embedded_test_server_.Started()) << "Called before setup"; |
| |
| extensions::ExtensionId local_extension_id; |
| if (!ParseCrxOuterData(crx_path, &local_extension_id)) |
| return false; |
| if (extension_id) |
| *extension_id = local_extension_id; |
| base::Version local_extension_version; |
| if (!ParseCrxInnerData(crx_path, &local_extension_version)) |
| return false; |
| if (extension_version) |
| *extension_version = local_extension_version; |
| return ServeExistingCrx(crx_path, local_extension_id, |
| local_extension_version) && |
| ForceInstallFromServedCrx(local_extension_id, local_extension_version, |
| wait_mode); |
| } |
| |
| bool ExtensionForceInstallMixin::ForceInstallFromSourceDir( |
| const base::FilePath& extension_dir_path, |
| const std::optional<base::FilePath>& pem_path, |
| WaitMode wait_mode, |
| extensions::ExtensionId* extension_id, |
| base::Version* extension_version) { |
| DCHECK(initialized_) << "Init not called"; |
| DCHECK(embedded_test_server_.Started()) << "Called before setup"; |
| |
| base::Version local_extension_version; |
| if (!ParseExtensionManifestData(extension_dir_path, &local_extension_version)) |
| return false; |
| if (extension_version) |
| *extension_version = local_extension_version; |
| extensions::ExtensionId local_extension_id; |
| if (!CreateAndServeCrx(extension_dir_path, pem_path, local_extension_version, |
| &local_extension_id)) { |
| return false; |
| } |
| if (extension_id) |
| *extension_id = local_extension_id; |
| return ForceInstallFromServedCrx(local_extension_id, local_extension_version, |
| wait_mode); |
| } |
| |
| bool ExtensionForceInstallMixin::UpdateFromCrx( |
| const base::FilePath& crx_path, |
| UpdateWaitMode wait_mode, |
| base::Version* extension_version) { |
| DCHECK(initialized_) << "Init not called"; |
| DCHECK(embedded_test_server_.Started()) << "Called before setup"; |
| |
| extensions::ExtensionId extension_id; |
| if (!ParseCrxOuterData(crx_path, &extension_id)) |
| return false; |
| base::Version local_extension_version; |
| if (!ParseCrxInnerData(crx_path, &local_extension_version)) |
| return false; |
| if (extension_version) |
| *extension_version = local_extension_version; |
| return ServeExistingCrx(crx_path, extension_id, local_extension_version) && |
| CreateAndServeUpdateManifestFile(extension_id, |
| local_extension_version) && |
| WaitForExtensionUpdate(extension_id, local_extension_version, |
| wait_mode); |
| } |
| |
| bool ExtensionForceInstallMixin::UpdateFromSourceDir( |
| const base::FilePath& extension_dir_path, |
| const extensions::ExtensionId& extension_id, |
| UpdateWaitMode wait_mode, |
| base::Version* extension_version) { |
| DCHECK(initialized_) << "Init not called"; |
| DCHECK(embedded_test_server_.Started()) << "Called before setup"; |
| |
| // Get the PEM path that was used for packing the extension last time, so that |
| // packing results in the same extension ID. |
| auto pem_path_iter = extension_id_to_pem_path_.find(extension_id); |
| if (pem_path_iter == extension_id_to_pem_path_.end()) { |
| ADD_FAILURE() << "Requested update of extension that wasn't installed via " |
| "ForceInstallFromSourceDir()"; |
| return false; |
| } |
| const base::FilePath pem_path = pem_path_iter->second; |
| |
| base::Version local_extension_version; |
| if (!ParseExtensionManifestData(extension_dir_path, &local_extension_version)) |
| return false; |
| if (extension_version) |
| *extension_version = local_extension_version; |
| extensions::ExtensionId packed_extension_id; |
| if (!CreateAndServeCrx(extension_dir_path, pem_path, local_extension_version, |
| &packed_extension_id)) { |
| return false; |
| } |
| if (packed_extension_id != extension_id) { |
| ADD_FAILURE() << "Unexpected extension ID after packing: " |
| << packed_extension_id << ", expected: " << extension_id; |
| return false; |
| } |
| return CreateAndServeUpdateManifestFile(extension_id, |
| local_extension_version) && |
| WaitForExtensionUpdate(extension_id, local_extension_version, |
| wait_mode); |
| } |
| |
| const extensions::Extension* ExtensionForceInstallMixin::GetInstalledExtension( |
| const extensions::ExtensionId& extension_id) const { |
| DCHECK(initialized_) << "Init not called"; |
| if (!profile_) { |
| ADD_FAILURE() << "No profile passed to the Init method"; |
| return nullptr; |
| } |
| |
| const auto* const registry = extensions::ExtensionRegistry::Get(profile_); |
| DCHECK(registry); |
| return registry->GetInstalledExtension(extension_id); |
| } |
| |
| const extensions::Extension* ExtensionForceInstallMixin::GetEnabledExtension( |
| const extensions::ExtensionId& extension_id) const { |
| DCHECK(initialized_) << "Init not called"; |
| if (!profile_) { |
| ADD_FAILURE() << "No profile passed to the Init method"; |
| return nullptr; |
| } |
| |
| const auto* const registry = extensions::ExtensionRegistry::Get(profile_); |
| DCHECK(registry); |
| return registry->enabled_extensions().GetByID(extension_id); |
| } |
| |
| void ExtensionForceInstallMixin::SetServerErrorMode( |
| ServerErrorMode server_error_mode) { |
| server_error_mode_.store(server_error_mode); |
| } |
| |
| void ExtensionForceInstallMixin::SetUpOnMainThread() { |
| // Create a temporary directory for keeping served and auxiliary files. |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| const base::FilePath served_dir_path = |
| temp_dir_.GetPath().AppendASCII(kServedDirName); |
| ASSERT_TRUE(base::CreateDirectory(served_dir_path)); |
| |
| // Start the embedded test server. The first request handler is a handler for |
| // simulating errors as configured (note that the error mode is shared via an |
| // atomic variable, so that its changes on the main thread are correctly |
| // picked up by the handler on the IO thread). The default handler is serving |
| // files from the directory created above. |
| embedded_test_server_.RegisterRequestHandler(base::BindRepeating( |
| &ErrorSimulatingRequestHandler, base::Unretained(&server_error_mode_))); |
| embedded_test_server_.ServeFilesFromDirectory(served_dir_path); |
| ASSERT_TRUE(embedded_test_server_.Start()); |
| } |
| |
| base::FilePath ExtensionForceInstallMixin::GetPathInServedDir( |
| const std::string& file_name) const { |
| return temp_dir_.GetPath().AppendASCII(kServedDirName).AppendASCII(file_name); |
| } |
| |
| GURL ExtensionForceInstallMixin::GetServedUpdateManifestUrl( |
| const extensions::ExtensionId& extension_id) const { |
| DCHECK(embedded_test_server_.Started()) << "Called before setup"; |
| |
| return embedded_test_server_.GetURL( |
| "/" + GetServedUpdateManifestFileName(extension_id)); |
| } |
| |
| GURL ExtensionForceInstallMixin::GetServedCrxUrl( |
| const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version) const { |
| DCHECK(embedded_test_server_.Started()) << "Called before setup"; |
| |
| return embedded_test_server_.GetURL( |
| "/" + GetServedCrxFileName(extension_id, extension_version)); |
| } |
| |
| bool ExtensionForceInstallMixin::ServeExistingCrx( |
| const base::FilePath& source_crx_path, |
| const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version) { |
| DCHECK(embedded_test_server_.Started()) << "Called before setup"; |
| |
| base::ScopedAllowBlockingForTesting scoped_allow_blocking; |
| |
| // First copy the CRX into a temporary location. |
| base::FilePath temp_crx_path; |
| if (!base::CreateTemporaryFileInDir(temp_dir_.GetPath(), &temp_crx_path)) { |
| ADD_FAILURE() << "Failed to create a temporary file."; |
| return false; |
| } |
| if (!base::CopyFile(source_crx_path, temp_crx_path)) { |
| ADD_FAILURE() << "Failed to copy CRX from " << source_crx_path.value() |
| << " to " << temp_crx_path.value(); |
| return false; |
| } |
| |
| // Then atomically move the created file into the served directory. This is |
| // important as the embedded test server is reading files on a different |
| // thread (IO) and, for example, we can be asked to re-serve the same version |
| // again. |
| const base::FilePath served_crx_path = |
| GetPathInServedDir(GetServedCrxFileName(extension_id, extension_version)); |
| if (!base::Move(temp_crx_path, served_crx_path)) { |
| ADD_FAILURE() << "Failed to move CRX from " << temp_crx_path.value() |
| << " to " << served_crx_path.value(); |
| return false; |
| } |
| return true; |
| } |
| |
| bool ExtensionForceInstallMixin::CreateAndServeCrx( |
| const base::FilePath& extension_dir_path, |
| const std::optional<base::FilePath>& pem_path, |
| const base::Version& extension_version, |
| extensions::ExtensionId* extension_id) { |
| base::ScopedAllowBlockingForTesting scoped_allow_blocking; |
| |
| // Use a temporary CRX file name, since the ID is yet unknown if |pem_path| is |
| // empty. Delete the file first in case the previous operation failed in the |
| // middle. |
| const std::string kTempCrxFileName = "temp.crx"; |
| const base::FilePath temp_crx_path = |
| temp_dir_.GetPath().AppendASCII(kTempCrxFileName); |
| base::DeleteFile(temp_crx_path); |
| |
| // Use the specified PEM file, if any. Otherwise, create a file in the temp |
| // dir and let the extension creator populate it with a random key. |
| base::FilePath final_pem_path; |
| if (pem_path) { |
| final_pem_path = *pem_path; |
| } else if (!base::CreateTemporaryFileInDir(temp_dir_.GetPath(), |
| &final_pem_path)) { |
| ADD_FAILURE() << "Failed to create a temp PEM file"; |
| return false; |
| } |
| |
| extensions::ExtensionCreator extension_creator; |
| if (!extension_creator.Run(extension_dir_path, temp_crx_path, |
| pem_path.value_or(base::FilePath()), |
| /*private_key_output_path=*/ |
| pem_path ? base::FilePath() : final_pem_path, |
| /*run_flags=*/0)) { |
| ADD_FAILURE() << "Failed to pack extension: " |
| << extension_creator.error_message(); |
| return false; |
| } |
| |
| if (!ParseCrxOuterData(temp_crx_path, extension_id)) |
| return false; |
| const base::FilePath served_crx_path = GetPathInServedDir( |
| GetServedCrxFileName(*extension_id, extension_version)); |
| if (!base::Move(temp_crx_path, served_crx_path)) { |
| ADD_FAILURE() << "Failed to move the created CRX file to " |
| << served_crx_path.value(); |
| return false; |
| } |
| |
| extension_id_to_pem_path_[*extension_id] = final_pem_path; |
| return true; |
| } |
| |
| bool ExtensionForceInstallMixin::ForceInstallFromServedCrx( |
| const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version, |
| WaitMode wait_mode) { |
| DCHECK(initialized_) << "Init not called"; |
| DCHECK(embedded_test_server_.Started()) << "Called before setup"; |
| |
| if (!CreateAndServeUpdateManifestFile(extension_id, extension_version)) |
| return false; |
| // Prepare the waiter's observers before setting the policy, so that we don't |
| // miss synchronous operations triggered by the policy update. |
| ForceInstallWaiter waiter(wait_mode, extension_id, profile_); |
| if (!UpdatePolicy(extension_id, GetServedUpdateManifestUrl(extension_id))) |
| return false; |
| return waiter.Wait(); |
| } |
| |
| bool ExtensionForceInstallMixin::CreateAndServeUpdateManifestFile( |
| const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version) { |
| DCHECK(embedded_test_server_.Started()) << "Called before setup"; |
| |
| const GURL crx_url = GetServedCrxUrl(extension_id, extension_version); |
| const std::string update_manifest = |
| GenerateUpdateManifest(extension_id, extension_version, crx_url); |
| const base::FilePath update_manifest_path = |
| GetPathInServedDir(GetServedUpdateManifestFileName(extension_id)); |
| // Note: Doing an atomic write, since the embedded test server might |
| // concurrently try to access this file from another thread. |
| base::ScopedAllowBlockingForTesting scoped_allow_blocking; |
| if (!base::ImportantFileWriter::WriteFileAtomically(update_manifest_path, |
| update_manifest)) { |
| ADD_FAILURE() << "Failed to write update manifest file"; |
| return false; |
| } |
| return true; |
| } |
| |
| bool ExtensionForceInstallMixin::UpdatePolicy( |
| const extensions::ExtensionId& extension_id, |
| const GURL& update_manifest_url) { |
| DCHECK(initialized_) << "Init not called"; |
| |
| if (mock_policy_provider_) { |
| UpdatePolicyViaMockPolicyProvider(extension_id, update_manifest_url, |
| mock_policy_provider_); |
| return true; |
| } |
| #if BUILDFLAG(IS_CHROMEOS) |
| if (device_state_mixin_) { |
| UpdatePolicyViaDeviceStateMixin(extension_id, update_manifest_url, |
| device_state_mixin_); |
| return true; |
| } |
| if (device_policy_cros_test_helper_) { |
| UpdatePolicyViaDevicePolicyCrosTestHelper(extension_id, update_manifest_url, |
| device_policy_cros_test_helper_); |
| return true; |
| } |
| if (policy_test_server_mixin_) { |
| bool success = false; |
| UpdatePolicyViaEmbeddedPolicyMixin( |
| extension_id, update_manifest_url, policy_test_server_mixin_, |
| user_policy_builder_, account_id_, policy_type_, &success); |
| return success; |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| NOTREACHED() << "Init not called"; |
| } |
| |
| bool ExtensionForceInstallMixin::WaitForExtensionUpdate( |
| const extensions::ExtensionId& extension_id, |
| const base::Version& extension_version, |
| UpdateWaitMode wait_mode) { |
| switch (wait_mode) { |
| case UpdateWaitMode::kNone: |
| return true; |
| } |
| } |