OLD | NEW |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/safe_browsing/download_protection_service.h" | 5 #include "chrome/browser/safe_browsing/download_protection_service.h" |
6 | 6 |
7 #include "base/bind.h" | 7 #include "base/bind.h" |
8 #include "base/memory/scoped_ptr.h" | 8 #include "base/memory/scoped_ptr.h" |
9 #include "base/metrics/histogram.h" | 9 #include "base/metrics/histogram.h" |
10 #include "base/stl_util.h" | 10 #include "base/stl_util.h" |
11 #include "base/string_util.h" | |
12 #include "chrome/browser/safe_browsing/safe_browsing_service.h" | 11 #include "chrome/browser/safe_browsing/safe_browsing_service.h" |
13 #include "chrome/browser/safe_browsing/signature_util.h" | |
14 #include "chrome/common/net/http_return.h" | 12 #include "chrome/common/net/http_return.h" |
15 #include "chrome/common/safe_browsing/csd.pb.h" | 13 #include "chrome/common/safe_browsing/csd.pb.h" |
16 #include "content/browser/browser_thread.h" | 14 #include "content/browser/browser_thread.h" |
17 #include "content/browser/download/download_item.h" | |
18 #include "content/public/common/url_fetcher.h" | 15 #include "content/public/common/url_fetcher.h" |
19 #include "content/public/common/url_fetcher_delegate.h" | |
20 #include "net/base/load_flags.h" | 16 #include "net/base/load_flags.h" |
21 #include "net/url_request/url_request_context_getter.h" | 17 #include "net/url_request/url_request_context_getter.h" |
22 #include "net/url_request/url_request_status.h" | 18 #include "net/url_request/url_request_status.h" |
23 | 19 |
24 namespace safe_browsing { | 20 namespace safe_browsing { |
25 | 21 |
26 const char DownloadProtectionService::kDownloadRequestUrl[] = | 22 const char DownloadProtectionService::kDownloadRequestUrl[] = |
27 "https://sb-ssl.google.com/safebrowsing/clientreport/download"; | 23 "https://sb-ssl.google.com/safebrowsing/clientreport/download"; |
28 | 24 |
29 namespace { | |
30 bool IsBinaryFile(const FilePath& file) { | |
31 return (file.MatchesExtension(FILE_PATH_LITERAL(".exe")) || | |
32 file.MatchesExtension(FILE_PATH_LITERAL(".cab")) || | |
33 file.MatchesExtension(FILE_PATH_LITERAL(".msi"))); | |
34 } | |
35 } // namespace | |
36 | |
37 DownloadProtectionService::DownloadInfo::DownloadInfo() | 25 DownloadProtectionService::DownloadInfo::DownloadInfo() |
38 : total_bytes(0), user_initiated(false) {} | 26 : total_bytes(0), user_initiated(false) {} |
39 | 27 |
40 DownloadProtectionService::DownloadInfo::~DownloadInfo() {} | 28 DownloadProtectionService::DownloadInfo::~DownloadInfo() {} |
41 | 29 |
42 // static | 30 DownloadProtectionService::DownloadProtectionService( |
43 DownloadProtectionService::DownloadInfo | 31 SafeBrowsingService* sb_service, |
44 DownloadProtectionService::DownloadInfo::FromDownloadItem( | 32 net::URLRequestContextGetter* request_context_getter) |
45 const DownloadItem& item) { | 33 : sb_service_(sb_service), |
46 DownloadInfo download_info; | 34 request_context_getter_(request_context_getter), |
47 download_info.local_file = item.full_path(); | 35 enabled_(false) {} |
48 download_info.download_url_chain = item.url_chain(); | 36 |
49 download_info.referrer_url = item.referrer_url(); | 37 DownloadProtectionService::~DownloadProtectionService() { |
50 // TODO(bryner): Fill in the hash (we shouldn't compute it again) | 38 STLDeleteContainerPairFirstPointers(download_requests_.begin(), |
51 download_info.total_bytes = item.total_bytes(); | 39 download_requests_.end()); |
52 // TODO(bryner): Populate user_initiated | 40 download_requests_.clear(); |
53 return download_info; | |
54 } | 41 } |
55 | 42 |
56 class DownloadProtectionService::CheckClientDownloadRequest | 43 void DownloadProtectionService::SetEnabled(bool enabled) { |
57 : public base::RefCountedThreadSafe< | 44 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
58 DownloadProtectionService::CheckClientDownloadRequest, | 45 BrowserThread::PostTask( |
59 BrowserThread::DeleteOnUIThread>, | 46 BrowserThread::IO, |
60 public content::URLFetcherDelegate { | 47 FROM_HERE, |
61 public: | 48 base::Bind(&DownloadProtectionService::SetEnabledOnIOThread, |
62 CheckClientDownloadRequest(const DownloadInfo& info, | 49 this, enabled)); |
63 const CheckDownloadCallback& callback, | 50 } |
64 DownloadProtectionService* service, | 51 |
65 SafeBrowsingService* sb_service) | 52 void DownloadProtectionService::SetEnabledOnIOThread(bool enabled) { |
66 : info_(info), | 53 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
67 callback_(callback), | 54 if (enabled == enabled_) { |
68 service_(service), | 55 return; |
69 sb_service_(sb_service), | |
70 pingback_enabled_(service_->enabled()) { | |
71 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
72 } | 56 } |
| 57 enabled_ = enabled; |
| 58 if (!enabled_) { |
| 59 for (DownloadRequests::iterator it = download_requests_.begin(); |
| 60 it != download_requests_.end(); ++it) { |
| 61 it->second.Run(SAFE); |
| 62 } |
| 63 STLDeleteContainerPairFirstPointers(download_requests_.begin(), |
| 64 download_requests_.end()); |
| 65 download_requests_.clear(); |
| 66 } |
| 67 } |
73 | 68 |
74 void Start() { | 69 void DownloadProtectionService::OnURLFetchComplete( |
75 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 70 const content::URLFetcher* source) { |
76 // TODO(noelutz): implement some cache to make sure we don't issue the same | 71 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
77 // request over and over again if a user downloads the same binary multiple | 72 scoped_ptr<const content::URLFetcher> s(source); |
78 // times. | 73 if (download_requests_.find(source) != download_requests_.end()) { |
79 if (info_.download_url_chain.empty()) { | 74 CheckDownloadCallback callback = download_requests_[source]; |
80 RecordStats(REASON_INVALID_URL); | 75 download_requests_.erase(source); |
81 PostFinishTask(SAFE); | 76 if (!enabled_) { |
| 77 // SafeBrowsing got disabled. We can't do anything. Note: the request |
| 78 // object will be deleted. |
| 79 RecordStats(REASON_SB_DISABLED); |
82 return; | 80 return; |
83 } | 81 } |
84 const GURL& final_url = info_.download_url_chain.back(); | |
85 if (!final_url.is_valid() || final_url.is_empty() || | |
86 !final_url.SchemeIs("http")) { | |
87 RecordStats(REASON_INVALID_URL); | |
88 PostFinishTask(SAFE); | |
89 return; // For now we only support HTTP download URLs. | |
90 } | |
91 | |
92 if (!IsBinaryFile(info_.local_file)) { | |
93 RecordStats(REASON_NOT_BINARY_FILE); | |
94 PostFinishTask(SAFE); | |
95 return; | |
96 } | |
97 | |
98 // Compute features from the file contents. Note that we record histograms | |
99 // based on the result, so this runs regardless of whether the pingbacks | |
100 // are enabled. Since we do blocking I/O, this happens on the file thread. | |
101 BrowserThread::PostTask( | |
102 BrowserThread::FILE, | |
103 FROM_HERE, | |
104 base::Bind(&CheckClientDownloadRequest::ExtractFileFeatures, this)); | |
105 } | |
106 | |
107 // Canceling a request will cause us to always report the result as SAFE. | |
108 // In addition, the DownloadProtectionService will not be notified when the | |
109 // request finishes, so it must drop its reference after calling Cancel. | |
110 void Cancel() { | |
111 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
112 service_ = NULL; | |
113 if (fetcher_.get()) { | |
114 // The DownloadProtectionService is going to release its reference, so we | |
115 // might be destroyed before the URLFetcher completes. Cancel the | |
116 // fetcher so it does not try to invoke OnURLFetchComplete. | |
117 FinishRequest(SAFE); | |
118 fetcher_.reset(); | |
119 } | |
120 // Note: If there is no fetcher, then some callback is still holding a | |
121 // reference to this object. We'll eventually wind up in some method on | |
122 // the UI thread that will call FinishRequest() and run the callback. | |
123 } | |
124 | |
125 // From the content::URLFetcherDelegate interface. | |
126 virtual void OnURLFetchComplete(const content::URLFetcher* source) OVERRIDE { | |
127 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
128 DCHECK_EQ(source, fetcher_.get()); | |
129 VLOG(2) << "Received a response for URL: " | |
130 << info_.download_url_chain.back() << ": success=" | |
131 << source->GetStatus().is_success() << " response_code=" | |
132 << source->GetResponseCode(); | |
133 DownloadCheckResultReason reason = REASON_MAX; | 82 DownloadCheckResultReason reason = REASON_MAX; |
134 reason = REASON_SERVER_PING_FAILED; | 83 reason = REASON_SERVER_PING_FAILED; |
135 if (source->GetStatus().is_success() && | 84 if (source->GetStatus().is_success() && |
136 RC_REQUEST_OK == source->GetResponseCode()) { | 85 RC_REQUEST_OK == source->GetResponseCode()) { |
137 std::string data; | 86 std::string data; |
138 source->GetResponseAsString(&data); | 87 source->GetResponseAsString(&data); |
139 if (data.size() > 0) { | 88 if (data.size() > 0) { |
140 // For now no matter what we'll always say the download is safe. | 89 // For now no matter what we'll always say the download is safe. |
141 // TODO(noelutz): Parse the response body to see exactly what's going | 90 // TODO(noelutz): Parse the response body to see exactly what's going |
142 // on. | 91 // on. |
143 reason = REASON_INVALID_RESPONSE_PROTO; | 92 reason = REASON_INVALID_RESPONSE_PROTO; |
144 } | 93 } |
145 } | 94 } |
146 | |
147 if (reason != REASON_MAX) { | |
148 RecordStats(reason); | |
149 } | |
150 // We don't need the fetcher anymore. | |
151 fetcher_.reset(); | |
152 FinishRequest(SAFE); | |
153 } | |
154 | |
155 private: | |
156 friend struct BrowserThread::DeleteOnThread<BrowserThread::UI>; | |
157 friend class DeleteTask<CheckClientDownloadRequest>; | |
158 | |
159 virtual ~CheckClientDownloadRequest() { | |
160 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
161 } | |
162 | |
163 void ExtractFileFeatures() { | |
164 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); | |
165 bool is_signed; | |
166 if (safe_browsing::signature_util::IsSigned(info_.local_file)) { | |
167 VLOG(2) << "Downloaded a signed binary: " << info_.local_file.value(); | |
168 is_signed = true; | |
169 } else { | |
170 VLOG(2) << "Downloaded an unsigned binary: " << info_.local_file.value(); | |
171 is_signed = false; | |
172 } | |
173 UMA_HISTOGRAM_BOOLEAN("SBClientDownload.SignedBinaryDownload", is_signed); | |
174 | |
175 // TODO(noelutz): DownloadInfo should also contain the IP address of every | |
176 // URL in the redirect chain. We also should check whether the download | |
177 // URL is hosted on the internal network. | |
178 BrowserThread::PostTask( | |
179 BrowserThread::IO, | |
180 FROM_HERE, | |
181 base::Bind(&CheckClientDownloadRequest::CheckWhitelists, this)); | |
182 } | |
183 | |
184 void CheckWhitelists() { | |
185 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); | |
186 DownloadCheckResultReason reason = REASON_MAX; | |
187 if (!pingback_enabled_ || !sb_service_.get()) { | |
188 reason = REASON_SB_DISABLED; | |
189 } else { | |
190 for (size_t i = 0; i < info_.download_url_chain.size(); ++i) { | |
191 const GURL& url = info_.download_url_chain[i]; | |
192 if (url.is_valid() && sb_service_->MatchDownloadWhitelistUrl(url)) { | |
193 reason = REASON_WHITELISTED_URL; | |
194 break; | |
195 } | |
196 } | |
197 if (info_.referrer_url.is_valid() && | |
198 sb_service_->MatchDownloadWhitelistUrl(info_.referrer_url)) { | |
199 reason = REASON_WHITELISTED_REFERRER; | |
200 } | |
201 } | |
202 if (reason != REASON_MAX) { | |
203 RecordStats(reason); | |
204 PostFinishTask(SAFE); | |
205 return; | |
206 } | |
207 | |
208 // TODO(noelutz): check signature and CA against whitelist. | |
209 | |
210 // The URLFetcher is owned by the UI thread, so post a message to | |
211 // start the pingback. | |
212 BrowserThread::PostTask( | 95 BrowserThread::PostTask( |
213 BrowserThread::UI, | 96 BrowserThread::UI, |
214 FROM_HERE, | 97 FROM_HERE, |
215 base::Bind(&CheckClientDownloadRequest::SendRequest, this)); | 98 base::Bind(&DownloadProtectionService::EndCheckClientDownload, |
| 99 this, SAFE, reason, callback)); |
| 100 } else { |
| 101 NOTREACHED(); |
| 102 } |
| 103 } |
| 104 |
| 105 bool DownloadProtectionService::CheckClientDownload( |
| 106 const DownloadInfo& info, |
| 107 const CheckDownloadCallback& callback) { |
| 108 // TODO(noelutz): implement some cache to make sure we don't issue the same |
| 109 // request over and over again if a user downloads the same binary multiple |
| 110 // times. |
| 111 if (info.download_url_chain.empty()) { |
| 112 RecordStats(REASON_INVALID_URL); |
| 113 return true; |
| 114 } |
| 115 const GURL& final_url = info.download_url_chain.back(); |
| 116 if (!final_url.is_valid() || final_url.is_empty() || |
| 117 !final_url.SchemeIs("http")) { |
| 118 RecordStats(REASON_INVALID_URL); |
| 119 return true; // For now we only support HTTP download URLs. |
| 120 } |
| 121 // TODO(noelutz): DownloadInfo should also contain the IP address of every |
| 122 // URL in the redirect chain. We also should check whether the download URL |
| 123 // is hosted on the internal network. |
| 124 BrowserThread::PostTask( |
| 125 BrowserThread::IO, |
| 126 FROM_HERE, |
| 127 base::Bind(&DownloadProtectionService::StartCheckClientDownload, |
| 128 this, info, callback)); |
| 129 return false; |
| 130 } |
| 131 |
| 132 void DownloadProtectionService::StartCheckClientDownload( |
| 133 const DownloadInfo& info, |
| 134 const CheckDownloadCallback& callback) { |
| 135 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); |
| 136 if (!enabled_ || !sb_service_.get()) { |
| 137 // This is a hard fail. We won't even call the callback in this case. |
| 138 RecordStats(REASON_SB_DISABLED); |
| 139 return; |
| 140 } |
| 141 DownloadCheckResultReason reason = REASON_MAX; |
| 142 for (size_t i = 0; i < info.download_url_chain.size(); ++i) { |
| 143 if (sb_service_->MatchDownloadWhitelistUrl(info.download_url_chain[i])) { |
| 144 reason = REASON_WHITELISTED_URL; |
| 145 break; |
| 146 } |
| 147 } |
| 148 if (sb_service_->MatchDownloadWhitelistUrl(info.referrer_url)) { |
| 149 reason = REASON_WHITELISTED_REFERRER; |
| 150 } |
| 151 // TODO(noelutz): check signature and CA against whitelist. |
| 152 |
| 153 ClientDownloadRequest request; |
| 154 request.set_url(info.download_url_chain.back().spec()); |
| 155 request.mutable_digests()->set_sha256(info.sha256_hash); |
| 156 request.set_length(info.total_bytes); |
| 157 for (size_t i = 0; i < info.download_url_chain.size(); ++i) { |
| 158 ClientDownloadRequest::Resource* resource = request.add_resources(); |
| 159 resource->set_url(info.download_url_chain[i].spec()); |
| 160 if (i == info.download_url_chain.size() - 1) { |
| 161 // The last URL in the chain is the download URL. |
| 162 resource->set_type(ClientDownloadRequest::DOWNLOAD_URL); |
| 163 resource->set_referrer(info.referrer_url.spec()); |
| 164 } else { |
| 165 resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT); |
| 166 } |
| 167 // TODO(noelutz): fill out the remote IP addresses. |
| 168 } |
| 169 request.set_user_initiated(info.user_initiated); |
| 170 std::string request_data; |
| 171 if (!request.SerializeToString(&request_data)) { |
| 172 reason = REASON_INVALID_REQUEST_PROTO; |
216 } | 173 } |
217 | 174 |
218 void SendRequest() { | 175 if (reason != REASON_MAX) { |
219 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 176 // We stop here because the download is considered safe. |
220 | |
221 // This is our last chance to check whether the request has been canceled | |
222 // before sending it. | |
223 if (!service_) { | |
224 FinishRequest(SAFE); | |
225 return; | |
226 } | |
227 | |
228 ClientDownloadRequest request; | |
229 request.set_url(info_.download_url_chain.back().spec()); | |
230 request.mutable_digests()->set_sha256(info_.sha256_hash); | |
231 request.set_length(info_.total_bytes); | |
232 for (size_t i = 0; i < info_.download_url_chain.size(); ++i) { | |
233 ClientDownloadRequest::Resource* resource = request.add_resources(); | |
234 resource->set_url(info_.download_url_chain[i].spec()); | |
235 if (i == info_.download_url_chain.size() - 1) { | |
236 // The last URL in the chain is the download URL. | |
237 resource->set_type(ClientDownloadRequest::DOWNLOAD_URL); | |
238 resource->set_referrer(info_.referrer_url.spec()); | |
239 } else { | |
240 resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT); | |
241 } | |
242 // TODO(noelutz): fill out the remote IP addresses. | |
243 } | |
244 request.set_user_initiated(info_.user_initiated); | |
245 std::string request_data; | |
246 if (!request.SerializeToString(&request_data)) { | |
247 RecordStats(REASON_INVALID_REQUEST_PROTO); | |
248 FinishRequest(SAFE); | |
249 return; | |
250 } | |
251 | |
252 VLOG(2) << "Sending a request for URL: " | |
253 << info_.download_url_chain.back(); | |
254 fetcher_.reset(content::URLFetcher::Create(0 /* ID used for testing */, | |
255 GURL(kDownloadRequestUrl), | |
256 content::URLFetcher::POST, | |
257 this)); | |
258 fetcher_->SetLoadFlags(net::LOAD_DISABLE_CACHE); | |
259 fetcher_->SetRequestContext(service_->request_context_getter_.get()); | |
260 fetcher_->SetUploadData("application/octet-stream", request_data); | |
261 fetcher_->Start(); | |
262 } | |
263 | |
264 void PostFinishTask(DownloadCheckResult result) { | |
265 BrowserThread::PostTask( | 177 BrowserThread::PostTask( |
266 BrowserThread::UI, | 178 BrowserThread::UI, |
267 FROM_HERE, | 179 FROM_HERE, |
268 base::Bind(&CheckClientDownloadRequest::FinishRequest, this, result)); | 180 base::Bind(&DownloadProtectionService::EndCheckClientDownload, |
| 181 this, SAFE, reason, callback)); |
| 182 return; |
269 } | 183 } |
270 | 184 |
271 void FinishRequest(DownloadCheckResult result) { | 185 content::URLFetcher* fetcher = content::URLFetcher::Create( |
272 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 186 0 /* ID used for testing */, GURL(kDownloadRequestUrl), |
273 if (service_) { | 187 content::URLFetcher::POST, this); |
274 callback_.Run(result); | 188 download_requests_[fetcher] = callback; |
275 service_->RequestFinished(this); | 189 fetcher->SetLoadFlags(net::LOAD_DISABLE_CACHE); |
276 } else { | 190 fetcher->SetRequestContext(request_context_getter_.get()); |
277 callback_.Run(SAFE); | 191 fetcher->SetUploadData("application/octet-stream", request_data); |
278 } | 192 fetcher->Start(); |
279 } | |
280 | |
281 void RecordStats(DownloadCheckResultReason reason) { | |
282 UMA_HISTOGRAM_ENUMERATION("SBClientDownload.CheckDownloadStats", | |
283 reason, | |
284 REASON_MAX); | |
285 } | |
286 | |
287 DownloadInfo info_; | |
288 CheckDownloadCallback callback_; | |
289 // Will be NULL if the request has been canceled. | |
290 DownloadProtectionService* service_; | |
291 scoped_refptr<SafeBrowsingService> sb_service_; | |
292 bool pingback_enabled_; | |
293 scoped_ptr<content::URLFetcher> fetcher_; | |
294 | |
295 DISALLOW_COPY_AND_ASSIGN(CheckClientDownloadRequest); | |
296 }; | |
297 | |
298 DownloadProtectionService::DownloadProtectionService( | |
299 SafeBrowsingService* sb_service, | |
300 net::URLRequestContextGetter* request_context_getter) | |
301 : sb_service_(sb_service), | |
302 request_context_getter_(request_context_getter), | |
303 enabled_(false) {} | |
304 | |
305 DownloadProtectionService::~DownloadProtectionService() { | |
306 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
307 CancelPendingRequests(); | |
308 } | 193 } |
309 | 194 |
310 void DownloadProtectionService::SetEnabled(bool enabled) { | 195 void DownloadProtectionService::EndCheckClientDownload( |
| 196 DownloadCheckResult result, |
| 197 DownloadCheckResultReason reason, |
| 198 const CheckDownloadCallback& callback) { |
311 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 199 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
312 if (enabled == enabled_) { | 200 RecordStats(reason); |
313 return; | 201 callback.Run(result); |
314 } | |
315 enabled_ = enabled; | |
316 if (!enabled_) { | |
317 CancelPendingRequests(); | |
318 } | |
319 } | 202 } |
320 | 203 |
321 void DownloadProtectionService::CheckClientDownload( | 204 void DownloadProtectionService::RecordStats(DownloadCheckResultReason reason) { |
322 const DownloadProtectionService::DownloadInfo& info, | 205 UMA_HISTOGRAM_ENUMERATION("SBClientDownload.CheckDownloadStats", |
323 const CheckDownloadCallback& callback) { | 206 reason, |
324 scoped_refptr<CheckClientDownloadRequest> request( | 207 REASON_MAX); |
325 new CheckClientDownloadRequest(info, callback, this, sb_service_)); | |
326 download_requests_.insert(request); | |
327 request->Start(); | |
328 } | 208 } |
329 | |
330 void DownloadProtectionService::CancelPendingRequests() { | |
331 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
332 for (std::set<scoped_refptr<CheckClientDownloadRequest> >::iterator it = | |
333 download_requests_.begin(); | |
334 it != download_requests_.end(); ++it) { | |
335 (*it)->Cancel(); | |
336 } | |
337 download_requests_.clear(); | |
338 } | |
339 | |
340 void DownloadProtectionService::RequestFinished( | |
341 CheckClientDownloadRequest* request) { | |
342 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
343 std::set<scoped_refptr<CheckClientDownloadRequest> >::iterator it = | |
344 download_requests_.find(request); | |
345 DCHECK(it != download_requests_.end()); | |
346 download_requests_.erase(*it); | |
347 } | |
348 | |
349 } // namespace safe_browsing | 209 } // namespace safe_browsing |
OLD | NEW |