blob: 5bd11650ce6241cfe54b3a4ddc7b268fb31beabd [file] [log] [blame]
// 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/metrics/histogram_macros.h"
#include "base/metrics/sparse_histogram.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/threading/thread_task_runner_handle.h"
#include "base/threading/worker_pool.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/srt_fetcher_win.h"
#include "chrome/browser/safe_browsing/srt_field_trial_win.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_thread.h"
namespace component_updater {
namespace {
using safe_browsing::SwReporterInvocation;
// 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 kSha256Hash[] = {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");
constexpr char kSessionIdSwitch[] = "session-id";
// SRT registry keys and value names.
const wchar_t kCleanerSuffixRegistryKey[] = L"Cleaner";
const wchar_t kExitCodeValueName[] = L"ExitCode";
const wchar_t kUploadResultsValueName[] = L"UploadResults";
const wchar_t kVersionValueName[] = L"Version";
constexpr base::Feature kExperimentalEngineFeature{
"ExperimentalSwReporterEngine", base::FEATURE_DISABLED_BY_DEFAULT};
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() == 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(SwReporterExperimentError error) {
UMA_HISTOGRAM_ENUMERATION("SoftwareReporter.ExperimentErrors", error,
SW_REPORTER_EXPERIMENT_ERROR_MAX);
}
// Once the Software Reporter is downloaded, schedules it to run sometime after
// the current browser startup is complete. (This is the default
// |reporter_runner| function passed to the |SwReporterInstallerTraits|
// constructor in |RegisterSwReporterComponent| below.)
void RunSwReportersAfterStartup(
const safe_browsing::SwReporterQueue& invocations,
const base::Version& version) {
content::BrowserThread::PostAfterStartupTask(
FROM_HERE, base::ThreadTaskRunnerHandle::Get(),
base::Bind(&safe_browsing::RunSwReporters, invocations, version,
base::ThreadTaskRunnerHandle::Get(),
base::WorkerPool::GetTaskRunner(true)));
}
// 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 launch the SwReporter with those parameters. If anything goes wrong the
// SwReporter should not be run at all.
void RunExperimentalSwReporter(const base::FilePath& exe_path,
const base::Version& version,
std::unique_ptr<base::DictionaryValue> manifest,
const SwReporterRunner& reporter_runner) {
// The experiment requires launch_params so if they aren't present just
// return. This isn't an error because the user could get into the experiment
// group before they've downloaded the experiment component.
base::Value* launch_params = nullptr;
if (!manifest->Get("launch_params", &launch_params))
return;
const base::ListValue* parameter_list = nullptr;
if (!launch_params->GetAsList(&parameter_list) || parameter_list->empty()) {
ReportExperimentError(SW_REPORTER_EXPERIMENT_ERROR_BAD_PARAMS);
return;
}
const std::string session_id = GenerateSessionId();
safe_browsing::SwReporterQueue invocations;
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;
}
// 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;
}
// 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;
}
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;
}
if (!argument.empty())
argv.push_back(argument);
}
base::CommandLine command_line(argv);
// Add a random session id to link experimental reporter runs together.
command_line.AppendSwitchASCII(kSessionIdSwitch, session_id);
const std::string experiment_group =
variations::GetVariationParamValueByFeature(
kExperimentalEngineFeature, "experiment_group_for_reporting");
command_line.AppendSwitchNative("engine-experiment-group",
experiment_group.empty()
? L"missing_experiment_group"
: base::UTF8ToUTF16(experiment_group));
// 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("registry-suffix", suffix);
SwReporterInvocation::Behaviours supported_behaviours = 0;
if (!GetOptionalBehaviour(invocation_params, "prompt",
SwReporterInvocation::BEHAVIOUR_TRIGGER_PROMPT,
&supported_behaviours))
return;
if (!GetOptionalBehaviour(
invocation_params, "allow-reporter-logs",
SwReporterInvocation::BEHAVIOUR_ALLOW_SEND_REPORTER_LOGS,
&supported_behaviours))
return;
auto invocation = SwReporterInvocation::FromCommandLine(command_line);
invocation.suffix = suffix;
invocation.supported_behaviours = supported_behaviours;
invocations.push(invocation);
}
DCHECK(!invocations.empty());
reporter_runner.Run(invocations, version);
}
} // namespace
SwReporterInstallerTraits::SwReporterInstallerTraits(
const SwReporterRunner& reporter_runner,
bool is_experimental_engine_supported)
: reporter_runner_(reporter_runner),
is_experimental_engine_supported_(is_experimental_engine_supported) {}
SwReporterInstallerTraits::~SwReporterInstallerTraits() {}
bool SwReporterInstallerTraits::VerifyInstallation(
const base::DictionaryValue& manifest,
const base::FilePath& dir) const {
return base::PathExists(dir.Append(kSwReporterExeName));
}
bool SwReporterInstallerTraits::SupportsGroupPolicyEnabledComponentUpdates()
const {
return true;
}
bool SwReporterInstallerTraits::RequiresNetworkEncryption() const {
return false;
}
update_client::CrxInstaller::Result SwReporterInstallerTraits::OnCustomInstall(
const base::DictionaryValue& manifest,
const base::FilePath& install_dir) {
return update_client::CrxInstaller::Result(0);
}
void SwReporterInstallerTraits::ComponentReady(
const base::Version& version,
const base::FilePath& install_dir,
std::unique_ptr<base::DictionaryValue> manifest) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
const base::FilePath exe_path(install_dir.Append(kSwReporterExeName));
if (IsExperimentalEngineEnabled()) {
RunExperimentalSwReporter(exe_path, version, std::move(manifest),
reporter_runner_);
} else {
base::CommandLine command_line(exe_path);
command_line.AppendSwitchASCII(kSessionIdSwitch, GenerateSessionId());
auto invocation = SwReporterInvocation::FromCommandLine(command_line);
invocation.supported_behaviours =
SwReporterInvocation::BEHAVIOUR_LOG_TO_RAPPOR |
SwReporterInvocation::BEHAVIOUR_LOG_EXIT_CODE_TO_PREFS |
SwReporterInvocation::BEHAVIOUR_TRIGGER_PROMPT |
SwReporterInvocation::BEHAVIOUR_ALLOW_SEND_REPORTER_LOGS;
safe_browsing::SwReporterQueue invocations;
invocations.push(invocation);
reporter_runner_.Run(invocations, version);
}
}
base::FilePath SwReporterInstallerTraits::GetRelativeInstallDir() const {
return base::FilePath(FILE_PATH_LITERAL("SwReporter"));
}
void SwReporterInstallerTraits::GetHash(std::vector<uint8_t>* hash) const {
DCHECK(hash);
hash->assign(kSha256Hash, kSha256Hash + sizeof(kSha256Hash));
}
std::string SwReporterInstallerTraits::GetName() const {
return "Software Reporter Tool";
}
update_client::InstallerAttributes
SwReporterInstallerTraits::GetInstallerAttributes() const {
update_client::InstallerAttributes attributes;
if (IsExperimentalEngineEnabled()) {
// Pass the "tag" parameter to the installer; it will be used to choose
// which binary is downloaded.
constexpr char kTagParam[] = "tag";
const std::string tag = variations::GetVariationParamValueByFeature(
kExperimentalEngineFeature, kTagParam);
// If the tag is not a valid attribute (see the regexp in
// ComponentInstallerTraits::InstallerAttributes), set it to a valid but
// unrecognized value so that nothing will be downloaded.
constexpr size_t kMaxAttributeLength = 256;
constexpr char kExtraAttributeChars[] = "-.,;+_=";
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> SwReporterInstallerTraits::GetMimeTypes() const {
return std::vector<std::string>();
}
bool SwReporterInstallerTraits::IsExperimentalEngineEnabled() const {
return is_experimental_engine_supported_ &&
base::FeatureList::IsEnabled(kExperimentalEngineFeature);
}
void RegisterSwReporterComponent(ComponentUpdateService* cus) {
if (!safe_browsing::IsSwReporterEnabled())
return;
// Check if we have information from Cleaner and record UMA statistics.
base::string16 cleaner_key_name(
safe_browsing::kSoftwareRemovalToolRegistryKey);
cleaner_key_name.append(1, L'\\').append(kCleanerSuffixRegistryKey);
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(safe_browsing::kStartTimeValueName)) {
// Get version number.
if (cleaner_key.HasValue(kVersionValueName)) {
DWORD version;
cleaner_key.ReadValueDW(kVersionValueName, &version);
UMA_HISTOGRAM_SPARSE_SLOWLY("SoftwareReporter.Cleaner.Version",
version);
cleaner_key.DeleteValue(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(safe_browsing::kStartTimeValueName,
&start_time_value);
bool completed = cleaner_key.HasValue(safe_browsing::kEndTimeValueName);
SRTHasCompleted(completed ? SRT_COMPLETED_YES : SRT_COMPLETED_NOT_YET);
if (completed) {
int64_t end_time_value;
cleaner_key.ReadInt64(safe_browsing::kEndTimeValueName,
&end_time_value);
cleaner_key.DeleteValue(safe_browsing::kEndTimeValueName);
base::TimeDelta run_time(
base::Time::FromInternalValue(end_time_value) -
base::Time::FromInternalValue(start_time_value));
UMA_HISTOGRAM_LONG_TIMES("SoftwareReporter.Cleaner.RunningTime",
run_time);
}
// Get exit code. Assume nothing was found if we can't read the exit code.
DWORD exit_code = safe_browsing::kSwReporterNothingFound;
if (cleaner_key.HasValue(kExitCodeValueName)) {
cleaner_key.ReadValueDW(kExitCodeValueName, &exit_code);
UMA_HISTOGRAM_SPARSE_SLOWLY("SoftwareReporter.Cleaner.ExitCode",
exit_code);
cleaner_key.DeleteValue(kExitCodeValueName);
}
cleaner_key.DeleteValue(safe_browsing::kStartTimeValueName);
if (exit_code == safe_browsing::kSwReporterPostRebootCleanupNeeded ||
exit_code ==
safe_browsing::kSwReporterDelayedPostRebootCleanupNeeded) {
// Check if we are running after the user has rebooted.
base::TimeDelta elapsed(
base::Time::Now() -
base::Time::FromInternalValue(start_time_value));
DCHECK_GT(elapsed.InMilliseconds(), 0);
UMA_HISTOGRAM_BOOLEAN(
"SoftwareReporter.Cleaner.HasRebooted",
static_cast<uint64_t>(elapsed.InMilliseconds()) > ::GetTickCount());
}
if (cleaner_key.HasValue(kUploadResultsValueName)) {
base::string16 upload_results;
cleaner_key.ReadValue(kUploadResultsValueName, &upload_results);
ReportUploadsWithUma(upload_results);
}
} else {
if (cleaner_key.HasValue(safe_browsing::kEndTimeValueName)) {
SRTHasCompleted(SRT_COMPLETED_LATER);
cleaner_key.DeleteValue(safe_browsing::kEndTimeValueName);
}
}
}
// The experiment is only enabled on x86. There's no way to check this in the
// variations config so we'll hard-code it.
const bool is_experimental_engine_supported =
base::win::OSInfo::GetInstance()->architecture() ==
base::win::OSInfo::X86_ARCHITECTURE;
// Install the component.
std::unique_ptr<ComponentInstallerTraits> traits(
new SwReporterInstallerTraits(base::Bind(&RunSwReportersAfterStartup),
is_experimental_engine_supported));
// |cus| will take ownership of |installer| during installer->Register(cus).
DefaultComponentInstaller* installer =
new DefaultComponentInstaller(std::move(traits));
installer->Register(cus, base::Closure());
}
void RegisterPrefsForSwReporter(PrefRegistrySimple* registry) {
registry->RegisterInt64Pref(prefs::kSwReporterLastTimeTriggered, 0);
registry->RegisterIntegerPref(prefs::kSwReporterLastExitCode, -1);
registry->RegisterBooleanPref(prefs::kSwReporterPendingPrompt, false);
registry->RegisterInt64Pref(prefs::kSwReporterLastTimeSentReport, 0);
}
void RegisterProfilePrefsForSwReporter(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterStringPref(prefs::kSwReporterPromptVersion, "");
registry->RegisterStringPref(prefs::kSwReporterPromptSeed, "");
}
} // namespace component_updater