blob: 40ae6e1545a8352040b27f612b48fe67d161c694 [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 "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 "chromeos/dbus/shill_service_client_stub.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 {
// Maximum number of portal detections for the same default network
// after network change.
const int kMaxRequestAttempts = 3;
// Minimum timeout between consecutive portal checks for the same
// network.
const int kMinTimeBetweenAttemptsSec = 3;
// Delay before portal detection caused by changes in proxy settings.
const int kProxyChangeDelaySec = 1;
// Delay between consecutive portal checks for a network in lazy mode.
const int kLazyCheckIntervalSec = 5;
std::string CaptivePortalStatusString(
NetworkPortalDetectorImpl::CaptivePortalStatus status) {
switch (status) {
case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_UNKNOWN:
return l10n_util::GetStringUTF8(
IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_UNKNOWN);
case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_OFFLINE:
return l10n_util::GetStringUTF8(
IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_OFFLINE);
case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_ONLINE:
return l10n_util::GetStringUTF8(
IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_ONLINE);
case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_PORTAL:
return l10n_util::GetStringUTF8(
IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_PORTAL);
case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_PROXY_AUTH_REQUIRED:
return l10n_util::GetStringUTF8(
IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_PROXY_AUTH_REQUIRED);
case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_COUNT:
NOTREACHED();
}
return l10n_util::GetStringUTF8(
IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_UNRECOGNIZED);
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// NetworkPortalDetectorImpl, public:
NetworkPortalDetectorImpl::NetworkPortalDetectorImpl(
const scoped_refptr<net::URLRequestContextGetter>& request_context)
: test_url_(CaptivePortalDetector::kDefaultURL),
enabled_(false),
weak_ptr_factory_(this),
attempt_count_(0),
lazy_detection_enabled_(false),
lazy_check_interval_(base::TimeDelta::FromSeconds(kLazyCheckIntervalSec)),
min_time_between_attempts_(
base::TimeDelta::FromSeconds(kMinTimeBetweenAttemptsSec)),
request_timeout_for_testing_initialized_(false) {
captive_portal_detector_.reset(new CaptivePortalDetector(request_context));
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());
}
NetworkPortalDetectorImpl::~NetworkPortalDetectorImpl() {
}
void NetworkPortalDetectorImpl::Init() {
DCHECK(CalledOnValidThread());
state_ = STATE_IDLE;
NetworkHandler::Get()->network_state_handler()->AddObserver(
this, FROM_HERE);
}
void NetworkPortalDetectorImpl::Shutdown() {
DCHECK(CalledOnValidThread());
detection_task_.Cancel();
detection_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))
return;
observers_.AddObserver(observer);
}
void NetworkPortalDetectorImpl::AddAndFireObserver(Observer* observer) {
DCHECK(CalledOnValidThread());
if (!observer)
return;
AddObserver(observer);
const NetworkState* network =
NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
observer->OnPortalDetectionCompleted(network, GetCaptivePortalState(network));
}
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;
enabled_ = true;
DCHECK(!IsPortalCheckPending());
DCHECK(!IsCheckingForPortal());
DCHECK(!lazy_detection_enabled());
if (!start_detection)
return;
state_ = STATE_IDLE;
attempt_count_ = 0;
const NetworkState* default_network =
NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
if (!default_network)
return;
portal_state_map_.erase(default_network->path());
DCHECK(CanPerformDetection());
DetectCaptivePortal(base::TimeDelta());
}
NetworkPortalDetectorImpl::CaptivePortalState
NetworkPortalDetectorImpl::GetCaptivePortalState(const NetworkState* network) {
DCHECK(CalledOnValidThread());
if (!network)
return CaptivePortalState();
CaptivePortalStateMap::const_iterator it =
portal_state_map_.find(network->path());
if (it == portal_state_map_.end())
return CaptivePortalState();
return it->second;
}
bool NetworkPortalDetectorImpl::StartDetectionIfIdle() {
if (IsPortalCheckPending() || IsCheckingForPortal())
return false;
if (!CanPerformDetection())
attempt_count_ = 0;
DCHECK(CanPerformDetection());
DetectCaptivePortal(base::TimeDelta());
return true;
}
void NetworkPortalDetectorImpl::EnableLazyDetection() {
if (lazy_detection_enabled())
return;
lazy_detection_enabled_ = true;
VLOG(1) << "Lazy detection mode enabled.";
StartDetectionIfIdle();
}
void NetworkPortalDetectorImpl::DisableLazyDetection() {
if (!lazy_detection_enabled())
return;
lazy_detection_enabled_ = false;
if (attempt_count_ == kMaxRequestAttempts && IsPortalCheckPending())
CancelPortalDetection();
VLOG(1) << "Lazy detection mode disabled.";
}
void NetworkPortalDetectorImpl::DefaultNetworkChanged(
const NetworkState* default_network) {
DCHECK(CalledOnValidThread());
if (!default_network) {
default_network_id_.clear();
return;
}
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) {
attempt_count_ = 0;
CancelPortalDetection();
}
if (CanPerformDetection() &&
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);
if (state.status == CAPTIVE_PORTAL_STATUS_UNKNOWN ||
state.status == CAPTIVE_PORTAL_STATUS_OFFLINE ||
(!network_changed && connection_state_changed)) {
DetectCaptivePortal(base::TimeDelta());
}
}
}
////////////////////////////////////////////////////////////////////////////////
// NetworkPortalDetectorImpl, private:
bool NetworkPortalDetectorImpl::CanPerformDetection() const {
if (IsPortalCheckPending() || IsCheckingForPortal())
return false;
return attempt_count_ < kMaxRequestAttempts || lazy_detection_enabled();
}
void NetworkPortalDetectorImpl::DetectCaptivePortal(
const base::TimeDelta& delay) {
DCHECK(CanPerformDetection());
if (!IsEnabled())
return;
detection_task_.Cancel();
detection_timeout_.Cancel();
state_ = STATE_PORTAL_CHECK_PENDING;
next_attempt_delay_ = delay;
if (attempt_count_ > 0) {
base::TimeTicks now = GetCurrentTimeTicks();
base::TimeDelta elapsed_time;
base::TimeDelta delay_between_attempts = min_time_between_attempts_;
if (attempt_count_ == kMaxRequestAttempts) {
DCHECK(lazy_detection_enabled());
delay_between_attempts = lazy_check_interval_;
}
if (now > attempt_start_time_)
elapsed_time = now - attempt_start_time_;
if (elapsed_time < delay_between_attempts &&
delay_between_attempts - elapsed_time > next_attempt_delay_) {
next_attempt_delay_ = delay_between_attempts - elapsed_time;
}
} else {
detection_start_time_ = GetCurrentTimeTicks();
}
detection_task_.Reset(
base::Bind(&NetworkPortalDetectorImpl::DetectCaptivePortalTask,
weak_ptr_factory_.GetWeakPtr()));
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE, detection_task_.callback(), next_attempt_delay_);
}
void NetworkPortalDetectorImpl::DetectCaptivePortalTask() {
DCHECK(IsPortalCheckPending());
state_ = STATE_CHECKING_FOR_PORTAL;
attempt_start_time_ = GetCurrentTimeTicks();
if (attempt_count_ < kMaxRequestAttempts) {
++attempt_count_;
VLOG(1) << "Portal detection started: "
<< "network=" << default_network_id_ << ", "
<< "attempt=" << attempt_count_ << " of " << kMaxRequestAttempts;
} else {
DCHECK(lazy_detection_enabled());
VLOG(1) << "Lazy portal detection attempt started";
}
captive_portal_detector_->DetectCaptivePortal(
test_url_,
base::Bind(&NetworkPortalDetectorImpl::OnPortalDetectionCompleted,
weak_ptr_factory_.GetWeakPtr()));
detection_timeout_.Reset(
base::Bind(&NetworkPortalDetectorImpl::PortalDetectionTimeout,
weak_ptr_factory_.GetWeakPtr()));
base::TimeDelta request_timeout;
// For easier unit testing check for testing state is performed here
// and not in the GetRequestTimeoutSec().
if (request_timeout_for_testing_initialized_)
request_timeout = request_timeout_for_testing_;
else
request_timeout = base::TimeDelta::FromSeconds(GetRequestTimeoutSec());
base::MessageLoop::current()->PostDelayedTask(
FROM_HERE, detection_timeout_.callback(), request_timeout);
}
void NetworkPortalDetectorImpl::PortalDetectionTimeout() {
DCHECK(CalledOnValidThread());
DCHECK(IsCheckingForPortal());
VLOG(1) << "Portal detection timeout: network=" << default_network_id_;
captive_portal_detector_->Cancel();
CaptivePortalDetector::Results results;
results.result = captive_portal::RESULT_NO_RESPONSE;
OnPortalDetectionCompleted(results);
}
void NetworkPortalDetectorImpl::CancelPortalDetection() {
if (IsPortalCheckPending())
detection_task_.Cancel();
else if (IsCheckingForPortal())
captive_portal_detector_->Cancel();
detection_timeout_.Cancel();
state_ = STATE_IDLE;
}
void NetworkPortalDetectorImpl::OnPortalDetectionCompleted(
const CaptivePortalDetector::Results& results) {
captive_portal::Result result = results.result;
int response_code = results.response_code;
if (ShillServiceClientStub::IsStubPortalledWifiEnabled(
default_service_path_)) {
result = captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL;
response_code = 200;
}
DCHECK(CalledOnValidThread());
DCHECK(IsCheckingForPortal());
VLOG(1) << "Portal detection completed: "
<< "network=" << default_network_id_ << ", "
<< "result=" << CaptivePortalDetector::CaptivePortalResultToString(
results.result) << ", "
<< "response_code=" << results.response_code;
state_ = STATE_IDLE;
detection_timeout_.Cancel();
const NetworkState* default_network =
NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
CaptivePortalState state;
state.response_code = response_code;
switch (result) {
case captive_portal::RESULT_NO_RESPONSE:
if (attempt_count_ >= kMaxRequestAttempts) {
if (state.response_code == net::HTTP_PROXY_AUTHENTICATION_REQUIRED) {
state.status = CAPTIVE_PORTAL_STATUS_PROXY_AUTH_REQUIRED;
} else if (default_network && (default_network->connection_state() ==
flimflam::kStatePortal)) {
// Take into account shill's detection results.
state.status = CAPTIVE_PORTAL_STATUS_PORTAL;
LOG(WARNING) << "Network " << default_network->guid() << " "
<< "is marked as "
<< CaptivePortalStatusString(state.status) << " "
<< "despite the fact that CaptivePortalDetector "
<< "received no response";
} else {
state.status = CAPTIVE_PORTAL_STATUS_OFFLINE;
}
SetCaptivePortalState(default_network, state);
} else {
DCHECK(CanPerformDetection());
DetectCaptivePortal(results.retry_after_delta);
}
break;
case captive_portal::RESULT_INTERNET_CONNECTED:
state.status = CAPTIVE_PORTAL_STATUS_ONLINE;
SetCaptivePortalState(default_network, state);
break;
case captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL:
state.status = CAPTIVE_PORTAL_STATUS_PORTAL;
SetCaptivePortalState(default_network, state);
break;
default:
break;
}
TryLazyDetection();
}
void NetworkPortalDetectorImpl::TryLazyDetection() {
if (lazy_detection_enabled() && CanPerformDetection())
DetectCaptivePortal(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 (IsPortalCheckPending())
return;
CancelPortalDetection();
DCHECK(CanPerformDetection());
DetectCaptivePortal(base::TimeDelta::FromSeconds(kProxyChangeDelaySec));
}
}
bool NetworkPortalDetectorImpl::IsPortalCheckPending() const {
return state_ == STATE_PORTAL_CHECK_PENDING;
}
bool NetworkPortalDetectorImpl::IsCheckingForPortal() const {
return state_ == STATE_CHECKING_FOR_PORTAL;
}
void NetworkPortalDetectorImpl::SetCaptivePortalState(
const NetworkState* network,
const CaptivePortalState& state) {
if (!detection_start_time_.is_null()) {
UMA_HISTOGRAM_TIMES("CaptivePortal.OOBE.DetectionDuration",
GetCurrentTimeTicks() - detection_start_time_);
}
if (!network) {
NotifyPortalDetectionCompleted(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: "
<< "network=" << network->guid() << ", "
<< "status=" << CaptivePortalStatusString(state.status) << ", "
<< "response_code=" << state.response_code;
portal_state_map_[network->path()] = state;
}
NotifyPortalDetectionCompleted(network, state);
}
void NetworkPortalDetectorImpl::NotifyPortalDetectionCompleted(
const NetworkState* network,
const CaptivePortalState& state) {
FOR_EACH_OBSERVER(Observer, observers_,
OnPortalDetectionCompleted(network, state));
}
base::TimeTicks NetworkPortalDetectorImpl::GetCurrentTimeTicks() const {
if (time_ticks_for_testing_.is_null())
return base::TimeTicks::Now();
return time_ticks_for_testing_;
}
bool NetworkPortalDetectorImpl::DetectionTimeoutIsCancelledForTesting() const {
return detection_timeout_.IsCancelled();
}
int NetworkPortalDetectorImpl::GetRequestTimeoutSec() const {
DCHECK_LE(0, attempt_count_);
const NetworkState* network =
NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
if (!network)
return kBaseRequestTimeoutSec;
if (lazy_detection_enabled_)
return kLazyRequestTimeoutSec;
return attempt_count_ * kBaseRequestTimeoutSec;
}
} // namespace chromeos