1// Copyright (c) 2012 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/captive_portal/captive_portal_service.h"
6
7#include "base/bind.h"
8#include "base/bind_helpers.h"
9#include "base/logging.h"
10#include "base/message_loop/message_loop.h"
11#include "base/metrics/histogram.h"
12#include "base/prefs/pref_service.h"
13#include "chrome/browser/chrome_notification_types.h"
14#include "chrome/browser/profiles/profile.h"
15#include "chrome/common/pref_names.h"
16#include "components/captive_portal/captive_portal_types.h"
17#include "content/public/browser/notification_service.h"
18
19#if defined(OS_MACOSX)
20#include "base/mac/mac_util.h"
21#endif
22
23#if defined(OS_WIN)
24#include "base/win/windows_version.h"
25#endif
26
27using captive_portal::CaptivePortalResult;
28
29namespace {
30
31// Make sure this enum is in sync with CaptivePortalDetectionResult enum
32// in histograms.xml. This enum is append-only, don't modify existing values.
33enum CaptivePortalDetectionResult {
34  // There's a confirmed connection to the Internet.
35  DETECTION_RESULT_INTERNET_CONNECTED,
36  // Received a network or HTTP error, or a non-HTTP response.
37  DETECTION_RESULT_NO_RESPONSE,
38  // Encountered a captive portal with a non-HTTPS landing URL.
39  DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL,
40  // Received a network or HTTP error with an HTTPS landing URL.
41  DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL,
42  // Encountered a captive portal with an HTTPS landing URL.
43  DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL,
44  // Received a network or HTTP error, or a non-HTTP response with IP address.
45  DETECTION_RESULT_NO_RESPONSE_IP_ADDRESS,
46  // Encountered a captive portal with a non-HTTPS, IP address landing URL.
47  DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_IP_ADDRESS,
48  // Received a network or HTTP error with an HTTPS, IP address landing URL.
49  DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL_IP_ADDRESS,
50  // Encountered a captive portal with an HTTPS, IP address landing URL.
51  DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL_IP_ADDRESS,
52  DETECTION_RESULT_COUNT
53};
54
55// Records histograms relating to how often captive portal detection attempts
56// ended with |result| in a row, and for how long |result| was the last result
57// of a detection attempt.  Recorded both on quit and on a new Result.
58//
59// |repeat_count| may be 0 if there were no captive portal checks during
60// a session.
61//
62// |result_duration| is the time between when a captive portal check first
63// returned |result| and when a check returned a different result, or when the
64// CaptivePortalService was shut down.
65void RecordRepeatHistograms(CaptivePortalResult result,
66                            int repeat_count,
67                            base::TimeDelta result_duration) {
68  // Histogram macros can't be used with variable names, since they cache
69  // pointers, so have to use the histogram functions directly.
70
71  // Record number of times the last result was received in a row.
72  base::HistogramBase* result_repeated_histogram =
73      base::Histogram::FactoryGet(
74          "CaptivePortal.ResultRepeated." + CaptivePortalResultToString(result),
75          1,  // min
76          100,  // max
77          100,  // bucket_count
78          base::Histogram::kUmaTargetedHistogramFlag);
79  result_repeated_histogram->Add(repeat_count);
80
81  if (repeat_count == 0)
82    return;
83
84  // Time between first request that returned |result| and now.
85  base::HistogramBase* result_duration_histogram =
86      base::Histogram::FactoryTimeGet(
87          "CaptivePortal.ResultDuration." + CaptivePortalResultToString(result),
88          base::TimeDelta::FromSeconds(1),  // min
89          base::TimeDelta::FromHours(1),  // max
90          50,  // bucket_count
91          base::Histogram::kUmaTargetedHistogramFlag);
92  result_duration_histogram->AddTime(result_duration);
93}
94
95int GetHistogramEntryForDetectionResult(
96    const captive_portal::CaptivePortalDetector::Results& results) {
97  bool is_https = results.landing_url.SchemeIs("https");
98  bool is_ip = results.landing_url.HostIsIPAddress();
99  switch (results.result) {
100    case captive_portal::RESULT_INTERNET_CONNECTED:
101      return DETECTION_RESULT_INTERNET_CONNECTED;
102    case captive_portal::RESULT_NO_RESPONSE:
103      if (is_ip) {
104        return is_https ?
105            DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL_IP_ADDRESS :
106            DETECTION_RESULT_NO_RESPONSE_IP_ADDRESS;
107      }
108      return is_https ?
109          DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL :
110          DETECTION_RESULT_NO_RESPONSE;
111    case captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL:
112      if (is_ip) {
113        return is_https ?
114          DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL_IP_ADDRESS :
115          DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_IP_ADDRESS;
116      }
117      return is_https ?
118          DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL :
119          DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL;
120    default:
121      NOTREACHED();
122      return -1;
123  }
124}
125
126bool ShouldDeferToNativeCaptivePortalDetection() {
127  // On Windows 8, defer to the native captive portal detection.  OSX Lion and
128  // later also have captive portal detection, but experimentally, this code
129  // works in cases its does not.
130  //
131  // TODO(mmenke): Investigate how well Windows 8's captive portal detection
132  // works.
133#if defined(OS_WIN)
134  return base::win::GetVersion() >= base::win::VERSION_WIN8;
135#else
136  return false;
137#endif
138}
139
140}  // namespace
141
142CaptivePortalService::TestingState CaptivePortalService::testing_state_ =
143    NOT_TESTING;
144
145class CaptivePortalService::RecheckBackoffEntry : public net::BackoffEntry {
146 public:
147  explicit RecheckBackoffEntry(CaptivePortalService* captive_portal_service)
148      : net::BackoffEntry(
149            &captive_portal_service->recheck_policy().backoff_policy),
150        captive_portal_service_(captive_portal_service) {
151  }
152
153 private:
154  virtual base::TimeTicks ImplGetTimeNow() const OVERRIDE {
155    return captive_portal_service_->GetCurrentTimeTicks();
156  }
157
158  CaptivePortalService* captive_portal_service_;
159
160  DISALLOW_COPY_AND_ASSIGN(RecheckBackoffEntry);
161};
162
163CaptivePortalService::RecheckPolicy::RecheckPolicy()
164    : initial_backoff_no_portal_ms(600 * 1000),
165      initial_backoff_portal_ms(20 * 1000) {
166  // Receiving a new Result is considered a success.  All subsequent requests
167  // that get the same Result are considered "failures", so a value of N
168  // means exponential backoff starts after getting a result N + 2 times:
169  // +1 for the initial success, and +1 because N failures are ignored.
170  //
171  // A value of 6 means to start backoff on the 7th failure, which is the 8th
172  // time the same result is received.
173  backoff_policy.num_errors_to_ignore = 6;
174
175  // It doesn't matter what this is initialized to.  It will be overwritten
176  // after the first captive portal detection request.
177  backoff_policy.initial_delay_ms = initial_backoff_no_portal_ms;
178
179  backoff_policy.multiply_factor = 2.0;
180  backoff_policy.jitter_factor = 0.3;
181  backoff_policy.maximum_backoff_ms = 2 * 60 * 1000;
182
183  // -1 means the entry never expires.  This doesn't really matter, as the
184  // service never checks for its expiration.
185  backoff_policy.entry_lifetime_ms = -1;
186
187  backoff_policy.always_use_initial_delay = true;
188}
189
190CaptivePortalService::CaptivePortalService(Profile* profile)
191    : profile_(profile),
192      state_(STATE_IDLE),
193      captive_portal_detector_(profile->GetRequestContext()),
194      enabled_(false),
195      last_detection_result_(captive_portal::RESULT_INTERNET_CONNECTED),
196      num_checks_with_same_result_(0),
197      test_url_(captive_portal::CaptivePortalDetector::kDefaultURL) {
198  // The order matters here:
199  // |resolve_errors_with_web_service_| must be initialized and |backoff_entry_|
200  // created before the call to UpdateEnabledState.
201  resolve_errors_with_web_service_.Init(
202      prefs::kAlternateErrorPagesEnabled,
203      profile_->GetPrefs(),
204      base::Bind(&CaptivePortalService::UpdateEnabledState,
205                 base::Unretained(this)));
206  ResetBackoffEntry(last_detection_result_);
207
208  UpdateEnabledState();
209}
210
211CaptivePortalService::~CaptivePortalService() {
212}
213
214void CaptivePortalService::DetectCaptivePortal() {
215  DCHECK(CalledOnValidThread());
216
217  // If a request is pending or running, do nothing.
218  if (state_ == STATE_CHECKING_FOR_PORTAL || state_ == STATE_TIMER_RUNNING)
219    return;
220
221  base::TimeDelta time_until_next_check = backoff_entry_->GetTimeUntilRelease();
222
223  // Start asynchronously.
224  state_ = STATE_TIMER_RUNNING;
225  check_captive_portal_timer_.Start(
226      FROM_HERE,
227      time_until_next_check,
228      this,
229      &CaptivePortalService::DetectCaptivePortalInternal);
230}
231
232void CaptivePortalService::DetectCaptivePortalInternal() {
233  DCHECK(CalledOnValidThread());
234  DCHECK(state_ == STATE_TIMER_RUNNING || state_ == STATE_IDLE);
235  DCHECK(!TimerRunning());
236
237  state_ = STATE_CHECKING_FOR_PORTAL;
238
239  // When not enabled, just claim there's an Internet connection.
240  if (!enabled_) {
241    // Count this as a success, so the backoff entry won't apply exponential
242    // backoff, but will apply the standard delay.
243    backoff_entry_->InformOfRequest(true);
244    OnResult(captive_portal::RESULT_INTERNET_CONNECTED);
245    return;
246  }
247
248  captive_portal_detector_.DetectCaptivePortal(
249      test_url_, base::Bind(
250          &CaptivePortalService::OnPortalDetectionCompleted,
251          base::Unretained(this)));
252}
253
254void CaptivePortalService::OnPortalDetectionCompleted(
255    const captive_portal::CaptivePortalDetector::Results& results) {
256  DCHECK(CalledOnValidThread());
257  DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_);
258  DCHECK(!TimerRunning());
259  DCHECK(enabled_);
260
261  CaptivePortalResult result = results.result;
262  const base::TimeDelta& retry_after_delta = results.retry_after_delta;
263  base::TimeTicks now = GetCurrentTimeTicks();
264
265  // Record histograms.
266  UMA_HISTOGRAM_ENUMERATION("CaptivePortal.DetectResult",
267                            GetHistogramEntryForDetectionResult(results),
268                            DETECTION_RESULT_COUNT);
269
270  // If this isn't the first captive portal result, record stats.
271  if (!last_check_time_.is_null()) {
272    UMA_HISTOGRAM_LONG_TIMES("CaptivePortal.TimeBetweenChecks",
273                             now - last_check_time_);
274
275    if (last_detection_result_ != result) {
276      // If the last result was different from the result of the latest test,
277      // record histograms about the previous period over which the result was
278      // the same.
279      RecordRepeatHistograms(last_detection_result_,
280                             num_checks_with_same_result_,
281                             now - first_check_time_with_same_result_);
282    }
283  }
284
285  if (last_check_time_.is_null() || result != last_detection_result_) {
286    first_check_time_with_same_result_ = now;
287    num_checks_with_same_result_ = 1;
288
289    // Reset the backoff entry both to update the default time and clear
290    // previous failures.
291    ResetBackoffEntry(result);
292
293    backoff_entry_->SetCustomReleaseTime(now + retry_after_delta);
294    // The BackoffEntry is not informed of this request, so there's no delay
295    // before the next request.  This allows for faster login when a captive
296    // portal is first detected.  It can also help when moving between captive
297    // portals.
298  } else {
299    DCHECK_LE(1, num_checks_with_same_result_);
300    ++num_checks_with_same_result_;
301
302    // Requests that have the same Result as the last one are considered
303    // "failures", to trigger backoff.
304    backoff_entry_->SetCustomReleaseTime(now + retry_after_delta);
305    backoff_entry_->InformOfRequest(false);
306  }
307
308  last_check_time_ = now;
309
310  OnResult(result);
311}
312
313void CaptivePortalService::Shutdown() {
314  DCHECK(CalledOnValidThread());
315  if (enabled_) {
316    RecordRepeatHistograms(
317        last_detection_result_,
318        num_checks_with_same_result_,
319        GetCurrentTimeTicks() - first_check_time_with_same_result_);
320  }
321}
322
323void CaptivePortalService::OnResult(CaptivePortalResult result) {
324  DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_);
325  state_ = STATE_IDLE;
326
327  Results results;
328  results.previous_result = last_detection_result_;
329  results.result = result;
330  last_detection_result_ = result;
331
332  content::NotificationService::current()->Notify(
333      chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT,
334      content::Source<Profile>(profile_),
335      content::Details<Results>(&results));
336}
337
338void CaptivePortalService::ResetBackoffEntry(CaptivePortalResult result) {
339  if (!enabled_ || result == captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL) {
340    // Use the shorter time when the captive portal service is not enabled, or
341    // behind a captive portal.
342    recheck_policy_.backoff_policy.initial_delay_ms =
343        recheck_policy_.initial_backoff_portal_ms;
344  } else {
345    recheck_policy_.backoff_policy.initial_delay_ms =
346        recheck_policy_.initial_backoff_no_portal_ms;
347  }
348
349  backoff_entry_.reset(new RecheckBackoffEntry(this));
350}
351
352void CaptivePortalService::UpdateEnabledState() {
353  DCHECK(CalledOnValidThread());
354  bool enabled_before = enabled_;
355  enabled_ = testing_state_ != DISABLED_FOR_TESTING &&
356             resolve_errors_with_web_service_.GetValue();
357
358  if (testing_state_ != SKIP_OS_CHECK_FOR_TESTING &&
359      ShouldDeferToNativeCaptivePortalDetection()) {
360    enabled_ = false;
361  }
362
363  if (enabled_before == enabled_)
364    return;
365
366  // Clear data used for histograms.
367  num_checks_with_same_result_ = 0;
368  first_check_time_with_same_result_ = base::TimeTicks();
369  last_check_time_ = base::TimeTicks();
370
371  ResetBackoffEntry(last_detection_result_);
372
373  if (state_ == STATE_CHECKING_FOR_PORTAL || state_ == STATE_TIMER_RUNNING) {
374    // If a captive portal check was running or pending, cancel check
375    // and the timer.
376    check_captive_portal_timer_.Stop();
377    captive_portal_detector_.Cancel();
378    state_ = STATE_IDLE;
379
380    // Since a captive portal request was queued or running, something may be
381    // expecting to receive a captive portal result.
382    DetectCaptivePortal();
383  }
384}
385
386base::TimeTicks CaptivePortalService::GetCurrentTimeTicks() const {
387  if (time_ticks_for_testing_.is_null())
388    return base::TimeTicks::Now();
389  else
390    return time_ticks_for_testing_;
391}
392
393bool CaptivePortalService::DetectionInProgress() const {
394  return state_ == STATE_CHECKING_FOR_PORTAL;
395}
396
397bool CaptivePortalService::TimerRunning() const {
398  return check_captive_portal_timer_.IsRunning();
399}
400