summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormmenke@chromium.org <mmenke@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-05-11 18:59:20 +0000
committermmenke@chromium.org <mmenke@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2012-05-11 18:59:20 +0000
commitdb92eeb9e9003acddf76ba1237630134be67ae56 (patch)
tree4a07390c75208a147a5e662cf1a2224d6cfe3b4a
parent1ec2f92f9dafeacae9050c226d25f89e71aacc60 (diff)
downloadchromium_src-db92eeb9e9003acddf76ba1237630134be67ae56.zip
chromium_src-db92eeb9e9003acddf76ba1237630134be67ae56.tar.gz
chromium_src-db92eeb9e9003acddf76ba1237630134be67ae56.tar.bz2
Internet connections in many public places (cafes, airports,
hotels, etc) make use of captive portals, which require users to login before allowing Internet access. This CL adds a ProfileKeyedService which checks for these captive portals on demand, and broadcasts a notification with the results. It does not add any consumers for this service. Design doc: https://docs.google.com/document/d/1k-gP2sswzYNvryu9NcgN7q5XrsMlUdlUdoW9WRaEmfM/edit R=cbentzel@chromium.org BUG=87100, 115487 Review URL: https://chromiumcodereview.appspot.com/9188049 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@136627 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/app/generated_resources.grd6
-rw-r--r--chrome/browser/about_flags.cc7
-rw-r--r--chrome/browser/captive_portal/OWNERS1
-rw-r--r--chrome/browser/captive_portal/captive_portal_service.cc423
-rw-r--r--chrome/browser/captive_portal/captive_portal_service.h217
-rw-r--r--chrome/browser/captive_portal/captive_portal_service_factory.cc42
-rw-r--r--chrome/browser/captive_portal/captive_portal_service_factory.h49
-rw-r--r--chrome/browser/captive_portal/captive_portal_service_unittest.cc623
-rw-r--r--chrome/browser/profiles/profile_dependency_manager.cc4
-rw-r--r--chrome/chrome_browser.gypi5
-rw-r--r--chrome/chrome_tests.gypi2
-rw-r--r--chrome/common/chrome_notification_types.h5
-rw-r--r--chrome/common/chrome_switches.cc4
-rw-r--r--chrome/common/chrome_switches.h1
14 files changed, 1389 insertions, 0 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 33e355f..a0af1f8 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -5635,6 +5635,12 @@ Keep your key file in a safe place. You will need it to create new versions of y
</message>
</if>
+ <message name="IDS_FLAGS_CAPTIVE_PORTAL_CHECK_ON_ERROR_NAME" desc="Name for the flag to enable automatic checking for captive portals.">
+ Enable captive portal detection.
+ </message>
+ <message name="IDS_FLAGS_CAPTIVE_PORTAL_CHECK_ON_ERROR_DESCRIPTION" desc="Description for the flag to enable automatic checking for captive portals.">
+ Check for captive portals and open a new tab at the login page if one is found.
+ </message>
<!-- Crashes -->
<message name="IDS_CRASHES_TITLE" desc="Title for the chrome://crashes page.">
Crashes
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 895885b..3c99393 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -608,6 +608,13 @@ const Experiment kExperiments[] = {
kOsAll,
SINGLE_VALUE_TYPE(switches::kDisableChromeToMobile)
},
+ {
+ "enable-captive-portal-detection",
+ IDS_FLAGS_CAPTIVE_PORTAL_CHECK_ON_ERROR_NAME,
+ IDS_FLAGS_CAPTIVE_PORTAL_CHECK_ON_ERROR_DESCRIPTION,
+ kOsMac | kOsWin | kOsLinux | kOsCrOS,
+ SINGLE_VALUE_TYPE(switches::kCaptivePortalDetection)
+ },
#if defined(GOOGLE_CHROME_BUILD)
{
"enable-asynchronous-spellchecking",
diff --git a/chrome/browser/captive_portal/OWNERS b/chrome/browser/captive_portal/OWNERS
new file mode 100644
index 0000000..747dc15
--- /dev/null
+++ b/chrome/browser/captive_portal/OWNERS
@@ -0,0 +1 @@
+mmenke@chromium.org \ No newline at end of file
diff --git a/chrome/browser/captive_portal/captive_portal_service.cc b/chrome/browser/captive_portal/captive_portal_service.cc
new file mode 100644
index 0000000..6f6e7c9
--- /dev/null
+++ b/chrome/browser/captive_portal/captive_portal_service.cc
@@ -0,0 +1,423 @@
+// 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_service.h"
+
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/logging.h"
+#include "base/message_loop.h"
+#include "base/metrics/histogram.h"
+#include "base/rand_util.h"
+#include "base/string_number_conversions.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/common/chrome_notification_types.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/pref_names.h"
+#include "content/public/browser/notification_service.h"
+#include "content/public/common/url_fetcher.h"
+#include "net/base/load_flags.h"
+#include "net/http/http_response_headers.h"
+#include "net/url_request/url_request_status.h"
+
+namespace captive_portal {
+
+namespace {
+
+// The test URL. When connected to the Internet, it should return a blank page
+// with a 204 status code. When behind a captive portal, requests for this
+// URL should get an HTTP redirect or a login page. When neither is true,
+// no server should respond to requests for this URL.
+// TODO(mmenke): Look into getting another domain, for better load management.
+// Need a cookieless domain, so can't use clients*.google.com.
+const char* const kDefaultTestURL = "http://www.gstatic.com/generate_204";
+
+// Used for histograms.
+const char* const kCaptivePortalResultNames[] = {
+ "InternetConnected",
+ "NoResponse",
+ "BehindCaptivePortal",
+ "NumCaptivePortalResults",
+};
+COMPILE_ASSERT(arraysize(kCaptivePortalResultNames) == RESULT_COUNT + 1,
+ captive_portal_result_name_count_mismatch);
+
+// Used for histograms.
+std::string CaptivePortalResultToString(Result result) {
+ DCHECK_GE(result, 0);
+ DCHECK_LT(static_cast<unsigned int>(result),
+ arraysize(kCaptivePortalResultNames));
+ return kCaptivePortalResultNames[result];
+}
+
+// Records histograms relating to how often captive portal detection attempts
+// ended with |result| in a row, and for how long |result| was the last result
+// of a detection attempt. Recorded both on quit and on a new Result.
+//
+// |repeat_count| may be 0 if there were no captive portal checks during
+// a session.
+//
+// |result_duration| is the time between when a captive portal check first
+// returned |result| and when a check returned a different result, or when the
+// CaptivePortalService was shut down.
+void RecordRepeatHistograms(Result result,
+ int repeat_count,
+ base::TimeDelta result_duration) {
+ // Histogram macros can't be used with variable names, since they cache
+ // pointers, so have to use the histogram functions directly.
+
+ // Record number of times the last result was received in a row.
+ base::Histogram* result_repeated_histogram =
+ base::Histogram::FactoryGet(
+ "CaptivePortal.ResultRepeated." +
+ CaptivePortalResultToString(result),
+ 1, // min
+ 100, // max
+ 100, // bucket_count
+ base::Histogram::kUmaTargetedHistogramFlag);
+ result_repeated_histogram->Add(repeat_count);
+
+ if (repeat_count == 0)
+ return;
+
+ // Time between first request that returned |result| and now.
+ base::Histogram* result_duration_histogram =
+ base::Histogram::FactoryTimeGet(
+ "CaptivePortal.ResultDuration." +
+ CaptivePortalResultToString(result),
+ base::TimeDelta::FromSeconds(1), // min
+ base::TimeDelta::FromHours(1), // max
+ 50, // bucket_count
+ base::Histogram::kUmaTargetedHistogramFlag);
+ result_duration_histogram->AddTime(result_duration);
+}
+
+} // namespace
+
+class CaptivePortalService::RecheckBackoffEntry : public net::BackoffEntry {
+ public:
+ explicit RecheckBackoffEntry(CaptivePortalService* captive_portal_service)
+ : net::BackoffEntry(
+ &captive_portal_service->recheck_policy().backoff_policy),
+ captive_portal_service_(captive_portal_service) {
+ }
+
+ private:
+ virtual base::TimeTicks ImplGetTimeNow() const OVERRIDE {
+ return captive_portal_service_->GetCurrentTimeTicks();
+ }
+
+ CaptivePortalService* captive_portal_service_;
+
+ DISALLOW_COPY_AND_ASSIGN(RecheckBackoffEntry);
+};
+
+CaptivePortalService::RecheckPolicy::RecheckPolicy()
+ : initial_backoff_no_portal_ms(600 * 1000),
+ initial_backoff_portal_ms(20 * 1000) {
+ // Receiving a new Result is considered a success. All subsequent requests
+ // that get the same Result are considered "failures", so a value of N
+ // means exponential backoff starts after getting a result N + 2 times:
+ // +1 for the initial success, and +1 because N failures are ignored.
+ //
+ // A value of 6 means to start backoff on the 7th failure, which is the 8th
+ // time the same result is received.
+ backoff_policy.num_errors_to_ignore = 6;
+
+ // It doesn't matter what this is initialized to. It will be overwritten
+ // after the first captive portal detection request.
+ backoff_policy.initial_delay_ms = initial_backoff_no_portal_ms;
+
+ backoff_policy.multiply_factor = 2.0;
+ backoff_policy.jitter_factor = 0.3;
+ backoff_policy.maximum_backoff_ms = 2 * 60 * 1000;
+
+ // -1 means the entry never expires. This doesn't really matter, as the
+ // service never checks for its expiration.
+ backoff_policy.entry_lifetime_ms = -1;
+
+ backoff_policy.always_use_initial_delay = true;
+}
+
+CaptivePortalService::CaptivePortalService(Profile* profile)
+ : profile_(profile),
+ state_(STATE_IDLE),
+ enabled_(false),
+ last_detection_result_(RESULT_INTERNET_CONNECTED),
+ num_checks_with_same_result_(0),
+ test_url_(kDefaultTestURL) {
+ // The order matters here:
+ // |resolve_errors_with_web_service_| must be initialized and |backoff_entry_|
+ // created before the call to UpdateEnabledState.
+ resolve_errors_with_web_service_.Init(
+ prefs::kAlternateErrorPagesEnabled,
+ profile_->GetPrefs(),
+ this);
+ ResetBackoffEntry(last_detection_result_);
+
+ UpdateEnabledState();
+}
+
+CaptivePortalService::~CaptivePortalService() {
+}
+
+void CaptivePortalService::DetectCaptivePortal() {
+ DCHECK(CalledOnValidThread());
+
+ // If a request is pending or running, do nothing.
+ if (state_ == STATE_CHECKING_FOR_PORTAL || state_ == STATE_TIMER_RUNNING)
+ return;
+
+ base::TimeDelta time_until_next_check = backoff_entry_->GetTimeUntilRelease();
+
+ // Start asynchronously.
+ state_ = STATE_TIMER_RUNNING;
+ check_captive_portal_timer_.Start(
+ FROM_HERE,
+ time_until_next_check,
+ this,
+ &CaptivePortalService::DetectCaptivePortalInternal);
+}
+
+void CaptivePortalService::DetectCaptivePortalInternal() {
+ DCHECK(CalledOnValidThread());
+ DCHECK(state_ == STATE_TIMER_RUNNING || state_ == STATE_IDLE);
+ DCHECK(!FetchingURL());
+ DCHECK(!TimerRunning());
+
+ state_ = STATE_CHECKING_FOR_PORTAL;
+
+ // When not enabled, just claim there's an Internet connection.
+ if (!enabled_) {
+ // Count this as a success, so the backoff entry won't apply exponential
+ // backoff, but will apply the standard delay.
+ backoff_entry_->InformOfRequest(true);
+ OnResult(RESULT_INTERNET_CONNECTED);
+ return;
+ }
+
+ // The first 0 means this can use a TestURLFetcherFactory in unit tests.
+ url_fetcher_.reset(content::URLFetcher::Create(0,
+ test_url_,
+ net::URLFetcher::GET,
+ this));
+ url_fetcher_->SetAutomaticallyRetryOn5xx(false);
+ url_fetcher_->SetRequestContext(profile_->GetRequestContext());
+ // Can't safely use net::LOAD_DISABLE_CERT_REVOCATION_CHECKING here,
+ // since then the connection may be reused without checking the cert.
+ url_fetcher_->SetLoadFlags(
+ net::LOAD_BYPASS_CACHE |
+ net::LOAD_DO_NOT_PROMPT_FOR_LOGIN |
+ net::LOAD_DO_NOT_SAVE_COOKIES |
+ net::LOAD_DO_NOT_SEND_COOKIES |
+ net::LOAD_DO_NOT_SEND_AUTH_DATA);
+ url_fetcher_->Start();
+}
+
+void CaptivePortalService::OnURLFetchComplete(const net::URLFetcher* source) {
+ DCHECK(CalledOnValidThread());
+ DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_);
+ DCHECK(FetchingURL());
+ DCHECK(!TimerRunning());
+ DCHECK_EQ(url_fetcher_.get(), source);
+ DCHECK(enabled_);
+
+ base::TimeTicks now = GetCurrentTimeTicks();
+
+ base::TimeDelta retry_after_delta;
+ Result new_result = GetCaptivePortalResultFromResponse(source,
+ &retry_after_delta);
+ url_fetcher_.reset();
+
+ // Record histograms.
+ UMA_HISTOGRAM_ENUMERATION("CaptivePortal.DetectResult",
+ new_result,
+ RESULT_COUNT);
+
+ // If this isn't the first captive portal result, record stats.
+ if (!last_check_time_.is_null()) {
+ UMA_HISTOGRAM_LONG_TIMES("CaptivePortal.TimeBetweenChecks",
+ now - last_check_time_);
+
+ if (last_detection_result_ != new_result) {
+ // If the last result was different from the result of the latest test,
+ // record histograms about the previous period over which the result was
+ // the same.
+ RecordRepeatHistograms(last_detection_result_,
+ num_checks_with_same_result_,
+ now - first_check_time_with_same_result_);
+ }
+ }
+
+ if (last_check_time_.is_null() || new_result != last_detection_result_) {
+ first_check_time_with_same_result_ = now;
+ num_checks_with_same_result_ = 1;
+
+ // Reset the backoff entry both to update the default time and clear
+ // previous failures.
+ ResetBackoffEntry(new_result);
+
+ backoff_entry_->SetCustomReleaseTime(now + retry_after_delta);
+ backoff_entry_->InformOfRequest(true);
+ } else {
+ DCHECK_LE(1, num_checks_with_same_result_);
+ ++num_checks_with_same_result_;
+
+ // Requests that have the same Result as the last one are considered
+ // "failures", to trigger backoff.
+ backoff_entry_->InformOfRequest(false);
+ }
+
+ last_check_time_ = now;
+
+ OnResult(new_result);
+}
+
+void CaptivePortalService::Observe(
+ int type,
+ const content::NotificationSource& source,
+ const content::NotificationDetails& details) {
+ DCHECK(CalledOnValidThread());
+ DCHECK_EQ(chrome::NOTIFICATION_PREF_CHANGED, type);
+ DCHECK_EQ(std::string(prefs::kAlternateErrorPagesEnabled),
+ *content::Details<std::string>(details).ptr());
+ UpdateEnabledState();
+}
+
+void CaptivePortalService::Shutdown() {
+ DCHECK(CalledOnValidThread());
+ if (enabled_) {
+ RecordRepeatHistograms(
+ last_detection_result_,
+ num_checks_with_same_result_,
+ GetCurrentTimeTicks() - first_check_time_with_same_result_);
+ }
+}
+
+void CaptivePortalService::OnResult(Result result) {
+ DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_);
+ state_ = STATE_IDLE;
+
+ Results results;
+ results.previous_result = last_detection_result_;
+ results.result = result;
+ last_detection_result_ = result;
+
+ content::NotificationService::current()->Notify(
+ chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT,
+ content::Source<Profile>(profile_),
+ content::Details<Results>(&results));
+}
+
+void CaptivePortalService::ResetBackoffEntry(Result result) {
+ if (!enabled_ || result == RESULT_BEHIND_CAPTIVE_PORTAL) {
+ // Use the shorter time when the captive portal service is not enabled, or
+ // behind a captive portal.
+ recheck_policy_.backoff_policy.initial_delay_ms =
+ recheck_policy_.initial_backoff_portal_ms;
+ } else {
+ recheck_policy_.backoff_policy.initial_delay_ms =
+ recheck_policy_.initial_backoff_no_portal_ms;
+ }
+
+ backoff_entry_.reset(new RecheckBackoffEntry(this));
+}
+
+void CaptivePortalService::UpdateEnabledState() {
+ bool enabled_before = enabled_;
+ enabled_ = resolve_errors_with_web_service_.GetValue() &&
+ CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kCaptivePortalDetection);
+ if (enabled_before == enabled_)
+ return;
+
+ // Clear data used for histograms.
+ num_checks_with_same_result_ = 0;
+ first_check_time_with_same_result_ = base::TimeTicks();
+ last_check_time_ = base::TimeTicks();
+
+ ResetBackoffEntry(last_detection_result_);
+
+ if (state_ == STATE_CHECKING_FOR_PORTAL || state_ == STATE_TIMER_RUNNING) {
+ // If a captive portal check was running or pending, stop the check or the
+ // timer.
+ url_fetcher_.reset();
+ check_captive_portal_timer_.Stop();
+ state_ = STATE_IDLE;
+
+ // Since a captive portal request was queued or running, something may be
+ // expecting to receive a captive portal result.
+ DetectCaptivePortal();
+ }
+}
+
+// Takes a content::URLFetcher that has finished trying to retrieve the test
+// URL, and returns a CaptivePortalService::Result based on its result.
+Result CaptivePortalService::GetCaptivePortalResultFromResponse(
+ const net::URLFetcher* url_fetcher,
+ base::TimeDelta* retry_after) const {
+ DCHECK(retry_after);
+ DCHECK(!url_fetcher->GetStatus().is_io_pending());
+
+ *retry_after = base::TimeDelta();
+
+ // If there's a network error of some sort when fetching a file via HTTP,
+ // there may be a networking problem, rather than a captive portal.
+ // TODO(mmenke): Consider special handling for redirects that end up at
+ // errors, especially SSL certificate errors.
+ if (url_fetcher->GetStatus().status() != net::URLRequestStatus::SUCCESS)
+ return RESULT_NO_RESPONSE;
+
+ // In the case of 503 errors, look for the Retry-After header.
+ int response_code = url_fetcher->GetResponseCode();
+ if (response_code == 503) {
+ net::HttpResponseHeaders* headers = url_fetcher->GetResponseHeaders();
+ std::string retry_after_string;
+
+ // If there's no Retry-After header, nothing else to do.
+ if (!headers->EnumerateHeader(NULL, "Retry-After", &retry_after_string))
+ return RESULT_NO_RESPONSE;
+
+ // Otherwise, try parsing it as an integer (seconds) or as an HTTP date.
+ int seconds;
+ base::Time full_date;
+ if (base::StringToInt(retry_after_string, &seconds)) {
+ *retry_after = base::TimeDelta::FromSeconds(seconds);
+ } else if (headers->GetTimeValuedHeader("Retry-After", &full_date)) {
+ base::Time now = GetCurrentTime();
+ if (full_date > now)
+ *retry_after = full_date - now;
+ }
+ return RESULT_NO_RESPONSE;
+ }
+
+ // Non-2xx/3xx HTTP responses may also indicate server errors.
+ if (response_code >= 400 || response_code < 200)
+ return RESULT_NO_RESPONSE;
+
+ // A 204 response code indicates there's no captive portal.
+ if (response_code == 204)
+ return RESULT_INTERNET_CONNECTED;
+
+ // Otherwise, assume it's a captive portal.
+ return RESULT_BEHIND_CAPTIVE_PORTAL;
+}
+
+base::Time CaptivePortalService::GetCurrentTime() const {
+ return base::Time::Now();
+}
+
+base::TimeTicks CaptivePortalService::GetCurrentTimeTicks() const {
+ return base::TimeTicks::Now();
+}
+
+bool CaptivePortalService::FetchingURL() const {
+ return url_fetcher_.get() != NULL;
+}
+
+bool CaptivePortalService::TimerRunning() const {
+ return check_captive_portal_timer_.IsRunning();
+}
+
+} // namespace captive_portal
diff --git a/chrome/browser/captive_portal/captive_portal_service.h b/chrome/browser/captive_portal/captive_portal_service.h
new file mode 100644
index 0000000..f211afe
--- /dev/null
+++ b/chrome/browser/captive_portal/captive_portal_service.h
@@ -0,0 +1,217 @@
+// 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_SERVICE_H_
+#define CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_SERVICE_H_
+#pragma once
+
+#include "base/basictypes.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/threading/non_thread_safe.h"
+#include "base/time.h"
+#include "base/timer.h"
+#include "chrome/browser/prefs/pref_member.h"
+#include "chrome/browser/profiles/profile_keyed_service.h"
+#include "content/public/browser/notification_observer.h"
+#include "content/public/common/url_fetcher_delegate.h"
+#include "googleurl/src/gurl.h"
+#include "net/base/backoff_entry.h"
+
+class Profile;
+
+namespace net {
+class URLFetcher;
+}
+
+namespace captive_portal {
+
+// Possible results of an attempt to detect a captive portal.
+enum Result {
+ // There's a confirmed connection to the Internet.
+ RESULT_INTERNET_CONNECTED,
+ // The URL request received a network or HTTP error, or a non-HTTP response.
+ RESULT_NO_RESPONSE,
+ // The URL request apparently encountered a captive portal. It received a
+ // a valid HTTP response with a 2xx or 3xx status code, other than a 204.
+ RESULT_BEHIND_CAPTIVE_PORTAL,
+ RESULT_COUNT
+};
+
+// Service that checks for captive portals when queried, and sends a
+// NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT with the Profile as the source and
+// a CaptivePortalService::Results as the details.
+//
+// Captive portal checks are rate-limited. The CaptivePortalService may only
+// be accessed on the UI thread.
+// Design doc: https://docs.google.com/document/d/1k-gP2sswzYNvryu9NcgN7q5XrsMlUdlUdoW9WRaEmfM/edit
+class CaptivePortalService : public ProfileKeyedService,
+ public content::URLFetcherDelegate,
+ public content::NotificationObserver,
+ public base::NonThreadSafe {
+ public:
+ // The details sent via a NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT.
+ struct Results {
+ // The result of the second most recent captive portal check.
+ Result previous_result;
+ // The result of the most recent captive portal check.
+ Result result;
+ };
+
+ explicit CaptivePortalService(Profile* profile);
+ virtual ~CaptivePortalService();
+
+ // Triggers a check for a captive portal. If there's already a check in
+ // progress, does nothing. Throttles the rate at which requests are sent.
+ // Always sends the result notification asynchronously.
+ void DetectCaptivePortal();
+
+ // Returns the URL used for captive portal testing. When a captive portal is
+ // detected, this URL will take us to the captive portal landing page.
+ const GURL& test_url() const { return test_url_; }
+
+ // Result of the most recent captive portal check.
+ Result last_detection_result() const { return last_detection_result_; }
+
+ // Whether or not the CaptivePortalService is enabled. When disabled, all
+ // checks return INTERNET_CONNECTED.
+ bool enabled() const { return enabled_; }
+
+ private:
+ friend class CaptivePortalServiceTest;
+
+ // Subclass of BackoffEntry that uses the CaptivePortalService's
+ // GetCurrentTime function, for unit testing.
+ class RecheckBackoffEntry;
+
+ enum State {
+ // No check is running or pending.
+ STATE_IDLE,
+ // The timer to check for a captive portal is running.
+ STATE_TIMER_RUNNING,
+ // There's an outstanding HTTP request to check for a captive portal.
+ STATE_CHECKING_FOR_PORTAL,
+ };
+
+ // Contains all the information about the minimum time allowed between two
+ // consecutive captive portal checks.
+ struct RecheckPolicy {
+ // Constructor initializes all values to defaults.
+ RecheckPolicy();
+
+ // The minimum amount of time between two captive portal checks, when the
+ // last check found no captive portal.
+ int initial_backoff_no_portal_ms;
+
+ // The minimum amount of time between two captive portal checks, when the
+ // last check found a captive portal. This is expected to be less than
+ // |initial_backoff_no_portal_ms|. Also used when the service is disabled.
+ int initial_backoff_portal_ms;
+
+ net::BackoffEntry::Policy backoff_policy;
+ };
+
+ // Initiates a captive portal check, without any throttling. If the service
+ // is disabled, just acts like there's an Internet connection.
+ void DetectCaptivePortalInternal();
+
+ // content::URLFetcherDelegate:
+ virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE;
+
+ // content::NotificationObserver:
+ virtual void Observe(
+ int type,
+ const content::NotificationSource& source,
+ const content::NotificationDetails& details) OVERRIDE;
+
+ // ProfileKeyedService:
+ virtual void Shutdown() OVERRIDE;
+
+ // Called when a captive portal check completes. Passes the result to all
+ // observers.
+ void OnResult(Result result);
+
+ // Updates BackoffEntry::Policy and creates a new BackoffEntry, which
+ // resets the count used for throttling.
+ void ResetBackoffEntry(Result result);
+
+ // Updates |enabled_| based on command line flags and Profile preferences,
+ // and sets |state_| to STATE_NONE if it's false.
+ // TODO(mmenke): Figure out on which platforms, if any, should not use
+ // automatic captive portal detection. Currently it's enabled
+ // on all platforms, though this code is not compiled on
+ // Android, since it lacks the Browser class.
+ void UpdateEnabledState();
+
+ // Takes a content::URLFetcher that has finished trying to retrieve the test
+ // URL, and returns a captive_portal::Result based on its result.
+ // If the response is a 503 with a Retry-After header, |retry_after| is
+ // populated accordingly. Otherwise, it's set to base::TimeDelta().
+ Result GetCaptivePortalResultFromResponse(const net::URLFetcher* url_fetcher,
+ base::TimeDelta* retry_after) const;
+
+ // Returns the current time. Used only when determining time until a
+ // Retry-After date. Overridden by unit tests.
+ virtual base::Time GetCurrentTime() const;
+
+ // Returns the current TimeTicks. Overridden by unit tests.
+ virtual base::TimeTicks GetCurrentTimeTicks() const;
+
+ // Returns true if a captive portal check is currently running.
+ bool FetchingURL() const;
+
+ // Returns true if the timer to try and detect a captive portal is running.
+ bool TimerRunning() const;
+
+ State state() const { return state_; }
+
+ RecheckPolicy& recheck_policy() { return recheck_policy_; }
+
+ // The profile that owns this CaptivePortalService.
+ Profile* profile_;
+
+ State state_;
+
+ // True if the service is enabled. When not enabled, all checks will return
+ // RESULT_INTERNET_CONNECTED.
+ bool enabled_;
+
+ // The result of the most recent captive portal check.
+ Result last_detection_result_;
+
+ // Number of sequential checks with the same captive portal result.
+ int num_checks_with_same_result_;
+
+ // Time when |last_detection_result_| was first received.
+ base::TimeTicks first_check_time_with_same_result_;
+
+ // Time the last captive portal check completed.
+ base::TimeTicks last_check_time_;
+
+ // Policy for throttling portal checks.
+ RecheckPolicy recheck_policy_;
+
+ // Implements behavior needed by |recheck_policy_|. Whenever there's a new
+ // captive_portal::Result, BackoffEntry::Policy is updated and
+ // |backoff_entry_| is recreated. Each check that returns the same Result
+ // is considered a "failure", to trigger throttling.
+ scoped_ptr<net::BackoffEntry> backoff_entry_;
+
+ scoped_ptr<net::URLFetcher> url_fetcher_;
+
+ // URL that returns a 204 response code when connected to the Internet.
+ GURL test_url_;
+
+ // The pref member for whether navigation errors should be resolved with a web
+ // service. Actually called "alternate_error_pages", since it's also used for
+ // the Link Doctor.
+ BooleanPrefMember resolve_errors_with_web_service_;
+
+ base::OneShotTimer<CaptivePortalService> check_captive_portal_timer_;
+
+ DISALLOW_COPY_AND_ASSIGN(CaptivePortalService);
+};
+
+} // namespace captive_portal
+
+#endif // CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_SERVICE_H_
diff --git a/chrome/browser/captive_portal/captive_portal_service_factory.cc b/chrome/browser/captive_portal/captive_portal_service_factory.cc
new file mode 100644
index 0000000..aad2672
--- /dev/null
+++ b/chrome/browser/captive_portal/captive_portal_service_factory.cc
@@ -0,0 +1,42 @@
+// 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_service_factory.h"
+
+#include "chrome/browser/captive_portal/captive_portal_service.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/profiles/profile_dependency_manager.h"
+
+namespace captive_portal {
+
+// static
+CaptivePortalService* CaptivePortalServiceFactory::GetForProfile(
+ Profile* profile) {
+ return static_cast<CaptivePortalService*>(
+ GetInstance()->GetServiceForProfile(profile, true));
+}
+
+// static
+CaptivePortalServiceFactory* CaptivePortalServiceFactory::GetInstance() {
+ return Singleton<CaptivePortalServiceFactory>::get();
+}
+
+CaptivePortalServiceFactory::CaptivePortalServiceFactory()
+ : ProfileKeyedServiceFactory("CaptivePortalService",
+ ProfileDependencyManager::GetInstance()) {
+}
+
+CaptivePortalServiceFactory::~CaptivePortalServiceFactory() {
+}
+
+ProfileKeyedService* CaptivePortalServiceFactory::BuildServiceInstanceFor(
+ Profile* profile) const {
+ return new CaptivePortalService(profile);
+}
+
+bool CaptivePortalServiceFactory::ServiceHasOwnInstanceInIncognito() {
+ return true;
+}
+
+} // namespace captive_portal
diff --git a/chrome/browser/captive_portal/captive_portal_service_factory.h b/chrome/browser/captive_portal/captive_portal_service_factory.h
new file mode 100644
index 0000000..46e2ce42
--- /dev/null
+++ b/chrome/browser/captive_portal/captive_portal_service_factory.h
@@ -0,0 +1,49 @@
+// 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_SERVICE_FACTORY_H_
+#define CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_SERVICE_FACTORY_H_
+#pragma once
+
+#include "base/basictypes.h"
+#include "base/compiler_specific.h"
+#include "base/memory/singleton.h"
+#include "chrome/browser/profiles/profile_keyed_service_factory.h"
+
+class Profile;
+
+namespace captive_portal {
+
+class CaptivePortalService;
+
+// Singleton that owns all CaptivePortalServices and associates them with
+// Profiles. Listens for the Profile's destruction notification and cleans up
+// the associated CaptivePortalService. Incognito profiles have their own
+// CaptivePortalService.
+class CaptivePortalServiceFactory : public ProfileKeyedServiceFactory {
+ public:
+ // Returns the CaptivePortalService for |profile|.
+ static CaptivePortalService* GetForProfile(Profile* profile);
+
+ static CaptivePortalServiceFactory* GetInstance();
+
+ private:
+ friend class CaptivePortalBrowserTest;
+ friend class CaptivePortalServiceTest;
+ friend struct DefaultSingletonTraits<CaptivePortalServiceFactory>;
+
+ CaptivePortalServiceFactory();
+ virtual ~CaptivePortalServiceFactory();
+
+ // ProfileKeyedServiceFactory:
+ virtual ProfileKeyedService* BuildServiceInstanceFor(
+ Profile* profile) const OVERRIDE;
+ virtual bool ServiceHasOwnInstanceInIncognito() OVERRIDE;
+
+ DISALLOW_COPY_AND_ASSIGN(CaptivePortalServiceFactory);
+};
+
+} // namespace captive_portal
+
+#endif // CHROME_BROWSER_CAPTIVE_PORTAL_CAPTIVE_PORTAL_SERVICE_FACTORY_H_
diff --git a/chrome/browser/captive_portal/captive_portal_service_unittest.cc b/chrome/browser/captive_portal/captive_portal_service_unittest.cc
new file mode 100644
index 0000000..de9953f
--- /dev/null
+++ b/chrome/browser/captive_portal/captive_portal_service_unittest.cc
@@ -0,0 +1,623 @@
+// 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_service.h"
+
+#include "base/bind.h"
+#include "base/command_line.h"
+#include "base/message_loop.h"
+#include "base/test/test_timeouts.h"
+#include "chrome/browser/prefs/pref_service.h"
+#include "chrome/common/chrome_notification_types.h"
+#include "chrome/common/chrome_switches.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/testing_profile.h"
+#include "chrome/test/base/ui_test_utils.h"
+#include "content/public/browser/notification_details.h"
+#include "content/public/browser/notification_observer.h"
+#include "content/public/browser/notification_registrar.h"
+#include "content/public/browser/notification_source.h"
+#include "content/test/test_url_fetcher_factory.h"
+#include "net/base/net_errors.h"
+#include "net/http/http_response_headers.h"
+#include "net/http/http_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace captive_portal {
+
+namespace {
+
+// A short amount of time that some tests wait for.
+const int kShortTimeMs = 10;
+
+scoped_refptr<net::HttpResponseHeaders> CreateResponseHeaders(
+ const std::string& response_headers) {
+ std::string raw_headers =
+ net::HttpUtil::AssembleRawHeaders(response_headers.c_str(),
+ response_headers.length());
+ return new net::HttpResponseHeaders(raw_headers);
+}
+
+// Allows setting the time, for testing timers.
+class TestCaptivePortalService : public CaptivePortalService {
+ public:
+ // The initial time does not matter, except it must not be NULL.
+ explicit TestCaptivePortalService(Profile* profile)
+ : CaptivePortalService(profile),
+ time_ticks_(base::TimeTicks::Now()),
+ time_(base::Time::Now()) {
+ }
+
+ virtual ~TestCaptivePortalService() {}
+
+ void AdvanceTime(base::TimeDelta delta) {
+ time_ticks_ += delta;
+ time_ += delta;
+ }
+
+ // CaptivePortalService:
+ virtual base::TimeTicks GetCurrentTimeTicks() const OVERRIDE {
+ return time_ticks_;
+ }
+
+ virtual base::Time GetCurrentTime() const OVERRIDE {
+ return time_;
+ }
+
+ void set_time(base::Time time) {
+ time_= time;
+ }
+
+ private:
+ base::TimeTicks time_ticks_;
+
+ // Not necessarily consistent with |time_ticks_|. Used solely with
+ // Retry-After dates.
+ base::Time time_;
+
+ DISALLOW_COPY_AND_ASSIGN(TestCaptivePortalService);
+};
+
+// An observer watches the CaptivePortalService. It tracks the last
+// received result and the total number of received results.
+class CaptivePortalObserver : public content::NotificationObserver {
+ public:
+ CaptivePortalObserver(Profile* profile,
+ CaptivePortalService* captive_portal_service)
+ : captive_portal_result_(
+ captive_portal_service->last_detection_result()),
+ num_results_received_(0),
+ profile_(profile),
+ captive_portal_service_(captive_portal_service) {
+ registrar_.Add(this,
+ chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT,
+ content::Source<Profile>(profile_));
+ }
+
+ Result captive_portal_result() const { return captive_portal_result_; }
+
+ int num_results_received() const { return num_results_received_; }
+
+ private:
+ void 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_;
+ }
+
+ Result captive_portal_result_;
+ int num_results_received_;
+
+ Profile* profile_;
+ CaptivePortalService* captive_portal_service_;
+
+ content::NotificationRegistrar registrar_;
+
+ DISALLOW_COPY_AND_ASSIGN(CaptivePortalObserver);
+};
+
+} // namespace
+
+class CaptivePortalServiceTest : public testing::Test {
+ public:
+ CaptivePortalServiceTest() {}
+
+ virtual ~CaptivePortalServiceTest() {}
+
+ void Initialize(bool enable_on_command_line) {
+ if (enable_on_command_line) {
+ CommandLine::ForCurrentProcess()->AppendSwitch(
+ switches::kCaptivePortalDetection);
+ }
+
+ profile_.reset(new TestingProfile());
+ service_.reset(new TestCaptivePortalService(profile_.get()));
+ scoped_ptr<TestCaptivePortalService> service_;
+
+ // Use no delays for most tests.
+ set_initial_backoff_no_portal(base::TimeDelta());
+ set_initial_backoff_portal(base::TimeDelta());
+
+ // Disable jitter, so can check exact values.
+ set_jitter_factor(0.0);
+
+ // These values make checking exponential backoff easier.
+ set_multiply_factor(2.0);
+ set_maximum_backoff(base::TimeDelta::FromSeconds(1600));
+
+ // This means backoff starts after the first "failure", which is the second
+ // captive portal test in a row that ends up with the same result.
+ set_num_errors_to_ignore(0);
+
+ EnableCaptivePortalDetection(true);
+ }
+
+ // Sets the captive portal checking preference.
+ void EnableCaptivePortalDetection(bool enabled) {
+ profile()->GetPrefs()->SetBoolean(prefs::kAlternateErrorPagesEnabled,
+ enabled);
+ }
+
+ // Calls the corresponding CaptivePortalService function.
+ void OnURLFetchComplete(content::URLFetcher* fetcher) {
+ service()->OnURLFetchComplete(fetcher);
+ }
+
+ // Triggers a captive portal check, then simulates the URL request
+ // returning with the specified |net_error| and |status_code|. If |net_error|
+ // is not OK, |status_code| is ignored. Expects the CaptivePortalService to
+ // return |expected_result|.
+ //
+ // |expected_delay_secs| is the expected value of GetTimeUntilNextRequest().
+ // The function makes sure the value is as expected, and then simulates
+ // waiting for that period of time before running the test.
+ //
+ // If |response_headers| is non-NULL, the response will use it as headers
+ // for the simulate URL request. It must use single linefeeds as line breaks.
+ void RunTest(Result expected_result,
+ int net_error,
+ int status_code,
+ int expected_delay_secs,
+ const char* response_headers) {
+ base::TimeDelta expected_delay =
+ base::TimeDelta::FromSeconds(expected_delay_secs);
+
+ ASSERT_EQ(CaptivePortalService::STATE_IDLE, service()->state());
+ ASSERT_EQ(expected_delay, GetTimeUntilNextRequest());
+
+ service()->AdvanceTime(expected_delay);
+ ASSERT_EQ(base::TimeDelta(), GetTimeUntilNextRequest());
+
+ CaptivePortalObserver observer(profile(), service());
+ TestURLFetcherFactory factory;
+ service()->DetectCaptivePortal();
+
+ EXPECT_EQ(CaptivePortalService::STATE_TIMER_RUNNING, service()->state());
+ EXPECT_FALSE(FetchingURL());
+ ASSERT_TRUE(TimerRunning());
+
+ MessageLoop::current()->RunAllPending();
+ EXPECT_EQ(CaptivePortalService::STATE_CHECKING_FOR_PORTAL,
+ service()->state());
+ ASSERT_TRUE(FetchingURL());
+ EXPECT_FALSE(TimerRunning());
+
+ TestURLFetcher* fetcher = factory.GetFetcherByID(0);
+ if (net_error != net::OK) {
+ EXPECT_FALSE(response_headers);
+ fetcher->set_status(net::URLRequestStatus(net::URLRequestStatus::FAILED,
+ net_error));
+ } else {
+ fetcher->set_response_code(status_code);
+ if (response_headers) {
+ scoped_refptr<net::HttpResponseHeaders> headers(
+ CreateResponseHeaders(response_headers));
+ // Sanity check.
+ EXPECT_EQ(status_code, headers->response_code());
+ fetcher->set_response_headers(headers);
+ }
+ }
+
+ OnURLFetchComplete(fetcher);
+
+ EXPECT_FALSE(FetchingURL());
+ EXPECT_FALSE(TimerRunning());
+ EXPECT_EQ(1, observer.num_results_received());
+ EXPECT_EQ(expected_result, observer.captive_portal_result());
+ }
+
+ // Runs a test when the captive portal service is disabled.
+ void RunDisabledTest(int expected_delay_secs) {
+ base::TimeDelta expected_delay =
+ base::TimeDelta::FromSeconds(expected_delay_secs);
+
+ ASSERT_EQ(CaptivePortalService::STATE_IDLE, service()->state());
+ ASSERT_EQ(expected_delay, GetTimeUntilNextRequest());
+
+ service()->AdvanceTime(expected_delay);
+ ASSERT_EQ(base::TimeDelta(), GetTimeUntilNextRequest());
+
+ CaptivePortalObserver observer(profile(), service());
+ service()->DetectCaptivePortal();
+
+ EXPECT_EQ(CaptivePortalService::STATE_TIMER_RUNNING, service()->state());
+ EXPECT_FALSE(FetchingURL());
+ ASSERT_TRUE(TimerRunning());
+
+ MessageLoop::current()->RunAllPending();
+ EXPECT_FALSE(FetchingURL());
+ EXPECT_FALSE(TimerRunning());
+ EXPECT_EQ(1, observer.num_results_received());
+ EXPECT_EQ(RESULT_INTERNET_CONNECTED, observer.captive_portal_result());
+ }
+
+ // Tests exponential backoff. Prior to calling, the relevant recheck settings
+ // must be set to have a minimum time of 100 seconds, with 2 checks before
+ // starting exponential backoff.
+ void RunBackoffTest(Result expected_result, int net_error, int status_code) {
+ RunTest(expected_result, net_error, status_code, 0, NULL);
+ RunTest(expected_result, net_error, status_code, 100, NULL);
+ RunTest(expected_result, net_error, status_code, 200, NULL);
+ RunTest(expected_result, net_error, status_code, 400, NULL);
+ RunTest(expected_result, net_error, status_code, 800, NULL);
+ RunTest(expected_result, net_error, status_code, 1600, NULL);
+ RunTest(expected_result, net_error, status_code, 1600, NULL);
+ }
+
+ bool FetchingURL() {
+ return service()->FetchingURL();
+ }
+
+ bool TimerRunning() {
+ return service()->TimerRunning();
+ }
+
+ base::TimeDelta GetTimeUntilNextRequest() {
+ return service()->backoff_entry_->GetTimeUntilRelease();
+ }
+
+ void set_initial_backoff_no_portal(
+ base::TimeDelta initial_backoff_no_portal) {
+ service()->recheck_policy().initial_backoff_no_portal_ms =
+ initial_backoff_no_portal.InMilliseconds();
+ }
+
+ void set_initial_backoff_portal(base::TimeDelta initial_backoff_portal) {
+ service()->recheck_policy().initial_backoff_portal_ms =
+ initial_backoff_portal.InMilliseconds();
+ }
+
+ void set_maximum_backoff(base::TimeDelta maximum_backoff) {
+ service()->recheck_policy().backoff_policy.maximum_backoff_ms =
+ maximum_backoff.InMilliseconds();
+ }
+
+ void set_num_errors_to_ignore(int num_errors_to_ignore) {
+ service()->recheck_policy().backoff_policy.num_errors_to_ignore =
+ num_errors_to_ignore;
+ }
+
+ void set_multiply_factor(double multiply_factor) {
+ service()->recheck_policy().backoff_policy.multiply_factor =
+ multiply_factor;
+ }
+
+ void set_jitter_factor(double jitter_factor) {
+ service()->recheck_policy().backoff_policy.jitter_factor = jitter_factor;
+ }
+
+ TestingProfile* profile() { return profile_.get(); }
+
+ TestCaptivePortalService* service() { return service_.get(); }
+
+ private:
+ MessageLoop message_loop_;
+
+ // Note that the construction order of these matters.
+ scoped_ptr<TestingProfile> profile_;
+ scoped_ptr<TestCaptivePortalService> service_;
+};
+
+// Test when connected to the Internet and get the expected 204 response.
+TEST_F(CaptivePortalServiceTest, CaptivePortalInternetConnected) {
+ Initialize(true);
+ RunTest(RESULT_INTERNET_CONNECTED, net::OK, 204, 0, NULL);
+}
+
+// Test when there's a connection to the Internet, but the server returns a 5xx
+// error.
+TEST_F(CaptivePortalServiceTest, CaptivePortalServiceDown) {
+ Initialize(true);
+ RunTest(RESULT_NO_RESPONSE, net::OK, 500, 0, NULL);
+}
+
+// Test when there's a network error.
+TEST_F(CaptivePortalServiceTest, CaptivePortalNetError) {
+ Initialize(true);
+ RunTest(RESULT_NO_RESPONSE, net::ERR_TIMED_OUT, -1, 0, NULL);
+}
+
+// Test when behind a captive portal.
+TEST_F(CaptivePortalServiceTest, CaptivePortalBehindCaptivePortal) {
+ Initialize(true);
+ RunTest(RESULT_BEHIND_CAPTIVE_PORTAL, net::OK, 200, 0, NULL);
+}
+
+// Verify that an observer doesn't get messages from the wrong profile.
+TEST_F(CaptivePortalServiceTest, CaptivePortalTwoProfiles) {
+ Initialize(true);
+ TestingProfile profile2;
+ TestCaptivePortalService service2(&profile2);
+ CaptivePortalObserver observer2(&profile2, &service2);
+
+ RunTest(RESULT_INTERNET_CONNECTED, net::OK, 204, 0, NULL);
+ EXPECT_EQ(0, observer2.num_results_received());
+}
+
+// Test receiving two results in a row, with no timeout.
+TEST_F(CaptivePortalServiceTest, CaptivePortalTwoResults) {
+ Initialize(true);
+ CaptivePortalObserver observer(profile(), service());
+
+ RunTest(RESULT_BEHIND_CAPTIVE_PORTAL, net::OK, 200, 0, NULL);
+ EXPECT_EQ(RESULT_BEHIND_CAPTIVE_PORTAL, observer.captive_portal_result());
+ EXPECT_EQ(1, observer.num_results_received());
+
+ RunTest(RESULT_INTERNET_CONNECTED, net::OK, 204, 0, NULL);
+ EXPECT_EQ(RESULT_INTERNET_CONNECTED, observer.captive_portal_result());
+ EXPECT_EQ(2, observer.num_results_received());
+}
+
+// Checks exponential backoff when the Internet is connected.
+TEST_F(CaptivePortalServiceTest, CaptivePortalRecheckInternetConnected) {
+ Initialize(true);
+
+ // This value should have no effect on this test, until the end.
+ set_initial_backoff_portal(base::TimeDelta::FromSeconds(1));
+
+ set_initial_backoff_no_portal(base::TimeDelta::FromSeconds(100));
+ RunBackoffTest(RESULT_INTERNET_CONNECTED, net::OK, 204);
+
+ // Make sure that getting a new result resets the timer.
+ RunTest(RESULT_BEHIND_CAPTIVE_PORTAL, net::OK, 200, 1600, NULL);
+ RunTest(RESULT_BEHIND_CAPTIVE_PORTAL, net::OK, 200, 1, NULL);
+}
+
+// Checks exponential backoff when there's an HTTP error.
+TEST_F(CaptivePortalServiceTest, CaptivePortalRecheckError) {
+ Initialize(true);
+
+ // This value should have no effect on this test.
+ set_initial_backoff_portal(base::TimeDelta::FromDays(1));
+
+ set_initial_backoff_no_portal(base::TimeDelta::FromSeconds(100));
+ RunBackoffTest(RESULT_NO_RESPONSE, net::OK, 500);
+
+ // Make sure that getting a new result resets the timer.
+ RunTest(RESULT_INTERNET_CONNECTED, net::OK, 204, 1600, NULL);
+ RunTest(RESULT_INTERNET_CONNECTED, net::OK, 204, 100, NULL);
+}
+
+// Checks exponential backoff when there's a captive portal.
+TEST_F(CaptivePortalServiceTest, CaptivePortalRecheckBehindPortal) {
+ Initialize(true);
+
+ // This value should have no effect on this test, until the end.
+ set_initial_backoff_no_portal(base::TimeDelta::FromSeconds(250));
+
+ set_initial_backoff_portal(base::TimeDelta::FromSeconds(100));
+ RunBackoffTest(RESULT_BEHIND_CAPTIVE_PORTAL, net::OK, 200);
+
+ // Make sure that getting a new result resets the timer.
+ RunTest(RESULT_INTERNET_CONNECTED, net::OK, 204, 1600, NULL);
+ RunTest(RESULT_INTERNET_CONNECTED, net::OK, 204, 250, NULL);
+}
+
+// Check that everything works as expected when captive portal checking is
+// disabled, including throttling. Then enables it again and runs another test.
+TEST_F(CaptivePortalServiceTest, CaptivePortalPrefDisabled) {
+ Initialize(true);
+
+ // This value should have no effect on this test.
+ set_initial_backoff_no_portal(base::TimeDelta::FromDays(1));
+
+ set_initial_backoff_portal(base::TimeDelta::FromSeconds(100));
+
+ EnableCaptivePortalDetection(false);
+
+ RunDisabledTest(0);
+ for (int i = 0; i < 6; ++i)
+ RunDisabledTest(100);
+
+ EnableCaptivePortalDetection(true);
+
+ RunTest(RESULT_BEHIND_CAPTIVE_PORTAL, net::OK, 200, 0, NULL);
+}
+
+// Check that disabling the captive portal service while a check is running
+// works.
+TEST_F(CaptivePortalServiceTest, CaptivePortalPrefDisabledWhileRunning) {
+ Initialize(true);
+ CaptivePortalObserver observer(profile(), service());
+
+ // Needed to create the URLFetcher, even if it never returns any results.
+ TestURLFetcherFactory factory;
+ service()->DetectCaptivePortal();
+
+ MessageLoop::current()->RunAllPending();
+ EXPECT_TRUE(FetchingURL());
+ EXPECT_FALSE(TimerRunning());
+
+ EnableCaptivePortalDetection(false);
+ EXPECT_FALSE(FetchingURL());
+ EXPECT_TRUE(TimerRunning());
+ EXPECT_EQ(0, observer.num_results_received());
+
+ MessageLoop::current()->RunAllPending();
+
+ EXPECT_FALSE(FetchingURL());
+ EXPECT_FALSE(TimerRunning());
+ EXPECT_EQ(1, observer.num_results_received());
+
+ EXPECT_EQ(RESULT_INTERNET_CONNECTED, observer.captive_portal_result());
+}
+
+// Check that disabling the captive portal service while a check is pending
+// works.
+TEST_F(CaptivePortalServiceTest, CaptivePortalPrefDisabledWhilePending) {
+ Initialize(true);
+ set_initial_backoff_no_portal(base::TimeDelta::FromDays(1));
+
+ // Needed to create the URLFetcher, even if it never returns any results.
+ TestURLFetcherFactory factory;
+
+ CaptivePortalObserver observer(profile(), service());
+ service()->DetectCaptivePortal();
+ EXPECT_FALSE(FetchingURL());
+ EXPECT_TRUE(TimerRunning());
+
+ EnableCaptivePortalDetection(false);
+ EXPECT_FALSE(FetchingURL());
+ EXPECT_TRUE(TimerRunning());
+ EXPECT_EQ(0, observer.num_results_received());
+
+ MessageLoop::current()->RunAllPending();
+
+ EXPECT_FALSE(FetchingURL());
+ EXPECT_FALSE(TimerRunning());
+ EXPECT_EQ(1, observer.num_results_received());
+
+ EXPECT_EQ(RESULT_INTERNET_CONNECTED, observer.captive_portal_result());
+}
+
+// Check that disabling the captive portal service while a check is pending
+// works.
+TEST_F(CaptivePortalServiceTest, CaptivePortalPrefEnabledWhilePending) {
+ Initialize(true);
+
+ EnableCaptivePortalDetection(false);
+ RunDisabledTest(0);
+
+ CaptivePortalObserver observer(profile(), service());
+ service()->DetectCaptivePortal();
+ EXPECT_FALSE(FetchingURL());
+ EXPECT_TRUE(TimerRunning());
+
+ TestURLFetcherFactory factory;
+
+ EnableCaptivePortalDetection(true);
+ EXPECT_FALSE(FetchingURL());
+ EXPECT_TRUE(TimerRunning());
+
+ MessageLoop::current()->RunAllPending();
+ ASSERT_TRUE(FetchingURL());
+ EXPECT_FALSE(TimerRunning());
+
+ TestURLFetcher* fetcher = factory.GetFetcherByID(0);
+ fetcher->set_response_code(200);
+ OnURLFetchComplete(fetcher);
+ EXPECT_FALSE(FetchingURL());
+ EXPECT_FALSE(TimerRunning());
+
+ EXPECT_EQ(1, observer.num_results_received());
+ EXPECT_EQ(RESULT_BEHIND_CAPTIVE_PORTAL, observer.captive_portal_result());
+}
+
+// Checks that disabling with a command line flag works as expected.
+TEST_F(CaptivePortalServiceTest, CaptivePortalDisabledAtCommandLine) {
+ Initialize(false);
+ RunDisabledTest(0);
+}
+
+// Checks that jitter gives us values in the correct range.
+TEST_F(CaptivePortalServiceTest, CaptivePortalJitter) {
+ Initialize(true);
+ set_jitter_factor(0.3);
+ set_initial_backoff_no_portal(base::TimeDelta::FromSeconds(100));
+ RunTest(RESULT_INTERNET_CONNECTED, net::OK, 204, 0, NULL);
+
+ for (int i = 0; i < 50; ++i) {
+ int interval_sec = GetTimeUntilNextRequest().InSeconds();
+ // Allow for roundoff, though shouldn't be necessary.
+ EXPECT_LE(69, interval_sec);
+ EXPECT_LE(interval_sec, 101);
+ }
+}
+
+// Check a Retry-After header that contains a delay in seconds.
+TEST_F(CaptivePortalServiceTest, CaptivePortalRetryAfterSeconds) {
+ Initialize(true);
+ set_initial_backoff_no_portal(base::TimeDelta::FromSeconds(100));
+
+ RunTest(RESULT_NO_RESPONSE,
+ net::OK,
+ 503,
+ 0,
+ "HTTP/1.1 503 OK\nRetry-After: 101\n\n");
+
+ // Run another captive portal check to make sure the time until the next check
+ // is as expected.
+ RunTest(RESULT_INTERNET_CONNECTED, net::OK, 204, 101, NULL);
+ EXPECT_EQ(base::TimeDelta::FromSeconds(100), GetTimeUntilNextRequest());
+}
+
+// Check that the RecheckPolicy is still respected on 503 responses with
+// Retry-After headers.
+TEST_F(CaptivePortalServiceTest, CaptivePortalRetryAfterSecondsTooShort) {
+ Initialize(true);
+ set_initial_backoff_no_portal(base::TimeDelta::FromSeconds(100));
+
+ RunTest(RESULT_NO_RESPONSE,
+ net::OK,
+ 503,
+ 0,
+ "HTTP/1.1 503 OK\nRetry-After: 99\n\n");
+ EXPECT_EQ(base::TimeDelta::FromSeconds(100), GetTimeUntilNextRequest());
+}
+
+// Check a Retry-After header that contains a date.
+TEST_F(CaptivePortalServiceTest, CaptivePortalRetryAfterDate) {
+ Initialize(true);
+ set_initial_backoff_no_portal(base::TimeDelta::FromSeconds(50));
+
+ // base has a function to get a time in the right format from a string, but
+ // not the other way around.
+ base::Time start_time;
+ ASSERT_TRUE(
+ base::Time::FromString("Tue, 17 Apr 2012 18:02:00 GMT", &start_time));
+ service()->set_time(start_time);
+
+ RunTest(RESULT_NO_RESPONSE,
+ net::OK,
+ 503,
+ 0,
+ "HTTP/1.1 503 OK\nRetry-After: Tue, 17 Apr 2012 18:02:51 GMT\n\n");
+ EXPECT_EQ(base::TimeDelta::FromSeconds(51), GetTimeUntilNextRequest());
+}
+
+// Check invalid Retry-After headers are ignored.
+TEST_F(CaptivePortalServiceTest, CaptivePortalRetryAfterInvalid) {
+ Initialize(true);
+ set_initial_backoff_no_portal(base::TimeDelta::FromSeconds(100));
+
+ RunTest(RESULT_NO_RESPONSE,
+ net::OK,
+ 503,
+ 0,
+ "HTTP/1.1 503 OK\nRetry-After: Christmas\n\n");
+ EXPECT_EQ(base::TimeDelta::FromSeconds(100), GetTimeUntilNextRequest());
+}
+
+} // namespace captive_portal
diff --git a/chrome/browser/profiles/profile_dependency_manager.cc b/chrome/browser/profiles/profile_dependency_manager.cc
index 27b0da1..3eb5356 100644
--- a/chrome/browser/profiles/profile_dependency_manager.cc
+++ b/chrome/browser/profiles/profile_dependency_manager.cc
@@ -10,6 +10,7 @@
#include "chrome/browser/autofill/personal_data_manager_factory.h"
#include "chrome/browser/background/background_contents_service_factory.h"
+#include "chrome/browser/captive_portal/captive_portal_service_factory.h"
#include "chrome/browser/content_settings/cookie_settings.h"
#include "chrome/browser/download/download_service_factory.h"
#include "chrome/browser/extensions/api/commands/extension_command_service_factory.h"
@@ -179,6 +180,9 @@ void ProfileDependencyManager::AssertFactoriesBuilt() {
#if defined(ENABLE_BACKGROUND)
BackgroundContentsServiceFactory::GetInstance();
#endif
+#if !defined(OS_ANDROID)
+ captive_portal::CaptivePortalServiceFactory::GetInstance();
+#endif
ChromeURLDataManagerFactory::GetInstance();
#if !defined(OS_ANDROID)
CloudPrintProxyServiceFactory::GetInstance();
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index 036fc1f..6cb7138 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -348,6 +348,10 @@
'browser/feedback/feedback_util.h',
'browser/cancelable_request.cc',
'browser/cancelable_request.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/certificate_manager_model.cc',
'browser/certificate_manager_model.h',
'browser/certificate_viewer.cc',
@@ -4538,6 +4542,7 @@
'browser/upgrade_detector_impl.h',
],
'sources/': [
+ ['exclude', '^browser/captive_portal/'],
['exclude', '^browser/chrome_to_mobile'],
['exclude', '^browser/importer/'],
['exclude', '^browser/printing/'],
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index c6d3137..9b3953d 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -1094,6 +1094,7 @@
'browser/browsing_data_quota_helper_unittest.cc',
'browser/browsing_data_remover_unittest.cc',
'browser/browsing_data_server_bound_cert_helper_unittest.cc',
+ 'browser/captive_portal/captive_portal_service_unittest.cc',
'browser/chrome_browser_application_mac_unittest.mm',
'browser/chrome_browser_main_unittest.cc',
'browser/chrome_page_zoom_unittest.cc',
@@ -2393,6 +2394,7 @@
'test/base/test_browser_window.h',
],
'sources/': [
+ ['exclude', '^browser/captive_portal/'],
['exclude', '^browser/chrome_to_mobile'],
['exclude', '^browser/printing/'],
['exclude', '^browser/tabs/pinned_tab_'],
diff --git a/chrome/common/chrome_notification_types.h b/chrome/common/chrome_notification_types.h
index 6d70c8e..5f377a4 100644
--- a/chrome/common/chrome_notification_types.h
+++ b/chrome/common/chrome_notification_types.h
@@ -967,6 +967,11 @@ enum NotificationType {
// supports instant. The source is not used.
NOTIFICATION_INSTANT_SUPPORT_DETERMINED,
+ // Sent when the CaptivePortalService checks if we're behind a captive portal.
+ // The Source is the Profile the CaptivePortalService belongs to, and the
+ // Details are a Details<CaptivePortalService::CheckResults>.
+ NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT,
+
// Password Store ----------------------------------------------------------
// This notification is sent whenenever login entries stored in the password
// store are changed. The detail of this notification is a list of changes
diff --git a/chrome/common/chrome_switches.cc b/chrome/common/chrome_switches.cc
index c76a53d..183cf68 100644
--- a/chrome/common/chrome_switches.cc
+++ b/chrome/common/chrome_switches.cc
@@ -125,6 +125,10 @@ const char kAutomationClientChannelID[] = "automation-channel";
const char kAutomationReinitializeOnChannelError[] =
"automation-reinitialize-on-channel-error";
+// This enables automatic captive portal checking on certain network errors.
+// If a captive portal is detected, a login tab will be opened.
+const char kCaptivePortalDetection[] = "enable-captive-portal-detection";
+
// How often (in seconds) to check for updates. Should only be used for testing
// purposes.
const char kCheckForUpdateIntervalSec[] = "check-for-update-interval";
diff --git a/chrome/common/chrome_switches.h b/chrome/common/chrome_switches.h
index a1348dd..22f3b44 100644
--- a/chrome/common/chrome_switches.h
+++ b/chrome/common/chrome_switches.h
@@ -50,6 +50,7 @@ extern const char kAuthServerWhitelist[];
extern const char kAutoLaunchAtStartup[];
extern const char kAutomationClientChannelID[];
extern const char kAutomationReinitializeOnChannelError[];
+extern const char kCaptivePortalDetection[];
extern const char kCheckForUpdateIntervalSec[];
extern const char kCheckCloudPrintConnectorPolicy[];
extern const char kChromeFrameShutdownDelay[];