summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormmenke@chromium.org <mmenke@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-06-14 21:28:32 +0000
committermmenke@chromium.org <mmenke@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-06-14 21:28:32 +0000
commite602696544fd704597a8ea2c907ffbcb0f688df4 (patch)
tree82c1032031c1e6cfe87ef43b367dd7bd24b5a4fa
parentbcba8aa49bfc2db2a74b042a22784b58ed005fc0 (diff)
downloadchromium_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
-rw-r--r--build/common.gypi10
-rw-r--r--chrome/browser/captive_portal/captive_portal_browsertest.cc1693
-rw-r--r--chrome/browser/captive_portal/captive_portal_login_detector.cc35
-rw-r--r--chrome/browser/captive_portal/captive_portal_login_detector.h48
-rw-r--r--chrome/browser/captive_portal/captive_portal_service.h3
-rw-r--r--chrome/browser/captive_portal/captive_portal_tab_helper.cc177
-rw-r--r--chrome/browser/captive_portal/captive_portal_tab_helper.h120
-rw-r--r--chrome/browser/captive_portal/captive_portal_tab_helper_unittest.cc301
-rw-r--r--chrome/browser/captive_portal/captive_portal_tab_reloader.cc233
-rw-r--r--chrome/browser/captive_portal/captive_portal_tab_reloader.h171
-rw-r--r--chrome/browser/captive_portal/captive_portal_tab_reloader_unittest.cc404
-rw-r--r--chrome/browser/profiles/profile_dependency_manager.cc2
-rw-r--r--chrome/browser/ui/tab_contents/tab_contents.cc5
-rw-r--r--chrome/browser/ui/tab_contents/tab_contents.h13
-rw-r--r--chrome/chrome_browser.gypi11
-rw-r--r--chrome/chrome_tests.gypi13
-rw-r--r--chrome/test/data/captive_portal/iframe_timeout.html12
-rw-r--r--chrome/test/data/captive_portal/login.html19
-rw-r--r--chrome/test/data/captive_portal/login.html.mock-http-headers2
-rw-r--r--chrome/test/data/captive_portal/page204.html1
-rw-r--r--chrome/test/data/captive_portal/page204.html.mock-http-headers3
-rw-r--r--content/test/net/url_request_mock_http_job.cc11
-rw-r--r--content/test/net/url_request_mock_http_job.h3
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(&params);
+ 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,