// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/captive_portal/captive_portal_service.h" #include "base/bind.h" #include "base/command_line.h" #include "base/logging.h" #include "base/message_loop.h" #include "base/metrics/histogram.h" #include "base/rand_util.h" #include "base/string_number_conversions.h" #include "chrome/browser/prefs/pref_service.h" #include "chrome/browser/profiles/profile.h" #include "chrome/common/chrome_notification_types.h" #include "chrome/common/pref_names.h" #include "content/public/browser/notification_service.h" #include "net/base/load_flags.h" #include "net/http/http_response_headers.h" #include "net/url_request/url_fetcher.h" #include "net/url_request/url_request_status.h" #if defined(OS_MACOSX) #include "base/mac/mac_util.h" #endif #if defined(OS_WIN) #include "base/win/windows_version.h" #endif namespace captive_portal { namespace { // The test URL. When connected to the Internet, it should return a blank page // with a 204 status code. When behind a captive portal, requests for this // URL should get an HTTP redirect or a login page. When neither is true, // no server should respond to requests for this URL. // TODO(mmenke): Look into getting another domain, for better load management. // Need a cookieless domain, so can't use clients*.google.com. const char* const kDefaultTestURL = "http://www.gstatic.com/generate_204"; // Used for histograms. const char* const kCaptivePortalResultNames[] = { "InternetConnected", "NoResponse", "BehindCaptivePortal", "NumCaptivePortalResults", }; COMPILE_ASSERT(arraysize(kCaptivePortalResultNames) == RESULT_COUNT + 1, captive_portal_result_name_count_mismatch); // Used for histograms. std::string CaptivePortalResultToString(Result result) { DCHECK_GE(result, 0); DCHECK_LT(static_cast(result), arraysize(kCaptivePortalResultNames)); return kCaptivePortalResultNames[result]; } // Records histograms relating to how often captive portal detection attempts // ended with |result| in a row, and for how long |result| was the last result // of a detection attempt. Recorded both on quit and on a new Result. // // |repeat_count| may be 0 if there were no captive portal checks during // a session. // // |result_duration| is the time between when a captive portal check first // returned |result| and when a check returned a different result, or when the // CaptivePortalService was shut down. void RecordRepeatHistograms(Result result, int repeat_count, base::TimeDelta result_duration) { // Histogram macros can't be used with variable names, since they cache // pointers, so have to use the histogram functions directly. // Record number of times the last result was received in a row. base::Histogram* result_repeated_histogram = base::Histogram::FactoryGet( "CaptivePortal.ResultRepeated." + CaptivePortalResultToString(result), 1, // min 100, // max 100, // bucket_count base::Histogram::kUmaTargetedHistogramFlag); result_repeated_histogram->Add(repeat_count); if (repeat_count == 0) return; // Time between first request that returned |result| and now. base::Histogram* result_duration_histogram = base::Histogram::FactoryTimeGet( "CaptivePortal.ResultDuration." + CaptivePortalResultToString(result), base::TimeDelta::FromSeconds(1), // min base::TimeDelta::FromHours(1), // max 50, // bucket_count base::Histogram::kUmaTargetedHistogramFlag); result_duration_histogram->AddTime(result_duration); } bool HasNativeCaptivePortalDetection() { // Lion and Windows 8 have their own captive portal detection that will open // a browser window as needed. #if defined(OS_MACOSX) return base::mac::IsOSLionOrLater(); #elif defined(OS_WIN) return base::win::GetVersion() >= base::win::VERSION_WIN8; #else return false; #endif } } // namespace CaptivePortalService::TestingState CaptivePortalService::testing_state_ = NOT_TESTING; class CaptivePortalService::RecheckBackoffEntry : public net::BackoffEntry { public: explicit RecheckBackoffEntry(CaptivePortalService* captive_portal_service) : net::BackoffEntry( &captive_portal_service->recheck_policy().backoff_policy), captive_portal_service_(captive_portal_service) { } private: virtual base::TimeTicks ImplGetTimeNow() const OVERRIDE { return captive_portal_service_->GetCurrentTimeTicks(); } CaptivePortalService* captive_portal_service_; DISALLOW_COPY_AND_ASSIGN(RecheckBackoffEntry); }; CaptivePortalService::RecheckPolicy::RecheckPolicy() : initial_backoff_no_portal_ms(600 * 1000), initial_backoff_portal_ms(20 * 1000) { // Receiving a new Result is considered a success. All subsequent requests // that get the same Result are considered "failures", so a value of N // means exponential backoff starts after getting a result N + 2 times: // +1 for the initial success, and +1 because N failures are ignored. // // A value of 6 means to start backoff on the 7th failure, which is the 8th // time the same result is received. backoff_policy.num_errors_to_ignore = 6; // It doesn't matter what this is initialized to. It will be overwritten // after the first captive portal detection request. backoff_policy.initial_delay_ms = initial_backoff_no_portal_ms; backoff_policy.multiply_factor = 2.0; backoff_policy.jitter_factor = 0.3; backoff_policy.maximum_backoff_ms = 2 * 60 * 1000; // -1 means the entry never expires. This doesn't really matter, as the // service never checks for its expiration. backoff_policy.entry_lifetime_ms = -1; backoff_policy.always_use_initial_delay = true; } CaptivePortalService::CaptivePortalService(Profile* profile) : profile_(profile), state_(STATE_IDLE), enabled_(false), last_detection_result_(RESULT_INTERNET_CONNECTED), num_checks_with_same_result_(0), test_url_(kDefaultTestURL) { // The order matters here: // |resolve_errors_with_web_service_| must be initialized and |backoff_entry_| // created before the call to UpdateEnabledState. resolve_errors_with_web_service_.Init( prefs::kAlternateErrorPagesEnabled, profile_->GetPrefs(), this); ResetBackoffEntry(last_detection_result_); UpdateEnabledState(); } CaptivePortalService::~CaptivePortalService() { } void CaptivePortalService::DetectCaptivePortal() { DCHECK(CalledOnValidThread()); // If a request is pending or running, do nothing. if (state_ == STATE_CHECKING_FOR_PORTAL || state_ == STATE_TIMER_RUNNING) return; base::TimeDelta time_until_next_check = backoff_entry_->GetTimeUntilRelease(); // Start asynchronously. state_ = STATE_TIMER_RUNNING; check_captive_portal_timer_.Start( FROM_HERE, time_until_next_check, this, &CaptivePortalService::DetectCaptivePortalInternal); } void CaptivePortalService::DetectCaptivePortalInternal() { DCHECK(CalledOnValidThread()); DCHECK(state_ == STATE_TIMER_RUNNING || state_ == STATE_IDLE); DCHECK(!FetchingURL()); DCHECK(!TimerRunning()); state_ = STATE_CHECKING_FOR_PORTAL; // When not enabled, just claim there's an Internet connection. if (!enabled_) { // Count this as a success, so the backoff entry won't apply exponential // backoff, but will apply the standard delay. backoff_entry_->InformOfRequest(true); OnResult(RESULT_INTERNET_CONNECTED); return; } // The first 0 means this can use a TestURLFetcherFactory in unit tests. url_fetcher_.reset(net::URLFetcher::Create(0, test_url_, net::URLFetcher::GET, this)); url_fetcher_->SetAutomaticallyRetryOn5xx(false); url_fetcher_->SetRequestContext(profile_->GetRequestContext()); // Can't safely use net::LOAD_DISABLE_CERT_REVOCATION_CHECKING here, // since then the connection may be reused without checking the cert. url_fetcher_->SetLoadFlags( net::LOAD_BYPASS_CACHE | net::LOAD_DO_NOT_PROMPT_FOR_LOGIN | net::LOAD_DO_NOT_SAVE_COOKIES | net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SEND_AUTH_DATA); url_fetcher_->Start(); } void CaptivePortalService::OnURLFetchComplete(const net::URLFetcher* source) { DCHECK(CalledOnValidThread()); DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_); DCHECK(FetchingURL()); DCHECK(!TimerRunning()); DCHECK_EQ(url_fetcher_.get(), source); DCHECK(enabled_); base::TimeTicks now = GetCurrentTimeTicks(); base::TimeDelta retry_after_delta; Result new_result = GetCaptivePortalResultFromResponse(source, &retry_after_delta); url_fetcher_.reset(); // Record histograms. UMA_HISTOGRAM_ENUMERATION("CaptivePortal.DetectResult", new_result, RESULT_COUNT); // If this isn't the first captive portal result, record stats. if (!last_check_time_.is_null()) { UMA_HISTOGRAM_LONG_TIMES("CaptivePortal.TimeBetweenChecks", now - last_check_time_); if (last_detection_result_ != new_result) { // If the last result was different from the result of the latest test, // record histograms about the previous period over which the result was // the same. RecordRepeatHistograms(last_detection_result_, num_checks_with_same_result_, now - first_check_time_with_same_result_); } } if (last_check_time_.is_null() || new_result != last_detection_result_) { first_check_time_with_same_result_ = now; num_checks_with_same_result_ = 1; // Reset the backoff entry both to update the default time and clear // previous failures. ResetBackoffEntry(new_result); backoff_entry_->SetCustomReleaseTime(now + retry_after_delta); // The BackoffEntry is not informed of this request, so there's no delay // before the next request. This allows for faster login when a captive // portal is first detected. It can also help when moving between captive // portals. } else { DCHECK_LE(1, num_checks_with_same_result_); ++num_checks_with_same_result_; // Requests that have the same Result as the last one are considered // "failures", to trigger backoff. backoff_entry_->SetCustomReleaseTime(now + retry_after_delta); backoff_entry_->InformOfRequest(false); } last_check_time_ = now; OnResult(new_result); } void CaptivePortalService::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { DCHECK(CalledOnValidThread()); DCHECK_EQ(chrome::NOTIFICATION_PREF_CHANGED, type); DCHECK_EQ(std::string(prefs::kAlternateErrorPagesEnabled), *content::Details(details).ptr()); UpdateEnabledState(); } void CaptivePortalService::Shutdown() { DCHECK(CalledOnValidThread()); if (enabled_) { RecordRepeatHistograms( last_detection_result_, num_checks_with_same_result_, GetCurrentTimeTicks() - first_check_time_with_same_result_); } } void CaptivePortalService::OnResult(Result result) { DCHECK_EQ(STATE_CHECKING_FOR_PORTAL, state_); state_ = STATE_IDLE; Results results; results.previous_result = last_detection_result_; results.result = result; last_detection_result_ = result; content::NotificationService::current()->Notify( chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT, content::Source(profile_), content::Details(&results)); } void CaptivePortalService::ResetBackoffEntry(Result result) { if (!enabled_ || result == RESULT_BEHIND_CAPTIVE_PORTAL) { // Use the shorter time when the captive portal service is not enabled, or // behind a captive portal. recheck_policy_.backoff_policy.initial_delay_ms = recheck_policy_.initial_backoff_portal_ms; } else { recheck_policy_.backoff_policy.initial_delay_ms = recheck_policy_.initial_backoff_no_portal_ms; } backoff_entry_.reset(new RecheckBackoffEntry(this)); } void CaptivePortalService::UpdateEnabledState() { bool enabled_before = enabled_; enabled_ = testing_state_ != DISABLED_FOR_TESTING && resolve_errors_with_web_service_.GetValue(); if (testing_state_ != SKIP_OS_CHECK_FOR_TESTING && HasNativeCaptivePortalDetection()) { enabled_ = false; } if (enabled_before == enabled_) return; // Clear data used for histograms. num_checks_with_same_result_ = 0; first_check_time_with_same_result_ = base::TimeTicks(); last_check_time_ = base::TimeTicks(); ResetBackoffEntry(last_detection_result_); if (state_ == STATE_CHECKING_FOR_PORTAL || state_ == STATE_TIMER_RUNNING) { // If a captive portal check was running or pending, stop the check or the // timer. url_fetcher_.reset(); check_captive_portal_timer_.Stop(); state_ = STATE_IDLE; // Since a captive portal request was queued or running, something may be // expecting to receive a captive portal result. DetectCaptivePortal(); } } // Takes a net::URLFetcher that has finished trying to retrieve the test // URL, and returns a CaptivePortalService::Result based on its result. Result CaptivePortalService::GetCaptivePortalResultFromResponse( const net::URLFetcher* url_fetcher, base::TimeDelta* retry_after) const { DCHECK(retry_after); DCHECK(!url_fetcher->GetStatus().is_io_pending()); *retry_after = base::TimeDelta(); // If there's a network error of some sort when fetching a file via HTTP, // there may be a networking problem, rather than a captive portal. // TODO(mmenke): Consider special handling for redirects that end up at // errors, especially SSL certificate errors. if (url_fetcher->GetStatus().status() != net::URLRequestStatus::SUCCESS) return RESULT_NO_RESPONSE; // In the case of 503 errors, look for the Retry-After header. int response_code = url_fetcher->GetResponseCode(); if (response_code == 503) { net::HttpResponseHeaders* headers = url_fetcher->GetResponseHeaders(); std::string retry_after_string; // If there's no Retry-After header, nothing else to do. if (!headers->EnumerateHeader(NULL, "Retry-After", &retry_after_string)) return RESULT_NO_RESPONSE; // Otherwise, try parsing it as an integer (seconds) or as an HTTP date. int seconds; base::Time full_date; if (base::StringToInt(retry_after_string, &seconds)) { *retry_after = base::TimeDelta::FromSeconds(seconds); } else if (headers->GetTimeValuedHeader("Retry-After", &full_date)) { base::Time now = GetCurrentTime(); if (full_date > now) *retry_after = full_date - now; } return RESULT_NO_RESPONSE; } // A 511 response (Network Authentication Required) means that the user needs // to login to whatever server issued the response. // See: http://tools.ietf.org/html/rfc6585 if (response_code == 511) return RESULT_BEHIND_CAPTIVE_PORTAL; // Other non-2xx/3xx HTTP responses may indicate server errors. if (response_code >= 400 || response_code < 200) return RESULT_NO_RESPONSE; // A 204 response code indicates there's no captive portal. if (response_code == 204) return RESULT_INTERNET_CONNECTED; // Otherwise, assume it's a captive portal. return RESULT_BEHIND_CAPTIVE_PORTAL; } base::Time CaptivePortalService::GetCurrentTime() const { return base::Time::Now(); } base::TimeTicks CaptivePortalService::GetCurrentTimeTicks() const { return base::TimeTicks::Now(); } bool CaptivePortalService::FetchingURL() const { return url_fetcher_.get() != NULL; } bool CaptivePortalService::TimerRunning() const { return check_captive_portal_timer_.IsRunning(); } } // namespace captive_portal