blob: 4006e99088d7437159babd61ba33a6f9f4f5e1fc [file] [log] [blame]
// Copyright 2019 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 "weblayer/test/weblayer_browser_test.h"
#include "base/files/file_path.h"
#include "base/macros.h"
#include "base/optional.h"
#include "base/scoped_observer.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "components/network_time/network_time_tracker.h"
#include "components/security_interstitials/content/insecure_form_blocking_page.h"
#include "components/security_interstitials/content/ssl_error_assistant.h"
#include "components/security_interstitials/content/ssl_error_handler.h"
#include "components/security_interstitials/core/features.h"
#include "net/ssl/ssl_info.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "weblayer/browser/browser_process.h"
#include "weblayer/browser/weblayer_security_blocking_page_factory.h"
#include "weblayer/public/browser.h"
#include "weblayer/public/browser_observer.h"
#include "weblayer/public/error_page.h"
#include "weblayer/public/error_page_delegate.h"
#include "weblayer/public/tab.h"
#include "weblayer/shell/browser/shell.h"
#include "weblayer/test/interstitial_utils.h"
#include "weblayer/test/load_completion_observer.h"
#include "weblayer/test/test_navigation_observer.h"
#include "weblayer/test/weblayer_browser_test_utils.h"
namespace weblayer {
namespace {
#if defined(OS_ANDROID)
// Waits for a new tab to be created, and then load |url|.
class NewTabWaiter : public BrowserObserver {
public:
NewTabWaiter(Browser* browser, const GURL& url) : url_(url) {
observer_.Add(browser);
}
void OnTabAdded(Tab* tab) override {
navigation_observer_ = std::make_unique<TestNavigationObserver>(
url_, TestNavigationObserver::NavigationEvent::kStart, tab);
run_loop_.Quit();
}
void Wait() {
if (!navigation_observer_)
run_loop_.Run();
navigation_observer_->Wait();
}
private:
GURL url_;
std::unique_ptr<TestNavigationObserver> navigation_observer_;
base::RunLoop run_loop_;
ScopedObserver<Browser, BrowserObserver> observer_{this};
};
#endif
class TestErrorPageDelegate : public ErrorPageDelegate {
public:
bool was_get_error_page_content_called() const {
return was_get_error_page_content_called_;
}
// ErrorPageDelegate:
bool OnBackToSafety() override { return false; }
std::unique_ptr<ErrorPage> GetErrorPageContent(
Navigation* navigation) override {
was_get_error_page_content_called_ = true;
return std::make_unique<ErrorPage>();
}
private:
bool was_get_error_page_content_called_ = false;
};
} // namespace
class SSLBrowserTest : public WebLayerBrowserTest {
public:
SSLBrowserTest() = default;
~SSLBrowserTest() override = default;
// WebLayerBrowserTest:
void SetUpOnMainThread() override {
https_server_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
https_server_->AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("weblayer/test/data")));
https_server_mismatched_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
https_server_mismatched_->SetSSLConfig(
net::EmbeddedTestServer::CERT_MISMATCHED_NAME);
https_server_mismatched_->AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("weblayer/test/data")));
https_server_expired_ = std::make_unique<net::EmbeddedTestServer>(
net::EmbeddedTestServer::TYPE_HTTPS);
https_server_expired_->SetSSLConfig(net::EmbeddedTestServer::CERT_EXPIRED);
https_server_expired_->AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("weblayer/test/data")));
ASSERT_TRUE(https_server_->Start());
ASSERT_TRUE(https_server_mismatched_->Start());
ASSERT_TRUE(https_server_expired_->Start());
}
void PostRunTestOnMainThread() override {
https_server_.reset();
https_server_mismatched_.reset();
WebLayerBrowserTest::PostRunTestOnMainThread();
}
void NavigateToOkPage() {
ASSERT_EQ("127.0.0.1", ok_url().host());
NavigateAndWaitForCompletion(ok_url(), shell());
EXPECT_FALSE(IsShowingSecurityInterstitial(shell()->tab()));
}
void NavigateToPageWithMismatchedCertExpectSSLInterstitial() {
// Do a navigation that should result in an SSL error.
NavigateAndWaitForFailure(mismatched_cert_url(), shell());
// First check that there *is* an interstitial.
ASSERT_TRUE(IsShowingSecurityInterstitial(shell()->tab()));
// Now verify that the interstitial is in fact an SSL interstitial.
EXPECT_TRUE(IsShowingSSLInterstitial(shell()->tab()));
// TODO(blundell): Check the security state once security state is available
// via the public WebLayer API, following the example of //chrome's
// ssl_browsertest.cc's CheckAuthenticationBrokenState() function.
}
void NavigateToPageWithMismatchedCertExpectCaptivePortalInterstitial() {
// Do a navigation that should result in an SSL error.
NavigateAndWaitForFailure(mismatched_cert_url(), shell());
// First check that there *is* an interstitial.
ASSERT_TRUE(IsShowingSecurityInterstitial(shell()->tab()));
// Now verify that the interstitial is in fact a captive portal
// interstitial.
EXPECT_TRUE(IsShowingCaptivePortalInterstitial(shell()->tab()));
// TODO(blundell): Check the security state once security state is available
// via the public WebLayer API, following the example of //chrome's
// ssl_browsertest.cc's CheckAuthenticationBrokenState() function.
}
void NavigateToPageWithExpiredCertExpectSSLInterstitial() {
// Do a navigation that should result in an SSL error.
NavigateAndWaitForFailure(expired_cert_url(), shell());
// First check that there *is* an interstitial.
ASSERT_TRUE(IsShowingSecurityInterstitial(shell()->tab()));
// Now verify that the interstitial is in fact an SSL interstitial.
EXPECT_TRUE(IsShowingSSLInterstitial(shell()->tab()));
// TODO(blundell): Check the security state once security state is available
// via the public WebLayer API, following the example of //chrome's
// ssl_browsertest.cc's CheckAuthenticationBrokenState() function.
}
void NavigateToPageWithExpiredCertExpectBadClockInterstitial() {
// Do a navigation that should result in an SSL error.
NavigateAndWaitForFailure(expired_cert_url(), shell());
// First check that there *is* an interstitial.
ASSERT_TRUE(IsShowingSecurityInterstitial(shell()->tab()));
// Now verify that the interstitial is in fact a bad clock interstitial.
EXPECT_TRUE(IsShowingBadClockInterstitial(shell()->tab()));
// TODO(blundell): Check the security state once security state is available
// via the public WebLayer API, following the example of //chrome's
// ssl_browsertest.cc's CheckAuthenticationBrokenState() function.
}
void NavigateToPageWithMismatchedCertExpectNotBlocked() {
NavigateAndWaitForCompletion(mismatched_cert_url(), shell());
EXPECT_FALSE(IsShowingSecurityInterstitial(shell()->tab()));
// TODO(blundell): Check the security state once security state is available
// via the public WebLayer API, following the example of //chrome's
// ssl_browsertest.cc's CheckAuthenticationBrokenState() function.
}
void SendInterstitialNavigationCommandAndWait(
bool proceed,
base::Optional<GURL> previous_url = base::nullopt) {
GURL expected_url =
proceed ? mismatched_cert_url() : previous_url.value_or(ok_url());
ASSERT_TRUE(IsShowingSSLInterstitial(shell()->tab()));
TestNavigationObserver navigation_observer(
expected_url, TestNavigationObserver::NavigationEvent::kCompletion,
shell());
ExecuteScript(shell(),
"window.certificateErrorPageController." +
std::string(proceed ? "proceed" : "dontProceed") + "();",
false /*use_separate_isolate*/);
navigation_observer.Wait();
EXPECT_FALSE(IsShowingSSLInterstitial(shell()->tab()));
}
void SendInterstitialReloadCommandAndWait() {
ASSERT_TRUE(IsShowingSSLInterstitial(shell()->tab()));
LoadCompletionObserver load_observer(shell());
ExecuteScript(shell(), "window.certificateErrorPageController.reload();",
false /*use_separate_isolate*/);
load_observer.Wait();
// Should still be showing the SSL interstitial after the reload command is
// processed.
EXPECT_TRUE(IsShowingSSLInterstitial(shell()->tab()));
}
#if defined(OS_ANDROID)
void SendInterstitialOpenLoginCommandAndWait() {
ASSERT_TRUE(IsShowingCaptivePortalInterstitial(shell()->tab()));
// Note: The embedded test server cannot actually load the captive portal
// login URL, so simply detect the start of the navigation to the page.
NewTabWaiter waiter(shell()->browser(),
WebLayerSecurityBlockingPageFactory::
GetCaptivePortalLoginPageUrlForTesting());
ExecuteScript(shell(), "window.certificateErrorPageController.openLogin();",
false /*use_separate_isolate*/);
waiter.Wait();
}
#endif
void NavigateToOtherOkPage() {
NavigateAndWaitForCompletion(https_server_->GetURL("/simple_page2.html"),
shell());
EXPECT_FALSE(IsShowingSecurityInterstitial(shell()->tab()));
}
GURL ok_url() { return https_server_->GetURL("/simple_page.html"); }
GURL mismatched_cert_url() {
return https_server_mismatched_->GetURL("/simple_page.html");
}
GURL expired_cert_url() {
return https_server_expired_->GetURL("/simple_page.html");
}
protected:
std::unique_ptr<net::EmbeddedTestServer> https_server_;
std::unique_ptr<net::EmbeddedTestServer> https_server_mismatched_;
std::unique_ptr<net::EmbeddedTestServer> https_server_expired_;
private:
DISALLOW_COPY_AND_ASSIGN(SSLBrowserTest);
};
// Tests clicking "take me back" on the interstitial page.
IN_PROC_BROWSER_TEST_F(SSLBrowserTest, TakeMeBack) {
NavigateToOkPage();
NavigateToPageWithMismatchedCertExpectSSLInterstitial();
// Click "Take me back".
SendInterstitialNavigationCommandAndWait(false /*proceed*/);
// Check that it's possible to navigate to a new page.
NavigateToOtherOkPage();
// Navigate to the bad SSL page again, an interstitial shows again (in
// contrast to what would happen had the user chosen to proceed).
NavigateToPageWithMismatchedCertExpectSSLInterstitial();
}
// Tests clicking "take me back" on the interstitial page when there's no
// navigation history. The user should be taken to a safe page (about:blank).
IN_PROC_BROWSER_TEST_F(SSLBrowserTest, TakeMeBackEmptyNavigationHistory) {
NavigateToPageWithMismatchedCertExpectSSLInterstitial();
// Click "Take me back".
SendInterstitialNavigationCommandAndWait(false /*proceed*/,
GURL("about:blank"));
}
IN_PROC_BROWSER_TEST_F(SSLBrowserTest, Reload) {
NavigateToOkPage();
NavigateToPageWithMismatchedCertExpectSSLInterstitial();
SendInterstitialReloadCommandAndWait();
// TODO(blundell): Ideally we would fix the SSL error, reload, and verify
// that the SSL interstitial isn't showing. However, currently this doesn't
// work: Calling ResetSSLConfig() on |http_server_mismatched_| passing
// CERT_OK does not cause future reloads or navigations to
// mismatched_cert_url() to succeed; they still fail and pop an interstitial.
// I verified that the LoadCompletionObserver is in fact waiting for a new
// load, i.e., there is actually a *new* SSL interstitial popped up. From
// looking at the ResetSSLConfig() impl there shouldn't be any waiting or
// anything needed within the client.
}
// Tests clicking proceed link on the interstitial page. This is a PRE_ test
// because it also acts as setup for the test below which verifies the behavior
// across restarts.
// TODO(crbug.com/654704): Android does not support PRE_ tests. For Android just
// run only the PRE_ version of this test.
#if defined(OS_ANDROID)
#define PRE_Proceed Proceed
#endif
IN_PROC_BROWSER_TEST_F(SSLBrowserTest, PRE_Proceed) {
NavigateToOkPage();
NavigateToPageWithMismatchedCertExpectSSLInterstitial();
SendInterstitialNavigationCommandAndWait(true /*proceed*/);
// Go back to an OK page, then try to navigate again. The "Proceed" decision
// should be saved, so no interstitial is shown this time.
NavigateToOkPage();
NavigateToPageWithMismatchedCertExpectNotBlocked();
}
#if !defined(OS_ANDROID)
// The proceed decision is perpetuated across WebLayer sessions, i.e. WebLayer
// will not block again when navigating to the same bad page that was previously
// proceeded through.
IN_PROC_BROWSER_TEST_F(SSLBrowserTest, Proceed) {
NavigateToPageWithMismatchedCertExpectNotBlocked();
}
#endif
// Tests navigating away from the interstitial page.
IN_PROC_BROWSER_TEST_F(SSLBrowserTest, NavigateAway) {
NavigateToOkPage();
NavigateToPageWithMismatchedCertExpectSSLInterstitial();
NavigateToOtherOkPage();
}
// Tests the scenario where the OS reports that an SSL error is due to a
// captive portal. A captive portal interstitial should be displayed. The test
// then switches OS captive portal status to false and reloads the page. This
// time, a normal SSL interstitial should be displayed.
IN_PROC_BROWSER_TEST_F(SSLBrowserTest, OSReportsCaptivePortal) {
SSLErrorHandler::SetOSReportsCaptivePortalForTesting(true);
NavigateToPageWithMismatchedCertExpectCaptivePortalInterstitial();
// Check that clearing the test setting causes behavior to revert to normal.
SSLErrorHandler::SetOSReportsCaptivePortalForTesting(false);
NavigateToPageWithMismatchedCertExpectSSLInterstitial();
}
#if defined(OS_ANDROID)
// Tests that after reaching a captive portal interstitial, clicking on the
// connect link will cause a navigation to the login page.
IN_PROC_BROWSER_TEST_F(SSLBrowserTest, CaptivePortalConnectToLoginPage) {
SSLErrorHandler::SetOSReportsCaptivePortalForTesting(true);
NavigateToPageWithMismatchedCertExpectCaptivePortalInterstitial();
SendInterstitialOpenLoginCommandAndWait();
}
#endif
IN_PROC_BROWSER_TEST_F(SSLBrowserTest, BadClockInterstitial) {
// Without the NetworkTimeTracker reporting that the clock is ahead or
// behind, navigating to a page with an expired cert should result in the
// default SSL interstitial appearing.
NavigateToPageWithExpiredCertExpectSSLInterstitial();
// Set network time back ten minutes.
BrowserProcess::GetInstance()->GetNetworkTimeTracker()->UpdateNetworkTime(
base::Time::Now() - base::TimeDelta::FromMinutes(10),
base::TimeDelta::FromMilliseconds(1), /* resolution */
base::TimeDelta::FromMilliseconds(500), /* latency */
base::TimeTicks::Now() /* posting time of this update */);
// Now navigating to a page with an expired cert should cause the bad clock
// interstitial to appear.
NavigateToPageWithExpiredCertExpectBadClockInterstitial();
}
// This test verifies that a certificate in the list of known captive portal
// certificates in ssl_error_assistant.asciipb is detected as such. This serves
// to verify that the ssl_error_assistant proto was correctly loaded.
IN_PROC_BROWSER_TEST_F(SSLBrowserTest,
CertificateInKnownCaptivePortalsListDetected) {
net::SSLInfo ssl_info_with_known_captive_portal_cert;
net::HashValue captive_portal_public_key;
// Set up the SSSLInfo with the certificate of captive-portal.badssl.com
// (taken from ssl_error_assistant.asciipb).
ASSERT_TRUE(captive_portal_public_key.FromString(
"sha256/fjZPHewEHTrMDX3I1ecEIeoy3WFxHyGplOLv28kIbtI="));
net::HashValueVector public_keys;
public_keys.push_back(captive_portal_public_key);
ssl_info_with_known_captive_portal_cert.public_key_hashes = public_keys;
EXPECT_TRUE(SSLErrorAssistant().IsKnownCaptivePortalCertificate(
ssl_info_with_known_captive_portal_cert));
}
// Verifies an error page is not requested for an ssl error.
IN_PROC_BROWSER_TEST_F(SSLBrowserTest, ErrorPageNotCalledForMismatch) {
TestErrorPageDelegate error_page_delegate;
shell()->tab()->SetErrorPageDelegate(&error_page_delegate);
NavigateToOkPage();
EXPECT_FALSE(error_page_delegate.was_get_error_page_content_called());
NavigateToPageWithMismatchedCertExpectSSLInterstitial();
EXPECT_FALSE(error_page_delegate.was_get_error_page_content_called());
}
class SSLBrowserTestWithInsecureFormsWarningEnabled : public SSLBrowserTest {
public:
SSLBrowserTestWithInsecureFormsWarningEnabled() {
feature_list_.InitAndEnableFeature(
security_interstitials::kInsecureFormSubmissionInterstitial);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Visits a page that displays an insecure form, submits the form, and checks an
// interstitial is shown.
IN_PROC_BROWSER_TEST_F(SSLBrowserTestWithInsecureFormsWarningEnabled,
TestDisplaysInsecureFormSubmissionWarning) {
GURL insecure_form_url = https_server_->GetURL("/insecure_form.html");
GURL form_target_url = GURL("http://does-not-exist.test/form_target.html?");
NavigateAndWaitForCompletion(insecure_form_url, shell());
// Submit the form and wait for the interstitial to load.
TestNavigationObserver navigation_observer(
form_target_url, TestNavigationObserver::NavigationEvent::kFailure,
shell());
ExecuteScript(shell(), "submitForm();", false /*use_separate_isolate*/);
navigation_observer.Wait();
// Check the correct interstitial loaded.
EXPECT_TRUE(IsShowingInsecureFormInterstitial(shell()->tab()));
}
class SSLBrowserTestWithInsecureFormsWarningDisabled : public SSLBrowserTest {
public:
SSLBrowserTestWithInsecureFormsWarningDisabled() {
feature_list_.InitAndDisableFeature(
security_interstitials::kInsecureFormSubmissionInterstitial);
}
private:
base::test::ScopedFeatureList feature_list_;
};
// Visits a page that displays an insecure form, submits the form, and checks no
// interstitial is displayed with the feature off.
IN_PROC_BROWSER_TEST_F(SSLBrowserTestWithInsecureFormsWarningDisabled,
TestNoInsecureFormWarning) {
GURL insecure_form_url = https_server_->GetURL("/insecure_form.html");
GURL form_target_url = GURL("http://does-not-exist.test/form_target.html?");
NavigateAndWaitForCompletion(insecure_form_url, shell());
// Submit the form and wait for the form target to load. We wait for a
// failure since the target url is not served.
TestNavigationObserver navigation_observer(
form_target_url, TestNavigationObserver::NavigationEvent::kFailure,
shell());
ExecuteScript(shell(), "submitForm();", false /*use_separate_isolate*/);
navigation_observer.Wait();
// Check no interstitial loaded.
EXPECT_FALSE(IsShowingSecurityInterstitial(shell()->tab()));
}
} // namespace weblayer