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