root/chrome/browser/chromeos/net/network_portal_detector_impl.cc

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

DEFINITIONS

This source file includes following definitions.
  1. DefaultNetwork
  2. InSession
  3. RecordDetectionResult
  4. RecordDetectionDuration
  5. RecordDiscrepancyWithShill
  6. RecordPortalToOnlineTransition
  7. error_screen_displayed_
  8. AddObserver
  9. AddAndFireObserver
  10. RemoveObserver
  11. IsEnabled
  12. Enable
  13. GetCaptivePortalState
  14. StartDetectionIfIdle
  15. DefaultNetworkChanged
  16. AttemptCount
  17. AttemptStartTime
  18. GetCurrentTimeTicks
  19. OnErrorScreenShow
  20. OnErrorScreenHide
  21. StartDetection
  22. StopDetection
  23. CanPerformAttempt
  24. ScheduleAttempt
  25. StartAttempt
  26. OnAttemptTimeout
  27. OnAttemptCompleted
  28. Observe
  29. OnDetectionCompleted
  30. NotifyDetectionCompleted
  31. AttemptTimeoutIsCancelledForTesting
  32. RecordDetectionStats
  33. UpdateCurrentStrategy
  34. SetStrategy

// Copyright (c) 2013 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/chromeos/net/network_portal_detector_impl.h"

#include <algorithm>

#include "base/bind.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/message_loop/message_loop.h"
#include "base/metrics/histogram.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/chromeos/login/user_manager.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/shill_profile_client.h"
#include "chromeos/network/network_state.h"
#include "chromeos/network/network_state_handler.h"
#include "content/public/browser/notification_service.h"
#include "grit/generated_resources.h"
#include "net/http/http_status_code.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
#include "ui/base/l10n/l10n_util.h"

using captive_portal::CaptivePortalDetector;

namespace chromeos {

namespace {

// Delay before portal detection caused by changes in proxy settings.
const int kProxyChangeDelaySec = 1;

const NetworkState* DefaultNetwork() {
  return NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
}

bool InSession() {
  return UserManager::IsInitialized() && UserManager::Get()->IsUserLoggedIn();
}

void RecordDetectionResult(NetworkPortalDetector::CaptivePortalStatus status) {
  if (InSession()) {
    UMA_HISTOGRAM_ENUMERATION(
        NetworkPortalDetectorImpl::kSessionDetectionResultHistogram,
        status,
        NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_COUNT);
  } else {
    UMA_HISTOGRAM_ENUMERATION(
        NetworkPortalDetectorImpl::kOobeDetectionResultHistogram,
        status,
        NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_COUNT);
  }
}

void RecordDetectionDuration(const base::TimeDelta& duration) {
  if (InSession()) {
    UMA_HISTOGRAM_MEDIUM_TIMES(
        NetworkPortalDetectorImpl::kSessionDetectionDurationHistogram,
        duration);
  } else {
    UMA_HISTOGRAM_MEDIUM_TIMES(
        NetworkPortalDetectorImpl::kOobeDetectionDurationHistogram, duration);
  }
}

void RecordDiscrepancyWithShill(
    const NetworkState* network,
    const NetworkPortalDetector::CaptivePortalStatus status) {
  if (InSession()) {
    if (network->connection_state() == shill::kStateOnline) {
      UMA_HISTOGRAM_ENUMERATION(
          NetworkPortalDetectorImpl::kSessionShillOnlineHistogram,
          status,
          NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_COUNT);
    } else if (network->connection_state() == shill::kStatePortal) {
      UMA_HISTOGRAM_ENUMERATION(
          NetworkPortalDetectorImpl::kSessionShillPortalHistogram,
          status,
          NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_COUNT);
    } else if (network->connection_state() == shill::kStateOffline) {
      UMA_HISTOGRAM_ENUMERATION(
          NetworkPortalDetectorImpl::kSessionShillOfflineHistogram,
          status,
          NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_COUNT);
    }
  } else {
    if (network->connection_state() == shill::kStateOnline) {
      UMA_HISTOGRAM_ENUMERATION(
          NetworkPortalDetectorImpl::kOobeShillOnlineHistogram,
          status,
          NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_COUNT);
    } else if (network->connection_state() == shill::kStatePortal) {
      UMA_HISTOGRAM_ENUMERATION(
          NetworkPortalDetectorImpl::kOobeShillPortalHistogram,
          status,
          NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_COUNT);
    } else if (network->connection_state() == shill::kStateOffline) {
      UMA_HISTOGRAM_ENUMERATION(
          NetworkPortalDetectorImpl::kOobeShillOfflineHistogram,
          status,
          NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_COUNT);
    }
  }
}

void RecordPortalToOnlineTransition(const base::TimeDelta& duration) {
  if (InSession()) {
    UMA_HISTOGRAM_LONG_TIMES(
        NetworkPortalDetectorImpl::kSessionPortalToOnlineHistogram,
        duration);
  } else {
    UMA_HISTOGRAM_LONG_TIMES(
        NetworkPortalDetectorImpl::kOobePortalToOnlineHistogram,
        duration);
  }
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// NetworkPortalDetectorImpl, public:

const char NetworkPortalDetectorImpl::kOobeDetectionResultHistogram[] =
    "CaptivePortal.OOBE.DetectionResult";
const char NetworkPortalDetectorImpl::kOobeDetectionDurationHistogram[] =
    "CaptivePortal.OOBE.DetectionDuration";
const char NetworkPortalDetectorImpl::kOobeShillOnlineHistogram[] =
    "CaptivePortal.OOBE.DiscrepancyWithShill_Online";
const char NetworkPortalDetectorImpl::kOobeShillPortalHistogram[] =
    "CaptivePortal.OOBE.DiscrepancyWithShill_RestrictedPool";
const char NetworkPortalDetectorImpl::kOobeShillOfflineHistogram[] =
    "CaptivePortal.OOBE.DiscrepancyWithShill_Offline";
const char NetworkPortalDetectorImpl::kOobePortalToOnlineHistogram[] =
    "CaptivePortal.OOBE.PortalToOnlineTransition";

const char NetworkPortalDetectorImpl::kSessionDetectionResultHistogram[] =
    "CaptivePortal.Session.DetectionResult";
const char NetworkPortalDetectorImpl::kSessionDetectionDurationHistogram[] =
    "CaptivePortal.Session.DetectionDuration";
const char NetworkPortalDetectorImpl::kSessionShillOnlineHistogram[] =
    "CaptivePortal.Session.DiscrepancyWithShill_Online";
const char NetworkPortalDetectorImpl::kSessionShillPortalHistogram[] =
    "CaptivePortal.Session.DiscrepancyWithShill_RestrictedPool";
const char NetworkPortalDetectorImpl::kSessionShillOfflineHistogram[] =
    "CaptivePortal.Session.DiscrepancyWithShill_Offline";
const char NetworkPortalDetectorImpl::kSessionPortalToOnlineHistogram[] =
    "CaptivePortal.Session.PortalToOnlineTransition";

NetworkPortalDetectorImpl::NetworkPortalDetectorImpl(
    const scoped_refptr<net::URLRequestContextGetter>& request_context)
    : state_(STATE_IDLE),
      test_url_(CaptivePortalDetector::kDefaultURL),
      enabled_(false),
      weak_factory_(this),
      attempt_count_(0),
      strategy_(PortalDetectorStrategy::CreateById(
          PortalDetectorStrategy::STRATEGY_ID_LOGIN_SCREEN)),
      error_screen_displayed_(false) {
  captive_portal_detector_.reset(new CaptivePortalDetector(request_context));
  strategy_->set_delegate(this);

  registrar_.Add(this,
                 chrome::NOTIFICATION_LOGIN_PROXY_CHANGED,
                 content::NotificationService::AllSources());
  registrar_.Add(this,
                 chrome::NOTIFICATION_AUTH_SUPPLIED,
                 content::NotificationService::AllSources());
  registrar_.Add(this,
                 chrome::NOTIFICATION_AUTH_CANCELLED,
                 content::NotificationService::AllSources());
  registrar_.Add(this,
                 chrome::NOTIFICATION_LOGIN_USER_CHANGED,
                 content::NotificationService::AllSources());

  NetworkHandler::Get()->network_state_handler()->AddObserver(this, FROM_HERE);
  UpdateCurrentStrategy();
}

NetworkPortalDetectorImpl::~NetworkPortalDetectorImpl() {
  DCHECK(CalledOnValidThread());

  attempt_task_.Cancel();
  attempt_timeout_.Cancel();

  captive_portal_detector_->Cancel();
  captive_portal_detector_.reset();
  observers_.Clear();
  if (NetworkHandler::IsInitialized()) {
    NetworkHandler::Get()->network_state_handler()->RemoveObserver(this,
                                                                   FROM_HERE);
  }
}

void NetworkPortalDetectorImpl::AddObserver(Observer* observer) {
  DCHECK(CalledOnValidThread());
  if (observer && !observers_.HasObserver(observer))
    observers_.AddObserver(observer);
}

void NetworkPortalDetectorImpl::AddAndFireObserver(Observer* observer) {
  DCHECK(CalledOnValidThread());
  if (!observer)
    return;
  AddObserver(observer);
  CaptivePortalState portal_state;
  const NetworkState* network = DefaultNetwork();
  if (network)
    portal_state = GetCaptivePortalState(network->path());
  observer->OnPortalDetectionCompleted(network, portal_state);
}

void NetworkPortalDetectorImpl::RemoveObserver(Observer* observer) {
  DCHECK(CalledOnValidThread());
  if (observer)
    observers_.RemoveObserver(observer);
}

bool NetworkPortalDetectorImpl::IsEnabled() { return enabled_; }

void NetworkPortalDetectorImpl::Enable(bool start_detection) {
  DCHECK(CalledOnValidThread());
  if (enabled_)
    return;

  DCHECK(is_idle());
  enabled_ = true;

  const NetworkState* network = DefaultNetwork();
  if (!start_detection || !network)
    return;
  portal_state_map_.erase(network->path());
  StartDetection();
}

NetworkPortalDetectorImpl::CaptivePortalState
NetworkPortalDetectorImpl::GetCaptivePortalState(
    const std::string& service_path) {
  DCHECK(CalledOnValidThread());
  CaptivePortalStateMap::const_iterator it =
      portal_state_map_.find(service_path);
  if (it == portal_state_map_.end())
    return CaptivePortalState();
  return it->second;
}

bool NetworkPortalDetectorImpl::StartDetectionIfIdle() {
  if (!is_idle())
    return false;
  StartDetection();
  return true;
}

void NetworkPortalDetectorImpl::DefaultNetworkChanged(
    const NetworkState* default_network) {
  DCHECK(CalledOnValidThread());

  if (!default_network) {
    default_network_name_.clear();
    default_network_id_.clear();

    StopDetection();

    CaptivePortalState state;
    state.status = CAPTIVE_PORTAL_STATUS_OFFLINE;
    OnDetectionCompleted(NULL, state);
    return;
  }

  default_network_name_ = default_network->name();
  default_network_id_ = default_network->guid();

  bool network_changed = (default_service_path_ != default_network->path());
  default_service_path_ = default_network->path();

  bool connection_state_changed =
      (default_connection_state_ != default_network->connection_state());
  default_connection_state_ = default_network->connection_state();

  if (network_changed || connection_state_changed)
    StopDetection();

  if (CanPerformAttempt() &&
      NetworkState::StateIsConnected(default_connection_state_)) {
    // Initiate Captive Portal detection if network's captive
    // portal state is unknown (e.g. for freshly created networks),
    // offline or if network connection state was changed.
    CaptivePortalState state = GetCaptivePortalState(default_network->path());
    if (state.status == CAPTIVE_PORTAL_STATUS_UNKNOWN ||
        state.status == CAPTIVE_PORTAL_STATUS_OFFLINE ||
        (!network_changed && connection_state_changed)) {
      ScheduleAttempt(base::TimeDelta());
    }
  }
}

int NetworkPortalDetectorImpl::AttemptCount() { return attempt_count_; }

base::TimeTicks NetworkPortalDetectorImpl::AttemptStartTime() {
  return attempt_start_time_;
}

base::TimeTicks NetworkPortalDetectorImpl::GetCurrentTimeTicks() {
  if (time_ticks_for_testing_.is_null())
    return base::TimeTicks::Now();
  return time_ticks_for_testing_;
}

void NetworkPortalDetectorImpl::OnErrorScreenShow() {
  error_screen_displayed_ = true;
  UpdateCurrentStrategy();
}

void NetworkPortalDetectorImpl::OnErrorScreenHide() {
  error_screen_displayed_ = false;
  UpdateCurrentStrategy();
}

////////////////////////////////////////////////////////////////////////////////
// NetworkPortalDetectorImpl, private:

void NetworkPortalDetectorImpl::StartDetection() {
  attempt_count_ = 0;
  DCHECK(CanPerformAttempt());
  detection_start_time_ = GetCurrentTimeTicks();
  ScheduleAttempt(base::TimeDelta());
}

void NetworkPortalDetectorImpl::StopDetection() {
  attempt_task_.Cancel();
  attempt_timeout_.Cancel();
  captive_portal_detector_->Cancel();
  state_ = STATE_IDLE;
  attempt_count_ = 0;
}

bool NetworkPortalDetectorImpl::CanPerformAttempt() const {
  return is_idle() && strategy_->CanPerformAttempt();
}

void NetworkPortalDetectorImpl::ScheduleAttempt(const base::TimeDelta& delay) {
  DCHECK(CanPerformAttempt());

  if (!IsEnabled())
    return;

  attempt_task_.Cancel();
  attempt_timeout_.Cancel();
  state_ = STATE_PORTAL_CHECK_PENDING;

  next_attempt_delay_ = std::max(delay, strategy_->GetDelayTillNextAttempt());
  attempt_task_.Reset(base::Bind(&NetworkPortalDetectorImpl::StartAttempt,
                                 weak_factory_.GetWeakPtr()));
  base::MessageLoop::current()->PostDelayedTask(
      FROM_HERE, attempt_task_.callback(), next_attempt_delay_);
}

void NetworkPortalDetectorImpl::StartAttempt() {
  DCHECK(is_portal_check_pending());

  state_ = STATE_CHECKING_FOR_PORTAL;
  attempt_start_time_ = GetCurrentTimeTicks();

  captive_portal_detector_->DetectCaptivePortal(
      test_url_,
      base::Bind(&NetworkPortalDetectorImpl::OnAttemptCompleted,
                 weak_factory_.GetWeakPtr()));
  attempt_timeout_.Reset(
      base::Bind(&NetworkPortalDetectorImpl::OnAttemptTimeout,
                 weak_factory_.GetWeakPtr()));

  base::MessageLoop::current()->PostDelayedTask(
      FROM_HERE,
      attempt_timeout_.callback(),
      strategy_->GetNextAttemptTimeout());
}

void NetworkPortalDetectorImpl::OnAttemptTimeout() {
  DCHECK(CalledOnValidThread());
  DCHECK(is_checking_for_portal());

  VLOG(1) << "Portal detection timeout: name=" << default_network_name_ << ", "
          << "id=" << default_network_id_;

  captive_portal_detector_->Cancel();
  CaptivePortalDetector::Results results;
  results.result = captive_portal::RESULT_NO_RESPONSE;
  OnAttemptCompleted(results);
}

void NetworkPortalDetectorImpl::OnAttemptCompleted(
    const CaptivePortalDetector::Results& results) {
  captive_portal::Result result = results.result;
  int response_code = results.response_code;

  DCHECK(CalledOnValidThread());
  DCHECK(is_checking_for_portal());

  VLOG(1) << "Detection attempt completed: "
          << "name=" << default_network_name_ << ", "
          << "id=" << default_network_id_ << ", "
          << "result="
          << CaptivePortalDetector::CaptivePortalResultToString(results.result)
          << ", "
          << "response_code=" << results.response_code;

  state_ = STATE_IDLE;
  attempt_timeout_.Cancel();
  ++attempt_count_;

  const NetworkState* network = DefaultNetwork();

  // If using a fake profile client, also fake being behind a captive portal
  // if the default network is in portal state.
  if (result != captive_portal::RESULT_NO_RESPONSE &&
      DBusThreadManager::Get()->GetShillProfileClient()->GetTestInterface() &&
      network && network->connection_state() == shill::kStatePortal) {
    result = captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL;
    response_code = 200;
  }

  CaptivePortalState state;
  state.response_code = response_code;
  state.time = GetCurrentTimeTicks();
  switch (result) {
    case captive_portal::RESULT_NO_RESPONSE:
      if (state.response_code == net::HTTP_PROXY_AUTHENTICATION_REQUIRED) {
        state.status = CAPTIVE_PORTAL_STATUS_PROXY_AUTH_REQUIRED;
      } else if (CanPerformAttempt()) {
        ScheduleAttempt(results.retry_after_delta);
        return;
      } else if (network &&
                 (network->connection_state() == shill::kStatePortal)) {
        // Take into account shill's detection results.
        state.status = CAPTIVE_PORTAL_STATUS_PORTAL;
        LOG(WARNING) << "Network name=" << network->name() << ", "
                     << "id=" << network->guid() << " "
                     << "is marked as "
                     << CaptivePortalStatusString(state.status) << " "
                     << "despite the fact that CaptivePortalDetector "
                     << "received no response";
      } else {
        state.status = CAPTIVE_PORTAL_STATUS_OFFLINE;
      }
      break;
    case captive_portal::RESULT_INTERNET_CONNECTED:
      state.status = CAPTIVE_PORTAL_STATUS_ONLINE;
      break;
    case captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL:
      state.status = CAPTIVE_PORTAL_STATUS_PORTAL;
      break;
    default:
      break;
  }

  OnDetectionCompleted(network, state);
  if (CanPerformAttempt() && strategy_->CanPerformAttemptAfterDetection())
    ScheduleAttempt(base::TimeDelta());
}

void NetworkPortalDetectorImpl::Observe(
    int type,
    const content::NotificationSource& source,
    const content::NotificationDetails& details) {
  if (type == chrome::NOTIFICATION_LOGIN_PROXY_CHANGED ||
      type == chrome::NOTIFICATION_AUTH_SUPPLIED ||
      type == chrome::NOTIFICATION_AUTH_CANCELLED) {
    VLOG(1) << "Restarting portal detection due to proxy change.";
    attempt_count_ = 0;
    if (is_portal_check_pending())
      return;
    StopDetection();
    ScheduleAttempt(base::TimeDelta::FromSeconds(kProxyChangeDelaySec));
  } else if (type == chrome::NOTIFICATION_LOGIN_USER_CHANGED) {
    UpdateCurrentStrategy();
  }
}

void NetworkPortalDetectorImpl::OnDetectionCompleted(
    const NetworkState* network,
    const CaptivePortalState& state) {
  if (!network) {
    NotifyDetectionCompleted(network, state);
    return;
  }

  CaptivePortalStateMap::const_iterator it =
      portal_state_map_.find(network->path());
  if (it == portal_state_map_.end() || it->second.status != state.status ||
      it->second.response_code != state.response_code) {
    VLOG(1) << "Updating Chrome Captive Portal state: "
            << "name=" << network->name() << ", "
            << "id=" << network->guid() << ", "
            << "status=" << CaptivePortalStatusString(state.status) << ", "
            << "response_code=" << state.response_code;

    // Record detection duration iff detection result differs from the
    // previous one for this network. The reason is to record all stats
    // only when network changes it's state.
    RecordDetectionStats(network, state.status);
    if (it != portal_state_map_.end() &&
        it->second.status == CAPTIVE_PORTAL_STATUS_PORTAL &&
        state.status == CAPTIVE_PORTAL_STATUS_ONLINE) {
      RecordPortalToOnlineTransition(state.time - it->second.time);
    }

    portal_state_map_[network->path()] = state;
  }
  NotifyDetectionCompleted(network, state);
}

void NetworkPortalDetectorImpl::NotifyDetectionCompleted(
    const NetworkState* network,
    const CaptivePortalState& state) {
  FOR_EACH_OBSERVER(
      Observer, observers_, OnPortalDetectionCompleted(network, state));
  notification_controller_.OnPortalDetectionCompleted(network, state);
}

bool NetworkPortalDetectorImpl::AttemptTimeoutIsCancelledForTesting() const {
  return attempt_timeout_.IsCancelled();
}

void NetworkPortalDetectorImpl::RecordDetectionStats(
    const NetworkState* network,
    CaptivePortalStatus status) {
  // Don't record stats for offline state.
  if (!network)
    return;

  if (!detection_start_time_.is_null())
    RecordDetectionDuration(GetCurrentTimeTicks() - detection_start_time_);
  RecordDetectionResult(status);

  switch (status) {
    case NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_UNKNOWN:
      NOTREACHED();
      break;
    case NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_OFFLINE:
      if (network->connection_state() == shill::kStateOnline ||
          network->connection_state() == shill::kStatePortal) {
        RecordDiscrepancyWithShill(network, status);
      }
      break;
    case NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_ONLINE:
      if (network->connection_state() != shill::kStateOnline)
        RecordDiscrepancyWithShill(network, status);
      break;
    case NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_PORTAL:
      if (network->connection_state() != shill::kStatePortal)
        RecordDiscrepancyWithShill(network, status);
      break;
    case NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_PROXY_AUTH_REQUIRED:
      if (network->connection_state() != shill::kStateOnline)
        RecordDiscrepancyWithShill(network, status);
      break;
    case NetworkPortalDetector::CAPTIVE_PORTAL_STATUS_COUNT:
      NOTREACHED();
      break;
  }
}

void NetworkPortalDetectorImpl::UpdateCurrentStrategy() {
  if (InSession()) {
    SetStrategy(PortalDetectorStrategy::STRATEGY_ID_SESSION);
    return;
  }
  if (error_screen_displayed_) {
    SetStrategy(PortalDetectorStrategy::STRATEGY_ID_ERROR_SCREEN);
    return;
  }
  SetStrategy(PortalDetectorStrategy::STRATEGY_ID_LOGIN_SCREEN);
}

void NetworkPortalDetectorImpl::SetStrategy(
    PortalDetectorStrategy::StrategyId id) {
  if (id == strategy_->Id())
    return;
  strategy_.reset(PortalDetectorStrategy::CreateById(id).release());
  strategy_->set_delegate(this);
  StopDetection();
  StartDetectionIfIdle();
}

}  // namespace chromeos

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