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" | |
11 #include "chrome/browser/safe_browsing/safe_browsing_service.h" | 12 #include "chrome/browser/safe_browsing/safe_browsing_service.h" |
13 #include "chrome/browser/safe_browsing/signature_util.h" | |
12 #include "chrome/common/net/http_return.h" | 14 #include "chrome/common/net/http_return.h" |
13 #include "chrome/common/safe_browsing/csd.pb.h" | 15 #include "chrome/common/safe_browsing/csd.pb.h" |
14 #include "content/browser/browser_thread.h" | 16 #include "content/browser/browser_thread.h" |
17 #include "content/browser/download/download_item.h" | |
18 #include "content/public/common/url_fetcher_delegate.h" | |
15 #include "content/common/net/url_fetcher.h" | 19 #include "content/common/net/url_fetcher.h" |
16 #include "net/base/load_flags.h" | 20 #include "net/base/load_flags.h" |
17 #include "net/url_request/url_request_context_getter.h" | 21 #include "net/url_request/url_request_context_getter.h" |
18 #include "net/url_request/url_request_status.h" | 22 #include "net/url_request/url_request_status.h" |
19 | 23 |
20 namespace safe_browsing { | 24 namespace safe_browsing { |
21 | 25 |
22 const char DownloadProtectionService::kDownloadRequestUrl[] = | 26 const char DownloadProtectionService::kDownloadRequestUrl[] = |
23 "https://sb-ssl.google.com/safebrowsing/clientreport/download"; | 27 "https://sb-ssl.google.com/safebrowsing/clientreport/download"; |
24 | 28 |
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 | |
25 DownloadProtectionService::DownloadInfo::DownloadInfo() | 37 DownloadProtectionService::DownloadInfo::DownloadInfo() |
26 : total_bytes(0), user_initiated(false) {} | 38 : total_bytes(0), user_initiated(false) {} |
27 | 39 |
28 DownloadProtectionService::DownloadInfo::~DownloadInfo() {} | 40 DownloadProtectionService::DownloadInfo::~DownloadInfo() {} |
29 | 41 |
30 DownloadProtectionService::DownloadProtectionService( | 42 // static |
31 SafeBrowsingService* sb_service, | 43 DownloadProtectionService::DownloadInfo |
32 net::URLRequestContextGetter* request_context_getter) | 44 DownloadProtectionService::DownloadInfo::FromDownloadItem( |
33 : sb_service_(sb_service), | 45 const DownloadItem& item) { |
34 request_context_getter_(request_context_getter), | 46 DownloadInfo download_info; |
35 enabled_(false) {} | 47 download_info.local_file = item.full_path(); |
36 | 48 download_info.download_url_chain = item.url_chain(); |
37 DownloadProtectionService::~DownloadProtectionService() { | 49 download_info.referrer_url = item.referrer_url(); |
38 STLDeleteContainerPairFirstPointers(download_requests_.begin(), | 50 // TODO(bryner): Fill in the hash (we shouldn't compute it again) |
39 download_requests_.end()); | 51 download_info.total_bytes = item.total_bytes(); |
40 download_requests_.clear(); | 52 // TODO(bryner): Populate user_initiated |
53 return download_info; | |
41 } | 54 } |
42 | 55 |
43 void DownloadProtectionService::SetEnabled(bool enabled) { | 56 class DownloadProtectionService::CheckClientDownloadRequest |
44 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 57 : public base::RefCountedThreadSafe< |
45 BrowserThread::PostTask( | 58 DownloadProtectionService::CheckClientDownloadRequest, |
46 BrowserThread::IO, | 59 BrowserThread::DeleteOnUIThread>, |
47 FROM_HERE, | 60 public content::URLFetcherDelegate { |
48 base::Bind(&DownloadProtectionService::SetEnabledOnIOThread, | 61 public: |
49 this, enabled)); | 62 CheckClientDownloadRequest(const DownloadInfo& info, |
50 } | 63 const CheckDownloadCallback& callback, |
64 DownloadProtectionService* service, | |
65 SafeBrowsingService* sb_service) | |
66 : info_(info), | |
67 callback_(callback), | |
68 service_(service), | |
69 sb_service_(sb_service), | |
70 pingback_enabled_(service_->enabled()) { | |
71 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
72 } | |
51 | 73 |
52 void DownloadProtectionService::SetEnabledOnIOThread(bool enabled) { | 74 void Start() { |
53 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); | 75 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
54 if (enabled == enabled_) { | 76 // TODO(noelutz): implement some cache to make sure we don't issue the same |
55 return; | 77 // request over and over again if a user downloads the same binary multiple |
56 } | 78 // times. |
57 enabled_ = enabled; | 79 if (info_.download_url_chain.empty()) { |
58 if (!enabled_) { | 80 RecordStats(REASON_INVALID_URL); |
59 for (DownloadRequests::iterator it = download_requests_.begin(); | 81 PostFinishTask(SAFE); |
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 } | |
68 | |
69 void DownloadProtectionService::OnURLFetchComplete( | |
70 const content::URLFetcher* source) { | |
71 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); | |
72 scoped_ptr<const content::URLFetcher> s(source); | |
73 if (download_requests_.find(source) != download_requests_.end()) { | |
74 CheckDownloadCallback callback = download_requests_[source]; | |
75 download_requests_.erase(source); | |
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); | |
80 return; | 82 return; |
81 } | 83 } |
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(); | |
82 DownloadCheckResultReason reason = REASON_MAX; | 133 DownloadCheckResultReason reason = REASON_MAX; |
83 reason = REASON_SERVER_PING_FAILED; | 134 reason = REASON_SERVER_PING_FAILED; |
84 if (source->GetStatus().is_success() && | 135 if (source->GetStatus().is_success() && |
85 RC_REQUEST_OK == source->GetResponseCode()) { | 136 RC_REQUEST_OK == source->GetResponseCode()) { |
86 std::string data; | 137 std::string data; |
87 source->GetResponseAsString(&data); | 138 source->GetResponseAsString(&data); |
88 if (data.size() > 0) { | 139 if (data.size() > 0) { |
89 // For now no matter what we'll always say the download is safe. | 140 // For now no matter what we'll always say the download is safe. |
90 // TODO(noelutz): Parse the response body to see exactly what's going | 141 // TODO(noelutz): Parse the response body to see exactly what's going |
91 // on. | 142 // on. |
92 reason = REASON_INVALID_RESPONSE_PROTO; | 143 reason = REASON_INVALID_RESPONSE_PROTO; |
93 } | 144 } |
94 } | 145 } |
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 pigback. | |
mattm
2011/10/26 23:27:36
pingback
Brian Ryner
2011/10/27 01:03:04
Done.
| |
95 BrowserThread::PostTask( | 212 BrowserThread::PostTask( |
96 BrowserThread::UI, | 213 BrowserThread::UI, |
97 FROM_HERE, | 214 FROM_HERE, |
98 base::Bind(&DownloadProtectionService::EndCheckClientDownload, | 215 base::Bind(&CheckClientDownloadRequest::SendRequest, this)); |
99 this, SAFE, reason, callback)); | 216 } |
100 } else { | 217 |
101 NOTREACHED(); | 218 void SendRequest() { |
102 } | 219 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
103 } | 220 |
104 | 221 // This is our last chance to check whether the request has been canceled |
105 bool DownloadProtectionService::CheckClientDownload( | 222 // before sending it. |
106 const DownloadInfo& info, | 223 if (!service_) { |
107 const CheckDownloadCallback& callback) { | 224 FinishRequest(SAFE); |
108 // TODO(noelutz): implement some cache to make sure we don't issue the same | 225 return; |
109 // request over and over again if a user downloads the same binary multiple | 226 } |
110 // times. | 227 |
111 if (info.download_url_chain.empty()) { | 228 ClientDownloadRequest request; |
112 RecordStats(REASON_INVALID_URL); | 229 request.set_url(info_.download_url_chain.back().spec()); |
113 return true; | 230 request.mutable_digests()->set_sha256(info_.sha256_hash); |
114 } | 231 request.set_length(info_.total_bytes); |
115 const GURL& final_url = info.download_url_chain.back(); | 232 for (size_t i = 0; i < info_.download_url_chain.size(); ++i) { |
116 if (!final_url.is_valid() || final_url.is_empty() || | 233 ClientDownloadRequest::Resource* resource = request.add_resources(); |
117 !final_url.SchemeIs("http")) { | 234 resource->set_url(info_.download_url_chain[i].spec()); |
118 RecordStats(REASON_INVALID_URL); | 235 if (i == info_.download_url_chain.size() - 1) { |
119 return true; // For now we only support HTTP download URLs. | 236 // The last URL in the chain is the download URL. |
120 } | 237 resource->set_type(ClientDownloadRequest::DOWNLOAD_URL); |
121 // TODO(noelutz): DownloadInfo should also contain the IP address of every | 238 resource->set_referrer(info_.referrer_url.spec()); |
122 // URL in the redirect chain. We also should check whether the download URL | 239 } else { |
123 // is hosted on the internal network. | 240 resource->set_type(ClientDownloadRequest::DOWNLOAD_REDIRECT); |
124 BrowserThread::PostTask( | 241 } |
125 BrowserThread::IO, | 242 // TODO(noelutz): fill out the remote IP addresses. |
126 FROM_HERE, | 243 } |
127 base::Bind(&DownloadProtectionService::StartCheckClientDownload, | 244 request.set_user_initiated(info_.user_initiated); |
128 this, info, callback)); | 245 std::string request_data; |
129 return false; | 246 if (!request.SerializeToString(&request_data)) { |
130 } | 247 RecordStats(REASON_INVALID_REQUEST_PROTO); |
131 | 248 FinishRequest(SAFE); |
132 void DownloadProtectionService::StartCheckClientDownload( | 249 return; |
133 const DownloadInfo& info, | 250 } |
134 const CheckDownloadCallback& callback) { | 251 |
135 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); | 252 VLOG(2) << "Sending a request for URL: " |
136 if (!enabled_ || !sb_service_.get()) { | 253 << info_.download_url_chain.back(); |
137 // This is a hard fail. We won't even call the callback in this case. | 254 fetcher_.reset(URLFetcher::Create(0 /* ID used for testing */, |
138 RecordStats(REASON_SB_DISABLED); | 255 GURL(kDownloadRequestUrl), |
139 return; | 256 URLFetcher::POST, |
140 } | 257 this)); |
141 DownloadCheckResultReason reason = REASON_MAX; | 258 fetcher_->SetLoadFlags(net::LOAD_DISABLE_CACHE); |
142 for (size_t i = 0; i < info.download_url_chain.size(); ++i) { | 259 fetcher_->SetRequestContext(service_->request_context_getter_.get()); |
143 if (sb_service_->MatchDownloadWhitelistUrl(info.download_url_chain[i])) { | 260 fetcher_->SetUploadData("application/octet-stream", request_data); |
144 reason = REASON_WHITELISTED_URL; | 261 fetcher_->Start(); |
145 break; | 262 } |
146 } | 263 |
147 } | 264 void PostFinishTask(DownloadCheckResult result) { |
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; | |
173 } | |
174 | |
175 if (reason != REASON_MAX) { | |
176 // We stop here because the download is considered safe. | |
177 BrowserThread::PostTask( | 265 BrowserThread::PostTask( |
178 BrowserThread::UI, | 266 BrowserThread::UI, |
179 FROM_HERE, | 267 FROM_HERE, |
180 base::Bind(&DownloadProtectionService::EndCheckClientDownload, | 268 base::Bind(&CheckClientDownloadRequest::FinishRequest, this, result)); |
181 this, SAFE, reason, callback)); | 269 } |
270 | |
271 void FinishRequest(DownloadCheckResult result) { | |
272 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
273 if (service_) { | |
274 callback_.Run(result); | |
275 service_->RequestFinished(this); | |
276 } else { | |
277 callback_.Run(SAFE); | |
278 } | |
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 } | |
309 | |
310 void DownloadProtectionService::SetEnabled(bool enabled) { | |
311 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | |
312 if (enabled == enabled_) { | |
182 return; | 313 return; |
183 } | 314 } |
184 | 315 enabled_ = enabled; |
185 URLFetcher* fetcher = URLFetcher::Create(0 /* ID used for testing */, | 316 if (!enabled_) { |
186 GURL(kDownloadRequestUrl), | 317 CancelPendingRequests(); |
187 URLFetcher::POST, | 318 } |
188 this); | 319 } |
189 download_requests_[fetcher] = callback; | 320 |
190 fetcher->SetLoadFlags(net::LOAD_DISABLE_CACHE); | 321 void DownloadProtectionService::CheckClientDownload( |
191 fetcher->SetRequestContext(request_context_getter_.get()); | 322 const DownloadProtectionService::DownloadInfo& info, |
192 fetcher->SetUploadData("application/octet-stream", request_data); | |
193 fetcher->Start(); | |
194 } | |
195 | |
196 void DownloadProtectionService::EndCheckClientDownload( | |
197 DownloadCheckResult result, | |
198 DownloadCheckResultReason reason, | |
199 const CheckDownloadCallback& callback) { | 323 const CheckDownloadCallback& callback) { |
200 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); | 324 scoped_refptr<CheckClientDownloadRequest> request( |
201 RecordStats(reason); | 325 new CheckClientDownloadRequest(info, callback, this, sb_service_)); |
202 callback.Run(result); | 326 download_requests_.insert(request); |
203 } | 327 request->Start(); |
204 | 328 } |
205 void DownloadProtectionService::RecordStats(DownloadCheckResultReason reason) { | 329 |
206 UMA_HISTOGRAM_ENUMERATION("SBClientDownload.CheckDownloadStats", | 330 void DownloadProtectionService::CancelPendingRequests() { |
207 reason, | 331 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
208 REASON_MAX); | 332 for (std::set<scoped_refptr<CheckClientDownloadRequest> >::iterator it = |
209 } | 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 | |
210 } // namespace safe_browsing | 349 } // namespace safe_browsing |
OLD | NEW |