| // Copyright 2018 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/telemetry/android/android_telemetry_service.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/task/post_task.h" |
| #include "chrome/browser/history/history_service_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/safe_browsing/safe_browsing_navigation_observer_manager.h" |
| #include "chrome/browser/safe_browsing/safe_browsing_service.h" |
| #include "components/keyed_service/core/service_access_type.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/safe_browsing/common/safe_browsing_prefs.h" |
| #include "components/safe_browsing/db/database_manager.h" |
| #include "components/safe_browsing/features.h" |
| #include "components/safe_browsing/ping_manager.h" |
| #include "components/safe_browsing/web_ui/safe_browsing_ui.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/download_item_utils.h" |
| #include "content/public/browser/download_manager.h" |
| #include "content/public/browser/web_contents.h" |
| |
| namespace safe_browsing { |
| |
| namespace { |
| // MIME-type for APKs. |
| const char kApkMimeType[] = "application/vnd.android.package-archive"; |
| |
| // The number of user gestures to trace back for the referrer chain. |
| const int kAndroidTelemetryUserGestureLimit = 2; |
| |
| bool IsFeatureEnabled() { |
| return base::FeatureList::IsEnabled(safe_browsing::kTelemetryForApkDownloads); |
| } |
| |
| // Enumerates the possibilities for whether the CSBRR report was sent (or not). |
| enum class ApkDownloadTelemetryOutcome { |
| NOT_SENT_SAFE_BROWSING_NOT_ENABLED = 0, |
| // |web_contents| was nullptr. This happens sometimes when downloads are |
| // resumed but it's not clear exactly when. |
| NOT_SENT_MISSING_WEB_CONTENTS = 1, |
| // No ping sent because the user is in Incognito mode. |
| NOT_SENT_INCOGNITO = 2, |
| // No ping sent because the user hasn't enabled extended reporting. |
| NOT_SENT_EXTENDED_REPORTING_DISABLED = 3, |
| // Download was cancelled. |
| NOT_SENT_DOWNLOAD_CANCELLED = 4, |
| // Failed to serialize the report. |
| NOT_SENT_FAILED_TO_SERIALIZE = 5, |
| // Feature not enabled so don't send. |
| NOT_SENT_FEATURE_NOT_ENABLED = 6, |
| // Download completed. Ping sent. |
| SENT = 7, |
| |
| kMaxValue = SENT |
| }; |
| |
| void RecordApkDownloadTelemetryOutcome(ApkDownloadTelemetryOutcome outcome) { |
| UMA_HISTOGRAM_ENUMERATION("SafeBrowsing.AndroidTelemetry.ApkDownload.Outcome", |
| outcome); |
| } |
| |
| enum class ApkDownloadTelemetryIncompleteReason { |
| // |web_contents| was nullptr. This happens sometimes when downloads are |
| // resumed but it's not clear exactly when. |
| MISSING_WEB_CONTENTS = 0, |
| // Navigation manager wasn't ready yet to provide the referrer chain. |
| SB_NAVIGATION_MANAGER_NOT_READY = 1, |
| // Full referrer chain captured. |
| COMPLETE = 2, |
| |
| kMaxValue = COMPLETE, |
| }; |
| |
| void RecordApkDownloadTelemetryIncompleteReason( |
| ApkDownloadTelemetryIncompleteReason reason) { |
| UMA_HISTOGRAM_ENUMERATION( |
| "SafeBrowsing.AndroidTelemetry.ApkDownload.IncompleteReason", reason); |
| } |
| |
| } // namespace |
| |
| AndroidTelemetryService::AndroidTelemetryService( |
| SafeBrowsingService* sb_service, |
| Profile* profile) |
| : TelemetryService(), |
| profile_(profile), |
| sb_service_(sb_service), |
| weak_ptr_factory_(this) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(profile_); |
| DCHECK(sb_service_); |
| |
| content::DownloadManager* download_manager = |
| content::BrowserContext::GetDownloadManager(profile_); |
| if (download_manager) { |
| // Look for new downloads being created. |
| download_manager->AddObserver(this); |
| } |
| } |
| |
| AndroidTelemetryService::~AndroidTelemetryService() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| content::DownloadManager* download_manager = |
| content::BrowserContext::GetDownloadManager(profile_); |
| if (download_manager) { |
| download_manager->RemoveObserver(this); |
| } |
| } |
| |
| void AndroidTelemetryService::OnDownloadCreated( |
| content::DownloadManager* manager, |
| download::DownloadItem* item) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (item->GetMimeType() != kApkMimeType) { |
| return; |
| } |
| |
| if (!CanSendPing(item)) { |
| return; |
| } |
| |
| // The report can be sent. Try capturing the safety net ID. This should |
| // complete before the download completes, but is not guaranteed, That's OK. |
| MaybeCaptureSafetyNetId(); |
| |
| item->AddObserver(this); |
| } |
| |
| void AndroidTelemetryService::OnDownloadUpdated(download::DownloadItem* item) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(IsFeatureEnabled()); |
| DCHECK_EQ(kApkMimeType, item->GetMimeType()); |
| |
| if (item->GetState() == download::DownloadItem::COMPLETE) { |
| // Download completed. Send report. |
| MaybeSendApkDownloadReport(item); |
| } else if (item->GetState() == download::DownloadItem::CANCELLED) { |
| RecordApkDownloadTelemetryOutcome( |
| ApkDownloadTelemetryOutcome::NOT_SENT_DOWNLOAD_CANCELLED); |
| } |
| } |
| |
| bool AndroidTelemetryService::CanSendPing(download::DownloadItem* item) { |
| content::WebContents* web_contents = |
| content::DownloadItemUtils::GetWebContents(item); |
| if (!web_contents) { |
| // TODO(vakh): This can happen sometimes when a download resumes on a |
| // browser re-launch. Identify this case and document it better. |
| RecordApkDownloadTelemetryOutcome( |
| ApkDownloadTelemetryOutcome::NOT_SENT_MISSING_WEB_CONTENTS); |
| return false; |
| } |
| |
| if (!IsSafeBrowsingEnabled()) { |
| RecordApkDownloadTelemetryOutcome( |
| ApkDownloadTelemetryOutcome::NOT_SENT_SAFE_BROWSING_NOT_ENABLED); |
| return false; |
| } |
| |
| if (web_contents->GetBrowserContext()->IsOffTheRecord()) { |
| RecordApkDownloadTelemetryOutcome( |
| ApkDownloadTelemetryOutcome::NOT_SENT_INCOGNITO); |
| return false; |
| } |
| |
| if (!IsExtendedReportingEnabled(*GetPrefs())) { |
| RecordApkDownloadTelemetryOutcome( |
| ApkDownloadTelemetryOutcome::NOT_SENT_EXTENDED_REPORTING_DISABLED); |
| return false; |
| } |
| |
| if (!IsFeatureEnabled()) { |
| RecordApkDownloadTelemetryOutcome( |
| ApkDownloadTelemetryOutcome::NOT_SENT_FEATURE_NOT_ENABLED); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| const PrefService* AndroidTelemetryService::GetPrefs() { |
| return profile_->GetPrefs(); |
| } |
| |
| bool AndroidTelemetryService::IsSafeBrowsingEnabled() { |
| return GetPrefs()->GetBoolean(prefs::kSafeBrowsingEnabled); |
| } |
| |
| void AndroidTelemetryService::FillReferrerChain( |
| content::WebContents* web_contents, |
| ClientSafeBrowsingReportRequest* report) { |
| if (!SafeBrowsingNavigationObserverManager::IsEnabledAndReady(profile_)) { |
| RecordApkDownloadTelemetryIncompleteReason( |
| ApkDownloadTelemetryIncompleteReason::SB_NAVIGATION_MANAGER_NOT_READY); |
| return; |
| } |
| |
| RecordApkDownloadTelemetryIncompleteReason( |
| web_contents |
| ? ApkDownloadTelemetryIncompleteReason::COMPLETE |
| : ApkDownloadTelemetryIncompleteReason::MISSING_WEB_CONTENTS); |
| SafeBrowsingNavigationObserverManager::AttributionResult result = |
| sb_service_->navigation_observer_manager() |
| ->IdentifyReferrerChainByWebContents( |
| web_contents, kAndroidTelemetryUserGestureLimit, |
| report->mutable_referrer_chain()); |
| |
| size_t referrer_chain_length = report->referrer_chain().size(); |
| UMA_HISTOGRAM_COUNTS_100( |
| "SafeBrowsing.ReferrerURLChainSize.ApkDownloadTelemetry", |
| referrer_chain_length); |
| UMA_HISTOGRAM_ENUMERATION( |
| "SafeBrowsing.ReferrerAttributionResult.ApkDownloadTelemetry", result, |
| SafeBrowsingNavigationObserverManager::ATTRIBUTION_FAILURE_TYPE_MAX); |
| |
| // Determines how many recent navigation events to append to referrer chain. |
| size_t recent_navigations_to_collect = |
| profile_ ? SafeBrowsingNavigationObserverManager:: |
| CountOfRecentNavigationsToAppend(*profile_, result) |
| : 0u; |
| sb_service_->navigation_observer_manager()->AppendRecentNavigations( |
| recent_navigations_to_collect, report->mutable_referrer_chain()); |
| } |
| |
| void AndroidTelemetryService::MaybeCaptureSafetyNetId() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(sb_service_->database_manager()); |
| if (!safety_net_id_on_ui_thread_.empty()) { |
| return; |
| } |
| |
| base::PostTaskWithTraitsAndReplyWithResult( |
| FROM_HERE, {content::BrowserThread::IO}, |
| base::BindOnce(&SafeBrowsingDatabaseManager::GetSafetyNetId, |
| sb_service_->database_manager()), |
| base::BindOnce(&AndroidTelemetryService::SetSafetyNetIdOnUIThread, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void AndroidTelemetryService::MaybeSendApkDownloadReport( |
| download::DownloadItem* item) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| DCHECK(item->IsDone()); |
| |
| std::unique_ptr<ClientSafeBrowsingReportRequest> report( |
| new ClientSafeBrowsingReportRequest()); |
| report->set_type( |
| safe_browsing::ClientSafeBrowsingReportRequest::APK_DOWNLOAD); |
| report->set_url(item->GetOriginalUrl().spec()); |
| report->set_page_url(item->GetTabUrl().spec()); |
| |
| // Fill referrer chain. |
| content::WebContents* web_contents = |
| content::DownloadItemUtils::GetWebContents(item); |
| FillReferrerChain(web_contents, report.get()); |
| |
| // Fill DownloadItemInfo |
| ClientSafeBrowsingReportRequest::DownloadItemInfo* |
| mutable_download_item_info = report->mutable_download_item_info(); |
| mutable_download_item_info->set_url(item->GetURL().spec()); |
| mutable_download_item_info->mutable_digests()->set_sha256(item->GetHash()); |
| mutable_download_item_info->set_length(item->GetReceivedBytes()); |
| mutable_download_item_info->set_file_basename( |
| item->GetTargetFilePath().BaseName().value()); |
| report->set_safety_net_id(safety_net_id_on_ui_thread_); |
| |
| std::string serialized; |
| if (!report->SerializeToString(&serialized)) { |
| DLOG(ERROR) << "Unable to serialize the APK download telemetry report."; |
| RecordApkDownloadTelemetryOutcome( |
| ApkDownloadTelemetryOutcome::NOT_SENT_FAILED_TO_SERIALIZE); |
| return; |
| } |
| sb_service_->ping_manager()->ReportThreatDetails(serialized); |
| |
| base::PostTaskWithTraits( |
| FROM_HERE, {content::BrowserThread::UI}, |
| base::BindOnce(&WebUIInfoSingleton::AddToCSBRRsSent, |
| base::Unretained(WebUIInfoSingleton::GetInstance()), |
| std::move(report))); |
| |
| RecordApkDownloadTelemetryOutcome(ApkDownloadTelemetryOutcome::SENT); |
| } |
| |
| void AndroidTelemetryService::SetSafetyNetIdOnUIThread(const std::string& sid) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| safety_net_id_on_ui_thread_ = sid; |
| } |
| |
| } // namespace safe_browsing |