| // Copyright (c) 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/component_updater/sw_reporter_installer_win.h" |
| |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/base64.h" |
| #include "base/base_paths.h" |
| #include "base/bind.h" |
| #include "base/command_line.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/logging.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/path_service.h" |
| #include "base/rand_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_tokenizer.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/post_task.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/time.h" |
| #include "base/win/registry.h" |
| #include "base/win/windows_version.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/safe_browsing/chrome_cleaner/chrome_cleaner_controller_win.h" |
| #include "chrome/browser/safe_browsing/chrome_cleaner/reporter_runner_win.h" |
| #include "chrome/browser/safe_browsing/chrome_cleaner/srt_field_trial_win.h" |
| #include "components/chrome_cleaner/public/constants/constants.h" |
| #include "components/component_updater/component_updater_paths.h" |
| #include "components/component_updater/component_updater_service.h" |
| #include "components/component_updater/pref_names.h" |
| #include "components/pref_registry/pref_registry_syncable.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/update_client/update_client.h" |
| #include "components/update_client/utils.h" |
| #include "components/variations/variations_associated_data.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| |
| namespace component_updater { |
| |
| namespace { |
| |
| using safe_browsing::SwReporterInvocation; |
| using safe_browsing::SwReporterInvocationSequence; |
| |
| // These values are used to send UMA information and are replicated in the |
| // histograms.xml file, so the order MUST NOT CHANGE. |
| enum SRTCompleted { |
| SRT_COMPLETED_NOT_YET = 0, |
| SRT_COMPLETED_YES = 1, |
| SRT_COMPLETED_LATER = 2, |
| SRT_COMPLETED_MAX, |
| }; |
| |
| // CRX hash. The extension id is: gkmgaooipdjhmangpemjhigmamcehddo. The hash was |
| // generated in Python with something like this: |
| // hashlib.sha256().update(open("<file>.crx").read()[16:16+294]).digest(). |
| const uint8_t kSwReporterSha2Hash[] = { |
| 0x6a, 0xc6, 0x0e, 0xe8, 0xf3, 0x97, 0xc0, 0xd6, 0xf4, 0xc9, 0x78, |
| 0x6c, 0x0c, 0x24, 0x73, 0x3e, 0x05, 0xa5, 0x62, 0x4b, 0x2e, 0xc7, |
| 0xb7, 0x1c, 0x5f, 0xea, 0xf0, 0x88, 0xf6, 0x97, 0x9b, 0xc7}; |
| |
| const base::FilePath::CharType kSwReporterExeName[] = |
| FILE_PATH_LITERAL("software_reporter_tool.exe"); |
| |
| // SwReporter is normally only registered in official builds. However, to |
| // enable testing in chromium build bots, test code can set this to true. |
| #if defined(GOOGLE_CHROME_BUILD) |
| bool is_sw_reporter_enabled = true; |
| #else |
| bool is_sw_reporter_enabled = false; |
| #endif |
| |
| // Callback function to be called once the registration of the component |
| // is complete. This is used only in tests. |
| base::OnceClosure* registration_cb_for_testing = new base::OnceClosure(); |
| |
| void SRTHasCompleted(SRTCompleted value) { |
| UMA_HISTOGRAM_ENUMERATION("SoftwareReporter.Cleaner.HasCompleted", value, |
| SRT_COMPLETED_MAX); |
| } |
| |
| void ReportUploadsWithUma(const base::string16& upload_results) { |
| base::WStringTokenizer tokenizer(upload_results, L";"); |
| int failure_count = 0; |
| int success_count = 0; |
| int longest_failure_run = 0; |
| int current_failure_run = 0; |
| bool last_result = false; |
| while (tokenizer.GetNext()) { |
| if (tokenizer.token_piece() == L"0") { |
| ++failure_count; |
| ++current_failure_run; |
| last_result = false; |
| } else { |
| ++success_count; |
| current_failure_run = 0; |
| last_result = true; |
| } |
| |
| if (current_failure_run > longest_failure_run) |
| longest_failure_run = current_failure_run; |
| } |
| |
| UMA_HISTOGRAM_COUNTS_100("SoftwareReporter.UploadFailureCount", |
| failure_count); |
| UMA_HISTOGRAM_COUNTS_100("SoftwareReporter.UploadSuccessCount", |
| success_count); |
| UMA_HISTOGRAM_COUNTS_100("SoftwareReporter.UploadLongestFailureRun", |
| longest_failure_run); |
| UMA_HISTOGRAM_BOOLEAN("SoftwareReporter.LastUploadResult", last_result); |
| } |
| |
| void ReportExperimentError(SoftwareReporterExperimentError error) { |
| UMA_HISTOGRAM_ENUMERATION("SoftwareReporter.ExperimentErrors", error, |
| SW_REPORTER_EXPERIMENT_ERROR_MAX); |
| } |
| |
| // Ensures |str| contains only alphanumeric characters and characters from |
| // |extras|, and is not longer than |max_length|. |
| bool ValidateString(const std::string& str, |
| const std::string& extras, |
| size_t max_length) { |
| return str.size() <= max_length && |
| std::all_of(str.cbegin(), str.cend(), [&extras](char c) { |
| return base::IsAsciiAlpha(c) || base::IsAsciiDigit(c) || |
| extras.find(c) != std::string::npos; |
| }); |
| } |
| |
| std::string GenerateSessionId() { |
| std::string session_id; |
| base::Base64Encode(base::RandBytesAsString(30), &session_id); |
| DCHECK(!session_id.empty()); |
| return session_id; |
| } |
| |
| // Add |behaviour_flag| to |supported_behaviours| if |behaviour_name| is found |
| // in the dictionary. Returns false on error. |
| bool GetOptionalBehaviour( |
| const base::DictionaryValue* invocation_params, |
| base::StringPiece behaviour_name, |
| SwReporterInvocation::Behaviours behaviour_flag, |
| SwReporterInvocation::Behaviours* supported_behaviours) { |
| DCHECK(invocation_params); |
| DCHECK(supported_behaviours); |
| |
| // Parameters enabling behaviours are optional, but if present must be |
| // boolean. |
| const base::Value* value = nullptr; |
| if (invocation_params->Get(behaviour_name, &value)) { |
| bool enable_behaviour = false; |
| if (!value->GetAsBoolean(&enable_behaviour)) { |
| ReportExperimentError(SW_REPORTER_EXPERIMENT_ERROR_BAD_PARAMS); |
| return false; |
| } |
| if (enable_behaviour) |
| *supported_behaviours |= behaviour_flag; |
| } |
| return true; |
| } |
| |
| // Reads the command-line params and an UMA histogram suffix from the manifest |
| // and adds the invocations to be run to |out_sequence|. |
| // Returns whether the manifest was successfully read. |
| bool ExtractInvocationSequenceFromManifest( |
| const base::FilePath& exe_path, |
| std::unique_ptr<base::DictionaryValue> manifest, |
| safe_browsing::SwReporterInvocationSequence* out_sequence) { |
| const base::ListValue* parameter_list = nullptr; |
| |
| // Allow an empty or missing launch_params list, but log an error if |
| // launch_params cannot be parsed as a list. |
| base::Value* launch_params = nullptr; |
| if (manifest->Get("launch_params", &launch_params) && |
| !launch_params->GetAsList(¶meter_list)) { |
| ReportExperimentError(SW_REPORTER_EXPERIMENT_ERROR_BAD_PARAMS); |
| return false; |
| } |
| |
| // Use a random session id to link reporter invocations together. |
| const std::string session_id = GenerateSessionId(); |
| |
| // If there are no launch parameters, create a single invocation with default |
| // behaviour. |
| if (!parameter_list || parameter_list->empty()) { |
| base::CommandLine command_line(exe_path); |
| command_line.AppendSwitchASCII(chrome_cleaner::kSessionIdSwitch, |
| session_id); |
| out_sequence->PushInvocation( |
| SwReporterInvocation(command_line) |
| .WithSupportedBehaviours( |
| SwReporterInvocation::BEHAVIOURS_ENABLED_BY_DEFAULT)); |
| return true; |
| } |
| |
| for (const auto& iter : *parameter_list) { |
| const base::DictionaryValue* invocation_params = nullptr; |
| if (!iter.GetAsDictionary(&invocation_params)) { |
| ReportExperimentError(SW_REPORTER_EXPERIMENT_ERROR_BAD_PARAMS); |
| return false; |
| } |
| |
| // Max length of the registry and histogram suffix. Fairly arbitrary: the |
| // Windows registry accepts much longer keys, but we need to display this |
| // string in histograms as well. |
| constexpr size_t kMaxSuffixLength = 80; |
| |
| // The suffix must be an alphanumeric string. (Empty is fine as long as the |
| // "suffix" key is present.) |
| std::string suffix; |
| if (!invocation_params->GetString("suffix", &suffix) || |
| !ValidateString(suffix, std::string(), kMaxSuffixLength)) { |
| ReportExperimentError(SW_REPORTER_EXPERIMENT_ERROR_BAD_PARAMS); |
| return false; |
| } |
| |
| // Build a command line for the reporter out of the executable path and the |
| // arguments from the manifest. (The "arguments" key must be present, but |
| // it's ok if it's an empty list or a list of empty strings.) |
| const base::ListValue* arguments = nullptr; |
| if (!invocation_params->GetList("arguments", &arguments)) { |
| ReportExperimentError(SW_REPORTER_EXPERIMENT_ERROR_BAD_PARAMS); |
| return false; |
| } |
| |
| std::vector<base::string16> argv = {exe_path.value()}; |
| for (const auto& value : *arguments) { |
| base::string16 argument; |
| if (!value.GetAsString(&argument)) { |
| ReportExperimentError(SW_REPORTER_EXPERIMENT_ERROR_BAD_PARAMS); |
| return false; |
| } |
| if (!argument.empty()) |
| argv.push_back(argument); |
| } |
| |
| base::CommandLine command_line(argv); |
| command_line.AppendSwitchASCII(chrome_cleaner::kSessionIdSwitch, |
| session_id); |
| |
| // Add the histogram suffix to the command-line as well, so that the |
| // reporter will add the same suffix to registry keys where it writes |
| // metrics. |
| if (!suffix.empty()) |
| command_line.AppendSwitchASCII(chrome_cleaner::kRegistrySuffixSwitch, |
| suffix); |
| |
| SwReporterInvocation::Behaviours supported_behaviours = 0; |
| if (!GetOptionalBehaviour(invocation_params, "prompt", |
| SwReporterInvocation::BEHAVIOUR_TRIGGER_PROMPT, |
| &supported_behaviours)) { |
| return false; |
| } |
| |
| out_sequence->PushInvocation( |
| SwReporterInvocation(command_line) |
| .WithSuffix(suffix) |
| .WithSupportedBehaviours(supported_behaviours)); |
| } |
| |
| return true; |
| } |
| |
| void ReportOnDemandUpdateSucceededHistogram(bool value) { |
| UMA_HISTOGRAM_BOOLEAN("SoftwareReporter.OnDemandUpdateSucceeded", value); |
| } |
| |
| } // namespace |
| |
| SwReporterInstallerPolicy::SwReporterInstallerPolicy( |
| const OnComponentReadyCallback& on_component_ready_callback) |
| : on_component_ready_callback_(on_component_ready_callback) {} |
| |
| SwReporterInstallerPolicy::~SwReporterInstallerPolicy() = default; |
| |
| bool SwReporterInstallerPolicy::VerifyInstallation( |
| const base::DictionaryValue& manifest, |
| const base::FilePath& dir) const { |
| return base::PathExists(dir.Append(kSwReporterExeName)); |
| } |
| |
| bool SwReporterInstallerPolicy::SupportsGroupPolicyEnabledComponentUpdates() |
| const { |
| return true; |
| } |
| |
| bool SwReporterInstallerPolicy::RequiresNetworkEncryption() const { |
| return false; |
| } |
| |
| update_client::CrxInstaller::Result SwReporterInstallerPolicy::OnCustomInstall( |
| const base::DictionaryValue& manifest, |
| const base::FilePath& install_dir) { |
| return update_client::CrxInstaller::Result(0); |
| } |
| |
| void SwReporterInstallerPolicy::OnCustomUninstall() {} |
| |
| void SwReporterInstallerPolicy::ComponentReady( |
| const base::Version& version, |
| const base::FilePath& install_dir, |
| std::unique_ptr<base::DictionaryValue> manifest) { |
| safe_browsing::SwReporterInvocationSequence invocations(version); |
| const base::FilePath exe_path(install_dir.Append(kSwReporterExeName)); |
| if (ExtractInvocationSequenceFromManifest(exe_path, std::move(manifest), |
| &invocations)) { |
| // Unless otherwise specified by a unit test, This will post |
| // |safe_browsing::OnSwReporterReady| to the UI thread. |
| on_component_ready_callback_.Run(std::move(invocations)); |
| } |
| } |
| |
| base::FilePath SwReporterInstallerPolicy::GetRelativeInstallDir() const { |
| return base::FilePath(FILE_PATH_LITERAL("SwReporter")); |
| } |
| |
| void SwReporterInstallerPolicy::GetHash(std::vector<uint8_t>* hash) const { |
| DCHECK(hash); |
| hash->assign(kSwReporterSha2Hash, |
| kSwReporterSha2Hash + sizeof(kSwReporterSha2Hash)); |
| } |
| |
| std::string SwReporterInstallerPolicy::GetName() const { |
| return "Software Reporter Tool"; |
| } |
| |
| update_client::InstallerAttributes |
| SwReporterInstallerPolicy::GetInstallerAttributes() const { |
| update_client::InstallerAttributes attributes; |
| if (base::FeatureList::IsEnabled( |
| safe_browsing::kChromeCleanupDistributionFeature)) { |
| // Pass the tag parameter to the installer as the "tag" attribute; it will |
| // be used to choose which binary is downloaded. |
| constexpr char kTagParamName[] = "reporter_omaha_tag"; |
| const std::string tag = variations::GetVariationParamValueByFeature( |
| safe_browsing::kChromeCleanupDistributionFeature, kTagParamName); |
| |
| // If the tag is not a valid attribute (see the regexp in |
| // ComponentInstallerPolicy::InstallerAttributes), set it to a valid but |
| // unrecognized value so that nothing will be downloaded. |
| constexpr size_t kMaxAttributeLength = 256; |
| constexpr char kExtraAttributeChars[] = "-.,;+_="; |
| constexpr char kTagParam[] = "tag"; |
| if (tag.empty() || |
| !ValidateString(tag, kExtraAttributeChars, kMaxAttributeLength)) { |
| ReportExperimentError(SW_REPORTER_EXPERIMENT_ERROR_BAD_TAG); |
| attributes[kTagParam] = "missing_tag"; |
| } else { |
| attributes[kTagParam] = tag; |
| } |
| } |
| return attributes; |
| } |
| |
| std::vector<std::string> SwReporterInstallerPolicy::GetMimeTypes() const { |
| return std::vector<std::string>(); |
| } |
| |
| SwReporterOnDemandFetcher::SwReporterOnDemandFetcher( |
| ComponentUpdateService* cus, |
| base::OnceClosure on_error_callback) |
| : cus_(cus), on_error_callback_(std::move(on_error_callback)) { |
| cus_->AddObserver(this); |
| cus_->GetOnDemandUpdater().OnDemandUpdate( |
| kSwReporterComponentId, OnDemandUpdater::Priority::FOREGROUND, |
| Callback()); |
| } |
| |
| SwReporterOnDemandFetcher::~SwReporterOnDemandFetcher() { |
| cus_->RemoveObserver(this); |
| } |
| |
| void SwReporterOnDemandFetcher::OnEvent(Events event, const std::string& id) { |
| if (id != kSwReporterComponentId) |
| return; |
| |
| if (event == Events::COMPONENT_NOT_UPDATED || |
| event == Events::COMPONENT_UPDATE_ERROR) { |
| ReportOnDemandUpdateSucceededHistogram(false); |
| std::move(on_error_callback_).Run(); |
| cus_->RemoveObserver(this); |
| } else if (event == Events::COMPONENT_UPDATED) { |
| ReportOnDemandUpdateSucceededHistogram(true); |
| cus_->RemoveObserver(this); |
| } |
| } |
| |
| void RegisterSwReporterComponent(ComponentUpdateService* cus) { |
| base::ScopedClosureRunner runner(std::move(*registration_cb_for_testing)); |
| |
| // Don't install the component if not allowed by policy. This prevents |
| // downloads and background scans. |
| if (!is_sw_reporter_enabled || !safe_browsing::SwReporterIsAllowedByPolicy()) |
| return; |
| |
| ReportUMAForLastCleanerRun(); |
| |
| // Once the component is ready and browser startup is complete, run |
| // |safe_browsing::OnSwReporterReady|. |
| auto lambda = [](safe_browsing::SwReporterInvocationSequence&& invocations) { |
| content::BrowserThread::PostAfterStartupTask( |
| FROM_HERE, |
| base::CreateSingleThreadTaskRunnerWithTraits( |
| {content::BrowserThread::UI}), |
| base::BindOnce( |
| &safe_browsing::ChromeCleanerController::OnSwReporterReady, |
| base::Unretained( |
| safe_browsing::ChromeCleanerController::GetInstance()), |
| base::Passed(&invocations))); |
| }; |
| |
| // Install the component. |
| auto installer = base::MakeRefCounted<ComponentInstaller>( |
| std::make_unique<SwReporterInstallerPolicy>(base::BindRepeating(lambda))); |
| installer->Register(cus, runner.Release()); |
| } |
| |
| void SetRegisterSwReporterComponentCallbackForTesting( |
| base::OnceClosure registration_cb) { |
| is_sw_reporter_enabled = true; |
| *registration_cb_for_testing = std::move(registration_cb); |
| } |
| |
| void RegisterPrefsForSwReporter(PrefRegistrySimple* registry) { |
| registry->RegisterInt64Pref(prefs::kSwReporterLastTimeTriggered, 0); |
| registry->RegisterIntegerPref(prefs::kSwReporterLastExitCode, -1); |
| registry->RegisterInt64Pref(prefs::kSwReporterLastTimeSentReport, 0); |
| registry->RegisterBooleanPref(prefs::kSwReporterEnabled, true); |
| registry->RegisterBooleanPref(prefs::kSwReporterReportingEnabled, true); |
| } |
| |
| void RegisterProfilePrefsForSwReporter( |
| user_prefs::PrefRegistrySyncable* registry) { |
| registry->RegisterStringPref(prefs::kSwReporterPromptVersion, ""); |
| |
| registry->RegisterStringPref(prefs::kSwReporterPromptSeed, ""); |
| } |
| |
| void ReportUMAForLastCleanerRun() { |
| base::string16 cleaner_key_name = |
| chrome_cleaner::kSoftwareRemovalToolRegistryKey; |
| cleaner_key_name.append(1, L'\\').append(chrome_cleaner::kCleanerSubKey); |
| base::win::RegKey cleaner_key(HKEY_CURRENT_USER, cleaner_key_name.c_str(), |
| KEY_ALL_ACCESS); |
| // Cleaner is assumed to have run if we have a start time. |
| if (cleaner_key.Valid()) { |
| if (cleaner_key.HasValue(chrome_cleaner::kStartTimeValueName)) { |
| // Get version number. |
| if (cleaner_key.HasValue(chrome_cleaner::kVersionValueName)) { |
| DWORD version = {}; |
| cleaner_key.ReadValueDW(chrome_cleaner::kVersionValueName, &version); |
| base::UmaHistogramSparse("SoftwareReporter.Cleaner.Version", version); |
| cleaner_key.DeleteValue(chrome_cleaner::kVersionValueName); |
| } |
| // Get start & end time. If we don't have an end time, we can assume the |
| // cleaner has not completed. |
| int64_t start_time_value = {}; |
| cleaner_key.ReadInt64(chrome_cleaner::kStartTimeValueName, |
| &start_time_value); |
| const base::Time start_time = base::Time::FromDeltaSinceWindowsEpoch( |
| base::TimeDelta::FromMicroseconds(start_time_value)); |
| |
| const bool completed = |
| cleaner_key.HasValue(chrome_cleaner::kEndTimeValueName); |
| SRTHasCompleted(completed ? SRT_COMPLETED_YES : SRT_COMPLETED_NOT_YET); |
| if (completed) { |
| int64_t end_time_value = {}; |
| cleaner_key.ReadInt64(chrome_cleaner::kEndTimeValueName, |
| &end_time_value); |
| const base::Time end_time = base::Time::FromDeltaSinceWindowsEpoch( |
| base::TimeDelta::FromMicroseconds(end_time_value)); |
| |
| cleaner_key.DeleteValue(chrome_cleaner::kEndTimeValueName); |
| UMA_HISTOGRAM_LONG_TIMES("SoftwareReporter.Cleaner.RunningTime", |
| end_time - start_time); |
| } |
| // Get exit code. Assume nothing was found if we can't read the exit code. |
| DWORD exit_code = chrome_cleaner::kSwReporterNothingFound; |
| if (cleaner_key.HasValue(chrome_cleaner::kExitCodeValueName)) { |
| cleaner_key.ReadValueDW(chrome_cleaner::kExitCodeValueName, &exit_code); |
| base::UmaHistogramSparse("SoftwareReporter.Cleaner.ExitCode", |
| exit_code); |
| cleaner_key.DeleteValue(chrome_cleaner::kExitCodeValueName); |
| } |
| cleaner_key.DeleteValue(chrome_cleaner::kStartTimeValueName); |
| |
| if (exit_code == chrome_cleaner::kSwReporterPostRebootCleanupNeeded || |
| exit_code == |
| chrome_cleaner::kSwReporterDelayedPostRebootCleanupNeeded) { |
| // Check if we are running after the user has rebooted. |
| const base::TimeDelta elapsed = base::Time::Now() - start_time; |
| DCHECK_GT(elapsed.InMilliseconds(), 0); |
| UMA_HISTOGRAM_BOOLEAN( |
| "SoftwareReporter.Cleaner.HasRebooted", |
| static_cast<uint64_t>(elapsed.InMilliseconds()) > ::GetTickCount()); |
| } |
| |
| if (cleaner_key.HasValue(chrome_cleaner::kUploadResultsValueName)) { |
| base::string16 upload_results; |
| cleaner_key.ReadValue(chrome_cleaner::kUploadResultsValueName, |
| &upload_results); |
| ReportUploadsWithUma(upload_results); |
| } |
| } else { |
| if (cleaner_key.HasValue(chrome_cleaner::kEndTimeValueName)) { |
| SRTHasCompleted(SRT_COMPLETED_LATER); |
| cleaner_key.DeleteValue(chrome_cleaner::kEndTimeValueName); |
| } |
| } |
| } |
| } |
| |
| } // namespace component_updater |