Index: chrome/browser/safe_browsing/download_protection_service.cc |
diff --git a/chrome/browser/safe_browsing/download_protection_service.cc b/chrome/browser/safe_browsing/download_protection_service.cc |
index c1ce0f33bb329a6398306de70edc597b01d2cb1f..668290b2191ca5c99424c77aaaebd77db1a96b97 100644 |
--- a/chrome/browser/safe_browsing/download_protection_service.cc |
+++ b/chrome/browser/safe_browsing/download_protection_service.cc |
@@ -8,11 +8,15 @@ |
#include "base/memory/scoped_ptr.h" |
#include "base/metrics/histogram.h" |
#include "base/stl_util.h" |
+#include "base/string_util.h" |
#include "chrome/browser/safe_browsing/safe_browsing_service.h" |
+#include "chrome/browser/safe_browsing/signature_util.h" |
#include "chrome/common/net/http_return.h" |
#include "chrome/common/safe_browsing/csd.pb.h" |
#include "content/browser/browser_thread.h" |
+#include "content/browser/download/download_item.h" |
#include "content/public/common/url_fetcher.h" |
+#include "content/public/common/url_fetcher_delegate.h" |
#include "net/base/load_flags.h" |
#include "net/url_request/url_request_context_getter.h" |
#include "net/url_request/url_request_status.h" |
@@ -22,63 +26,110 @@ namespace safe_browsing { |
const char DownloadProtectionService::kDownloadRequestUrl[] = |
"https://sb-ssl.google.com/safebrowsing/clientreport/download"; |
+namespace { |
+bool IsBinaryFile(const FilePath& file) { |
+ return (file.MatchesExtension(FILE_PATH_LITERAL(".exe")) || |
+ file.MatchesExtension(FILE_PATH_LITERAL(".cab")) || |
+ file.MatchesExtension(FILE_PATH_LITERAL(".msi"))); |
+} |
+} // namespace |
+ |
DownloadProtectionService::DownloadInfo::DownloadInfo() |
: total_bytes(0), user_initiated(false) {} |
DownloadProtectionService::DownloadInfo::~DownloadInfo() {} |
-DownloadProtectionService::DownloadProtectionService( |
- SafeBrowsingService* sb_service, |
- net::URLRequestContextGetter* request_context_getter) |
- : sb_service_(sb_service), |
- request_context_getter_(request_context_getter), |
- enabled_(false) {} |
- |
-DownloadProtectionService::~DownloadProtectionService() { |
- STLDeleteContainerPairFirstPointers(download_requests_.begin(), |
- download_requests_.end()); |
- download_requests_.clear(); |
+// static |
+DownloadProtectionService::DownloadInfo |
+DownloadProtectionService::DownloadInfo::FromDownloadItem( |
+ const DownloadItem& item) { |
+ DownloadInfo download_info; |
+ download_info.local_file = item.full_path(); |
+ download_info.download_url_chain = item.url_chain(); |
+ download_info.referrer_url = item.referrer_url(); |
+ // TODO(bryner): Fill in the hash (we shouldn't compute it again) |
+ download_info.total_bytes = item.total_bytes(); |
+ // TODO(bryner): Populate user_initiated |
+ return download_info; |
} |
-void DownloadProtectionService::SetEnabled(bool enabled) { |
- DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
- BrowserThread::PostTask( |
- BrowserThread::IO, |
- FROM_HERE, |
- base::Bind(&DownloadProtectionService::SetEnabledOnIOThread, |
- this, enabled)); |
-} |
- |
-void DownloadProtectionService::SetEnabledOnIOThread(bool enabled) { |
- DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
- if (enabled == enabled_) { |
- return; |
+class DownloadProtectionService::CheckClientDownloadRequest |
+ : public base::RefCountedThreadSafe< |
+ DownloadProtectionService::CheckClientDownloadRequest, |
+ BrowserThread::DeleteOnUIThread>, |
+ public content::URLFetcherDelegate { |
+ public: |
+ CheckClientDownloadRequest(const DownloadInfo& info, |
+ const CheckDownloadCallback& callback, |
+ DownloadProtectionService* service, |
+ SafeBrowsingService* sb_service) |
+ : info_(info), |
+ callback_(callback), |
+ service_(service), |
+ sb_service_(sb_service), |
+ pingback_enabled_(service_->enabled()) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
} |
- enabled_ = enabled; |
- if (!enabled_) { |
- for (DownloadRequests::iterator it = download_requests_.begin(); |
- it != download_requests_.end(); ++it) { |
- it->second.Run(SAFE); |
+ |
+ void Start() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ // TODO(noelutz): implement some cache to make sure we don't issue the same |
+ // request over and over again if a user downloads the same binary multiple |
+ // times. |
+ if (info_.download_url_chain.empty()) { |
+ RecordStats(REASON_INVALID_URL); |
+ PostFinishTask(SAFE); |
+ return; |
+ } |
+ const GURL& final_url = info_.download_url_chain.back(); |
+ if (!final_url.is_valid() || final_url.is_empty() || |
+ !final_url.SchemeIs("http")) { |
+ RecordStats(REASON_INVALID_URL); |
+ PostFinishTask(SAFE); |
+ return; // For now we only support HTTP download URLs. |
} |
- STLDeleteContainerPairFirstPointers(download_requests_.begin(), |
- download_requests_.end()); |
- download_requests_.clear(); |
- } |
-} |
-void DownloadProtectionService::OnURLFetchComplete( |
- const content::URLFetcher* source) { |
- DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
- scoped_ptr<const content::URLFetcher> s(source); |
- if (download_requests_.find(source) != download_requests_.end()) { |
- CheckDownloadCallback callback = download_requests_[source]; |
- download_requests_.erase(source); |
- if (!enabled_) { |
- // SafeBrowsing got disabled. We can't do anything. Note: the request |
- // object will be deleted. |
- RecordStats(REASON_SB_DISABLED); |
+ if (!IsBinaryFile(info_.local_file)) { |
+ RecordStats(REASON_NOT_BINARY_FILE); |
+ PostFinishTask(SAFE); |
return; |
} |
+ |
+ // Compute features from the file contents. Note that we record histograms |
+ // based on the result, so this runs regardless of whether the pingbacks |
+ // are enabled. Since we do blocking I/O, this happens on the file thread. |
+ BrowserThread::PostTask( |
+ BrowserThread::FILE, |
+ FROM_HERE, |
+ base::Bind(&CheckClientDownloadRequest::ExtractFileFeatures, this)); |
+ } |
+ |
+ // Canceling a request will cause us to always report the result as SAFE. |
+ // In addition, the DownloadProtectionService will not be notified when the |
+ // request finishes, so it must drop its reference after calling Cancel. |
+ void Cancel() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ service_ = NULL; |
+ if (fetcher_.get()) { |
+ // The DownloadProtectionService is going to release its reference, so we |
+ // might be destroyed before the URLFetcher completes. Cancel the |
+ // fetcher so it does not try to invoke OnURLFetchComplete. |
+ FinishRequest(SAFE); |
+ fetcher_.reset(); |
+ } |
+ // Note: If there is no fetcher, then some callback is still holding a |
+ // reference to this object. We'll eventually wind up in some method on |
+ // the UI thread that will call FinishRequest() and run the callback. |
+ } |
+ |
+ // From the content::URLFetcherDelegate interface. |
+ virtual void OnURLFetchComplete(const content::URLFetcher* source) OVERRIDE { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ DCHECK_EQ(source, fetcher_.get()); |
+ VLOG(2) << "Received a response for URL: " |
+ << info_.download_url_chain.back() << ": success=" |
+ << source->GetStatus().is_success() << " response_code=" |
+ << source->GetResponseCode(); |
DownloadCheckResultReason reason = REASON_MAX; |
reason = REASON_SERVER_PING_FAILED; |
if (source->GetStatus().is_success() && |
@@ -92,118 +143,207 @@ void DownloadProtectionService::OnURLFetchComplete( |
reason = REASON_INVALID_RESPONSE_PROTO; |
} |
} |
- BrowserThread::PostTask( |
- BrowserThread::UI, |
- FROM_HERE, |
- base::Bind(&DownloadProtectionService::EndCheckClientDownload, |
- this, SAFE, reason, callback)); |
- } else { |
- NOTREACHED(); |
- } |
-} |
-bool DownloadProtectionService::CheckClientDownload( |
- const DownloadInfo& info, |
- const CheckDownloadCallback& callback) { |
- // TODO(noelutz): implement some cache to make sure we don't issue the same |
- // request over and over again if a user downloads the same binary multiple |
- // times. |
- if (info.download_url_chain.empty()) { |
- RecordStats(REASON_INVALID_URL); |
- return true; |
- } |
- const GURL& final_url = info.download_url_chain.back(); |
- if (!final_url.is_valid() || final_url.is_empty() || |
- !final_url.SchemeIs("http")) { |
- RecordStats(REASON_INVALID_URL); |
- return true; // For now we only support HTTP download URLs. |
+ if (reason != REASON_MAX) { |
+ RecordStats(reason); |
+ } |
+ // We don't need the fetcher anymore. |
+ fetcher_.reset(); |
+ FinishRequest(SAFE); |
} |
- // TODO(noelutz): DownloadInfo should also contain the IP address of every |
- // URL in the redirect chain. We also should check whether the download URL |
- // is hosted on the internal network. |
- BrowserThread::PostTask( |
- BrowserThread::IO, |
- FROM_HERE, |
- base::Bind(&DownloadProtectionService::StartCheckClientDownload, |
- this, info, callback)); |
- return false; |
-} |
-void DownloadProtectionService::StartCheckClientDownload( |
- const DownloadInfo& info, |
- const CheckDownloadCallback& callback) { |
- DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
- if (!enabled_ || !sb_service_.get()) { |
- // This is a hard fail. We won't even call the callback in this case. |
- RecordStats(REASON_SB_DISABLED); |
- return; |
+ private: |
+ friend struct BrowserThread::DeleteOnThread<BrowserThread::UI>; |
+ friend class DeleteTask<CheckClientDownloadRequest>; |
+ |
+ virtual ~CheckClientDownloadRequest() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
} |
- DownloadCheckResultReason reason = REASON_MAX; |
- for (size_t i = 0; i < info.download_url_chain.size(); ++i) { |
- if (sb_service_->MatchDownloadWhitelistUrl(info.download_url_chain[i])) { |
- reason = REASON_WHITELISTED_URL; |
- break; |
+ |
+ void ExtractFileFeatures() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); |
+ bool is_signed; |
+ if (safe_browsing::signature_util::IsSigned(info_.local_file)) { |
+ VLOG(2) << "Downloaded a signed binary: " << info_.local_file.value(); |
+ is_signed = true; |
+ } else { |
+ VLOG(2) << "Downloaded an unsigned binary: " << info_.local_file.value(); |
+ is_signed = false; |
} |
+ UMA_HISTOGRAM_BOOLEAN("SBClientDownload.SignedBinaryDownload", is_signed); |
+ |
+ // TODO(noelutz): DownloadInfo should also contain the IP address of every |
+ // URL in the redirect chain. We also should check whether the download |
+ // URL is hosted on the internal network. |
+ BrowserThread::PostTask( |
+ BrowserThread::IO, |
+ FROM_HERE, |
+ base::Bind(&CheckClientDownloadRequest::CheckWhitelists, this)); |
} |
- if (sb_service_->MatchDownloadWhitelistUrl(info.referrer_url)) { |
- reason = REASON_WHITELISTED_REFERRER; |
- } |
- // TODO(noelutz): check signature and CA against whitelist. |
- |
- ClientDownloadRequest request; |
- request.set_url(info.download_url_chain.back().spec()); |
- request.mutable_digests()->set_sha256(info.sha256_hash); |
- request.set_length(info.total_bytes); |
- for (size_t i = 0; i < info.download_url_chain.size(); ++i) { |
- ClientDownloadRequest::Resource* resource = request.add_resources(); |
- resource->set_url(info.download_url_chain[i].spec()); |
- if (i == info.download_url_chain.size() - 1) { |
- // The last URL in the chain is the download URL. |
- resource->set_type(ClientDownloadRequest::DOWNLOAD_URL); |
- resource->set_referrer(info.referrer_url.spec()); |
+ |
+ void CheckWhitelists() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
+ DownloadCheckResultReason reason = REASON_MAX; |
+ if (!pingback_enabled_ || !sb_service_.get()) { |
+ reason = REASON_SB_DISABLED; |
} else { |
- resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT); |
+ for (size_t i = 0; i < info_.download_url_chain.size(); ++i) { |
+ const GURL& url = info_.download_url_chain[i]; |
+ if (url.is_valid() && sb_service_->MatchDownloadWhitelistUrl(url)) { |
+ reason = REASON_WHITELISTED_URL; |
+ break; |
+ } |
+ } |
+ if (info_.referrer_url.is_valid() && |
+ sb_service_->MatchDownloadWhitelistUrl(info_.referrer_url)) { |
+ reason = REASON_WHITELISTED_REFERRER; |
+ } |
+ } |
+ if (reason != REASON_MAX) { |
+ RecordStats(reason); |
+ PostFinishTask(SAFE); |
+ return; |
} |
- // TODO(noelutz): fill out the remote IP addresses. |
+ |
+ // TODO(noelutz): check signature and CA against whitelist. |
+ |
+ // The URLFetcher is owned by the UI thread, so post a message to |
+ // start the pingback. |
+ BrowserThread::PostTask( |
+ BrowserThread::UI, |
+ FROM_HERE, |
+ base::Bind(&CheckClientDownloadRequest::SendRequest, this)); |
} |
- request.set_user_initiated(info.user_initiated); |
- std::string request_data; |
- if (!request.SerializeToString(&request_data)) { |
- reason = REASON_INVALID_REQUEST_PROTO; |
+ |
+ void SendRequest() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ |
+ // This is our last chance to check whether the request has been canceled |
+ // before sending it. |
+ if (!service_) { |
+ FinishRequest(SAFE); |
+ return; |
+ } |
+ |
+ ClientDownloadRequest request; |
+ request.set_url(info_.download_url_chain.back().spec()); |
+ request.mutable_digests()->set_sha256(info_.sha256_hash); |
+ request.set_length(info_.total_bytes); |
+ for (size_t i = 0; i < info_.download_url_chain.size(); ++i) { |
+ ClientDownloadRequest::Resource* resource = request.add_resources(); |
+ resource->set_url(info_.download_url_chain[i].spec()); |
+ if (i == info_.download_url_chain.size() - 1) { |
+ // The last URL in the chain is the download URL. |
+ resource->set_type(ClientDownloadRequest::DOWNLOAD_URL); |
+ resource->set_referrer(info_.referrer_url.spec()); |
+ } else { |
+ resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT); |
+ } |
+ // TODO(noelutz): fill out the remote IP addresses. |
+ } |
+ request.set_user_initiated(info_.user_initiated); |
+ std::string request_data; |
+ if (!request.SerializeToString(&request_data)) { |
+ RecordStats(REASON_INVALID_REQUEST_PROTO); |
+ FinishRequest(SAFE); |
+ return; |
+ } |
+ |
+ VLOG(2) << "Sending a request for URL: " |
+ << info_.download_url_chain.back(); |
+ fetcher_.reset(content::URLFetcher::Create(0 /* ID used for testing */, |
+ GURL(kDownloadRequestUrl), |
+ content::URLFetcher::POST, |
+ this)); |
+ fetcher_->SetLoadFlags(net::LOAD_DISABLE_CACHE); |
+ fetcher_->SetRequestContext(service_->request_context_getter_.get()); |
+ fetcher_->SetUploadData("application/octet-stream", request_data); |
+ fetcher_->Start(); |
} |
- if (reason != REASON_MAX) { |
- // We stop here because the download is considered safe. |
+ void PostFinishTask(DownloadCheckResult result) { |
BrowserThread::PostTask( |
BrowserThread::UI, |
FROM_HERE, |
- base::Bind(&DownloadProtectionService::EndCheckClientDownload, |
- this, SAFE, reason, callback)); |
- return; |
+ base::Bind(&CheckClientDownloadRequest::FinishRequest, this, result)); |
} |
- content::URLFetcher* fetcher = content::URLFetcher::Create( |
- 0 /* ID used for testing */, GURL(kDownloadRequestUrl), |
- content::URLFetcher::POST, this); |
- download_requests_[fetcher] = callback; |
- fetcher->SetLoadFlags(net::LOAD_DISABLE_CACHE); |
- fetcher->SetRequestContext(request_context_getter_.get()); |
- fetcher->SetUploadData("application/octet-stream", request_data); |
- fetcher->Start(); |
+ void FinishRequest(DownloadCheckResult result) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ if (service_) { |
+ callback_.Run(result); |
+ service_->RequestFinished(this); |
+ } else { |
+ callback_.Run(SAFE); |
+ } |
+ } |
+ |
+ void RecordStats(DownloadCheckResultReason reason) { |
+ UMA_HISTOGRAM_ENUMERATION("SBClientDownload.CheckDownloadStats", |
+ reason, |
+ REASON_MAX); |
+ } |
+ |
+ DownloadInfo info_; |
+ CheckDownloadCallback callback_; |
+ // Will be NULL if the request has been canceled. |
+ DownloadProtectionService* service_; |
+ scoped_refptr<SafeBrowsingService> sb_service_; |
+ bool pingback_enabled_; |
+ scoped_ptr<content::URLFetcher> fetcher_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(CheckClientDownloadRequest); |
+}; |
+ |
+DownloadProtectionService::DownloadProtectionService( |
+ SafeBrowsingService* sb_service, |
+ net::URLRequestContextGetter* request_context_getter) |
+ : sb_service_(sb_service), |
+ request_context_getter_(request_context_getter), |
+ enabled_(false) {} |
+ |
+DownloadProtectionService::~DownloadProtectionService() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ CancelPendingRequests(); |
+} |
+ |
+void DownloadProtectionService::SetEnabled(bool enabled) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ if (enabled == enabled_) { |
+ return; |
+ } |
+ enabled_ = enabled; |
+ if (!enabled_) { |
+ CancelPendingRequests(); |
+ } |
} |
-void DownloadProtectionService::EndCheckClientDownload( |
- DownloadCheckResult result, |
- DownloadCheckResultReason reason, |
+void DownloadProtectionService::CheckClientDownload( |
+ const DownloadProtectionService::DownloadInfo& info, |
const CheckDownloadCallback& callback) { |
+ scoped_refptr<CheckClientDownloadRequest> request( |
+ new CheckClientDownloadRequest(info, callback, this, sb_service_)); |
+ download_requests_.insert(request); |
+ request->Start(); |
+} |
+ |
+void DownloadProtectionService::CancelPendingRequests() { |
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
- RecordStats(reason); |
- callback.Run(result); |
+ for (std::set<scoped_refptr<CheckClientDownloadRequest> >::iterator it = |
+ download_requests_.begin(); |
+ it != download_requests_.end(); ++it) { |
+ (*it)->Cancel(); |
+ } |
+ download_requests_.clear(); |
} |
-void DownloadProtectionService::RecordStats(DownloadCheckResultReason reason) { |
- UMA_HISTOGRAM_ENUMERATION("SBClientDownload.CheckDownloadStats", |
- reason, |
- REASON_MAX); |
+void DownloadProtectionService::RequestFinished( |
+ CheckClientDownloadRequest* request) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ std::set<scoped_refptr<CheckClientDownloadRequest> >::iterator it = |
+ download_requests_.find(request); |
+ DCHECK(it != download_requests_.end()); |
+ download_requests_.erase(*it); |
} |
+ |
} // namespace safe_browsing |