blob: 73cc2dd2c64186cd6cb956abd96b6c82fcefbf0f [file] [log] [blame]
// 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/users/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)) {
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());
NetworkHandler::Get()->network_state_handler()->AddObserver(this, FROM_HERE);
StartDetectionIfIdle();
}
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::SetStrategy(
PortalDetectorStrategy::StrategyId id) {
if (id == strategy_->Id())
return;
strategy_.reset(PortalDetectorStrategy::CreateById(id).release());
strategy_->set_delegate(this);
StopDetection();
StartDetectionIfIdle();
}
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_;
}
////////////////////////////////////////////////////////////////////////////////
// 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::CaptivePortalResult 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="
<< captive_portal::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 (CanPerformAttempt()) {
ScheduleAttempt(results.retry_after_delta);
return;
} else if (state.response_code ==
net::HTTP_PROXY_AUTHENTICATION_REQUIRED) {
state.status = CAPTIVE_PORTAL_STATUS_PROXY_AUTH_REQUIRED;
} 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));
}
}
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;
}
}
} // namespace chromeos