root/chrome/browser/captive_portal/captive_portal_service.cc

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. RecordRepeatHistograms
  2. GetHistogramEntryForDetectionResult
  3. ShouldDeferToNativeCaptivePortalDetection
  4. captive_portal_service_
  5. ImplGetTimeNow
  6. initial_backoff_portal_ms
  7. test_url_
  8. DetectCaptivePortal
  9. DetectCaptivePortalInternal
  10. OnPortalDetectionCompleted
  11. Shutdown
  12. OnResult
  13. ResetBackoffEntry
  14. UpdateEnabledState
  15. GetCurrentTimeTicks
  16. DetectionInProgress
  17. TimerRunning

// 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/bind_helpers.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/metrics/histogram.h"
#include "base/prefs/pref_service.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "content/public/browser/notification_service.h"

#if defined(OS_MACOSX)
#include "base/mac/mac_util.h"
#endif

#if defined(OS_WIN)
#include "base/win/windows_version.h"
#endif

namespace captive_portal {

namespace {

// Make sure this enum is in sync with CaptivePortalDetectionResult enum
// in histograms.xml. This enum is append-only, don't modify existing values.
enum CaptivePortalDetectionResult {
  // There's a confirmed connection to the Internet.
  DETECTION_RESULT_INTERNET_CONNECTED,
  // Received a network or HTTP error, or a non-HTTP response.
  DETECTION_RESULT_NO_RESPONSE,
  // Encountered a captive portal with a non-HTTPS landing URL.
  DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL,
  // Received a network or HTTP error with an HTTPS landing URL.
  DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL,
  // Encountered a captive portal with an HTTPS landing URL.
  DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL,
  DETECTION_RESULT_COUNT
};

// 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::HistogramBase* result_repeated_histogram =
      base::Histogram::FactoryGet(
          "CaptivePortal.ResultRepeated." +
              CaptivePortalDetector::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::HistogramBase* result_duration_histogram =
      base::Histogram::FactoryTimeGet(
          "CaptivePortal.ResultDuration." +
              CaptivePortalDetector::CaptivePortalResultToString(result),
          base::TimeDelta::FromSeconds(1),  // min
          base::TimeDelta::FromHours(1),  // max
          50,  // bucket_count
          base::Histogram::kUmaTargetedHistogramFlag);
  result_duration_histogram->AddTime(result_duration);
}

int GetHistogramEntryForDetectionResult(
    const CaptivePortalDetector::Results& results) {
  bool is_https = results.landing_url.SchemeIs("https");
  switch (results.result) {
    case RESULT_INTERNET_CONNECTED:
      return DETECTION_RESULT_INTERNET_CONNECTED;
    case RESULT_NO_RESPONSE:
      return is_https ?
          DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL :
          DETECTION_RESULT_NO_RESPONSE;
    case RESULT_BEHIND_CAPTIVE_PORTAL:
      return is_https ?
          DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL :
          DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL;
    default:
      NOTREACHED();
      return -1;
  }
}

bool ShouldDeferToNativeCaptivePortalDetection() {
  // On Windows 8, defer to the native captive portal detection.  OSX Lion and
  // later also have captive portal detection, but experimentally, this code
  // works in cases its does not.
  //
  // TODO(mmenke): Investigate how well Windows 8's captive portal detection
  // works.
#if defined(OS_WIN)
  return base::win::GetVersion() >= base::win::VERSION_WIN8;
#else
  return false;
#endif
}

}  // namespace

CaptivePortalService::TestingState CaptivePortalService::testing_state_ =
    NOT_TESTING;

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),
      captive_portal_detector_(profile->GetRequestContext()),
      enabled_(false),
      last_detection_result_(RESULT_INTERNET_CONNECTED),
      num_checks_with_same_result_(0),
      test_url_(CaptivePortalDetector::kDefaultURL) {
  // 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(),
      base::Bind(&CaptivePortalService::UpdateEnabledState,
                 base::Unretained(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(!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;
  }

  captive_portal_detector_.DetectCaptivePortal(
      test_url_, base::Bind(
          &CaptivePortalService::OnPortalDetectionCompleted,
          base::Unretained(this)));
}

void CaptivePortalService::OnPortalDetectionCompleted(
    const CaptivePortalDetector::Results& results) {
  DCHECK(CalledOnValidThread());
  DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_);
  DCHECK(!TimerRunning());
  DCHECK(enabled_);

  Result result = results.result;
  const base::TimeDelta& retry_after_delta = results.retry_after_delta;
  base::TimeTicks now = GetCurrentTimeTicks();

  // Record histograms.
  UMA_HISTOGRAM_ENUMERATION("CaptivePortal.DetectResult",
                            GetHistogramEntryForDetectionResult(results),
                            DETECTION_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_ != 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() || 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(result);

    backoff_entry_->SetCustomReleaseTime(now + retry_after_delta);
    // The BackoffEntry is not informed of this request, so there's no delay
    // before the next request.  This allows for faster login when a captive
    // portal is first detected.  It can also help when moving between captive
    // portals.
  } 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_->SetCustomReleaseTime(now + retry_after_delta);
    backoff_entry_->InformOfRequest(false);
  }

  last_check_time_ = now;

  OnResult(result);
}

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() {
  DCHECK(CalledOnValidThread());
  bool enabled_before = enabled_;
  enabled_ = testing_state_ != DISABLED_FOR_TESTING &&
             resolve_errors_with_web_service_.GetValue();

  if (testing_state_ != SKIP_OS_CHECK_FOR_TESTING &&
      ShouldDeferToNativeCaptivePortalDetection()) {
    enabled_ = false;
  }

  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, cancel check
    // and the timer.
    check_captive_portal_timer_.Stop();
    captive_portal_detector_.Cancel();
    state_ = STATE_IDLE;

    // Since a captive portal request was queued or running, something may be
    // expecting to receive a captive portal result.
    DetectCaptivePortal();
  }
}

base::TimeTicks CaptivePortalService::GetCurrentTimeTicks() const {
  if (time_ticks_for_testing_.is_null())
    return base::TimeTicks::Now();
  else
    return time_ticks_for_testing_;
}

bool CaptivePortalService::DetectionInProgress() const {
  return state_ == STATE_CHECKING_FOR_PORTAL;
}

bool CaptivePortalService::TimerRunning() const {
  return check_captive_portal_timer_.IsRunning();
}

}  // namespace captive_portal

/* [<][>][^][v][top][bottom][index][help] */