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