| // 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 |