blob: 76851628f23a6f993002a011d71d5c02e16e3374 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/safe_browsing/incident_reporting/incident_reporting_service.h"
#include <math.h>
#include <algorithm>
#include <vector>
#include "base/metrics/histogram.h"
#include "base/prefs/pref_service.h"
#include "base/prefs/scoped_user_pref_update.h"
#include "base/process/process_info.h"
#include "base/single_thread_task_runner.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/thread_task_runner_handle.h"
#include "base/threading/sequenced_worker_pool.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/prefs/tracked/tracked_preference_validation_delegate.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/database_manager.h"
#include "chrome/browser/safe_browsing/incident_reporting/binary_integrity_incident_handlers.h"
#include "chrome/browser/safe_browsing/incident_reporting/environment_data_collection.h"
#include "chrome/browser/safe_browsing/incident_reporting/incident_report_uploader_impl.h"
#include "chrome/browser/safe_browsing/incident_reporting/preference_validation_delegate.h"
#include "chrome/browser/safe_browsing/incident_reporting/tracked_preference_incident_handlers.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/safe_browsing/csd.pb.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/notification_service.h"
#include "net/url_request/url_request_context_getter.h"
namespace safe_browsing {
namespace {
// The type of an incident. Used for user metrics and for pruning of
// previously-reported incidents.
enum IncidentType {
// Start with 1 rather than zero; otherwise there won't be enough buckets for
// the histogram.
TRACKED_PREFERENCE = 1,
BINARY_INTEGRITY = 2,
// Values for new incident types go here.
NUM_INCIDENT_TYPES
};
// The action taken for an incident; used for user metrics (see
// LogIncidentDataType).
enum IncidentDisposition {
DROPPED,
ACCEPTED,
};
// The state persisted for a specific instance of an incident to enable pruning
// of previously-reported incidents.
struct PersistentIncidentState {
// The type of the incident.
IncidentType type;
// The key for a specific instance of an incident.
std::string key;
// A hash digest representing a specific instance of an incident.
uint32_t digest;
};
// The amount of time the service will wait to collate incidents.
const int64 kDefaultUploadDelayMs = 1000 * 60; // one minute
// The amount of time between running delayed analysis callbacks.
const int64 kDefaultCallbackIntervalMs = 1000 * 20;
// Returns the number of incidents contained in |incident|. The result is
// expected to be 1. Used in DCHECKs.
size_t CountIncidents(const ClientIncidentReport_IncidentData& incident) {
size_t result = 0;
if (incident.has_tracked_preference())
++result;
if (incident.has_binary_integrity())
++result;
// Add detection for new incident types here.
return result;
}
// Returns the type of incident contained in |incident_data|.
IncidentType GetIncidentType(
const ClientIncidentReport_IncidentData& incident_data) {
if (incident_data.has_tracked_preference())
return TRACKED_PREFERENCE;
if (incident_data.has_binary_integrity())
return BINARY_INTEGRITY;
// Add detection for new incident types here.
COMPILE_ASSERT(BINARY_INTEGRITY + 1 == NUM_INCIDENT_TYPES,
add_support_for_new_types);
NOTREACHED();
return NUM_INCIDENT_TYPES;
}
// Logs the type of incident in |incident_data| to a user metrics histogram.
void LogIncidentDataType(
IncidentDisposition disposition,
const ClientIncidentReport_IncidentData& incident_data) {
IncidentType type = GetIncidentType(incident_data);
if (disposition == ACCEPTED) {
UMA_HISTOGRAM_ENUMERATION("SBIRS.Incident", type, NUM_INCIDENT_TYPES);
} else {
DCHECK_EQ(disposition, DROPPED);
UMA_HISTOGRAM_ENUMERATION("SBIRS.DroppedIncident", type,
NUM_INCIDENT_TYPES);
}
}
// Computes the persistent state for an incident.
PersistentIncidentState ComputeIncidentState(
const ClientIncidentReport_IncidentData& incident) {
PersistentIncidentState state = {GetIncidentType(incident)};
switch (state.type) {
case TRACKED_PREFERENCE:
state.key = GetTrackedPreferenceIncidentKey(incident);
state.digest = GetTrackedPreferenceIncidentDigest(incident);
break;
case BINARY_INTEGRITY:
state.key = GetBinaryIntegrityIncidentKey(incident);
state.digest = GetBinaryIntegrityIncidentDigest(incident);
break;
// Add handling for new incident types here.
default:
COMPILE_ASSERT(BINARY_INTEGRITY + 1 == NUM_INCIDENT_TYPES,
add_support_for_new_types);
NOTREACHED();
break;
}
return state;
}
// Returns true if the incident described by |state| has already been reported
// based on the bookkeeping in the |incidents_sent| preference dictionary.
bool IncidentHasBeenReported(const base::DictionaryValue* incidents_sent,
const PersistentIncidentState& state) {
const base::DictionaryValue* type_dict = NULL;
std::string digest_string;
return (incidents_sent &&
incidents_sent->GetDictionaryWithoutPathExpansion(
base::IntToString(state.type), &type_dict) &&
type_dict->GetStringWithoutPathExpansion(state.key, &digest_string) &&
digest_string == base::UintToString(state.digest));
}
// Marks the incidents described by |states| as having been reported
// in |incidents_set|.
void MarkIncidentsAsReported(const std::vector<PersistentIncidentState>& states,
base::DictionaryValue* incidents_sent) {
for (size_t i = 0; i < states.size(); ++i) {
const PersistentIncidentState& data = states[i];
base::DictionaryValue* type_dict = NULL;
const std::string type_string(base::IntToString(data.type));
if (!incidents_sent->GetDictionaryWithoutPathExpansion(type_string,
&type_dict)) {
type_dict = new base::DictionaryValue();
incidents_sent->SetWithoutPathExpansion(type_string, type_dict);
}
type_dict->SetStringWithoutPathExpansion(data.key,
base::UintToString(data.digest));
}
}
// Runs |callback| on the thread to which |thread_runner| belongs. The callback
// is run immediately if this function is called on |thread_runner|'s thread.
void AddIncidentOnOriginThread(
const AddIncidentCallback& callback,
scoped_refptr<base::SingleThreadTaskRunner> thread_runner,
scoped_ptr<ClientIncidentReport_IncidentData> incident) {
if (thread_runner->BelongsToCurrentThread())
callback.Run(incident.Pass());
else
thread_runner->PostTask(FROM_HERE,
base::Bind(callback, base::Passed(&incident)));
}
} // namespace
struct IncidentReportingService::ProfileContext {
ProfileContext();
~ProfileContext();
// The incidents collected for this profile pending creation and/or upload.
ScopedVector<ClientIncidentReport_IncidentData> incidents;
// False until PROFILE_ADDED notification is received.
bool added;
private:
DISALLOW_COPY_AND_ASSIGN(ProfileContext);
};
class IncidentReportingService::UploadContext {
public:
typedef std::map<Profile*, std::vector<PersistentIncidentState> >
PersistentIncidentStateCollection;
explicit UploadContext(scoped_ptr<ClientIncidentReport> report);
~UploadContext();
// The report being uploaded.
scoped_ptr<ClientIncidentReport> report;
// The uploader in use. This is NULL until the CSD killswitch is checked.
scoped_ptr<IncidentReportUploader> uploader;
// A mapping of profiles to the data to be persisted upon successful upload.
PersistentIncidentStateCollection profiles_to_state;
private:
DISALLOW_COPY_AND_ASSIGN(UploadContext);
};
IncidentReportingService::ProfileContext::ProfileContext() : added() {
}
IncidentReportingService::ProfileContext::~ProfileContext() {
}
IncidentReportingService::UploadContext::UploadContext(
scoped_ptr<ClientIncidentReport> report)
: report(report.Pass()) {
}
IncidentReportingService::UploadContext::~UploadContext() {
}
IncidentReportingService::IncidentReportingService(
SafeBrowsingService* safe_browsing_service,
const scoped_refptr<net::URLRequestContextGetter>& request_context_getter)
: database_manager_(safe_browsing_service ?
safe_browsing_service->database_manager() : NULL),
url_request_context_getter_(request_context_getter),
collect_environment_data_fn_(&CollectEnvironmentData),
environment_collection_task_runner_(
content::BrowserThread::GetBlockingPool()
->GetTaskRunnerWithShutdownBehavior(
base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)),
environment_collection_pending_(),
collation_timeout_pending_(),
collation_timer_(FROM_HERE,
base::TimeDelta::FromMilliseconds(kDefaultUploadDelayMs),
this,
&IncidentReportingService::OnCollationTimeout),
delayed_analysis_callbacks_(
base::TimeDelta::FromMilliseconds(kDefaultCallbackIntervalMs),
content::BrowserThread::GetBlockingPool()
->GetTaskRunnerWithShutdownBehavior(
base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)),
receiver_weak_ptr_factory_(this),
weak_ptr_factory_(this) {
notification_registrar_.Add(this,
chrome::NOTIFICATION_PROFILE_ADDED,
content::NotificationService::AllSources());
notification_registrar_.Add(this,
chrome::NOTIFICATION_PROFILE_DESTROYED,
content::NotificationService::AllSources());
}
IncidentReportingService::~IncidentReportingService() {
CancelIncidentCollection();
// Cancel all internal asynchronous tasks.
weak_ptr_factory_.InvalidateWeakPtrs();
CancelEnvironmentCollection();
CancelDownloadCollection();
CancelAllReportUploads();
STLDeleteValues(&profiles_);
}
AddIncidentCallback IncidentReportingService::GetAddIncidentCallback(
Profile* profile) {
// Force the context to be created so that incidents added before
// OnProfileAdded is called are held until the profile's preferences can be
// queried.
ignore_result(GetOrCreateProfileContext(profile));
return base::Bind(&IncidentReportingService::AddIncident,
receiver_weak_ptr_factory_.GetWeakPtr(),
profile);
}
scoped_ptr<TrackedPreferenceValidationDelegate>
IncidentReportingService::CreatePreferenceValidationDelegate(Profile* profile) {
DCHECK(thread_checker_.CalledOnValidThread());
if (profile->IsOffTheRecord())
return scoped_ptr<TrackedPreferenceValidationDelegate>();
return scoped_ptr<TrackedPreferenceValidationDelegate>(
new PreferenceValidationDelegate(GetAddIncidentCallback(profile)));
}
void IncidentReportingService::RegisterDelayedAnalysisCallback(
const DelayedAnalysisCallback& callback) {
DCHECK(thread_checker_.CalledOnValidThread());
// |callback| will be run on the blocking pool, so it will likely run the
// AddIncidentCallback there as well. Bounce the run of that callback back to
// the current thread via AddIncidentOnOriginThread.
delayed_analysis_callbacks_.RegisterCallback(
base::Bind(callback,
base::Bind(&AddIncidentOnOriginThread,
GetAddIncidentCallback(NULL),
base::ThreadTaskRunnerHandle::Get())));
// Start running the callbacks if any profiles are participating in safe
// browsing. If none are now, running will commence if/when a participaing
// profile is added.
if (FindEligibleProfile())
delayed_analysis_callbacks_.Start();
}
IncidentReportingService::IncidentReportingService(
SafeBrowsingService* safe_browsing_service,
const scoped_refptr<net::URLRequestContextGetter>& request_context_getter,
base::TimeDelta delayed_task_interval,
const scoped_refptr<base::TaskRunner>& delayed_task_runner)
: database_manager_(safe_browsing_service ?
safe_browsing_service->database_manager() : NULL),
url_request_context_getter_(request_context_getter),
collect_environment_data_fn_(&CollectEnvironmentData),
environment_collection_task_runner_(
content::BrowserThread::GetBlockingPool()
->GetTaskRunnerWithShutdownBehavior(
base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)),
environment_collection_pending_(),
collation_timeout_pending_(),
collation_timer_(FROM_HERE,
base::TimeDelta::FromMilliseconds(kDefaultUploadDelayMs),
this,
&IncidentReportingService::OnCollationTimeout),
delayed_analysis_callbacks_(delayed_task_interval, delayed_task_runner),
receiver_weak_ptr_factory_(this),
weak_ptr_factory_(this) {
notification_registrar_.Add(this,
chrome::NOTIFICATION_PROFILE_ADDED,
content::NotificationService::AllSources());
notification_registrar_.Add(this,
chrome::NOTIFICATION_PROFILE_DESTROYED,
content::NotificationService::AllSources());
}
void IncidentReportingService::SetCollectEnvironmentHook(
CollectEnvironmentDataFn collect_environment_data_hook,
const scoped_refptr<base::TaskRunner>& task_runner) {
if (collect_environment_data_hook) {
collect_environment_data_fn_ = collect_environment_data_hook;
environment_collection_task_runner_ = task_runner;
} else {
collect_environment_data_fn_ = &CollectEnvironmentData;
environment_collection_task_runner_ =
content::BrowserThread::GetBlockingPool()
->GetTaskRunnerWithShutdownBehavior(
base::SequencedWorkerPool::SKIP_ON_SHUTDOWN);
}
}
void IncidentReportingService::OnProfileAdded(Profile* profile) {
DCHECK(thread_checker_.CalledOnValidThread());
// Track the addition of all profiles even when no report is being assembled
// so that the service can determine whether or not it can evaluate a
// profile's preferences at the time of incident addition.
ProfileContext* context = GetOrCreateProfileContext(profile);
context->added = true;
const bool safe_browsing_enabled =
profile->GetPrefs()->GetBoolean(prefs::kSafeBrowsingEnabled);
// Start processing delayed analysis callbacks if this new profile
// participates in safe browsing. Start is idempotent, so this is safe even if
// they're already running.
if (safe_browsing_enabled)
delayed_analysis_callbacks_.Start();
// Start a new report if this profile participates in safe browsing and there
// are process-wide incidents.
if (safe_browsing_enabled && GetProfileContext(NULL) &&
GetProfileContext(NULL)->incidents.size()) {
BeginReportProcessing();
}
// TODO(grt): register for pref change notifications to start delayed analysis
// and/or report processing if sb is currently disabled but subsequently
// enabled.
// Nothing else to do if a report is not being assembled.
if (!report_)
return;
// Drop all incidents associated with this profile that were received prior to
// its addition if the profile is not participating in safe browsing.
if (!context->incidents.empty() && !safe_browsing_enabled) {
for (size_t i = 0; i < context->incidents.size(); ++i)
LogIncidentDataType(DROPPED, *context->incidents[i]);
context->incidents.clear();
}
// Take another stab at finding the most recent download if a report is being
// assembled and one hasn't been found yet (the LastDownloadFinder operates
// only on profiles that have been added to the ProfileManager).
BeginDownloadCollection();
}
scoped_ptr<LastDownloadFinder> IncidentReportingService::CreateDownloadFinder(
const LastDownloadFinder::LastDownloadCallback& callback) {
return LastDownloadFinder::Create(callback).Pass();
}
scoped_ptr<IncidentReportUploader> IncidentReportingService::StartReportUpload(
const IncidentReportUploader::OnResultCallback& callback,
const scoped_refptr<net::URLRequestContextGetter>& request_context_getter,
const ClientIncidentReport& report) {
return IncidentReportUploaderImpl::UploadReport(
callback, request_context_getter, report).Pass();
}
IncidentReportingService::ProfileContext*
IncidentReportingService::GetOrCreateProfileContext(Profile* profile) {
ProfileContextCollection::iterator it =
profiles_.insert(ProfileContextCollection::value_type(profile, NULL))
.first;
if (!it->second)
it->second = new ProfileContext();
return it->second;
}
IncidentReportingService::ProfileContext*
IncidentReportingService::GetProfileContext(Profile* profile) {
ProfileContextCollection::iterator it = profiles_.find(profile);
return it == profiles_.end() ? NULL : it->second;
}
void IncidentReportingService::OnProfileDestroyed(Profile* profile) {
DCHECK(thread_checker_.CalledOnValidThread());
ProfileContextCollection::iterator it = profiles_.find(profile);
if (it == profiles_.end())
return;
// TODO(grt): Persist incidents for upload on future profile load.
// Forget about this profile. Incidents not yet sent for upload are lost.
// No new incidents will be accepted for it.
delete it->second;
profiles_.erase(it);
// Remove the association with this profile from all pending uploads.
for (size_t i = 0; i < uploads_.size(); ++i)
uploads_[i]->profiles_to_state.erase(profile);
}
Profile* IncidentReportingService::FindEligibleProfile() const {
Profile* candidate = NULL;
for (ProfileContextCollection::const_iterator scan = profiles_.begin();
scan != profiles_.end();
++scan) {
// Skip over profiles that have yet to be added to the profile manager.
// This will also skip over the NULL-profile context used to hold
// process-wide incidents.
if (!scan->second->added)
continue;
PrefService* prefs = scan->first->GetPrefs();
if (prefs->GetBoolean(prefs::kSafeBrowsingEnabled)) {
if (!candidate)
candidate = scan->first;
if (prefs->GetBoolean(prefs::kSafeBrowsingExtendedReportingEnabled)) {
candidate = scan->first;
break;
}
}
}
return candidate;
}
void IncidentReportingService::AddIncident(
Profile* profile,
scoped_ptr<ClientIncidentReport_IncidentData> incident_data) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK_EQ(1U, CountIncidents(*incident_data));
ProfileContext* context = GetProfileContext(profile);
// It is forbidden to call this function with a destroyed profile.
DCHECK(context);
// If this is a process-wide incident, the context must not indicate that the
// profile (which is NULL) has been added to the profile manager.
DCHECK(profile || !context->added);
// Drop the incident immediately if the profile has already been added to the
// manager and is not participating in safe browsing. Preference evaluation is
// deferred until OnProfileAdded() otherwise.
if (context->added &&
!profile->GetPrefs()->GetBoolean(prefs::kSafeBrowsingEnabled)) {
LogIncidentDataType(DROPPED, *incident_data);
return;
}
// Provide time to the new incident if the caller didn't do so.
if (!incident_data->has_incident_time_msec())
incident_data->set_incident_time_msec(base::Time::Now().ToJavaTime());
// Take ownership of the incident.
context->incidents.push_back(incident_data.release());
// Remember when the first incident for this report arrived.
if (first_incident_time_.is_null())
first_incident_time_ = base::Time::Now();
// Log the time between the previous incident and this one.
if (!last_incident_time_.is_null()) {
UMA_HISTOGRAM_TIMES("SBIRS.InterIncidentTime",
base::TimeTicks::Now() - last_incident_time_);
}
last_incident_time_ = base::TimeTicks::Now();
// Persist the incident data.
// Start assembling a new report if this is the first incident ever or the
// first since the last upload.
BeginReportProcessing();
}
void IncidentReportingService::BeginReportProcessing() {
DCHECK(thread_checker_.CalledOnValidThread());
// Creates a new report if needed.
if (!report_)
report_.reset(new ClientIncidentReport());
// Ensure that collection tasks are running (calls are idempotent).
BeginIncidentCollation();
BeginEnvironmentCollection();
BeginDownloadCollection();
}
void IncidentReportingService::BeginIncidentCollation() {
// Restart the delay timer to send the report upon expiration.
collation_timeout_pending_ = true;
collation_timer_.Reset();
}
void IncidentReportingService::BeginEnvironmentCollection() {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(report_);
// Nothing to do if environment collection is pending or has already
// completed.
if (environment_collection_pending_ || report_->has_environment())
return;
environment_collection_begin_ = base::TimeTicks::Now();
ClientIncidentReport_EnvironmentData* environment_data =
new ClientIncidentReport_EnvironmentData();
environment_collection_pending_ =
environment_collection_task_runner_->PostTaskAndReply(
FROM_HERE,
base::Bind(collect_environment_data_fn_, environment_data),
base::Bind(&IncidentReportingService::OnEnvironmentDataCollected,
weak_ptr_factory_.GetWeakPtr(),
base::Passed(make_scoped_ptr(environment_data))));
// Posting the task will fail if the runner has been shut down. This should
// never happen since the blocking pool is shut down after this service.
DCHECK(environment_collection_pending_);
}
bool IncidentReportingService::WaitingForEnvironmentCollection() {
return environment_collection_pending_;
}
void IncidentReportingService::CancelEnvironmentCollection() {
environment_collection_begin_ = base::TimeTicks();
environment_collection_pending_ = false;
if (report_)
report_->clear_environment();
}
void IncidentReportingService::OnEnvironmentDataCollected(
scoped_ptr<ClientIncidentReport_EnvironmentData> environment_data) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(environment_collection_pending_);
DCHECK(report_ && !report_->has_environment());
environment_collection_pending_ = false;
// CurrentProcessInfo::CreationTime() is missing on some platforms.
#if defined(OS_MACOSX) || defined(OS_WIN) || defined(OS_LINUX)
base::TimeDelta uptime =
first_incident_time_ - base::CurrentProcessInfo::CreationTime();
environment_data->mutable_process()->set_uptime_msec(uptime.InMilliseconds());
#endif
report_->set_allocated_environment(environment_data.release());
UMA_HISTOGRAM_TIMES("SBIRS.EnvCollectionTime",
base::TimeTicks::Now() - environment_collection_begin_);
environment_collection_begin_ = base::TimeTicks();
UploadIfCollectionComplete();
}
bool IncidentReportingService::WaitingToCollateIncidents() {
return collation_timeout_pending_;
}
void IncidentReportingService::CancelIncidentCollection() {
collation_timeout_pending_ = false;
last_incident_time_ = base::TimeTicks();
report_.reset();
}
void IncidentReportingService::OnCollationTimeout() {
DCHECK(thread_checker_.CalledOnValidThread());
// Exit early if collection was cancelled.
if (!collation_timeout_pending_)
return;
// Wait another round if profile-bound incidents have come in from a profile
// that has yet to complete creation.
for (ProfileContextCollection::iterator scan = profiles_.begin();
scan != profiles_.end();
++scan) {
if (scan->first && !scan->second->added &&
!scan->second->incidents.empty()) {
collation_timer_.Reset();
return;
}
}
collation_timeout_pending_ = false;
UploadIfCollectionComplete();
}
void IncidentReportingService::BeginDownloadCollection() {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(report_);
// Nothing to do if a search for the most recent download is already pending
// or if one has already been found.
if (last_download_finder_ || report_->has_download())
return;
last_download_begin_ = base::TimeTicks::Now();
last_download_finder_ = CreateDownloadFinder(
base::Bind(&IncidentReportingService::OnLastDownloadFound,
weak_ptr_factory_.GetWeakPtr()));
// No instance is returned if there are no eligible loaded profiles. Another
// search will be attempted in OnProfileAdded() if another profile appears on
// the scene.
if (!last_download_finder_)
last_download_begin_ = base::TimeTicks();
}
bool IncidentReportingService::WaitingForMostRecentDownload() {
DCHECK(report_); // Only call this when a report is being assembled.
// The easy case: not waiting if a download has already been found.
if (report_->has_download())
return false;
// The next easy case: waiting if the finder is operating.
if (last_download_finder_)
return true;
// The harder case: waiting if a profile has not yet been added.
for (ProfileContextCollection::const_iterator scan = profiles_.begin();
scan != profiles_.end();
++scan) {
if (!scan->second->added)
return true;
}
// There is no most recent download and there's nothing more to wait for.
return false;
}
void IncidentReportingService::CancelDownloadCollection() {
last_download_finder_.reset();
last_download_begin_ = base::TimeTicks();
if (report_)
report_->clear_download();
}
void IncidentReportingService::OnLastDownloadFound(
scoped_ptr<ClientIncidentReport_DownloadDetails> last_download) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(report_);
UMA_HISTOGRAM_TIMES("SBIRS.FindDownloadedBinaryTime",
base::TimeTicks::Now() - last_download_begin_);
last_download_begin_ = base::TimeTicks();
// Harvest the finder.
last_download_finder_.reset();
if (last_download)
report_->set_allocated_download(last_download.release());
UploadIfCollectionComplete();
}
void IncidentReportingService::UploadIfCollectionComplete() {
DCHECK(report_);
// Bail out if there are still outstanding collection tasks. Completion of any
// of these will start another upload attempt.
if (WaitingForEnvironmentCollection() ||
WaitingToCollateIncidents() ||
WaitingForMostRecentDownload()) {
return;
}
// Take ownership of the report and clear things for future reports.
scoped_ptr<ClientIncidentReport> report(report_.Pass());
first_incident_time_ = base::Time();
last_incident_time_ = base::TimeTicks();
// Drop the report if no executable download was found.
if (!report->has_download()) {
UMA_HISTOGRAM_ENUMERATION("SBIRS.UploadResult",
IncidentReportUploader::UPLOAD_NO_DOWNLOAD,
IncidentReportUploader::NUM_UPLOAD_RESULTS);
return;
}
ClientIncidentReport_EnvironmentData_Process* process =
report->mutable_environment()->mutable_process();
// Not all platforms have a metrics reporting preference.
if (g_browser_process->local_state()->FindPreference(
prefs::kMetricsReportingEnabled)) {
process->set_metrics_consent(g_browser_process->local_state()->GetBoolean(
prefs::kMetricsReportingEnabled));
}
// Find the profile that benefits from the strongest protections.
Profile* eligible_profile = FindEligibleProfile();
process->set_extended_consent(
eligible_profile ? eligible_profile->GetPrefs()->GetBoolean(
prefs::kSafeBrowsingExtendedReportingEnabled) :
false);
// Associate process-wide incidents with the profile that benefits from the
// strongest safe browsing protections.
ProfileContext* null_context = GetProfileContext(NULL);
if (null_context && !null_context->incidents.empty() && eligible_profile) {
ProfileContext* eligible_context = GetProfileContext(eligible_profile);
// Move the incidents to the target context.
eligible_context->incidents.insert(eligible_context->incidents.end(),
null_context->incidents.begin(),
null_context->incidents.end());
null_context->incidents.weak_clear();
}
// Collect incidents across all profiles participating in safe browsing. Drop
// incidents if the profile stopped participating before collection completed.
// Prune previously submitted incidents.
// Associate the profiles and their incident data with the upload.
size_t prune_count = 0;
UploadContext::PersistentIncidentStateCollection profiles_to_state;
for (ProfileContextCollection::iterator scan = profiles_.begin();
scan != profiles_.end();
++scan) {
// Bypass process-wide incidents that have not yet been associated with a
// profile.
if (!scan->first)
continue;
PrefService* prefs = scan->first->GetPrefs();
ProfileContext* context = scan->second;
if (context->incidents.empty())
continue;
if (!prefs->GetBoolean(prefs::kSafeBrowsingEnabled)) {
for (size_t i = 0; i < context->incidents.size(); ++i) {
LogIncidentDataType(DROPPED, *context->incidents[i]);
}
context->incidents.clear();
continue;
}
std::vector<PersistentIncidentState> states;
const base::DictionaryValue* incidents_sent =
prefs->GetDictionary(prefs::kSafeBrowsingIncidentsSent);
// Prep persistent data and prune any incidents already sent.
for (size_t i = 0; i < context->incidents.size(); ++i) {
ClientIncidentReport_IncidentData* incident = context->incidents[i];
const PersistentIncidentState state = ComputeIncidentState(*incident);
if (IncidentHasBeenReported(incidents_sent, state)) {
++prune_count;
delete context->incidents[i];
context->incidents[i] = NULL;
} else {
states.push_back(state);
}
}
if (prefs->GetBoolean(prefs::kSafeBrowsingIncidentReportSent)) {
// Prune all incidents as if they had been reported, migrating to the new
// technique. TODO(grt): remove this branch after it has shipped.
for (size_t i = 0; i < context->incidents.size(); ++i) {
if (context->incidents[i])
++prune_count;
}
context->incidents.clear();
prefs->ClearPref(prefs::kSafeBrowsingIncidentReportSent);
DictionaryPrefUpdate pref_update(prefs,
prefs::kSafeBrowsingIncidentsSent);
MarkIncidentsAsReported(states, pref_update.Get());
} else {
for (size_t i = 0; i < context->incidents.size(); ++i) {
ClientIncidentReport_IncidentData* incident = context->incidents[i];
if (incident) {
LogIncidentDataType(ACCEPTED, *incident);
// Ownership of the incident is passed to the report.
report->mutable_incident()->AddAllocated(incident);
}
}
context->incidents.weak_clear();
std::vector<PersistentIncidentState>& profile_states =
profiles_to_state[scan->first];
profile_states.swap(states);
}
}
const int count = report->incident_size();
// Abandon the request if all incidents were dropped with none pruned.
if (!count && !prune_count)
return;
UMA_HISTOGRAM_COUNTS_100("SBIRS.IncidentCount", count + prune_count);
{
double prune_pct = static_cast<double>(prune_count);
prune_pct = prune_pct * 100.0 / (count + prune_count);
prune_pct = round(prune_pct);
UMA_HISTOGRAM_PERCENTAGE("SBIRS.PruneRatio", static_cast<int>(prune_pct));
}
// Abandon the report if all incidents were pruned.
if (!count)
return;
scoped_ptr<UploadContext> context(new UploadContext(report.Pass()));
context->profiles_to_state.swap(profiles_to_state);
if (!database_manager_) {
// No database manager during testing. Take ownership of the context and
// continue processing.
UploadContext* temp_context = context.get();
uploads_.push_back(context.release());
IncidentReportingService::OnKillSwitchResult(temp_context, false);
} else {
if (content::BrowserThread::PostTaskAndReplyWithResult(
content::BrowserThread::IO,
FROM_HERE,
base::Bind(&SafeBrowsingDatabaseManager::IsCsdWhitelistKillSwitchOn,
database_manager_),
base::Bind(&IncidentReportingService::OnKillSwitchResult,
weak_ptr_factory_.GetWeakPtr(),
context.get()))) {
uploads_.push_back(context.release());
} // else should not happen. Let the context be deleted automatically.
}
}
void IncidentReportingService::CancelAllReportUploads() {
for (size_t i = 0; i < uploads_.size(); ++i) {
UMA_HISTOGRAM_ENUMERATION("SBIRS.UploadResult",
IncidentReportUploader::UPLOAD_CANCELLED,
IncidentReportUploader::NUM_UPLOAD_RESULTS);
}
uploads_.clear();
}
void IncidentReportingService::OnKillSwitchResult(UploadContext* context,
bool is_killswitch_on) {
DCHECK(thread_checker_.CalledOnValidThread());
if (!is_killswitch_on) {
// Initiate the upload.
context->uploader =
StartReportUpload(
base::Bind(&IncidentReportingService::OnReportUploadResult,
weak_ptr_factory_.GetWeakPtr(),
context),
url_request_context_getter_,
*context->report).Pass();
if (!context->uploader) {
OnReportUploadResult(context,
IncidentReportUploader::UPLOAD_INVALID_REQUEST,
scoped_ptr<ClientIncidentResponse>());
}
} else {
OnReportUploadResult(context,
IncidentReportUploader::UPLOAD_SUPPRESSED,
scoped_ptr<ClientIncidentResponse>());
}
}
void IncidentReportingService::HandleResponse(const UploadContext& context) {
for (UploadContext::PersistentIncidentStateCollection::const_iterator scan =
context.profiles_to_state.begin();
scan != context.profiles_to_state.end();
++scan) {
DictionaryPrefUpdate pref_update(scan->first->GetPrefs(),
prefs::kSafeBrowsingIncidentsSent);
MarkIncidentsAsReported(scan->second, pref_update.Get());
}
}
void IncidentReportingService::OnReportUploadResult(
UploadContext* context,
IncidentReportUploader::Result result,
scoped_ptr<ClientIncidentResponse> response) {
DCHECK(thread_checker_.CalledOnValidThread());
UMA_HISTOGRAM_ENUMERATION(
"SBIRS.UploadResult", result, IncidentReportUploader::NUM_UPLOAD_RESULTS);
// The upload is no longer outstanding, so take ownership of the context (from
// the collection of outstanding uploads) in this scope.
ScopedVector<UploadContext>::iterator it(
std::find(uploads_.begin(), uploads_.end(), context));
DCHECK(it != uploads_.end());
scoped_ptr<UploadContext> upload(context); // == *it
*it = uploads_.back();
uploads_.weak_erase(uploads_.end() - 1);
if (result == IncidentReportUploader::UPLOAD_SUCCESS)
HandleResponse(*upload);
// else retry?
}
void IncidentReportingService::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
switch (type) {
case chrome::NOTIFICATION_PROFILE_ADDED: {
Profile* profile = content::Source<Profile>(source).ptr();
if (!profile->IsOffTheRecord())
OnProfileAdded(profile);
break;
}
case chrome::NOTIFICATION_PROFILE_DESTROYED: {
Profile* profile = content::Source<Profile>(source).ptr();
if (!profile->IsOffTheRecord())
OnProfileDestroyed(profile);
break;
}
default:
break;
}
}
} // namespace safe_browsing