// Copyright (c) 2012 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. // // This test uses the safebrowsing test server published at // http://code.google.com/p/google-safe-browsing/ to test the safebrowsing // protocol implemetation. Details of the safebrowsing testing flow is // documented at // http://code.google.com/p/google-safe-browsing/wiki/ProtocolTesting // // This test launches safebrowsing test server and issues several update // requests against that server. Each update would get different data and after // each update, the test will get a list of URLs from the test server to verify // its repository. The test will succeed only if all updates are performed and // URLs match what the server expected. #include #include "base/bind.h" #include "base/command_line.h" #include "base/environment.h" #include "base/path_service.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_split.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "base/synchronization/lock.h" #include "base/test/test_timeouts.h" #include "base/threading/platform_thread.h" #include "base/threading/thread.h" #include "base/time/time.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/safe_browsing/database_manager.h" #include "chrome/browser/safe_browsing/local_safebrowsing_test_server.h" #include "chrome/browser/safe_browsing/protocol_manager.h" #include "chrome/browser/safe_browsing/safe_browsing_service.h" #include "chrome/browser/ui/browser.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/url_constants.h" #include "chrome/test/base/in_process_browser_test.h" #include "content/public/browser/browser_context.h" #include "content/public/test/test_browser_thread.h" #include "content/public/test/test_utils.h" #include "net/base/load_flags.h" #include "net/base/net_log.h" #include "net/dns/host_resolver.h" #include "net/test/python_utils.h" #include "net/url_request/url_fetcher.h" #include "net/url_request/url_fetcher_delegate.h" #include "net/url_request/url_request_status.h" #include "testing/gtest/include/gtest/gtest.h" using content::BrowserThread; namespace { const base::FilePath::CharType kDataFile[] = FILE_PATH_LITERAL("testing_input_nomac.dat"); const char kUrlVerifyPath[] = "safebrowsing/verify_urls"; const char kDBVerifyPath[] = "safebrowsing/verify_database"; const char kTestCompletePath[] = "test_complete"; struct PhishingUrl { std::string url; std::string list_name; bool is_phishing; }; // Parses server response for verify_urls. The expected format is: // // first.random.url.com/ internal-test-shavar yes // second.random.url.com/ internal-test-shavar yes // ... bool ParsePhishingUrls(const std::string& data, std::vector* phishing_urls) { if (data.empty()) return false; std::vector urls; base::SplitString(data, '\n', &urls); for (size_t i = 0; i < urls.size(); ++i) { if (urls[i].empty()) continue; PhishingUrl phishing_url; std::vector record_parts; base::SplitString(urls[i], '\t', &record_parts); if (record_parts.size() != 3) { LOG(ERROR) << "Unexpected URL format in phishing URL list: " << urls[i]; return false; } phishing_url.url = std::string(url::kHttpScheme) + "://" + record_parts[0]; phishing_url.list_name = record_parts[1]; if (record_parts[2] == "yes") { phishing_url.is_phishing = true; } else if (record_parts[2] == "no") { phishing_url.is_phishing = false; } else { LOG(ERROR) << "Unrecognized expectation in " << urls[i] << ": " << record_parts[2]; return false; } phishing_urls->push_back(phishing_url); } return true; } class FakeSafeBrowsingService : public SafeBrowsingService { public: explicit FakeSafeBrowsingService(const std::string& url_prefix) : url_prefix_(url_prefix) {} SafeBrowsingProtocolConfig GetProtocolConfig() const override { SafeBrowsingProtocolConfig config; config.url_prefix = url_prefix_; // Makes sure the auto update is not triggered. The tests will force the // update when needed. config.disable_auto_update = true; #if defined(OS_ANDROID) config.disable_connection_check = true; #endif config.client_name = "browser_tests"; return config; } private: ~FakeSafeBrowsingService() override {} std::string url_prefix_; DISALLOW_COPY_AND_ASSIGN(FakeSafeBrowsingService); }; // Factory that creates FakeSafeBrowsingService instances. class TestSafeBrowsingServiceFactory : public SafeBrowsingServiceFactory { public: explicit TestSafeBrowsingServiceFactory(const std::string& url_prefix) : url_prefix_(url_prefix) {} SafeBrowsingService* CreateSafeBrowsingService() override { return new FakeSafeBrowsingService(url_prefix_); } private: std::string url_prefix_; }; } // namespace // This starts the browser and keeps status of states related to SafeBrowsing. class SafeBrowsingServerTest : public InProcessBrowserTest { public: SafeBrowsingServerTest() : safe_browsing_service_(NULL), is_database_ready_(true), is_update_scheduled_(false), is_checked_url_in_db_(false), is_checked_url_safe_(false) { } ~SafeBrowsingServerTest() override {} void UpdateSafeBrowsingStatus() { ASSERT_TRUE(safe_browsing_service_); base::AutoLock lock(update_status_mutex_); last_update_ = safe_browsing_service_->protocol_manager_->last_update(); is_update_scheduled_ = safe_browsing_service_->protocol_manager_->update_timer_.IsRunning(); } void ForceUpdate() { content::WindowedNotificationObserver observer( chrome::NOTIFICATION_SAFE_BROWSING_UPDATE_COMPLETE, content::Source(database_manager())); BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, base::Bind(&SafeBrowsingServerTest::ForceUpdateOnIOThread, this)); observer.Wait(); } void ForceUpdateOnIOThread() { EXPECT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::IO)); ASSERT_TRUE(safe_browsing_service_); safe_browsing_service_->protocol_manager_->ForceScheduleNextUpdate( base::TimeDelta::FromSeconds(0)); } void CheckIsDatabaseReady() { base::AutoLock lock(update_status_mutex_); is_database_ready_ = !database_manager()->database_update_in_progress_; } void CheckUrl(SafeBrowsingDatabaseManager::Client* helper, const GURL& url) { ASSERT_TRUE(safe_browsing_service_); base::AutoLock lock(update_status_mutex_); if (database_manager()->CheckBrowseUrl(url, helper)) { is_checked_url_in_db_ = false; is_checked_url_safe_ = true; } else { // In this case, Safebrowsing service will fetch the full hash // from the server and examine that. Once it is done, // set_is_checked_url_safe() will be called via callback. is_checked_url_in_db_ = true; } } SafeBrowsingDatabaseManager* database_manager() { return safe_browsing_service_->database_manager().get(); } bool is_checked_url_in_db() { base::AutoLock l(update_status_mutex_); return is_checked_url_in_db_; } void set_is_checked_url_safe(bool safe) { base::AutoLock l(update_status_mutex_); is_checked_url_safe_ = safe; } bool is_checked_url_safe() { base::AutoLock l(update_status_mutex_); return is_checked_url_safe_; } bool is_database_ready() { base::AutoLock l(update_status_mutex_); return is_database_ready_; } base::Time last_update() { base::AutoLock l(update_status_mutex_); return last_update_; } bool is_update_scheduled() { base::AutoLock l(update_status_mutex_); return is_update_scheduled_; } base::MessageLoop* SafeBrowsingMessageLoop() { return database_manager()->safe_browsing_thread_->message_loop(); } const net::SpawnedTestServer& test_server() const { return *test_server_; } protected: bool InitSafeBrowsingService() { safe_browsing_service_ = g_browser_process->safe_browsing_service(); return safe_browsing_service_ != NULL; } void SetUp() override { base::FilePath datafile_path; ASSERT_TRUE(PathService::Get(base::DIR_SOURCE_ROOT, &datafile_path)); datafile_path = datafile_path.Append(FILE_PATH_LITERAL("third_party")) .Append(FILE_PATH_LITERAL("safe_browsing")) .Append(FILE_PATH_LITERAL("testing")) .Append(kDataFile); test_server_.reset(new LocalSafeBrowsingTestServer(datafile_path)); ASSERT_TRUE(test_server_->Start()); LOG(INFO) << "server is " << test_server_->host_port_pair().ToString(); // Point to the testing server for all SafeBrowsing requests. std::string url_prefix = test_server_->GetURL("safebrowsing").spec(); sb_factory_.reset(new TestSafeBrowsingServiceFactory(url_prefix)); SafeBrowsingService::RegisterFactory(sb_factory_.get()); InProcessBrowserTest::SetUp(); } void TearDown() override { InProcessBrowserTest::TearDown(); SafeBrowsingService::RegisterFactory(NULL); } void SetUpCommandLine(base::CommandLine* command_line) override { // This test uses loopback. No need to use IPv6 especially it makes // local requests slow on Windows trybot when ipv6 local address [::1] // is not setup. command_line->AppendSwitch(switches::kDisableIPv6); // TODO(lzheng): The test server does not understand download related // requests. We need to fix the server. command_line->AppendSwitch(switches::kSbDisableDownloadProtection); // TODO(gcasto): Generate new testing data that includes the // client-side phishing whitelist. command_line->AppendSwitch( switches::kDisableClientSidePhishingDetection); // TODO(kalman): Generate new testing data that includes the extension // blacklist. command_line->AppendSwitch(switches::kSbDisableExtensionBlacklist); // TODO(tburkard): Generate new testing data that includes the side-effect // free whitelist. command_line->AppendSwitch(switches::kSbDisableSideEffectFreeWhitelist); } void SetTestStep(int step) { std::string test_step = base::StringPrintf("test_step=%d", step); safe_browsing_service_->protocol_manager_->set_additional_query(test_step); } private: scoped_ptr sb_factory_; SafeBrowsingService* safe_browsing_service_; scoped_ptr test_server_; // Protects all variables below since they are read on UI thread // but updated on IO thread or safebrowsing thread. base::Lock update_status_mutex_; // States associated with safebrowsing service updates. bool is_database_ready_; base::Time last_update_; bool is_update_scheduled_; // Indicates if there is a match between a URL's prefix and safebrowsing // database (thus potentially it is a phishing URL). bool is_checked_url_in_db_; // True if last verified URL is not a phishing URL and thus it is safe. bool is_checked_url_safe_; DISALLOW_COPY_AND_ASSIGN(SafeBrowsingServerTest); }; // A ref counted helper class that handles callbacks between IO thread and UI // thread. class SafeBrowsingServerTestHelper : public base::RefCountedThreadSafe, public SafeBrowsingDatabaseManager::Client, public net::URLFetcherDelegate { public: SafeBrowsingServerTestHelper(SafeBrowsingServerTest* safe_browsing_test, net::URLRequestContextGetter* request_context) : safe_browsing_test_(safe_browsing_test), response_status_(net::URLRequestStatus::FAILED), request_context_(request_context) { } // Callbacks for SafeBrowsingDatabaseManager::Client. void OnCheckBrowseUrlResult(const GURL& url, SBThreatType threat_type, const std::string& metadata) override { EXPECT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::IO)); EXPECT_TRUE(safe_browsing_test_->is_checked_url_in_db()); safe_browsing_test_->set_is_checked_url_safe( threat_type == SB_THREAT_TYPE_SAFE); BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(&SafeBrowsingServerTestHelper::OnCheckUrlDone, this)); } virtual void OnBlockingPageComplete(bool proceed) { NOTREACHED() << "Not implemented."; } // Functions and callbacks related to CheckUrl. These are used to verify // phishing URLs. void CheckUrl(const GURL& url) { BrowserThread::PostTask(BrowserThread::IO, FROM_HERE, base::Bind(&SafeBrowsingServerTestHelper::CheckUrlOnIOThread, this, url)); content::RunMessageLoop(); } void CheckUrlOnIOThread(const GURL& url) { EXPECT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::IO)); safe_browsing_test_->CheckUrl(this, url); if (!safe_browsing_test_->is_checked_url_in_db()) { // Ends the checking since this URL's prefix is not in database. BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(&SafeBrowsingServerTestHelper::OnCheckUrlDone, this)); } // Otherwise, OnCheckUrlDone is called in OnUrlCheckResult since // safebrowsing service further fetches hashes from safebrowsing server. } void OnCheckUrlDone() { StopUILoop(); } // Updates status from IO Thread. void CheckStatusOnIOThread() { EXPECT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::IO)); safe_browsing_test_->UpdateSafeBrowsingStatus(); safe_browsing_test_->SafeBrowsingMessageLoop()->PostTask(FROM_HERE, base::Bind(&SafeBrowsingServerTestHelper::CheckIsDatabaseReady, this)); } // Checks status in SafeBrowsing Thread. void CheckIsDatabaseReady() { EXPECT_EQ(base::MessageLoop::current(), safe_browsing_test_->SafeBrowsingMessageLoop()); safe_browsing_test_->CheckIsDatabaseReady(); BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(&SafeBrowsingServerTestHelper::OnWaitForStatusUpdateDone, this)); } void OnWaitForStatusUpdateDone() { StopUILoop(); } // Update safebrowsing status. void UpdateStatus() { BrowserThread::PostTask( BrowserThread::IO, FROM_HERE, base::Bind(&SafeBrowsingServerTestHelper::CheckStatusOnIOThread, this)); // Will continue after OnWaitForStatusUpdateDone(). content::RunMessageLoop(); } // Calls test server to fetch database for verification. net::URLRequestStatus::Status FetchDBToVerify( const net::SpawnedTestServer& test_server, int test_step) { // TODO(lzheng): Remove chunk_type=add once it is not needed by the server. std::string path = base::StringPrintf( "%s?client=chromium&appver=1.0&pver=3.0&test_step=%d&chunk_type=add", kDBVerifyPath, test_step); return FetchUrl(test_server.GetURL(path)); } // Calls test server to fetch URLs for verification. net::URLRequestStatus::Status FetchUrlsToVerify( const net::SpawnedTestServer& test_server, int test_step) { std::string path = base::StringPrintf( "%s?client=chromium&appver=1.0&pver=3.0&test_step=%d", kUrlVerifyPath, test_step); return FetchUrl(test_server.GetURL(path)); } // Calls test server to check if test data is done. E.g.: if there is a // bad URL that server expects test to fetch full hash but the test didn't, // this verification will fail. net::URLRequestStatus::Status VerifyTestComplete( const net::SpawnedTestServer& test_server, int test_step) { std::string path = base::StringPrintf( "%s?test_step=%d", kTestCompletePath, test_step); return FetchUrl(test_server.GetURL(path)); } // Callback for URLFetcher. void OnURLFetchComplete(const net::URLFetcher* source) override { source->GetResponseAsString(&response_data_); response_status_ = source->GetStatus().status(); StopUILoop(); } const std::string& response_data() { return response_data_; } private: friend class base::RefCountedThreadSafe; ~SafeBrowsingServerTestHelper() override {} // Stops UI loop after desired status is updated. void StopUILoop() { EXPECT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::UI)); base::MessageLoopForUI::current()->Quit(); } // Fetch a URL. If message_loop_started is true, starts the message loop // so the caller could wait till OnURLFetchComplete is called. net::URLRequestStatus::Status FetchUrl(const GURL& url) { url_fetcher_.reset(net::URLFetcher::Create( url, net::URLFetcher::GET, this)); url_fetcher_->SetLoadFlags(net::LOAD_DISABLE_CACHE); url_fetcher_->SetRequestContext(request_context_); url_fetcher_->Start(); content::RunMessageLoop(); return response_status_; } base::OneShotTimer check_update_timer_; SafeBrowsingServerTest* safe_browsing_test_; scoped_ptr url_fetcher_; std::string response_data_; net::URLRequestStatus::Status response_status_; net::URLRequestContextGetter* request_context_; DISALLOW_COPY_AND_ASSIGN(SafeBrowsingServerTestHelper); }; // TODO(shess): Disabled pending new data for third_party/safe_browsing/testing/ IN_PROC_BROWSER_TEST_F(SafeBrowsingServerTest, DISABLED_SafeBrowsingServerTest) { LOG(INFO) << "Start test"; ASSERT_TRUE(InitSafeBrowsingService()); net::URLRequestContextGetter* request_context = browser()->profile()->GetRequestContext(); scoped_refptr safe_browsing_helper( new SafeBrowsingServerTestHelper(this, request_context)); int last_step = 0; // Waits and makes sure safebrowsing update is not happening. // The wait will stop once OnWaitForStatusUpdateDone in // safe_browsing_helper is called and status from safe_browsing_service_ // is checked. safe_browsing_helper->UpdateStatus(); EXPECT_TRUE(is_database_ready()); EXPECT_FALSE(is_update_scheduled()); EXPECT_TRUE(last_update().is_null()); // Starts updates. After each update, the test will fetch a list of URLs with // expected results to verify with safebrowsing service. If there is no error, // the test moves on to the next step to get more update chunks. // This repeats till there is no update data. for (int step = 1;; step++) { // Every step should be a fresh start. SCOPED_TRACE(base::StringPrintf("step=%d", step)); EXPECT_TRUE(is_database_ready()); EXPECT_FALSE(is_update_scheduled()); // Starts safebrowsing update on IO thread. Waits till scheduled // update finishes. base::Time now = base::Time::Now(); SetTestStep(step); ForceUpdate(); safe_browsing_helper->UpdateStatus(); EXPECT_TRUE(is_database_ready()); EXPECT_FALSE(is_update_scheduled()); EXPECT_FALSE(last_update().is_null()); if (last_update() < now) { // This means no data available anymore. break; } // Fetches URLs to verify and waits till server responses with data. EXPECT_EQ(net::URLRequestStatus::SUCCESS, safe_browsing_helper->FetchUrlsToVerify(test_server(), step)); std::vector phishing_urls; EXPECT_TRUE(ParsePhishingUrls(safe_browsing_helper->response_data(), &phishing_urls)); EXPECT_GT(phishing_urls.size(), 0U); for (size_t j = 0; j < phishing_urls.size(); ++j) { // Verifes with server if a URL is a phishing URL and waits till server // responses. safe_browsing_helper->CheckUrl(GURL(phishing_urls[j].url)); if (phishing_urls[j].is_phishing) { EXPECT_TRUE(is_checked_url_in_db()) << phishing_urls[j].url << " is_phishing: " << phishing_urls[j].is_phishing << " test step: " << step; EXPECT_FALSE(is_checked_url_safe()) << phishing_urls[j].url << " is_phishing: " << phishing_urls[j].is_phishing << " test step: " << step; } else { EXPECT_TRUE(is_checked_url_safe()) << phishing_urls[j].url << " is_phishing: " << phishing_urls[j].is_phishing << " test step: " << step; } } // TODO(lzheng): We should verify the fetched database with local // database to make sure they match. EXPECT_EQ(net::URLRequestStatus::SUCCESS, safe_browsing_helper->FetchDBToVerify(test_server(), step)); EXPECT_GT(safe_browsing_helper->response_data().size(), 0U); last_step = step; } // Verifies with server if test is done and waits till server responses. EXPECT_EQ(net::URLRequestStatus::SUCCESS, safe_browsing_helper->VerifyTestComplete(test_server(), last_step)); EXPECT_EQ("yes", safe_browsing_helper->response_data()); }