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