// Copyright (c) 2011 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/client_side_detection_service.h"
#include "base/command_line.h"
#include "base/file_path.h"
#include "base/file_util_proxy.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop.h"
#include "base/metrics/histogram.h"
#include "base/platform_file.h"
#include "base/stl_util-inl.h"
#include "base/task.h"
#include "base/time.h"
#include "chrome/common/net/http_return.h"
#include "chrome/common/net/url_fetcher.h"
#include "chrome/common/safe_browsing/csd.pb.h"
#include "content/browser/browser_thread.h"
#include "googleurl/src/gurl.h"
#include "net/base/load_flags.h"
#include "net/url_request/url_request_context_getter.h"
#include "net/url_request/url_request_status.h"
namespace safe_browsing {
const int ClientSideDetectionService::kMaxReportsPerInterval = 3;
const base::TimeDelta ClientSideDetectionService::kReportsInterval =
base::TimeDelta::FromDays(1);
const base::TimeDelta ClientSideDetectionService::kNegativeCacheInterval =
base::TimeDelta::FromDays(1);
const base::TimeDelta ClientSideDetectionService::kPositiveCacheInterval =
base::TimeDelta::FromMinutes(30);
const char ClientSideDetectionService::kClientReportPhishingUrl[] =
"https://sb-ssl.google.com/safebrowsing/clientreport/phishing";
// Note: when updatng the model version, don't forget to change the filename
// in chrome/common/chrome_constants.cc as well, or else existing users won't
// download the new model.
//
// TODO(bryner): add version metadata so that clients can download new models
// without needing a new model filename.
const char ClientSideDetectionService::kClientModelUrl[] =
"https://ssl.gstatic.com/safebrowsing/csd/client_model_v1.pb";
struct ClientSideDetectionService::ClientReportInfo {
scoped_ptr<ClientReportPhishingRequestCallback> callback;
GURL phishing_url;
};
ClientSideDetectionService::CacheState::CacheState(bool phish, base::Time time)
: is_phishing(phish),
timestamp(time) {}
ClientSideDetectionService::ClientSideDetectionService(
const FilePath& model_path,
net::URLRequestContextGetter* request_context_getter)
: model_path_(model_path),
model_status_(UNKNOWN_STATUS),
model_file_(base::kInvalidPlatformFileValue),
ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)),
ALLOW_THIS_IN_INITIALIZER_LIST(callback_factory_(this)),
request_context_getter_(request_context_getter) {}
ClientSideDetectionService::~ClientSideDetectionService() {
method_factory_.RevokeAll();
STLDeleteContainerPairPointers(client_phishing_reports_.begin(),
client_phishing_reports_.end());
client_phishing_reports_.clear();
STLDeleteElements(&open_callbacks_);
CloseModelFile();
}
/* static */
ClientSideDetectionService* ClientSideDetectionService::Create(
const FilePath& model_path,
net::URLRequestContextGetter* request_context_getter) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
scoped_ptr<ClientSideDetectionService> service(
new ClientSideDetectionService(model_path, request_context_getter));
if (!service->InitializePrivateNetworks()) {
UMA_HISTOGRAM_COUNTS("SBClientPhishing.InitPrivateNetworksFailed", 1);
return NULL;
}
// We try to open the model file right away and start fetching it if
// it does not already exist on disk.
base::FileUtilProxy::CreateOrOpenCallback* cb =
service.get()->callback_factory_.NewCallback(
&ClientSideDetectionService::OpenModelFileDone);
if (!base::FileUtilProxy::CreateOrOpen(
BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE),
model_path,
base::PLATFORM_FILE_OPEN | base::PLATFORM_FILE_READ,
cb)) {
delete cb;
return NULL;
}
// Delete the previous-version model file.
// TODO(bryner): Remove this for M14.
base::FileUtilProxy::Delete(
BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE),
model_path.DirName().AppendASCII("Safe Browsing Phishing Model"),
false /* not recursive */,
NULL /* not interested in result */);
return service.release();
}
void ClientSideDetectionService::GetModelFile(OpenModelDoneCallback* callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
MessageLoop::current()->PostTask(
FROM_HERE,
method_factory_.NewRunnableMethod(
&ClientSideDetectionService::StartGetModelFile, callback));
}
void ClientSideDetectionService::SendClientReportPhishingRequest(
ClientPhishingRequest* verdict,
ClientReportPhishingRequestCallback* callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
MessageLoop::current()->PostTask(
FROM_HERE,
method_factory_.NewRunnableMethod(
&ClientSideDetectionService::StartClientReportPhishingRequest,
verdict, callback));
}
bool ClientSideDetectionService::IsPrivateIPAddress(
const std::string& ip_address) const {
net::IPAddressNumber ip_number;
if (!net::ParseIPLiteralToNumber(ip_address, &ip_number)) {
DLOG(WARNING) << "Unable to parse IP address: " << ip_address;
// Err on the side of safety and assume this might be private.
return true;
}
for (std::vector<AddressRange>::const_iterator it =
private_networks_.begin();
it != private_networks_.end(); ++it) {
if (net::IPNumberMatchesPrefix(ip_number, it->first, it->second)) {
return true;
}
}
return false;
}
void ClientSideDetectionService::OnURLFetchComplete(
const URLFetcher* source,
const GURL& url,
const net::URLRequestStatus& status,
int response_code,
const ResponseCookies& cookies,
const std::string& data) {
if (source == model_fetcher_.get()) {
HandleModelResponse(source, url, status, response_code, cookies, data);
} else if (client_phishing_reports_.find(source) !=
client_phishing_reports_.end()) {
HandlePhishingVerdict(source, url, status, response_code, cookies, data);
} else {
NOTREACHED();
}
}
void ClientSideDetectionService::SetModelStatus(ModelStatus status) {
DCHECK_NE(READY_STATUS, model_status_);
model_status_ = status;
if (READY_STATUS == status || ERROR_STATUS == status) {
for (size_t i = 0; i < open_callbacks_.size(); ++i) {
open_callbacks_[i]->Run(model_file_);
}
STLDeleteElements(&open_callbacks_);
} else {
NOTREACHED();
}
}
void ClientSideDetectionService::OpenModelFileDone(
base::PlatformFileError error_code,
base::PassPlatformFile file,
bool created) {
DCHECK(!created);
if (base::PLATFORM_FILE_OK == error_code) {
// The model file already exists. There is no need to fetch the model.
model_file_ = file.ReleaseValue();
SetModelStatus(READY_STATUS);
} else if (base::PLATFORM_FILE_ERROR_NOT_FOUND == error_code) {
// We need to fetch the model since it does not exist yet.
model_fetcher_.reset(URLFetcher::Create(0 /* ID is not used */,
GURL(kClientModelUrl),
URLFetcher::GET,
this));
model_fetcher_->set_request_context(request_context_getter_.get());
model_fetcher_->Start();
} else {
// It is not clear what we should do in this case. For now we simply fail.
// Hopefully, we'll be able to read the model during the next browser
// restart.
SetModelStatus(ERROR_STATUS);
}
}
void ClientSideDetectionService::CreateModelFileDone(
base::PlatformFileError error_code,
base::PassPlatformFile file,
bool created) {
model_file_ = file.ReleaseValue();
base::FileUtilProxy::WriteCallback* cb = callback_factory_.NewCallback(
&ClientSideDetectionService::WriteModelFileDone);
if (!created ||
base::PLATFORM_FILE_OK != error_code ||
!base::FileUtilProxy::Write(
BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE),
model_file_,
0 /* offset */, tmp_model_string_->data(), tmp_model_string_->size(),
cb)) {
delete cb;
// An error occurred somewhere. We close the model file if necessary and
// then run all the pending callbacks giving them an invalid model file.
CloseModelFile();
SetModelStatus(ERROR_STATUS);
}
}
void ClientSideDetectionService::WriteModelFileDone(
base::PlatformFileError error_code,
int bytes_written) {
if (base::PLATFORM_FILE_OK == error_code) {
SetModelStatus(READY_STATUS);
} else {
// TODO(noelutz): maybe we should retry writing the model since we
// did already fetch the model?
CloseModelFile();
SetModelStatus(ERROR_STATUS);
}
// Delete the model string that we kept around while we were writing the
// string to disk - we don't need it anymore.
tmp_model_string_.reset();
}
void ClientSideDetectionService::CloseModelFile() {
if (model_file_ != base::kInvalidPlatformFileValue) {
base::FileUtilProxy::Close(
BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE),
model_file_,
NULL);
}
model_file_ = base::kInvalidPlatformFileValue;
}
void ClientSideDetectionService::StartGetModelFile(
OpenModelDoneCallback* callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (UNKNOWN_STATUS == model_status_) {
// Store the callback which will be called once we know the status of the
// model file.
open_callbacks_.push_back(callback);
} else {
// The model is either in READY or ERROR state which means we can
// call the callback right away.
callback->Run(model_file_);
delete callback;
}
}
void ClientSideDetectionService::StartClientReportPhishingRequest(
ClientPhishingRequest* verdict,
ClientReportPhishingRequestCallback* callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
scoped_ptr<ClientPhishingRequest> request(verdict);
scoped_ptr<ClientReportPhishingRequestCallback> cb(callback);
std::string request_data;
if (!request->SerializeToString(&request_data)) {
UMA_HISTOGRAM_COUNTS("SBClientPhishing.RequestNotSerialized", 1);
VLOG(1) << "Unable to serialize the CSD request. Proto file changed?";
cb->Run(GURL(request->url()), false);
return;
}
URLFetcher* fetcher = URLFetcher::Create(0 /* ID is not used */,
GURL(kClientReportPhishingUrl),
URLFetcher::POST,
this);
// Remember which callback and URL correspond to the current fetcher object.
ClientReportInfo* info = new ClientReportInfo;
info->callback.swap(cb); // takes ownership of the callback.
info->phishing_url = GURL(request->url());
client_phishing_reports_[fetcher] = info;
fetcher->set_load_flags(net::LOAD_DISABLE_CACHE);
fetcher->set_request_context(request_context_getter_.get());
fetcher->set_upload_data("application/octet-stream", request_data);
fetcher->Start();
// Record that we made a request
phishing_report_times_.push(base::Time::Now());
}
void ClientSideDetectionService::HandleModelResponse(
const URLFetcher* source,
const GURL& url,
const net::URLRequestStatus& status,
int response_code,
const ResponseCookies& cookies,
const std::string& data) {
if (status.is_success() && RC_REQUEST_OK == response_code) {
// Copy the model because it has to be accessible after this function
// returns. Once we have written the model to a file we will delete the
// temporary model string. TODO(noelutz): don't store the model to disk if
// it's invalid.
tmp_model_string_.reset(new std::string(data));
base::FileUtilProxy::CreateOrOpenCallback* cb =
callback_factory_.NewCallback(
&ClientSideDetectionService::CreateModelFileDone);
if (!base::FileUtilProxy::CreateOrOpen(
BrowserThread::GetMessageLoopProxyForThread(BrowserThread::FILE),
model_path_,
base::PLATFORM_FILE_CREATE_ALWAYS |
base::PLATFORM_FILE_WRITE |
base::PLATFORM_FILE_READ,
cb)) {
delete cb;
SetModelStatus(ERROR_STATUS);
}
} else {
SetModelStatus(ERROR_STATUS);
}
}
void ClientSideDetectionService::HandlePhishingVerdict(
const URLFetcher* source,
const GURL& url,
const net::URLRequestStatus& status,
int response_code,
const ResponseCookies& cookies,
const std::string& data) {
ClientPhishingResponse response;
scoped_ptr<ClientReportInfo> info(client_phishing_reports_[source]);
if (status.is_success() && RC_REQUEST_OK == response_code &&
response.ParseFromString(data)) {
// Cache response, possibly flushing an old one.
cache_[info->phishing_url] =
make_linked_ptr(new CacheState(response.phishy(), base::Time::Now()));
info->callback->Run(info->phishing_url, response.phishy());
} else {
DLOG(ERROR) << "Unable to get the server verdict for URL: "
<< info->phishing_url << " status: " << status.status() << " "
<< "response_code:" << response_code;
info->callback->Run(info->phishing_url, false);
}
client_phishing_reports_.erase(source);
delete source;
}
bool ClientSideDetectionService::IsInCache(const GURL& url) {
UpdateCache();
return cache_.find(url) != cache_.end();
}
bool ClientSideDetectionService::GetValidCachedResult(const GURL& url,
bool* is_phishing) {
UpdateCache();
PhishingCache::iterator it = cache_.find(url);
if (it == cache_.end()) {
return false;
}
// We still need to check if the result is valid.
const CacheState& cache_state = *it->second;
if (cache_state.is_phishing ?
cache_state.timestamp > base::Time::Now() - kPositiveCacheInterval :
cache_state.timestamp > base::Time::Now() - kNegativeCacheInterval) {
*is_phishing = cache_state.is_phishing;
return true;
}
return false;
}
void ClientSideDetectionService::UpdateCache() {
// Since we limit the number of requests but allow pass-through for cache
// refreshes, we don't want to remove elements from the cache if they
// could be used for this purpose even if we will not use the entry to
// satisfy the request from the cache.
base::TimeDelta positive_cache_interval =
std::max(kPositiveCacheInterval, kReportsInterval);
base::TimeDelta negative_cache_interval =
std::max(kNegativeCacheInterval, kReportsInterval);
// Remove elements from the cache that will no longer be used.
for (PhishingCache::iterator it = cache_.begin(); it != cache_.end();) {
const CacheState& cache_state = *it->second;
if (cache_state.is_phishing ?
cache_state.timestamp > base::Time::Now() - positive_cache_interval :
cache_state.timestamp > base::Time::Now() - negative_cache_interval) {
++it;
} else {
cache_.erase(it++);
}
}
}
bool ClientSideDetectionService::OverReportLimit() {
return GetNumReports() > kMaxReportsPerInterval;
}
int ClientSideDetectionService::GetNumReports() {
base::Time cutoff = base::Time::Now() - kReportsInterval;
// Erase items older than cutoff because we will never care about them again.
while (!phishing_report_times_.empty() &&
phishing_report_times_.front() < cutoff) {
phishing_report_times_.pop();
}
// Return the number of elements that are above the cutoff.
return phishing_report_times_.size();
}
bool ClientSideDetectionService::InitializePrivateNetworks() {
static const char* const kPrivateNetworks[] = {
"10.0.0.0/8",
"127.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
// IPv6 address ranges
"fc00::/7",
"fec0::/10",
"::1/128",
};
for (size_t i = 0; i < arraysize(kPrivateNetworks); ++i) {
net::IPAddressNumber ip_number;
size_t prefix_length;
if (net::ParseCIDRBlock(kPrivateNetworks[i], &ip_number, &prefix_length)) {
private_networks_.push_back(std::make_pair(ip_number, prefix_length));
} else {
DLOG(FATAL) << "Unable to parse IP address range: "
<< kPrivateNetworks[i];
return false;
}
}
return true;
}
} // namespace safe_browsing