network_portal_detector_impl.cc revision 8bcbed890bc3ce4d7a057a8f32cab53fa534672e
1// Copyright (c) 2013 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/chromeos/net/network_portal_detector_impl.h"
6
7#include "base/bind.h"
8#include "base/command_line.h"
9#include "base/logging.h"
10#include "base/message_loop/message_loop.h"
11#include "base/metrics/histogram.h"
12#include "chrome/browser/chrome_notification_types.h"
13#include "chromeos/dbus/shill_stub_helper.h"
14#include "chromeos/network/network_state.h"
15#include "chromeos/network/network_state_handler.h"
16#include "content/public/browser/notification_service.h"
17#include "grit/generated_resources.h"
18#include "net/http/http_status_code.h"
19#include "third_party/cros_system_api/dbus/service_constants.h"
20#include "ui/base/l10n/l10n_util.h"
21
22using captive_portal::CaptivePortalDetector;
23
24namespace chromeos {
25
26namespace {
27
28// Maximum number of portal detections for the same default network
29// after network change.
30const int kMaxRequestAttempts = 3;
31
32// Minimum timeout between consecutive portal checks for the same
33// network.
34const int kMinTimeBetweenAttemptsSec = 3;
35
36// Delay before portal detection caused by changes in proxy settings.
37const int kProxyChangeDelaySec = 1;
38
39// Delay between consecutive portal checks for a network in lazy mode.
40const int kLazyCheckIntervalSec = 5;
41
42std::string CaptivePortalStatusString(
43    NetworkPortalDetectorImpl::CaptivePortalStatus status) {
44  switch (status) {
45    case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_UNKNOWN:
46      return l10n_util::GetStringUTF8(
47          IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_UNKNOWN);
48    case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_OFFLINE:
49      return l10n_util::GetStringUTF8(
50          IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_OFFLINE);
51    case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_ONLINE:
52      return l10n_util::GetStringUTF8(
53          IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_ONLINE);
54    case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_PORTAL:
55      return l10n_util::GetStringUTF8(
56          IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_PORTAL);
57    case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_PROXY_AUTH_REQUIRED:
58      return l10n_util::GetStringUTF8(
59          IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_PROXY_AUTH_REQUIRED);
60    case NetworkPortalDetectorImpl::CAPTIVE_PORTAL_STATUS_COUNT:
61      NOTREACHED();
62  }
63  return l10n_util::GetStringUTF8(
64      IDS_CHROMEOS_CAPTIVE_PORTAL_STATUS_UNRECOGNIZED);
65}
66
67}  // namespace
68
69////////////////////////////////////////////////////////////////////////////////
70// NetworkPortalDetectorImpl, public:
71
72NetworkPortalDetectorImpl::NetworkPortalDetectorImpl(
73    const scoped_refptr<net::URLRequestContextGetter>& request_context)
74    : state_(STATE_IDLE),
75      test_url_(CaptivePortalDetector::kDefaultURL),
76      enabled_(false),
77      weak_ptr_factory_(this),
78      attempt_count_(0),
79      lazy_detection_enabled_(false),
80      lazy_check_interval_(base::TimeDelta::FromSeconds(kLazyCheckIntervalSec)),
81      min_time_between_attempts_(
82          base::TimeDelta::FromSeconds(kMinTimeBetweenAttemptsSec)),
83      request_timeout_for_testing_initialized_(false) {
84  captive_portal_detector_.reset(new CaptivePortalDetector(request_context));
85
86  registrar_.Add(this,
87                 chrome::NOTIFICATION_LOGIN_PROXY_CHANGED,
88                 content::NotificationService::AllSources());
89  registrar_.Add(this,
90                 chrome::NOTIFICATION_AUTH_SUPPLIED,
91                 content::NotificationService::AllSources());
92  registrar_.Add(this,
93                 chrome::NOTIFICATION_AUTH_CANCELLED,
94                 content::NotificationService::AllSources());
95
96  NetworkHandler::Get()->network_state_handler()->AddObserver(
97      this, FROM_HERE);
98}
99
100NetworkPortalDetectorImpl::~NetworkPortalDetectorImpl() {
101  DCHECK(CalledOnValidThread());
102
103  detection_task_.Cancel();
104  detection_timeout_.Cancel();
105
106  captive_portal_detector_->Cancel();
107  captive_portal_detector_.reset();
108  observers_.Clear();
109  if (NetworkHandler::IsInitialized()) {
110    NetworkHandler::Get()->network_state_handler()->RemoveObserver(
111        this, FROM_HERE);
112  }
113}
114
115void NetworkPortalDetectorImpl::AddObserver(Observer* observer) {
116  DCHECK(CalledOnValidThread());
117  if (!observer || observers_.HasObserver(observer))
118    return;
119  observers_.AddObserver(observer);
120}
121
122void NetworkPortalDetectorImpl::AddAndFireObserver(Observer* observer) {
123  DCHECK(CalledOnValidThread());
124  if (!observer)
125    return;
126  AddObserver(observer);
127  const NetworkState* network =
128      NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
129  observer->OnPortalDetectionCompleted(network, GetCaptivePortalState(network));
130}
131
132void NetworkPortalDetectorImpl::RemoveObserver(Observer* observer) {
133  DCHECK(CalledOnValidThread());
134  if (observer)
135    observers_.RemoveObserver(observer);
136}
137
138bool NetworkPortalDetectorImpl::IsEnabled() {
139  return enabled_;
140}
141
142void NetworkPortalDetectorImpl::Enable(bool start_detection) {
143  DCHECK(CalledOnValidThread());
144  if (enabled_)
145    return;
146  enabled_ = true;
147  DCHECK(!IsPortalCheckPending());
148  DCHECK(!IsCheckingForPortal());
149  DCHECK(!lazy_detection_enabled());
150  if (!start_detection)
151    return;
152  state_ = STATE_IDLE;
153  attempt_count_ = 0;
154  const NetworkState* default_network =
155      NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
156  if (!default_network)
157    return;
158  portal_state_map_.erase(default_network->path());
159  DCHECK(CanPerformDetection());
160  DetectCaptivePortal(base::TimeDelta());
161}
162
163NetworkPortalDetectorImpl::CaptivePortalState
164NetworkPortalDetectorImpl::GetCaptivePortalState(const NetworkState* network) {
165  DCHECK(CalledOnValidThread());
166  if (!network)
167    return CaptivePortalState();
168  CaptivePortalStateMap::const_iterator it =
169      portal_state_map_.find(network->path());
170  if (it == portal_state_map_.end())
171    return CaptivePortalState();
172  return it->second;
173}
174
175bool NetworkPortalDetectorImpl::StartDetectionIfIdle() {
176  if (IsPortalCheckPending() || IsCheckingForPortal())
177    return false;
178  if (!CanPerformDetection())
179    attempt_count_ = 0;
180  DCHECK(CanPerformDetection());
181  DetectCaptivePortal(base::TimeDelta());
182  return true;
183}
184
185void NetworkPortalDetectorImpl::EnableLazyDetection() {
186  if (lazy_detection_enabled())
187    return;
188  lazy_detection_enabled_ = true;
189  VLOG(1) << "Lazy detection mode enabled.";
190  StartDetectionIfIdle();
191}
192
193void NetworkPortalDetectorImpl::DisableLazyDetection() {
194  if (!lazy_detection_enabled())
195    return;
196  lazy_detection_enabled_ = false;
197  if (attempt_count_ == kMaxRequestAttempts && IsPortalCheckPending())
198    CancelPortalDetection();
199  VLOG(1) << "Lazy detection mode disabled.";
200}
201
202void NetworkPortalDetectorImpl::DefaultNetworkChanged(
203    const NetworkState* default_network) {
204  DCHECK(CalledOnValidThread());
205  if (!default_network) {
206    default_network_id_.clear();
207    return;
208  }
209
210  default_network_id_ = default_network->guid();
211
212  bool network_changed = (default_service_path_ != default_network->path());
213  default_service_path_ = default_network->path();
214
215  bool connection_state_changed = (default_connection_state_ !=
216                                   default_network->connection_state());
217  default_connection_state_ = default_network->connection_state();
218
219  if (network_changed || connection_state_changed) {
220    attempt_count_ = 0;
221    CancelPortalDetection();
222  }
223
224  if (CanPerformDetection() &&
225      NetworkState::StateIsConnected(default_connection_state_)) {
226    // Initiate Captive Portal detection if network's captive
227    // portal state is unknown (e.g. for freshly created networks),
228    // offline or if network connection state was changed.
229    CaptivePortalState state = GetCaptivePortalState(default_network);
230    if (state.status == CAPTIVE_PORTAL_STATUS_UNKNOWN ||
231        state.status == CAPTIVE_PORTAL_STATUS_OFFLINE ||
232        (!network_changed && connection_state_changed)) {
233      DetectCaptivePortal(base::TimeDelta());
234    }
235  }
236}
237
238////////////////////////////////////////////////////////////////////////////////
239// NetworkPortalDetectorImpl, private:
240
241bool NetworkPortalDetectorImpl::CanPerformDetection() const {
242  if (IsPortalCheckPending() || IsCheckingForPortal())
243    return false;
244  return attempt_count_ < kMaxRequestAttempts || lazy_detection_enabled();
245}
246
247void NetworkPortalDetectorImpl::DetectCaptivePortal(
248    const base::TimeDelta& delay) {
249  DCHECK(CanPerformDetection());
250
251  if (!IsEnabled())
252    return;
253
254  detection_task_.Cancel();
255  detection_timeout_.Cancel();
256  state_ = STATE_PORTAL_CHECK_PENDING;
257
258  next_attempt_delay_ = delay;
259  if (attempt_count_ > 0) {
260    base::TimeTicks now = GetCurrentTimeTicks();
261    base::TimeDelta elapsed_time;
262
263    base::TimeDelta delay_between_attempts = min_time_between_attempts_;
264    if (attempt_count_ == kMaxRequestAttempts) {
265      DCHECK(lazy_detection_enabled());
266      delay_between_attempts = lazy_check_interval_;
267    }
268    if (now > attempt_start_time_)
269      elapsed_time = now - attempt_start_time_;
270    if (elapsed_time < delay_between_attempts &&
271        delay_between_attempts - elapsed_time > next_attempt_delay_) {
272      next_attempt_delay_ = delay_between_attempts - elapsed_time;
273    }
274  } else {
275    detection_start_time_ = GetCurrentTimeTicks();
276  }
277  detection_task_.Reset(
278      base::Bind(&NetworkPortalDetectorImpl::DetectCaptivePortalTask,
279                 weak_ptr_factory_.GetWeakPtr()));
280  base::MessageLoop::current()->PostDelayedTask(
281      FROM_HERE, detection_task_.callback(), next_attempt_delay_);
282}
283
284void NetworkPortalDetectorImpl::DetectCaptivePortalTask() {
285  DCHECK(IsPortalCheckPending());
286
287  state_ = STATE_CHECKING_FOR_PORTAL;
288  attempt_start_time_ = GetCurrentTimeTicks();
289
290  if (attempt_count_ < kMaxRequestAttempts) {
291    ++attempt_count_;
292    VLOG(1) << "Portal detection started: "
293            << "network=" << default_network_id_ << ", "
294            << "attempt=" << attempt_count_ << " of " << kMaxRequestAttempts;
295  } else {
296    DCHECK(lazy_detection_enabled());
297    VLOG(1) << "Lazy portal detection attempt started";
298  }
299
300  captive_portal_detector_->DetectCaptivePortal(
301      test_url_,
302      base::Bind(&NetworkPortalDetectorImpl::OnPortalDetectionCompleted,
303                 weak_ptr_factory_.GetWeakPtr()));
304  detection_timeout_.Reset(
305      base::Bind(&NetworkPortalDetectorImpl::PortalDetectionTimeout,
306                 weak_ptr_factory_.GetWeakPtr()));
307  base::TimeDelta request_timeout;
308
309  // For easier unit testing check for testing state is performed here
310  // and not in the GetRequestTimeoutSec().
311  if (request_timeout_for_testing_initialized_)
312    request_timeout = request_timeout_for_testing_;
313  else
314    request_timeout = base::TimeDelta::FromSeconds(GetRequestTimeoutSec());
315  base::MessageLoop::current()->PostDelayedTask(
316      FROM_HERE, detection_timeout_.callback(), request_timeout);
317}
318
319void NetworkPortalDetectorImpl::PortalDetectionTimeout() {
320  DCHECK(CalledOnValidThread());
321  DCHECK(IsCheckingForPortal());
322
323  VLOG(1) << "Portal detection timeout: network=" << default_network_id_;
324
325  captive_portal_detector_->Cancel();
326  CaptivePortalDetector::Results results;
327  results.result = captive_portal::RESULT_NO_RESPONSE;
328  OnPortalDetectionCompleted(results);
329}
330
331void NetworkPortalDetectorImpl::CancelPortalDetection() {
332  if (IsPortalCheckPending())
333    detection_task_.Cancel();
334  else if (IsCheckingForPortal())
335    captive_portal_detector_->Cancel();
336  detection_timeout_.Cancel();
337  state_ = STATE_IDLE;
338}
339
340void NetworkPortalDetectorImpl::OnPortalDetectionCompleted(
341    const CaptivePortalDetector::Results& results) {
342  captive_portal::Result result = results.result;
343  int response_code = results.response_code;
344
345  if (shill_stub_helper::IsStubPortalledWifiEnabled(default_service_path_)) {
346    result = captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL;
347    response_code = 200;
348  }
349
350  DCHECK(CalledOnValidThread());
351  DCHECK(IsCheckingForPortal());
352
353  VLOG(1) << "Portal detection completed: "
354          << "network=" << default_network_id_ << ", "
355          << "result=" << CaptivePortalDetector::CaptivePortalResultToString(
356              results.result) << ", "
357          << "response_code=" << results.response_code;
358
359  state_ = STATE_IDLE;
360  detection_timeout_.Cancel();
361
362  const NetworkState* default_network =
363      NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
364
365  CaptivePortalState state;
366  state.response_code = response_code;
367  switch (result) {
368    case captive_portal::RESULT_NO_RESPONSE:
369      if (attempt_count_ >= kMaxRequestAttempts) {
370        if (state.response_code == net::HTTP_PROXY_AUTHENTICATION_REQUIRED) {
371          state.status = CAPTIVE_PORTAL_STATUS_PROXY_AUTH_REQUIRED;
372        } else if (default_network && (default_network->connection_state() ==
373                                       shill::kStatePortal)) {
374          // Take into account shill's detection results.
375          state.status = CAPTIVE_PORTAL_STATUS_PORTAL;
376          LOG(WARNING) << "Network " << default_network->guid() << " "
377                       << "is marked as "
378                       << CaptivePortalStatusString(state.status) << " "
379                       << "despite the fact that CaptivePortalDetector "
380                       << "received no response";
381        } else {
382          state.status = CAPTIVE_PORTAL_STATUS_OFFLINE;
383        }
384        SetCaptivePortalState(default_network, state);
385      } else {
386        DCHECK(CanPerformDetection());
387        DetectCaptivePortal(results.retry_after_delta);
388      }
389      break;
390    case captive_portal::RESULT_INTERNET_CONNECTED:
391      state.status = CAPTIVE_PORTAL_STATUS_ONLINE;
392      SetCaptivePortalState(default_network, state);
393      break;
394    case captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL:
395      state.status = CAPTIVE_PORTAL_STATUS_PORTAL;
396      SetCaptivePortalState(default_network, state);
397      break;
398    default:
399      break;
400  }
401
402  TryLazyDetection();
403}
404
405void NetworkPortalDetectorImpl::TryLazyDetection() {
406  if (lazy_detection_enabled() && CanPerformDetection())
407    DetectCaptivePortal(base::TimeDelta());
408}
409
410void NetworkPortalDetectorImpl::Observe(
411    int type,
412    const content::NotificationSource& source,
413    const content::NotificationDetails& details) {
414  if (type == chrome::NOTIFICATION_LOGIN_PROXY_CHANGED ||
415      type == chrome::NOTIFICATION_AUTH_SUPPLIED ||
416      type == chrome::NOTIFICATION_AUTH_CANCELLED) {
417    VLOG(1) << "Restarting portal detection due to proxy change.";
418    attempt_count_ = 0;
419    if (IsPortalCheckPending())
420      return;
421    CancelPortalDetection();
422    DCHECK(CanPerformDetection());
423    DetectCaptivePortal(base::TimeDelta::FromSeconds(kProxyChangeDelaySec));
424  }
425}
426
427bool NetworkPortalDetectorImpl::IsPortalCheckPending() const {
428  return state_ == STATE_PORTAL_CHECK_PENDING;
429}
430
431bool NetworkPortalDetectorImpl::IsCheckingForPortal() const {
432  return state_ == STATE_CHECKING_FOR_PORTAL;
433}
434
435void NetworkPortalDetectorImpl::SetCaptivePortalState(
436    const NetworkState* network,
437    const CaptivePortalState& state) {
438  if (!detection_start_time_.is_null()) {
439    UMA_HISTOGRAM_TIMES("CaptivePortal.OOBE.DetectionDuration",
440                        GetCurrentTimeTicks() - detection_start_time_);
441  }
442
443  if (!network) {
444    NotifyPortalDetectionCompleted(network, state);
445    return;
446  }
447
448  CaptivePortalStateMap::const_iterator it =
449      portal_state_map_.find(network->path());
450  if (it == portal_state_map_.end() ||
451      it->second.status != state.status ||
452      it->second.response_code != state.response_code) {
453    VLOG(1) << "Updating Chrome Captive Portal state: "
454            << "network=" << network->guid() << ", "
455            << "status=" << CaptivePortalStatusString(state.status) << ", "
456            << "response_code=" << state.response_code;
457    portal_state_map_[network->path()] = state;
458  }
459  NotifyPortalDetectionCompleted(network, state);
460}
461
462void NetworkPortalDetectorImpl::NotifyPortalDetectionCompleted(
463    const NetworkState* network,
464    const CaptivePortalState& state) {
465  FOR_EACH_OBSERVER(Observer, observers_,
466                    OnPortalDetectionCompleted(network, state));
467}
468
469base::TimeTicks NetworkPortalDetectorImpl::GetCurrentTimeTicks() const {
470  if (time_ticks_for_testing_.is_null())
471    return base::TimeTicks::Now();
472  return time_ticks_for_testing_;
473}
474
475bool NetworkPortalDetectorImpl::DetectionTimeoutIsCancelledForTesting() const {
476  return detection_timeout_.IsCancelled();
477}
478
479int NetworkPortalDetectorImpl::GetRequestTimeoutSec() const {
480  DCHECK_LE(0, attempt_count_);
481  const NetworkState* network =
482      NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
483  if (!network)
484    return kBaseRequestTimeoutSec;
485  if (lazy_detection_enabled_)
486    return kLazyRequestTimeoutSec;
487  return attempt_count_ * kBaseRequestTimeoutSec;
488}
489
490}  // namespace chromeos
491