diff options
author | mmenke@chromium.org <mmenke@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-06-14 21:28:32 +0000 |
---|---|---|
committer | mmenke@chromium.org <mmenke@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-06-14 21:28:32 +0000 |
commit | e602696544fd704597a8ea2c907ffbcb0f688df4 (patch) | |
tree | 82c1032031c1e6cfe87ef43b367dd7bd24b5a4fa | |
parent | bcba8aa49bfc2db2a74b042a22784b58ed005fc0 (diff) | |
download | chromium_src-e602696544fd704597a8ea2c907ffbcb0f688df4.zip chromium_src-e602696544fd704597a8ea2c907ffbcb0f688df4.tar.gz chromium_src-e602696544fd704597a8ea2c907ffbcb0f688df4.tar.bz2 |
Captive portals intercept all HTTP requests until the user
has logged in, like at Starbucks and airports. When behind
one, all SSL requests timeout after a potentially
substantial delay.
This CL Adds a CaptivePortalTabHelper which triggers captive
portal checks when an SSL load is taking too long. If a
captive portal is found, opens a login tab. Whenever the
new tab is navigated, we check again for a captive portal.
Once we discover the portal is gone, we reload the original
tab.
design doc: https://docs.google.com/a/chromium.org/document/d/1k-gP2sswzYNvryu9NcgN7q5XrsMlUdlUdoW9WRaEmfM/edit
R=cbentzel@chromium.org,avi@chromium.org
BUG=87100, 115487
Review URL: https://chromiumcodereview.appspot.com/10020051
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@142242 0039d316-1c4b-4281-b951-d872f2087c98
23 files changed, 3287 insertions, 3 deletions
diff --git a/build/common.gypi b/build/common.gypi index c2ae4c2..c62afdb 100644 --- a/build/common.gypi +++ b/build/common.gypi @@ -479,6 +479,12 @@ 'linux_use_gold_flags%': 0, }], + ['OS=="android"', { + 'enable_captive_portal_detection%': 0, + }, { + 'enable_captive_portal_detection%': 1, + }], + # Enable Skia UI text drawing incrementally on different platforms. # http://crbug.com/105550 # @@ -559,6 +565,7 @@ 'test_isolation_outdir%': '<(test_isolation_outdir)', 'enable_automation%': '<(enable_automation)', 'enable_printing%': '<(enable_printing)', + 'enable_captive_portal_detection%': '<(enable_captive_portal_detection)', 'force_rlz_use_chrome_net%': '<(force_rlz_use_chrome_net)', 'enable_task_manager%': '<(enable_task_manager)', 'platformsdk_path%': '<(platformsdk_path)', @@ -1518,6 +1525,9 @@ ['enable_printing==1', { 'defines': ['ENABLE_PRINTING=1'], }], + ['enable_captive_portal_detection==1', { + 'defines': ['ENABLE_CAPTIVE_PORTAL_DETECTION=1'], + }], ], # conditions for 'target_defaults' 'target_conditions': [ ['enable_wexit_time_destructors==1', { diff --git a/chrome/browser/captive_portal/captive_portal_browsertest.cc b/chrome/browser/captive_portal/captive_portal_browsertest.cc new file mode 100644 index 0000000..c4ba602 --- /dev/null +++ b/chrome/browser/captive_portal/captive_portal_browsertest.cc @@ -0,0 +1,1693 @@ +// 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. + +#include <map> +#include <set> + +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/command_line.h" +#include "base/compiler_specific.h" +#include "base/file_path.h" +#include "base/message_loop.h" +#include "base/path_service.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/captive_portal/captive_portal_service.h" +#include "chrome/browser/captive_portal/captive_portal_service_factory.h" +#include "chrome/browser/captive_portal/captive_portal_tab_helper.h" +#include "chrome/browser/captive_portal/captive_portal_tab_reloader.h" +#include "chrome/browser/net/url_request_mock_util.h" +#include "chrome/browser/prefs/pref_service.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/browser_navigator.h" +#include "chrome/browser/ui/tab_contents/tab_contents_iterator.h" +#include "chrome/browser/ui/tab_contents/tab_contents.h" +#include "chrome/common/chrome_notification_types.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/chrome_switches.h" +#include "chrome/common/pref_names.h" +#include "chrome/test/base/in_process_browser_test.h" +#include "chrome/test/base/ui_test_utils.h" +#include "content/public/browser/browser_thread.h" +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_types.h" +#include "content/public/browser/render_view_host.h" +#include "content/public/browser/web_contents.h" +#include "content/public/common/url_constants.h" +#include "content/test/net/url_request_failed_job.h" +#include "content/test/net/url_request_mock_http_job.h" +#include "net/base/net_errors.h" +#include "net/url_request/url_request_filter.h" +#include "net/url_request/url_request_job.h" +#include "net/url_request/url_request_status.h" +#include "testing/gtest/include/gtest/gtest.h" + +using content::BrowserThread; + +namespace captive_portal { + +namespace { + +// Path of the fake login page, when using the TestServer. +const char* const kTestServerLoginPath = "files/captive_portal/login.html"; + +// Path of a page with an iframe that has a mock SSL timeout, when using the +// TestServer. +const char* const kTestServerIframeTimeoutPath = + "files/captive_portal/iframe_timeout.html"; + +// The following URLs each have two different behaviors, depending on whether +// URLRequestMockCaptivePortalJobFactory is currently simulating the presence +// of a captive portal or not. + +// A mock URL for the CaptivePortalService's |test_url|. When behind a captive +// portal, this URL return a mock login page. When connected to the Internet, +// it returns a 204 response. +const char* const kMockCaptivePortalTestUrl = + "http://mock.captive.portal/captive_portal_test/"; + +// When behind a captive portal, this URL hangs without committing until a call +// to URLRequestTimeoutOnDemandJob::FailRequests. When that function is called, +// the request will time out. +// +// When connected to the Internet, this URL returns a non-error page. +const char* const kMockHttpsUrl = "https://mock.captive.portal/long_timeout/"; + +// Same as kMockHttpsUrl, except the timeout happens instantly. +const char* const kMockHttpsQuickTimeoutUrl = + "https://mock.captive.portal/quick_timeout/"; + +// Expected title of a tab once an HTTPS load completes, when not behind a +// captive portal. +const char* const kInternetConnectedTitle = "Title Of Awesomeness"; + +// A URL request job that hangs until FailRequests() is called. Started jobs +// are stored in a static class variable containing a linked list so that +// FailRequests() can locate them. +class URLRequestTimeoutOnDemandJob : public net::URLRequestJob, + public base::NonThreadSafe { + public: + // net::URLRequestJob: + virtual void Start() OVERRIDE; + + // All the public static methods below can be called on any thread. + + // Fails all active URLRequestFailOnDemandJobs with connection timeouts. + // Must only be called when there are requests that have been started but not + // yet timed out. + static void FailRequests(); + + // Clears the |waiting_jobs_list_| without having the jobs return anything. + // Used to allow an assertion that jobs are not in the |waiting_jobs_list_| + // when destroyed. Must only be called when there are requests that have + // been started but not yet timed out. + static void AbandonRequests(); + + private: + friend class URLRequestMockCaptivePortalJobFactory; + + explicit URLRequestTimeoutOnDemandJob(net::URLRequest* request); + virtual ~URLRequestTimeoutOnDemandJob(); + + // Attempts to removes |this| from |jobs_|. Returns true if it was removed + // from the list. + bool RemoveFromList(); + + // These do all the work of the corresponding public functions, with the only + // difference being that they must be called on the IO thread. + static void FailRequestsOnIOThread(); + static void AbandonRequestsOnIOThread(); + + // Head of linked list of jobs that have been started and are now waiting to + // be timed out. + static URLRequestTimeoutOnDemandJob* job_list_; + + // The next job that had been started but not yet timed out. + URLRequestTimeoutOnDemandJob* next_job_; + + DISALLOW_COPY_AND_ASSIGN(URLRequestTimeoutOnDemandJob); +}; + +URLRequestTimeoutOnDemandJob* URLRequestTimeoutOnDemandJob::job_list_ = NULL; + +void URLRequestTimeoutOnDemandJob::Start() { + EXPECT_TRUE(CalledOnValidThread()); + + // Insert at start of the list. + next_job_ = job_list_; + job_list_ = this; +} + +// static +void URLRequestTimeoutOnDemandJob::FailRequests() { + content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind(&URLRequestTimeoutOnDemandJob::FailRequestsOnIOThread)); +} + +// static +void URLRequestTimeoutOnDemandJob::AbandonRequests() { + content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind(&URLRequestTimeoutOnDemandJob::AbandonRequestsOnIOThread)); +} + +URLRequestTimeoutOnDemandJob::URLRequestTimeoutOnDemandJob( + net::URLRequest* request) + : net::URLRequestJob(request), + next_job_(NULL) { +} + +URLRequestTimeoutOnDemandJob::~URLRequestTimeoutOnDemandJob() { + // |this| shouldn't be in the list. + EXPECT_FALSE(RemoveFromList()); +} + +bool URLRequestTimeoutOnDemandJob::RemoveFromList() { + URLRequestTimeoutOnDemandJob** job = &job_list_; + while (*job) { + if (*job == this) { + *job = next_job_; + next_job_ = NULL; + return true; + } + job = &next_job_; + } + + // If the job wasn't in this list, |next_job_| should be NULL. + EXPECT_FALSE(next_job_); + return false; +} + +// static +void URLRequestTimeoutOnDemandJob::FailRequestsOnIOThread() { + ASSERT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::IO)); + EXPECT_TRUE(job_list_); + while (job_list_) { + URLRequestTimeoutOnDemandJob* job = job_list_; + // Since the error notification may result in the job's destruction, remove + // it from the job list before the error. + EXPECT_TRUE(job->RemoveFromList()); + job->NotifyStartError(net::URLRequestStatus( + net::URLRequestStatus::FAILED, + net::ERR_CONNECTION_TIMED_OUT)); + } +} + +// static +void URLRequestTimeoutOnDemandJob::AbandonRequestsOnIOThread() { + ASSERT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::IO)); + EXPECT_TRUE(job_list_); + while (job_list_) + EXPECT_TRUE(job_list_->RemoveFromList()); +} + +// URLRequestCaptivePortalJobFactory emulates captive portal behavior. +// Initially, it emulates being behind a captive portal. When +// SetBehindCaptivePortal(false) is called, it emulates behavior when not behind +// a captive portal. The class itself is never instantiated. +// +// It handles requests for kMockCaptivePortalTestUrl, kMockHttpsUrl, and +// kMockHttpsQuickTimeoutUrl. +class URLRequestMockCaptivePortalJobFactory { + public: + // The public static methods below can be called on any thread. + + // Adds the testing URLs to the net::URLRequestFilter. Should only be called + // once. + static void AddUrlHandlers(); + + // Sets whether or not there is a captive portal. Outstanding requests are + // not affected. + static void SetBehindCaptivePortal(bool behind_captive_portal); + + private: + // These do all the work of the corresponding public functions, with the only + // difference being that they must be called on the IO thread. + static void AddUrlHandlersOnIOThread(); + static void SetBehindCaptivePortalOnIOThread(bool behind_captive_portal); + + // Returns a URLRequestJob that reflects the current captive portal state + // for the URLs: kMockCaptivePortalTestUrl, kMockHttpsUrl, and + // kMockHttpsQuickTimeoutUrl. See documentation of individual URLs for + // actual behavior. + static net::URLRequestJob* Factory(net::URLRequest* request, + const std::string& scheme); + + static bool behind_captive_portal_; + + DISALLOW_IMPLICIT_CONSTRUCTORS(URLRequestMockCaptivePortalJobFactory); +}; + +bool URLRequestMockCaptivePortalJobFactory::behind_captive_portal_ = true; + +// static +void URLRequestMockCaptivePortalJobFactory::AddUrlHandlers() { + content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind( + &URLRequestMockCaptivePortalJobFactory::AddUrlHandlersOnIOThread)); +} + +// static +void URLRequestMockCaptivePortalJobFactory::SetBehindCaptivePortal( + bool behind_captive_portal) { + content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind( + &URLRequestMockCaptivePortalJobFactory:: + SetBehindCaptivePortalOnIOThread, + behind_captive_portal)); +} + +// static +void URLRequestMockCaptivePortalJobFactory::AddUrlHandlersOnIOThread() { + EXPECT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::IO)); + net::URLRequestFilter* filter = net::URLRequestFilter::GetInstance(); + filter->AddHostnameHandler("http", "mock.captive.portal", + URLRequestMockCaptivePortalJobFactory::Factory); + filter->AddHostnameHandler("https", "mock.captive.portal", + URLRequestMockCaptivePortalJobFactory::Factory); +} + +// static +void URLRequestMockCaptivePortalJobFactory::SetBehindCaptivePortalOnIOThread( + bool behind_captive_portal) { + EXPECT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::IO)); + behind_captive_portal_ = behind_captive_portal; +} + +// static +net::URLRequestJob* URLRequestMockCaptivePortalJobFactory::Factory( + net::URLRequest* request, + const std::string& scheme) { + EXPECT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::IO)); + + // The PathService is threadsafe. + FilePath root_http; + PathService::Get(chrome::DIR_TEST_DATA, &root_http); + + if (scheme == "https") { + if (behind_captive_portal_) { + // If not logged in to the captive portal, HTTPS requests will time out, + // either immediately on on demand. + if (request->url() == GURL(kMockHttpsQuickTimeoutUrl)) + return new URLRequestFailedJob(request, net::ERR_CONNECTION_TIMED_OUT); + return new URLRequestTimeoutOnDemandJob(request); + } + // Once logged in to the portal, HTTPS requests return the page that was + // actually requested. + return new URLRequestMockHTTPJob( + request, + root_http.Append(FILE_PATH_LITERAL("title2.html"))); + } + + // The URL is the captive portal test URL. + + if (behind_captive_portal_) { + // Prior to logging in to the portal, HTTP requests go to the login page. + return new URLRequestMockHTTPJob( + request, + root_http.Append( + FILE_PATH_LITERAL("captive_portal/login.html"))); + } + // After logging in to the portal, the test URL returns a 204 response. + return new URLRequestMockHTTPJob( + request, + root_http.Append( + FILE_PATH_LITERAL("captive_portal/page204.html"))); +} + +// Creates a server-side redirect for use with the TestServer. +std::string CreateServerRedirect(const std::string& dest_url) { + const char* const kServerRedirectBase = "server-redirect?"; + return kServerRedirectBase + dest_url; +} + +// Returns the total number of loading tabs across all Browsers, for all +// Profiles. +int NumLoadingTabs() { + int num_loading_tabs = 0; + for (TabContentsIterator tab_contents_it; + !tab_contents_it.done(); + ++tab_contents_it) { + if (tab_contents_it->web_contents()->IsLoading()) + ++num_loading_tabs; + } + return num_loading_tabs; +} + +bool IsLoginTab(TabContents* tab_contents) { + return tab_contents->captive_portal_tab_helper()->IsLoginTab(); +} + +// Tracks how many times each tab has been navigated since the Observer was +// created. The standard TestNavigationObserver can only watch specific +// pre-existing tabs or loads in serial for all tabs. +class MultiNavigationObserver : public content::NotificationObserver { + public: + MultiNavigationObserver(); + virtual ~MultiNavigationObserver(); + + // Waits for exactly |num_navigations_to_wait_for| LOAD_STOP + // notifications to have occurred since the construction of |this|. More + // navigations than expected occuring will trigger a expect failure. + void WaitForNavigations(int num_navigations_to_wait_for); + + // Returns the number of LOAD_STOP events that have occurred for + // |web_contents| since this was constructed. + int NumNavigationsForTab(content::WebContents* web_contents) const; + + // The number of LOAD_STOP events since |this| was created. + int num_navigations() const { return num_navigations_; } + + private: + typedef std::map<content::WebContents*, int> TabNavigationMap; + + // content::NotificationObserver: + virtual void Observe(int type, const content::NotificationSource& source, + const content::NotificationDetails& details) OVERRIDE; + + int num_navigations_; + + // Map of how many times each tab has navigated since |this| was created. + TabNavigationMap tab_navigation_map_; + + // Total number of navigations to wait for. Value only matters when + // |waiting_for_navigation_| is true. + int num_navigations_to_wait_for_; + + // True if WaitForNavigations has been called, until + // |num_navigations_to_wait_for_| have been observed. + bool waiting_for_navigation_; + + content::NotificationRegistrar registrar_; + + DISALLOW_COPY_AND_ASSIGN(MultiNavigationObserver); +}; + +MultiNavigationObserver::MultiNavigationObserver() + : num_navigations_(0), + num_navigations_to_wait_for_(0), + waiting_for_navigation_(false) { + registrar_.Add(this, content::NOTIFICATION_LOAD_STOP, + content::NotificationService::AllSources()); +} + +MultiNavigationObserver::~MultiNavigationObserver() { +} + +void MultiNavigationObserver::WaitForNavigations( + int num_navigations_to_wait_for) { + // Shouldn't already be waiting for navigations. + EXPECT_FALSE(waiting_for_navigation_); + EXPECT_LT(0, num_navigations_to_wait_for); + if (num_navigations_ < num_navigations_to_wait_for) { + num_navigations_to_wait_for_ = num_navigations_to_wait_for; + waiting_for_navigation_ = true; + ui_test_utils::RunMessageLoop(); + EXPECT_FALSE(waiting_for_navigation_); + } + EXPECT_EQ(num_navigations_, num_navigations_to_wait_for); +} + +int MultiNavigationObserver::NumNavigationsForTab( + content::WebContents* web_contents) const { + TabNavigationMap::const_iterator tab_navigations = + tab_navigation_map_.find(web_contents); + if (tab_navigations == tab_navigation_map_.end()) + return 0; + return tab_navigations->second; +} + +void MultiNavigationObserver::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + ASSERT_EQ(type, content::NOTIFICATION_LOAD_STOP); + content::NavigationController* controller = + content::Source<content::NavigationController>(source).ptr(); + ++num_navigations_; + ++tab_navigation_map_[controller->GetWebContents()]; + if (waiting_for_navigation_ && + num_navigations_to_wait_for_ == num_navigations_) { + waiting_for_navigation_ = false; + MessageLoopForUI::current()->Quit(); + } +} + +// An observer for watching the CaptivePortalService. It tracks the last +// received result and the total number of received results. +class CaptivePortalObserver : public content::NotificationObserver { + public: + explicit CaptivePortalObserver(Profile* profile); + + // Runs the message loop until until at exactly |update_count| capitive portal + // results have been received, since this creation of |this|. Expects no + // additional captive portal results. + void WaitForResults(int num_results_to_wait_for); + + int num_results_received() const { return num_results_received_; } + + Result captive_portal_result() const { + return captive_portal_result_; + } + + private: + // Records results and exits the message loop, if needed. + void Observe(int type, + const content::NotificationSource& source, + const content::NotificationDetails& details); + + // Number of times OnPortalResult has been called since construction. + int num_results_received_; + + // If WaitForResults was called, the total number of updates for which to + // wait. Value doesn't matter when |waiting_for_result_| is false. + int num_results_to_wait_for_; + + bool waiting_for_result_; + + Profile* profile_; + + CaptivePortalService* captive_portal_service_; + + // Last result received. + Result captive_portal_result_; + + content::NotificationRegistrar registrar_; + + DISALLOW_COPY_AND_ASSIGN(CaptivePortalObserver); +}; + +CaptivePortalObserver::CaptivePortalObserver(Profile* profile) + : num_results_received_(0), + num_results_to_wait_for_(0), + waiting_for_result_(false), + profile_(profile), + captive_portal_service_( + CaptivePortalServiceFactory::GetForProfile(profile)), + captive_portal_result_( + captive_portal_service_->last_detection_result()) { + registrar_.Add(this, + chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT, + content::Source<Profile>(profile_)); +} + +void CaptivePortalObserver::WaitForResults(int num_results_to_wait_for) { + EXPECT_LT(0, num_results_to_wait_for); + EXPECT_FALSE(waiting_for_result_); + if (num_results_received_ < num_results_to_wait_for) { + num_results_to_wait_for_ = num_results_to_wait_for; + waiting_for_result_ = true; + ui_test_utils::RunMessageLoop(); + EXPECT_FALSE(waiting_for_result_); + } + EXPECT_EQ(num_results_received_, num_results_to_wait_for); +} + +void CaptivePortalObserver::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + ASSERT_EQ(type, chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT); + ASSERT_EQ(profile_, content::Source<Profile>(source).ptr()); + + CaptivePortalService::Results* results = + content::Details<CaptivePortalService::Results>(details).ptr(); + + EXPECT_EQ(captive_portal_result_, results->previous_result); + EXPECT_EQ(captive_portal_service_->last_detection_result(), + results->result); + + captive_portal_result_ = results->result; + ++num_results_received_; + + if (waiting_for_result_ && + num_results_to_wait_for_ == num_results_received_) { + waiting_for_result_ = false; + MessageLoop::current()->Quit(); + } +} + +} // namespace + +class CaptivePortalBrowserTest : public InProcessBrowserTest { + public: + CaptivePortalBrowserTest(); + + // InProcessBrowserTest: + virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE; + virtual void SetUpOnMainThread() OVERRIDE; + virtual void CleanUpOnMainThread() OVERRIDE; + + // Sets the captive portal checking preference. Does not affect the command + // line flag, which is set in SetUpCommandLine. + void EnableCaptivePortalDetection(Profile* profile, bool enabled); + + // Sets up the captive portal service for the given profile so that + // all checks go to |test_url|. Also disables all timers. + void SetUpCaptivePortalService(Profile* profile, const GURL& test_url); + + // Returns true if |browser|'s profile is currently running a captive portal + // check. + bool CheckPending(Browser* browser); + + // Returns the CaptivePortalTabReloader::State of |tab_contents|. + CaptivePortalTabReloader::State GetStateOfTabReloader( + TabContents* tab_contents) const; + + // Returns the CaptivePortalTabReloader::State of the indicated tab. + CaptivePortalTabReloader::State GetStateOfTabReloaderAt(Browser* browser, + int index) const; + + // Returns the number of tabs with the given state, across all profiles. + int NumTabsWithState(CaptivePortalTabReloader::State state) const; + + // Returns the number of tabs broken by captive portals, across all profiles. + int NumBrokenTabs() const; + + // Returns the number of tabs that need to be reloaded due to having logged + // in to a captive portal, across all profiles. + int NumNeedReloadTabs() const; + + // Navigates |browser|'s active tab to |url| and expects no captive portal + // test to be triggered. |expected_navigations| is the number of times the + // active tab will end up being navigated. It should be 1, except for the + // Link Doctor page, which acts like two navigations. + void NavigateToPageExpectNoTest(Browser* browser, + const GURL& url, + int expected_navigations); + + // Navigates |browser|'s active tab to an SSL tab that takes a while to load, + // triggering a captive portal check, which is expected to give the result + // |expected_result|. The page finishes loading, with a timeout, after the + // captive portal check. + void SlowLoadNoCaptivePortal(Browser* browser, Result expected_result); + + // Navigates |browser|'s active tab to an SSL timeout, expecting a captive + // portal check to be triggered and return a result which will indicates + // there's no detected captive portal. + void FastTimeoutNoCaptivePortal(Browser* browser, Result expected_result); + + // Navigates the active tab to a slow loading SSL page, which will then + // trigger a captive portal test. The test is expected to find a captive + // portal. The slow loading page will continue to load after the function + // returns, until URLRequestTimeoutOnDemandJob::FailRequests() is called, + // at which point it will timeout. + // + // When |expect_login_tab| is false, no login tab is expected to be opened, + // because one already exists, and the function returns once the captive + // portal test is complete. + // + // If |expect_login_tab| is true, a login tab is then expected to be opened. + // It waits until both the login tab has finished loading, and two captive + // portal tests complete. The second test is triggered by the load of the + // captive portal tab completing. + // + // This function must not be called when the active tab is currently loading. + void SlowLoadBehindCaptivePortal(Browser* browser, bool expect_login_tab); + + // Just like SlowLoadBehindCaptivePortal, except the navigated tab has + // a connection timeout rather having its time trigger, and the function + // waits until that timeout occurs. + void FastTimeoutBehindCaptivePortal(Browser* browser, + bool expect_open_login_tab); + + // Navigates the login tab without logging in. The login tab must be the + // specified browser's active tab. Expects no other tab to change state. + // |num_loading_tabs| and |num_timed_out_tabs| are used as extra checks + // that nothing has gone wrong prior to the function call. + void NavigateLoginTab(Browser* browser, + int num_loading_tabs, + int num_timed_out_tabs); + + // Simulates a login by updating the URLRequestMockCaptivePortalJob's + // behind captive portal state, and navigating the login tab. Waits for + // all broken but not loading tabs to be reloaded. + // |num_loading_tabs| and |num_timed_out_tabs| are used as extra checks + // that nothing has gone wrong prior to the function call. + void Login(Browser* browser, int num_loading_tabs, int num_timed_out_tabs); + + // Makes the slow SSL loads of all active tabs time out at once, and waits for + // them to finish both that load and the automatic reload it should trigger. + // There should be no timed out tabs when this is called. The former login + // tab should be the active tab. + void FailLoadsAfterLogin(Browser* browser, int num_loading_tabs); + + // Makes the slow SSL loads of all active tabs time out at once, and waits for + // them to finish displaying their error pages. The login tab should be the + // active tab. There should be no timed out tabs when this is called. + void FailLoadsWithoutLogin(Browser* browser, int num_loading_tabs); + + // Sets the timeout used by a CaptivePortalTabReloader on slow SSL loads + // before a captive portal check. + void SetSlowSSLLoadTime(CaptivePortalTabReloader* tab_reloader, + base::TimeDelta slow_ssl_load_time); + + CaptivePortalTabReloader* GetTabReloader(TabContents* tab_contents) const; + + private: + DISALLOW_COPY_AND_ASSIGN(CaptivePortalBrowserTest); +}; + +CaptivePortalBrowserTest::CaptivePortalBrowserTest() { +} + +void CaptivePortalBrowserTest::SetUpOnMainThread() { + // Enable mock requests. + content::BrowserThread::PostTask( + content::BrowserThread::IO, FROM_HERE, + base::Bind(&chrome_browser_net::SetUrlRequestMocksEnabled, true)); + URLRequestMockCaptivePortalJobFactory::AddUrlHandlers(); + + EnableCaptivePortalDetection(browser()->profile(), true); + + // Set the captive portal service to use URLRequestMockCaptivePortalJob's + // mock URL, by default. + SetUpCaptivePortalService(browser()->profile(), + GURL(kMockCaptivePortalTestUrl)); +} + +void CaptivePortalBrowserTest::CleanUpOnMainThread() { + // No test should have a captive portal check pending on quit. + EXPECT_FALSE(CheckPending(browser())); +} + +void CaptivePortalBrowserTest::SetUpCommandLine( + CommandLine* command_line) { + command_line->AppendSwitch(switches::kCaptivePortalDetection); +} + +void CaptivePortalBrowserTest::EnableCaptivePortalDetection( + Profile* profile, bool enabled) { + profile->GetPrefs()->SetBoolean(prefs::kAlternateErrorPagesEnabled, enabled); +} + +void CaptivePortalBrowserTest::SetUpCaptivePortalService(Profile* profile, + const GURL& test_url) { + CaptivePortalService* captive_portal_service = + CaptivePortalServiceFactory::GetForProfile(profile); + captive_portal_service->set_test_url(test_url); + + // Don't use any non-zero timers. Timers are checked in unit tests. + CaptivePortalService::RecheckPolicy* recheck_policy = + &captive_portal_service->recheck_policy(); + recheck_policy->initial_backoff_no_portal_ms = 0; + recheck_policy->initial_backoff_portal_ms = 0; + recheck_policy->backoff_policy.maximum_backoff_ms = 0; +} + +bool CaptivePortalBrowserTest::CheckPending(Browser* browser) { + CaptivePortalService* captive_portal_service = + CaptivePortalServiceFactory::GetForProfile(browser->profile()); + + return captive_portal_service->FetchingURL() || + captive_portal_service->TimerRunning(); +} + +CaptivePortalTabReloader::State CaptivePortalBrowserTest::GetStateOfTabReloader( + TabContents* tab_contents) const { + return GetTabReloader(tab_contents)->state(); +} + +CaptivePortalTabReloader::State +CaptivePortalBrowserTest::GetStateOfTabReloaderAt(Browser* browser, + int index) const { + return GetStateOfTabReloader(browser->GetTabContentsAt(index)); +} + +int CaptivePortalBrowserTest::NumTabsWithState( + CaptivePortalTabReloader::State state) const { + int num_tabs = 0; + for (TabContentsIterator tab_contents_it; + !tab_contents_it.done(); + ++tab_contents_it) { + if (GetStateOfTabReloader(*tab_contents_it) == state) + ++num_tabs; + } + return num_tabs; +} + +int CaptivePortalBrowserTest::NumBrokenTabs() const { + return NumTabsWithState(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL); +} + +int CaptivePortalBrowserTest::NumNeedReloadTabs() const { + return NumTabsWithState(CaptivePortalTabReloader::STATE_NEEDS_RELOAD); +} + +void CaptivePortalBrowserTest::NavigateToPageExpectNoTest( + Browser* browser, + const GURL& url, + int expected_navigations) { + MultiNavigationObserver navigation_observer; + CaptivePortalObserver portal_observer(browser->profile()); + + ui_test_utils::NavigateToURLBlockUntilNavigationsComplete( + browser, url, expected_navigations); + + // No captive portal checks should have ocurred or be pending, and there + // should be no new tabs. + EXPECT_EQ(0, portal_observer.num_results_received()); + EXPECT_FALSE(CheckPending(browser)); + EXPECT_EQ(1, browser->tab_count()); + EXPECT_EQ(expected_navigations, navigation_observer.num_navigations()); + EXPECT_EQ(0, NumLoadingTabs()); +} + +void CaptivePortalBrowserTest::SlowLoadNoCaptivePortal( + Browser* browser, Result expected_result) { + CaptivePortalTabReloader* tab_reloader = + GetTabReloader(browser->GetActiveTabContents()); + ASSERT_TRUE(tab_reloader); + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta()); + + MultiNavigationObserver navigation_observer; + CaptivePortalObserver portal_observer(browser->profile()); + ui_test_utils::NavigateToURLWithDisposition(browser, + GURL(kMockHttpsUrl), + CURRENT_TAB, + ui_test_utils::BROWSER_TEST_NONE); + + portal_observer.WaitForResults(1); + + ASSERT_EQ(1, browser->tab_count()); + EXPECT_EQ(expected_result, portal_observer.captive_portal_result()); + EXPECT_EQ(1, portal_observer.num_results_received()); + EXPECT_EQ(0, navigation_observer.num_navigations()); + EXPECT_FALSE(CheckPending(browser)); + + // First tab should still be loading. + EXPECT_EQ(1, NumLoadingTabs()); + + // Original request times out. + URLRequestTimeoutOnDemandJob::FailRequests(); + navigation_observer.WaitForNavigations(1); + + ASSERT_EQ(1, browser->tab_count()); + EXPECT_EQ(1, portal_observer.num_results_received()); + EXPECT_FALSE(CheckPending(browser)); + EXPECT_EQ(0, NumLoadingTabs()); + + // Set a slow SSL load time to prevent the timer from triggering. + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta::FromDays(1)); +} + +void CaptivePortalBrowserTest::FastTimeoutNoCaptivePortal( + Browser* browser, Result expected_result) { + ASSERT_NE(expected_result, RESULT_BEHIND_CAPTIVE_PORTAL); + + // Set the load time to be large, so the timer won't trigger. The value is + // not restored at the end of the function. + CaptivePortalTabReloader* tab_reloader = + GetTabReloader(browser->GetActiveTabContents()); + ASSERT_TRUE(tab_reloader); + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta::FromHours(1)); + + MultiNavigationObserver navigation_observer; + CaptivePortalObserver portal_observer(browser->profile()); + + // Neither of these should be changed by the navigation. + int active_index = browser->active_index(); + int expected_tab_count = browser->tab_count(); + + ui_test_utils::NavigateToURL( + browser, + URLRequestFailedJob::GetMockHttpsUrl(net::ERR_CONNECTION_TIMED_OUT)); + + // An attempt to detect a captive portal should have started by now. If not, + // abort early to prevent hanging. + ASSERT_TRUE(portal_observer.num_results_received() > 0 || + CheckPending(browser)); + + portal_observer.WaitForResults(1); + navigation_observer.WaitForNavigations(1); + + // Check the result. + EXPECT_EQ(1, portal_observer.num_results_received()); + EXPECT_EQ(expected_result, portal_observer.captive_portal_result()); + + // Check that the right tab was navigated, and there were no extra + // navigations. + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser->GetWebContentsAt(active_index))); + EXPECT_EQ(0, NumLoadingTabs()); + + // Check the tab's state, and verify no captive portal check is pending. + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(browser, 0)); + EXPECT_FALSE(CheckPending(browser)); + + // Make sure no login tab was opened. + EXPECT_EQ(expected_tab_count, browser->tab_count()); +} + +void CaptivePortalBrowserTest::SlowLoadBehindCaptivePortal( + Browser* browser, + bool expect_open_login_tab) { + // Calling this on a tab that's waiting for a load to manually be timed out + // will result in a hang. + ASSERT_FALSE(browser->GetActiveWebContents()->IsLoading()); + + // Trigger a captive portal check quickly. + CaptivePortalTabReloader* tab_reloader = + GetTabReloader(browser->GetActiveTabContents()); + ASSERT_TRUE(tab_reloader); + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta()); + + // Number of tabs expected to be open after the captive portal checks + // have completed. + int initial_tab_count = browser->tab_count(); + int initial_active_index = browser->active_index(); + int initial_loading_tabs = NumLoadingTabs(); + int expected_broken_tabs = NumBrokenTabs(); + if (CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL != + GetStateOfTabReloader(browser->GetActiveTabContents())) { + ++expected_broken_tabs; + } + + MultiNavigationObserver navigation_observer; + CaptivePortalObserver portal_observer(browser->profile()); + ui_test_utils::NavigateToURLWithDisposition(browser, + GURL(kMockHttpsUrl), + CURRENT_TAB, + ui_test_utils::BROWSER_TEST_NONE); + + if (expect_open_login_tab) { + portal_observer.WaitForResults(2); + navigation_observer.WaitForNavigations(1); + EXPECT_EQ(2, portal_observer.num_results_received()); + + ASSERT_EQ(initial_tab_count + 1, browser->tab_count()); + EXPECT_EQ(initial_tab_count, browser->active_index()); + + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser->GetWebContentsAt(initial_tab_count))); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser->GetTabContentsAt(1))); + EXPECT_TRUE(IsLoginTab(browser->GetTabContentsAt(1))); + } else { + portal_observer.WaitForResults(1); + EXPECT_EQ(0, navigation_observer.num_navigations()); + EXPECT_EQ(initial_active_index, browser->active_index()); + ASSERT_EQ(initial_tab_count, browser->tab_count()); + EXPECT_EQ(initial_active_index, browser->active_index()); + } + + EXPECT_EQ(initial_loading_tabs + 1, NumLoadingTabs()); + EXPECT_EQ(expected_broken_tabs, NumBrokenTabs()); + EXPECT_EQ(RESULT_BEHIND_CAPTIVE_PORTAL, + portal_observer.captive_portal_result()); + EXPECT_FALSE(CheckPending(browser)); + + EXPECT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + GetStateOfTabReloader( + browser->GetTabContentsAt(initial_active_index))); + + // Reset the load time to be large, so the timer won't trigger on a reload. + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta::FromHours(1)); +} + +void CaptivePortalBrowserTest::FastTimeoutBehindCaptivePortal( + Browser* browser, + bool expect_open_login_tab) { + // Calling this on a tab that's waiting for a load to manually be timed out + // will result in a hang. + ASSERT_FALSE(browser->GetActiveWebContents()->IsLoading()); + + // Set the load time to be large, so the timer won't trigger. The value is + // not restored at the end of the function. + CaptivePortalTabReloader* tab_reloader = + GetTabReloader(browser->GetActiveTabContents()); + ASSERT_TRUE(tab_reloader); + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta::FromHours(1)); + + // Number of tabs expected to be open after the captive portal checks + // have completed. + int initial_tab_count = browser->tab_count(); + int initial_active_index = browser->active_index(); + int initial_loading_tabs = NumLoadingTabs(); + int expected_broken_tabs = NumBrokenTabs(); + if (CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL != + GetStateOfTabReloader(browser->GetActiveTabContents())) { + ++expected_broken_tabs; + } + + MultiNavigationObserver navigation_observer; + CaptivePortalObserver portal_observer(browser->profile()); + ui_test_utils::NavigateToURLWithDisposition(browser, + GURL(kMockHttpsQuickTimeoutUrl), + CURRENT_TAB, + ui_test_utils::BROWSER_TEST_NONE); + + if (expect_open_login_tab) { + portal_observer.WaitForResults(2); + navigation_observer.WaitForNavigations(2); + EXPECT_EQ(2, portal_observer.num_results_received()); + + ASSERT_EQ(initial_tab_count + 1, browser->tab_count()); + EXPECT_EQ(initial_tab_count, browser->active_index()); + // Make sure that the originally active tab and the captive portal tab have + // each loaded once. + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser->GetWebContentsAt(initial_active_index))); + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser->GetWebContentsAt(initial_tab_count))); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser->GetTabContentsAt(1))); + EXPECT_TRUE(IsLoginTab(browser->GetTabContentsAt(1))); + } else { + portal_observer.WaitForResults(1); + navigation_observer.WaitForNavigations(1); + EXPECT_EQ(1, portal_observer.num_results_received()); + + EXPECT_EQ(initial_active_index, browser->active_index()); + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser->GetWebContentsAt(initial_active_index))); + ASSERT_EQ(initial_tab_count, browser->tab_count()); + EXPECT_EQ(initial_active_index, browser->active_index()); + } + + EXPECT_EQ(initial_loading_tabs, NumLoadingTabs()); + EXPECT_EQ(expected_broken_tabs, NumBrokenTabs()); + EXPECT_EQ(RESULT_BEHIND_CAPTIVE_PORTAL, + portal_observer.captive_portal_result()); + EXPECT_FALSE(CheckPending(browser)); + + EXPECT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + GetStateOfTabReloader( + browser->GetTabContentsAt(initial_active_index))); +} + +void CaptivePortalBrowserTest::NavigateLoginTab(Browser* browser, + int num_loading_tabs, + int num_timed_out_tabs) { + MultiNavigationObserver navigation_observer; + CaptivePortalObserver portal_observer(browser->profile()); + + int initial_tab_count = browser->tab_count(); + EXPECT_EQ(num_loading_tabs, NumLoadingTabs()); + EXPECT_EQ(num_timed_out_tabs, NumBrokenTabs() - NumLoadingTabs()); + + int login_tab_index = browser->active_index(); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser->GetActiveTabContents())); + ASSERT_TRUE(IsLoginTab(browser->GetActiveTabContents())); + + // Do the navigation. + content::RenderViewHost* render_view_host = + browser->GetActiveWebContents()->GetRenderViewHost(); + render_view_host->ExecuteJavascriptInWebFrame( + string16(), + ASCIIToUTF16("submitForm()")); + + portal_observer.WaitForResults(1); + navigation_observer.WaitForNavigations(1); + + // Check the captive portal result. + EXPECT_EQ(RESULT_BEHIND_CAPTIVE_PORTAL, + portal_observer.captive_portal_result()); + EXPECT_EQ(1, portal_observer.num_results_received()); + EXPECT_FALSE(CheckPending(browser)); + + // Make sure not much has changed. + EXPECT_EQ(initial_tab_count, browser->tab_count()); + EXPECT_EQ(num_loading_tabs, NumLoadingTabs()); + EXPECT_EQ(num_loading_tabs + num_timed_out_tabs, NumBrokenTabs()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser->GetTabContentsAt(login_tab_index))); + EXPECT_TRUE(IsLoginTab(browser->GetTabContentsAt(login_tab_index))); + + // Make sure there were no unexpected navigations. + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser->GetWebContentsAt(login_tab_index))); +} + +void CaptivePortalBrowserTest::Login(Browser* browser, + int num_loading_tabs, + int num_timed_out_tabs) { + // Simulate logging in. + URLRequestMockCaptivePortalJobFactory::SetBehindCaptivePortal(false); + + MultiNavigationObserver navigation_observer; + CaptivePortalObserver portal_observer(browser->profile()); + + int initial_tab_count = browser->tab_count(); + ASSERT_EQ(num_loading_tabs, NumLoadingTabs()); + EXPECT_EQ(num_timed_out_tabs, NumBrokenTabs() - NumLoadingTabs()); + + // Verify that the login page is on top. + int login_tab_index = browser->active_index(); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser->GetTabContentsAt(login_tab_index))); + ASSERT_TRUE(IsLoginTab(browser->GetTabContentsAt(login_tab_index))); + + // Trigger a navigation. + content::RenderViewHost* render_view_host = + browser->GetActiveWebContents()->GetRenderViewHost(); + render_view_host->ExecuteJavascriptInWebFrame( + string16(), + ASCIIToUTF16("submitForm()")); + + portal_observer.WaitForResults(1); + + // Wait for all the timed out tabs to reload. + navigation_observer.WaitForNavigations(1 + num_timed_out_tabs); + EXPECT_EQ(1, portal_observer.num_results_received()); + + // The tabs that were loading before should still be loading, and now be in + // STATE_NEEDS_RELOAD. + EXPECT_EQ(0, NumBrokenTabs()); + EXPECT_EQ(num_loading_tabs, NumLoadingTabs()); + EXPECT_EQ(num_loading_tabs, NumNeedReloadTabs()); + + // Make sure that the broken tabs have reloaded, and there's no more + // captive portal tab. + EXPECT_EQ(initial_tab_count, browser->tab_count()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(browser, login_tab_index)); + EXPECT_FALSE(IsLoginTab(browser->GetTabContentsAt(login_tab_index))); + + // Make sure there were no unexpected navigations of the login tab. + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser->GetWebContentsAt(login_tab_index))); +} + +void CaptivePortalBrowserTest::FailLoadsAfterLogin(Browser* browser, + int num_loading_tabs) { + ASSERT_EQ(num_loading_tabs, NumLoadingTabs()); + ASSERT_EQ(num_loading_tabs, NumNeedReloadTabs()); + EXPECT_EQ(0, NumBrokenTabs()); + + int initial_num_tabs = browser->tab_count(); + int initial_active_tab = browser->active_index(); + + CaptivePortalObserver portal_observer(browser->profile()); + MultiNavigationObserver navigation_observer; + // Connection finally times out. + URLRequestTimeoutOnDemandJob::FailRequests(); + + navigation_observer.WaitForNavigations(2 * num_loading_tabs); + + // No captive portal checks should have ocurred or be pending, and there + // should be no new tabs. + EXPECT_EQ(0, portal_observer.num_results_received()); + EXPECT_FALSE(CheckPending(browser)); + EXPECT_EQ(initial_num_tabs, browser->tab_count()); + + EXPECT_EQ(initial_active_tab, browser->active_index()); + + EXPECT_EQ(0, NumNeedReloadTabs()); + EXPECT_EQ(0, NumLoadingTabs()); + EXPECT_EQ(0, navigation_observer.NumNavigationsForTab( + browser->GetActiveWebContents())); +} + +void CaptivePortalBrowserTest::FailLoadsWithoutLogin(Browser* browser, + int num_loading_tabs) { + ASSERT_EQ(num_loading_tabs, NumLoadingTabs()); + ASSERT_EQ(0, NumNeedReloadTabs()); + EXPECT_EQ(num_loading_tabs, NumBrokenTabs()); + + int initial_num_tabs = browser->tab_count(); + int login_tab = browser->active_index(); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser->GetActiveTabContents())); + ASSERT_TRUE(IsLoginTab(browser->GetActiveTabContents())); + + CaptivePortalObserver portal_observer(browser->profile()); + MultiNavigationObserver navigation_observer; + // Connection finally times out. + URLRequestTimeoutOnDemandJob::FailRequests(); + + navigation_observer.WaitForNavigations(num_loading_tabs); + + // No captive portal checks should have ocurred or be pending, and there + // should be no new tabs. + EXPECT_EQ(0, portal_observer.num_results_received()); + EXPECT_FALSE(CheckPending(browser)); + EXPECT_EQ(initial_num_tabs, browser->tab_count()); + + EXPECT_EQ(0, NumNeedReloadTabs()); + EXPECT_EQ(0, NumLoadingTabs()); + EXPECT_EQ(num_loading_tabs, NumBrokenTabs()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser->GetActiveTabContents())); + EXPECT_TRUE(IsLoginTab(browser->GetActiveTabContents())); + EXPECT_EQ(login_tab, browser->active_index()); + + EXPECT_EQ(0, navigation_observer.NumNavigationsForTab( + browser->GetWebContentsAt(login_tab))); +} + +void CaptivePortalBrowserTest::SetSlowSSLLoadTime( + CaptivePortalTabReloader* tab_reloader, + base::TimeDelta slow_ssl_load_time) { + tab_reloader->set_slow_ssl_load_time(slow_ssl_load_time); +} + +CaptivePortalTabReloader* CaptivePortalBrowserTest::GetTabReloader( + TabContents* tab_contents) const { + return tab_contents->captive_portal_tab_helper()->GetTabReloaderForTest(); +} + +// Make sure there's no test for a captive portal on HTTP timeouts. This will +// also trigger the link doctor page, which results in the load of a second +// error page. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, HttpTimeout) { + GURL url = URLRequestFailedJob::GetMockHttpUrl(net::ERR_CONNECTION_TIMED_OUT); + NavigateToPageExpectNoTest(browser(), url, 2); +} + +// Make sure there's no check for a captive portal on HTTPS errors other than +// timeouts, when they preempt the slow load timer. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, HttpsNonTimeoutError) { + GURL url = URLRequestFailedJob::GetMockHttpsUrl(net::ERR_UNEXPECTED); + NavigateToPageExpectNoTest(browser(), url, 1); +} + +// Make sure no captive portal test triggers on HTTPS timeouts of iframes. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, HttpsIframeTimeout) { + CaptivePortalObserver portal_observer(browser()->profile()); + + // Use an HTTPS server for the top level page. + net::TestServer https_server(net::TestServer::TYPE_HTTPS, + net::TestServer::kLocalhost, + FilePath(FILE_PATH_LITERAL("chrome/test/data"))); + ASSERT_TRUE(https_server.Start()); + + GURL url = https_server.GetURL(kTestServerIframeTimeoutPath); + NavigateToPageExpectNoTest(browser(), url, 1); +} + +// Check the captive portal result when the test request reports a network +// error. The check is triggered by a slow loading page, and the page +// errors out only after getting a captive portal result. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, RequestFails) { + SetUpCaptivePortalService( + browser()->profile(), + URLRequestFailedJob::GetMockHttpUrl(net::ERR_CONNECTION_CLOSED)); + SlowLoadNoCaptivePortal(browser(), RESULT_NO_RESPONSE); +} + +// Same as above, but for the rather unlikely case that the connection times out +// before the timer triggers. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, RequestFailsFastTimout) { + SetUpCaptivePortalService( + browser()->profile(), + URLRequestFailedJob::GetMockHttpUrl(net::ERR_CONNECTION_CLOSED)); + FastTimeoutNoCaptivePortal(browser(), RESULT_NO_RESPONSE); +} + +// Checks the case that captive portal detection is disabled. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, Disabled) { + EnableCaptivePortalDetection(browser()->profile(), false); + SlowLoadNoCaptivePortal(browser(), RESULT_INTERNET_CONNECTED); +} + +// Checks that we look for a captive portal on HTTPS timeouts and don't reload +// the error tab when the captive portal probe gets a 204 response, indicating +// there is no captive portal. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, InternetConnected) { + // Can't just use SetBehindCaptivePortal(false), since then there wouldn't + // be a timeout. + ASSERT_TRUE(test_server()->Start()); + SetUpCaptivePortalService(browser()->profile(), + test_server()->GetURL("nocontent")); + SlowLoadNoCaptivePortal(browser(), RESULT_INTERNET_CONNECTED); +} + +// Checks that no login page is opened when the HTTP test URL redirects to an +// SSL certificate error. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, RedirectSSLCertError) { + // Need an HTTP TestServer to handle a dynamically created server redirect. + ASSERT_TRUE(test_server()->Start()); + + net::TestServer::HTTPSOptions https_options; + https_options.server_certificate = + net::TestServer::HTTPSOptions::CERT_MISMATCHED_NAME; + net::TestServer https_server(https_options, + FilePath(FILE_PATH_LITERAL("chrome/test/data"))); + ASSERT_TRUE(https_server.Start()); + + GURL ssl_login_url = https_server.GetURL(kTestServerLoginPath); + + CaptivePortalService* captive_portal_service = + CaptivePortalServiceFactory::GetForProfile(browser()->profile()); + ASSERT_TRUE(captive_portal_service); + SetUpCaptivePortalService( + browser()->profile(), + test_server()->GetURL(CreateServerRedirect(ssl_login_url.spec()))); + + SlowLoadNoCaptivePortal(browser(), RESULT_NO_RESPONSE); +} + +// A slow SSL load triggers a captive portal check. The user logs on before +// the SSL page times out. We wait for the timeout and subsequent reload. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, Login) { + // Load starts, detect captive portal and open up a login tab. + SlowLoadBehindCaptivePortal(browser(), true); + + // Log in. One loading tab, no timed out ones. + Login(browser(), 1, 0); + + string16 expected_title = ASCIIToUTF16(kInternetConnectedTitle); + ui_test_utils::TitleWatcher title_watcher( + browser()->GetWebContentsAt(0), + expected_title); + + // Timeout occurs, and page is automatically reloaded. + FailLoadsAfterLogin(browser(), 1); + + // Double check the tab's title. + EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle()); +} + +// Same as above, except we make sure everything works with an incognito +// profile. Main issues it tests for are that the incognito has its own +// non-NULL captive portal service, and we open the tab in the correct +// window. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, LoginIncognito) { + // This will watch tabs for both profiles, but only used to make sure no + // navigations occur for the non-incognito profile. + MultiNavigationObserver navigation_observer; + CaptivePortalObserver non_incognito_portal_observer(browser()->profile()); + + Browser* incognito_browser = CreateIncognitoBrowser(); + EnableCaptivePortalDetection(incognito_browser->profile(), true); + SetUpCaptivePortalService(incognito_browser->profile(), + GURL(kMockCaptivePortalTestUrl)); + + SlowLoadBehindCaptivePortal(incognito_browser, true); + + EXPECT_EQ(1, browser()->tab_count()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(browser(), 0)); + + Login(incognito_browser, 1, 0); + FailLoadsAfterLogin(incognito_browser, 1); + + EXPECT_EQ(1, browser()->tab_count()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(browser(), 0)); + + EXPECT_EQ(0, navigation_observer.NumNavigationsForTab( + browser()->GetWebContentsAt(0))); + EXPECT_EQ(0, non_incognito_portal_observer.num_results_received()); +} + +// The captive portal page is opened before the SSL page times out, +// but the user logs in only after the page times out. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, LoginSlow) { + SlowLoadBehindCaptivePortal(browser(), true); + FailLoadsWithoutLogin(browser(), 1); + Login(browser(), 0, 1); +} + +// Checks the unlikely case that the tab times out before the timer triggers. +// This most likely won't happen, but should still work: +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, LoginFastTimeout) { + FastTimeoutBehindCaptivePortal(browser(), true); + Login(browser(), 0, 1); +} + +// Tries navigating both the tab that encounters an SSL timeout and the +// login tab twice, only logging in the second time. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, LoginExtraNavigations) { + FastTimeoutBehindCaptivePortal(browser(), true); + + // Activate the timed out tab and navigate it to a timeout again. + browser()->ActivateTabAt(0, true); + FastTimeoutBehindCaptivePortal(browser(), false); + + // Activate and navigate the captive portal tab. This should not trigger a + // reload of the tab with the error. + browser()->ActivateTabAt(1, true); + NavigateLoginTab(browser(), 0, 1); + + // Simulate logging in. + Login(browser(), 0, 1); +} + +// After the first SSL timeout, closes the login tab and makes sure it's opened +// it again on a second timeout. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, CloseLoginTab) { + // First load starts, opens a login tab, and then times out. + SlowLoadBehindCaptivePortal(browser(), true); + FailLoadsWithoutLogin(browser(), 1); + + // Close login tab. + browser()->CloseTab(); + + // Go through the standard slow load login, and make sure it still works. + SlowLoadBehindCaptivePortal(browser(), true); + Login(browser(), 1, 0); + FailLoadsAfterLogin(browser(), 1); +} + +// Checks that two tabs with SSL timeouts in the same window work. Both +// tabs only timeout after logging in. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, TwoBrokenTabs) { + SlowLoadBehindCaptivePortal(browser(), true); + + // Can't set the TabReloader HTTPS timeout on a new tab without doing some + // acrobatics, so open a new tab at a normal page, and then navigate it to a + // timeout. + MultiNavigationObserver navigation_observer; + CaptivePortalObserver portal_observer(browser()->profile()); + ui_test_utils::NavigateToURLWithDisposition( + browser(), + URLRequestMockHTTPJob::GetMockUrl( + FilePath(FILE_PATH_LITERAL("title2.html"))), + NEW_FOREGROUND_TAB, + ui_test_utils::BROWSER_TEST_WAIT_FOR_NAVIGATION); + + ASSERT_EQ(3, browser()->tab_count()); + EXPECT_FALSE(CheckPending(browser())); + EXPECT_EQ(0, portal_observer.num_results_received()); + EXPECT_EQ(1, NumLoadingTabs()); + EXPECT_EQ(1, navigation_observer.num_navigations()); + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser()->GetWebContentsAt(2))); + ASSERT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + GetStateOfTabReloaderAt(browser(), 0)); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser()->GetTabContentsAt(1))); + ASSERT_TRUE(IsLoginTab(browser()->GetTabContentsAt(1))); + ASSERT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(browser(), 2)); + ASSERT_EQ(2, browser()->active_index()); + + SlowLoadBehindCaptivePortal(browser(), false); + + browser()->ActivateTabAt(1, true); + Login(browser(), 2, 0); + FailLoadsAfterLogin(browser(), 2); +} + +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, AbortLoad) { + // Go to the error page. + SlowLoadBehindCaptivePortal(browser(), true); + // The load will be destroyed without returning any result, so remove it from + // the list of jobs that will timeout. + URLRequestTimeoutOnDemandJob::AbandonRequests(); + + CaptivePortalObserver portal_observer(browser()->profile()); + MultiNavigationObserver navigation_observer; + + // Switch back to the hung tab from the login tab, and abort the navigation. + browser()->ActivateTabAt(0, true); + browser()->Stop(); + navigation_observer.WaitForNavigations(1); + + EXPECT_EQ(0, NumBrokenTabs()); + EXPECT_EQ(0, portal_observer.num_results_received()); + EXPECT_FALSE(CheckPending(browser())); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(browser(), 0)); + + browser()->ActivateTabAt(1, true); + Login(browser(), 0, 0); +} + +// Checks the case where the timed out tab is successfully navigated before +// logging in. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, NavigateBrokenTab) { + // Go to the error page. + SlowLoadBehindCaptivePortal(browser(), true); + FailLoadsWithoutLogin(browser(), 1); + + // Navigate the error tab to a non-error page. + browser()->ActivateTabAt(0, true); + ui_test_utils::NavigateToURL(browser(), + URLRequestMockHTTPJob::GetMockUrl( + FilePath(FILE_PATH_LITERAL("title2.html")))); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(browser(), 0)); + + // Simulate logging in. + browser()->ActivateTabAt(1, true); + Login(browser(), 0, 0); +} + +// Navigates a broken, but still loading, tab to another timeout before logging +// in. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, + NavigateBrokenToTimeoutTabWhileLoading) { + // Go to the error page. + SlowLoadBehindCaptivePortal(browser(), true); + URLRequestTimeoutOnDemandJob::AbandonRequests(); + + CaptivePortalTabReloader* tab_reloader = + GetTabReloader(browser()->GetTabContentsAt(0)); + ASSERT_TRUE(tab_reloader); + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta()); + + CaptivePortalObserver portal_observer(browser()->profile()); + MultiNavigationObserver navigation_observer; + + // Navigate the error tab to a non-error page. Can't have ui_test_utils + // do the navigation because it will wait for loading tabs to stop loading + // before navigating. + browser()->ActivateTabAt(0, true); + browser()->OpenURL(content::OpenURLParams(GURL(kMockHttpsUrl), + content::Referrer(), + CURRENT_TAB, + content::PAGE_TRANSITION_TYPED, + false)); + navigation_observer.WaitForNavigations(1); + portal_observer.WaitForResults(1); + EXPECT_FALSE(CheckPending(browser())); + EXPECT_EQ(1, NumLoadingTabs()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + GetStateOfTabReloaderAt(browser(), 0)); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser()->GetTabContentsAt(1))); + ASSERT_TRUE(IsLoginTab(browser()->GetTabContentsAt(1))); + + // Simulate logging in. + browser()->ActivateTabAt(1, true); + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta::FromDays(1)); + Login(browser(), 1, 0); + + // Timeout occurs, and page is automatically reloaded. + FailLoadsAfterLogin(browser(), 1); +} + +// Checks that navigating a timed out tab back clears its state. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, GoBack) { + // Navigate to a working page. + ui_test_utils::NavigateToURL( + browser(), + URLRequestMockHTTPJob::GetMockUrl( + FilePath(FILE_PATH_LITERAL("title2.html")))); + + // Go to the error page. + SlowLoadBehindCaptivePortal(browser(), true); + FailLoadsWithoutLogin(browser(), 1); + + CaptivePortalObserver portal_observer(browser()->profile()); + MultiNavigationObserver navigation_observer; + + // Activate the error page tab again and go back. + browser()->ActivateTabAt(0, true); + browser()->GoBack(CURRENT_TAB); + navigation_observer.WaitForNavigations(1); + + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser()->GetWebContentsAt(0))); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(browser(), 0)); + EXPECT_EQ(0, portal_observer.num_results_received()); +} + +// Checks that navigating back to a timeout triggers captive portal detection. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, GoBackToTimeout) { + // Disable captive portal detection so the first navigation doesn't open a + // login tab. + EnableCaptivePortalDetection(browser()->profile(), false); + + SlowLoadNoCaptivePortal(browser(), RESULT_INTERNET_CONNECTED); + + // Navigate to a working page. + ui_test_utils::NavigateToURL(browser(), + URLRequestMockHTTPJob::GetMockUrl( + FilePath(FILE_PATH_LITERAL("title2.html")))); + ASSERT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(browser(), 0)); + + EnableCaptivePortalDetection(browser()->profile(), true); + + CaptivePortalTabReloader* tab_reloader = + GetTabReloader(browser()->GetActiveTabContents()); + ASSERT_TRUE(tab_reloader); + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta()); + + // Go to the error page. + MultiNavigationObserver navigation_observer; + CaptivePortalObserver portal_observer(browser()->profile()); + browser()->GoBack(CURRENT_TAB); + + // Wait for both the check triggered by the broken tab and the first load + // of the login tab, and for the login tab to stop loading. + portal_observer.WaitForResults(2); + navigation_observer.WaitForNavigations(1); + + EXPECT_EQ(2, portal_observer.num_results_received()); + ASSERT_FALSE(CheckPending(browser())); + ASSERT_EQ(RESULT_BEHIND_CAPTIVE_PORTAL, + portal_observer.captive_portal_result()); + + ASSERT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + GetStateOfTabReloader(browser()->GetTabContentsAt(0))); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser()->GetTabContentsAt(1))); + ASSERT_TRUE(IsLoginTab(browser()->GetTabContentsAt(1))); + + ASSERT_EQ(2, browser()->tab_count()); + EXPECT_EQ(1, browser()->active_index()); + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser()->GetWebContentsAt(1))); + EXPECT_EQ(1, NumLoadingTabs()); + + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta::FromDays(1)); + Login(browser(), 1, 0); + FailLoadsAfterLogin(browser(), 1); +} + +// Checks that reloading a timeout triggers captive portal detection. +// Much like the last test, though the captive portal is disabled before +// the inital navigation, rather than captive portal detection. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, ReloadTimeout) { + URLRequestMockCaptivePortalJobFactory::SetBehindCaptivePortal(false); + + // Do the first navigation while not behind a captive portal. + CaptivePortalObserver portal_observer(browser()->profile()); + ui_test_utils::NavigateToURL(browser(), GURL(kMockHttpsUrl)); + ASSERT_EQ(0, portal_observer.num_results_received()); + ASSERT_EQ(1, browser()->tab_count()); + + // A captive portal spontaneously appears. + URLRequestMockCaptivePortalJobFactory::SetBehindCaptivePortal(true); + + CaptivePortalTabReloader* tab_reloader = + GetTabReloader(browser()->GetActiveTabContents()); + ASSERT_TRUE(tab_reloader); + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta()); + + MultiNavigationObserver navigation_observer; + browser()->GetActiveWebContents()->GetController().Reload(true); + + // Wait for the login tab to open, and the two captive portal results from + // opening an it. + portal_observer.WaitForResults(2); + navigation_observer.WaitForNavigations(1); + + ASSERT_EQ(2, portal_observer.num_results_received()); + ASSERT_FALSE(CheckPending(browser())); + ASSERT_EQ(RESULT_BEHIND_CAPTIVE_PORTAL, + portal_observer.captive_portal_result()); + + ASSERT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + GetStateOfTabReloader(browser()->GetTabContentsAt(0))); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloader(browser()->GetTabContentsAt(1))); + ASSERT_TRUE(IsLoginTab(browser()->GetTabContentsAt(1))); + + ASSERT_EQ(2, browser()->tab_count()); + EXPECT_EQ(1, browser()->active_index()); + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + browser()->GetWebContentsAt(1))); + EXPECT_EQ(1, NumLoadingTabs()); + + SetSlowSSLLoadTime(tab_reloader, base::TimeDelta::FromDays(1)); + Login(browser(), 1, 0); + FailLoadsAfterLogin(browser(), 1); +} + +// Checks the case where there are two windows, and there's an SSL timeout in +// the background one. +IN_PROC_BROWSER_TEST_F(CaptivePortalBrowserTest, TwoWindows) { + Browser* browser2 = Browser::Create(browser()->profile()); + // Navigate the new browser window so it'll be shown and we can pick the + // active window. + ui_test_utils::NavigateToURL(browser2, GURL(chrome::kAboutBlankURL)); + + // Generally, |browser2| will be the active window. However, if the + // original browser window lost focus before creating the new one, such as + // when running multiple tests at once, the original browser window may + // remain the profile's active window. + Browser* active_browser = + browser::FindTabbedBrowser(browser()->profile(), true); + Browser* inactive_browser; + if (active_browser == browser2) { + // When only one test is running at a time, the new browser will probably be + // on top, but when multiple tests are running at once, this is not + // guaranteed. + inactive_browser = browser(); + } else { + ASSERT_EQ(active_browser, browser()); + inactive_browser = browser2; + } + + CaptivePortalObserver portal_observer(browser()->profile()); + MultiNavigationObserver navigation_observer; + + // Navigate the tab in the inactive browser to an SSL timeout. Have to use + // browser::NavigateParams and NEW_BACKGROUND_TAB to avoid activating the + // window. + browser::NavigateParams params(inactive_browser, + GURL(kMockHttpsQuickTimeoutUrl), + content::PAGE_TRANSITION_TYPED); + params.disposition = NEW_BACKGROUND_TAB; + params.window_action = browser::NavigateParams::NO_ACTION; + ui_test_utils::NavigateToURL(¶ms); + navigation_observer.WaitForNavigations(2); + + // Make sure the active window hasn't changed, and its new tab is + // active. + ASSERT_EQ(active_browser, + browser::FindTabbedBrowser(browser()->profile(), true)); + ASSERT_EQ(1, active_browser->active_index()); + + // Check that the only two navigated tabs were the new error tab in the + // backround windows, and the login tab in the active window. + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + inactive_browser->GetWebContentsAt(1))); + EXPECT_EQ(1, navigation_observer.NumNavigationsForTab( + active_browser->GetWebContentsAt(1))); + EXPECT_EQ(0, NumLoadingTabs()); + + // Check captive portal test results. + portal_observer.WaitForResults(2); + ASSERT_EQ(RESULT_BEHIND_CAPTIVE_PORTAL, + portal_observer.captive_portal_result()); + EXPECT_EQ(2, portal_observer.num_results_received()); + + // Check the inactive browser. + EXPECT_EQ(2, inactive_browser->tab_count()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(inactive_browser, 0)); + EXPECT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + GetStateOfTabReloaderAt(inactive_browser, 1)); + + // Check the active browser. + ASSERT_EQ(2, active_browser->tab_count()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(active_browser, 0)); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, + GetStateOfTabReloaderAt(active_browser, 1)); + EXPECT_TRUE(IsLoginTab(active_browser->GetTabContentsAt(1))); + + // Simulate logging in. + Login(active_browser, 0, 1); +} + +} // namespace captive_portal diff --git a/chrome/browser/captive_portal/captive_portal_login_detector.cc b/chrome/browser/captive_portal/captive_portal_login_detector.cc new file mode 100644 index 0000000..ce079c9 --- /dev/null +++ b/chrome/browser/captive_portal/captive_portal_login_detector.cc @@ -0,0 +1,35 @@ +// 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. + +#include "chrome/browser/captive_portal/captive_portal_login_detector.h" + +#include "chrome/browser/captive_portal/captive_portal_service_factory.h" + +namespace captive_portal { + +CaptivePortalLoginDetector::CaptivePortalLoginDetector( + Profile* profile) + : profile_(profile), + is_login_tab_(false) { +} + +CaptivePortalLoginDetector::~CaptivePortalLoginDetector() { +} + +void CaptivePortalLoginDetector::OnStoppedLoading() { + if (!is_login_tab_) + return; + // The service is guaranteed to exist if |is_login_tab_| is true, since it's + // only set to true once a captive portal is detected. + CaptivePortalServiceFactory::GetForProfile(profile_)->DetectCaptivePortal(); +} + +void CaptivePortalLoginDetector::OnCaptivePortalResults( + Result previous_result, + Result result) { + if (result != RESULT_BEHIND_CAPTIVE_PORTAL) + is_login_tab_ = false; +} + +} // namespace captive_portal diff --git a/chrome/browser/captive_portal/captive_portal_login_detector.h b/chrome/browser/captive_portal/captive_portal_login_detector.h new file mode 100644 index 0000000..b8b0734 --- /dev/null +++ b/chrome/browser/captive_portal/captive_portal_login_detector.h @@ -0,0 +1,48 @@ +// 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. + +#ifndef CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_LOGIN_DETECTOR_H_ +#define CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_LOGIN_DETECTOR_H_ +#pragma once + +#include "base/basictypes.h" +#include "chrome/browser/captive_portal/captive_portal_service.h" + +class Profile; + +namespace captive_portal { + +// Triggers a captive portal test on navigations that may indicate a captive +// portal has been logged into. Currently only tracks if a page was opened +// at a captive portal tab's login page, and triggers checks every navigation +// until there's no longer a captive portal, relying on the +// CaptivePortalService's throttling to prevent excessive server load. +// +// TODO(mmenke): If a page has been broken by a captive portal, and it's +// successfully reloaded, trigger a captive portal check. +class CaptivePortalLoginDetector { + public: + explicit CaptivePortalLoginDetector(Profile* profile); + + ~CaptivePortalLoginDetector(); + + void OnStoppedLoading(); + void OnCaptivePortalResults(Result previous_result, Result result); + + bool is_login_tab() const { return is_login_tab_; } + void set_is_login_tab() { is_login_tab_ = true; } + + private: + Profile* profile_; + + // True if this is a login tab. Set manually, automatically cleared once + // login is detected. + bool is_login_tab_; + + DISALLOW_COPY_AND_ASSIGN(CaptivePortalLoginDetector); +}; + +} // namespace captive_portal + +#endif // CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_LOGIN_DETECTOR_H_ diff --git a/chrome/browser/captive_portal/captive_portal_service.h b/chrome/browser/captive_portal/captive_portal_service.h index 09eab15..bdcd50a 100644 --- a/chrome/browser/captive_portal/captive_portal_service.h +++ b/chrome/browser/captive_portal/captive_portal_service.h @@ -79,6 +79,7 @@ class CaptivePortalService : public ProfileKeyedService, private: friend class CaptivePortalServiceTest; + friend class CaptivePortalBrowserTest; // Subclass of BackoffEntry that uses the CaptivePortalService's // GetCurrentTime function, for unit testing. @@ -167,6 +168,8 @@ class CaptivePortalService : public ProfileKeyedService, RecheckPolicy& recheck_policy() { return recheck_policy_; } + void set_test_url(const GURL& test_url) { test_url_ = test_url; } + // The profile that owns this CaptivePortalService. Profile* profile_; diff --git a/chrome/browser/captive_portal/captive_portal_tab_helper.cc b/chrome/browser/captive_portal/captive_portal_tab_helper.cc new file mode 100644 index 0000000..485cb16 --- /dev/null +++ b/chrome/browser/captive_portal/captive_portal_tab_helper.cc @@ -0,0 +1,177 @@ +// 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. + +#include "chrome/browser/captive_portal/captive_portal_tab_helper.h" + +#include "base/bind.h" +#include "chrome/browser/captive_portal/captive_portal_login_detector.h" +#include "chrome/browser/captive_portal/captive_portal_tab_reloader.h" +#include "chrome/browser/captive_portal/captive_portal_service.h" +#include "chrome/browser/captive_portal/captive_portal_service_factory.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_finder.h" +#include "chrome/browser/ui/tab_contents/tab_contents.h" +#include "chrome/common/chrome_notification_types.h" +#include "content/public/browser/notification_details.h" +#include "content/public/browser/notification_source.h" +#include "net/base/net_errors.h" + +namespace captive_portal { + +CaptivePortalTabHelper::CaptivePortalTabHelper( + Profile* profile, + content::WebContents* web_contents) + : content::WebContentsObserver(web_contents), + tab_reloader_( + new CaptivePortalTabReloader( + profile, + web_contents, + base::Bind(&CaptivePortalTabHelper::OpenLoginTab, + base::Unretained(this)))), + login_detector_(new CaptivePortalLoginDetector(profile)), + profile_(profile), + pending_error_code_(net::OK) { + registrar_.Add(this, + chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT, + content::Source<Profile>(profile_)); +} + +CaptivePortalTabHelper::~CaptivePortalTabHelper() { +} + +void CaptivePortalTabHelper::DidStartProvisionalLoadForFrame( + int64 frame_id, + bool is_main_frame, + const GURL& validated_url, + bool is_error_page, + content::RenderViewHost* render_view_host) { + DCHECK(CalledOnValidThread()); + + // Ignore subframes. + if (!is_main_frame) + return; + + // If loading an error page for a previous failure, treat this as part of + // the previous load. The second check is needed because Link Doctor pages + // result in two error page provisional loads in a row. Currently, the + // second load is treated as a normal load, rather than reusing old error + // codes. + if (is_error_page && pending_error_code_ != net::OK) + return; + + // Makes the second load for Link Doctor pages act as a normal load. + // TODO(mmenke): Figure out if this affects any other cases. + pending_error_code_ = net::OK; + + tab_reloader_->OnLoadStart(validated_url.SchemeIsSecure()); +} + +void CaptivePortalTabHelper::DidCommitProvisionalLoadForFrame( + int64 frame_id, + bool is_main_frame, + const GURL& url, + content::PageTransition transition_type, + content::RenderViewHost* render_view_host) { + DCHECK(CalledOnValidThread()); + + // Ignore subframes. + if (!is_main_frame) + return; + + tab_reloader_->OnLoadCommitted(pending_error_code_); + pending_error_code_ = net::OK; +} + +void CaptivePortalTabHelper::DidFailProvisionalLoad( + int64 frame_id, + bool is_main_frame, + const GURL& validated_url, + int error_code, + const string16& error_description, + content::RenderViewHost* render_view_host) { + DCHECK(CalledOnValidThread()); + + // Ignore subframes. + if (!is_main_frame) + return; + + // Aborts generally aren't followed by loading an error page, so go ahead and + // reset the state now, to prevent any captive portal checks from triggering. + if (error_code == net::ERR_ABORTED) { + // May have been aborting the load of an error page. + pending_error_code_ = net::OK; + + tab_reloader_->OnAbort(); + return; + } + + pending_error_code_ = error_code; +} + +void CaptivePortalTabHelper::DidStopLoading() { + DCHECK(CalledOnValidThread()); + + login_detector_->OnStoppedLoading(); +} + +void CaptivePortalTabHelper::Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + DCHECK(CalledOnValidThread()); + DCHECK_EQ(chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT, type); + DCHECK_EQ(profile_, content::Source<Profile>(source).ptr()); + + CaptivePortalService::Results* results = + content::Details<CaptivePortalService::Results>(details).ptr(); + + tab_reloader_->OnCaptivePortalResults(results->previous_result, + results->result); + login_detector_->OnCaptivePortalResults(results->previous_result, + results->result); +} + +bool CaptivePortalTabHelper::IsLoginTab() const { + return login_detector_->is_login_tab(); +} + +void CaptivePortalTabHelper::SetIsLoginTab() { + login_detector_->set_is_login_tab(); +} + +void CaptivePortalTabHelper::SetTabReloaderForTest( + CaptivePortalTabReloader* tab_reloader) { + tab_reloader_.reset(tab_reloader); +} + +CaptivePortalTabReloader* CaptivePortalTabHelper::GetTabReloaderForTest() { + return tab_reloader_.get(); +} + +void CaptivePortalTabHelper::OpenLoginTab() { + Browser* browser = browser::FindTabbedBrowser(profile_, true); + // If the Profile doesn't have a tabbed browser window open, do nothing. + if (!browser) + return; + + // Check if the Profile's topmost browser window already has a login tab. + // If so, do nothing. + // TODO(mmenke): Consider focusing that tab, at least if this is the tab + // helper for the currently active tab for the profile. + for (int i = 0; i < browser->tab_count(); ++i) { + TabContents* tab_contents = browser->GetTabContentsAt(i); + if (tab_contents->captive_portal_tab_helper()->IsLoginTab()) + return; + } + + // Otherwise, open a login tab. Only end up here when a captive portal result + // was received, so it's safe to assume |profile_| has a CaptivePortalService. + TabContents* tab_contents = + browser->AddSelectedTabWithURL( + CaptivePortalServiceFactory::GetForProfile(profile_)->test_url(), + content::PAGE_TRANSITION_TYPED); + tab_contents->captive_portal_tab_helper()->SetIsLoginTab(); +} + +} // namespace captive_portal diff --git a/chrome/browser/captive_portal/captive_portal_tab_helper.h b/chrome/browser/captive_portal/captive_portal_tab_helper.h new file mode 100644 index 0000000..9638628 --- /dev/null +++ b/chrome/browser/captive_portal/captive_portal_tab_helper.h @@ -0,0 +1,120 @@ +// 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. + +#ifndef CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_TAB_HELPER_H_ +#define CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_TAB_HELPER_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/non_thread_safe.h" +#include "content/public/browser/notification_observer.h" +#include "content/public/browser/notification_registrar.h" +#include "content/public/browser/web_contents_observer.h" + +class Profile; + +namespace captive_portal { + +class CaptivePortalLoginDetector; +class CaptivePortalTabReloader; + +// Along with the classes it owns, responsible for detecting page loads broken +// by a captive portal, triggering captive portal checks on navigation events +// that may indicate a captive portal is present, or has been removed / logged +// in to, and taking any correcting actions. +// +// It acts as a WebContentsObserver for its CaptivePortalLoginDetector and +// CaptivePortalTabReloader. It filters out non-main-frame resource loads, and +// treats the commit of an error page as a single event, rather than as 3 +// (ProvisionalLoadFail, DidStartProvisionalLoad, DidCommit), which simplifies +// the CaptivePortalTabReloader. It is also needed by CaptivePortalTabReloaders +// to inform the tab's CaptivePortalLoginDetector when the tab is at a captive +// portal's login page. +// +// TODO(mmenke): Support redirects. Needed for HSTS, which simulates redirects +// at the network layer. Also may reduce the number of +// unnecessary captive portal checks on high latency connections. +// +// For the design doc, see: +// https://docs.google.com/document/d/1k-gP2sswzYNvryu9NcgN7q5XrsMlUdlUdoW9WRaEmfM/edit +class CaptivePortalTabHelper : public content::WebContentsObserver, + public content::NotificationObserver, + public base::NonThreadSafe { + public: + CaptivePortalTabHelper(Profile* profile, + content::WebContents* web_contents); + virtual ~CaptivePortalTabHelper(); + + // content::WebContentsObserver: + virtual void DidStartProvisionalLoadForFrame( + int64 frame_id, + bool is_main_frame, + const GURL& validated_url, + bool is_error_page, + content::RenderViewHost* render_view_host) OVERRIDE; + + virtual void DidCommitProvisionalLoadForFrame( + int64 frame_id, + bool is_main_frame, + const GURL& url, + content::PageTransition transition_type, + content::RenderViewHost* render_view_host) OVERRIDE; + + virtual void DidFailProvisionalLoad( + int64 frame_id, + bool is_main_frame, + const GURL& validated_url, + int error_code, + const string16& error_description, + content::RenderViewHost* render_view_host) OVERRIDE; + + virtual void DidStopLoading() OVERRIDE; + + // content::NotificationObserver: + virtual void Observe( + int type, + const content::NotificationSource& source, + const content::NotificationDetails& details) OVERRIDE; + + // A "Login Tab" is a tab that was originally at a captive portal login + // page. This is set to false when a captive portal is no longer detected. + bool IsLoginTab() const; + + private: + friend class CaptivePortalBrowserTest; + friend class CaptivePortalTabHelperTest; + + // Called to indicate a tab is at, or is navigating to, the captive portal + // login page. + void SetIsLoginTab(); + + // |this| takes ownership of |tab_reloader|. + void SetTabReloaderForTest(CaptivePortalTabReloader* tab_reloader); + + CaptivePortalTabReloader* GetTabReloaderForTest(); + + // Opens a login tab if the profile's active window doesn't have one already. + void OpenLoginTab(); + + // Neither of these will ever be NULL. + scoped_ptr<CaptivePortalTabReloader> tab_reloader_; + scoped_ptr<CaptivePortalLoginDetector> login_detector_; + + Profile* profile_; + + // If a provisional load has failed, and the tab is loading an error page, the + // error code associated with the error page we're loading. + // net::OK, otherwise. + int pending_error_code_; + + content::NotificationRegistrar registrar_; + + DISALLOW_COPY_AND_ASSIGN(CaptivePortalTabHelper); +}; + +} // namespace captive_portal + +#endif // CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_TAB_HELPER_H_ diff --git a/chrome/browser/captive_portal/captive_portal_tab_helper_unittest.cc b/chrome/browser/captive_portal/captive_portal_tab_helper_unittest.cc new file mode 100644 index 0000000..d23035b --- /dev/null +++ b/chrome/browser/captive_portal/captive_portal_tab_helper_unittest.cc @@ -0,0 +1,301 @@ +// 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. + +#include "chrome/browser/captive_portal/captive_portal_tab_helper.h" + +#include "base/callback.h" +#include "chrome/browser/captive_portal/captive_portal_service.h" +#include "chrome/browser/captive_portal/captive_portal_tab_reloader.h" +#include "chrome/common/chrome_notification_types.h" +#include "content/public/browser/notification_details.h" +#include "content/public/browser/notification_service.h" +#include "content/public/browser/notification_source.h" +#include "net/base/net_errors.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace captive_portal { + +namespace { + +const char* const kHttpUrl = "http://whatever.com/"; +const char* const kHttpsUrl = "https://whatever.com/"; +// Error pages use a "data:" URL. Shouldn't actually matter what this is. +const char* const kErrorPageUrl = "data:blah"; + +} // namespace + +class MockCaptivePortalTabReloader : public CaptivePortalTabReloader { + public: + MockCaptivePortalTabReloader() + : CaptivePortalTabReloader(NULL, NULL, base::Callback<void()>()) { + } + + MOCK_METHOD1(OnLoadStart, void(bool)); + MOCK_METHOD1(OnLoadCommitted, void(int)); + MOCK_METHOD0(OnAbort, void()); + MOCK_METHOD2(OnCaptivePortalResults, void(Result, Result)); +}; + +class CaptivePortalTabHelperTest : public testing::Test { + public: + CaptivePortalTabHelperTest() + : tab_helper_(NULL, NULL), + mock_reloader_(new testing::StrictMock<MockCaptivePortalTabReloader>) { + tab_helper_.SetTabReloaderForTest(mock_reloader_); + } + virtual ~CaptivePortalTabHelperTest() {} + + // Simulates a successful load of |url|. + void SimulateSuccess(const GURL& url) { + EXPECT_CALL(mock_reloader(), OnLoadStart(url.SchemeIsSecure())).Times(1); + tab_helper().DidStartProvisionalLoadForFrame(1, true, url, false, NULL); + + EXPECT_CALL(mock_reloader(), OnLoadCommitted(net::OK)).Times(1); + tab_helper().DidCommitProvisionalLoadForFrame( + 1, true, url, content::PAGE_TRANSITION_LINK, NULL); + } + + // Simulates a connection timeout while requesting |url|. + void SimulateTimeout(const GURL& url) { + EXPECT_CALL(mock_reloader(), OnLoadStart(url.SchemeIsSecure())).Times(1); + tab_helper().DidStartProvisionalLoadForFrame(1, true, url, false, NULL); + + tab_helper().DidFailProvisionalLoad( + 1, true, url, net::ERR_TIMED_OUT, string16(), NULL); + + // Provisional load starts for the error page. + tab_helper().DidStartProvisionalLoadForFrame( + 1, true, GURL(kErrorPageUrl), true, NULL); + + EXPECT_CALL(mock_reloader(), OnLoadCommitted(net::ERR_TIMED_OUT)).Times(1); + tab_helper().DidCommitProvisionalLoadForFrame( + 1, true, GURL(kErrorPageUrl), content::PAGE_TRANSITION_LINK, NULL); + } + + // Simulates an abort while requesting |url|. + void SimulateAbort(const GURL& url) { + EXPECT_CALL(mock_reloader(), OnLoadStart(url.SchemeIsSecure())).Times(1); + tab_helper().DidStartProvisionalLoadForFrame(1, true, url, false, NULL); + + EXPECT_CALL(mock_reloader(), OnAbort()).Times(1); + tab_helper().DidFailProvisionalLoad( + 1, true, url, net::ERR_ABORTED, string16(), NULL); + } + + // Simulates an abort while loading an error page. + void SimulateAbortTimeout(const GURL& url) { + EXPECT_CALL(mock_reloader(), OnLoadStart(url.SchemeIsSecure())).Times(1); + tab_helper().DidStartProvisionalLoadForFrame(1, true, url, false, NULL); + + tab_helper().DidFailProvisionalLoad( + 1, true, url, net::ERR_TIMED_OUT, string16(), NULL); + + // Start event for the error page. + tab_helper().DidStartProvisionalLoadForFrame(1, true, url, true, NULL); + + EXPECT_CALL(mock_reloader(), OnAbort()).Times(1); + tab_helper().DidFailProvisionalLoad( + 1, true, url, net::ERR_ABORTED, string16(), NULL); + } + + CaptivePortalTabHelper& tab_helper() { + return tab_helper_; + } + + void ObservePortalResult(Result previous_result, Result result) { + content::Source<Profile> source_profile(NULL); + + CaptivePortalService::Results results; + results.previous_result = previous_result; + results.result = result; + content::Details<CaptivePortalService::Results> details_results(&results); + + EXPECT_CALL(mock_reloader(), OnCaptivePortalResults(previous_result, + result)).Times(1); + tab_helper().Observe(chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT, + source_profile, + details_results); + } + + MockCaptivePortalTabReloader& mock_reloader() { return *mock_reloader_; } + + void SetIsLoginTab() { + tab_helper().SetIsLoginTab(); + } + + private: + CaptivePortalTabHelper tab_helper_; + + // Owned by |tab_helper_|. + testing::StrictMock<MockCaptivePortalTabReloader>* mock_reloader_; + + DISALLOW_COPY_AND_ASSIGN(CaptivePortalTabHelperTest); +}; + +TEST_F(CaptivePortalTabHelperTest, HttpSuccess) { + SimulateSuccess(GURL(kHttpUrl)); + tab_helper().DidStopLoading(); +} + +TEST_F(CaptivePortalTabHelperTest, HttpTimeout) { + SimulateTimeout(GURL(kHttpUrl)); + tab_helper().DidStopLoading(); +} + +// Same as above, but simulates what happens when the Link Doctor is enabled, +// which adds another provisional load/commit for the error page, after the +// first two. +TEST_F(CaptivePortalTabHelperTest, HttpTimeoutLinkDoctor) { + SimulateTimeout(GURL(kHttpUrl)); + + EXPECT_CALL(mock_reloader(), OnLoadStart(false)).Times(1); + // Provisional load starts for the error page. + tab_helper().DidStartProvisionalLoadForFrame( + 1, true, GURL(kErrorPageUrl), true, NULL); + + EXPECT_CALL(mock_reloader(), OnLoadCommitted(net::OK)).Times(1); + tab_helper().DidCommitProvisionalLoadForFrame( + 1, true, GURL(kErrorPageUrl), content::PAGE_TRANSITION_LINK, NULL); + tab_helper().DidStopLoading(); +} + +TEST_F(CaptivePortalTabHelperTest, HttpsSuccess) { + SimulateSuccess(GURL(kHttpsUrl)); + tab_helper().DidStopLoading(); + EXPECT_FALSE(tab_helper().IsLoginTab()); +} + +TEST_F(CaptivePortalTabHelperTest, HttpsTimeout) { + SimulateTimeout(GURL(kHttpsUrl)); + // Make sure no state was carried over from the timeout. + SimulateSuccess(GURL(kHttpsUrl)); + EXPECT_FALSE(tab_helper().IsLoginTab()); +} + +TEST_F(CaptivePortalTabHelperTest, HttpsAbort) { + SimulateAbort(GURL(kHttpsUrl)); + // Make sure no state was carried over from the abort. + SimulateSuccess(GURL(kHttpsUrl)); + EXPECT_FALSE(tab_helper().IsLoginTab()); +} + +// Abort while there's a provisional timeout error page loading. +TEST_F(CaptivePortalTabHelperTest, HttpsAbortTimeout) { + SimulateAbortTimeout(GURL(kHttpsUrl)); + // Make sure no state was carried over from the timeout or the abort. + SimulateSuccess(GURL(kHttpsUrl)); + EXPECT_FALSE(tab_helper().IsLoginTab()); +} + +// Simulates navigations for a number of subframes, and makes sure no +// CaptivePortalTabHelper function is called. +TEST_F(CaptivePortalTabHelperTest, HttpsSubframe) { + GURL url = GURL(kHttpsUrl); + // Normal load. + tab_helper().DidStartProvisionalLoadForFrame(1, false, url, false, NULL); + tab_helper().DidCommitProvisionalLoadForFrame( + 1, false, url, content::PAGE_TRANSITION_LINK, NULL); + + // Timeout. + tab_helper().DidStartProvisionalLoadForFrame(2, false, url, false, NULL); + tab_helper().DidFailProvisionalLoad( + 2, false, url, net::ERR_TIMED_OUT, string16(), NULL); + tab_helper().DidStartProvisionalLoadForFrame(2, false, url, true, NULL); + tab_helper().DidFailProvisionalLoad( + 2, false, url, net::ERR_ABORTED, string16(), NULL); + + // Abort. + tab_helper().DidStartProvisionalLoadForFrame(3, false, url, false, NULL); + tab_helper().DidFailProvisionalLoad( + 3, false, url, net::ERR_ABORTED, string16(), NULL); +} + +// Simulates a subframe erroring out at the same time as a provisional load, +// but with a different error code. Make sure the TabHelper sees the correct +// error. +TEST_F(CaptivePortalTabHelperTest, HttpsSubframeParallelError) { + // URL used by both frames. + GURL url = GURL(kHttpsUrl); + + int frame_id = 2; + int subframe_id = 1; + + // Loads start. + EXPECT_CALL(mock_reloader(), OnLoadStart(url.SchemeIsSecure())).Times(1); + tab_helper().DidStartProvisionalLoadForFrame( + frame_id, true, url, false, NULL); + tab_helper().DidStartProvisionalLoadForFrame( + subframe_id, false, url, false, NULL); + + // Loads return errors. + tab_helper().DidFailProvisionalLoad( + frame_id, true, url, net::ERR_UNEXPECTED, string16(), NULL); + tab_helper().DidFailProvisionalLoad( + subframe_id, false, url, net::ERR_TIMED_OUT, string16(), NULL); + + // Provisional load starts for the error pages. + tab_helper().DidStartProvisionalLoadForFrame( + frame_id, true, url, true, NULL); + tab_helper().DidStartProvisionalLoadForFrame( + subframe_id, false, url, true, NULL); + + // Error page load finishes. + tab_helper().DidCommitProvisionalLoadForFrame( + subframe_id, false, url, content::PAGE_TRANSITION_AUTO_SUBFRAME, NULL); + EXPECT_CALL(mock_reloader(), OnLoadCommitted(net::ERR_UNEXPECTED)).Times(1); + tab_helper().DidCommitProvisionalLoadForFrame( + frame_id, true, url, content::PAGE_TRANSITION_LINK, NULL); +} + +TEST_F(CaptivePortalTabHelperTest, LoginTabLogin) { + EXPECT_FALSE(tab_helper().IsLoginTab()); + SetIsLoginTab(); + EXPECT_TRUE(tab_helper().IsLoginTab()); + + ObservePortalResult(RESULT_INTERNET_CONNECTED, RESULT_INTERNET_CONNECTED); + EXPECT_FALSE(tab_helper().IsLoginTab()); +} + +TEST_F(CaptivePortalTabHelperTest, LoginTabError) { + EXPECT_FALSE(tab_helper().IsLoginTab()); + + SetIsLoginTab(); + EXPECT_TRUE(tab_helper().IsLoginTab()); + + ObservePortalResult(RESULT_INTERNET_CONNECTED, RESULT_NO_RESPONSE); + EXPECT_FALSE(tab_helper().IsLoginTab()); +} + +TEST_F(CaptivePortalTabHelperTest, LoginTabMultipleResultsBeforeLogin) { + EXPECT_FALSE(tab_helper().IsLoginTab()); + + SetIsLoginTab(); + EXPECT_TRUE(tab_helper().IsLoginTab()); + + ObservePortalResult(RESULT_INTERNET_CONNECTED, RESULT_BEHIND_CAPTIVE_PORTAL); + EXPECT_TRUE(tab_helper().IsLoginTab()); + + ObservePortalResult(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_BEHIND_CAPTIVE_PORTAL); + EXPECT_TRUE(tab_helper().IsLoginTab()); + + ObservePortalResult(RESULT_NO_RESPONSE, RESULT_INTERNET_CONNECTED); + EXPECT_FALSE(tab_helper().IsLoginTab()); +} + +TEST_F(CaptivePortalTabHelperTest, NoLoginTab) { + EXPECT_FALSE(tab_helper().IsLoginTab()); + + ObservePortalResult(RESULT_INTERNET_CONNECTED, RESULT_BEHIND_CAPTIVE_PORTAL); + EXPECT_FALSE(tab_helper().IsLoginTab()); + + ObservePortalResult(RESULT_BEHIND_CAPTIVE_PORTAL, RESULT_NO_RESPONSE); + EXPECT_FALSE(tab_helper().IsLoginTab()); + + ObservePortalResult(RESULT_NO_RESPONSE, RESULT_INTERNET_CONNECTED); + EXPECT_FALSE(tab_helper().IsLoginTab()); +} + +} // namespace captive_portal diff --git a/chrome/browser/captive_portal/captive_portal_tab_reloader.cc b/chrome/browser/captive_portal/captive_portal_tab_reloader.cc new file mode 100644 index 0000000..249d580 --- /dev/null +++ b/chrome/browser/captive_portal/captive_portal_tab_reloader.cc @@ -0,0 +1,233 @@ +// 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. + +#include "chrome/browser/captive_portal/captive_portal_tab_reloader.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/message_loop.h" +#include "chrome/browser/captive_portal/captive_portal_service.h" +#include "chrome/browser/captive_portal/captive_portal_service_factory.h" +#include "chrome/common/chrome_notification_types.h" +#include "content/public/browser/navigation_controller.h" +#include "content/public/browser/navigation_entry.h" +#include "content/public/browser/web_contents.h" +#include "net/base/net_errors.h" + +namespace captive_portal { + +namespace { + +// The time to wait for a slow loading SSL page before triggering a captive +// portal check. +const int kDefaultSlowSSLTimeSeconds = 30; + +} // namespace + +CaptivePortalTabReloader::CaptivePortalTabReloader( + Profile* profile, + content::WebContents* web_contents, + const OpenLoginTabCallback& open_login_tab_callback) + : profile_(profile), + web_contents_(web_contents), + state_(STATE_NONE), + provisional_main_frame_load_(false), + slow_ssl_load_time_( + base::TimeDelta::FromSeconds(kDefaultSlowSSLTimeSeconds)), + open_login_tab_callback_(open_login_tab_callback), + ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)) { +} + +CaptivePortalTabReloader::~CaptivePortalTabReloader() { +} + +void CaptivePortalTabReloader::OnLoadStart(bool is_ssl) { + provisional_main_frame_load_ = true; + + SetState(STATE_NONE); + + // Start the slow load timer for SSL pages. + // TODO(mmenke): Should this look at the port instead? The reason the + // request never connects is because of the port, not the + // protocol. + if (is_ssl) + SetState(STATE_TIMER_RUNNING); +} + +void CaptivePortalTabReloader::OnLoadCommitted(int net_error) { + provisional_main_frame_load_ = false; + + if (state_ == STATE_NONE) + return; + + // If there's no timeout error, reset the state. + if (net_error != net::ERR_CONNECTION_TIMED_OUT) { + // TODO(mmenke): If the new URL is the same as the old broken URL, and the + // request succeeds, should probably trigger another + // captive portal check. + SetState(STATE_NONE); + return; + } + + // The page timed out before the timer triggered. This is not terribly + // likely, but if it does happen, the tab may have been broken by a captive + // portal. Go ahead and try to detect a portal now, rather than waiting for + // the timer. + if (state_ == STATE_TIMER_RUNNING) { + OnSlowSSLConnect(); + return; + } + + // If the tab needs to reload, do so asynchronously, to avoid reentrancy + // issues. + if (state_ == STATE_NEEDS_RELOAD) { + MessageLoop::current()->PostTask( + FROM_HERE, + base::Bind(&CaptivePortalTabReloader::ReloadTabIfNeeded, + weak_factory_.GetWeakPtr())); + } +} + +void CaptivePortalTabReloader::OnAbort() { + provisional_main_frame_load_ = false; + SetState(STATE_NONE); +} + +void CaptivePortalTabReloader::OnCaptivePortalResults( + Result previous_result, + Result result) { + if (result == RESULT_BEHIND_CAPTIVE_PORTAL) { + if (state_ == STATE_MAYBE_BROKEN_BY_PORTAL) { + SetState(STATE_BROKEN_BY_PORTAL); + MaybeOpenCaptivePortalLoginTab(); + } + return; + } + + switch (state_) { + case STATE_MAYBE_BROKEN_BY_PORTAL: + case STATE_TIMER_RUNNING: + // If the previous result was BEHIND_CAPTIVE_PORTAL, and the state is + // either STATE_MAYBE_BROKEN_BY_PORTAL or STATE_TIMER_RUNNING, reload the + // tab. In the latter case, the tab has yet to commit, but is an SSL + // page, so if the page ends up at a timeout error, it will be reloaded. + // If not, the state will just be reset. The helps in the case that a + // user tries to reload a tab, and then quickly logs in. + if (previous_result == RESULT_BEHIND_CAPTIVE_PORTAL) { + SetState(STATE_NEEDS_RELOAD); + return; + } + SetState(STATE_NONE); + return; + + case STATE_BROKEN_BY_PORTAL: + // Either reload the tab now, if a connection timed out error page has + // already been committed, or reload it if and when a timeout commits. + SetState(STATE_NEEDS_RELOAD); + return; + + case STATE_NEEDS_RELOAD: + case STATE_NONE: + // If the tab needs to reload or is in STATE_NONE, do nothing. The reload + // case shouldn't be very common, since it only lasts until a tab times + // out, but it's still possible. + return; + + default: + NOTREACHED(); + } +} + +void CaptivePortalTabReloader::OnSlowSSLConnect() { + SetState(STATE_MAYBE_BROKEN_BY_PORTAL); +} + +void CaptivePortalTabReloader::SetState(State new_state) { + // Stop the timer even when old and new states are the same. + if (state_ == STATE_TIMER_RUNNING) { + slow_ssl_load_timer_.Stop(); + } else { + DCHECK(!slow_ssl_load_timer_.IsRunning()); + } + + // Check for unexpected state transitions. + switch (state_) { + case STATE_NONE: + DCHECK(new_state == STATE_NONE || + new_state == STATE_TIMER_RUNNING); + break; + case STATE_TIMER_RUNNING: + DCHECK(new_state == STATE_NONE || + new_state == STATE_MAYBE_BROKEN_BY_PORTAL || + new_state == STATE_NEEDS_RELOAD); + break; + case STATE_MAYBE_BROKEN_BY_PORTAL: + DCHECK(new_state == STATE_NONE || + new_state == STATE_BROKEN_BY_PORTAL || + new_state == STATE_NEEDS_RELOAD); + break; + case STATE_BROKEN_BY_PORTAL: + DCHECK(new_state == STATE_NONE || + new_state == STATE_NEEDS_RELOAD); + break; + case STATE_NEEDS_RELOAD: + DCHECK_EQ(STATE_NONE, new_state); + break; + default: + NOTREACHED(); + break; + }; + + state_ = new_state; + + switch (state_) { + case STATE_TIMER_RUNNING: + slow_ssl_load_timer_.Start( + FROM_HERE, + slow_ssl_load_time_, + this, + &CaptivePortalTabReloader::OnSlowSSLConnect); + break; + + case STATE_MAYBE_BROKEN_BY_PORTAL: + CheckForCaptivePortal(); + break; + + case STATE_NEEDS_RELOAD: + // Try to reload the tab now. + ReloadTabIfNeeded(); + break; + + default: + break; + } +} + +void CaptivePortalTabReloader::ReloadTabIfNeeded() { + // If there's still a provisional load going, or the page no longer needs + // to be reloaded, due to a new navigation, do nothing. + if (state_ != STATE_NEEDS_RELOAD || provisional_main_frame_load_) + return; + SetState(STATE_NONE); + ReloadTab(); +} + +void CaptivePortalTabReloader::ReloadTab() { + content::NavigationController* controller = &web_contents_->GetController(); + if (!controller->GetActiveEntry()->GetHasPostData()) + controller->Reload(true); +} + +void CaptivePortalTabReloader::MaybeOpenCaptivePortalLoginTab() { + open_login_tab_callback_.Run(); +} + +void CaptivePortalTabReloader::CheckForCaptivePortal() { + CaptivePortalService* service = + CaptivePortalServiceFactory::GetForProfile(profile_); + if (service) + service->DetectCaptivePortal(); +} + +} // namespace captive_portal diff --git a/chrome/browser/captive_portal/captive_portal_tab_reloader.h b/chrome/browser/captive_portal/captive_portal_tab_reloader.h new file mode 100644 index 0000000..04b4c97 --- /dev/null +++ b/chrome/browser/captive_portal/captive_portal_tab_reloader.h @@ -0,0 +1,171 @@ +// 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. + +#ifndef CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_TAB_RELOADER_H_ +#define CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_TAB_RELOADER_H_ +#pragma once + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/compiler_specific.h" +#include "base/memory/weak_ptr.h" +#include "base/time.h" +#include "base/timer.h" +#include "chrome/browser/captive_portal/captive_portal_service.h" + +class Profile; + +namespace content { +class WebContents; +} + +namespace captive_portal { + +// Keeps track of whether a tab has encountered a navigation error caused by a +// captive portal. Also triggers captive portal checks when a page load may +// have been broken or be taking longer due to a captive portal. All methods +// may only be called on the UI thread. +// +// Only supports SSL main frames which time out in response to captive portals, +// since these make for a particularly bad user experience. Non-SSL requests +// are intercepted by captive portals, which take users to the login page. SSL +// requests, however, are generally silently blackholed. They then take a +// while to timeout, and will timeout again when refreshed. +class CaptivePortalTabReloader { + public: + enum State { + STATE_NONE, + // The slow load timer is running. Only started on SSL provisional loads. + // If the timer triggers before the page has been committed, a captive + // portal test will be requested. + STATE_TIMER_RUNNING, + // The tab may have been broken by a captive portal. A tab switches to + // this state either on an ERR_CONNECTION_TIMEOUT of an SSL page or when + // an SSL request takes too long to commit. The tab will remain in this + // state until the current load succeeds, a new provisional load starts, + // or it gets a captive portal result. + STATE_MAYBE_BROKEN_BY_PORTAL, + // The TabHelper switches to this state from STATE_MAYBE_BROKEN_BY_PORTAL in + // response to a RESULT_BEHIND_CAPTIVE_PORTAL. The tab will remain in this + // state until a new provisional load starts, the original load successfully + // commits, the current load is aborted, or the tab reloads the page in + // response to receiving a captive portal result other than + // RESULT_BEHIND_CAPTIVE_PORTAL. + STATE_BROKEN_BY_PORTAL, + // The page may need to be reloaded. The tab will be reloaded if the page + // fails the next load with a timeout, or immediately upon switching to this + // state, if the page already timed out. If anything else happens + // when in this state (Another error, successful navigation, or the original + // navigation was aborted), the TabHelper transitions to STATE_NONE without + // reloading. + STATE_NEEDS_RELOAD, + }; + + // Function to open a login tab, if there isn't one already. + typedef base::Callback<void()> OpenLoginTabCallback; + + // |profile| and |web_contents| will only be dereferenced in ReloadTab, + // MaybeOpenCaptivePortalLoginTab, and CheckForCaptivePortal, so they can + // both be NULL in the unit tests as long as those functions are not called. + CaptivePortalTabReloader(Profile* profile, + content::WebContents* web_contents, + const OpenLoginTabCallback& open_login_tab_callback); + + virtual ~CaptivePortalTabReloader(); + + // The following 4 functions are all invoked by the CaptivePortalTabHelper: + + // Called when a non-error main frame load starts. Resets current state, + // unless this is a login tab. Each load will eventually result in a call to + // OnLoadCommitted or OnAbort. The former will be called both on successful + // loads and for error pages. + virtual void OnLoadStart(bool is_ssl); + + // Called when a page is committed. |net_error| will be net::OK in the case + // of a successful load. For an errror page, the entire 3-step process of + // getting the error, starting a new provisional load for the error page, and + // committing the error page is treated as a single commit. + // + // The Link Doctor page will typically be one OnLoadCommitted with an error + // code, followed by another OnLoadCommitted with net::OK for the Link Doctor + // page. + virtual void OnLoadCommitted(int net_error); + + // This is called when the current provisional load is canceled. + // Sets state to STATE_NONE, unless this is a login tab. + virtual void OnAbort(); + + // Called by CaptivePortalTabHelper whenever a captive portal test completes. + virtual void OnCaptivePortalResults(Result previous_result, Result result); + + protected: + // The following functions are used only when testing: + + State state() const { return state_; } + + void set_slow_ssl_load_time(base::TimeDelta slow_ssl_load_time) { + slow_ssl_load_time_ = slow_ssl_load_time; + } + + // Started whenever an SSL tab starts loading, when the state is switched to + // STATE_TIMER_RUNNING. Stopped on any state change, including when a page + // commits or there's an error. If the timer triggers, the state switches to + // STATE_MAYBE_BROKEN_BY_PORTAL and |this| kicks off a captive portal check. + // TODO(mmenke): On redirects, update this timer. + base::OneShotTimer<CaptivePortalTabReloader> slow_ssl_load_timer_; + + private: + friend class CaptivePortalBrowserTest; + + // Sets |state_| and takes any action associated with the new state. Also + // stops the timer, if needed. + void SetState(State new_state); + + // Called by a timer when an SSL main frame provisional load is taking a + // while to commit. + void OnSlowSSLConnect(); + + // Reloads the tab if there's no provisional load going on and the current + // state is STATE_NEEDS_RELOAD. Not safe to call synchronously when called + // by from a WebContentsObserver function, since the WebContents is currently + // performing some action. + void ReloadTabIfNeeded(); + + // Reloads the tab. + virtual void ReloadTab(); + + // Opens a login tab in the topmost browser window for the |profile_|, if the + // profile has a tabbed browser window and the window doesn't already have a + // login tab. Otherwise, does nothing. + virtual void MaybeOpenCaptivePortalLoginTab(); + + // Tries to get |profile_|'s CaptivePortalService and have it start a captive + // portal check. + virtual void CheckForCaptivePortal(); + + Profile* profile_; + content::WebContents* web_contents_; + + State state_; + + // Tracks if there's a load going on that can't safely be interrupted. This + // is true between the time when a provisional load fails and when an error + // page's provisional load starts, so does not perfectly align with the + // notion of a provisional load used by the WebContents. + bool provisional_main_frame_load_; + + // Time to wait after a provisional HTTPS load before triggering a captive + // portal check. + base::TimeDelta slow_ssl_load_time_; + + const OpenLoginTabCallback open_login_tab_callback_; + + base::WeakPtrFactory<CaptivePortalTabReloader> weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(CaptivePortalTabReloader); +}; + +} // namespace captive_portal + +#endif // CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_TAB_RELOADER_H_ diff --git a/chrome/browser/captive_portal/captive_portal_tab_reloader_unittest.cc b/chrome/browser/captive_portal/captive_portal_tab_reloader_unittest.cc new file mode 100644 index 0000000..7048aa1 --- /dev/null +++ b/chrome/browser/captive_portal/captive_portal_tab_reloader_unittest.cc @@ -0,0 +1,404 @@ +// 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. + +#include "chrome/browser/captive_portal/captive_portal_tab_reloader.h" + +#include "base/callback.h" +#include "base/message_loop.h" +#include "chrome/browser/captive_portal/captive_portal_service.h" +#include "net/base/net_errors.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace captive_portal { + +// Used for testing CaptivePortalTabReloader in isolation from the observer. +// Exposes a number of private functions and mocks out others. +class TestCaptivePortalTabReloader : public CaptivePortalTabReloader { + public: + TestCaptivePortalTabReloader() + : CaptivePortalTabReloader(NULL, NULL, base::Callback<void(void)>()) { + } + + bool TimerRunning() { + return slow_ssl_load_timer_.IsRunning(); + } + + // The following methods are aliased so they can be publicly accessed by the + // unit tests. + + State state() const { + return CaptivePortalTabReloader::state(); + } + + void set_slow_ssl_load_time(base::TimeDelta slow_ssl_load_time) { + EXPECT_FALSE(TimerRunning()); + CaptivePortalTabReloader::set_slow_ssl_load_time(slow_ssl_load_time); + } + + MOCK_METHOD0(ReloadTab, void()); + MOCK_METHOD0(MaybeOpenCaptivePortalLoginTab, void()); + MOCK_METHOD0(CheckForCaptivePortal, void()); +}; + +class CaptivePortalTabReloaderTest : public testing::Test { + public: + CaptivePortalTabReloaderTest() { + // Most tests don't run the message loop, so don't use a timer for them. + tab_reloader_.set_slow_ssl_load_time(base::TimeDelta()); + } + + virtual ~CaptivePortalTabReloaderTest() { + } + + // testing::Test + virtual void TearDown() OVERRIDE { + EXPECT_FALSE(tab_reloader().TimerRunning()); + // Run any pending operations, so the test fails if there was a call to + // a mocked out function pending. + MessageLoop::current()->RunAllPending(); + } + + TestCaptivePortalTabReloader& tab_reloader() { return tab_reloader_; } + + private: + MessageLoop message_loop_; + + testing::StrictMock<TestCaptivePortalTabReloader> tab_reloader_; +}; + +// Simulates a slow SSL load when the Internet is connected. +TEST_F(CaptivePortalTabReloaderTest, InternetConnected) { + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); + + tab_reloader().OnLoadStart(true); + EXPECT_EQ(CaptivePortalTabReloader::STATE_TIMER_RUNNING, + tab_reloader().state()); + EXPECT_TRUE(tab_reloader().TimerRunning()); + + EXPECT_CALL(tab_reloader(), CheckForCaptivePortal()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_FALSE(tab_reloader().TimerRunning()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_MAYBE_BROKEN_BY_PORTAL, + tab_reloader().state()); + + tab_reloader().OnCaptivePortalResults(RESULT_INTERNET_CONNECTED, + RESULT_INTERNET_CONNECTED); + + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); + EXPECT_FALSE(tab_reloader().TimerRunning()); + + tab_reloader().OnLoadCommitted(net::OK); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// Simulates a slow SSL load when the Internet is connected. In this case, +// the timeout error occurs before the timer triggers. Unlikely to happen +// in practice, but best if it still works. +TEST_F(CaptivePortalTabReloaderTest, InternetConnectedTimeout) { + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); + + tab_reloader().OnLoadStart(true); + EXPECT_EQ(CaptivePortalTabReloader::STATE_TIMER_RUNNING, + tab_reloader().state()); + EXPECT_TRUE(tab_reloader().TimerRunning()); + + EXPECT_CALL(tab_reloader(), CheckForCaptivePortal()).Times(1); + tab_reloader().OnLoadCommitted(net::ERR_CONNECTION_TIMED_OUT); + EXPECT_FALSE(tab_reloader().TimerRunning()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_MAYBE_BROKEN_BY_PORTAL, + tab_reloader().state()); + + tab_reloader().OnCaptivePortalResults(RESULT_INTERNET_CONNECTED, + RESULT_INTERNET_CONNECTED); + + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// Simulates a slow SSL load when captive portal checks return no response. +TEST_F(CaptivePortalTabReloaderTest, NoResponse) { + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); + + tab_reloader().OnLoadStart(true); + EXPECT_EQ(CaptivePortalTabReloader::STATE_TIMER_RUNNING, + tab_reloader().state()); + EXPECT_TRUE(tab_reloader().TimerRunning()); + + EXPECT_CALL(tab_reloader(), CheckForCaptivePortal()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_FALSE(tab_reloader().TimerRunning()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_MAYBE_BROKEN_BY_PORTAL, + tab_reloader().state()); + + tab_reloader().OnCaptivePortalResults(RESULT_NO_RESPONSE, RESULT_NO_RESPONSE); + + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); + EXPECT_FALSE(tab_reloader().TimerRunning()); + + tab_reloader().OnLoadCommitted(net::OK); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// Simulates a slow HTTP load when behind a captive portal, that eventually. +// tiems out. Since it's HTTP, the TabReloader should do nothing. +TEST_F(CaptivePortalTabReloaderTest, DoesNothingOnHttp) { + tab_reloader().OnLoadStart(false); + EXPECT_FALSE(tab_reloader().TimerRunning()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); + + tab_reloader().OnCaptivePortalResults(RESULT_INTERNET_CONNECTED, + RESULT_BEHIND_CAPTIVE_PORTAL); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); + + // The user logs in. + tab_reloader().OnCaptivePortalResults(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_INTERNET_CONNECTED); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); + + // The page times out. + tab_reloader().OnLoadCommitted(net::ERR_CONNECTION_TIMED_OUT); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// Simulate the normal login process. The user logs in before the error page +// in the original tab commits. +TEST_F(CaptivePortalTabReloaderTest, Login) { + tab_reloader().OnLoadStart(true); + + EXPECT_CALL(tab_reloader(), CheckForCaptivePortal()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_FALSE(tab_reloader().TimerRunning()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_MAYBE_BROKEN_BY_PORTAL, + tab_reloader().state()); + + // The captive portal service detects a captive portal. The TabReloader + // should try and create a new login tab in response. + EXPECT_CALL(tab_reloader(), MaybeOpenCaptivePortalLoginTab()).Times(1); + tab_reloader().OnCaptivePortalResults(RESULT_INTERNET_CONNECTED, + RESULT_BEHIND_CAPTIVE_PORTAL); + EXPECT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + tab_reloader().state()); + EXPECT_FALSE(tab_reloader().TimerRunning()); + + // The user logs on from another tab, and a captive portal check is triggered. + tab_reloader().OnCaptivePortalResults(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_INTERNET_CONNECTED); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + + // The error page commits, which should start an asynchronous reload. + tab_reloader().OnLoadCommitted(net::ERR_CONNECTION_TIMED_OUT); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + + EXPECT_CALL(tab_reloader(), ReloadTab()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// Simulate the normal login process. The user logs in after the tab finishes +// loading the error page. +TEST_F(CaptivePortalTabReloaderTest, LoginLate) { + tab_reloader().OnLoadStart(true); + + EXPECT_CALL(tab_reloader(), CheckForCaptivePortal()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_FALSE(tab_reloader().TimerRunning()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_MAYBE_BROKEN_BY_PORTAL, + tab_reloader().state()); + + // The captive portal service detects a captive portal. The TabReloader + // should try and create a new login tab in response. + EXPECT_CALL(tab_reloader(), MaybeOpenCaptivePortalLoginTab()).Times(1); + tab_reloader().OnCaptivePortalResults(RESULT_INTERNET_CONNECTED, + RESULT_BEHIND_CAPTIVE_PORTAL); + EXPECT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + tab_reloader().state()); + EXPECT_FALSE(tab_reloader().TimerRunning()); + + // The error page commits. + tab_reloader().OnLoadCommitted(net::ERR_CONNECTION_TIMED_OUT); + EXPECT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + tab_reloader().state()); + + // The user logs on from another tab, and a captive portal check is triggered. + EXPECT_CALL(tab_reloader(), ReloadTab()).Times(1); + tab_reloader().OnCaptivePortalResults(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_INTERNET_CONNECTED); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// Simulate a login after the tab times out unexpectedly quickly. +TEST_F(CaptivePortalTabReloaderTest, TimeoutFast) { + tab_reloader().OnLoadStart(true); + + // The error page commits, which should trigger a captive portal check, + // since the timer's still running. + EXPECT_CALL(tab_reloader(), CheckForCaptivePortal()).Times(1); + tab_reloader().OnLoadCommitted(net::ERR_CONNECTION_TIMED_OUT); + EXPECT_EQ(CaptivePortalTabReloader::STATE_MAYBE_BROKEN_BY_PORTAL, + tab_reloader().state()); + + // The captive portal service detects a captive portal. The TabReloader + // should try and create a new login tab in response. + EXPECT_CALL(tab_reloader(), MaybeOpenCaptivePortalLoginTab()).Times(1); + tab_reloader().OnCaptivePortalResults(RESULT_INTERNET_CONNECTED, + RESULT_BEHIND_CAPTIVE_PORTAL); + EXPECT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + tab_reloader().state()); + EXPECT_FALSE(tab_reloader().TimerRunning()); + + // The user logs on from another tab, and a captive portal check is triggered. + EXPECT_CALL(tab_reloader(), ReloadTab()).Times(1); + tab_reloader().OnCaptivePortalResults(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_INTERNET_CONNECTED); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// Simulate the case that a user has already logged in before the tab receives a +// captive portal result, but a RESULT_BEHIND_CAPTIVE_PORTAL was received +// before the tab started loading. +TEST_F(CaptivePortalTabReloaderTest, AlreadyLoggedIn) { + tab_reloader().OnLoadStart(true); + + EXPECT_CALL(tab_reloader(), CheckForCaptivePortal()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_FALSE(tab_reloader().TimerRunning()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_MAYBE_BROKEN_BY_PORTAL, + tab_reloader().state()); + + // The user has already logged in. Since the last result found a captive + // portal, the tab will be reloaded if a timeout is committed. + tab_reloader().OnCaptivePortalResults(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_INTERNET_CONNECTED); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + + // The error page commits, which should start an asynchronous reload. + tab_reloader().OnLoadCommitted(net::ERR_CONNECTION_TIMED_OUT); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + + EXPECT_CALL(tab_reloader(), ReloadTab()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// Same as above, except the result is received even before the timer triggers, +// due to a captive portal test request from some external source, like a login +// tab. +TEST_F(CaptivePortalTabReloaderTest, AlreadyLoggedInBeforeTimerTriggers) { + tab_reloader().OnLoadStart(true); + + // The user has already logged in. Since the last result indicated there is + // a captive portal, the tab will be reloaded if it times out. + tab_reloader().OnCaptivePortalResults(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_INTERNET_CONNECTED); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + EXPECT_FALSE(tab_reloader().TimerRunning()); + + // The error page commits, which should start an asynchronous reload. + tab_reloader().OnLoadCommitted(net::ERR_CONNECTION_TIMED_OUT); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + + EXPECT_CALL(tab_reloader(), ReloadTab()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// Simulate the user logging in while the timer is still running. May happen +// if the tab is reloaded just before logging in on another tab. +TEST_F(CaptivePortalTabReloaderTest, LogInWhileTimerRunning) { + tab_reloader().OnLoadStart(true); + EXPECT_EQ(CaptivePortalTabReloader::STATE_TIMER_RUNNING, + tab_reloader().state()); + EXPECT_TRUE(tab_reloader().TimerRunning()); + + // The user has already logged in. + tab_reloader().OnCaptivePortalResults(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_INTERNET_CONNECTED); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + + // The error page commits, which should start an asynchronous reload. + tab_reloader().OnLoadCommitted(net::ERR_CONNECTION_TIMED_OUT); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + + EXPECT_CALL(tab_reloader(), ReloadTab()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// Simulate a captive portal being detected while the time is still running. +// The captive portal check triggered by the timer detects the captive portal +// again, and then the user logs in. +TEST_F(CaptivePortalTabReloaderTest, BehindPortalResultWhileTimerRunning) { + tab_reloader().OnLoadStart(true); + EXPECT_EQ(CaptivePortalTabReloader::STATE_TIMER_RUNNING, + tab_reloader().state()); + EXPECT_TRUE(tab_reloader().TimerRunning()); + + // The user is behind a captive portal, but since the tab hasn't timed out, + // the message is ignored. + tab_reloader().OnCaptivePortalResults(RESULT_INTERNET_CONNECTED, + RESULT_BEHIND_CAPTIVE_PORTAL); + EXPECT_EQ(CaptivePortalTabReloader::STATE_TIMER_RUNNING, + tab_reloader().state()); + + // The rest proceeds as normal. + EXPECT_CALL(tab_reloader(), CheckForCaptivePortal()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_EQ(CaptivePortalTabReloader::STATE_MAYBE_BROKEN_BY_PORTAL, + tab_reloader().state()); + + // The captive portal service detects a captive portal, and this time the + // tab tries to create a login tab. + EXPECT_CALL(tab_reloader(), MaybeOpenCaptivePortalLoginTab()).Times(1); + tab_reloader().OnCaptivePortalResults(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_BEHIND_CAPTIVE_PORTAL); + EXPECT_EQ(CaptivePortalTabReloader::STATE_BROKEN_BY_PORTAL, + tab_reloader().state()); + EXPECT_FALSE(tab_reloader().TimerRunning()); + + // The user logs on from another tab, and a captive portal check is triggered. + tab_reloader().OnCaptivePortalResults(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_INTERNET_CONNECTED); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + + // The error page commits, which should start an asynchronous reload. + tab_reloader().OnLoadCommitted(net::ERR_CONNECTION_TIMED_OUT); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + + EXPECT_CALL(tab_reloader(), ReloadTab()).Times(1); + MessageLoop::current()->RunAllPending(); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +// The CaptivePortalService detects the user has logged in to a captive portal +// while the timer is still running, but the original load succeeds, so no +// reload is done. +TEST_F(CaptivePortalTabReloaderTest, LogInWhileTimerRunningNoError) { + tab_reloader().OnLoadStart(true); + EXPECT_EQ(CaptivePortalTabReloader::STATE_TIMER_RUNNING, + tab_reloader().state()); + EXPECT_TRUE(tab_reloader().TimerRunning()); + + // The user has already logged in. + tab_reloader().OnCaptivePortalResults(RESULT_BEHIND_CAPTIVE_PORTAL, + RESULT_INTERNET_CONNECTED); + EXPECT_FALSE(tab_reloader().TimerRunning()); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NEEDS_RELOAD, + tab_reloader().state()); + + // The page successfully commits, so no reload is triggered. + tab_reloader().OnLoadCommitted(net::OK); + EXPECT_EQ(CaptivePortalTabReloader::STATE_NONE, tab_reloader().state()); +} + +} // namespace captive_portal diff --git a/chrome/browser/profiles/profile_dependency_manager.cc b/chrome/browser/profiles/profile_dependency_manager.cc index 04ce697..f25df77 100644 --- a/chrome/browser/profiles/profile_dependency_manager.cc +++ b/chrome/browser/profiles/profile_dependency_manager.cc @@ -184,7 +184,7 @@ void ProfileDependencyManager::AssertFactoriesBuilt() { BackgroundContentsServiceFactory::GetInstance(); #endif BookmarkModelFactory::GetInstance(); -#if !defined(OS_ANDROID) +#if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) captive_portal::CaptivePortalServiceFactory::GetInstance(); #endif ChromeURLDataManagerFactory::GetInstance(); diff --git a/chrome/browser/ui/tab_contents/tab_contents.cc b/chrome/browser/ui/tab_contents/tab_contents.cc index 5c0553f..47b894d 100644 --- a/chrome/browser/ui/tab_contents/tab_contents.cc +++ b/chrome/browser/ui/tab_contents/tab_contents.cc @@ -10,6 +10,7 @@ #include "chrome/browser/autofill/autofill_external_delegate.h" #include "chrome/browser/autofill/autofill_manager.h" #include "chrome/browser/automation/automation_tab_helper.h" +#include "chrome/browser/captive_portal/captive_portal_tab_helper.h" #include "chrome/browser/content_settings/tab_specific_content_settings.h" #include "chrome/browser/extensions/api/web_navigation/web_navigation_api.h" #include "chrome/browser/extensions/extension_tab_helper.h" @@ -97,6 +98,10 @@ TabContents::TabContents(WebContents* contents) #endif blocked_content_tab_helper_.reset(new BlockedContentTabHelper(this)); bookmark_tab_helper_.reset(new BookmarkTabHelper(this)); +#if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) + captive_portal_tab_helper_.reset( + new captive_portal::CaptivePortalTabHelper(profile(), web_contents())); +#endif constrained_window_tab_helper_.reset(new ConstrainedWindowTabHelper(this)); core_tab_helper_.reset(new CoreTabHelper(contents)); extension_tab_helper_.reset(new ExtensionTabHelper(this)); diff --git a/chrome/browser/ui/tab_contents/tab_contents.h b/chrome/browser/ui/tab_contents/tab_contents.h index 458e40a..d38956b 100644 --- a/chrome/browser/ui/tab_contents/tab_contents.h +++ b/chrome/browser/ui/tab_contents/tab_contents.h @@ -55,6 +55,10 @@ namespace browser_sync { class SyncedTabDelegate; } +namespace captive_portal { +class CaptivePortalTabHelper; +} + namespace extensions { class WebNavigationTabObserver; } @@ -138,6 +142,12 @@ class TabContents : public content::WebContentsObserver { return bookmark_tab_helper_.get(); } +#if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) + captive_portal::CaptivePortalTabHelper* captive_portal_tab_helper() { + return captive_portal_tab_helper_.get(); + } +#endif + ConstrainedWindowTabHelper* constrained_window_tab_helper() { return constrained_window_tab_helper_.get(); } @@ -242,6 +252,9 @@ class TabContents : public content::WebContentsObserver { scoped_ptr<AutomationTabHelper> automation_tab_helper_; scoped_ptr<BlockedContentTabHelper> blocked_content_tab_helper_; scoped_ptr<BookmarkTabHelper> bookmark_tab_helper_; +#if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) + scoped_ptr<captive_portal::CaptivePortalTabHelper> captive_portal_tab_helper_; +#endif scoped_ptr<ConstrainedWindowTabHelper> constrained_window_tab_helper_; scoped_ptr<CoreTabHelper> core_tab_helper_; scoped_ptr<ExtensionTabHelper> extension_tab_helper_; diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index cdf4a8b..8ba0424 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -360,10 +360,16 @@ 'browser/feedback/feedback_util.h', 'browser/cancelable_request.cc', 'browser/cancelable_request.h', + 'browser/captive_portal/captive_portal_login_detector.cc', + 'browser/captive_portal/captive_portal_login_detector.h', 'browser/captive_portal/captive_portal_service.cc', 'browser/captive_portal/captive_portal_service.h', 'browser/captive_portal/captive_portal_service_factory.cc', 'browser/captive_portal/captive_portal_service_factory.h', + 'browser/captive_portal/captive_portal_tab_helper.cc', + 'browser/captive_portal/captive_portal_tab_helper.h', + 'browser/captive_portal/captive_portal_tab_reloader.cc', + 'browser/captive_portal/captive_portal_tab_reloader.h', 'browser/certificate_manager_model.cc', 'browser/certificate_manager_model.h', 'browser/certificate_viewer.cc', @@ -4668,6 +4674,11 @@ ['exclude', '^browser/ui/webui/print_preview/'], ], }], + ['enable_captive_portal_detection!=1', { + 'sources/': [ + ['exclude', '^browser/captive_portal/'], + ], + }], ['enable_session_service!=1', { 'sources!': [ 'browser/sessions/session_restore.cc', diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index f6d7f8a..9942709 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -1062,6 +1062,8 @@ 'browser/browsing_data_remover_unittest.cc', 'browser/browsing_data_server_bound_cert_helper_unittest.cc', 'browser/captive_portal/captive_portal_service_unittest.cc', + 'browser/captive_portal/captive_portal_tab_helper_unittest.cc', + 'browser/captive_portal/captive_portal_tab_reloader_unittest.cc', 'browser/chrome_browser_application_mac_unittest.mm', 'browser/chrome_browser_main_unittest.cc', 'browser/chrome_page_zoom_unittest.cc', @@ -2132,6 +2134,11 @@ ['exclude', '^browser/ui/webui/print_preview/'], ], }], + ['enable_captive_portal_detection!=1', { + 'sources/': [ + ['exclude', '^browser/captive_portal/'], + ], + }], ['enable_session_service!=1', { 'sources!': [ 'browser/sessions/session_service_unittest.cc', @@ -2637,6 +2644,7 @@ 'browser/browsing_data_helper_browsertest.h', 'browser/browsing_data_indexed_db_helper_browsertest.cc', 'browser/browsing_data_local_storage_helper_browsertest.cc', + 'browser/captive_portal/captive_portal_browsertest.cc', 'browser/chrome_main_browsertest.cc', 'browser/chrome_switches_browsertest.cc', 'browser/chromeos/bluetooth/test/mock_bluetooth_adapter.cc', @@ -3167,6 +3175,11 @@ ['exclude', '^renderer/safe_browsing/'], ], }], + ['enable_captive_portal_detection!=1', { + 'sources/': [ + ['exclude', '^browser/captive_portal/'], + ], + }], ['internal_pdf', { 'sources': [ 'browser/ui/pdf/pdf_browsertest.cc', diff --git a/chrome/test/data/captive_portal/iframe_timeout.html b/chrome/test/data/captive_portal/iframe_timeout.html new file mode 100644 index 0000000..36286ca --- /dev/null +++ b/chrome/test/data/captive_portal/iframe_timeout.html @@ -0,0 +1,12 @@ +<html> +<head> +<title>Iframe Timeout</title> + +</head> +<body> +This iframe will fail to load with an HTTPS connection timeout. +"mock.failed.request" is the magic hostname used by URLRequestFailedJob, +and -118 is CONNECTION_TIMED_OUT. +<iframe src="https://mock.failed.request/-118"></iframe> +</body> +</html> diff --git a/chrome/test/data/captive_portal/login.html b/chrome/test/data/captive_portal/login.html new file mode 100644 index 0000000..ffc3130 --- /dev/null +++ b/chrome/test/data/captive_portal/login.html @@ -0,0 +1,19 @@ +<html> +<head> +<title>Fake Login Page</title> + +<script> + +function submitForm() { + document.getElementById('form').submit(); +} + +</script> + +</head> +<body> +<form id='form' action="login.html" method="post"> +<input type="submit" value="Login" /> +</form> +</body> +</html> diff --git a/chrome/test/data/captive_portal/login.html.mock-http-headers b/chrome/test/data/captive_portal/login.html.mock-http-headers new file mode 100644 index 0000000..2da6d2c --- /dev/null +++ b/chrome/test/data/captive_portal/login.html.mock-http-headers @@ -0,0 +1,2 @@ +HTTP/1.0 200 Just Peachy + diff --git a/chrome/test/data/captive_portal/page204.html b/chrome/test/data/captive_portal/page204.html new file mode 100644 index 0000000..64322a2 --- /dev/null +++ b/chrome/test/data/captive_portal/page204.html @@ -0,0 +1 @@ +<title>This page intentionally left blank</title> diff --git a/chrome/test/data/captive_portal/page204.html.mock-http-headers b/chrome/test/data/captive_portal/page204.html.mock-http-headers new file mode 100644 index 0000000..cc9dc4b --- /dev/null +++ b/chrome/test/data/captive_portal/page204.html.mock-http-headers @@ -0,0 +1,3 @@ +HTTP/1.0 204 No Content +Content-Length: 0 + diff --git a/content/test/net/url_request_mock_http_job.cc b/content/test/net/url_request_mock_http_job.cc index 24e3436..805e9859 100644 --- a/content/test/net/url_request_mock_http_job.cc +++ b/content/test/net/url_request_mock_http_job.cc @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// 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. @@ -120,6 +120,15 @@ bool URLRequestMockHTTPJob::GetMimeType(std::string* mime_type) const { return info.headers && info.headers->GetMimeType(mime_type); } +int URLRequestMockHTTPJob::GetResponseCode() const { + net::HttpResponseInfo info; + GetResponseInfoConst(&info); + // If we have headers, get the response code from them. + if (info.headers) + return info.headers->response_code(); + return net::URLRequestJob::GetResponseCode(); +} + bool URLRequestMockHTTPJob::GetCharset(std::string* charset) { net::HttpResponseInfo info; GetResponseInfo(&info); diff --git a/content/test/net/url_request_mock_http_job.h b/content/test/net/url_request_mock_http_job.h index 21a84ee..4c311d7 100644 --- a/content/test/net/url_request_mock_http_job.h +++ b/content/test/net/url_request_mock_http_job.h @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// 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. // @@ -19,6 +19,7 @@ class URLRequestMockHTTPJob : public net::URLRequestFileJob { URLRequestMockHTTPJob(net::URLRequest* request, const FilePath& file_path); virtual bool GetMimeType(std::string* mime_type) const OVERRIDE; + virtual int GetResponseCode() const OVERRIDE; virtual bool GetCharset(std::string* charset) OVERRIDE; virtual void GetResponseInfo(net::HttpResponseInfo* info) OVERRIDE; virtual bool IsRedirectResponse(GURL* location, |