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