1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/safe_browsing/client_side_detection_service.h"
10 #include "base/command_line.h"
11 #include "base/logging.h"
12 #include "base/memory/scoped_ptr.h"
13 #include "base/message_loop/message_loop.h"
14 #include "base/metrics/histogram.h"
15 #include "base/metrics/sparse_histogram.h"
16 #include "base/prefs/pref_service.h"
17 #include "base/stl_util.h"
18 #include "base/strings/string_util.h"
19 #include "base/time/time.h"
20 #include "chrome/browser/browser_process.h"
21 #include "chrome/browser/profiles/profile.h"
22 #include "chrome/common/chrome_switches.h"
23 #include "chrome/common/pref_names.h"
24 #include "chrome/common/safe_browsing/client_model.pb.h"
25 #include "chrome/common/safe_browsing/csd.pb.h"
26 #include "chrome/common/safe_browsing/safebrowsing_messages.h"
27 #include "content/public/browser/browser_thread.h"
28 #include "content/public/browser/notification_service.h"
29 #include "content/public/browser/notification_types.h"
30 #include "content/public/browser/render_process_host.h"
31 #include "crypto/sha2.h"
32 #include "google_apis/google_api_keys.h"
33 #include "net/base/escape.h"
34 #include "net/base/load_flags.h"
35 #include "net/base/net_util.h"
36 #include "net/http/http_response_headers.h"
37 #include "net/http/http_status_code.h"
38 #include "net/url_request/url_fetcher.h"
39 #include "net/url_request/url_request_context_getter.h"
40 #include "net/url_request/url_request_status.h"
43 using content::BrowserThread
;
45 namespace safe_browsing
{
49 // malware report type for UMA histogram counting.
50 enum MalwareReportTypes
{
53 REPORT_FAILED_SERIALIZATION
,
59 void UpdateEnumUMAHistogram(MalwareReportTypes report_type
) {
60 DCHECK(report_type
>= 0 && report_type
< REPORT_RESULT_MAX
);
61 UMA_HISTOGRAM_ENUMERATION("SBClientMalware.SentReports",
62 report_type
, REPORT_RESULT_MAX
);
67 const size_t ClientSideDetectionService::kMaxModelSizeBytes
= 90 * 1024;
68 const int ClientSideDetectionService::kMaxReportsPerInterval
= 3;
69 // TODO(noelutz): once we know this mechanism works as intended we should fetch
70 // the model much more frequently. E.g., every 5 minutes or so.
71 const int ClientSideDetectionService::kClientModelFetchIntervalMs
= 3600 * 1000;
72 const int ClientSideDetectionService::kInitialClientModelFetchDelayMs
= 10000;
74 const int ClientSideDetectionService::kReportsIntervalDays
= 1;
75 const int ClientSideDetectionService::kNegativeCacheIntervalDays
= 1;
76 const int ClientSideDetectionService::kPositiveCacheIntervalMinutes
= 30;
78 const char ClientSideDetectionService::kClientReportPhishingUrl
[] =
79 "https://sb-ssl.google.com/safebrowsing/clientreport/phishing";
80 const char ClientSideDetectionService::kClientReportMalwareUrl
[] =
81 "https://sb-ssl.google.com/safebrowsing/clientreport/malware-check";
82 const char ClientSideDetectionService::kClientModelUrl
[] =
83 "https://ssl.gstatic.com/safebrowsing/csd/client_model_v5.pb";
85 struct ClientSideDetectionService::ClientReportInfo
{
86 ClientReportPhishingRequestCallback callback
;
90 struct ClientSideDetectionService::ClientMalwareReportInfo
{
91 ClientReportMalwareRequestCallback callback
;
92 // This is the original landing url, may not be the malware url.
96 ClientSideDetectionService::CacheState::CacheState(bool phish
, base::Time time
)
100 ClientSideDetectionService::ClientSideDetectionService(
101 net::URLRequestContextGetter
* request_context_getter
)
103 request_context_getter_(request_context_getter
),
104 weak_factory_(this) {
105 registrar_
.Add(this, content::NOTIFICATION_RENDERER_PROCESS_CREATED
,
106 content::NotificationService::AllBrowserContextsAndSources());
109 ClientSideDetectionService::~ClientSideDetectionService() {
110 weak_factory_
.InvalidateWeakPtrs();
111 STLDeleteContainerPairPointers(client_phishing_reports_
.begin(),
112 client_phishing_reports_
.end());
113 client_phishing_reports_
.clear();
114 STLDeleteContainerPairPointers(client_malware_reports_
.begin(),
115 client_malware_reports_
.end());
116 client_malware_reports_
.clear();
120 ClientSideDetectionService
* ClientSideDetectionService::Create(
121 net::URLRequestContextGetter
* request_context_getter
) {
122 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
123 return new ClientSideDetectionService(request_context_getter
);
126 void ClientSideDetectionService::SetEnabledAndRefreshState(bool enabled
) {
127 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
128 SendModelToRenderers(); // always refresh the renderer state
129 if (enabled
== enabled_
)
133 // Refresh the model when the service is enabled. This can happen when the
134 // preference is toggled, or early during startup if the preference is
135 // already enabled. In a lot of cases the model will be in the cache so it
136 // won't actually be fetched from the network.
137 // We delay the first model fetch to avoid slowing down browser startup.
138 ScheduleFetchModel(kInitialClientModelFetchDelayMs
);
140 // Cancel pending requests.
141 model_fetcher_
.reset();
142 // Invoke pending callbacks with a false verdict.
143 for (std::map
<const net::URLFetcher
*, ClientReportInfo
*>::iterator it
=
144 client_phishing_reports_
.begin();
145 it
!= client_phishing_reports_
.end(); ++it
) {
146 ClientReportInfo
* info
= it
->second
;
147 if (!info
->callback
.is_null())
148 info
->callback
.Run(info
->phishing_url
, false);
150 STLDeleteContainerPairPointers(client_phishing_reports_
.begin(),
151 client_phishing_reports_
.end());
152 client_phishing_reports_
.clear();
153 for (std::map
<const net::URLFetcher
*, ClientMalwareReportInfo
*>::iterator it
154 = client_malware_reports_
.begin();
155 it
!= client_malware_reports_
.end(); ++it
) {
156 ClientMalwareReportInfo
* info
= it
->second
;
157 if (!info
->callback
.is_null())
158 info
->callback
.Run(info
->original_url
, info
->original_url
, false);
160 STLDeleteContainerPairPointers(client_malware_reports_
.begin(),
161 client_malware_reports_
.end());
162 client_malware_reports_
.clear();
167 void ClientSideDetectionService::SendClientReportPhishingRequest(
168 ClientPhishingRequest
* verdict
,
169 const ClientReportPhishingRequestCallback
& callback
) {
170 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
171 base::MessageLoop::current()->PostTask(
173 base::Bind(&ClientSideDetectionService::StartClientReportPhishingRequest
,
174 weak_factory_
.GetWeakPtr(), verdict
, callback
));
177 void ClientSideDetectionService::SendClientReportMalwareRequest(
178 ClientMalwareRequest
* verdict
,
179 const ClientReportMalwareRequestCallback
& callback
) {
180 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
181 base::MessageLoop::current()->PostTask(
183 base::Bind(&ClientSideDetectionService::StartClientReportMalwareRequest
,
184 weak_factory_
.GetWeakPtr(), verdict
, callback
));
187 bool ClientSideDetectionService::IsPrivateIPAddress(
188 const std::string
& ip_address
) const {
189 net::IPAddressNumber ip_number
;
190 if (!net::ParseIPLiteralToNumber(ip_address
, &ip_number
)) {
191 DVLOG(2) << "Unable to parse IP address: '" << ip_address
<< "'";
192 // Err on the side of safety and assume this might be private.
196 return net::IsIPAddressReserved(ip_number
);
199 void ClientSideDetectionService::OnURLFetchComplete(
200 const net::URLFetcher
* source
) {
202 source
->GetResponseAsString(&data
);
203 if (source
== model_fetcher_
.get()) {
205 source
, source
->GetURL(), source
->GetStatus(),
206 source
->GetResponseCode(), source
->GetCookies(), data
);
207 } else if (client_phishing_reports_
.find(source
) !=
208 client_phishing_reports_
.end()) {
209 HandlePhishingVerdict(
210 source
, source
->GetURL(), source
->GetStatus(),
211 source
->GetResponseCode(), source
->GetCookies(), data
);
212 } else if (client_malware_reports_
.find(source
) !=
213 client_malware_reports_
.end()) {
214 HandleMalwareVerdict(
215 source
, source
->GetURL(), source
->GetStatus(),
216 source
->GetResponseCode(), source
->GetCookies(), data
);
222 void ClientSideDetectionService::Observe(
224 const content::NotificationSource
& source
,
225 const content::NotificationDetails
& details
) {
226 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
227 DCHECK(type
== content::NOTIFICATION_RENDERER_PROCESS_CREATED
);
229 // Model might not be ready or maybe there was an error.
233 content::Source
<content::RenderProcessHost
>(source
).ptr());
236 void ClientSideDetectionService::SendModelToProcess(
237 content::RenderProcessHost
* process
) {
238 // The ClientSideDetectionService is enabled if _any_ active profile has
239 // SafeBrowsing turned on. Here we check the profile for each renderer
240 // process and only send the model to those that have SafeBrowsing enabled.
241 Profile
* profile
= Profile::FromBrowserContext(process
->GetBrowserContext());
243 if (profile
->GetPrefs()->GetBoolean(prefs::kSafeBrowsingEnabled
)) {
244 DVLOG(2) << "Sending phishing model to RenderProcessHost @" << process
;
247 DVLOG(2) << "Disabling client-side phishing detection for "
248 << "RenderProcessHost @" << process
;
250 process
->Send(new SafeBrowsingMsg_SetPhishingModel(model
));
253 void ClientSideDetectionService::SendModelToRenderers() {
254 for (content::RenderProcessHost::iterator
i(
255 content::RenderProcessHost::AllHostsIterator());
256 !i
.IsAtEnd(); i
.Advance()) {
257 SendModelToProcess(i
.GetCurrentValue());
261 void ClientSideDetectionService::ScheduleFetchModel(int64 delay_ms
) {
262 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
263 switches::kSbDisableAutoUpdate
))
265 base::MessageLoop::current()->PostDelayedTask(
267 base::Bind(&ClientSideDetectionService::StartFetchModel
,
268 weak_factory_
.GetWeakPtr()),
269 base::TimeDelta::FromMilliseconds(delay_ms
));
272 void ClientSideDetectionService::StartFetchModel() {
274 // Start fetching the model either from the cache or possibly from the
275 // network if the model isn't in the cache.
276 model_fetcher_
.reset(net::URLFetcher::Create(
277 0 /* ID used for testing */, GURL(kClientModelUrl
),
278 net::URLFetcher::GET
, this));
279 model_fetcher_
->SetRequestContext(request_context_getter_
.get());
280 model_fetcher_
->Start();
284 void ClientSideDetectionService::EndFetchModel(ClientModelStatus status
) {
285 UMA_HISTOGRAM_ENUMERATION("SBClientPhishing.ClientModelStatus",
288 if (status
== MODEL_SUCCESS
) {
289 SetBadSubnets(*model_
, &bad_subnets_
);
290 SendModelToRenderers();
292 int delay_ms
= kClientModelFetchIntervalMs
;
293 // If the most recently fetched model had a valid max-age and the model was
294 // valid we're scheduling the next model update for after the max-age expired.
295 if (model_max_age_
.get() &&
296 (status
== MODEL_SUCCESS
|| status
== MODEL_NOT_CHANGED
)) {
297 // We're adding 60s of additional delay to make sure we're past
299 *model_max_age_
+= base::TimeDelta::FromMinutes(1);
300 delay_ms
= model_max_age_
->InMilliseconds();
302 model_max_age_
.reset();
304 // Schedule the next model reload.
305 ScheduleFetchModel(delay_ms
);
308 void ClientSideDetectionService::StartClientReportPhishingRequest(
309 ClientPhishingRequest
* verdict
,
310 const ClientReportPhishingRequestCallback
& callback
) {
311 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
312 scoped_ptr
<ClientPhishingRequest
> request(verdict
);
315 if (!callback
.is_null())
316 callback
.Run(GURL(request
->url()), false);
320 std::string request_data
;
321 if (!request
->SerializeToString(&request_data
)) {
322 UMA_HISTOGRAM_COUNTS("SBClientPhishing.RequestNotSerialized", 1);
323 DVLOG(1) << "Unable to serialize the CSD request. Proto file changed?";
324 if (!callback
.is_null())
325 callback
.Run(GURL(request
->url()), false);
329 net::URLFetcher
* fetcher
= net::URLFetcher::Create(
330 0 /* ID used for testing */,
331 GetClientReportUrl(kClientReportPhishingUrl
),
332 net::URLFetcher::POST
, this);
334 // Remember which callback and URL correspond to the current fetcher object.
335 ClientReportInfo
* info
= new ClientReportInfo
;
336 info
->callback
= callback
;
337 info
->phishing_url
= GURL(request
->url());
338 client_phishing_reports_
[fetcher
] = info
;
340 fetcher
->SetLoadFlags(net::LOAD_DISABLE_CACHE
);
341 fetcher
->SetRequestContext(request_context_getter_
.get());
342 fetcher
->SetUploadData("application/octet-stream", request_data
);
345 // Record that we made a request
346 phishing_report_times_
.push(base::Time::Now());
349 void ClientSideDetectionService::StartClientReportMalwareRequest(
350 ClientMalwareRequest
* verdict
,
351 const ClientReportMalwareRequestCallback
& callback
) {
352 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
353 scoped_ptr
<ClientMalwareRequest
> request(verdict
);
356 if (!callback
.is_null())
357 callback
.Run(GURL(request
->url()), GURL(request
->url()), false);
361 std::string request_data
;
362 if (!request
->SerializeToString(&request_data
)) {
363 UpdateEnumUMAHistogram(REPORT_FAILED_SERIALIZATION
);
364 DVLOG(1) << "Unable to serialize the CSD request. Proto file changed?";
365 if (!callback
.is_null())
366 callback
.Run(GURL(request
->url()), GURL(request
->url()), false);
370 net::URLFetcher
* fetcher
= net::URLFetcher::Create(
371 0 /* ID used for testing */,
372 GetClientReportUrl(kClientReportMalwareUrl
),
373 net::URLFetcher::POST
, this);
375 // Remember which callback and URL correspond to the current fetcher object.
376 ClientMalwareReportInfo
* info
= new ClientMalwareReportInfo
;
377 info
->callback
= callback
;
378 info
->original_url
= GURL(request
->url());
379 client_malware_reports_
[fetcher
] = info
;
381 fetcher
->SetLoadFlags(net::LOAD_DISABLE_CACHE
);
382 fetcher
->SetRequestContext(request_context_getter_
.get());
383 fetcher
->SetUploadData("application/octet-stream", request_data
);
386 UMA_HISTOGRAM_ENUMERATION("SBClientMalware.SentReports",
387 REPORT_SENT
, REPORT_RESULT_MAX
);
389 UMA_HISTOGRAM_COUNTS("SBClientMalware.IPBlacklistRequestPayloadSize",
390 request_data
.size());
392 // Record that we made a malware request
393 malware_report_times_
.push(base::Time::Now());
396 void ClientSideDetectionService::HandleModelResponse(
397 const net::URLFetcher
* source
,
399 const net::URLRequestStatus
& status
,
401 const net::ResponseCookies
& cookies
,
402 const std::string
& data
) {
403 base::TimeDelta max_age
;
404 if (status
.is_success() && net::HTTP_OK
== response_code
&&
405 source
->GetResponseHeaders() &&
406 source
->GetResponseHeaders()->GetMaxAgeValue(&max_age
)) {
407 model_max_age_
.reset(new base::TimeDelta(max_age
));
409 scoped_ptr
<ClientSideModel
> model(new ClientSideModel());
410 ClientModelStatus model_status
;
411 if (!status
.is_success() || net::HTTP_OK
!= response_code
) {
412 model_status
= MODEL_FETCH_FAILED
;
413 } else if (data
.empty()) {
414 model_status
= MODEL_EMPTY
;
415 } else if (data
.size() > kMaxModelSizeBytes
) {
416 model_status
= MODEL_TOO_LARGE
;
417 } else if (!model
->ParseFromString(data
)) {
418 model_status
= MODEL_PARSE_ERROR
;
419 } else if (!model
->IsInitialized() || !model
->has_version()) {
420 model_status
= MODEL_MISSING_FIELDS
;
421 } else if (!ModelHasValidHashIds(*model
)) {
422 model_status
= MODEL_BAD_HASH_IDS
;
423 } else if (model
->version() < 0 ||
424 (model_
.get() && model
->version() < model_
->version())) {
425 model_status
= MODEL_INVALID_VERSION_NUMBER
;
426 } else if (model_
.get() && model
->version() == model_
->version()) {
427 model_status
= MODEL_NOT_CHANGED
;
429 // The model is valid => replace the existing model with the new one.
430 model_str_
.assign(data
);
432 model_status
= MODEL_SUCCESS
;
434 EndFetchModel(model_status
);
437 void ClientSideDetectionService::HandlePhishingVerdict(
438 const net::URLFetcher
* source
,
440 const net::URLRequestStatus
& status
,
442 const net::ResponseCookies
& cookies
,
443 const std::string
& data
) {
444 ClientPhishingResponse response
;
445 scoped_ptr
<ClientReportInfo
> info(client_phishing_reports_
[source
]);
446 bool is_phishing
= false;
447 if (status
.is_success() && net::HTTP_OK
== response_code
&&
448 response
.ParseFromString(data
)) {
449 // Cache response, possibly flushing an old one.
450 cache_
[info
->phishing_url
] =
451 make_linked_ptr(new CacheState(response
.phishy(), base::Time::Now()));
452 is_phishing
= response
.phishy();
454 DLOG(ERROR
) << "Unable to get the server verdict for URL: "
455 << info
->phishing_url
<< " status: " << status
.status() << " "
456 << "response_code:" << response_code
;
458 if (!info
->callback
.is_null())
459 info
->callback
.Run(info
->phishing_url
, is_phishing
);
460 client_phishing_reports_
.erase(source
);
464 void ClientSideDetectionService::HandleMalwareVerdict(
465 const net::URLFetcher
* source
,
467 const net::URLRequestStatus
& status
,
469 const net::ResponseCookies
& cookies
,
470 const std::string
& data
) {
471 if (status
.is_success()) {
472 UMA_HISTOGRAM_SPARSE_SLOWLY(
473 "SBClientMalware.IPBlacklistRequestResponseCode", response_code
);
475 // status error is negative, so we put - in front of it.
476 UMA_HISTOGRAM_SPARSE_SLOWLY(
477 "SBClientMalware.IPBlacklistRequestNetError", -status
.error());
479 ClientMalwareResponse response
;
480 scoped_ptr
<ClientMalwareReportInfo
> info(client_malware_reports_
[source
]);
481 bool should_blacklist
= false;
482 if (status
.is_success() && net::HTTP_OK
== response_code
&&
483 response
.ParseFromString(data
)) {
484 should_blacklist
= response
.blacklist();
486 DLOG(ERROR
) << "Unable to get the server verdict for URL: "
487 << info
->original_url
<< " status: " << status
.status() << " "
488 << "response_code:" << response_code
;
491 if (!info
->callback
.is_null()) {
492 if (response
.has_bad_url())
493 info
->callback
.Run(info
->original_url
, GURL(response
.bad_url()),
496 info
->callback
.Run(info
->original_url
, info
->original_url
, false);
499 client_malware_reports_
.erase(source
);
503 bool ClientSideDetectionService::IsInCache(const GURL
& url
) {
506 return cache_
.find(url
) != cache_
.end();
509 bool ClientSideDetectionService::GetValidCachedResult(const GURL
& url
,
513 PhishingCache::iterator it
= cache_
.find(url
);
514 if (it
== cache_
.end()) {
518 // We still need to check if the result is valid.
519 const CacheState
& cache_state
= *it
->second
;
520 if (cache_state
.is_phishing
?
521 cache_state
.timestamp
> base::Time::Now() -
522 base::TimeDelta::FromMinutes(kPositiveCacheIntervalMinutes
) :
523 cache_state
.timestamp
> base::Time::Now() -
524 base::TimeDelta::FromDays(kNegativeCacheIntervalDays
)) {
525 *is_phishing
= cache_state
.is_phishing
;
531 void ClientSideDetectionService::UpdateCache() {
532 // Since we limit the number of requests but allow pass-through for cache
533 // refreshes, we don't want to remove elements from the cache if they
534 // could be used for this purpose even if we will not use the entry to
535 // satisfy the request from the cache.
536 base::TimeDelta positive_cache_interval
=
537 std::max(base::TimeDelta::FromMinutes(kPositiveCacheIntervalMinutes
),
538 base::TimeDelta::FromDays(kReportsIntervalDays
));
539 base::TimeDelta negative_cache_interval
=
540 std::max(base::TimeDelta::FromDays(kNegativeCacheIntervalDays
),
541 base::TimeDelta::FromDays(kReportsIntervalDays
));
543 // Remove elements from the cache that will no longer be used.
544 for (PhishingCache::iterator it
= cache_
.begin(); it
!= cache_
.end();) {
545 const CacheState
& cache_state
= *it
->second
;
546 if (cache_state
.is_phishing
?
547 cache_state
.timestamp
> base::Time::Now() - positive_cache_interval
:
548 cache_state
.timestamp
> base::Time::Now() - negative_cache_interval
) {
556 bool ClientSideDetectionService::OverMalwareReportLimit() {
557 return GetMalwareNumReports() > kMaxReportsPerInterval
;
560 bool ClientSideDetectionService::OverPhishingReportLimit() {
561 return GetPhishingNumReports() > kMaxReportsPerInterval
;
564 int ClientSideDetectionService::GetMalwareNumReports() {
565 return GetNumReports(&malware_report_times_
);
568 int ClientSideDetectionService::GetPhishingNumReports() {
569 return GetNumReports(&phishing_report_times_
);
572 int ClientSideDetectionService::GetNumReports(
573 std::queue
<base::Time
>* report_times
) {
575 base::Time::Now() - base::TimeDelta::FromDays(kReportsIntervalDays
);
577 // Erase items older than cutoff because we will never care about them again.
578 while (!report_times
->empty() &&
579 report_times
->front() < cutoff
) {
583 // Return the number of elements that are above the cutoff.
584 return report_times
->size();
588 void ClientSideDetectionService::SetBadSubnets(const ClientSideModel
& model
,
589 BadSubnetMap
* bad_subnets
) {
590 bad_subnets
->clear();
591 for (int i
= 0; i
< model
.bad_subnet_size(); ++i
) {
592 int size
= model
.bad_subnet(i
).size();
593 if (size
< 0 || size
> static_cast<int>(net::kIPv6AddressSize
) * 8) {
594 DLOG(ERROR
) << "Invalid bad subnet size: " << size
;
597 if (model
.bad_subnet(i
).prefix().size() != crypto::kSHA256Length
) {
598 DLOG(ERROR
) << "Invalid bad subnet prefix length: "
599 << model
.bad_subnet(i
).prefix().size();
602 // We precompute the mask for the given subnet size to speed up lookups.
603 // Basically we need to create a 16B long string which has the highest
604 // |size| bits sets to one.
605 std::string
mask(net::kIPv6AddressSize
, '\x00');
606 mask
.replace(0, size
/ 8, size
/ 8, '\xFF');
608 mask
[size
/ 8] = 0xFF << (8 - (size
% 8));
610 (*bad_subnets
)[mask
].insert(model
.bad_subnet(i
).prefix());
615 bool ClientSideDetectionService::ModelHasValidHashIds(
616 const ClientSideModel
& model
) {
617 const int max_index
= model
.hashes_size() - 1;
618 for (int i
= 0; i
< model
.rule_size(); ++i
) {
619 for (int j
= 0; j
< model
.rule(i
).feature_size(); ++j
) {
620 if (model
.rule(i
).feature(j
) < 0 ||
621 model
.rule(i
).feature(j
) > max_index
) {
626 for (int i
= 0; i
< model
.page_term_size(); ++i
) {
627 if (model
.page_term(i
) < 0 || model
.page_term(i
) > max_index
) {
635 GURL
ClientSideDetectionService::GetClientReportUrl(
636 const std::string
& report_url
) {
637 GURL
url(report_url
);
638 std::string api_key
= google_apis::GetAPIKey();
639 if (!api_key
.empty())
640 url
= url
.Resolve("?key=" + net::EscapeQueryParamValue(api_key
, true));
644 } // namespace safe_browsing