diff options
author | grt@chromium.org <grt@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-07-08 21:13:57 +0000 |
---|---|---|
committer | grt@chromium.org <grt@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-07-08 21:13:57 +0000 |
commit | 5f24715c505ab7e22bf12cd88179eb8c0fdf7313 (patch) | |
tree | 41404a183bd8237d9e7d1536fdd6bba8f3890529 | |
parent | 6f625a200cf121fdff091509bdb6efce9ba51d26 (diff) | |
download | chromium_src-5f24715c505ab7e22bf12cd88179eb8c0fdf7313.zip chromium_src-5f24715c505ab7e22bf12cd88179eb8c0fdf7313.tar.gz chromium_src-5f24715c505ab7e22bf12cd88179eb8c0fdf7313.tar.bz2 |
Merge 281089 "Include the latest binary download info in safe br..."
> Include the latest binary download info in safe browsing incident reports.
>
> BUG=383042
>
> Review URL: https://codereview.chromium.org/330653006
TBR=grt@chromium.org
Review URL: https://codereview.chromium.org/379563006
git-svn-id: svn://svn.chromium.org/chrome/branches/2062/src@281833 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/browser/safe_browsing/download_protection_service.cc | 16 | ||||
-rw-r--r-- | chrome/browser/safe_browsing/incident_report_uploader.h | 1 | ||||
-rw-r--r-- | chrome/browser/safe_browsing/incident_reporting_service.cc | 148 | ||||
-rw-r--r-- | chrome/browser/safe_browsing/incident_reporting_service.h | 59 | ||||
-rw-r--r-- | chrome/browser/safe_browsing/incident_reporting_service_unittest.cc | 229 | ||||
-rw-r--r-- | chrome/browser/safe_browsing/last_download_finder.cc | 249 | ||||
-rw-r--r-- | chrome/browser/safe_browsing/last_download_finder.h | 117 | ||||
-rw-r--r-- | chrome/browser/safe_browsing/last_download_finder_unittest.cc | 331 | ||||
-rw-r--r-- | chrome/chrome_browser.gypi | 2 | ||||
-rw-r--r-- | chrome/chrome_common.gypi | 1 | ||||
-rw-r--r-- | chrome/chrome_tests_unit.gypi | 1 | ||||
-rw-r--r-- | chrome/common/BUILD.gn | 1 | ||||
-rw-r--r-- | chrome/common/safe_browsing/download_protection_util.cc | 17 | ||||
-rw-r--r-- | chrome/common/safe_browsing/download_protection_util.h | 6 | ||||
-rw-r--r-- | tools/metrics/histograms/histograms.xml | 43 |
15 files changed, 1113 insertions, 108 deletions
diff --git a/chrome/browser/safe_browsing/download_protection_service.cc b/chrome/browser/safe_browsing/download_protection_service.cc index 256aba6..785ceca 100644 --- a/chrome/browser/safe_browsing/download_protection_service.cc +++ b/chrome/browser/safe_browsing/download_protection_service.cc @@ -56,20 +56,6 @@ const char DownloadProtectionService::kDownloadRequestUrl[] = "https://sb-ssl.google.com/safebrowsing/clientreport/download"; namespace { -ClientDownloadRequest::DownloadType GetDownloadType( - const base::FilePath& file) { - DCHECK(download_protection_util::IsBinaryFile(file)); - if (file.MatchesExtension(FILE_PATH_LITERAL(".apk"))) - return ClientDownloadRequest::ANDROID_APK; - else if (file.MatchesExtension(FILE_PATH_LITERAL(".crx"))) - return ClientDownloadRequest::CHROME_EXTENSION; - // For zip files, we use the ZIPPED_EXECUTABLE type since we will only send - // the pingback if we find an executable inside the zip archive. - else if (file.MatchesExtension(FILE_PATH_LITERAL(".zip"))) - return ClientDownloadRequest::ZIPPED_EXECUTABLE; - return ClientDownloadRequest::WIN_EXECUTABLE; -} - // List of extensions for which we track some UMA stats. enum MaliciousExtensionType { EXTENSION_EXE, @@ -477,7 +463,7 @@ class DownloadProtectionService::CheckClientDownloadRequest *reason = REASON_NOT_BINARY_FILE; return false; } - *type = GetDownloadType(target_path); + *type = download_protection_util::GetDownloadType(target_path); return true; } diff --git a/chrome/browser/safe_browsing/incident_report_uploader.h b/chrome/browser/safe_browsing/incident_report_uploader.h index 4449c95..d564848 100644 --- a/chrome/browser/safe_browsing/incident_report_uploader.h +++ b/chrome/browser/safe_browsing/incident_report_uploader.h @@ -24,6 +24,7 @@ class IncidentReportUploader { UPLOAD_CANCELLED = 3, // The upload was cancelled. UPLOAD_REQUEST_FAILED = 4, // Upload failed. UPLOAD_INVALID_RESPONSE = 5, // The response was not recognized. + UPLOAD_NO_DOWNLOAD = 6, // No last download was found. NUM_UPLOAD_RESULTS }; diff --git a/chrome/browser/safe_browsing/incident_reporting_service.cc b/chrome/browser/safe_browsing/incident_reporting_service.cc index fb251f6..46cb9f3 100644 --- a/chrome/browser/safe_browsing/incident_reporting_service.cc +++ b/chrome/browser/safe_browsing/incident_reporting_service.cc @@ -73,8 +73,8 @@ struct IncidentReportingService::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; + // False until PROFILE_ADDED notification is received. + bool added; private: DISALLOW_COPY_AND_ASSIGN(ProfileContext); @@ -98,7 +98,7 @@ class IncidentReportingService::UploadContext { DISALLOW_COPY_AND_ASSIGN(UploadContext); }; -IncidentReportingService::ProfileContext::ProfileContext() : created() { +IncidentReportingService::ProfileContext::ProfileContext() : added() { } IncidentReportingService::ProfileContext::~ProfileContext() { @@ -132,7 +132,7 @@ IncidentReportingService::IncidentReportingService( receiver_weak_ptr_factory_(this), weak_ptr_factory_(this) { notification_registrar_.Add(this, - chrome::NOTIFICATION_PROFILE_CREATED, + chrome::NOTIFICATION_PROFILE_ADDED, content::NotificationService::AllSources()); notification_registrar_.Add(this, chrome::NOTIFICATION_PROFILE_DESTROYED, @@ -146,6 +146,7 @@ IncidentReportingService::~IncidentReportingService() { weak_ptr_factory_.InvalidateWeakPtrs(); CancelEnvironmentCollection(); + CancelDownloadCollection(); CancelAllReportUploads(); STLDeleteValues(&profiles_); @@ -154,7 +155,7 @@ IncidentReportingService::~IncidentReportingService() { 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 + // OnProfileAdded is called are held until the profile's preferences can be // queried. ignore_result(GetOrCreateProfileContext(profile)); @@ -188,13 +189,21 @@ void IncidentReportingService::SetCollectEnvironmentHook( } } -void IncidentReportingService::OnProfileCreated(Profile* profile) { +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->created = true; + context->added = true; - // Drop all incidents if this profile is not participating in safe browsing. + // 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) { @@ -202,19 +211,24 @@ void IncidentReportingService::OnProfileCreated(Profile* profile) { } 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) { -#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* @@ -272,21 +286,23 @@ void IncidentReportingService::AddIncident( DCHECK(context); // Drop the incident immediately if profile creation has completed and the - // profile is not participating in safe browsing. - if (context->created && + // profile is not participating in safe browsing. Preference evaluation is + // deferred until OnProfileAdded() if profile creation has not yet + // completed. + if (context->added && !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. + // 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 provide it. + // 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()); @@ -306,11 +322,14 @@ void IncidentReportingService::AddIncident( upload_timer_.Reset(); BeginEnvironmentCollection(); + BeginDownloadCollection(); } 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; @@ -330,6 +349,10 @@ void IncidentReportingService::BeginEnvironmentCollection() { DCHECK(environment_collection_pending_); } +bool IncidentReportingService::WaitingForEnvironmentCollection() { + return environment_collection_pending_; +} + void IncidentReportingService::CancelEnvironmentCollection() { environment_collection_begin_ = base::TimeTicks(); environment_collection_pending_ = false; @@ -361,6 +384,10 @@ void IncidentReportingService::OnEnvironmentDataCollected( UploadIfCollectionComplete(); } +bool IncidentReportingService::WaitingToCollateIncidents() { + return collection_timeout_pending_; +} + void IncidentReportingService::CancelIncidentCollection() { collection_timeout_pending_ = false; last_incident_time_ = base::TimeTicks(); @@ -379,7 +406,7 @@ void IncidentReportingService::OnCollectionTimeout() { for (ProfileContextCollection::iterator scan = profiles_.begin(); scan != profiles_.end(); ++scan) { - if (!scan->second->created && !scan->second->incidents.empty()) { + if (!scan->second->added && !scan->second->incidents.empty()) { upload_timer_.Reset(); return; } @@ -390,22 +417,91 @@ void IncidentReportingService::OnCollectionTimeout() { UploadIfCollectionComplete(); } -void IncidentReportingService::CollectDownloadDetails( - ClientIncidentReport_DownloadDetails* download_details) { +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()); - // TODO(grt): collect download info; http://crbug.com/383042. + 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. - if (environment_collection_pending_ || collection_timeout_pending_) + // 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()); 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(); @@ -566,10 +662,10 @@ void IncidentReportingService::Observe( const content::NotificationSource& source, const content::NotificationDetails& details) { switch (type) { - case chrome::NOTIFICATION_PROFILE_CREATED: { + case chrome::NOTIFICATION_PROFILE_ADDED: { Profile* profile = content::Source<Profile>(source).ptr(); if (!profile->IsOffTheRecord()) - OnProfileCreated(profile); + OnProfileAdded(profile); break; } case chrome::NOTIFICATION_PROFILE_DESTROYED: { diff --git a/chrome/browser/safe_browsing/incident_reporting_service.h b/chrome/browser/safe_browsing/incident_reporting_service.h index 39e9fa2..bd02472 100644 --- a/chrome/browser/safe_browsing/incident_reporting_service.h +++ b/chrome/browser/safe_browsing/incident_reporting_service.h @@ -20,6 +20,7 @@ #include "base/timer/timer.h" #include "chrome/browser/safe_browsing/add_incident_callback.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" #include "content/public/browser/notification_registrar.h" @@ -54,10 +55,10 @@ class ClientIncidentReport_IncidentData; // 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 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. +// 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, @@ -94,11 +95,18 @@ class IncidentReportingService : public content::NotificationObserver { CollectEnvironmentDataFn collect_environment_data_hook, const scoped_refptr<base::TaskRunner>& task_runner); - // Handles the creation of a new profile. Creates a new context for |profile| - // if one does not exist, and drops any received incidents for the profile if - // the profile is not participating in safe browsing. Overridden by unit tests + // Handles the addition of a new profile to the ProfileManager. Creates a new + // context for |profile| if one does not exist, drops any received incidents + // for the profile if the profile is not participating in safe browsing, and + // initiates a new search for the most recent download if a report is being + // assembled and the most recent has not been found. Overridden by unit tests // to inject incidents prior to creation. - virtual void OnProfileCreated(Profile* profile); + virtual void OnProfileAdded(Profile* profile); + + // Initiates a search for the most recent binary download. Overriden by unit + // tests to provide a fake finder. + virtual scoped_ptr<LastDownloadFinder> CreateDownloadFinder( + const LastDownloadFinder::LastDownloadCallback& callback); // Initiates an upload. Overridden by unit tests to provide a fake uploader. virtual scoped_ptr<IncidentReportUploader> StartReportUpload( @@ -131,6 +139,9 @@ class IncidentReportingService : public content::NotificationObserver { // Starts a task to collect environment data in the blocking pool. void BeginEnvironmentCollection(); + // Returns true if the environment collection task is outstanding. + bool WaitingForEnvironmentCollection(); + // Cancels any pending environment collection task and drops any data that has // already been collected. void CancelEnvironmentCollection(); @@ -141,6 +152,10 @@ class IncidentReportingService : public content::NotificationObserver { void OnEnvironmentDataCollected( scoped_ptr<ClientIncidentReport_EnvironmentData> environment_data); + // Returns true if the service is waiting for additional incidents before + // uploading a report. + bool WaitingToCollateIncidents(); + // Cancels the collection timeout. void CancelIncidentCollection(); @@ -149,10 +164,23 @@ class IncidentReportingService : public content::NotificationObserver { // environment data to arrive or by sending an incident report. void OnCollectionTimeout(); - // Populates |download_details| with information about the most recent - // download. - void CollectDownloadDetails( - ClientIncidentReport_DownloadDetails* download_details); + // Starts the asynchronous process of finding the most recent executable + // download if one is not currently being search for and/or has not already + // been found. + void BeginDownloadCollection(); + + // True if the service is waiting to discover the most recent download either + // because a task to do so is outstanding, or because one or more profiles + // have yet to be added to the ProfileManager. + bool WaitingForMostRecentDownload(); + + // Cancels the search for the most recent executable download. + void CancelDownloadCollection(); + + // A callback invoked on the UI thread by the last download finder when the + // search for the most recent binary download is complete. + void OnLastDownloadFound( + scoped_ptr<ClientIncidentReport_DownloadDetails> last_download); // Uploads an incident report if all data collection is complete. Incidents // originating from profiles that do not participate in safe browsing are @@ -227,12 +255,19 @@ class IncidentReportingService : public content::NotificationObserver { // The time at which environmental data collection was initiated. base::TimeTicks environment_collection_begin_; + // The time at which download collection was initiated. + base::TimeTicks last_download_begin_; + // Context data for all on-the-record profiles. ProfileContextCollection profiles_; // The collection of uploads in progress. ScopedVector<UploadContext> uploads_; + // An object that asynchronously searches for the most recent binary download. + // Non-NULL while such a search is outstanding. + scoped_ptr<LastDownloadFinder> last_download_finder_; + // A factory for handing out weak pointers for AddIncident callbacks. base::WeakPtrFactory<IncidentReportingService> receiver_weak_ptr_factory_; diff --git a/chrome/browser/safe_browsing/incident_reporting_service_unittest.cc b/chrome/browser/safe_browsing/incident_reporting_service_unittest.cc index 1496486..1a369f8 100644 --- a/chrome/browser/safe_browsing/incident_reporting_service_unittest.cc +++ b/chrome/browser/safe_browsing/incident_reporting_service_unittest.cc @@ -16,6 +16,7 @@ #include "base/threading/thread_local.h" #include "chrome/browser/prefs/browser_prefs.h" #include "chrome/browser/safe_browsing/incident_report_uploader.h" +#include "chrome/browser/safe_browsing/last_download_finder.h" #include "chrome/common/pref_names.h" #include "chrome/common/safe_browsing/csd.pb.h" #include "chrome/test/base/testing_browser_process.h" @@ -35,24 +36,30 @@ class IncidentReportingServiceTest : public testing::Test { class TestIncidentReportingService : public safe_browsing::IncidentReportingService { public: - typedef base::Callback<void(Profile*)> PreProfileCreateCallback; + typedef base::Callback<void(Profile*)> PreProfileAddCallback; typedef base::Callback< void(safe_browsing::ClientIncidentReport_EnvironmentData*)> CollectEnvironmentCallback; + typedef base::Callback<scoped_ptr<safe_browsing::LastDownloadFinder>( + const safe_browsing::LastDownloadFinder::LastDownloadCallback& + callback)> CreateDownloadFinderCallback; + typedef base::Callback<scoped_ptr<safe_browsing::IncidentReportUploader>( const safe_browsing::IncidentReportUploader::OnResultCallback&, const safe_browsing::ClientIncidentReport& report)> StartUploadCallback; TestIncidentReportingService( const scoped_refptr<base::TaskRunner>& task_runner, - const PreProfileCreateCallback& pre_profile_create_callback, + const PreProfileAddCallback& pre_profile_add_callback, const CollectEnvironmentCallback& collect_environment_callback, + const CreateDownloadFinderCallback& create_download_finder_callback, const StartUploadCallback& start_upload_callback) : IncidentReportingService(NULL, NULL), - pre_profile_create_callback_(pre_profile_create_callback), + pre_profile_add_callback_(pre_profile_add_callback), collect_environment_callback_(collect_environment_callback), + create_download_finder_callback_(create_download_finder_callback), start_upload_callback_(start_upload_callback) { SetCollectEnvironmentHook(&CollectEnvironmentData, task_runner); test_instance_.Get().Set(this); @@ -61,9 +68,15 @@ class IncidentReportingServiceTest : public testing::Test { virtual ~TestIncidentReportingService() { test_instance_.Get().Set(NULL); } protected: - virtual void OnProfileCreated(Profile* profile) OVERRIDE { - pre_profile_create_callback_.Run(profile); - safe_browsing::IncidentReportingService::OnProfileCreated(profile); + virtual void OnProfileAdded(Profile* profile) OVERRIDE { + pre_profile_add_callback_.Run(profile); + safe_browsing::IncidentReportingService::OnProfileAdded(profile); + } + + virtual scoped_ptr<safe_browsing::LastDownloadFinder> CreateDownloadFinder( + const safe_browsing::LastDownloadFinder::LastDownloadCallback& callback) + OVERRIDE { + return create_download_finder_callback_.Run(callback); } virtual scoped_ptr<safe_browsing::IncidentReportUploader> StartReportUpload( @@ -87,8 +100,9 @@ class IncidentReportingServiceTest : public testing::Test { static base::LazyInstance<base::ThreadLocalPointer< TestIncidentReportingService> >::Leaky test_instance_; - PreProfileCreateCallback pre_profile_create_callback_; + PreProfileAddCallback pre_profile_add_callback_; CollectEnvironmentCallback collect_environment_callback_; + CreateDownloadFinderCallback create_download_finder_callback_; StartUploadCallback start_upload_callback_; }; @@ -100,14 +114,26 @@ class IncidentReportingServiceTest : public testing::Test { }; // A type for specifying the action to be taken by the test fixture during - // profile initialization (before NOTIFICATION_PROFILE_CREATED is sent). - enum OnProfileCreationAction { - ON_PROFILE_CREATION_NO_ACTION, - ON_PROFILE_CREATION_ADD_INCIDENT, // Add an incident to the service. + // profile initialization (before NOTIFICATION_PROFILE_ADDED is sent). + enum OnProfileAdditionAction { + ON_PROFILE_ADDITION_NO_ACTION, + ON_PROFILE_ADDITION_ADD_INCIDENT, // Add an incident to the service. + }; + + // A type for specifying the action to be taken by the test fixture when the + // service creates a LastDownloadFinder. + enum OnCreateDownloadFinderAction { + // Post a task that reports a download. + ON_CREATE_DOWNLOAD_FINDER_DOWNLOAD_FOUND, + // Post a task that reports no downloads found. + ON_CREATE_DOWNLOAD_FINDER_NO_DOWNLOADS, + // Immediately return due to a lack of eligible profiles. + ON_CREATE_DOWNLOAD_FINDER_NO_PROFILES, }; static const int64 kIncidentTimeMsec; static const char kFakeOsName[]; + static const char kFakeDownloadToken[]; IncidentReportingServiceTest() : task_runner_(new base::TestSimpleTaskRunner), @@ -115,14 +141,20 @@ class IncidentReportingServiceTest : public testing::Test { profile_manager_(TestingBrowserProcess::GetGlobal()), instance_(new TestIncidentReportingService( task_runner_, - base::Bind(&IncidentReportingServiceTest::PreProfileCreate, + base::Bind(&IncidentReportingServiceTest::PreProfileAdd, base::Unretained(this)), base::Bind(&IncidentReportingServiceTest::CollectEnvironmentData, base::Unretained(this)), + base::Bind(&IncidentReportingServiceTest::CreateDownloadFinder, + base::Unretained(this)), base::Bind(&IncidentReportingServiceTest::StartUpload, base::Unretained(this)))), + on_create_download_finder_action_( + ON_CREATE_DOWNLOAD_FINDER_DOWNLOAD_FOUND), upload_result_(safe_browsing::IncidentReportUploader::UPLOAD_SUCCESS), environment_collected_(), + download_finder_created_(), + download_finder_destroyed_(), uploader_destroyed_() {} virtual void SetUp() OVERRIDE { @@ -130,12 +162,18 @@ class IncidentReportingServiceTest : public testing::Test { ASSERT_TRUE(profile_manager_.SetUp()); } + // Sets the action to be taken by the test fixture when the service creates a + // LastDownloadFinder. + void SetCreateDownloadFinderAction(OnCreateDownloadFinderAction action) { + on_create_download_finder_action_ = action; + } + // Creates and returns a profile (owned by the profile manager) with or // without safe browsing enabled. An incident will be created within - // PreProfileCreate if requested. + // PreProfileAdd if requested. TestingProfile* CreateProfile(const std::string& profile_name, SafeBrowsingDisposition safe_browsing_opt_in, - OnProfileCreationAction on_creation_action) { + OnProfileAdditionAction on_addition_action) { // Create prefs for the profile with safe browsing enabled or not. scoped_ptr<TestingPrefServiceSyncable> prefs( new TestingPrefServiceSyncable); @@ -144,7 +182,7 @@ class IncidentReportingServiceTest : public testing::Test { safe_browsing_opt_in == SAFE_BROWSING_OPT_IN); // Remember whether or not to create an incident. - profile_properties_[profile_name].on_creation_action = on_creation_action; + profile_properties_[profile_name].on_addition_action = on_addition_action; // Boom (or fizzle). return profile_manager_.CreateTestingProfile( @@ -195,13 +233,17 @@ class IncidentReportingServiceTest : public testing::Test { ASSERT_TRUE(uploaded_report_->environment().os().has_os_name()); ASSERT_EQ(std::string(kFakeOsName), uploaded_report_->environment().os().os_name()); + ASSERT_EQ(std::string(kFakeDownloadToken), + uploaded_report_->download().token()); uploaded_report_.reset(); } - void ExpectNoUpload() { ASSERT_FALSE(uploaded_report_); } + void AssertNoUpload() { ASSERT_FALSE(uploaded_report_); } bool HasCollectedEnvironmentData() const { return environment_collected_; } + bool HasCreatedDownloadFinder() const { return download_finder_created_; } + bool DownloadFinderDestroyed() const { return download_finder_destroyed_; } bool UploaderDestroyed() const { return uploader_destroyed_; } scoped_refptr<base::TestSimpleTaskRunner> task_runner_; @@ -209,9 +251,12 @@ class IncidentReportingServiceTest : public testing::Test { TestingProfileManager profile_manager_; scoped_ptr<safe_browsing::IncidentReportingService> instance_; base::Closure on_start_upload_callback_; + OnCreateDownloadFinderAction on_create_download_finder_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_; private: @@ -248,20 +293,46 @@ class IncidentReportingServiceTest : public testing::Test { DISALLOW_COPY_AND_ASSIGN(FakeUploader); }; + class FakeDownloadFinder : public safe_browsing::LastDownloadFinder { + public: + static scoped_ptr<safe_browsing::LastDownloadFinder> Create( + const base::Closure& on_deleted, + scoped_ptr<safe_browsing::ClientIncidentReport_DownloadDetails> + download, + const safe_browsing::LastDownloadFinder::LastDownloadCallback& + callback) { + // Post a task to run the callback. + base::ThreadTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::Bind(callback, base::Passed(&download))); + return scoped_ptr<safe_browsing::LastDownloadFinder>( + new FakeDownloadFinder(on_deleted)); + } + + virtual ~FakeDownloadFinder() { on_deleted_.Run(); } + + private: + explicit FakeDownloadFinder(const base::Closure& on_deleted) + : on_deleted_(on_deleted) {} + + base::Closure on_deleted_; + + DISALLOW_COPY_AND_ASSIGN(FakeDownloadFinder); + }; + // Properties for a profile that impact the behavior of the test. struct ProfileProperties { - ProfileProperties() : on_creation_action(ON_PROFILE_CREATION_NO_ACTION) {} + ProfileProperties() : on_addition_action(ON_PROFILE_ADDITION_NO_ACTION) {} // The action taken by the test fixture during profile initialization - // (before NOTIFICATION_PROFILE_CREATED is sent). - OnProfileCreationAction on_creation_action; + // (before NOTIFICATION_PROFILE_ADDED is sent). + OnProfileAdditionAction on_addition_action; }; // Returns the name of a profile as provided to CreateProfile. static std::string GetProfileName(Profile* profile) { // Cannot reliably use profile->GetProfileName() since the test needs the // name before the profile manager sets it (which happens after profile - // creation). + // addition). return profile->GetPath().BaseName().AsUTF8Unsafe(); } @@ -274,20 +345,20 @@ class IncidentReportingServiceTest : public testing::Test { GetProfileName(profile))); } - // A callback run by the test fixture when a profile is created. An incident + // A callback run by the test fixture when a profile is added. An incident // is added. - void PreProfileCreate(Profile* profile) { + void PreProfileAdd(Profile* profile) { // The instance must have already been created. ASSERT_TRUE(instance_); // Add a test incident to the service if requested. - switch (profile_properties_[GetProfileName(profile)].on_creation_action) { - case ON_PROFILE_CREATION_ADD_INCIDENT: + switch (profile_properties_[GetProfileName(profile)].on_addition_action) { + case ON_PROFILE_ADDITION_ADD_INCIDENT: AddTestIncident(profile); break; default: ASSERT_EQ( - ON_PROFILE_CREATION_NO_ACTION, - profile_properties_[GetProfileName(profile)].on_creation_action); + ON_PROFILE_ADDITION_NO_ACTION, + profile_properties_[GetProfileName(profile)].on_addition_action); break; } } @@ -303,6 +374,29 @@ class IncidentReportingServiceTest : public testing::Test { environment_collected_ = true; } + // A fake CreateDownloadFinder implementation invoked by the service during + // operation. + scoped_ptr<safe_browsing::LastDownloadFinder> CreateDownloadFinder( + const safe_browsing::LastDownloadFinder::LastDownloadCallback& callback) { + download_finder_created_ = true; + scoped_ptr<safe_browsing::ClientIncidentReport_DownloadDetails> download; + if (on_create_download_finder_action_ == + ON_CREATE_DOWNLOAD_FINDER_NO_PROFILES) { + return scoped_ptr<safe_browsing::LastDownloadFinder>(); + } + if (on_create_download_finder_action_ == + ON_CREATE_DOWNLOAD_FINDER_DOWNLOAD_FOUND) { + download.reset(new safe_browsing::ClientIncidentReport_DownloadDetails); + download->set_token(kFakeDownloadToken); + } + return scoped_ptr<safe_browsing::LastDownloadFinder>( + FakeDownloadFinder::Create( + base::Bind(&IncidentReportingServiceTest::OnDownloadFinderDestroyed, + base::Unretained(this)), + download.Pass(), + callback)); + } + // A fake StartUpload implementation invoked by the service during operation. scoped_ptr<safe_browsing::IncidentReportUploader> StartUpload( const safe_browsing::IncidentReportUploader::OnResultCallback& callback, @@ -314,13 +408,16 @@ class IncidentReportingServiceTest : public testing::Test { on_start_upload_callback_.Run(); on_start_upload_callback_ = base::Closure(); } - return scoped_ptr<safe_browsing::IncidentReportUploader>(new FakeUploader( - base::Bind(&IncidentReportingServiceTest::OnUploaderDestroyed, - base::Unretained(this)), - callback, - upload_result_)); + return scoped_ptr<safe_browsing::IncidentReportUploader>( + new FakeUploader( + base::Bind( + &IncidentReportingServiceTest::OnUploaderDestroyed, + base::Unretained(this)), + callback, + upload_result_)).Pass(); } + void OnDownloadFinderDestroyed() { download_finder_destroyed_ = true; } void OnUploaderDestroyed() { uploader_destroyed_ = true; } // A mapping of profile name to its corresponding properties. @@ -335,12 +432,13 @@ base::LazyInstance<base::ThreadLocalPointer< const int64 IncidentReportingServiceTest::kIncidentTimeMsec = 47LL; const char IncidentReportingServiceTest::kFakeOsName[] = "fakedows"; +const char IncidentReportingServiceTest::kFakeDownloadToken[] = "fakedlt"; // Tests that an incident added during profile initialization when safe browsing // is on is uploaded. TEST_F(IncidentReportingServiceTest, AddIncident) { CreateProfile( - "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_CREATION_ADD_INCIDENT); + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_ADD_INCIDENT); // Let all tasks run. task_runner_->RunUntilIdle(); @@ -348,11 +446,15 @@ TEST_F(IncidentReportingServiceTest, AddIncident) { // Verify that environment collection took place. EXPECT_TRUE(HasCollectedEnvironmentData()); - // Verify that report upload took place and contained the incident and - // environment data. + // Verify that the most recent download was looked for. + EXPECT_TRUE(HasCreatedDownloadFinder()); + + // Verify that report upload took place and contained the incident, + // environment data, and download details. ExpectTestIncidentUploaded(); - // Verify that the uploader was destroyed. + // Verify that the download finder and the uploader were destroyed. + ASSERT_TRUE(DownloadFinderDestroyed()); ASSERT_TRUE(UploaderDestroyed()); } @@ -361,20 +463,61 @@ TEST_F(IncidentReportingServiceTest, AddIncident) { TEST_F(IncidentReportingServiceTest, NoSafeBrowsing) { // Create the profile, thereby causing the test to begin. CreateProfile( - "profile1", SAFE_BROWSING_OPT_OUT, ON_PROFILE_CREATION_ADD_INCIDENT); + "profile1", SAFE_BROWSING_OPT_OUT, ON_PROFILE_ADDITION_ADD_INCIDENT); // Let all tasks run. task_runner_->RunUntilIdle(); // Verify that no report upload took place. - ExpectNoUpload(); + AssertNoUpload(); +} + +// Tests that no incident report is uploaded if there is no recent download. +TEST_F(IncidentReportingServiceTest, NoDownloadNoUpload) { + // Tell the fixture to return no downloads found. + SetCreateDownloadFinderAction(ON_CREATE_DOWNLOAD_FINDER_NO_DOWNLOADS); + + // Create the profile, thereby causing the test to begin. + CreateProfile( + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_ADD_INCIDENT); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // Verify that the download finder was run but that no report upload took + // place. + EXPECT_TRUE(HasCreatedDownloadFinder()); + AssertNoUpload(); + EXPECT_TRUE(DownloadFinderDestroyed()); +} + +// Tests that no incident report is uploaded if there is no recent download. +TEST_F(IncidentReportingServiceTest, NoProfilesNoUpload) { + // Tell the fixture to pretend there are no profiles eligible for finding + // downloads. + SetCreateDownloadFinderAction(ON_CREATE_DOWNLOAD_FINDER_NO_PROFILES); + + // Create the profile, thereby causing the test to begin. + CreateProfile( + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_ADD_INCIDENT); + + // Let all tasks run. + task_runner_->RunUntilIdle(); + + // Verify that the download finder was run but that no report upload took + // place. + EXPECT_TRUE(HasCreatedDownloadFinder()); + AssertNoUpload(); + // Although CreateDownloadFinder was called, no instance was returned so there + // is nothing to have been destroyed. + EXPECT_FALSE(DownloadFinderDestroyed()); } // Tests that an incident added after upload is not uploaded again. TEST_F(IncidentReportingServiceTest, OnlyOneUpload) { // Create the profile, thereby causing the test to begin. Profile* profile = CreateProfile( - "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_CREATION_ADD_INCIDENT); + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_ADD_INCIDENT); // Let all tasks run. task_runner_->RunUntilIdle(); @@ -390,7 +533,7 @@ TEST_F(IncidentReportingServiceTest, OnlyOneUpload) { task_runner_->RunUntilIdle(); // Verify that no additional report upload took place. - ExpectNoUpload(); + AssertNoUpload(); } // Tests that the same incident added for two different profiles in sequence @@ -398,7 +541,7 @@ TEST_F(IncidentReportingServiceTest, OnlyOneUpload) { TEST_F(IncidentReportingServiceTest, TwoProfilesTwoUploads) { // Create the profile, thereby causing the test to begin. CreateProfile( - "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_CREATION_ADD_INCIDENT); + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_ADD_INCIDENT); // Let all tasks run. task_runner_->RunUntilIdle(); @@ -407,9 +550,9 @@ TEST_F(IncidentReportingServiceTest, TwoProfilesTwoUploads) { // environment data. ExpectTestIncidentUploaded(); - // Create a second profile with its own incident on creation. + // Create a second profile with its own incident on addition. CreateProfile( - "profile2", SAFE_BROWSING_OPT_IN, ON_PROFILE_CREATION_ADD_INCIDENT); + "profile2", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_ADD_INCIDENT); // Let all tasks run. task_runner_->RunUntilIdle(); @@ -423,7 +566,7 @@ TEST_F(IncidentReportingServiceTest, TwoProfilesTwoUploads) { TEST_F(IncidentReportingServiceTest, ProfileDestroyedDuringUpload) { // Create a profile for which an incident will be added. Profile* profile = CreateProfile( - "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_CREATION_ADD_INCIDENT); + "profile1", SAFE_BROWSING_OPT_IN, ON_PROFILE_ADDITION_ADD_INCIDENT); // Hook up a callback to run when the upload is started that will post a task // to delete the profile. This task will run before the upload finishes. diff --git a/chrome/browser/safe_browsing/last_download_finder.cc b/chrome/browser/safe_browsing/last_download_finder.cc new file mode 100644 index 0000000..05e7c42 --- /dev/null +++ b/chrome/browser/safe_browsing/last_download_finder.cc @@ -0,0 +1,249 @@ +// 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/last_download_finder.h" + +#include <algorithm> +#include <functional> +#include <utility> + +#include "base/bind.h" +#include "base/macros.h" +#include "base/prefs/pref_service.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/chrome_notification_types.h" +#include "chrome/browser/history/history_service.h" +#include "chrome/browser/history/history_service_factory.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/safe_browsing/csd.pb.h" +#include "chrome/common/safe_browsing/download_protection_util.h" +#include "content/public/browser/download_item.h" +#include "content/public/browser/notification_details.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_source.h" + +namespace safe_browsing { + +namespace { + +// Returns true if |first| is more recent than |second|, preferring opened over +// non-opened for downloads that completed at the same time (extraordinarily +// unlikely). Only files that look like some kind of executable are considered. +bool IsMoreInterestingThan(const history::DownloadRow& first, + const history::DownloadRow& second) { + if (first.end_time < second.end_time) + return false; + // TODO(grt): Peek into archives to see if they contain binaries; + // http://crbug.com/386915. + if (!download_protection_util::IsBinaryFile(first.target_path) || + download_protection_util::IsArchiveFile(first.target_path)) { + return false; + } + return (first.end_time != second.end_time || + (first.opened && !second.opened)); +} + +// Returns a pointer to the most interesting completed download in |downloads|. +const history::DownloadRow* FindMostInteresting( + const std::vector<history::DownloadRow>& downloads) { + const history::DownloadRow* most_recent_row = NULL; + for (size_t i = 0; i < downloads.size(); ++i) { + const history::DownloadRow& row = downloads[i]; + // Ignore incomplete downloads. + if (row.state != content::DownloadItem::COMPLETE) + continue; + if (!most_recent_row || IsMoreInterestingThan(row, *most_recent_row)) + most_recent_row = &row; + } + return most_recent_row; +} + +// Populates the |details| protobuf with information pertaining to |download|. +void PopulateDetails(const history::DownloadRow& download, + ClientIncidentReport_DownloadDetails* details) { + ClientDownloadRequest* download_request = details->mutable_download(); + download_request->set_url(download.url_chain.back().spec()); + // digests is a required field, so force it to exist. + // TODO(grt): Include digests in reports; http://crbug.com/389123. + ignore_result(download_request->mutable_digests()); + download_request->set_length(download.received_bytes); + for (size_t i = 0; i < download.url_chain.size(); ++i) { + const GURL& url = download.url_chain[i]; + ClientDownloadRequest_Resource* resource = + download_request->add_resources(); + resource->set_url(url.spec()); + if (i != download.url_chain.size() - 1) { // An intermediate redirect. + resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT); + } else { // The final download URL. + resource->set_type(ClientDownloadRequest::DOWNLOAD_URL); + if (!download.referrer_url.is_empty()) + resource->set_referrer(download.referrer_url.spec()); + } + } + download_request->set_file_basename( + download.target_path.BaseName().AsUTF8Unsafe()); + download_request->set_download_type( + download_protection_util::GetDownloadType(download.target_path)); + download_request->set_locale( + g_browser_process->local_state()->GetString(prefs::kApplicationLocale)); + + details->set_download_time_msec(download.end_time.ToJavaTime()); + // Opened time is unknown for now, so use the download time if the file was + // opened in Chrome. + if (download.opened) + details->set_open_time_msec(download.end_time.ToJavaTime()); +} + +} // namespace + +LastDownloadFinder::~LastDownloadFinder() { +} + +// static +scoped_ptr<LastDownloadFinder> LastDownloadFinder::Create( + const LastDownloadCallback& callback) { + scoped_ptr<LastDownloadFinder> finder(make_scoped_ptr(new LastDownloadFinder( + g_browser_process->profile_manager()->GetLoadedProfiles(), callback))); + // Return NULL if there is no work to do. + if (finder->profiles_.empty()) + return scoped_ptr<LastDownloadFinder>(); + return finder.Pass(); +} + +LastDownloadFinder::LastDownloadFinder() : weak_ptr_factory_(this) { +} + +LastDownloadFinder::LastDownloadFinder(const std::vector<Profile*>& profiles, + const LastDownloadCallback& callback) + : callback_(callback), weak_ptr_factory_(this) { + // Observe profile lifecycle events so that the finder can begin or abandon + // the search in profiles while it is running. + notification_registrar_.Add(this, + chrome::NOTIFICATION_PROFILE_ADDED, + content::NotificationService::AllSources()); + notification_registrar_.Add(this, + chrome::NOTIFICATION_HISTORY_LOADED, + content::NotificationService::AllSources()); + notification_registrar_.Add(this, + chrome::NOTIFICATION_PROFILE_DESTROYED, + content::NotificationService::AllSources()); + + // Begin the seach for all given profiles. + std::for_each( + profiles.begin(), + profiles.end(), + std::bind1st(std::mem_fun(&LastDownloadFinder::SearchInProfile), this)); +} + +void LastDownloadFinder::SearchInProfile(Profile* profile) { + // Do not look in OTR profiles or in profiles that do not participate in + // safe browsing. + if (profile->IsOffTheRecord() || + !profile->GetPrefs()->GetBoolean(prefs::kSafeBrowsingEnabled)) { + return; + } + + HistoryService* history_service = + HistoryServiceFactory::GetForProfile(profile, Profile::IMPLICIT_ACCESS); + // No history service is returned for profiles that do not save history. + if (!history_service) + return; + + profiles_.push_back(profile); + if (history_service->BackendLoaded()) { + history_service->QueryDownloads( + base::Bind(&LastDownloadFinder::OnDownloadQuery, + weak_ptr_factory_.GetWeakPtr(), + profile)); + } // else wait until history is loaded. +} + +void LastDownloadFinder::OnProfileHistoryLoaded( + Profile* profile, + HistoryService* history_service) { + if (std::find(profiles_.begin(), profiles_.end(), profile) != + profiles_.end()) { + history_service->QueryDownloads( + base::Bind(&LastDownloadFinder::OnDownloadQuery, + weak_ptr_factory_.GetWeakPtr(), + profile)); + } +} + +void LastDownloadFinder::AbandonSearchInProfile(Profile* profile) { + // |profile| may not be present in the set of profiles. + std::vector<Profile*>::iterator it = + std::find(profiles_.begin(), profiles_.end(), profile); + if (it != profiles_.end()) + RemoveProfileAndReportIfDone(it); +} + +void LastDownloadFinder::OnDownloadQuery( + Profile* profile, + scoped_ptr<std::vector<history::DownloadRow> > downloads) { + // Early-exit if the history search for this profile was abandoned. + std::vector<Profile*>::iterator it = + std::find(profiles_.begin(), profiles_.end(), profile); + if (it == profiles_.end()) + return; + + // Find the most recent from this profile and use it if it's better than + // anything else found so far. + const history::DownloadRow* profile_best = FindMostInteresting(*downloads); + if (profile_best && IsMoreInterestingThan(*profile_best, most_recent_row_)) + most_recent_row_ = *profile_best; + + RemoveProfileAndReportIfDone(it); +} + +void LastDownloadFinder::RemoveProfileAndReportIfDone( + std::vector<Profile*>::iterator it) { + DCHECK(it != profiles_.end()); + + *it = profiles_.back(); + profiles_.resize(profiles_.size() - 1); + + // Finish processing if all results are in. + if (profiles_.empty()) + ReportResults(); + // Do not touch this instance after reporting results. +} + +void LastDownloadFinder::ReportResults() { + DCHECK(profiles_.empty()); + if (most_recent_row_.end_time.is_null()) { + callback_.Run(scoped_ptr<ClientIncidentReport_DownloadDetails>()); + // Do not touch this instance after running the callback, since it may have + // been deleted. + } else { + scoped_ptr<ClientIncidentReport_DownloadDetails> details( + new ClientIncidentReport_DownloadDetails()); + PopulateDetails(most_recent_row_, details.get()); + callback_.Run(details.Pass()); + // Do not touch this instance after running the callback, since it may have + // been deleted. + } +} + +void LastDownloadFinder::Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + switch (type) { + case chrome::NOTIFICATION_PROFILE_ADDED: + SearchInProfile(content::Source<Profile>(source).ptr()); + break; + case chrome::NOTIFICATION_HISTORY_LOADED: + OnProfileHistoryLoaded(content::Source<Profile>(source).ptr(), + content::Details<HistoryService>(details).ptr()); + break; + case chrome::NOTIFICATION_PROFILE_DESTROYED: + AbandonSearchInProfile(content::Source<Profile>(source).ptr()); + break; + default: + break; + } +} + +} // namespace safe_browsing diff --git a/chrome/browser/safe_browsing/last_download_finder.h b/chrome/browser/safe_browsing/last_download_finder.h new file mode 100644 index 0000000..4fef70c --- /dev/null +++ b/chrome/browser/safe_browsing/last_download_finder.h @@ -0,0 +1,117 @@ +// 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_LAST_DOWNLOAD_FINDER_H_ +#define CHROME_BROWSER_SAFE_BROWSING_LAST_DOWNLOAD_FINDER_H_ + +#include <vector> + +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/macros.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "chrome/browser/history/download_row.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" + +class HistoryService; +class Profile; + +namespace content { +class NotificationDetails; +class NotificationSource; +} + +namespace history { +struct DownloadRow; +} + +namespace safe_browsing { + +class ClientIncidentReport_DownloadDetails; + +// Finds the most recent executable downloaded by any on-the-record profile with +// history that participates in safe browsing. +class LastDownloadFinder : public content::NotificationObserver { + public: + // The type of a callback run by the finder upon completion. The argument is a + // protobuf containing details of the download that was found, or an empty + // pointer if none was found. + typedef base::Callback<void(scoped_ptr<ClientIncidentReport_DownloadDetails>)> + LastDownloadCallback; + + virtual ~LastDownloadFinder(); + + // Initiates an asynchronous search for the most recent download. |callback| + // will be run when the search is complete. The returned instance can be + // deleted to terminate the search, in which case |callback| is not invoked. + // Returns NULL without running |callback| if there are no eligible profiles + // to search. + static scoped_ptr<LastDownloadFinder> Create( + const LastDownloadCallback& callback); + + protected: + // Protected constructor so that unit tests can create a fake finder. + LastDownloadFinder(); + + private: + LastDownloadFinder(const std::vector<Profile*>& profiles, + const LastDownloadCallback& callback); + + // Adds |profile| to the set of profiles to be searched if it is an + // on-the-record profile with history that participates in safe browsing. The + // search is initiated if the profile has already loaded history. + void SearchInProfile(Profile* profile); + + // Initiates a search in |profile| if it is in the set of profiles to be + // searched. + void OnProfileHistoryLoaded(Profile* profile, + HistoryService* history_service); + + // Abandons the search for downloads in |profile|, reporting results if there + // are no more pending queries. + void AbandonSearchInProfile(Profile* profile); + + // HistoryService::DownloadQueryCallback. Retrieves the most recent completed + // executable download from |downloads| and reports results if there are no + // more pending queries. + void OnDownloadQuery( + Profile* profile, + scoped_ptr<std::vector<history::DownloadRow> > downloads); + + // Removes the profile pointed to by |it| from profiles_ and reports results + // if there are no more pending queries. + void RemoveProfileAndReportIfDone(std::vector<Profile*>::iterator it); + + // Invokes the caller-supplied callback with the download found. + void ReportResults(); + + // content::NotificationObserver methods. + virtual void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) OVERRIDE; + + // Caller-supplied callback to be invoked when the most recent download is + // found. + LastDownloadCallback callback_; + + // The profiles for which a download query is pending. + std::vector<Profile*> profiles_; + + // Registrar for observing profile lifecycle notifications. + content::NotificationRegistrar notification_registrar_; + + // The most recent download, updated progressively as query results arrive. + history::DownloadRow most_recent_row_; + + // A factory for asynchronous operations on profiles' HistoryService. + base::WeakPtrFactory<LastDownloadFinder> weak_ptr_factory_; + + DISALLOW_COPY_AND_ASSIGN(LastDownloadFinder); +}; + +} // namespace safe_browsing + +#endif // CHROME_BROWSER_SAFE_BROWSING_LAST_DOWNLOAD_FINDER_H_ diff --git a/chrome/browser/safe_browsing/last_download_finder_unittest.cc b/chrome/browser/safe_browsing/last_download_finder_unittest.cc new file mode 100644 index 0000000..eedacb6 --- /dev/null +++ b/chrome/browser/safe_browsing/last_download_finder_unittest.cc @@ -0,0 +1,331 @@ +// 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/last_download_finder.h" + +#include <string> +#include <vector> + +#include "base/bind.h" +#include "base/callback.h" +#include "base/file_util.h" +#include "base/run_loop.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/utf_string_conversions.h" +#include "chrome/browser/history/chrome_history_client.h" +#include "chrome/browser/history/chrome_history_client_factory.h" +#include "chrome/browser/history/download_row.h" +#include "chrome/browser/history/history_service.h" +#include "chrome/browser/history/history_service_factory.h" +#include "chrome/browser/history/web_history_service_factory.h" +#include "chrome/browser/prefs/browser_prefs.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/safe_browsing/csd.pb.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_pref_service_syncable.h" +#include "chrome/test/base/testing_profile.h" +#include "chrome/test/base/testing_profile_manager.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "content/public/test/test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// A BrowserContextKeyedServiceFactory::TestingFactoryFunction that creates a +// HistoryService for a TestingProfile. +KeyedService* BuildHistoryService(content::BrowserContext* context) { + TestingProfile* profile = static_cast<TestingProfile*>(context); + + // Delete the file before creating the service. + base::FilePath history_path( + profile->GetPath().Append(chrome::kHistoryFilename)); + if (!base::DeleteFile(history_path, false) || + base::PathExists(history_path)) { + ADD_FAILURE() << "failed to delete history db file " + << history_path.value(); + return NULL; + } + + HistoryService* history_service = new HistoryService( + ChromeHistoryClientFactory::GetForProfile(profile), profile); + if (history_service->Init(profile->GetPath())) + return history_service; + + ADD_FAILURE() << "failed to initialize history service"; + delete history_service; + return NULL; +} + +} // namespace + +class LastDownloadFinderTest : public testing::Test { + public: + void NeverCalled(scoped_ptr< + safe_browsing::ClientIncidentReport_DownloadDetails> download) { + FAIL(); + } + + // Creates a new profile that participates in safe browsing and adds a + // download to its history. + void CreateProfileWithDownload() { + TestingProfile* profile = CreateProfile(SAFE_BROWSING_OPT_IN); + HistoryService* history_service = + HistoryServiceFactory::GetForProfile(profile, Profile::EXPLICIT_ACCESS); + history_service->CreateDownload( + CreateTestDownloadRow(), + base::Bind(&LastDownloadFinderTest::OnDownloadCreated, + base::Unretained(this))); + } + + // safe_browsing::LastDownloadFinder::LastDownloadCallback implementation that + // passes the found download to |result| and then runs a closure. + void OnLastDownload( + scoped_ptr<safe_browsing::ClientIncidentReport_DownloadDetails>* result, + const base::Closure& quit_closure, + scoped_ptr<safe_browsing::ClientIncidentReport_DownloadDetails> + download) { + *result = download.Pass(); + quit_closure.Run(); + } + + protected: + // A type for specifying whether or not a profile created by CreateProfile + // participates in safe browsing. + enum SafeBrowsingDisposition { + SAFE_BROWSING_OPT_OUT, + SAFE_BROWSING_OPT_IN, + }; + + LastDownloadFinderTest() : profile_number_() {} + + virtual void SetUp() OVERRIDE { + testing::Test::SetUp(); + profile_manager_.reset( + new TestingProfileManager(TestingBrowserProcess::GetGlobal())); + ASSERT_TRUE(profile_manager_->SetUp()); + } + + virtual void TearDown() OVERRIDE { + // Shut down the history service on all profiles. + std::vector<Profile*> profiles( + profile_manager_->profile_manager()->GetLoadedProfiles()); + for (size_t i = 0; i < profiles.size(); ++i) { + profiles[0]->AsTestingProfile()->DestroyHistoryService(); + } + profile_manager_.reset(); + TestingBrowserProcess::DeleteInstance(); + testing::Test::TearDown(); + } + + TestingProfile* CreateProfile(SafeBrowsingDisposition safe_browsing_opt_in) { + std::string profile_name("profile"); + profile_name.append(base::IntToString(++profile_number_)); + + // Set up keyed service factories. + TestingProfile::TestingFactories factories; + // Build up a custom history service. + factories.push_back(std::make_pair(HistoryServiceFactory::GetInstance(), + &BuildHistoryService)); + // Suppress WebHistoryService since it makes network requests. + factories.push_back(std::make_pair( + WebHistoryServiceFactory::GetInstance(), + static_cast<BrowserContextKeyedServiceFactory::TestingFactoryFunction>( + NULL))); + + // Create prefs for the profile with safe browsing enabled or not. + scoped_ptr<TestingPrefServiceSyncable> prefs( + new TestingPrefServiceSyncable); + chrome::RegisterUserProfilePrefs(prefs->registry()); + prefs->SetBoolean(prefs::kSafeBrowsingEnabled, + safe_browsing_opt_in == SAFE_BROWSING_OPT_IN); + + TestingProfile* profile = profile_manager_->CreateTestingProfile( + profile_name, + prefs.PassAs<PrefServiceSyncable>(), + base::UTF8ToUTF16(profile_name), // user_name + 0, // avatar_id + std::string(), // supervised_user_id + factories); + + return profile; + } + + void AddDownload(Profile* profile, const history::DownloadRow& download) { + base::RunLoop run_loop; + + HistoryService* history_service = + HistoryServiceFactory::GetForProfile(profile, Profile::EXPLICIT_ACCESS); + history_service->CreateDownload( + download, + base::Bind(&LastDownloadFinderTest::ContinueOnDownloadCreated, + base::Unretained(this), + run_loop.QuitClosure())); + run_loop.Run(); + } + + // Wait for the history backend thread to process any outstanding tasks. + // This is needed because HistoryService::QueryDownloads uses PostTaskAndReply + // to do work on the backend thread and then invoke the caller's callback on + // the originating thread. The PostTaskAndReplyRelay holds a reference to the + // backend until its RunReplyAndSelfDestruct is called on the originating + // thread. This reference MUST be released (on the originating thread, + // remember) _before_ calling DestroyHistoryService in TearDown(). See the + // giant comment in HistoryService::Cleanup explaining where the backend's + // dtor must be run. + void FlushHistoryBackend(Profile* profile) { + base::RunLoop run_loop; + HistoryServiceFactory::GetForProfile(profile, Profile::EXPLICIT_ACCESS) + ->FlushForTest(run_loop.QuitClosure()); + run_loop.Run(); + // Then make sure anything bounced back to the main thread has been handled. + base::RunLoop().RunUntilIdle(); + } + + // Runs the last download finder on all loaded profiles, returning the found + // download or an empty pointer if none was found. + scoped_ptr<safe_browsing::ClientIncidentReport_DownloadDetails> + RunLastDownloadFinder() { + base::RunLoop run_loop; + + scoped_ptr<safe_browsing::ClientIncidentReport_DownloadDetails> + last_download; + + scoped_ptr<safe_browsing::LastDownloadFinder> finder( + safe_browsing::LastDownloadFinder::Create( + base::Bind(&LastDownloadFinderTest::OnLastDownload, + base::Unretained(this), + &last_download, + run_loop.QuitClosure()))); + + if (finder) + run_loop.Run(); + + return last_download.Pass(); + } + + history::DownloadRow CreateTestDownloadRow() { + base::Time now(base::Time::Now()); + return history::DownloadRow( + base::FilePath(FILE_PATH_LITERAL("spam.exe")), + base::FilePath(FILE_PATH_LITERAL("spam.exe")), + std::vector<GURL>(1, GURL("http://www.google.com")), // url_chain + GURL(), // referrer + "application/octet-stream", // mime_type + "application/octet-stream", // original_mime_type + now - base::TimeDelta::FromMinutes(10), // start + now - base::TimeDelta::FromMinutes(9), // end + std::string(), // etag + std::string(), // last_modified + 47LL, // received + 47LL, // total + content::DownloadItem::COMPLETE, // download_state + content::DOWNLOAD_DANGER_TYPE_NOT_DANGEROUS, // danger_type + content::DOWNLOAD_INTERRUPT_REASON_NONE, // interrupt_reason, + 1, // id + false, // download_opened + std::string(), // ext_id + std::string()); // ext_name + } + + void ExpectNoDownloadFound(scoped_ptr< + safe_browsing::ClientIncidentReport_DownloadDetails> download) { + EXPECT_FALSE(download); + } + + void ExpectFoundTestDownload(scoped_ptr< + safe_browsing::ClientIncidentReport_DownloadDetails> download) { + ASSERT_TRUE(download); + } + + content::TestBrowserThreadBundle browser_thread_bundle_; + scoped_ptr<TestingProfileManager> profile_manager_; + + private: + // A HistoryService::DownloadCreateCallback that asserts that the download was + // created and runs |closure|. + void ContinueOnDownloadCreated(const base::Closure& closure, bool created) { + ASSERT_TRUE(created); + closure.Run(); + } + + // A HistoryService::DownloadCreateCallback that asserts that the download was + // created. + void OnDownloadCreated(bool created) { ASSERT_TRUE(created); } + + int profile_number_; +}; + +// Tests that nothing happens if there are no profiles at all. +TEST_F(LastDownloadFinderTest, NoProfiles) { + ExpectNoDownloadFound(RunLastDownloadFinder()); +} + +// Tests that nothing happens other than the callback being invoked if there are +// no profiles participating in safe browsing. +TEST_F(LastDownloadFinderTest, NoParticipatingProfiles) { + // Create a profile with a history service that is opted-out + TestingProfile* profile = CreateProfile(SAFE_BROWSING_OPT_OUT); + + // Add a download. + AddDownload(profile, CreateTestDownloadRow()); + + ExpectNoDownloadFound(RunLastDownloadFinder()); +} + +// Tests that a download is found from a single profile. +TEST_F(LastDownloadFinderTest, SimpleEndToEnd) { + // Create a profile with a history service that is opted-in. + TestingProfile* profile = CreateProfile(SAFE_BROWSING_OPT_IN); + + // Add a download. + AddDownload(profile, CreateTestDownloadRow()); + + ExpectFoundTestDownload(RunLastDownloadFinder()); +} + +// Tests that there is no crash if the finder is deleted before results arrive. +TEST_F(LastDownloadFinderTest, DeleteBeforeResults) { + // Create a profile with a history service that is opted-in. + TestingProfile* profile = CreateProfile(SAFE_BROWSING_OPT_IN); + + // Add a download. + AddDownload(profile, CreateTestDownloadRow()); + + // Start a finder and kill it before the search completes. + safe_browsing::LastDownloadFinder::Create( + base::Bind(&LastDownloadFinderTest::NeverCalled, base::Unretained(this))) + .reset(); + + // Flush tasks on the history backend thread. + FlushHistoryBackend(profile); +} + +// Tests that a download in profile added after the search is begun is found. +TEST_F(LastDownloadFinderTest, AddProfileAfterStarting) { + // Create a profile with a history service that is opted-in. + CreateProfile(SAFE_BROWSING_OPT_IN); + + scoped_ptr<safe_browsing::ClientIncidentReport_DownloadDetails> last_download; + base::RunLoop run_loop; + + // Post a task that will create a second profile once the main loop is run. + base::MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&LastDownloadFinderTest::CreateProfileWithDownload, + base::Unretained(this))); + + // Create a finder that we expect will find a download in the second profile. + scoped_ptr<safe_browsing::LastDownloadFinder> finder( + safe_browsing::LastDownloadFinder::Create( + base::Bind(&LastDownloadFinderTest::OnLastDownload, + base::Unretained(this), + &last_download, + run_loop.QuitClosure()))); + + run_loop.Run(); + + ExpectFoundTestDownload(last_download.Pass()); +} diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index f871ffb..dbf5571 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -1875,6 +1875,8 @@ 'browser/safe_browsing/incident_report_uploader_impl.h', 'browser/safe_browsing/incident_reporting_service.cc', 'browser/safe_browsing/incident_reporting_service.h', + 'browser/safe_browsing/last_download_finder.cc', + 'browser/safe_browsing/last_download_finder.h', 'browser/safe_browsing/malware_details.cc', 'browser/safe_browsing/malware_details.h', 'browser/safe_browsing/malware_details_cache.cc', diff --git a/chrome/chrome_common.gypi b/chrome/chrome_common.gypi index 4125a86..fb1aee2 100644 --- a/chrome/chrome_common.gypi +++ b/chrome/chrome_common.gypi @@ -287,6 +287,7 @@ 'common_version', 'installer_util', 'metrics_proto', + 'safe_browsing_proto', '<(DEPTH)/base/base.gyp:base', '<(DEPTH)/base/base.gyp:base_i18n', '<(DEPTH)/base/base.gyp:base_prefs', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index 15dd4aa7..6752069 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -1251,6 +1251,7 @@ 'browser/safe_browsing/download_protection_service_unittest.cc', 'browser/safe_browsing/incident_report_uploader_impl_unittest.cc', 'browser/safe_browsing/incident_reporting_service_unittest.cc', + 'browser/safe_browsing/last_download_finder_unittest.cc', 'browser/safe_browsing/local_two_phase_testserver.cc', 'browser/safe_browsing/malware_details_unittest.cc', 'browser/safe_browsing/pe_image_reader_win_unittest.cc', diff --git a/chrome/common/BUILD.gn b/chrome/common/BUILD.gn index e4560b0..6cb0b5e 100644 --- a/chrome/common/BUILD.gn +++ b/chrome/common/BUILD.gn @@ -43,6 +43,7 @@ static_library("common") { "//chrome/common:constants", "//chrome/common/metrics/proto", "//chrome/common/net", + "//chrome/common/safe_browsing:proto", #"//components/cloud_devices:common", TODO(GYP) #"//components/json_schema", TODO(GYP) "//components/metrics", diff --git a/chrome/common/safe_browsing/download_protection_util.cc b/chrome/common/safe_browsing/download_protection_util.cc index 88d5cab..ee09cd1 100644 --- a/chrome/common/safe_browsing/download_protection_util.cc +++ b/chrome/common/safe_browsing/download_protection_util.cc @@ -2,7 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "chrome/common/safe_browsing/download_protection_util.h" + #include "base/files/file_path.h" +#include "base/logging.h" namespace safe_browsing { namespace download_protection_util { @@ -34,5 +37,19 @@ bool IsBinaryFile(const base::FilePath& file) { IsArchiveFile(file)); } +ClientDownloadRequest::DownloadType GetDownloadType( + const base::FilePath& file) { + DCHECK(IsBinaryFile(file)); + if (file.MatchesExtension(FILE_PATH_LITERAL(".apk"))) + return ClientDownloadRequest::ANDROID_APK; + else if (file.MatchesExtension(FILE_PATH_LITERAL(".crx"))) + return ClientDownloadRequest::CHROME_EXTENSION; + // For zip files, we use the ZIPPED_EXECUTABLE type since we will only send + // the pingback if we find an executable inside the zip archive. + else if (file.MatchesExtension(FILE_PATH_LITERAL(".zip"))) + return ClientDownloadRequest::ZIPPED_EXECUTABLE; + return ClientDownloadRequest::WIN_EXECUTABLE; +} + } // namespace download_protection_util } // namespace safe_browsing diff --git a/chrome/common/safe_browsing/download_protection_util.h b/chrome/common/safe_browsing/download_protection_util.h index 8a0ff7c..a9eaf53 100644 --- a/chrome/common/safe_browsing/download_protection_util.h +++ b/chrome/common/safe_browsing/download_protection_util.h @@ -5,6 +5,8 @@ #ifndef CHROME_COMMON_SAFE_BROWSING_DOWNLOAD_PROTECTION_UTIL_H_ #define CHROME_COMMON_SAFE_BROWSING_DOWNLOAD_PROTECTION_UTIL_H_ +#include "chrome/common/safe_browsing/csd.pb.h" + namespace base { class FilePath; } @@ -18,6 +20,10 @@ bool IsBinaryFile(const base::FilePath& file); // Returns true if the given file is a supported archive file type. bool IsArchiveFile(const base::FilePath& file); +// Returns the DownloadType of the file at |path|. This function is only valid +// for paths that satisfy IsBinaryFile() above. +ClientDownloadRequest::DownloadType GetDownloadType(const base::FilePath& file); + } // namespace download_protection_util } // namespace safe_browsing diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml index ea2df26..f554f77 100644 --- a/tools/metrics/histograms/histograms.xml +++ b/tools/metrics/histograms/histograms.xml @@ -25081,6 +25081,14 @@ Therefore, the affected-histogram name has to have at least one dot in it. </summary> </histogram> +<histogram name="SBIRS.FindDownloadedBinaryTime" units="milliseconds"> + <owner>grt@google.com</owner> + <summary> + The elapsed time to find the most recent binary download from all loaded + profiles when creating a safe browsing incident report. + </summary> +</histogram> + <histogram name="SBIRS.Incident" enum="IncidentType"> <owner>grt@google.com</owner> <summary> @@ -25122,11 +25130,11 @@ Therefore, the affected-histogram name has to have at least one dot in it. <summary>The elapsed time to upload a safe browsing incident report.</summary> </histogram> -<histogram name="SBIRS.UploadResult" enum="UploadResult"> +<histogram name="SBIRS.UploadResult" enum="ReportProcessingResult"> <owner>grt@google.com</owner> <summary> - The result of a report upload by the safe browsing incident reporting - service. + The result of an attempted report upload by the safe browsing incident + reporting service. </summary> </histogram> @@ -43316,6 +43324,26 @@ Therefore, the affected-histogram name has to have at least one dot in it. <int value="58" label="IDC_SPELLCHECK_SUGGESTION"/> </enum> +<enum name="ReportProcessingResult" type="int"> + <int value="0" label="Success">A report was created and uploaded</int> + <int value="1" label="Suppressed"> + A report was not uploaded because the CSD Whitelist killswitch was present + </int> + <int value="2" label="InvalidRequest"> + A report was not uploaded because it could not be serialized + </int> + <int value="3" label="Cancelled"> + A report upload was cancelled due to service shutdown + </int> + <int value="4" label="RequestFailed">A report upload failed</int> + <int value="5" label="InvalidResponse"> + The response from a report upload was invalid + </int> + <int value="6" label="NoDownload"> + A report was not uploaded because no binary download was found to report + </int> +</enum> + <enum name="ResolutionCategory" type="int"> <int value="0" label="RESOLVE_SUCCESS"/> <int value="1" label="RESOLVE_FAIL"/> @@ -45489,15 +45517,6 @@ Therefore, the affected-histogram name has to have at least one dot in it. <int value="3" label="AUTO_UPDATES_ONLY"/> </enum> -<enum name="UploadResult" type="int"> - <int value="0" label="Success"/> - <int value="1" label="Suppressed">CSD Whitelist killswitch present</int> - <int value="2" label="InvalidRequest"/> - <int value="3" label="Cancelled"/> - <int value="4" label="RequestFailed"/> - <int value="5" label="InvalidResponse"/> -</enum> - <enum name="UrlResolutionResult" type="int"> <int value="0" label="Absolute URL"/> <int value="1" label="Resolutions Differ"/> |