diff options
author | mmenke@chromium.org <mmenke@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-11 18:59:20 +0000 |
---|---|---|
committer | mmenke@chromium.org <mmenke@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-05-11 18:59:20 +0000 |
commit | db92eeb9e9003acddf76ba1237630134be67ae56 (patch) | |
tree | 4a07390c75208a147a5e662cf1a2224d6cfe3b4a | |
parent | 1ec2f92f9dafeacae9050c226d25f89e71aacc60 (diff) | |
download | chromium_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.grd | 6 | ||||
-rw-r--r-- | chrome/browser/about_flags.cc | 7 | ||||
-rw-r--r-- | chrome/browser/captive_portal/OWNERS | 1 | ||||
-rw-r--r-- | chrome/browser/captive_portal/captive_portal_service.cc | 423 | ||||
-rw-r--r-- | chrome/browser/captive_portal/captive_portal_service.h | 217 | ||||
-rw-r--r-- | chrome/browser/captive_portal/captive_portal_service_factory.cc | 42 | ||||
-rw-r--r-- | chrome/browser/captive_portal/captive_portal_service_factory.h | 49 | ||||
-rw-r--r-- | chrome/browser/captive_portal/captive_portal_service_unittest.cc | 623 | ||||
-rw-r--r-- | chrome/browser/profiles/profile_dependency_manager.cc | 4 | ||||
-rw-r--r-- | chrome/chrome_browser.gypi | 5 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 2 | ||||
-rw-r--r-- | chrome/common/chrome_notification_types.h | 5 | ||||
-rw-r--r-- | chrome/common/chrome_switches.cc | 4 | ||||
-rw-r--r-- | chrome/common/chrome_switches.h | 1 |
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[]; |