Home | History | Annotate | Download | only in captive_portal
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/captive_portal/captive_portal_service.h"
      6 
      7 #include "base/bind.h"
      8 #include "base/bind_helpers.h"
      9 #include "base/logging.h"
     10 #include "base/message_loop/message_loop.h"
     11 #include "base/metrics/histogram.h"
     12 #include "base/prefs/pref_service.h"
     13 #include "chrome/browser/chrome_notification_types.h"
     14 #include "chrome/browser/profiles/profile.h"
     15 #include "chrome/common/pref_names.h"
     16 #include "components/captive_portal/captive_portal_types.h"
     17 #include "content/public/browser/notification_service.h"
     18 
     19 #if defined(OS_MACOSX)
     20 #include "base/mac/mac_util.h"
     21 #endif
     22 
     23 #if defined(OS_WIN)
     24 #include "base/win/windows_version.h"
     25 #endif
     26 
     27 using captive_portal::CaptivePortalResult;
     28 
     29 namespace {
     30 
     31 // Make sure this enum is in sync with CaptivePortalDetectionResult enum
     32 // in histograms.xml. This enum is append-only, don't modify existing values.
     33 enum CaptivePortalDetectionResult {
     34   // There's a confirmed connection to the Internet.
     35   DETECTION_RESULT_INTERNET_CONNECTED,
     36   // Received a network or HTTP error, or a non-HTTP response.
     37   DETECTION_RESULT_NO_RESPONSE,
     38   // Encountered a captive portal with a non-HTTPS landing URL.
     39   DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL,
     40   // Received a network or HTTP error with an HTTPS landing URL.
     41   DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL,
     42   // Encountered a captive portal with an HTTPS landing URL.
     43   DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL,
     44   DETECTION_RESULT_COUNT
     45 };
     46 
     47 // Records histograms relating to how often captive portal detection attempts
     48 // ended with |result| in a row, and for how long |result| was the last result
     49 // of a detection attempt.  Recorded both on quit and on a new Result.
     50 //
     51 // |repeat_count| may be 0 if there were no captive portal checks during
     52 // a session.
     53 //
     54 // |result_duration| is the time between when a captive portal check first
     55 // returned |result| and when a check returned a different result, or when the
     56 // CaptivePortalService was shut down.
     57 void RecordRepeatHistograms(CaptivePortalResult result,
     58                             int repeat_count,
     59                             base::TimeDelta result_duration) {
     60   // Histogram macros can't be used with variable names, since they cache
     61   // pointers, so have to use the histogram functions directly.
     62 
     63   // Record number of times the last result was received in a row.
     64   base::HistogramBase* result_repeated_histogram =
     65       base::Histogram::FactoryGet(
     66           "CaptivePortal.ResultRepeated." + CaptivePortalResultToString(result),
     67           1,  // min
     68           100,  // max
     69           100,  // bucket_count
     70           base::Histogram::kUmaTargetedHistogramFlag);
     71   result_repeated_histogram->Add(repeat_count);
     72 
     73   if (repeat_count == 0)
     74     return;
     75 
     76   // Time between first request that returned |result| and now.
     77   base::HistogramBase* result_duration_histogram =
     78       base::Histogram::FactoryTimeGet(
     79           "CaptivePortal.ResultDuration." + CaptivePortalResultToString(result),
     80           base::TimeDelta::FromSeconds(1),  // min
     81           base::TimeDelta::FromHours(1),  // max
     82           50,  // bucket_count
     83           base::Histogram::kUmaTargetedHistogramFlag);
     84   result_duration_histogram->AddTime(result_duration);
     85 }
     86 
     87 int GetHistogramEntryForDetectionResult(
     88     const captive_portal::CaptivePortalDetector::Results& results) {
     89   bool is_https = results.landing_url.SchemeIs("https");
     90   switch (results.result) {
     91     case captive_portal::RESULT_INTERNET_CONNECTED:
     92       return DETECTION_RESULT_INTERNET_CONNECTED;
     93     case captive_portal::RESULT_NO_RESPONSE:
     94       return is_https ?
     95           DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL :
     96           DETECTION_RESULT_NO_RESPONSE;
     97     case captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL:
     98       return is_https ?
     99           DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL :
    100           DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL;
    101     default:
    102       NOTREACHED();
    103       return -1;
    104   }
    105 }
    106 
    107 bool ShouldDeferToNativeCaptivePortalDetection() {
    108   // On Windows 8, defer to the native captive portal detection.  OSX Lion and
    109   // later also have captive portal detection, but experimentally, this code
    110   // works in cases its does not.
    111   //
    112   // TODO(mmenke): Investigate how well Windows 8's captive portal detection
    113   // works.
    114 #if defined(OS_WIN)
    115   return base::win::GetVersion() >= base::win::VERSION_WIN8;
    116 #else
    117   return false;
    118 #endif
    119 }
    120 
    121 }  // namespace
    122 
    123 CaptivePortalService::TestingState CaptivePortalService::testing_state_ =
    124     NOT_TESTING;
    125 
    126 class CaptivePortalService::RecheckBackoffEntry : public net::BackoffEntry {
    127  public:
    128   explicit RecheckBackoffEntry(CaptivePortalService* captive_portal_service)
    129       : net::BackoffEntry(
    130             &captive_portal_service->recheck_policy().backoff_policy),
    131         captive_portal_service_(captive_portal_service) {
    132   }
    133 
    134  private:
    135   virtual base::TimeTicks ImplGetTimeNow() const OVERRIDE {
    136     return captive_portal_service_->GetCurrentTimeTicks();
    137   }
    138 
    139   CaptivePortalService* captive_portal_service_;
    140 
    141   DISALLOW_COPY_AND_ASSIGN(RecheckBackoffEntry);
    142 };
    143 
    144 CaptivePortalService::RecheckPolicy::RecheckPolicy()
    145     : initial_backoff_no_portal_ms(600 * 1000),
    146       initial_backoff_portal_ms(20 * 1000) {
    147   // Receiving a new Result is considered a success.  All subsequent requests
    148   // that get the same Result are considered "failures", so a value of N
    149   // means exponential backoff starts after getting a result N + 2 times:
    150   // +1 for the initial success, and +1 because N failures are ignored.
    151   //
    152   // A value of 6 means to start backoff on the 7th failure, which is the 8th
    153   // time the same result is received.
    154   backoff_policy.num_errors_to_ignore = 6;
    155 
    156   // It doesn't matter what this is initialized to.  It will be overwritten
    157   // after the first captive portal detection request.
    158   backoff_policy.initial_delay_ms = initial_backoff_no_portal_ms;
    159 
    160   backoff_policy.multiply_factor = 2.0;
    161   backoff_policy.jitter_factor = 0.3;
    162   backoff_policy.maximum_backoff_ms = 2 * 60 * 1000;
    163 
    164   // -1 means the entry never expires.  This doesn't really matter, as the
    165   // service never checks for its expiration.
    166   backoff_policy.entry_lifetime_ms = -1;
    167 
    168   backoff_policy.always_use_initial_delay = true;
    169 }
    170 
    171 CaptivePortalService::CaptivePortalService(Profile* profile)
    172     : profile_(profile),
    173       state_(STATE_IDLE),
    174       captive_portal_detector_(profile->GetRequestContext()),
    175       enabled_(false),
    176       last_detection_result_(captive_portal::RESULT_INTERNET_CONNECTED),
    177       num_checks_with_same_result_(0),
    178       test_url_(captive_portal::CaptivePortalDetector::kDefaultURL) {
    179   // The order matters here:
    180   // |resolve_errors_with_web_service_| must be initialized and |backoff_entry_|
    181   // created before the call to UpdateEnabledState.
    182   resolve_errors_with_web_service_.Init(
    183       prefs::kAlternateErrorPagesEnabled,
    184       profile_->GetPrefs(),
    185       base::Bind(&CaptivePortalService::UpdateEnabledState,
    186                  base::Unretained(this)));
    187   ResetBackoffEntry(last_detection_result_);
    188 
    189   UpdateEnabledState();
    190 }
    191 
    192 CaptivePortalService::~CaptivePortalService() {
    193 }
    194 
    195 void CaptivePortalService::DetectCaptivePortal() {
    196   DCHECK(CalledOnValidThread());
    197 
    198   // If a request is pending or running, do nothing.
    199   if (state_ == STATE_CHECKING_FOR_PORTAL || state_ == STATE_TIMER_RUNNING)
    200     return;
    201 
    202   base::TimeDelta time_until_next_check = backoff_entry_->GetTimeUntilRelease();
    203 
    204   // Start asynchronously.
    205   state_ = STATE_TIMER_RUNNING;
    206   check_captive_portal_timer_.Start(
    207       FROM_HERE,
    208       time_until_next_check,
    209       this,
    210       &CaptivePortalService::DetectCaptivePortalInternal);
    211 }
    212 
    213 void CaptivePortalService::DetectCaptivePortalInternal() {
    214   DCHECK(CalledOnValidThread());
    215   DCHECK(state_ == STATE_TIMER_RUNNING || state_ == STATE_IDLE);
    216   DCHECK(!TimerRunning());
    217 
    218   state_ = STATE_CHECKING_FOR_PORTAL;
    219 
    220   // When not enabled, just claim there's an Internet connection.
    221   if (!enabled_) {
    222     // Count this as a success, so the backoff entry won't apply exponential
    223     // backoff, but will apply the standard delay.
    224     backoff_entry_->InformOfRequest(true);
    225     OnResult(captive_portal::RESULT_INTERNET_CONNECTED);
    226     return;
    227   }
    228 
    229   captive_portal_detector_.DetectCaptivePortal(
    230       test_url_, base::Bind(
    231           &CaptivePortalService::OnPortalDetectionCompleted,
    232           base::Unretained(this)));
    233 }
    234 
    235 void CaptivePortalService::OnPortalDetectionCompleted(
    236     const captive_portal::CaptivePortalDetector::Results& results) {
    237   DCHECK(CalledOnValidThread());
    238   DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_);
    239   DCHECK(!TimerRunning());
    240   DCHECK(enabled_);
    241 
    242   CaptivePortalResult result = results.result;
    243   const base::TimeDelta& retry_after_delta = results.retry_after_delta;
    244   base::TimeTicks now = GetCurrentTimeTicks();
    245 
    246   // Record histograms.
    247   UMA_HISTOGRAM_ENUMERATION("CaptivePortal.DetectResult",
    248                             GetHistogramEntryForDetectionResult(results),
    249                             DETECTION_RESULT_COUNT);
    250 
    251   // If this isn't the first captive portal result, record stats.
    252   if (!last_check_time_.is_null()) {
    253     UMA_HISTOGRAM_LONG_TIMES("CaptivePortal.TimeBetweenChecks",
    254                              now - last_check_time_);
    255 
    256     if (last_detection_result_ != result) {
    257       // If the last result was different from the result of the latest test,
    258       // record histograms about the previous period over which the result was
    259       // the same.
    260       RecordRepeatHistograms(last_detection_result_,
    261                              num_checks_with_same_result_,
    262                              now - first_check_time_with_same_result_);
    263     }
    264   }
    265 
    266   if (last_check_time_.is_null() || result != last_detection_result_) {
    267     first_check_time_with_same_result_ = now;
    268     num_checks_with_same_result_ = 1;
    269 
    270     // Reset the backoff entry both to update the default time and clear
    271     // previous failures.
    272     ResetBackoffEntry(result);
    273 
    274     backoff_entry_->SetCustomReleaseTime(now + retry_after_delta);
    275     // The BackoffEntry is not informed of this request, so there's no delay
    276     // before the next request.  This allows for faster login when a captive
    277     // portal is first detected.  It can also help when moving between captive
    278     // portals.
    279   } else {
    280     DCHECK_LE(1, num_checks_with_same_result_);
    281     ++num_checks_with_same_result_;
    282 
    283     // Requests that have the same Result as the last one are considered
    284     // "failures", to trigger backoff.
    285     backoff_entry_->SetCustomReleaseTime(now + retry_after_delta);
    286     backoff_entry_->InformOfRequest(false);
    287   }
    288 
    289   last_check_time_ = now;
    290 
    291   OnResult(result);
    292 }
    293 
    294 void CaptivePortalService::Shutdown() {
    295   DCHECK(CalledOnValidThread());
    296   if (enabled_) {
    297     RecordRepeatHistograms(
    298         last_detection_result_,
    299         num_checks_with_same_result_,
    300         GetCurrentTimeTicks() - first_check_time_with_same_result_);
    301   }
    302 }
    303 
    304 void CaptivePortalService::OnResult(CaptivePortalResult result) {
    305   DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_);
    306   state_ = STATE_IDLE;
    307 
    308   Results results;
    309   results.previous_result = last_detection_result_;
    310   results.result = result;
    311   last_detection_result_ = result;
    312 
    313   content::NotificationService::current()->Notify(
    314       chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT,
    315       content::Source<Profile>(profile_),
    316       content::Details<Results>(&results));
    317 }
    318 
    319 void CaptivePortalService::ResetBackoffEntry(CaptivePortalResult result) {
    320   if (!enabled_ || result == captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL) {
    321     // Use the shorter time when the captive portal service is not enabled, or
    322     // behind a captive portal.
    323     recheck_policy_.backoff_policy.initial_delay_ms =
    324         recheck_policy_.initial_backoff_portal_ms;
    325   } else {
    326     recheck_policy_.backoff_policy.initial_delay_ms =
    327         recheck_policy_.initial_backoff_no_portal_ms;
    328   }
    329 
    330   backoff_entry_.reset(new RecheckBackoffEntry(this));
    331 }
    332 
    333 void CaptivePortalService::UpdateEnabledState() {
    334   DCHECK(CalledOnValidThread());
    335   bool enabled_before = enabled_;
    336   enabled_ = testing_state_ != DISABLED_FOR_TESTING &&
    337              resolve_errors_with_web_service_.GetValue();
    338 
    339   if (testing_state_ != SKIP_OS_CHECK_FOR_TESTING &&
    340       ShouldDeferToNativeCaptivePortalDetection()) {
    341     enabled_ = false;
    342   }
    343 
    344   if (enabled_before == enabled_)
    345     return;
    346 
    347   // Clear data used for histograms.
    348   num_checks_with_same_result_ = 0;
    349   first_check_time_with_same_result_ = base::TimeTicks();
    350   last_check_time_ = base::TimeTicks();
    351 
    352   ResetBackoffEntry(last_detection_result_);
    353 
    354   if (state_ == STATE_CHECKING_FOR_PORTAL || state_ == STATE_TIMER_RUNNING) {
    355     // If a captive portal check was running or pending, cancel check
    356     // and the timer.
    357     check_captive_portal_timer_.Stop();
    358     captive_portal_detector_.Cancel();
    359     state_ = STATE_IDLE;
    360 
    361     // Since a captive portal request was queued or running, something may be
    362     // expecting to receive a captive portal result.
    363     DetectCaptivePortal();
    364   }
    365 }
    366 
    367 base::TimeTicks CaptivePortalService::GetCurrentTimeTicks() const {
    368   if (time_ticks_for_testing_.is_null())
    369     return base::TimeTicks::Now();
    370   else
    371     return time_ticks_for_testing_;
    372 }
    373 
    374 bool CaptivePortalService::DetectionInProgress() const {
    375   return state_ == STATE_CHECKING_FOR_PORTAL;
    376 }
    377 
    378 bool CaptivePortalService::TimerRunning() const {
    379   return check_captive_portal_timer_.IsRunning();
    380 }
    381