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