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"
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"
19 #if defined(OS_MACOSX)
20 #include "base/mac/mac_util.h"
24 #include "base/win/windows_version.h"
27 using captive_portal::CaptivePortalResult
;
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.
33 enum 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
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.
59 // |repeat_count| may be 0 if there were no captive portal checks during
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.
65 void RecordRepeatHistograms(CaptivePortalResult result
,
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.
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
),
78 base::Histogram::kUmaTargetedHistogramFlag
);
79 result_repeated_histogram
->Add(repeat_count
);
81 if (repeat_count
== 0)
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
91 base::Histogram::kUmaTargetedHistogramFlag
);
92 result_duration_histogram
->AddTime(result_duration
);
95 int 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
:
105 DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL_IP_ADDRESS
:
106 DETECTION_RESULT_NO_RESPONSE_IP_ADDRESS
;
109 DETECTION_RESULT_NO_RESPONSE_HTTPS_LANDING_URL
:
110 DETECTION_RESULT_NO_RESPONSE
;
111 case captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL
:
114 DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL_IP_ADDRESS
:
115 DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_IP_ADDRESS
;
118 DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL_HTTPS_LANDING_URL
:
119 DETECTION_RESULT_BEHIND_CAPTIVE_PORTAL
;
126 bool 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.
131 // TODO(mmenke): Investigate how well Windows 8's captive portal detection
134 return base::win::GetVersion() >= base::win::VERSION_WIN8
;
142 CaptivePortalService::TestingState
CaptivePortalService::testing_state_
=
145 class CaptivePortalService::RecheckBackoffEntry
: public net::BackoffEntry
{
147 explicit RecheckBackoffEntry(CaptivePortalService
* captive_portal_service
)
149 &captive_portal_service
->recheck_policy().backoff_policy
),
150 captive_portal_service_(captive_portal_service
) {
154 virtual base::TimeTicks
ImplGetTimeNow() const OVERRIDE
{
155 return captive_portal_service_
->GetCurrentTimeTicks();
158 CaptivePortalService
* captive_portal_service_
;
160 DISALLOW_COPY_AND_ASSIGN(RecheckBackoffEntry
);
163 CaptivePortalService::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.
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;
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
;
179 backoff_policy
.multiply_factor
= 2.0;
180 backoff_policy
.jitter_factor
= 0.3;
181 backoff_policy
.maximum_backoff_ms
= 2 * 60 * 1000;
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;
187 backoff_policy
.always_use_initial_delay
= true;
190 CaptivePortalService::CaptivePortalService(Profile
* profile
)
193 captive_portal_detector_(profile
->GetRequestContext()),
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_
);
208 UpdateEnabledState();
211 CaptivePortalService::~CaptivePortalService() {
214 void CaptivePortalService::DetectCaptivePortal() {
215 DCHECK(CalledOnValidThread());
217 // If a request is pending or running, do nothing.
218 if (state_
== STATE_CHECKING_FOR_PORTAL
|| state_
== STATE_TIMER_RUNNING
)
221 base::TimeDelta time_until_next_check
= backoff_entry_
->GetTimeUntilRelease();
223 // Start asynchronously.
224 state_
= STATE_TIMER_RUNNING
;
225 check_captive_portal_timer_
.Start(
227 time_until_next_check
,
229 &CaptivePortalService::DetectCaptivePortalInternal
);
232 void CaptivePortalService::DetectCaptivePortalInternal() {
233 DCHECK(CalledOnValidThread());
234 DCHECK(state_
== STATE_TIMER_RUNNING
|| state_
== STATE_IDLE
);
235 DCHECK(!TimerRunning());
237 state_
= STATE_CHECKING_FOR_PORTAL
;
239 // When not enabled, just claim there's an Internet connection.
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
);
248 captive_portal_detector_
.DetectCaptivePortal(
249 test_url_
, base::Bind(
250 &CaptivePortalService::OnPortalDetectionCompleted
,
251 base::Unretained(this)));
254 void CaptivePortalService::OnPortalDetectionCompleted(
255 const captive_portal::CaptivePortalDetector::Results
& results
) {
256 DCHECK(CalledOnValidThread());
257 DCHECK_EQ(STATE_CHECKING_FOR_PORTAL
, state_
);
258 DCHECK(!TimerRunning());
261 CaptivePortalResult result
= results
.result
;
262 const base::TimeDelta
& retry_after_delta
= results
.retry_after_delta
;
263 base::TimeTicks now
= GetCurrentTimeTicks();
265 // Record histograms.
266 UMA_HISTOGRAM_ENUMERATION("CaptivePortal.DetectResult",
267 GetHistogramEntryForDetectionResult(results
),
268 DETECTION_RESULT_COUNT
);
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_
);
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
279 RecordRepeatHistograms(last_detection_result_
,
280 num_checks_with_same_result_
,
281 now
- first_check_time_with_same_result_
);
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;
289 // Reset the backoff entry both to update the default time and clear
290 // previous failures.
291 ResetBackoffEntry(result
);
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
299 DCHECK_LE(1, num_checks_with_same_result_
);
300 ++num_checks_with_same_result_
;
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);
308 last_check_time_
= now
;
313 void CaptivePortalService::Shutdown() {
314 DCHECK(CalledOnValidThread());
316 RecordRepeatHistograms(
317 last_detection_result_
,
318 num_checks_with_same_result_
,
319 GetCurrentTimeTicks() - first_check_time_with_same_result_
);
323 void CaptivePortalService::OnResult(CaptivePortalResult result
) {
324 DCHECK_EQ(STATE_CHECKING_FOR_PORTAL
, state_
);
328 results
.previous_result
= last_detection_result_
;
329 results
.result
= result
;
330 last_detection_result_
= result
;
332 content::NotificationService::current()->Notify(
333 chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT
,
334 content::Source
<Profile
>(profile_
),
335 content::Details
<Results
>(&results
));
338 void 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
;
345 recheck_policy_
.backoff_policy
.initial_delay_ms
=
346 recheck_policy_
.initial_backoff_no_portal_ms
;
349 backoff_entry_
.reset(new RecheckBackoffEntry(this));
352 void CaptivePortalService::UpdateEnabledState() {
353 DCHECK(CalledOnValidThread());
354 bool enabled_before
= enabled_
;
355 enabled_
= testing_state_
!= DISABLED_FOR_TESTING
&&
356 resolve_errors_with_web_service_
.GetValue();
358 if (testing_state_
!= SKIP_OS_CHECK_FOR_TESTING
&&
359 ShouldDeferToNativeCaptivePortalDetection()) {
363 if (enabled_before
== enabled_
)
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();
371 ResetBackoffEntry(last_detection_result_
);
373 if (state_
== STATE_CHECKING_FOR_PORTAL
|| state_
== STATE_TIMER_RUNNING
) {
374 // If a captive portal check was running or pending, cancel check
376 check_captive_portal_timer_
.Stop();
377 captive_portal_detector_
.Cancel();
380 // Since a captive portal request was queued or running, something may be
381 // expecting to receive a captive portal result.
382 DetectCaptivePortal();
386 base::TimeTicks
CaptivePortalService::GetCurrentTimeTicks() const {
387 if (time_ticks_for_testing_
.is_null())
388 return base::TimeTicks::Now();
390 return time_ticks_for_testing_
;
393 bool CaptivePortalService::DetectionInProgress() const {
394 return state_
== STATE_CHECKING_FOR_PORTAL
;
397 bool CaptivePortalService::TimerRunning() const {
398 return check_captive_portal_timer_
.IsRunning();