| // Copyright 2014 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/ssl/ssl_error_handler.h" |
| |
| #include <stdint.h> |
| #include <utility> |
| |
| #include "base/callback_helpers.h" |
| #include "base/macros.h" |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/histogram.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/time/clock.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ssl/bad_clock_blocking_page.h" |
| #include "chrome/browser/ssl/ssl_blocking_page.h" |
| #include "chrome/browser/ssl/ssl_cert_reporter.h" |
| #include "components/ssl_errors/error_classification.h" |
| #include "components/ssl_errors/error_info.h" |
| #include "content/public/browser/notification_service.h" |
| #include "content/public/browser/notification_source.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/web_contents.h" |
| #include "net/base/net_errors.h" |
| |
| #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) |
| #include "chrome/browser/captive_portal/captive_portal_service.h" |
| #include "chrome/browser/captive_portal/captive_portal_service_factory.h" |
| #include "chrome/browser/captive_portal/captive_portal_tab_helper.h" |
| #include "chrome/browser/ssl/captive_portal_blocking_page.h" |
| #endif |
| |
| namespace network_time { |
| class NetworkTimeTracker; |
| } |
| |
| namespace { |
| |
| // The delay in milliseconds before displaying the SSL interstitial. |
| // This can be changed in tests. |
| // - If there is a name mismatch and a suggested URL available result arrives |
| // during this time, the user is redirected to the suggester URL. |
| // - If a "captive portal detected" result arrives during this time, |
| // a captive portal interstitial is displayed. |
| // - Otherwise, an SSL interstitial is displayed. |
| int64_t g_interstitial_delay_in_milliseconds = 3000; |
| |
| // Callback to call when the interstitial timer is started. Used for testing. |
| SSLErrorHandler::TimerStartedCallback* g_timer_started_callback = nullptr; |
| |
| // The clock to use when deciding which error type to display. Used for testing. |
| base::Clock* g_testing_clock = nullptr; |
| |
| // Events for UMA. |
| enum SSLErrorHandlerEvent { |
| HANDLE_ALL, |
| SHOW_CAPTIVE_PORTAL_INTERSTITIAL_NONOVERRIDABLE, |
| SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE, |
| SHOW_SSL_INTERSTITIAL_NONOVERRIDABLE, |
| SHOW_SSL_INTERSTITIAL_OVERRIDABLE, |
| WWW_MISMATCH_FOUND, |
| WWW_MISMATCH_URL_AVAILABLE, |
| WWW_MISMATCH_URL_NOT_AVAILABLE, |
| SHOW_BAD_CLOCK, |
| SSL_ERROR_HANDLER_EVENT_COUNT |
| }; |
| |
| // Adds a message to console after navigation commits and then, deletes itself. |
| // Also deletes itself if the navigation is stopped. |
| class CommonNameMismatchRedirectObserver |
| : public content::WebContentsObserver, |
| public content::WebContentsUserData<CommonNameMismatchRedirectObserver> { |
| public: |
| static void AddToConsoleAfterNavigation( |
| content::WebContents* web_contents, |
| const std::string& request_url_hostname, |
| const std::string& suggested_url_hostname) { |
| web_contents->SetUserData( |
| UserDataKey(), |
| new CommonNameMismatchRedirectObserver( |
| web_contents, request_url_hostname, suggested_url_hostname)); |
| } |
| |
| private: |
| CommonNameMismatchRedirectObserver(content::WebContents* web_contents, |
| const std::string& request_url_hostname, |
| const std::string& suggested_url_hostname) |
| : WebContentsObserver(web_contents), |
| web_contents_(web_contents), |
| request_url_hostname_(request_url_hostname), |
| suggested_url_hostname_(suggested_url_hostname) {} |
| ~CommonNameMismatchRedirectObserver() override {} |
| |
| // WebContentsObserver: |
| void NavigationStopped() override { |
| // Deletes |this|. |
| web_contents_->RemoveUserData(UserDataKey()); |
| } |
| |
| void NavigationEntryCommitted( |
| const content::LoadCommittedDetails& /* load_details */) override { |
| web_contents_->GetMainFrame()->AddMessageToConsole( |
| content::CONSOLE_MESSAGE_LEVEL_LOG, |
| base::StringPrintf( |
| "Redirecting navigation %s -> %s because the server presented a " |
| "certificate valid for %s but not for %s. To disable such " |
| "redirects launch Chrome with the following flag: " |
| "--force-fieldtrials=SSLCommonNameMismatchHandling/Disabled/", |
| request_url_hostname_.c_str(), suggested_url_hostname_.c_str(), |
| suggested_url_hostname_.c_str(), request_url_hostname_.c_str())); |
| web_contents_->RemoveUserData(UserDataKey()); |
| } |
| |
| void WebContentsDestroyed() override { |
| web_contents_->RemoveUserData(UserDataKey()); |
| } |
| |
| content::WebContents* web_contents_; |
| const std::string request_url_hostname_; |
| const std::string suggested_url_hostname_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CommonNameMismatchRedirectObserver); |
| }; |
| |
| void RecordUMA(SSLErrorHandlerEvent event) { |
| UMA_HISTOGRAM_ENUMERATION("interstitial.ssl_error_handler", event, |
| SSL_ERROR_HANDLER_EVENT_COUNT); |
| } |
| |
| #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) |
| bool IsCaptivePortalInterstitialEnabled() { |
| return base::FieldTrialList::FindFullName("CaptivePortalInterstitial") == |
| "Enabled"; |
| } |
| #endif |
| |
| bool IsSSLCommonNameMismatchHandlingEnabled() { |
| return base::FieldTrialList::FindFullName("SSLCommonNameMismatchHandling") == |
| "Enabled"; |
| } |
| |
| } // namespace |
| |
| DEFINE_WEB_CONTENTS_USER_DATA_KEY(SSLErrorHandler); |
| DEFINE_WEB_CONTENTS_USER_DATA_KEY(CommonNameMismatchRedirectObserver); |
| |
| void SSLErrorHandler::HandleSSLError( |
| content::WebContents* web_contents, |
| int cert_error, |
| const net::SSLInfo& ssl_info, |
| const GURL& request_url, |
| int options_mask, |
| std::unique_ptr<SSLCertReporter> ssl_cert_reporter, |
| const base::Callback<void(bool)>& callback) { |
| DCHECK(!FromWebContents(web_contents)); |
| SSLErrorHandler* error_handler = |
| new SSLErrorHandler(web_contents, cert_error, ssl_info, request_url, |
| options_mask, std::move(ssl_cert_reporter), callback); |
| web_contents->SetUserData(UserDataKey(), error_handler); |
| error_handler->StartHandlingError(); |
| } |
| |
| // static |
| void SSLErrorHandler::SetInterstitialDelayForTest(base::TimeDelta delay) { |
| g_interstitial_delay_in_milliseconds = delay.InMilliseconds(); |
| } |
| |
| // static |
| void SSLErrorHandler::SetInterstitialTimerStartedCallbackForTest( |
| TimerStartedCallback* callback) { |
| DCHECK(!callback || !callback->is_null()); |
| g_timer_started_callback = callback; |
| } |
| |
| // static |
| void SSLErrorHandler::SetClockForTest(base::Clock* testing_clock) { |
| g_testing_clock = testing_clock; |
| } |
| |
| SSLErrorHandler::SSLErrorHandler( |
| content::WebContents* web_contents, |
| int cert_error, |
| const net::SSLInfo& ssl_info, |
| const GURL& request_url, |
| int options_mask, |
| std::unique_ptr<SSLCertReporter> ssl_cert_reporter, |
| const base::Callback<void(bool)>& callback) |
| : content::WebContentsObserver(web_contents), |
| web_contents_(web_contents), |
| cert_error_(cert_error), |
| ssl_info_(ssl_info), |
| request_url_(request_url), |
| options_mask_(options_mask), |
| callback_(callback), |
| profile_(Profile::FromBrowserContext(web_contents->GetBrowserContext())), |
| ssl_cert_reporter_(std::move(ssl_cert_reporter)) {} |
| |
| SSLErrorHandler::~SSLErrorHandler() { |
| } |
| |
| void SSLErrorHandler::StartHandlingError() { |
| RecordUMA(HANDLE_ALL); |
| |
| const base::Time now = g_testing_clock == nullptr |
| ? base::Time::NowFromSystemTime() |
| : g_testing_clock->Now(); |
| if (ssl_errors::ErrorInfo::NetErrorToErrorType(cert_error_) == |
| ssl_errors::ErrorInfo::CERT_DATE_INVALID) { |
| ssl_errors::ClockState clock_state = ssl_errors::GetClockState( |
| now, g_browser_process->network_time_tracker()); |
| if (clock_state == ssl_errors::CLOCK_STATE_FUTURE || |
| clock_state == ssl_errors::CLOCK_STATE_PAST) { |
| ShowBadClockInterstitial(now, clock_state); |
| return; // |this| is deleted after showing the interstitial. |
| } |
| } |
| |
| std::vector<std::string> dns_names; |
| ssl_info_.cert->GetDNSNames(&dns_names); |
| DCHECK(!dns_names.empty()); |
| GURL suggested_url; |
| if (IsSSLCommonNameMismatchHandlingEnabled() && |
| cert_error_ == net::ERR_CERT_COMMON_NAME_INVALID && |
| IsErrorOverridable() && GetSuggestedUrl(dns_names, &suggested_url)) { |
| RecordUMA(WWW_MISMATCH_FOUND); |
| net::CertStatus extra_cert_errors = |
| ssl_info_.cert_status ^ net::CERT_STATUS_COMMON_NAME_INVALID; |
| |
| // Show the SSL intersitial if |CERT_STATUS_COMMON_NAME_INVALID| is not |
| // the only error. Need not check for captive portal in this case. |
| // (See the comment below). |
| if (net::IsCertStatusError(extra_cert_errors) && |
| !net::IsCertStatusMinorError(ssl_info_.cert_status)) { |
| ShowSSLInterstitial(); |
| return; |
| } |
| CheckSuggestedUrl(suggested_url); |
| timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds( |
| g_interstitial_delay_in_milliseconds), |
| this, &SSLErrorHandler::ShowSSLInterstitial); |
| if (g_timer_started_callback) |
| g_timer_started_callback->Run(web_contents_); |
| |
| // Do not check for a captive portal in this case, because a captive |
| // portal most likely cannot serve a valid certificate which passes the |
| // similarity check. |
| return; |
| } |
| |
| #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) |
| CaptivePortalTabHelper* captive_portal_tab_helper = |
| CaptivePortalTabHelper::FromWebContents(web_contents_); |
| if (captive_portal_tab_helper) { |
| captive_portal_tab_helper->OnSSLCertError(ssl_info_); |
| } |
| |
| registrar_.Add(this, chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT, |
| content::Source<Profile>(profile_)); |
| |
| if (IsCaptivePortalInterstitialEnabled()) { |
| CheckForCaptivePortal(); |
| timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds( |
| g_interstitial_delay_in_milliseconds), |
| this, &SSLErrorHandler::ShowSSLInterstitial); |
| if (g_timer_started_callback) |
| g_timer_started_callback->Run(web_contents_); |
| return; |
| } |
| #endif |
| // Display an SSL interstitial. |
| ShowSSLInterstitial(); |
| } |
| |
| void SSLErrorHandler::CheckForCaptivePortal() { |
| #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) |
| CaptivePortalService* captive_portal_service = |
| CaptivePortalServiceFactory::GetForProfile(profile_); |
| captive_portal_service->DetectCaptivePortal(); |
| #else |
| NOTREACHED(); |
| #endif |
| } |
| |
| bool SSLErrorHandler::GetSuggestedUrl(const std::vector<std::string>& dns_names, |
| GURL* suggested_url) const { |
| return CommonNameMismatchHandler::GetSuggestedUrl(request_url_, dns_names, |
| suggested_url); |
| } |
| |
| void SSLErrorHandler::CheckSuggestedUrl(const GURL& suggested_url) { |
| scoped_refptr<net::URLRequestContextGetter> request_context( |
| profile_->GetRequestContext()); |
| common_name_mismatch_handler_.reset( |
| new CommonNameMismatchHandler(request_url_, request_context)); |
| |
| common_name_mismatch_handler_->CheckSuggestedUrl( |
| suggested_url, |
| base::Bind(&SSLErrorHandler::CommonNameMismatchHandlerCallback, |
| base::Unretained(this))); |
| } |
| |
| void SSLErrorHandler::NavigateToSuggestedURL(const GURL& suggested_url) { |
| content::NavigationController::LoadURLParams load_params(suggested_url); |
| load_params.transition_type = ui::PAGE_TRANSITION_TYPED; |
| web_contents()->GetController().LoadURLWithParams(load_params); |
| } |
| |
| bool SSLErrorHandler::IsErrorOverridable() const { |
| return SSLBlockingPage::IsOverridable(options_mask_, profile_); |
| } |
| |
| void SSLErrorHandler::ShowCaptivePortalInterstitial(const GURL& landing_url) { |
| #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) |
| // Show captive portal blocking page. The interstitial owns the blocking page. |
| RecordUMA(IsErrorOverridable() |
| ? SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE |
| : SHOW_CAPTIVE_PORTAL_INTERSTITIAL_NONOVERRIDABLE); |
| (new CaptivePortalBlockingPage(web_contents_, request_url_, landing_url, |
| std::move(ssl_cert_reporter_), ssl_info_, |
| callback_)) |
| ->Show(); |
| // Once an interstitial is displayed, no need to keep the handler around. |
| // This is the equivalent of "delete this". It also destroys the timer. |
| web_contents_->RemoveUserData(UserDataKey()); |
| #else |
| NOTREACHED(); |
| #endif |
| } |
| |
| void SSLErrorHandler::ShowSSLInterstitial() { |
| // Show SSL blocking page. The interstitial owns the blocking page. |
| RecordUMA(IsErrorOverridable() ? SHOW_SSL_INTERSTITIAL_OVERRIDABLE |
| : SHOW_SSL_INTERSTITIAL_NONOVERRIDABLE); |
| |
| (new SSLBlockingPage(web_contents_, cert_error_, ssl_info_, request_url_, |
| options_mask_, base::Time::NowFromSystemTime(), |
| std::move(ssl_cert_reporter_), callback_)) |
| ->Show(); |
| // Once an interstitial is displayed, no need to keep the handler around. |
| // This is the equivalent of "delete this". |
| web_contents_->RemoveUserData(UserDataKey()); |
| } |
| |
| void SSLErrorHandler::ShowBadClockInterstitial( |
| const base::Time& now, |
| ssl_errors::ClockState clock_state) { |
| RecordUMA(SHOW_BAD_CLOCK); |
| (new BadClockBlockingPage(web_contents_, cert_error_, ssl_info_, request_url_, |
| now, clock_state, std::move(ssl_cert_reporter_), |
| callback_)) |
| ->Show(); |
| // Once an interstitial is displayed, no need to keep the handler around. |
| // This is the equivalent of "delete this". |
| web_contents_->RemoveUserData(UserDataKey()); |
| } |
| |
| void SSLErrorHandler::CommonNameMismatchHandlerCallback( |
| const CommonNameMismatchHandler::SuggestedUrlCheckResult& result, |
| const GURL& suggested_url) { |
| timer_.Stop(); |
| if (result == CommonNameMismatchHandler::SuggestedUrlCheckResult:: |
| SUGGESTED_URL_AVAILABLE) { |
| RecordUMA(WWW_MISMATCH_URL_AVAILABLE); |
| CommonNameMismatchRedirectObserver::AddToConsoleAfterNavigation( |
| web_contents(), request_url_.host(), suggested_url.host()); |
| NavigateToSuggestedURL(suggested_url); |
| } else { |
| RecordUMA(WWW_MISMATCH_URL_NOT_AVAILABLE); |
| ShowSSLInterstitial(); |
| } |
| } |
| |
| void SSLErrorHandler::Observe( |
| int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) { |
| #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION) |
| DCHECK_EQ(chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT, type); |
| |
| timer_.Stop(); |
| CaptivePortalService::Results* results = |
| content::Details<CaptivePortalService::Results>(details).ptr(); |
| if (results->result == captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL) |
| ShowCaptivePortalInterstitial(results->landing_url); |
| else |
| ShowSSLInterstitial(); |
| #endif |
| } |
| |
| void SSLErrorHandler::DidStartNavigationToPendingEntry( |
| const GURL& /* url */, |
| content::NavigationController::ReloadType /* reload_type */) { |
| // Destroy the error handler on all new navigations. This ensures that the |
| // handler is properly recreated when a hanging page is navigated to an SSL |
| // error, even when the tab's WebContents doesn't change. |
| DeleteSSLErrorHandler(); |
| } |
| |
| void SSLErrorHandler::NavigationStopped() { |
| // Destroy the error handler when the page load is stopped. |
| DeleteSSLErrorHandler(); |
| } |
| |
| void SSLErrorHandler::DeleteSSLErrorHandler() { |
| // Need to explicity deny the certificate via the callback, otherwise memory |
| // is leaked. |
| if (!callback_.is_null()) { |
| base::ResetAndReturn(&callback_).Run(false); |
| } |
| if (common_name_mismatch_handler_) { |
| common_name_mismatch_handler_->Cancel(); |
| common_name_mismatch_handler_.reset(); |
| } |
| // Deletes |this| and also destroys the timer. |
| web_contents_->RemoveUserData(UserDataKey()); |
| } |