diff options
author | grt@chromium.org <grt@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-08-07 11:05:31 +0000 |
---|---|---|
committer | grt@chromium.org <grt@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-08-07 11:05:31 +0000 |
commit | f3fcec39cf71c69e80c23f088b207ca4a419df05 (patch) | |
tree | 1be935a34430d05d96f0c35375228feea628237e | |
parent | 891f17cd94abd488fe06f9dd342acbfb8859f95d (diff) | |
download | chromium_src-f3fcec39cf71c69e80c23f088b207ca4a419df05.zip chromium_src-f3fcec39cf71c69e80c23f088b207ca4a419df05.tar.gz chromium_src-f3fcec39cf71c69e80c23f088b207ca4a419df05.tar.bz2 |
Support for process-wide incidents in the safe browsing incident reporting service.
BUG=399428
R=robertshield@chromium.org
Review URL: https://codereview.chromium.org/441453002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@288028 0039d316-1c4b-4281-b951-d872f2087c98
11 files changed, 761 insertions, 58 deletions
diff --git a/chrome/browser/safe_browsing/delayed_analysis_callback.h b/chrome/browser/safe_browsing/delayed_analysis_callback.h new file mode 100644 index 0000000..e36282f --- /dev/null +++ b/chrome/browser/safe_browsing/delayed_analysis_callback.h @@ -0,0 +1,22 @@ +// 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. + +#ifndef CHROME_BROWSER_SAFE_BROWSING_DELAYED_ANALYSIS_CALLBACK_H_ +#define CHROME_BROWSER_SAFE_BROWSING_DELAYED_ANALYSIS_CALLBACK_H_ + +#include "base/callback_forward.h" +#include "chrome/browser/safe_browsing/add_incident_callback.h" + +namespace safe_browsing { + +// A callback used by external components to register a process-wide analysis +// step. The callback will be run after some delay following process launch in +// the blocking pool. The argument is a callback by which the consumer can add +// incidents to the incident reporting service. +typedef base::Callback<void(const AddIncidentCallback&)> + DelayedAnalysisCallback; + +} // namespace safe_browsing + +#endif // CHROME_BROWSER_SAFE_BROWSING_DELAYED_ANALYSIS_CALLBACK_H_ diff --git a/chrome/browser/safe_browsing/delayed_callback_runner.cc b/chrome/browser/safe_browsing/delayed_callback_runner.cc new file mode 100644 index 0000000..f344459 --- /dev/null +++ b/chrome/browser/safe_browsing/delayed_callback_runner.cc @@ -0,0 +1,51 @@ +// 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/delayed_callback_runner.h" + +#include "base/location.h" + +namespace safe_browsing { + +DelayedCallbackRunner::DelayedCallbackRunner( + base::TimeDelta delay, + const scoped_refptr<base::TaskRunner>& task_runner) + : task_runner_(task_runner), + next_callback_(callbacks_.end()), + timer_(FROM_HERE, delay, this, &DelayedCallbackRunner::OnTimer) { +} + +DelayedCallbackRunner::~DelayedCallbackRunner() { +} + +void DelayedCallbackRunner::RegisterCallback(const base::Closure& callback) { + DCHECK(thread_checker_.CalledOnValidThread()); + callbacks_.push_back(callback); +} + +void DelayedCallbackRunner::Start() { + DCHECK(thread_checker_.CalledOnValidThread()); + + // Nothing to do if the runner is already running or nothing has been added. + if (next_callback_ != callbacks_.end() || callbacks_.empty()) + return; + + // Prime the system with the first callback. + next_callback_ = callbacks_.begin(); + + // Point the starter pistol in the air and pull the trigger. + timer_.Reset(); +} + +void DelayedCallbackRunner::OnTimer() { + // Run the next callback on the task runner. + task_runner_->PostTask(FROM_HERE, *next_callback_); + + // Remove this callback and get ready for the next if there is one. + next_callback_ = callbacks_.erase(next_callback_); + if (next_callback_ != callbacks_.end()) + timer_.Reset(); +} + +} // namespace safe_browsing diff --git a/chrome/browser/safe_browsing/delayed_callback_runner.h b/chrome/browser/safe_browsing/delayed_callback_runner.h new file mode 100644 index 0000000..ff37837 --- /dev/null +++ b/chrome/browser/safe_browsing/delayed_callback_runner.h @@ -0,0 +1,66 @@ +// 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. + +#ifndef CHROME_BROWSER_SAFE_BROWSING_DELAYED_CALLBACK_RUNNER_H_ +#define CHROME_BROWSER_SAFE_BROWSING_DELAYED_CALLBACK_RUNNER_H_ + +#include <list> + +#include "base/callback_forward.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/task_runner.h" +#include "base/threading/thread_checker.h" +#include "base/time/time.h" +#include "base/timer/timer.h" + +namespace safe_browsing { + +// Runs callbacks on a given task runner, waiting a certain amount of time +// between each. The delay also applies to running the first callback (i.e., +// the first callback will be run some time after Start() is invoked). Callbacks +// are deleted after they are run. Start() is idempotent: calling it while the +// runner is doing its job has no effect. +class DelayedCallbackRunner { + public: + // Constructs an instance that runs tasks on |callback_runner|, waiting for + // |delay| time to pass before and between each callback. + DelayedCallbackRunner(base::TimeDelta delay, + const scoped_refptr<base::TaskRunner>& task_runner); + ~DelayedCallbackRunner(); + + // Registers |callback| with the runner. A copy of |callback| is held until it + // is run. + void RegisterCallback(const base::Closure& callback); + + // Starts running the callbacks after the delay. + void Start(); + + private: + typedef std::list<base::Closure> CallbackList; + + // A callback invoked by the timer to run the next callback. The timer is + // restarted to process the next callback if there is one. + void OnTimer(); + + base::ThreadChecker thread_checker_; + + // The runner on which callbacks are to be run. + scoped_refptr<base::TaskRunner> task_runner_; + + // The list of callbacks to run. Callbacks are removed when run. + CallbackList callbacks_; + + // callbacks_.end() when no work is being done. Any other value otherwise. + CallbackList::iterator next_callback_; + + // A timer upon the firing of which the next callback will be run. + base::DelayTimer<DelayedCallbackRunner> timer_; + + DISALLOW_COPY_AND_ASSIGN(DelayedCallbackRunner); +}; + +} // namespace safe_browsing + +#endif // CHROME_BROWSER_SAFE_BROWSING_DELAYED_CALLBACK_RUNNER_H_ diff --git a/chrome/browser/safe_browsing/delayed_callback_runner_unittest.cc b/chrome/browser/safe_browsing/delayed_callback_runner_unittest.cc new file mode 100644 index 0000000..c1b4a79 --- /dev/null +++ b/chrome/browser/safe_browsing/delayed_callback_runner_unittest.cc @@ -0,0 +1,160 @@ +// 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/delayed_callback_runner.h" + +#include <map> +#include <string> + +#include "base/callback.h" +#include "base/macros.h" +#include "base/test/test_simple_task_runner.h" +#include "base/thread_task_runner_handle.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// A class of objects that invoke a callback upon destruction. This is used as +// an owned argument on callbacks given to a DelayedCallbackRunner under test. +class CallbackArgument { + public: + explicit CallbackArgument(const base::Closure& on_delete) + : on_delete_(on_delete) {} + ~CallbackArgument() { on_delete_.Run(); } + + private: + base::Closure on_delete_; + + DISALLOW_COPY_AND_ASSIGN(CallbackArgument); +}; + +} // namespace + +// A test fixture that prepares a DelayedCallbackRunner instance for use and +// tracks the lifecycle of callbacks sent to it. +class DelayedCallbackRunnerTest : public testing::Test { + public: + // Registers a callback that will record its running and destruction to the + // test fixture under the given name. + void RegisterTestCallback(const std::string& name) { + callbacks_[name] = CallbackState(); + instance_->RegisterCallback(MakeCallback(name)); + } + + protected: + DelayedCallbackRunnerTest() + : task_runner_(new base::TestSimpleTaskRunner), + thread_task_runner_handle_(task_runner_) {} + + virtual void SetUp() OVERRIDE { + instance_.reset(new safe_browsing::DelayedCallbackRunner( + base::TimeDelta::FromMilliseconds(1), // ignored by simple runner. + task_runner_)); + } + + virtual void TearDown() OVERRIDE { instance_.reset(); } + + void OnRun(const std::string& name, CallbackArgument* arg) { + EXPECT_FALSE(callbacks_[name].run); + callbacks_[name].run = true; + } + + void OnDelete(const std::string& name) { + EXPECT_FALSE(callbacks_[name].deleted); + callbacks_[name].deleted = true; + } + + // Returns a callback argument that calls the test fixture's OnDelete method + // on behalf of the given callback name. + scoped_ptr<CallbackArgument> MakeCallbackArgument(const std::string& name) { + return make_scoped_ptr(new CallbackArgument(base::Bind( + &DelayedCallbackRunnerTest::OnDelete, base::Unretained(this), name))); + } + + // Returns a closure that calls |OnRun| when run and |OnDelete| when deleted + // on behalf of the given callback name. + base::Closure MakeCallback(const std::string& name) { + return base::Bind(&DelayedCallbackRunnerTest::OnRun, + base::Unretained(this), + name, + base::Owned(MakeCallbackArgument(name).release())); + } + + bool CallbackWasRun(const std::string& name) { return callbacks_[name].run; } + + bool CallbackWasDeleted(const std::string& name) { + return callbacks_[name].deleted; + } + + scoped_refptr<base::TestSimpleTaskRunner> task_runner_; + base::ThreadTaskRunnerHandle thread_task_runner_handle_; + scoped_ptr<safe_browsing::DelayedCallbackRunner> instance_; + + private: + struct CallbackState { + CallbackState() : run(), deleted() {} + bool run; + bool deleted; + }; + + std::map<std::string, CallbackState> callbacks_; +}; + +// Tests that a callback is deleted when not run before the runner is destroyed. +TEST_F(DelayedCallbackRunnerTest, NotRunDeleted) { + const std::string name("one"); + RegisterTestCallback(name); + instance_.reset(); + EXPECT_FALSE(CallbackWasRun(name)); + EXPECT_TRUE(CallbackWasDeleted(name)); +} + +// Tests that a callback is run and deleted while the runner is alive. +TEST_F(DelayedCallbackRunnerTest, RunDeleted) { + const std::string name("one"); + RegisterTestCallback(name); + instance_->Start(); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(CallbackWasRun(name)); + EXPECT_TRUE(CallbackWasDeleted(name)); +} + +// Tests that a callback registered after Start() is called is also run and +// deleted. +TEST_F(DelayedCallbackRunnerTest, AddWhileRunningRun) { + const std::string name("one"); + const std::string name2("two"); + + // Post a task to register a new callback after Start() is called. + task_runner_->PostTask( + FROM_HERE, + base::Bind(&DelayedCallbackRunnerTest::RegisterTestCallback, + base::Unretained(this), + name2)); + + RegisterTestCallback(name); + instance_->Start(); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(CallbackWasRun(name)); + EXPECT_TRUE(CallbackWasDeleted(name)); + EXPECT_TRUE(CallbackWasRun(name2)); + EXPECT_TRUE(CallbackWasDeleted(name2)); +} + +TEST_F(DelayedCallbackRunnerTest, MultipleRuns) { + const std::string name("one"); + const std::string name2("two"); + + RegisterTestCallback(name); + instance_->Start(); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(CallbackWasRun(name)); + EXPECT_TRUE(CallbackWasDeleted(name)); + + RegisterTestCallback(name2); + instance_->Start(); + task_runner_->RunUntilIdle(); + EXPECT_TRUE(CallbackWasRun(name2)); + EXPECT_TRUE(CallbackWasDeleted(name2)); +} diff --git a/chrome/browser/safe_browsing/incident_reporting_service.cc b/chrome/browser/safe_browsing/incident_reporting_service.cc index 6c8ce1b..51df03b 100644 --- a/chrome/browser/safe_browsing/incident_reporting_service.cc +++ b/chrome/browser/safe_browsing/incident_reporting_service.cc @@ -13,8 +13,10 @@ #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" @@ -70,6 +72,9 @@ struct PersistentIncidentState { // 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) { @@ -157,6 +162,19 @@ void MarkIncidentsAsReported(const std::vector<PersistentIncidentState>& states, } } +// 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 { @@ -220,11 +238,16 @@ IncidentReportingService::IncidentReportingService( ->GetTaskRunnerWithShutdownBehavior( base::SequencedWorkerPool::SKIP_ON_SHUTDOWN)), environment_collection_pending_(), - collection_timeout_pending_(), - upload_timer_(FROM_HERE, - base::TimeDelta::FromMilliseconds(kDefaultUploadDelayMs), - this, - &IncidentReportingService::OnCollectionTimeout), + 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, @@ -270,6 +293,56 @@ IncidentReportingService::CreatePreferenceValidationDelegate(Profile* profile) { 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) { @@ -294,17 +367,33 @@ void IncidentReportingService::OnProfileAdded(Profile* profile) { 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)) + 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 received prior to creation if the 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) { + // 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(); } @@ -362,35 +451,51 @@ void IncidentReportingService::OnProfileDestroyed(Profile* profile) { 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()); - // Incidents outside the context of a profile are not supported at the moment. - DCHECK(profile); 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 profile creation has completed and the - // profile is not participating in safe browsing. Preference evaluation is - // deferred until OnProfileAdded() if profile creation has not yet - // completed. + // 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; } - // Start assembling a new report if this is the first incident ever or the - // first since the 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 do so. if (!incident_data->has_incident_time_msec()) incident_data->set_incident_time_msec(base::Time::Now().ToJavaTime()); @@ -398,6 +503,10 @@ void IncidentReportingService::AddIncident( // 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_); @@ -406,14 +515,30 @@ void IncidentReportingService::AddIncident( // Persist the incident data. - // Restart the delay timer to send the report upon expiration. - collection_timeout_pending_ = true; - upload_timer_.Reset(); + // 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_); @@ -462,7 +587,6 @@ void IncidentReportingService::OnEnvironmentDataCollected( 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()); @@ -474,34 +598,35 @@ void IncidentReportingService::OnEnvironmentDataCollected( } bool IncidentReportingService::WaitingToCollateIncidents() { - return collection_timeout_pending_; + return collation_timeout_pending_; } void IncidentReportingService::CancelIncidentCollection() { - collection_timeout_pending_ = false; + collation_timeout_pending_ = false; last_incident_time_ = base::TimeTicks(); report_.reset(); } -void IncidentReportingService::OnCollectionTimeout() { +void IncidentReportingService::OnCollationTimeout() { DCHECK(thread_checker_.CalledOnValidThread()); // Exit early if collection was cancelled. - if (!collection_timeout_pending_) + if (!collation_timeout_pending_) return; - // Wait another round if incidents have come in from a profile that has yet to - // complete creation. + // 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->second->added && !scan->second->incidents.empty()) { - upload_timer_.Reset(); + if (scan->first && !scan->second->added && + !scan->second->incidents.empty()) { + collation_timer_.Reset(); return; } } - collection_timeout_pending_ = false; + collation_timeout_pending_ = false; UploadIfCollectionComplete(); } @@ -581,6 +706,7 @@ void IncidentReportingService::UploadIfCollectionComplete() { // 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. @@ -601,8 +727,25 @@ void IncidentReportingService::UploadIfCollectionComplete() { prefs::kMetricsReportingEnabled)); } - // Check for extended consent in any profile while collecting incidents. - process->set_extended_consent(false); + // 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. @@ -612,12 +755,11 @@ void IncidentReportingService::UploadIfCollectionComplete() { 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(); - 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; diff --git a/chrome/browser/safe_browsing/incident_reporting_service.h b/chrome/browser/safe_browsing/incident_reporting_service.h index bd02472..196885f 100644 --- a/chrome/browser/safe_browsing/incident_reporting_service.h +++ b/chrome/browser/safe_browsing/incident_reporting_service.h @@ -19,6 +19,8 @@ #include "base/time/time.h" #include "base/timer/timer.h" #include "chrome/browser/safe_browsing/add_incident_callback.h" +#include "chrome/browser/safe_browsing/delayed_analysis_callback.h" +#include "chrome/browser/safe_browsing/delayed_callback_runner.h" #include "chrome/browser/safe_browsing/incident_report_uploader.h" #include "chrome/browser/safe_browsing/last_download_finder.h" #include "content/public/browser/notification_observer.h" @@ -54,11 +56,13 @@ class ClientIncidentReport_IncidentData; // begins operation when an incident is reported via the AddIncident method. // Incidents reported from a profile that is loading are held until the profile // is fully created. Incidents originating from profiles that do not participate -// in safe browsing are dropped. Following the addition of an incident that is -// not dropped, the service collects environmental data, finds the most recent -// binary download, and waits a bit. Additional incidents that arrive during -// this time are collated with the initial incident. Finally, already-reported -// incidents are pruned and any remaining are uploaded in an incident report. +// in safe browsing are dropped. Process-wide incidents are affiliated with a +// profile that participates in safe browsing when one becomes available. +// Following the addition of an incident that is not dropped, the service +// collects environmental data, finds the most recent binary download, and waits +// a bit. Additional incidents that arrive during this time are collated with +// the initial incident. Finally, already-reported incidents are pruned and any +// remaining are uploaded in an incident report. class IncidentReportingService : public content::NotificationObserver { public: IncidentReportingService(SafeBrowsingService* safe_browsing_service, @@ -83,11 +87,22 @@ class IncidentReportingService : public content::NotificationObserver { scoped_ptr<TrackedPreferenceValidationDelegate> CreatePreferenceValidationDelegate(Profile* profile); + // Registers |callback| to be run after some delay following process launch. + void RegisterDelayedAnalysisCallback(const DelayedAnalysisCallback& callback); + protected: // A pointer to a function that populates a protobuf with environment data. typedef void (*CollectEnvironmentDataFn)( ClientIncidentReport_EnvironmentData*); + // For testing so that the TaskRunner used for delayed analysis callbacks can + // be specified. + 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); + // Sets the function called by the service to collect environment data and the // task runner on which it is called. Used by unit tests to provide a fake // environment data collector. @@ -131,11 +146,23 @@ class IncidentReportingService : public content::NotificationObserver { // but not yet uploaded are dropped. void OnProfileDestroyed(Profile* profile); + // Returns an initialized profile that participates in safe browsing. Profiles + // participating in extended safe browsing are preferred. + Profile* FindEligibleProfile() const; + // Adds |incident_data| to the service. The incident_time_msec field is // populated with the current time if the caller has not already done so. void AddIncident(Profile* profile, scoped_ptr<ClientIncidentReport_IncidentData> incident_data); + // Begins processing a report. If processing is already underway, ensures that + // collection tasks have completed or are running. + void BeginReportProcessing(); + + // Begins the process of collating incidents by waiting for incidents to + // arrive. This function is idempotent. + void BeginIncidentCollation(); + // Starts a task to collect environment data in the blocking pool. void BeginEnvironmentCollection(); @@ -159,10 +186,11 @@ class IncidentReportingService : public content::NotificationObserver { // Cancels the collection timeout. void CancelIncidentCollection(); - // A callback invoked on the UI thread after which incident collection has + // A callback invoked on the UI thread after which incident collation has // completed. Incident report processing continues, either by waiting for - // environment data to arrive or by sending an incident report. - void OnCollectionTimeout(); + // environment data or the most recent download to arrive or by sending an + // incident report. + void OnCollationTimeout(); // Starts the asynchronous process of finding the most recent executable // download if one is not currently being search for and/or has not already @@ -234,12 +262,12 @@ class IncidentReportingService : public content::NotificationObserver { bool environment_collection_pending_; // True when an incident has been received and the service is waiting for the - // upload_timer_ to fire. - bool collection_timeout_pending_; + // collation_timer_ to fire. + bool collation_timeout_pending_; // A timer upon the firing of which the service will report received // incidents. - base::DelayTimer<IncidentReportingService> upload_timer_; + base::DelayTimer<IncidentReportingService> collation_timer_; // The report currently being assembled. This becomes non-NULL when an initial // incident is reported, and returns to NULL when the report is sent for @@ -258,9 +286,13 @@ class IncidentReportingService : public content::NotificationObserver { // The time at which download collection was initiated. base::TimeTicks last_download_begin_; - // Context data for all on-the-record profiles. + // Context data for all on-the-record profiles plus the process-wide (NULL) + // context. ProfileContextCollection profiles_; + // Callbacks registered for performing delayed analysis. + DelayedCallbackRunner delayed_analysis_callbacks_; + // The collection of uploads in progress. ScopedVector<UploadContext> uploads_; diff --git a/chrome/browser/safe_browsing/incident_reporting_service_unittest.cc b/chrome/browser/safe_browsing/incident_reporting_service_unittest.cc index d3dcc19..903002a 100644 --- a/chrome/browser/safe_browsing/incident_reporting_service_unittest.cc +++ b/chrome/browser/safe_browsing/incident_reporting_service_unittest.cc @@ -56,7 +56,10 @@ class IncidentReportingServiceTest : public testing::Test { const CollectEnvironmentCallback& collect_environment_callback, const CreateDownloadFinderCallback& create_download_finder_callback, const StartUploadCallback& start_upload_callback) - : IncidentReportingService(NULL, NULL), + : IncidentReportingService(NULL, + NULL, + base::TimeDelta::FromMilliseconds(5), + task_runner), pre_profile_add_callback_(pre_profile_add_callback), collect_environment_callback_(collect_environment_callback), create_download_finder_callback_(create_download_finder_callback), @@ -132,6 +135,13 @@ class IncidentReportingServiceTest : public testing::Test { ON_CREATE_DOWNLOAD_FINDER_NO_PROFILES, }; + // A type for specifying the action to be taken by the test fixture when its + // delayed analysis callback is run. + enum OnDelayedAnalysisAction { + ON_DELAYED_ANALYSIS_NO_ACTION, + ON_DELAYED_ANALYSIS_ADD_INCIDENT, // Add an incident to the service. + }; + static const int64 kIncidentTimeMsec; static const char kFakeOsName[]; static const char kFakeDownloadToken[]; @@ -153,11 +163,13 @@ class IncidentReportingServiceTest : public testing::Test { base::Unretained(this)))), on_create_download_finder_action_( ON_CREATE_DOWNLOAD_FINDER_DOWNLOAD_FOUND), + on_delayed_analysis_action_(ON_DELAYED_ANALYSIS_NO_ACTION), upload_result_(safe_browsing::IncidentReportUploader::UPLOAD_SUCCESS), environment_collected_(), download_finder_created_(), download_finder_destroyed_(), - uploader_destroyed_() {} + uploader_destroyed_(), + delayed_analysis_ran_() {} virtual void SetUp() OVERRIDE { testing::Test::SetUp(); @@ -224,6 +236,14 @@ class IncidentReportingServiceTest : public testing::Test { instance_->GetAddIncidentCallback(profile).Run(MakeTestIncident().Pass()); } + // Registers the callback to be run for delayed analysis. + void RegisterAnalysis(OnDelayedAnalysisAction on_delayed_analysis_action) { + on_delayed_analysis_action_ = on_delayed_analysis_action; + instance_->RegisterDelayedAnalysisCallback( + base::Bind(&IncidentReportingServiceTest::OnDelayedAnalysis, + base::Unretained(this))); + } + // Confirms that the test incident(s) was/were uploaded by the service, then // clears the instance for subsequent incidents. void ExpectTestIncidentUploaded(int incident_count) { @@ -256,6 +276,7 @@ class IncidentReportingServiceTest : public testing::Test { bool HasCreatedDownloadFinder() const { return download_finder_created_; } bool DownloadFinderDestroyed() const { return download_finder_destroyed_; } bool UploaderDestroyed() const { return uploader_destroyed_; } + bool DelayedAnalysisRan() const { return delayed_analysis_ran_; } scoped_refptr<base::TestSimpleTaskRunner> task_runner_; base::ThreadTaskRunnerHandle thread_task_runner_handle_; @@ -263,12 +284,14 @@ class IncidentReportingServiceTest : public testing::Test { scoped_ptr<safe_browsing::IncidentReportingService> instance_; base::Closure on_start_upload_callback_; OnCreateDownloadFinderAction on_create_download_finder_action_; + OnDelayedAnalysisAction on_delayed_analysis_action_; safe_browsing::IncidentReportUploader::Result upload_result_; bool environment_collected_; bool download_finder_created_; scoped_ptr<safe_browsing::ClientIncidentReport> uploaded_report_; bool download_finder_destroyed_; bool uploader_destroyed_; + bool delayed_analysis_ran_; private: // A fake IncidentReportUploader that posts a task to provide a given response @@ -435,6 +458,12 @@ class IncidentReportingServiceTest : public testing::Test { void OnDownloadFinderDestroyed() { download_finder_destroyed_ = true; } void OnUploaderDestroyed() { uploader_destroyed_ = true; } + void OnDelayedAnalysis(const safe_browsing::AddIncidentCallback& callback) { + delayed_analysis_ran_ = true; + if (on_delayed_analysis_action_ == ON_DELAYED_ANALYSIS_ADD_INCIDENT) + callback.Run(MakeTestIncident().Pass()); + } + // A mapping of profile name to its corresponding properties. std::map<std::string, ProfileProperties> profile_properties_; }; @@ -680,6 +709,188 @@ TEST_F(IncidentReportingServiceTest, MigrateOldPref) { AssertNoUpload(); } +// Tests that no upload results from adding an incident that is not affiliated +// with a profile. +TEST_F(IncidentReportingServiceTest, ProcessWideNoProfileNoUpload) { + // Add the test incident. + AddTestIncident(NULL); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // No upload should have taken place. + AssertNoUpload(); +} + +// Tests that there is an upload when a profile is present for a proc-wide +// incident and that pruning works. +TEST_F(IncidentReportingServiceTest, ProcessWideOneUpload) { + // Add a profile that participates in safe browsing. + CreateProfile( + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_NO_ACTION); + + // Add the test incident. + AddTestIncident(NULL); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // An upload should have taken place. + ExpectTestIncidentUploaded(1); + + // Add the incident to the service again. + AddTestIncident(NULL); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // Verify that no additional report upload took place. + AssertNoUpload(); +} + +// Tests that two process-wide incidents of the same type with different +// payloads added via the same callback lead to two uploads. +TEST_F(IncidentReportingServiceTest, ProcessWideTwoUploads) { + // Add a profile that participates in safe browsing. + CreateProfile( + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_NO_ACTION); + + // Add the test incident. + safe_browsing::AddIncidentCallback add_incident( + instance_->GetAddIncidentCallback(NULL)); + add_incident.Run(MakeTestIncident().Pass()); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // An upload should have taken place. + ExpectTestIncidentUploaded(1); + + // Add a variation on the incident to the service. + scoped_ptr<safe_browsing::ClientIncidentReport_IncidentData> incident( + MakeTestIncident()); + incident->mutable_tracked_preference()->set_atomic_value("leeches"); + add_incident.Run(incident.Pass()); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // Verify that an additional report upload took place. + ExpectTestIncidentUploaded(1); +} + +// Tests that there is an upload when a profile appears after a proc-wide +// incident. +TEST_F(IncidentReportingServiceTest, ProcessWideOneUploadAfterProfile) { + // Add the test incident. + AddTestIncident(NULL); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // Verify that no report upload took place. + AssertNoUpload(); + + // Add a profile that participates in safe browsing. + CreateProfile( + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_NO_ACTION); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // An upload should have taken place. + ExpectTestIncidentUploaded(1); +} + +// Tests that delayed analysis callbacks are called following the addition of a +// profile that participates in safe browsing. +TEST_F(IncidentReportingServiceTest, AnalysisAfterProfile) { + // Register a callback. + RegisterAnalysis(ON_DELAYED_ANALYSIS_NO_ACTION); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // Not run yet. + ASSERT_FALSE(DelayedAnalysisRan()); + + // Add a profile that participates in safe browsing. + CreateProfile( + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_NO_ACTION); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // And now they have. + ASSERT_TRUE(DelayedAnalysisRan()); +} + +// Tests that delayed analysis callbacks are called following their registration +// when a profile that participates in safe browsing is already present. +TEST_F(IncidentReportingServiceTest, AnalysisWhenRegisteredWithProfile) { + // Add a profile that participates in safe browsing. + CreateProfile( + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_NO_ACTION); + + // Register a callback. + RegisterAnalysis(ON_DELAYED_ANALYSIS_NO_ACTION); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // Confirm that the callbacks were run. + ASSERT_TRUE(DelayedAnalysisRan()); +} + +// Tests that no upload results from a delayed analysis incident when no +// safe browsing profile is present. +TEST_F(IncidentReportingServiceTest, DelayedAnalysisNoProfileNoUpload) { + // Register a callback that will add an incident. + RegisterAnalysis(ON_DELAYED_ANALYSIS_ADD_INCIDENT); + + // Add a profile that does not participate in safe browsing. + CreateProfile( + "profile1", SAFE_BROWSING_OPT_OUT, ON_PROFILE_ADDITION_NO_ACTION); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // The callback should not have been run. + ASSERT_FALSE(DelayedAnalysisRan()); + + // No upload should have taken place. + AssertNoUpload(); +} + +// Tests that there is an upload when a profile is present for a delayed +// analysis incident and that pruning works. +TEST_F(IncidentReportingServiceTest, DelayedAnalysisOneUpload) { + // Register a callback that will add an incident. + RegisterAnalysis(ON_DELAYED_ANALYSIS_ADD_INCIDENT); + + // Add a profile that participates in safe browsing. + CreateProfile( + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_NO_ACTION); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // The callback should have been run. + ASSERT_TRUE(DelayedAnalysisRan()); + + // An upload should have taken place. + ExpectTestIncidentUploaded(1); + + // Add the incident to the service again. + AddTestIncident(NULL); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // Verify that no additional report upload took place. + AssertNoUpload(); +} + // Parallel uploads // Shutdown during processing // environment colection taking longer than incident delay timer diff --git a/chrome/browser/safe_browsing/safe_browsing_service.cc b/chrome/browser/safe_browsing/safe_browsing_service.cc index 277b910..71b7b13 100644 --- a/chrome/browser/safe_browsing/safe_browsing_service.cc +++ b/chrome/browser/safe_browsing/safe_browsing_service.cc @@ -332,6 +332,14 @@ SafeBrowsingService::CreatePreferenceValidationDelegate( return scoped_ptr<TrackedPreferenceValidationDelegate>(); } +void SafeBrowsingService::RegisterDelayedAnalysisCallback( + const safe_browsing::DelayedAnalysisCallback& callback) { +#if defined(FULL_SAFE_BROWSING) + if (incident_service_) + incident_service_->RegisterDelayedAnalysisCallback(callback); +#endif +} + SafeBrowsingUIManager* SafeBrowsingService::CreateUIManager() { return new SafeBrowsingUIManager(this); } diff --git a/chrome/browser/safe_browsing/safe_browsing_service.h b/chrome/browser/safe_browsing/safe_browsing_service.h index 2fbfbc3..6254d51 100644 --- a/chrome/browser/safe_browsing/safe_browsing_service.h +++ b/chrome/browser/safe_browsing/safe_browsing_service.h @@ -17,6 +17,7 @@ #include "base/memory/scoped_ptr.h" #include "base/observer_list.h" #include "base/sequenced_task_runner_helpers.h" +#include "chrome/browser/safe_browsing/delayed_analysis_callback.h" #include "chrome/browser/safe_browsing/safe_browsing_util.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/notification_observer.h" @@ -124,6 +125,12 @@ class SafeBrowsingService scoped_ptr<TrackedPreferenceValidationDelegate> CreatePreferenceValidationDelegate(Profile* profile) const; + // Registers |callback| to be run after some delay following process launch. + // |callback| will be dropped if the service is not applicable for the + // process. + void RegisterDelayedAnalysisCallback( + const safe_browsing::DelayedAnalysisCallback& callback); + protected: // Creates the safe browsing service. Need to initialize before using. SafeBrowsingService(); diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 5bbcd0b..8986b5a 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -2653,6 +2653,9 @@ 'browser/safe_browsing/client_side_detection_service.h', 'browser/safe_browsing/database_manager.cc', 'browser/safe_browsing/database_manager.h', + 'browser/safe_browsing/delayed_analysis_callback.h', + 'browser/safe_browsing/delayed_callback_runner.cc', + 'browser/safe_browsing/delayed_callback_runner.h', 'browser/safe_browsing/download_feedback.cc', 'browser/safe_browsing/download_feedback.h', 'browser/safe_browsing/download_feedback_service.cc', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index 900e9d9..858e3e6 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -1205,6 +1205,7 @@ 'browser/safe_browsing/chunk_range_unittest.cc', 'browser/safe_browsing/client_side_detection_host_unittest.cc', 'browser/safe_browsing/client_side_detection_service_unittest.cc', + 'browser/safe_browsing/delayed_callback_runner_unittest.cc', 'browser/safe_browsing/database_manager_unittest.cc', 'browser/safe_browsing/download_feedback_service_unittest.cc', 'browser/safe_browsing/download_feedback_unittest.cc', |