| // 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_service.h" |
| |
| #include <math.h> |
| |
| #include <algorithm> |
| #include <vector> |
| |
| #include "base/metrics/histogram.h" |
| #include "base/prefs/pref_service.h" |
| #include "base/process/process_info.h" |
| #include "base/stl_util.h" |
| #include "base/threading/sequenced_worker_pool.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/environment_data_collection.h" |
| #include "chrome/browser/safe_browsing/incident_report_uploader_impl.h" |
| #include "chrome/browser/safe_browsing/preference_validation_delegate.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 { |
| |
| enum IncidentType { |
| // Start with 1 rather than zero; otherwise there won't be enough buckets for |
| // the histogram. |
| TRACKED_PREFERENCE = 1, |
| NUM_INCIDENT_TYPES |
| }; |
| |
| enum IncidentDisposition { |
| DROPPED, |
| ACCEPTED, |
| }; |
| |
| const int64 kDefaultUploadDelayMs = 1000 * 60; // one minute |
| |
| void LogIncidentDataType( |
| IncidentDisposition disposition, |
| const ClientIncidentReport_IncidentData& incident_data) { |
| IncidentType type = TRACKED_PREFERENCE; |
| |
| // Add a switch statement once other types are supported. |
| DCHECK(incident_data.has_tracked_preference()); |
| |
| 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); |
| } |
| } |
| |
| } // namespace |
| |
| struct IncidentReportingService::ProfileContext { |
| ProfileContext(); |
| ~ProfileContext(); |
| |
| // The incidents collected for this profile pending creation and/or upload. |
| ScopedVector<ClientIncidentReport_IncidentData> incidents; |
| |
| // False until PROFILE_CREATED notification is received. |
| bool created; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(ProfileContext); |
| }; |
| |
| class IncidentReportingService::UploadContext { |
| public: |
| 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; |
| |
| // The set of profiles from which incidents in |report| originated. |
| std::vector<Profile*> profiles; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(UploadContext); |
| }; |
| |
| IncidentReportingService::ProfileContext::ProfileContext() : created() { |
| } |
| |
| 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_(), |
| collection_timeout_pending_(), |
| upload_timer_(FROM_HERE, |
| base::TimeDelta::FromMilliseconds(kDefaultUploadDelayMs), |
| this, |
| &IncidentReportingService::OnCollectionTimeout), |
| receiver_weak_ptr_factory_(this), |
| weak_ptr_factory_(this) { |
| notification_registrar_.Add(this, |
| chrome::NOTIFICATION_PROFILE_CREATED, |
| 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(); |
| CancelAllReportUploads(); |
| |
| STLDeleteValues(&profiles_); |
| } |
| |
| AddIncidentCallback IncidentReportingService::GetAddIncidentCallback( |
| Profile* profile) { |
| // Force the context to be created so that incidents added before |
| // OnProfileCreated 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::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::OnProfileCreated(Profile* profile) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| ProfileContext* context = GetOrCreateProfileContext(profile); |
| context->created = true; |
| |
| // Drop all incidents if this profile is not participating in safe browsing. |
| if (!context->incidents.empty() && |
| !profile->GetPrefs()->GetBoolean(prefs::kSafeBrowsingEnabled)) { |
| for (size_t i = 0; i < context->incidents.size(); ++i) { |
| LogIncidentDataType(DROPPED, *context->incidents[i]); |
| } |
| context->incidents.clear(); |
| } |
| } |
| |
| scoped_ptr<IncidentReportUploader> IncidentReportingService::StartReportUpload( |
| const IncidentReportUploader::OnResultCallback& callback, |
| const scoped_refptr<net::URLRequestContextGetter>& request_context_getter, |
| const ClientIncidentReport& report) { |
| #if 0 |
| return IncidentReportUploaderImpl::UploadReport( |
| callback, request_context_getter, report).Pass(); |
| #else |
| // TODO(grt): Remove this temporary suppression of all uploads. |
| return scoped_ptr<IncidentReportUploader>(); |
| #endif |
| } |
| |
| 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 any pending uploads. |
| for (size_t i = 0; i < uploads_.size(); ++i) { |
| UploadContext* upload = uploads_[i]; |
| std::vector<Profile*>::iterator it = |
| std::find(upload->profiles.begin(), upload->profiles.end(), profile); |
| if (it != upload->profiles.end()) { |
| *it = upload->profiles.back(); |
| upload->profiles.resize(upload->profiles.size() - 1); |
| break; |
| } |
| } |
| } |
| |
| void IncidentReportingService::AddIncident( |
| Profile* profile, |
| scoped_ptr<ClientIncidentReport_IncidentData> incident_data) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| // Incidents outside the context of a profile are not supported at the moment. |
| DCHECK(profile); |
| |
| ProfileContext* context = GetProfileContext(profile); |
| // It is forbidden to call this function with a destroyed profile. |
| DCHECK(context); |
| |
| // Drop the incident immediately if profile creation has completed and the |
| // profile is not participating in safe browsing. |
| if (context->created && |
| !profile->GetPrefs()->GetBoolean(prefs::kSafeBrowsingEnabled)) { |
| LogIncidentDataType(DROPPED, *incident_data); |
| return; |
| } |
| |
| // Create a new report if this is the first incident ever or first since last |
| // upload. |
| if (!report_) { |
| report_.reset(new ClientIncidentReport()); |
| first_incident_time_ = base::Time::Now(); |
| } |
| |
| // Provide time to the new incident if the caller didn't provide it. |
| 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()); |
| |
| 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. |
| |
| // Restart the delay timer to send the report upon expiration. |
| collection_timeout_pending_ = true; |
| upload_timer_.Reset(); |
| |
| BeginEnvironmentCollection(); |
| } |
| |
| void IncidentReportingService::BeginEnvironmentCollection() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK(report_); |
| 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_); |
| } |
| |
| 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 |
| first_incident_time_ = base::Time(); |
| |
| report_->set_allocated_environment(environment_data.release()); |
| |
| UMA_HISTOGRAM_TIMES("SBIRS.EnvCollectionTime", |
| base::TimeTicks::Now() - environment_collection_begin_); |
| environment_collection_begin_ = base::TimeTicks(); |
| |
| UploadIfCollectionComplete(); |
| } |
| |
| void IncidentReportingService::CancelIncidentCollection() { |
| collection_timeout_pending_ = false; |
| last_incident_time_ = base::TimeTicks(); |
| report_.reset(); |
| } |
| |
| void IncidentReportingService::OnCollectionTimeout() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| // Exit early if collection was cancelled. |
| if (!collection_timeout_pending_) |
| return; |
| |
| // Wait another round if 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->second->created && !scan->second->incidents.empty()) { |
| upload_timer_.Reset(); |
| return; |
| } |
| } |
| |
| collection_timeout_pending_ = false; |
| |
| UploadIfCollectionComplete(); |
| } |
| |
| void IncidentReportingService::CollectDownloadDetails( |
| ClientIncidentReport_DownloadDetails* download_details) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| // TODO(grt): collect download info; http://crbug.com/383042. |
| } |
| |
| void IncidentReportingService::UploadIfCollectionComplete() { |
| DCHECK(report_); |
| // Bail out if there are still outstanding collection tasks. |
| if (environment_collection_pending_ || collection_timeout_pending_) |
| return; |
| |
| // Take ownership of the report and clear things for future reports. |
| scoped_ptr<ClientIncidentReport> report(report_.Pass()); |
| last_incident_time_ = base::TimeTicks(); |
| |
| 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)); |
| } |
| |
| // Check for extended consent in any profile while collecting incidents. |
| process->set_extended_consent(false); |
| // Collect incidents across all profiles participating in safe browsing. Drop |
| // incidents if the profile stopped participating before collection completed. |
| // Prune incidents if the profile has already submitted any incidents. |
| // Associate the participating profiles with the upload. |
| size_t prune_count = 0; |
| std::vector<Profile*> profiles; |
| for (ProfileContextCollection::iterator scan = profiles_.begin(); |
| scan != profiles_.end(); |
| ++scan) { |
| PrefService* prefs = scan->first->GetPrefs(); |
| if (process && |
| prefs->GetBoolean(prefs::kSafeBrowsingExtendedReportingEnabled)) { |
| process->set_extended_consent(true); |
| process = NULL; // Don't check any more once one is found. |
| } |
| 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(); |
| } else if (prefs->GetBoolean(prefs::kSafeBrowsingIncidentReportSent)) { |
| // Prune all incidents. |
| // TODO(grt): Only prune previously submitted incidents; |
| // http://crbug.com/383043. |
| prune_count += context->incidents.size(); |
| context->incidents.clear(); |
| } else { |
| for (size_t i = 0; i < context->incidents.size(); ++i) { |
| ClientIncidentReport_IncidentData* incident = context->incidents[i]; |
| LogIncidentDataType(ACCEPTED, *incident); |
| // Ownership of the incident is passed to the report. |
| report->mutable_incident()->AddAllocated(incident); |
| } |
| context->incidents.weak_clear(); |
| profiles.push_back(scan->first); |
| } |
| } |
| |
| 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.swap(profiles); |
| 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 (size_t i = 0; i < context.profiles.size(); ++i) { |
| context.profiles[i]->GetPrefs()->SetBoolean( |
| prefs::kSafeBrowsingIncidentReportSent, true); |
| } |
| } |
| |
| 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_CREATED: { |
| Profile* profile = content::Source<Profile>(source).ptr(); |
| if (!profile->IsOffTheRecord()) |
| OnProfileCreated(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 |