| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <algorithm> |
| #include <memory> |
| #include <vector> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/simple_test_clock.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/captive_portal/captive_portal_service_factory.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/extensions/api/settings_private/generated_prefs.h" |
| #include "chrome/browser/interstitials/security_interstitial_page_test_utils.h" |
| #include "chrome/browser/preloading/scoped_prewarm_feature_list.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ssl/chrome_security_blocking_page_factory.h" |
| #include "chrome/browser/ssl/generated_https_first_mode_pref.h" |
| #include "chrome/browser/ssl/https_first_mode_settings_tracker.h" |
| #include "chrome/browser/ssl/https_upgrades_interceptor.h" |
| #include "chrome/browser/ssl/https_upgrades_navigation_throttle.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_tabstrip.h" |
| #include "chrome/browser/ui/browser_window.h" |
| #include "chrome/browser/ui/location_bar/location_bar.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/test/base/in_process_browser_test.h" |
| #include "chrome/test/base/ui_test_utils.h" |
| #include "components/captive_portal/content/captive_portal_service.h" |
| #include "components/content_settings/core/browser/host_content_settings_map.h" |
| #include "components/embedder_support/pref_names.h" |
| #include "components/omnibox/browser/omnibox_client.h" |
| #include "components/omnibox/browser/omnibox_controller.h" |
| #include "components/omnibox/browser/omnibox_view.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/security_interstitials/content/stateful_ssl_host_state_delegate.h" |
| #include "components/security_interstitials/core/https_only_mode_metrics.h" |
| #include "components/security_interstitials/core/metrics_helper.h" |
| #include "components/security_state/content/security_state_tab_helper.h" |
| #include "components/site_engagement/content/site_engagement_service.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/ukm/test_ukm_recorder.h" |
| #include "components/variations/active_field_trials.h" |
| #include "components/variations/hashing.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "content/public/test/content_browser_test_utils.h" |
| #include "content/public/test/content_mock_cert_verifier.h" |
| #include "content/public/test/test_navigation_observer.h" |
| #include "content/public/test/url_loader_interceptor.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/http/http_util.h" |
| #include "net/test/cert_test_util.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_request.h" |
| #include "net/test/embedded_test_server/request_handler_util.h" |
| #include "net/test/test_data_directory.h" |
| #include "services/metrics/public/cpp/ukm_builders.h" |
| #include "services/metrics/public/cpp/ukm_source.h" |
| #include "services/network/public/cpp/ip_address_space_overrides_test_utils.h" |
| #include "services/network/public/cpp/network_switches.h" |
| #include "services/network/public/cpp/url_loader_completion_status.h" |
| #include "services/network/public/mojom/ip_address_space.mojom.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "url/url_constants.h" |
| |
| using chrome_browser_interstitials::HFMInterstitialType; |
| using security_interstitials::https_only_mode::BlockingResult; |
| using security_interstitials::https_only_mode::Event; |
| using security_interstitials::https_only_mode::InterstitialReason; |
| using security_interstitials::https_only_mode::kEventHistogram; |
| using security_interstitials::https_only_mode:: |
| kEventHistogramWithEngagementHeuristic; |
| using security_interstitials::https_only_mode::kInterstitialReasonHistogram; |
| using security_interstitials::https_only_mode:: |
| kNavigationRequestSecurityLevelHistogram; |
| using security_interstitials::https_only_mode:: |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram; |
| using security_interstitials::https_only_mode:: |
| kSiteEngagementHeuristicEnforcementDurationHistogram; |
| using security_interstitials::https_only_mode:: |
| kSiteEngagementHeuristicHostCountHistogram; |
| using security_interstitials::https_only_mode:: |
| kSiteEngagementHeuristicStateHistogram; |
| using security_interstitials::https_only_mode::NavigationRequestSecurityLevel; |
| using security_interstitials::https_only_mode::SiteEngagementHeuristicState; |
| using UkmEntry = ukm::builders::HttpsFirstMode_Event; |
| |
| // Many of the following tests have only minor variations for HTTPS-First Mode |
| // vs. HTTPS-Upgrades. These get parameterized so the tests run under both |
| // versions on their own as well as when both HTTPS Upgrades and HTTPS-First |
| // Mode are enabled (to test any interactions between the two upgrade modes). |
| // HTTPS-Upgrades is now enabled by default, so all of these variations build |
| // on top of that baseline. |
| // |
| // Quick summary of all features tested here: |
| // * HTTPS-Upgrades: |
| // Automatically upgrades main frame navigations to HTTPS. Silently falls |
| // back to HTTP on failure. |
| // * HTTPS First Mode: |
| // Automatically upgrades main frame navigations to HTTPS. Shows an |
| // interstitial on failure. |
| // * HTTPS First Mode With Site Engagement: |
| // Automatically enables HTTPS First Mode for sites that are visited mainly |
| // over HTTPS. |
| // * HTTPS First Mode for Typically Secure Users |
| // Automatically enables HTTPS First Mode for users that mainly visit HTTPS |
| // sites. |
| // * HTTPS First Mode in Incognito: |
| // Automatically enables HTTPS First Mode in Incognito windows. |
| // * HTTPS First Balanced Mode: |
| // Enables HTTPS First Mode like full HFM, but exempt navigations that are |
| // likely to fail. |
| // |
| enum class HttpsUpgradesTestType { |
| // Enables the HFM pref. |
| kHttpsFirstModeOnly, |
| |
| // Enables HFM with Site Engagement heuristic. |
| kHttpsFirstModeWithSiteEngagement, |
| |
| // Enables HFM for Typically Secure Users. |
| kHttpsFirstModeForTypicallySecureUsers, |
| |
| // Enables HFM with Site Engagement and HFM for Typically Secure Users (both |
| // automatically enable HFM). Disables BalancedModeByDefault as that case is |
| // already tested by kHttpsFirstBalancedMode. |
| kAllAutoHFM, |
| |
| // Enables HFM in Incognito mode. Runs testcases inside an Incognito |
| // window. |
| kHttpsFirstModeIncognito, |
| |
| // Enables HFM in balanced mode. |
| kHttpsFirstBalancedMode, |
| |
| // Enables HFM pref, HFM with Site Engagement heuristic, HFM for typically |
| // secure users, HFM in incognito, and balanced HFM feature flags. |
| kAll, |
| |
| // Disables HFM pref, HFM with Site Engagement heuristic, the HFM for |
| // typically secure users feature, and the HFM in Incognito feature. |
| kNone, |
| |
| // HttpsUpgradesBrowserTest tests don't run in these modes. They are used to |
| // instantiate individual tests instead: |
| |
| // Enables HFM with Site Engagement heuristic without Balanced Mode. This |
| // should be a no-op. |
| kHttpsFirstModeWithSiteEngagementWithoutBalancedMode, |
| }; |
| |
| // Stores the number of times the HTTPS-First Mode interstitial is shown for the |
| // given reason. |
| struct ExpectedInterstitialReasons { |
| // The number of times the interstitial was shown because the HFM pref was |
| // enabled. |
| size_t pref = 0; |
| // The number of times the interstitial was shown because of the Typically |
| // Secure User heuristic. |
| size_t typically_secure_user = 0; |
| // The number of times the interstitial was shown because of being in balanced |
| // mode. |
| size_t balanced = 0; |
| }; |
| |
| // A very low site engagement score. |
| constexpr int kLowSiteEngagementScore = 1; |
| // A very high site engagement score. |
| constexpr int kHighSiteEnagementScore = 99; |
| |
| // Tests for HTTPS-Upgrades and the v2 implementation of HTTPS-First Mode. |
| class HttpsUpgradesBrowserTest |
| : public testing::WithParamInterface<HttpsUpgradesTestType>, |
| public InProcessBrowserTest { |
| public: |
| HttpsUpgradesBrowserTest() = default; |
| ~HttpsUpgradesBrowserTest() override = default; |
| |
| void SetUp() override { |
| // HFM heuristics are disabled on enterprise managed machines, so some of |
| // the tests may fail on bots. Explicitly set management status to false. |
| // Some of the tests check enterprise policies, so this configuration is |
| // unusual because non-enterprise machines are unlikely to have an |
| // enterprise allowlist, but it's good for test coverage. |
| ChromeSecurityBlockingPageFactory::SetEnterpriseManagedForTesting(false); |
| |
| // HFM is controlled by a pref (configured in SetUpOnMainThread). |
| switch (https_upgrades_test_type()) { |
| case HttpsUpgradesTestType::kHttpsFirstModeOnly: |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/{}, |
| /*disabled_features=*/{ |
| features::kHttpsFirstModeV2ForEngagedSites, |
| features::kHttpsFirstModeV2ForTypicallySecureUsers, |
| features::kHttpsFirstDialogUi}); |
| break; |
| |
| case HttpsUpgradesTestType::kHttpsFirstModeWithSiteEngagement: |
| // HFM pref is disabled in SetUpOnMainThread. |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/{features::kHttpsFirstModeV2ForEngagedSites, |
| features::kHttpsFirstBalancedMode}, |
| /*disabled_features=*/{ |
| features::kHttpsFirstModeV2ForTypicallySecureUsers, |
| features::kHttpsFirstBalancedModeAutoEnable, |
| features::kHttpsFirstDialogUi}); |
| break; |
| |
| case HttpsUpgradesTestType:: |
| kHttpsFirstModeWithSiteEngagementWithoutBalancedMode: |
| // HFM pref is disabled in SetUpOnMainThread. |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/{features::kHttpsFirstModeV2ForEngagedSites}, |
| /*disabled_features=*/{ |
| features::kHttpsFirstModeV2ForTypicallySecureUsers, |
| features::kHttpsFirstBalancedMode, |
| features::kHttpsFirstDialogUi}); |
| break; |
| |
| case HttpsUpgradesTestType::kHttpsFirstModeForTypicallySecureUsers: |
| // HFM pref is disabled in SetUpOnMainThread. |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/{features:: |
| kHttpsFirstModeV2ForTypicallySecureUsers, |
| features::kHttpsFirstBalancedMode}, |
| /*disabled_features=*/{features::kHttpsFirstModeV2ForEngagedSites, |
| features::kHttpsFirstBalancedModeAutoEnable, |
| features::kHttpsFirstDialogUi}); |
| break; |
| |
| case HttpsUpgradesTestType::kAllAutoHFM: |
| // HFM pref is disabled in SetUpOnMainThread. |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/{features:: |
| kHttpsFirstModeV2ForTypicallySecureUsers, |
| features::kHttpsFirstModeV2ForEngagedSites, |
| features::kHttpsFirstBalancedMode}, |
| /*disabled_features=*/{features::kHttpsFirstBalancedModeAutoEnable, |
| features::kHttpsFirstDialogUi}); |
| break; |
| |
| case HttpsUpgradesTestType::kHttpsFirstModeIncognito: |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/{features::kHttpsFirstModeIncognito}, |
| /*disabled_features=*/{features::kHttpsFirstDialogUi}); |
| break; |
| |
| case HttpsUpgradesTestType::kHttpsFirstBalancedMode: |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/{features::kHttpsFirstBalancedMode, |
| features::kHttpsFirstBalancedModeAutoEnable}, |
| /*disabled_features=*/{ |
| features::kHttpsFirstModeV2ForTypicallySecureUsers, |
| features::kHttpsFirstModeV2ForEngagedSites, |
| features::kHttpsFirstDialogUi}); |
| break; |
| |
| // Enable HFM, HFM with Site Engagement heuristic, HFM for typically |
| // secure users, and HFM in Incognito. |
| case HttpsUpgradesTestType::kAll: |
| // HFM pref is enabled in SetUpOnMainThread. |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/ |
| { |
| features::kHttpsFirstModeV2ForEngagedSites, |
| features::kHttpsFirstModeV2ForTypicallySecureUsers, |
| features::kHttpsFirstModeForAdvancedProtectionUsers, |
| features::kHttpsFirstModeIncognito, |
| features::kHttpsFirstBalancedMode, |
| features::kHttpsFirstBalancedModeAutoEnable, |
| }, |
| /*disabled_features=*/{features::kHttpsFirstDialogUi}); |
| break; |
| |
| // Disable HFM, HFM with Site Engagement heuristic, HFM for Typically |
| // Secure Users, and HFM in Incognito. (HFM pref is disabled in |
| // SetUpOnMainThread.) This is equivalent to the baseline default of |
| // HTTPS-Upgrades. |
| case HttpsUpgradesTestType::kNone: |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/{}, |
| /*disabled_features=*/{ |
| features::kHttpsFirstModeV2ForEngagedSites, |
| features::kHttpsFirstModeV2ForTypicallySecureUsers, |
| features::kHttpsFirstBalancedMode, |
| features::kHttpsFirstBalancedModeAutoEnable, |
| features::kHttpsFirstDialogUi}); |
| break; |
| } |
| |
| InProcessBrowserTest::SetUp(); |
| } |
| |
| void SetUpOnMainThread() override { |
| // By default allow all hosts on HTTPS. |
| mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| |
| // Set up "bad-https.com", "bad-https2.com", "nonunique-hostname-bad-https" |
| // and "nonunique-hostname-bad-https2" as hostnames with an SSL error. |
| // HTTPS upgrades to these hosts will fail. |
| scoped_refptr<net::X509Certificate> cert(https_server_.GetCertificate()); |
| net::CertVerifyResult verify_result; |
| verify_result.is_issued_by_known_root = false; |
| verify_result.verified_cert = cert; |
| verify_result.cert_status = net::CERT_STATUS_COMMON_NAME_INVALID; |
| mock_cert_verifier_.mock_cert_verifier()->AddResultForCertAndHost( |
| cert, "bad-https.com", verify_result, |
| net::ERR_CERT_COMMON_NAME_INVALID); |
| mock_cert_verifier_.mock_cert_verifier()->AddResultForCertAndHost( |
| cert, "www.bad-https.com", verify_result, |
| net::ERR_CERT_COMMON_NAME_INVALID); |
| mock_cert_verifier_.mock_cert_verifier()->AddResultForCertAndHost( |
| cert, "bad-https2.com", verify_result, |
| net::ERR_CERT_COMMON_NAME_INVALID); |
| mock_cert_verifier_.mock_cert_verifier()->AddResultForCertAndHost( |
| cert, "nonunique-hostname-bad-https", verify_result, |
| net::ERR_CERT_COMMON_NAME_INVALID); |
| mock_cert_verifier_.mock_cert_verifier()->AddResultForCertAndHost( |
| cert, "nonunique-hostname-bad-https2", verify_result, |
| net::ERR_CERT_COMMON_NAME_INVALID); |
| |
| http_server_.AddDefaultHandlers(GetChromeTestDataDir()); |
| https_server_.AddDefaultHandlers(GetChromeTestDataDir()); |
| ASSERT_TRUE(http_server_.Start()); |
| ASSERT_TRUE(https_server_.Start()); |
| |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(https_server()->port()); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(http_server()->port()); |
| |
| // Incognito tests swap out the default Browser instance for an Incognito |
| // window, and then should behave like kHttpsFirstMode type tests but |
| // without enabling the full HFM pref. |
| if (https_upgrades_test_type() == |
| HttpsUpgradesTestType::kHttpsFirstModeIncognito) { |
| UseIncognitoBrowser(); |
| SetPref(false); |
| } |
| |
| // Only enable the HTTPS-First Mode pref when the test config calls for it. |
| // Some of the HFM heuristics check that the preference wasn't set so as |
| // not to override user preference (e.g. if the user changed the pref by |
| // turning it off from the UI, we don't want to override it). |
| if (IsHttpsFirstModePrefEnabled()) { |
| SetPref(true); |
| } |
| |
| if (InBalancedMode()) { |
| SetBalancedPref(true); |
| } |
| |
| test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>(); |
| } |
| |
| void TearDownOnMainThread() override { |
| browser()->profile()->GetPrefs()->ClearPref(prefs::kHttpsOnlyModeEnabled); |
| browser()->profile()->GetPrefs()->ClearPref( |
| prefs::kHttpsOnlyModeAutoEnabled); |
| browser()->profile()->GetPrefs()->ClearPref(prefs::kHttpsUpgradeFallbacks); |
| browser()->profile()->GetPrefs()->ClearPref( |
| prefs::kHttpsUpgradeNavigations); |
| browser()->profile()->GetPrefs()->ClearPref(prefs::kHttpsFirstBalancedMode); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| mock_cert_verifier_.SetUpCommandLine(command_line); |
| } |
| |
| void SetUpInProcessBrowserTestFixture() override { |
| mock_cert_verifier_.SetUpInProcessBrowserTestFixture(); |
| } |
| |
| void TearDownInProcessBrowserTestFixture() override { |
| mock_cert_verifier_.TearDownInProcessBrowserTestFixture(); |
| } |
| |
| // Incognito testing support |
| // |
| // Returns the active Browser for the test type being run. |
| Browser* GetBrowser() const { |
| return incognito_browser_ ? incognito_browser_.get() : browser(); |
| } |
| // Call to use an Incognito browser rather than the default. |
| void UseIncognitoBrowser() { |
| ASSERT_EQ(nullptr, incognito_browser_.get()); |
| incognito_browser_ = CreateIncognitoBrowser(); |
| } |
| bool IsIncognito() const { return incognito_browser_ != nullptr; } |
| bool OnlyInBalancedMode() const { |
| return https_upgrades_test_type() == |
| HttpsUpgradesTestType::kHttpsFirstBalancedMode; |
| } |
| bool InBalancedMode() const { |
| return https_upgrades_test_type() == |
| HttpsUpgradesTestType::kHttpsFirstBalancedMode || |
| https_upgrades_test_type() == HttpsUpgradesTestType::kAll; |
| } |
| |
| protected: |
| HttpsUpgradesTestType https_upgrades_test_type() const { return GetParam(); } |
| |
| void SetPref(bool enabled) { |
| auto* prefs = browser()->profile()->GetPrefs(); |
| prefs->SetBoolean(prefs::kHttpsOnlyModeEnabled, enabled); |
| } |
| |
| bool GetPref() const { |
| auto* prefs = browser()->profile()->GetPrefs(); |
| return prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled); |
| } |
| |
| void SetBalancedPref(bool enabled) { |
| auto* prefs = browser()->profile()->GetPrefs(); |
| prefs->SetBoolean(prefs::kHttpsFirstBalancedMode, enabled); |
| } |
| |
| bool GetBalancedPref() const { |
| auto* prefs = browser()->profile()->GetPrefs(); |
| return prefs->GetBoolean(prefs::kHttpsFirstBalancedMode); |
| } |
| |
| void ProceedThroughInterstitial(content::WebContents* tab) { |
| content::TestNavigationObserver nav_observer(tab, 1); |
| std::string javascript = "window.certificateErrorPageController.proceed();"; |
| ASSERT_TRUE(content::ExecJs(tab, javascript)); |
| nav_observer.Wait(); |
| } |
| |
| void DontProceedThroughInterstitial(content::WebContents* tab) { |
| content::TestNavigationObserver nav_observer(tab, 1); |
| std::string javascript = |
| "window.certificateErrorPageController.dontProceed();"; |
| ASSERT_TRUE(content::ExecJs(tab, javascript)); |
| nav_observer.Wait(); |
| } |
| |
| void NavigateAndWaitForFallback(content::WebContents* tab, const GURL& url) { |
| // TODO(crbug.com/40248833): With fallback as part of the same navigation, |
| // this helper is no longer particularly useful. Consider updating callers. |
| content::NavigateToURLBlockUntilNavigationsComplete(tab, url, 1); |
| } |
| |
| // Whether HFM is enabled by the UI setting. |
| bool IsHttpsFirstModePrefEnabled() const { |
| return https_upgrades_test_type() == |
| HttpsUpgradesTestType::kHttpsFirstModeOnly || |
| https_upgrades_test_type() == HttpsUpgradesTestType::kAll; |
| } |
| |
| // Whether HFM is enabled for many sites, and thus the tests should run steps |
| // that assume the HTTP interstitial will trigger (i.e., for fallback HTTP |
| // navigations when HTTPS-First Mode is enabled). |
| bool IsHttpsFirstModeInterstitialEnabledAcrossSites() const { |
| return IsHttpsFirstModePrefEnabled() || InBalancedMode() || IsIncognito(); |
| } |
| |
| // Whether HTTPS-First Mode with Site Engagement Heuristic is enabled. When |
| // enabled, this feature will enable HFM on sites that have high Site |
| // Engagement scores on their HTTPS URLs. HFM with Site Engagement requires |
| // HTTPS-Upgrades to be enabled. |
| bool IsSiteEngagementHeuristicEnabled() const { |
| bool enabled = |
| https_upgrades_test_type() == |
| HttpsUpgradesTestType::kHttpsFirstModeWithSiteEngagement || |
| https_upgrades_test_type() == HttpsUpgradesTestType::kAllAutoHFM || |
| https_upgrades_test_type() == HttpsUpgradesTestType::kAll; |
| return enabled; |
| } |
| |
| // Whether automatic HTTPS-First Mode for typically secure users is enabled. |
| // When enabled, this feature will enable HFM for users who would see HFM |
| // warnings very rarely. HFM for typically secure users requires |
| // HTTPS-Upgrades to be enabled. |
| bool IsTypicallySecureUserFeatureEnabled() const { |
| bool enabled = |
| https_upgrades_test_type() == |
| HttpsUpgradesTestType::kHttpsFirstModeForTypicallySecureUsers || |
| https_upgrades_test_type() == HttpsUpgradesTestType::kAllAutoHFM || |
| https_upgrades_test_type() == HttpsUpgradesTestType::kAll; |
| return enabled; |
| } |
| |
| void SetSiteEngagementScore(const GURL& url, double score) { |
| site_engagement::SiteEngagementService* service = |
| site_engagement::SiteEngagementService::Get(browser()->profile()); |
| service->ResetBaseScoreForURL(url, score); |
| ASSERT_EQ(score, service->GetScore(url)); |
| } |
| |
| // Checks that the HTTPS-First Mode interstitial has been shown for the |
| // correct reasons. |
| void CheckInterstitialReasonHistogram( |
| const ExpectedInterstitialReasons& expected_reasons) { |
| histograms()->ExpectTotalCount(kInterstitialReasonHistogram, |
| expected_reasons.pref + |
| expected_reasons.typically_secure_user + |
| expected_reasons.balanced); |
| histograms()->ExpectBucketCount(kInterstitialReasonHistogram, |
| static_cast<int>(InterstitialReason::kPref), |
| expected_reasons.pref); |
| histograms()->ExpectBucketCount( |
| kInterstitialReasonHistogram, |
| static_cast<int>(InterstitialReason::kBalanced), |
| expected_reasons.balanced); |
| histograms()->ExpectBucketCount( |
| kInterstitialReasonHistogram, |
| static_cast<int>(InterstitialReason::kTypicallySecureUserHeuristic), |
| expected_reasons.typically_secure_user); |
| } |
| |
| // Verifies that an HFM interstitial is shown. |
| void ExpectInterstitial(content::WebContents* contents) { |
| EXPECT_EQ(HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } |
| |
| // Verifies that an HFM interstitial is shown only if the HFM-pref is enabled |
| // or we're in balanced mode. |
| void ExpectInterstitialOnlyIfPrefIsSetOrInBalancedMode( |
| content::WebContents* contents) { |
| if (IsHttpsFirstModePrefEnabled() || InBalancedMode()) { |
| ExpectInterstitial(contents); |
| } else { |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| } |
| |
| // Verifies that an HFM interstitial is shown either due to prefs being |
| // enabled or typically secure heuristic. |
| void MaybeExpectTypicallySecureInterstitial(content::WebContents* contents) { |
| if (IsHttpsFirstModePrefEnabled() || InBalancedMode() || |
| IsTypicallySecureUserFeatureEnabled()) { |
| // Typically secure interstitial should only be shown iff HFM is not |
| // enabled by prefs. |
| if (IsTypicallySecureUserFeatureEnabled() && |
| !IsHttpsFirstModePrefEnabled() && !InBalancedMode()) { |
| EXPECT_EQ( |
| HFMInterstitialType::kTypicallySecure, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } else { |
| // Otherwise, interstitial is enabled by prefs. |
| EXPECT_EQ( |
| HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } |
| return; |
| } |
| // Interstitial isn't enabled by the prefs or heuristic. |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| // Prepare the profile so that HFM can be automatically enabled. |
| void SatisfyTypicallySecureHeuristicRequirements( |
| base::SimpleTestClock* clock) { |
| // The total engagement score of all sites must be over a certain threshold. |
| SetSiteEngagementScore(GURL("https://google.com:12345"), 90); |
| |
| // Profile must be old enough. |
| browser()->profile()->SetCreationTimeForTesting(clock->Now() - |
| base::Days(30)); |
| hfm_service()->SetClockForTesting(clock); |
| |
| // There must be a lot of recorded navigations. |
| for (size_t i = 0; i < 1500; i++) { |
| hfm_service()->IncrementRecentNavigationCount(); |
| } |
| clock->Advance(base::Days(15)); |
| |
| // Navigate to an HTTP URL that will upgrade and fall back to HTTP. |
| // This will start Typically Secure observation. |
| GURL http_url("http://bad-https2.com/simple.html"); |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| content::NavigateToURLBlockUntilNavigationsComplete( |
| contents, http_url, /*number_of_navigations=*/1); |
| ExpectInterstitialOnlyIfPrefIsSetOrInBalancedMode(contents); |
| |
| // Advance the clock and navigate to an HTTP URL again. This will drop the |
| // first fallback event. |
| clock->Advance(base::Days(35)); |
| } |
| |
| net::EmbeddedTestServer* http_server() { return &http_server_; } |
| net::EmbeddedTestServer* https_server() { return &https_server_; } |
| base::HistogramTester* histograms() { return &histograms_; } |
| base::test::ScopedFeatureList* feature_list() { return &feature_list_; } |
| |
| HttpsFirstModeService* hfm_service() const { |
| return HttpsFirstModeServiceFactory::GetForProfile(browser()->profile()); |
| } |
| |
| // Checks that the interstitial UKM has an entry for `url` and `result`. |
| void ExpectUKMEntry( |
| const GURL& url, |
| security_interstitials::https_only_mode::BlockingResult result) { |
| auto entries = test_ukm_recorder_->GetEntriesByName(UkmEntry::kEntryName); |
| EXPECT_EQ(1u, entries.size()); |
| |
| test_ukm_recorder_->ExpectEntrySourceHasUrl(entries[0], url); |
| test_ukm_recorder_->ExpectEntryMetric(entries[0], "Result", |
| static_cast<int>(result)); |
| } |
| |
| // Checks that the interstitial UKM has no entry. |
| void ExpectEmptyUKM() { |
| auto entries = test_ukm_recorder_->GetEntriesByName(UkmEntry::kEntryName); |
| EXPECT_EQ(0u, entries.size()); |
| } |
| |
| void EnableCaptivePortalDetection(Browser* browser); |
| |
| private: |
| // TODO(https://crbug.com/423465927): Explore a better approach to make the |
| // existing tests run with the prewarm feature enabled. |
| test::ScopedPrewarmFeatureList prewarm_feature_list_{ |
| test::ScopedPrewarmFeatureList::PrewarmState::kDisabled}; |
| base::test::ScopedFeatureList feature_list_; |
| net::EmbeddedTestServer http_server_{net::EmbeddedTestServer::TYPE_HTTP}; |
| net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS}; |
| content::ContentMockCertVerifier mock_cert_verifier_; |
| base::HistogramTester histograms_; |
| raw_ptr<Browser, AcrossTasksDanglingUntriaged> incognito_browser_ = nullptr; |
| std::unique_ptr<ukm::TestAutoSetUkmRecorder> test_ukm_recorder_; |
| }; |
| |
| // HttpsUpgradesBrowserTest is NOT instantiated for unusual configurations like |
| // kHttpsFirstModeWithSiteEngagementWithoutBalancedMode. |
| INSTANTIATE_TEST_SUITE_P( |
| /* no prefix */, |
| HttpsUpgradesBrowserTest, |
| ::testing::Values( |
| HttpsUpgradesTestType::kHttpsFirstModeOnly, |
| HttpsUpgradesTestType::kHttpsFirstModeWithSiteEngagement, |
| HttpsUpgradesTestType::kHttpsFirstModeForTypicallySecureUsers, |
| HttpsUpgradesTestType::kAllAutoHFM, |
| HttpsUpgradesTestType::kHttpsFirstModeIncognito, |
| HttpsUpgradesTestType::kHttpsFirstBalancedMode, |
| HttpsUpgradesTestType::kAll, |
| HttpsUpgradesTestType::kNone), |
| // Map param to a human-readable string for better test output. |
| [](testing::TestParamInfo<HttpsUpgradesTestType> input_type) |
| -> std::string { |
| switch (input_type.param) { |
| case HttpsUpgradesTestType::kHttpsFirstModeOnly: |
| return "HttpsFirstModeOnly"; |
| case HttpsUpgradesTestType::kHttpsFirstModeWithSiteEngagement: |
| return "HttpsFirstModeWithSiteEngagement"; |
| case HttpsUpgradesTestType::kHttpsFirstModeForTypicallySecureUsers: |
| return "HttpsFirstModeForTypicallySecureUsers"; |
| case HttpsUpgradesTestType::kAllAutoHFM: |
| return "AllAutoHFM"; |
| case HttpsUpgradesTestType::kHttpsFirstModeIncognito: |
| return "HttpsFirstModeIncognito"; |
| case HttpsUpgradesTestType::kHttpsFirstBalancedMode: |
| return "HttpsFirstBalancedMode"; |
| case HttpsUpgradesTestType::kAll: |
| return "AllFeatures"; |
| case HttpsUpgradesTestType::kNone: |
| return "None"; |
| case HttpsUpgradesTestType:: |
| kHttpsFirstModeWithSiteEngagementWithoutBalancedMode: |
| return "kHttpsFirstModeWithSiteEngagementWithoutBalancedMode"; |
| } |
| }); |
| |
| // If the user navigates to an HTTP URL for a site that supports HTTPS, the |
| // navigation should end up on the HTTPS version of the URL if upgrading is |
| // enabled. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| UrlWithHttpScheme_ShouldUpgrade) { |
| GURL http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("foo.com", "/simple.html"); |
| |
| // The NavigateToURL() call returns `false` because the navigation is |
| // redirected to HTTPS. |
| auto* contents = browser()->tab_strip_model()->GetActiveWebContents(); |
| content::TestNavigationObserver nav_observer(contents, 1); |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| nav_observer.Wait(); |
| |
| EXPECT_TRUE(nav_observer.last_navigation_succeeded()); |
| EXPECT_FALSE(chrome_browser_interstitials::IsShowingInterstitial(contents)); |
| |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectTotalCount(kEventHistogram, 2); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeSucceeded, 1); |
| |
| // Also record general request metrics. |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 2); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSecure, 1); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, 1); |
| |
| ExpectEmptyUKM(); |
| } |
| |
| // If the user navigates to an HTTPS URL for a site that supports HTTPS, the |
| // navigation should end up on that exact URL. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| UrlWithHttpsScheme_ShouldLoad) { |
| GURL https_url = https_server()->GetURL("foo.com", "/simple.html"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_TRUE(content::NavigateToURL(contents, https_url)); |
| |
| // Verify that navigation event metrics were not recorded as the navigation |
| // was not upgraded. |
| histograms()->ExpectTotalCount(kEventHistogram, 0); |
| |
| // General navigation metrics should still be recorded. |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 1); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSecure, 1); |
| |
| ExpectEmptyUKM(); |
| } |
| |
| // If the user navigates to a localhost URL, the navigation should end up on |
| // that exact URL. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, Localhost_ShouldNotUpgrade) { |
| GURL localhost_url = http_server()->GetURL("localhost", "/simple.html"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_TRUE(content::NavigateToURL(contents, localhost_url)); |
| |
| // Verify that navigation event metrics were not recorded as the navigation |
| // was not upgraded. |
| histograms()->ExpectTotalCount(kEventHistogram, 0); |
| |
| // Verify that general navigation request metrics were recorded. |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 1); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kLocalhost, |
| 1); |
| |
| ExpectEmptyUKM(); |
| } |
| |
| // Test that HTTPS Upgrades are skipped for non-unique hostnames, such as |
| // non-publicly routable (RFC1918/4193) IP addresses, but HTTPS-First Mode |
| // should still apply. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| NonRoutableIPAddress_ShouldNotUpgrade) { |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| |
| // Set up an interceptor because the test server can't listen on private IPs. |
| GURL local_ip_url("http://192.168.0.1/simple.html"); |
| auto url_loader_interceptor = |
| content::URLLoaderInterceptor::ServeFilesFromDirectoryAtOrigin( |
| GetChromeTestDataDir().MaybeAsASCII(), |
| local_ip_url.GetWithEmptyPath()); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| if (IsHttpsFirstModePrefEnabled()) { |
| // HFM should attempt the upgrade, fail, and fallback to the interstitial. |
| EXPECT_FALSE(content::NavigateToURL(contents, local_ip_url)); |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Verify that upgrade events were recorded because an upgrade was attempted |
| // and failed. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount( |
| kEventHistogram, |
| security_interstitials::https_only_mode::Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount( |
| kEventHistogram, |
| security_interstitials::https_only_mode::Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount( |
| kEventHistogram, |
| security_interstitials::https_only_mode::Event::kUpgradeTimedOut, 1); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 1); |
| } else { |
| // If HFM strict mode is not enabled, we should not attempt to upgrade the |
| // navigation. |
| EXPECT_TRUE(content::NavigateToURL(contents, local_ip_url)); |
| histograms()->ExpectTotalCount(kEventHistogram, 0); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kNonUniqueHostname, 1); |
| } |
| |
| ExpectEmptyUKM(); |
| } |
| |
| // Test that unique single-label hostnames (e.g. gTLDs) are only upgraded and |
| // warned on in strict mode. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| UniqueSingleLabel_NoWarnInBalancedMode) { |
| // Set an HTTPS testing port that does not match the test server to have all |
| // attempted upgrades fail. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| |
| GURL singlelabel_url = http_server()->GetURL("cl", "/simple.html"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| if (IsHttpsFirstModePrefEnabled()) { |
| // HFM should attempt the upgrade, fail, and fallback to the interstitial. |
| EXPECT_FALSE(content::NavigateToURL(contents, singlelabel_url)); |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 2); |
| } else { |
| // Otherwise, the request should not be upgraded and just navigate to HTTP. |
| EXPECT_TRUE(content::NavigateToURL(contents, singlelabel_url)); |
| EXPECT_EQ(singlelabel_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSingleLabelHostname, 1); |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 1); |
| } |
| |
| // If in Strict Mode, verify that upgrade events were recorded because an |
| // upgrade was attempted and failed. |
| if (IsHttpsFirstModePrefEnabled()) { |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount( |
| kEventHistogram, |
| security_interstitials::https_only_mode::Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount( |
| kEventHistogram, |
| security_interstitials::https_only_mode::Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount( |
| kEventHistogram, |
| security_interstitials::https_only_mode::Event::kUpgradeTimedOut, 1); |
| |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 1); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSecure, 1); |
| } |
| |
| ExpectEmptyUKM(); |
| } |
| |
| // If the user navigates to a non-unique hostname, the navigation should be |
| // upgraded only if strict mode is enabled. If we skip upgrading we should |
| // record that the reason was the non-unique hostname. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, NonUniqueHost_RecordsMetrics) { |
| GURL nonunique_url1 = http_server()->GetURL("test.local", "/simple.html"); |
| GURL nonunique_url2 = http_server()->GetURL("test", "/simple.html"); |
| // Note that we don't test with an RFC1918 IP because the test server |
| // wouldn't receive the traffic (since it relies on DNS). |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| if (IsHttpsFirstModePrefEnabled()) { |
| EXPECT_FALSE(content::NavigateToURL(contents, nonunique_url1)); |
| EXPECT_FALSE(content::NavigateToURL(contents, nonunique_url2)); |
| // Other histograms are still recorded. |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 2); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSecure, 2); |
| } else { |
| // When HFM strict mode is not enabled, Chrome does NOT upgrade, so other |
| // histograms are not recorded. |
| EXPECT_TRUE(content::NavigateToURL(contents, nonunique_url1)); |
| EXPECT_TRUE(content::NavigateToURL(contents, nonunique_url2)); |
| histograms()->ExpectUniqueSample( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kNonUniqueHostname, 2); |
| } |
| |
| ExpectEmptyUKM(); |
| } |
| |
| // Test that non-default ports (e.g. not HTTP80) are only upgraded and warned on |
| // in strict mode and Incognito. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| NonDefaultPorts_NoWarnInBalancedMode) { |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| |
| // Set up an interceptor so we can test non-default (and non-testing) ports. |
| GURL non_default_http_url = GURL("http://example.com:8080/simple.html"); |
| auto url_loader_interceptor = |
| content::URLLoaderInterceptor::ServeFilesFromDirectoryAtOrigin( |
| GetChromeTestDataDir().MaybeAsASCII(), |
| non_default_http_url.GetWithEmptyPath()); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| if (IsHttpsFirstModePrefEnabled() || IsIncognito()) { |
| // HFM should attempt the upgrade, fail, and fallback to the interstitial. |
| EXPECT_FALSE(content::NavigateToURL(contents, non_default_http_url)); |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 2); |
| } else { |
| // Otherwise, the request should not be upgraded and just navigate to HTTP. |
| EXPECT_TRUE(content::NavigateToURL(contents, non_default_http_url)); |
| EXPECT_EQ(non_default_http_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kNonDefaultPorts, 1); |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 1); |
| } |
| |
| // If in Strict Mode or Incognito, verify that upgrade events were recorded |
| // because an upgrade was attempted and failed. |
| if (IsHttpsFirstModePrefEnabled() || IsIncognito()) { |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount( |
| kEventHistogram, |
| security_interstitials::https_only_mode::Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount( |
| kEventHistogram, |
| security_interstitials::https_only_mode::Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount( |
| kEventHistogram, |
| security_interstitials::https_only_mode::Event::kUpgradeNetError, 1); |
| |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 1); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSecure, 1); |
| } |
| |
| ExpectEmptyUKM(); |
| } |
| |
| // If the user navigates to an HTTPS URL, the navigation should end up on that |
| // exact URL, even if the site has an SSL error. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| UrlWithHttpsScheme_BrokenSSL_ShouldNotFallback) { |
| GURL https_url = https_server()->GetURL("bad-https.com", "/simple.html"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_FALSE(content::NavigateToURL(contents, https_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| |
| // The SSL error should show regardless of the HFM state. |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingSSLInterstitial(contents)); |
| |
| // Verify that navigation event metrics were not recorded as the navigation |
| // was not upgraded. |
| histograms()->ExpectTotalCount(kEventHistogram, 0); |
| |
| ExpectEmptyUKM(); |
| } |
| |
| // If the user navigates to an HTTP URL for a site with broken HTTPS, the |
| // navigation should end up on the HTTPS URL and show the HTTPS-Only Mode |
| // interstitial. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| UrlWithHttpScheme_BrokenSSL_ShouldInterstitial) { |
| GURL http_url = http_server()->GetURL("bad-https.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("bad-https.com", "/simple.html"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| // Verify that navigation event metrics were correctly recorded. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeCertError, 1); |
| |
| // The user hasn't taken action yet, so this should be empty. |
| ExpectEmptyUKM(); |
| } |
| |
| // HTTPS-First Mode in Incognito should customize the interstitial. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| IncognitoInterstitialVariation) { |
| // This test only applies to fully-enabled HFM and HFM-in-Incognito. |
| if (!IsHttpsFirstModePrefEnabled() && !IsIncognito()) { |
| return; |
| } |
| |
| GURL http_url = http_server()->GetURL("bad-https.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("bad-https.com", "/simple.html"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| if (IsHttpsFirstModePrefEnabled()) { |
| EXPECT_EQ(HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } else if (IsIncognito()) { |
| // Test that HFM-in-Incognito overrides the default interstitial text. |
| EXPECT_EQ(HFMInterstitialType::kIncognito, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } |
| |
| // The user hasn't taken action yet, so this should be empty. |
| ExpectEmptyUKM(); |
| } |
| |
| void MaybeEnableHttpsFirstModeForEngagedSitesAndWait( |
| HttpsFirstModeService* hfm_service) { |
| base::RunLoop run_loop; |
| hfm_service->MaybeEnableHttpsFirstModeForEngagedSites(run_loop.QuitClosure()); |
| run_loop.Run(); |
| } |
| |
| // Returns a URL loader interceptor that responds to HTTPS URLs with a cert |
| // error and to HTTP URLs with a good response. |
| std::unique_ptr<content::URLLoaderInterceptor> |
| MakeInterceptorForSiteEngagementHeuristic() { |
| return std::make_unique<content::URLLoaderInterceptor>( |
| base::BindLambdaForTesting( |
| [](content::URLLoaderInterceptor::RequestParams* params) { |
| if (params->url_request.url.SchemeIs("https")) { |
| // Fail with an SSL error. |
| network::URLLoaderCompletionStatus status; |
| status.error_code = net::ERR_CERT_COMMON_NAME_INVALID; |
| status.ssl_info = net::SSLInfo(); |
| status.ssl_info->cert_status = |
| net::CERT_STATUS_COMMON_NAME_INVALID; |
| // The cert doesn't matter. |
| status.ssl_info->cert = net::ImportCertFromFile( |
| net::GetTestCertsDirectory(), "ok_cert.pem"); |
| status.ssl_info->unverified_cert = status.ssl_info->cert; |
| params->client->OnComplete(status); |
| return true; |
| } |
| content::URLLoaderInterceptor::WriteResponse( |
| "HTTP/1.1 200 OK\nContent-type: text/html\n\n", |
| "<html>Done</html>", params->client.get()); |
| return true; |
| })); |
| } |
| |
| // TODO(crbug.com/40904694): Fails on the linux-wayland-rel bot. |
| #if defined(OZONE_PLATFORM_WAYLAND) |
| #define MAYBE_UrlWithHttpScheme_BrokenSSL_SiteEngagementHeuristic_ShouldInterstitial \ |
| DISABLED_UrlWithHttpScheme_BrokenSSL_SiteEngagementHeuristic_ShouldInterstitial |
| #else |
| #define MAYBE_UrlWithHttpScheme_BrokenSSL_SiteEngagementHeuristic_ShouldInterstitial \ |
| UrlWithHttpScheme_BrokenSSL_SiteEngagementHeuristic_ShouldInterstitial |
| #endif |
| // Test for Site Engagement Heuristic, a feature that enables HFM on specific |
| // sites based on their site engagement scores. |
| // If the user navigates to an HTTP URL for a site with broken HTTPS, the |
| // navigation should end up on the HTTPS URL and show the HTTPS-Only Mode |
| // interstitial. It should also record a separate histogram for Site Engagement |
| // Heuristic if the interstitial isn't enabled. |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| MAYBE_UrlWithHttpScheme_BrokenSSL_SiteEngagementHeuristic_ShouldInterstitial) { |
| // HFM+SE is not enabled in Incognito. |
| if (IsIncognito()) { |
| return; |
| } |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| auto url_loader_interceptor = MakeInterceptorForSiteEngagementHeuristic(); |
| |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| Profile* profile = GetBrowser()->profile(); |
| content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); |
| |
| // Set test clock. |
| auto clock = std::make_unique<base::SimpleTestClock>(); |
| auto* clock_ptr = clock.get(); |
| StatefulSSLHostStateDelegate* chrome_state = |
| static_cast<StatefulSSLHostStateDelegate*>(state); |
| chrome_state->SetClockForTesting(std::move(clock)); |
| |
| // Start the clock at standard system time. |
| clock_ptr->SetNow(base::Time::NowFromSystemTime()); |
| |
| GURL http_url("http://bad-https.com"); |
| GURL https_url("https://bad-https.com"); |
| SetSiteEngagementScore(http_url, kLowSiteEngagementScore); |
| SetSiteEngagementScore(https_url, kHighSiteEnagementScore); |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(profile); |
| MaybeEnableHttpsFirstModeForEngagedSitesAndWait(hfm_service); |
| const bool is_interstitial_due_to_se_heuristic = |
| IsSiteEngagementHeuristicEnabled() && !IsHttpsFirstModePrefEnabled() && |
| !InBalancedMode(); |
| |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites() || |
| IsSiteEngagementHeuristicEnabled()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| EXPECT_EQ(is_interstitial_due_to_se_heuristic |
| ? HFMInterstitialType::kSiteEngagement |
| : HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } else { |
| EXPECT_EQ(HFMInterstitialType::kNone, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } |
| |
| // Verify that navigation event metrics were correctly recorded. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeCertError, 1); |
| |
| // Check engagement heuristic metrics. These are only recorded when the |
| // site engagement heuristic is enabled and the interstitial is due to this |
| // heuristic and not because of prefs. |
| if (is_interstitial_due_to_se_heuristic) { |
| histograms()->ExpectTotalCount(kEventHistogramWithEngagementHeuristic, 3); |
| histograms()->ExpectBucketCount(kEventHistogramWithEngagementHeuristic, |
| Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogramWithEngagementHeuristic, |
| Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogramWithEngagementHeuristic, |
| Event::kUpgradeCertError, 1); |
| // Check the heuristic state. |
| histograms()->ExpectTotalCount(kSiteEngagementHeuristicStateHistogram, 1); |
| histograms()->ExpectBucketCount(kSiteEngagementHeuristicStateHistogram, |
| SiteEngagementHeuristicState::kDisabled, 0); |
| histograms()->ExpectBucketCount(kSiteEngagementHeuristicStateHistogram, |
| SiteEngagementHeuristicState::kEnabled, 1); |
| // Check host count. |
| histograms()->ExpectTotalCount(kSiteEngagementHeuristicHostCountHistogram, |
| 1); |
| histograms()->ExpectBucketCount(kSiteEngagementHeuristicHostCountHistogram, |
| 0, |
| /*expected_count=*/0); |
| histograms()->ExpectBucketCount(kSiteEngagementHeuristicHostCountHistogram, |
| 1, |
| /*expected_count=*/1); |
| // Check accumulated host count. |
| histograms()->ExpectTotalCount( |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram, 1); |
| histograms()->ExpectBucketCount( |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram, 0, |
| /*expected_count=*/0); |
| histograms()->ExpectBucketCount( |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram, 1, |
| /*expected_count=*/1); |
| // Check enforcement duration. Since the host isn't removed from HFM |
| // enforcement list, no duration should be recorded yet. |
| histograms()->ExpectTotalCount( |
| kSiteEngagementHeuristicEnforcementDurationHistogram, 0); |
| |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kHttpsEnforcedOnHostname, 1); |
| } else { |
| histograms()->ExpectTotalCount(kEventHistogramWithEngagementHeuristic, 0); |
| |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kHttpsEnforcedOnHostname, 0); |
| } |
| |
| // Lower HTTPS engagement score. This disables HFM on the site. Also advance |
| // the clock. |
| SetSiteEngagementScore(https_url, 5); |
| clock_ptr->Advance(base::Hours(1)); |
| MaybeEnableHttpsFirstModeForEngagedSitesAndWait(hfm_service); |
| |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| // Should only show the interstitial if the HFM pref is enabled. Site |
| // engagement heuristic alone will no longer cause an interstitial. |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Proceed through the interstitial, which will add the host to the |
| // allowlist and navigate to the HTTP fallback URL. |
| ProceedThroughInterstitial(contents); |
| |
| // Verify that the interstitial metrics were correctly recorded. The |
| // interstitial was shown twice, once clicked through and once not. |
| histograms()->ExpectTotalCount("interstitial.https_first_mode.decision", 4); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::SHOW, 2); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::PROCEED, 1); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::DONT_PROCEED, 1); |
| } else { |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| if (IsSiteEngagementHeuristicEnabled()) { |
| // Verify that the interstitial metrics were correctly recorded. The |
| // interstitial was shown once and navigated away from. |
| histograms()->ExpectTotalCount("interstitial.https_first_mode.decision", |
| 2); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::SHOW, 1); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::DONT_PROCEED, 1); |
| } else { |
| histograms()->ExpectTotalCount("interstitial.https_first_mode.decision", |
| 0); |
| } |
| } |
| |
| // Check engagement heuristic metrics. These are only recorded when the |
| // site engagement heuristic is enabled and the interstitial is due to this |
| // heuristic and not because of prefs. |
| if (IsSiteEngagementHeuristicEnabled() && |
| !IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| // Event histogram shouldn't change because Site Engagement heuristic didn't |
| // kick in. |
| histograms()->ExpectTotalCount(kEventHistogramWithEngagementHeuristic, 3); |
| |
| // Check host count. |
| histograms()->ExpectTotalCount(kSiteEngagementHeuristicHostCountHistogram, |
| 2); |
| histograms()->ExpectBucketCount(kSiteEngagementHeuristicHostCountHistogram, |
| 0, |
| /*expected_count=*/1); |
| histograms()->ExpectBucketCount(kSiteEngagementHeuristicHostCountHistogram, |
| 1, |
| /*expected_count=*/1); |
| // Check accumulated host count. |
| histograms()->ExpectTotalCount( |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram, 2); |
| histograms()->ExpectBucketCount( |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram, 0, |
| /*expected_count=*/0); |
| histograms()->ExpectBucketCount( |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram, 1, |
| /*expected_count=*/2); |
| // Check enforcement duration. The host is now removed from HFM |
| // enforcement list, so its HFM enforcement duration should be recorded now. |
| histograms()->ExpectTotalCount( |
| kSiteEngagementHeuristicEnforcementDurationHistogram, 1); |
| histograms()->ExpectTimeBucketCount( |
| kSiteEngagementHeuristicEnforcementDurationHistogram, base::Hours(1), |
| 1); |
| |
| // This bucket was recorded once previously, shouldn't be recorded again. |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kHttpsEnforcedOnHostname, 1); |
| |
| } else { |
| // Event histogram shouldn't change because Site Engagement heuristic didn't |
| // kick in. |
| histograms()->ExpectTotalCount(kEventHistogramWithEngagementHeuristic, 0); |
| |
| // If HFM pref was enabled, no SE metrics should be recorded because HFM |
| // won't be auto-enabled. |
| histograms()->ExpectTotalCount(kSiteEngagementHeuristicHostCountHistogram, |
| 0); |
| histograms()->ExpectTotalCount( |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram, 0); |
| histograms()->ExpectTotalCount( |
| kSiteEngagementHeuristicEnforcementDurationHistogram, 0); |
| |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kHttpsEnforcedOnHostname, 0); |
| } |
| } |
| |
| // Test that Site Engagement Heuristic doesn't enforce HTTPS on URLs with |
| // non-default ports. |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| UrlWithHttpScheme_BrokenSSL_SiteEngagementHeuristic_ShouldIgnoreUrlsWithNonDefaultPorts) { |
| // HFM+SE is not enabled in Incognito. |
| if (IsIncognito()) { |
| return; |
| } |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| auto url_loader_interceptor = MakeInterceptorForSiteEngagementHeuristic(); |
| |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| Profile* profile = GetBrowser()->profile(); |
| content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); |
| |
| // Set test clock. |
| auto clock = std::make_unique<base::SimpleTestClock>(); |
| auto* clock_ptr = clock.get(); |
| StatefulSSLHostStateDelegate* chrome_state = |
| static_cast<StatefulSSLHostStateDelegate*>(state); |
| chrome_state->SetClockForTesting(std::move(clock)); |
| |
| // Start the clock at standard system time. |
| clock_ptr->SetNow(base::Time::NowFromSystemTime()); |
| |
| GURL http_url("http://bad-https.com"); |
| GURL https_url("https://bad-https.com"); |
| GURL navigated_url("http://bad-https.com:8080"); |
| |
| // Set engagement for the HTTP and HTTPS origins with default ports. |
| SetSiteEngagementScore(http_url, kLowSiteEngagementScore); |
| SetSiteEngagementScore(https_url, kHighSiteEnagementScore); |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(profile); |
| MaybeEnableHttpsFirstModeForEngagedSitesAndWait(hfm_service); |
| |
| // Navigate to a non-default port version of the URL. |
| NavigateAndWaitForFallback(contents, navigated_url); |
| EXPECT_EQ(navigated_url, contents->GetLastCommittedURL()); |
| |
| // Non-strict modes should not upgrade because `navigated_url` has a |
| // non-default port, regardless of whether the hostname is on the enforcelist. |
| if (IsHttpsFirstModePrefEnabled()) { |
| EXPECT_EQ(HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } else { |
| EXPECT_EQ(HFMInterstitialType::kNone, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } |
| |
| // Strict mode should have upgraded and fallen back to HTTP. |
| if (IsHttpsFirstModePrefEnabled()) { |
| // Verify that navigation event metrics were correctly recorded. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, |
| 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeCertError, |
| 1); |
| } else { |
| histograms()->ExpectTotalCount(kEventHistogram, 0); |
| } |
| |
| // Engagement heuristic shouldn't handle any navigation events because we |
| // didn't navigate to bad-https.com:80. |
| histograms()->ExpectTotalCount(kEventHistogramWithEngagementHeuristic, 0); |
| |
| // Check engagement heuristic metrics. These are only recorded when the |
| // site engagement interstitial is enabled. |
| if (IsSiteEngagementHeuristicEnabled() && |
| !IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| // Check the heuristic state. The heuristic should enable HFM for |
| // example.com |
| histograms()->ExpectTotalCount(kSiteEngagementHeuristicStateHistogram, 1); |
| histograms()->ExpectBucketCount(kSiteEngagementHeuristicStateHistogram, |
| SiteEngagementHeuristicState::kDisabled, 0); |
| histograms()->ExpectBucketCount(kSiteEngagementHeuristicStateHistogram, |
| SiteEngagementHeuristicState::kEnabled, 1); |
| // Check host count. |
| histograms()->ExpectTotalCount(kSiteEngagementHeuristicHostCountHistogram, |
| 1); |
| histograms()->ExpectBucketCount(kSiteEngagementHeuristicHostCountHistogram, |
| 0, |
| /*expected_count=*/0); |
| histograms()->ExpectBucketCount(kSiteEngagementHeuristicHostCountHistogram, |
| 1, |
| /*expected_count=*/1); |
| // Check accumulated host count. |
| histograms()->ExpectTotalCount( |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram, 1); |
| histograms()->ExpectBucketCount( |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram, 0, |
| /*expected_count=*/0); |
| histograms()->ExpectBucketCount( |
| kSiteEngagementHeuristicAccumulatedHostCountHistogram, 1, |
| /*expected_count=*/1); |
| // Check enforcement duration. Since the host isn't removed from HFM |
| // enforcement list, no duration should be recorded yet. |
| histograms()->ExpectTotalCount( |
| kSiteEngagementHeuristicEnforcementDurationHistogram, 0); |
| } else { |
| histograms()->ExpectTotalCount(kEventHistogramWithEngagementHeuristic, 0); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| PRE_UrlWithHttpScheme_BrokenSSL_ShouldInterstitial_TypicallySecureUser) { |
| // HFM-for-Typically-Secure-Users is not enabled in Incognito. |
| if (IsIncognito()) { |
| return; |
| } |
| |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| |
| if (!IsHttpsFirstModePrefEnabled() && !InBalancedMode()) { |
| // When HFM is not enabled via pref, these should never be set in this test. |
| EXPECT_FALSE( |
| profile->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeEnabled)); |
| EXPECT_FALSE( |
| profile->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeAutoEnabled)); |
| } |
| |
| // Typically Secure User heuristic requires a minimum total site engagement |
| // score. |
| SetSiteEngagementScore(GURL("https://google.com"), 90); |
| |
| base::SimpleTestClock clock; |
| base::Time now; |
| EXPECT_TRUE(base::Time::FromUTCString("2023-10-15T06:00:00Z", &now)); |
| // Start the clock at standard system time. |
| clock.SetNow(now); |
| |
| profile->SetCreationTimeForTesting(clock.Now() - base::Days(30)); |
| |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(profile); |
| hfm_service->SetClockForTesting(&clock); |
| |
| GURL http_url = http_server()->GetURL("bad-https.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("bad-https.com", "/simple.html"); |
| |
| // Visit the HTTP URL. Profile age is old enough but we haven't been observing |
| // navigations for long enough, so Typically Secure Users feature won't show |
| // an interstitial here. |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| ExpectedInterstitialReasons expected_reasons; |
| |
| if (IsHttpsFirstModePrefEnabled()) { |
| ExpectInterstitial(contents); |
| expected_reasons.pref++; |
| } else if (InBalancedMode()) { |
| ExpectInterstitial(contents); |
| expected_reasons.balanced++; |
| } else { |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| CheckInterstitialReasonHistogram(expected_reasons); |
| |
| // Move the clock forward and revisit HTTP. Profile is old enough now, but |
| // Typically Secure Users feature will only auto-enable HFM after a restart |
| // and show an interstitial. |
| clock.Advance(base::Days(15)); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| if (IsHttpsFirstModePrefEnabled()) { |
| ExpectInterstitial(contents); |
| expected_reasons.pref++; |
| } else if (InBalancedMode()) { |
| ExpectInterstitial(contents); |
| expected_reasons.balanced++; |
| } else { |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| CheckInterstitialReasonHistogram(expected_reasons); |
| |
| if (!IsHttpsFirstModePrefEnabled() && !InBalancedMode()) { |
| EXPECT_FALSE( |
| profile->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeEnabled)); |
| EXPECT_FALSE( |
| profile->GetPrefs()->HasPrefPath(prefs::kHttpsOnlyModeAutoEnabled)); |
| } |
| } |
| |
| // TODO(crbug.com/40925331): Fails on the linux-wayland-rel bot. |
| #if defined(OZONE_PLATFORM_WAYLAND) |
| #define MAYBE_UrlWithHttpScheme_BrokenSSL_ShouldInterstitial_TypicallySecureUser \ |
| DISABLED_UrlWithHttpScheme_BrokenSSL_ShouldInterstitial_TypicallySecureUser |
| #else |
| #define MAYBE_UrlWithHttpScheme_BrokenSSL_ShouldInterstitial_TypicallySecureUser \ |
| UrlWithHttpScheme_BrokenSSL_ShouldInterstitial_TypicallySecureUser |
| #endif |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| MAYBE_UrlWithHttpScheme_BrokenSSL_ShouldInterstitial_TypicallySecureUser) { |
| // HFM-for-Typically-Secure-Users is not enabled in Incognito. |
| if (IsIncognito()) { |
| return; |
| } |
| |
| // Advance the clock to one day after the last fallback event, which happened |
| // on the 15th day. |
| base::SimpleTestClock clock; |
| clock.SetNow(base::Time::NowFromSystemTime() + base::Days(16)); |
| |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(profile); |
| // Do lots of navigations so that Typically Secure User can kick in. |
| for (size_t i = 0; i < 1500; i++) { |
| hfm_service->IncrementRecentNavigationCount(); |
| } |
| |
| hfm_service->SetClockForTesting(&clock); |
| // HFM service runs this on startup, but we can't set the test clock before it |
| // runs, and we need to move the clock forward for this to work. So call it |
| // explicitly again here. |
| hfm_service->CheckUserIsTypicallySecureAndMaybeEnableHttpsFirstBalancedMode(); |
| size_t initial_navigation_count = hfm_service->GetRecentNavigationCount(); |
| |
| // Use a different hostname than the PRE_ test so that we don't hit the |
| // allowlist. |
| GURL http_url = http_server()->GetURL("bad-https2.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("bad-https2.com", "/simple.html"); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_EQ(initial_navigation_count + 1u, |
| hfm_service->GetRecentNavigationCount()); |
| |
| bool expect_interstitial = IsHttpsFirstModeInterstitialEnabledAcrossSites() || |
| IsTypicallySecureUserFeatureEnabled(); |
| // Expect typically secure text only when HFM is auto-enabled, so exclude |
| // HttpsUpgradesTestType::kAll where HFM is enabled via pref). |
| bool expect_typically_secure_user_interstitial_text = |
| https_upgrades_test_type() == |
| HttpsUpgradesTestType::kHttpsFirstModeForTypicallySecureUsers || |
| https_upgrades_test_type() == HttpsUpgradesTestType::kAllAutoHFM; |
| |
| ExpectedInterstitialReasons expected_reasons; |
| if (expect_interstitial) { |
| EXPECT_EQ(expect_typically_secure_user_interstitial_text |
| ? HFMInterstitialType::kTypicallySecure |
| : HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| |
| if (expect_typically_secure_user_interstitial_text) { |
| expected_reasons.typically_secure_user++; |
| } else if (IsHttpsFirstModePrefEnabled()) { |
| expected_reasons.pref++; |
| } else if (InBalancedMode()) { |
| expected_reasons.balanced++; |
| } else { |
| NOTREACHED(); |
| } |
| } else { |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| CheckInterstitialReasonHistogram(expected_reasons); |
| |
| // Move the clock forward a day and revisit HTTP. Should still show HFM |
| // interstitial. |
| clock.Advance(base::Days(1)); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_EQ(initial_navigation_count + 2u, |
| hfm_service->GetRecentNavigationCount()); |
| |
| if (expect_interstitial) { |
| EXPECT_EQ(expect_typically_secure_user_interstitial_text |
| ? HFMInterstitialType::kTypicallySecure |
| : HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| |
| if (expect_typically_secure_user_interstitial_text) { |
| expected_reasons.typically_secure_user++; |
| } else if (IsHttpsFirstModePrefEnabled()) { |
| expected_reasons.pref++; |
| } else if (InBalancedMode()) { |
| expected_reasons.balanced++; |
| } else { |
| NOTREACHED(); |
| } |
| |
| } else { |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| CheckInterstitialReasonHistogram(expected_reasons); |
| |
| // Disable HFM and HF-balanced-mode. Should no longer auto-enable it. |
| SetPref(false); |
| auto* state = static_cast<StatefulSSLHostStateDelegate*>( |
| profile->GetSSLHostStateDelegate()); |
| state->SetHttpsFirstBalancedModeSuppressedForTesting(true); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(initial_navigation_count + 3u, |
| hfm_service->GetRecentNavigationCount()); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Re-enable HFM. Should now show HFM interstitial without the auto-enabled |
| // text. |
| SetPref(true); |
| state->SetHttpsFirstBalancedModeSuppressedForTesting(false); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_EQ(initial_navigation_count + 4u, |
| hfm_service->GetRecentNavigationCount()); |
| |
| EXPECT_EQ(HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| expected_reasons.pref++; |
| |
| CheckInterstitialReasonHistogram(expected_reasons); |
| } |
| |
| // Checks that navigation to a non-unique hostname doesn't display a typically |
| // secure interstitial. |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| TypicallySecure_NonUniqueHostname_ShouldNotShowInterstitial) { |
| // HFM-for-Typically-Secure-Users is not enabled in Incognito. |
| if (IsIncognito()) { |
| return; |
| } |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| auto url_loader_interceptor = MakeInterceptorForSiteEngagementHeuristic(); |
| |
| base::SimpleTestClock clock; |
| SatisfyTypicallySecureHeuristicRequirements(&clock); |
| // This should auto-enable HFM now: |
| hfm_service() |
| ->CheckUserIsTypicallySecureAndMaybeEnableHttpsFirstBalancedMode(); |
| |
| // Check that a bad HTTPS URL should show an interstitial due to the |
| // heuristic. |
| GURL http_url("http://bad-https.com/simple.html"); |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| content::NavigateToURLBlockUntilNavigationsComplete( |
| contents, http_url, /*number_of_navigations=*/1); |
| MaybeExpectTypicallySecureInterstitial(contents); |
| |
| // Check that a non-unique hostname shouldn't show an interstitial due to the |
| // heuristic. |
| GURL nonunique_url("http://nonunique-hostname-bad-https/simple.html"); |
| content::NavigateToURLBlockUntilNavigationsComplete( |
| contents, nonunique_url, /*number_of_navigations=*/1); |
| if (IsHttpsFirstModePrefEnabled()) { |
| // Non-unique hostnames should only show an interstitial in strict mode. |
| EXPECT_EQ(HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } else { |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| } |
| |
| // Same as TypicallySecure_NonUniqueHostname_ShouldNotShowInterstitial, but |
| // also navigates to a non-unique URL before checking the heuristic. The |
| // non-unique URL should not count as a fallback navigation and should not |
| // disable the Typically Secure heuristic. |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| TypicallySecure_NonUniqueHostnameFallbackShouldNotDisableTypicallySecureHeuristic) { |
| // HFM-for-Typically-Secure-Users is not enabled in Incognito. |
| if (IsIncognito()) { |
| return; |
| } |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| auto url_loader_interceptor = MakeInterceptorForSiteEngagementHeuristic(); |
| |
| base::SimpleTestClock clock; |
| SatisfyTypicallySecureHeuristicRequirements(&clock); |
| |
| // Before running the heuristic checks, also navigate to a non-unique |
| // hostname. This will result in an interstitial iff strict mode is enabled. |
| content::WebContents* contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| content::NavigateToURLBlockUntilNavigationsComplete( |
| contents, GURL("http://nonunique-hostname-bad-https2/simple.html"), |
| /*number_of_navigations=*/1); |
| if (IsHttpsFirstModePrefEnabled()) { |
| // Non-unique hostnames should only show an interstitial in strict mode. |
| EXPECT_EQ(HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } else { |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| // This should still auto-enable HFM despite the interstitial for the |
| // non-unique hostname because Typically Secure heuristic ignores fallbacks |
| // for non-unique hostnames. |
| hfm_service() |
| ->CheckUserIsTypicallySecureAndMaybeEnableHttpsFirstBalancedMode(); |
| |
| // Check that a bad HTTPS URL should show an interstitial due to the |
| // heuristic. |
| GURL http_url("http://bad-https.com/simple.html"); |
| content::NavigateToURLBlockUntilNavigationsComplete( |
| contents, http_url, /*number_of_navigations=*/1); |
| MaybeExpectTypicallySecureInterstitial(contents); |
| |
| // Check that a non-unique hostname shouldn't show an interstitial due to the |
| // heuristic. |
| GURL nonunique_url("http://nonunique-hostname-bad-https/simple.html"); |
| content::NavigateToURLBlockUntilNavigationsComplete( |
| contents, nonunique_url, /*number_of_navigations=*/1); |
| if (IsHttpsFirstModePrefEnabled()) { |
| // Non-unique hostnames should only show an interstitial in strict mode. |
| EXPECT_EQ(HFMInterstitialType::kStandard, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| } else { |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| } |
| |
| // Regression test for crbug.com/1441276. Sequence of events: |
| // 1. Loads http://example.com. This gets upgraded to https://example.com. |
| // 2. https://example.com has an iframe for https://nonexistentsite.com. It |
| // navigates away immediately to http://example.com. |
| // 3. This causes a crash in |
| // HttpsUpgradesInterceptor::MaybeCreateLoaderForResponse() for |
| // nonexistentsite.com. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| LoadIFrameAndNavigateAway_ShouldNotCrash) { |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| |
| bool navigated_once = false; |
| auto url_loader_interceptor = std::make_unique<content::URLLoaderInterceptor>( |
| base::BindLambdaForTesting( |
| [&navigated_once]( |
| content::URLLoaderInterceptor::RequestParams* params) { |
| if (params->url_request.url == GURL("https://example.com")) { |
| if (!navigated_once) { |
| // Load an iframe that will result in an error and immediately |
| // navigate away. |
| content::URLLoaderInterceptor::WriteResponse( |
| "HTTP/1.1 200 OK\nContent-type: text/html\n\n", |
| "<html>" |
| "<iframe src='https://nonexistentsite.com'></iframe>" |
| "<script>window.location.href = " |
| "'http://example.com';</script></html>", |
| params->client.get()); |
| navigated_once = true; |
| return true; |
| } |
| // Return a normal response the second time this is called, |
| // otherwise the test will timeout due to navigating back and |
| // forth between http and https URLs. |
| content::URLLoaderInterceptor::WriteResponse( |
| "HTTP/1.1 200 OK\nContent-type: text/html\n\n", |
| "<html>Done</html>", params->client.get()); |
| return true; |
| } |
| |
| if (params->url_request.url == GURL("http://example.com")) { |
| content::URLLoaderInterceptor::WriteResponse( |
| "HTTP/1.1 200 OK\nContent-type: text/html\n\n", |
| "<html>Test</html>", params->client.get()); |
| return true; |
| } |
| |
| if (params->url_request.url.host() == "nonexistentsite.com") { |
| // This request must fail for the bug to trigger. |
| params->client->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_CONNECTION_RESET)); |
| return true; |
| } |
| return false; |
| })); |
| |
| GURL http_url("http://example.com"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| } |
| |
| // If the user triggers an HTTPS-Only Mode interstitial for a host and then |
| // clicks through the interstitial, they should end up on the HTTP URL. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| InterstitialBypassed_HttpFallbackLoaded) { |
| GURL http_url = http_server()->GetURL("bad-https.com", "/simple.html"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Proceed through the interstitial, which will add the host to the |
| // allowlist and navigate to the HTTP fallback URL. |
| ProceedThroughInterstitial(contents); |
| |
| // Verify that the interstitial metrics were correctly recorded. |
| histograms()->ExpectTotalCount("interstitial.https_first_mode.decision", 2); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::SHOW, 1); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::PROCEED, 1); |
| |
| ExpectUKMEntry(http_url, BlockingResult::kInterstitialProceed); |
| } |
| |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| // Verify that navigation event metrics were correctly recorded. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeCertError, 1); |
| |
| // Revisit the site. Should load without a warning, but also record another |
| // UKM. |
| // TODO(crbug.com/406530494): This should also record the allowlisted status. |
| NavigateAndWaitForFallback(contents, http_url); |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| ExpectUKMEntry(http_url, BlockingResult::kInterstitialProceed); |
| } |
| } |
| |
| // If the upgraded HTTPS URL is not available due to a net error, it should |
| // trigger the HTTPS-Only Mode interstitial and offer fallback. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| NetErrorOnUpgrade_ShouldInterstitial) { |
| GURL http_url = http_server()->GetURL("foo.com", "/close-socket"); |
| GURL https_url = https_server()->GetURL("foo.com", "/close-socket"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| // Verify that navigation event metrics were correctly recorded. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeNetError, 1); |
| } |
| |
| // If the upgraded HTTPS URL is not available due to a potentially-transient |
| // exempted net error (here a DNS resolution error), show the regular net error |
| // page instead of the HTTPS-First Mode interstitial. If the network conditions |
| // change such that the network error no longer triggers, reloading the tab |
| // should continue the upgraded navigation, which will fail and trigger fallback |
| // to HTTP. (Regression test for crbug.com/1277211.) |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| ExemptNetErrorOnUpgrade_ShouldNotFallback) { |
| // This test is only interesting when HTTPS-First Mode is enabled. |
| if (!IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| return; |
| } |
| |
| GURL http_url = http_server()->GetURL("bad-https.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("bad-https.com", "/simple.html"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| { |
| // Set up an interceptor that will return ERR_NAME_NOT_RESOLVED. Navigating |
| // to the HTTP URL should get upgraded to HTTPS, but fail with a net error |
| // page on the HTTPS URL. |
| auto dns_failure_interceptor = |
| std::make_unique<content::URLLoaderInterceptor>(base::BindRepeating( |
| [](content::URLLoaderInterceptor::RequestParams* params) { |
| params->client->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_NAME_NOT_RESOLVED)); |
| return true; |
| })); |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE(chrome_browser_interstitials::IsShowingInterstitial(contents)); |
| |
| // Reload the tab. The net error should still be showing as the navigation |
| // still results in ERR_NAME_NOT_RESOLVED. |
| content::TestNavigationObserver nav_observer(contents, 1); |
| contents->GetController().Reload(content::ReloadType::NORMAL, |
| /*check_for_repost=*/false); |
| nav_observer.Wait(); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE(chrome_browser_interstitials::IsShowingInterstitial(contents)); |
| } |
| |
| // Interceptor is now out of scope and no longer applies. Reload the tab and |
| // the upgraded navigation should continue, fail due to the bad HTTPS on the |
| // server, and fall back to HTTP. |
| content::TestNavigationObserver nav_observer(contents, 1); |
| contents->GetController().Reload(content::ReloadType::NORMAL, |
| /*check_for_repost=*/false); |
| nav_observer.Wait(); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| ASSERT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| ProceedThroughInterstitial(contents); |
| } |
| |
| // Should now be on the HTTP URL and it should be allowlisted. |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); |
| EXPECT_TRUE(state->IsHttpAllowedForHost( |
| http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); |
| } |
| |
| // Test that if one site redirects to a non-existent site, that we show the |
| // regular net error page instead of the HTTPS-First Mode interstitial. |
| // (Regression test for crbug.com/1277211.) |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| RedirectToNonexistentSite_ShouldNotInterstitial) { |
| // This test is only interesting when HTTPS-First Mode is enabled. |
| if (!IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| return; |
| } |
| |
| std::string nonexistent_domain = "nonexistentsite.com"; |
| GURL nonexistent_http_url = |
| http_server()->GetURL(nonexistent_domain, "/simple.html"); |
| GURL nonexistent_https_url = |
| https_server()->GetURL(nonexistent_domain, "/simple.html"); |
| std::string www_redirect_path = |
| base::StrCat({"/server-redirect?", nonexistent_http_url.spec()}); |
| GURL redirecting_https_url = |
| https_server()->GetURL("foo.com", www_redirect_path); |
| GURL redirecting_http_url = |
| http_server()->GetURL("foo.com", www_redirect_path); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Set up an interceptor that will return ERR_NAME_NOT_RESOLVED for |
| // nonexistentsite.com. |
| auto dns_failure_interceptor = |
| std::make_unique<content::URLLoaderInterceptor>( |
| base::BindLambdaForTesting( |
| [nonexistent_domain]( |
| content::URLLoaderInterceptor::RequestParams* params) { |
| if (params->url_request.url.host() == nonexistent_domain) { |
| params->client->OnComplete(network::URLLoaderCompletionStatus( |
| net::ERR_NAME_NOT_RESOLVED)); |
| return true; |
| } |
| return false; |
| })); |
| |
| // Navigating to the HTTP URL should get upgraded to HTTPS, but fail with a |
| // net error page on the HTTPS URL. |
| EXPECT_FALSE(content::NavigateToURL(contents, redirecting_http_url)); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| EXPECT_EQ(url::kHttpsScheme, contents->GetLastCommittedURL().scheme()); |
| EXPECT_EQ(nonexistent_domain, contents->GetLastCommittedURL().host()); |
| } |
| |
| // If the upgraded HTTPS URL is not available due to a potentially-transient |
| // exempted net error but the hostname is non-unique, don't show the net error |
| // page and instead just fallback to HTTP and the HTTPS-First Mode interstitial. |
| // Otherwise, the user can be stuck on the net error page when the HTTP version |
| // of the host would have resolved, such as for corp single-label hostnames. |
| // (Regression test for crbug.com/1451040.) |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| ExemptNetErrorOnUpgrade_NonUniqueHostname_ShouldFallback) { |
| // This test is only interesting when HTTPS-First Mode is fully enabled. |
| if (!IsHttpsFirstModePrefEnabled()) { |
| return; |
| } |
| |
| GURL http_url = http_server()->GetURL("blorp", "/simple.html"); |
| GURL https_url = https_server()->GetURL("blorp", "/simple.html"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Set up an interceptor that will return ERR_NAME_NOT_RESOLVED. Navigating |
| // to the HTTP URL should get upgraded to HTTPS, and then fallback to HTTP |
| // and the HFM interstitial. |
| auto dns_failure_interceptor = |
| std::make_unique<content::URLLoaderInterceptor>(base::BindRepeating( |
| [](content::URLLoaderInterceptor::RequestParams* params) { |
| params->client->OnComplete( |
| network::URLLoaderCompletionStatus(net::ERR_NAME_NOT_RESOLVED)); |
| return true; |
| })); |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingInterstitial(contents)); |
| ProceedThroughInterstitial(contents); |
| |
| // Should now be on the HTTP URL and it should be allowlisted. |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); |
| EXPECT_TRUE(state->IsHttpAllowedForHost( |
| http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); |
| |
| ExpectUKMEntry(http_url, BlockingResult::kInterstitialProceed); |
| } |
| |
| // If the upgraded HTTPS URL is not available due to an exempted net error but |
| // is to a single-label unique hostname (i.e. a TLD) don't show the net error |
| // page. This is the same as |
| // ExemptNetErrorOnUpgrade_NonUniqueHostname_ShouldFallback except with a unique |
| // one-label hostname. |
| // (Partial regression test for impact of crrev.com/c/5507613 on b/348330182.) |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| ExemptNetErrorOnUpgrade_UniqueSingleLabelHostname_ShouldFallback) { |
| // This test is only interesting when HTTPS-First Strict Mode is enabled. |
| // Balanced Mode won't try to upgrade these requests at all. |
| if (!IsHttpsFirstModePrefEnabled() || IsIncognito()) { |
| return; |
| } |
| |
| GURL http_url = http_server()->GetURL("cl", "/simple.html"); |
| GURL https_url = https_server()->GetURL("cl", "/simple.html"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Set up an interceptor that will return ERR_NAME_NOT_RESOLVED. Navigating |
| // to the HTTP URL should get upgraded to HTTPS, and then fallback to HTTP |
| // and the HFM interstitial. |
| auto dns_failure_interceptor = |
| std::make_unique<content::URLLoaderInterceptor>(base::BindRepeating( |
| [](content::URLLoaderInterceptor::RequestParams* params) { |
| params->client->OnComplete( |
| network::URLLoaderCompletionStatus(net::ERR_NAME_NOT_RESOLVED)); |
| return true; |
| })); |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingInterstitial(contents)); |
| ProceedThroughInterstitial(contents); |
| |
| // Should now be on the HTTP URL and the hostname should be allowlisted. |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); |
| EXPECT_TRUE(state->IsHttpAllowedForHost( |
| http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); |
| |
| ExpectUKMEntry(http_url, BlockingResult::kInterstitialProceed); |
| } |
| |
| // Navigations in subframes should not get upgraded by HTTPS-Only Mode. They |
| // should be blocked as mixed content. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| HttpsParentHttpSubframeNavigation_Blocked) { |
| const GURL parent_url( |
| https_server()->GetURL("foo.com", "/iframe_blank.html")); |
| const GURL iframe_url(http_server()->GetURL("foo.com", "/simple.html")); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_TRUE(content::NavigateToURL(contents, parent_url)); |
| |
| content::TestNavigationObserver nav_observer(contents, 1); |
| EXPECT_TRUE(content::NavigateIframeToURL(contents, "test", iframe_url)); |
| nav_observer.Wait(); |
| EXPECT_NE(iframe_url, nav_observer.last_navigation_url()); |
| |
| // Verify that no navigation event metrics were recorded. |
| histograms()->ExpectTotalCount(kEventHistogram, 0); |
| } |
| |
| // Navigating to an HTTP URL in a subframe of an HTTP page should not upgrade |
| // the subframe navigation to HTTPS (even if the subframe navigation is to a |
| // different host than the parent frame). |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| HttpParentHttpSubframeNavigation_NotUpgraded) { |
| // The parent frame will fail to upgrade to HTTPS. |
| const GURL parent_url( |
| http_server()->GetURL("bad-https.com", "/iframe_blank.html")); |
| const GURL iframe_url(http_server()->GetURL("bar.com", "/simple.html")); |
| |
| // Navigate to `parent_url` and bypass the HTTPS-Only Mode warning. |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, parent_url); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| // Proceeding through the interstitial will add the hostname to the |
| // allowlist. |
| ProceedThroughInterstitial(contents); |
| } |
| |
| // Verify that navigation event metrics were recorded for the main frame. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| |
| // Navigate the iframe to `iframe_url`. It should successfully navigate and |
| // not get upgraded to HTTPS as the hostname is now in the allowlist. |
| content::TestNavigationObserver nav_observer(contents, 1); |
| EXPECT_TRUE(content::NavigateIframeToURL(contents, "test", iframe_url)); |
| nav_observer.Wait(); |
| EXPECT_EQ(iframe_url, nav_observer.last_navigation_url()); |
| |
| // Verify that no new navigation event metrics were recorded for the subframe. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| } |
| |
| // Tests that a navigation to the HTTP version of a site with an HTTPS version |
| // that is slow to respond gets upgraded to HTTPS but times out and shows the |
| // HTTPS-Only Mode interstitial. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, SlowHttps_ShouldInterstitial) { |
| // Set timeout to zero so that HTTPS upgrades immediately timeout. |
| HttpsUpgradesNavigationThrottle::set_timeout_for_testing(base::TimeDelta()); |
| |
| // Set up a custom HTTPS server that times out without sending a response. |
| net::EmbeddedTestServer timeout_server{net::EmbeddedTestServer::TYPE_HTTPS}; |
| timeout_server.RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| // Server will hang until destroyed. |
| return std::make_unique<net::test_server::HungResponse>(); |
| })); |
| ASSERT_TRUE(timeout_server.Start()); |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(timeout_server.port()); |
| |
| const GURL http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| } |
| |
| // Tests that an HTTP POST form navigation to "bar.com" from an HTTP page on |
| // "foo.com" is not upgraded to HTTPS. (HTTP form navigations from HTTPS are |
| // blocked by the Mixed Forms warning.) |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, HttpPageHttpPost_NotUpgraded) { |
| // Point the HTTP form target to "bar.com". |
| base::StringPairs replacement_text; |
| replacement_text.emplace_back(make_pair( |
| "REPLACE_WITH_HOST_AND_PORT", |
| net::HostPortPair::FromURL(http_server()->GetURL("foo.com", "/")) |
| .ToString())); |
| auto replacement_path = net::test_server::GetFilePathWithReplacements( |
| "/ssl/page_with_form_targeting_http_url.html", replacement_text); |
| |
| // Navigate to the page hosting the form on "foo.com". |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| content::NavigateToURLBlockUntilNavigationsComplete( |
| contents, http_server()->GetURL("bad-https.com", replacement_path), 1); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| // The HTTPS-Only Mode interstitial should trigger. |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| // Proceed through the interstitial to add the hostname to the allowlist. |
| ProceedThroughInterstitial(contents); |
| } |
| |
| // Verify that navigation event metrics were recorded for the initial page. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| |
| // Submit the form and wait for the navigation to complete. |
| content::TestNavigationObserver nav_observer(contents, 1); |
| ASSERT_TRUE( |
| content::ExecJs(contents, "document.getElementById('submit').click();")); |
| nav_observer.Wait(); |
| |
| // Check that the navigation has ended up on the HTTP target. |
| EXPECT_EQ("foo.com", contents->GetLastCommittedURL().host()); |
| EXPECT_TRUE(contents->GetLastCommittedURL().SchemeIs(url::kHttpScheme)); |
| |
| // Verify that no new navigation event metrics were recorded for the POST |
| // navigation. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| } |
| |
| // Tests that if an HTTPS navigation redirects to HTTP on a different host, it |
| // should upgrade to HTTPS on that new host. (A downgrade redirect on the same |
| // host would imply a redirect loop.) |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| HttpsToHttpRedirect_ShouldUpgrade) { |
| GURL target_url = http_server()->GetURL("bar.com", "/title1.html"); |
| GURL url = https_server()->GetURL("foo.com", |
| "/server-redirect?" + target_url.spec()); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // NavigateToURL() returns `false` because the final redirected URL does not |
| // match `url`. Separately ensure the navigation succeeded using a navigation |
| // observer. |
| content::TestNavigationObserver nav_observer(contents, 1); |
| EXPECT_FALSE(content::NavigateToURL(contents, url)); |
| nav_observer.Wait(); |
| EXPECT_TRUE(nav_observer.last_navigation_succeeded()); |
| |
| // Verify that navigation event metrics were correctly recorded. |
| EXPECT_TRUE(contents->GetLastCommittedURL().SchemeIs(url::kHttpsScheme)); |
| histograms()->ExpectTotalCount(kEventHistogram, 2); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeSucceeded, 1); |
| |
| EXPECT_EQ("bar.com", contents->GetLastCommittedURL().host()); |
| } |
| |
| // Regression test for crbug.com/41488861. |
| // Tests that a slow fallback load is not cancelled with timeout. |
| // bad-ssl.com is configured to return a slow load over http. Navigating to |
| // http://bad-ssl.com should upgrade and immediately fall back, then display the |
| // http response without cancelling it for timeout. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| CancelTimeoutForFallbackNavigations) { |
| net::EmbeddedTestServer http_server; |
| net::EmbeddedTestServer https_server{net::EmbeddedTestServer::TYPE_HTTPS}; |
| |
| // Make the HTTP server return a slow response. |
| http_server.RegisterRequestHandler(base::BindRepeating( |
| [](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| // The HTTP load needs to be slower than the 1 second timeout configured |
| // by the the test. |
| auto slow_http_response = |
| std::make_unique<net::test_server::DelayedHttpResponse>( |
| base::Seconds(2)); |
| slow_http_response->set_content_type("text/html"); |
| slow_http_response->set_content("hello from http"); |
| return std::move(slow_http_response); |
| })); |
| ASSERT_TRUE(http_server.Start()); |
| ASSERT_TRUE(https_server.Start()); |
| |
| // Set the timeout short enough, but not zero. We can't set it to zero |
| // because it'll cancel the HTTPS load with timeout instead of an error. |
| HttpsUpgradesNavigationThrottle::set_timeout_for_testing(base::Seconds(1)); |
| |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(http_server.port()); |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(https_server.port()); |
| |
| GURL http_url(http_server.GetURL("bad-https.com", "/")); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| histograms()->ExpectTotalCount("Net.ErrorCodesForMainFrame4", 1); |
| histograms()->ExpectBucketCount("Net.ErrorCodesForMainFrame4", |
| -net::ERR_ABORTED, 1); |
| } else { |
| // Shouldn't record any net errors. |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| histograms()->ExpectTotalCount("Net.ErrorCodesForMainFrame4", 0); |
| } |
| |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeCertError, 1); |
| } |
| |
| // Creates a redirect response. |
| std::unique_ptr<net::test_server::HttpResponse> RedirectResponseHandler( |
| const GURL& dest_url, |
| const net::test_server::HttpRequest& request) { |
| std::unique_ptr<net::test_server::BasicHttpResponse> http_response( |
| new net::test_server::BasicHttpResponse); |
| http_response->set_code(net::HTTP_TEMPORARY_REDIRECT); |
| http_response->AddCustomHeader("Location", dest_url.spec()); |
| return std::move(http_response); |
| } |
| |
| // Creates a response that causes a redirect loop over https and returns a slow |
| // response over http. |
| std::unique_ptr<net::test_server::HttpResponse> RedirectLoopResponse( |
| int& http_port, |
| int& https_port, |
| const net::test_server::HttpRequest& request) { |
| if (request.GetURL().path() == "/redirect") { |
| // Over https, this URL redirects to itself. |
| if (request.GetURL().SchemeIs("https")) { |
| GURL url(base::StringPrintf("http://a.com:%d/redirect", http_port)); |
| return RedirectResponseHandler(url, request); |
| } |
| // Over http, it prints a slow hello. This should delay longer than the |
| // HTTPS upgrade timeout which is set to 1 second. |
| auto slow_http_response = |
| std::make_unique<net::test_server::DelayedHttpResponse>( |
| base::Seconds(2)); |
| slow_http_response->set_content_type("text/html"); |
| slow_http_response->set_content("hello from http"); |
| return std::move(slow_http_response); |
| } |
| return nullptr; |
| } |
| |
| // Another regression test for crbug.com/41488861. |
| // Tests that a slow load that's detected as a redirect loop will not display |
| // a flash of a net error page. |
| // |
| // Assume that a.com/redirect: |
| // - Shows a slow loading response when loaded over http |
| // - Redirects to http://a.com/redirect when loaded over https. |
| // |
| // The flow of the test is as follows: |
| // 1. Load http://a.com/redirect. |
| // 2. http://a.com/redirect is upgraded to http. |
| // 3. https://a.com/redirect redirects to http://a.com/redirect |
| // 4. This triggers a redirect loop (the URL was seen at step 1). |
| // 5. a.com is allowlisted and http://a.com/redirect is loaded as fallback. |
| // 6. http://a.com/redirect prints a message after a slow load. |
| // |
| // This flow should never display a net error page with ERR_TIMED_OUT to the |
| // user. If the interstitial is enabled, it should be displayed at the final |
| // step. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| RedirectLoopWithSlowRedirect_ShouldInterstitial) { |
| net::EmbeddedTestServer redirect_server_http; |
| net::EmbeddedTestServer redirect_server_https{ |
| net::EmbeddedTestServer::TYPE_HTTPS}; |
| |
| // Set the timeout short enough, but not zero. We can't set it to zero |
| // because it'll cancel the HTTPS load with timeout instead of an error. |
| // The HTTP load in this test needs to be slower than this timeout for the |
| // test to be meaningful. |
| HttpsUpgradesNavigationThrottle::set_timeout_for_testing(base::Seconds(1)); |
| |
| // We don't know the ports without starting the servers and we can't start |
| // the servers without registering request handlers. Pass them as refs so |
| // that we can change them later. |
| int http_port = 0; |
| int https_port = 0; |
| redirect_server_http.RegisterRequestHandler(base::BindRepeating( |
| &RedirectLoopResponse, std::ref(http_port), std::ref(https_port))); |
| redirect_server_https.RegisterRequestHandler(base::BindRepeating( |
| &RedirectLoopResponse, std::ref(http_port), std::ref(https_port))); |
| |
| ASSERT_TRUE(redirect_server_http.Start()); |
| ASSERT_TRUE(redirect_server_https.Start()); |
| |
| http_port = redirect_server_http.port(); |
| https_port = redirect_server_https.port(); |
| |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(redirect_server_http.port()); |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting( |
| redirect_server_https.port()); |
| |
| GURL http_url(redirect_server_http.GetURL("a.com", "/redirect")); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| histograms()->ExpectTotalCount("Net.ErrorCodesForMainFrame4", 1); |
| histograms()->ExpectBucketCount("Net.ErrorCodesForMainFrame4", |
| -net::ERR_ABORTED, 1); |
| } else { |
| // Shouldn't record any net errors. |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| histograms()->ExpectTotalCount("Net.ErrorCodesForMainFrame4", 0); |
| } |
| |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeRedirectLoop, |
| 1); |
| } |
| |
| // Tests that navigating to an HTTPS page that downgrades to HTTP on the same |
| // host will fail and trigger the HTTPS-Only Mode interstitial (due to |
| // interceptor detecting a redirect loop and triggering fallback). |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| RedirectLoop_ShouldInterstitial) { |
| // Set up a new test server instance so it can have a custom handler. |
| net::EmbeddedTestServer downgrading_server{ |
| net::EmbeddedTestServer::TYPE_HTTPS}; |
| // Downgrade by swapping the scheme for HTTP. HTTPS-Only Mode will upgrade it |
| // back to HTTPS. |
| downgrading_server.RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| GURL::Replacements http_downgrade; |
| http_downgrade.SetSchemeStr(url::kHttpScheme); |
| // The HttpRequest will by default refer to the test server by the |
| // loopback address rather than any hostname in the navigation (i.e., |
| // the EmbeddedTestServer has no notion of virtual hosts). This |
| // explicitly sets the hostname back to the test host so that this |
| // doesn't fail due to the exception for localhost. |
| http_downgrade.SetHostStr("foo.com"); |
| auto redirect_url = request.GetURL().ReplaceComponents(http_downgrade); |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_TEMPORARY_REDIRECT); |
| response->AddCustomHeader("Location", redirect_url.spec()); |
| return response; |
| })); |
| ASSERT_TRUE(downgrading_server.Start()); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(downgrading_server.port()); |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(downgrading_server.port()); |
| |
| GURL url = downgrading_server.GetURL("foo.com", "/"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, url); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| // Verify that navigation event metrics were correctly recorded. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeRedirectLoop, |
| 1); |
| } |
| |
| // Tests that the security level is WARNING when the HTTPS-Only Mode |
| // interstitial is shown for a net error on HTTPS. (Without HTTPS-Only Mode, a |
| // net error would be a security level of NONE.) |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| NetErrorOnUpgrade_SecurityLevelWarning) { |
| GURL http_url = http_server()->GetURL("foo.com", "/close-socket"); |
| GURL https_url = https_server()->GetURL("foo.com", "/close-socket"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| auto* helper = SecurityStateTabHelper::FromWebContents(contents); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| EXPECT_EQ(security_state::WARNING, helper->GetSecurityLevel()); |
| |
| // Proceed through the interstitial to navigate to the HTTP site. |
| ProceedThroughInterstitial(contents); |
| } |
| |
| // The HTTP site results in a net error, which should have security level NONE |
| // (as no connection was made). |
| // TODO(crbug.com/40248833): Uncomment once upgrades are tracked |
| // per-navigation. |
| // EXPECT_EQ(security_state::NONE, helper->GetSecurityLevel()); |
| } |
| |
| // Tests that the security level is WARNING when the HTTPS-Only Mode |
| // interstitial is shown for a cert error on HTTPS. (Without HTTPS-Only Mode, a |
| // a cert error would be a security level of DANGEROUS.) After clicking through |
| // the interstitial, the security level should still be WARNING. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| BrokenSSLOnUpgrade_SecurityLevelWarning) { |
| GURL http_url = http_server()->GetURL("bad-https.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("bad-https.com", "/simple.html"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| auto* helper = SecurityStateTabHelper::FromWebContents(contents); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| EXPECT_EQ(security_state::WARNING, helper->GetSecurityLevel()); |
| |
| // Proceed through the interstitial to navigate to the HTTP page. |
| ProceedThroughInterstitial(contents); |
| } |
| |
| // The security level should still be WARNING. |
| EXPECT_EQ(security_state::WARNING, helper->GetSecurityLevel()); |
| } |
| |
| // Regression test for crbug.com/1233207. |
| // Tests the case where the HTTP version of a site redirects to HTTPS, but the |
| // HTTPS version of the site has a cert error. If the user initially navigates |
| // to the HTTP URL, then HTTPS-First Mode should upgrade the navigation to HTTPS |
| // and trigger the HTTPS-First Mode interstitial when that fails, but if the |
| // user clicks through the HTTPS-First Mode interstitial and falls back into the |
| // HTTP->HTTPS redirect back to the cert error, then the SSL interstitial should |
| // be shown and the user should be able to click through the SSL interstitial to |
| // visit the HTTPS version of the site (but in a DANGEROUS security level |
| // state). |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| HttpsUpgradeWithBrokenSSL_ShouldTriggerSSLInterstitial) { |
| // Set up a new test server instance so it can have a custom handler that |
| // redirects to the HTTPS server. |
| net::EmbeddedTestServer upgrading_server{net::EmbeddedTestServer::TYPE_HTTP}; |
| upgrading_server.RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_TEMPORARY_REDIRECT); |
| response->AddCustomHeader( |
| "Location", |
| "https://bad-https.com:" + |
| base::NumberToString( |
| HttpsUpgradesInterceptor::GetHttpsPortForTesting()) + |
| "/simple.html"); |
| return response; |
| })); |
| ASSERT_TRUE(upgrading_server.Start()); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(upgrading_server.port()); |
| |
| GURL http_url = upgrading_server.GetURL("bad-https.com", "/simple.html"); |
| // HTTPS server will have a cert error. |
| GURL https_url = https_server()->GetURL("bad-https.com", "/simple.html"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| // The HTTPS-First Mode interstitial should trigger first. |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Proceeding through the HTTPS-First Mode interstitial will hit the |
| // upgrading server's HTTP->HTTPS redirect. This should result in an SSL |
| // interstitial (not an HTTPS-First Mode interstitial). |
| ProceedThroughInterstitial(contents); |
| } |
| |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingSSLInterstitial(contents)); |
| |
| // Proceeding through the SSL interstitial should navigate to the HTTPS |
| // version of the site but with the DANGEROUS security level. |
| ProceedThroughInterstitial(contents); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| auto* helper = SecurityStateTabHelper::FromWebContents(contents); |
| EXPECT_EQ(security_state::DANGEROUS, helper->GetSecurityLevel()); |
| |
| // Verify that navigation event metrics were correctly recorded. They should |
| // only have been recorded for the initial navigation that resulted in the |
| // HTTPS-First Mode interstitial. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeCertError, 1); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| // Verify that the interstitial metrics were correctly recorded. |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::SHOW, 1); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::PROCEED, 1); |
| } |
| } |
| |
| // Tests that clicking the "Learn More" link in the HTTPS-First Mode |
| // interstitial opens a new tab for the help center article. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, InterstitialLearnMoreLink) { |
| // This test is only relevant to HTTPS-First Mode. |
| if (!IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| return; |
| } |
| |
| GURL http_url = http_server()->GetURL("foo.com", "/close-socket"); |
| GURL https_url = https_server()->GetURL("foo.com", "/close-socket"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Simulate clicking the learn more link (CMD_OPEN_HELP_CENTER). |
| ASSERT_TRUE(content::ExecJs( |
| contents, "window.certificateErrorPageController.openHelpCenter();")); |
| |
| // New tab should include the p-link "first_mode". |
| EXPECT_EQ(GetBrowser() |
| ->tab_strip_model() |
| ->GetActiveWebContents() |
| ->GetVisibleURL() |
| .query(), |
| "p=first_mode"); |
| |
| // Verify that the interstitial metrics were correctly recorded. |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::SHOW, 1); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.interaction", |
| security_interstitials::MetricsHelper::Interaction::TOTAL_VISITS, 1); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.interaction", |
| security_interstitials::MetricsHelper::Interaction::SHOW_LEARN_MORE, 1); |
| } |
| |
| // Tests that if the user bypasses the HTTPS-First Mode interstitial, and then |
| // later the server fixes their HTTPS support and the user successfully connects |
| // over HTTPS, the allowlist entry is cleared (so HFM will kick in again for |
| // that site). |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, BadHttpsFollowedByGoodHttps) { |
| // TODO(crbug.com/40248833): This test is flakey when only HTTPS Upgrades are |
| // enabled. |
| if (!IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| return; |
| } |
| |
| GURL http_url = http_server()->GetURL("foo.com", "/close-socket"); |
| GURL bad_https_url = https_server()->GetURL("foo.com", "/close-socket"); |
| GURL good_https_url = https_server()->GetURL("foo.com", "/ssl/google.html"); |
| |
| ASSERT_EQ(http_url.host(), bad_https_url.host()); |
| ASSERT_EQ(bad_https_url.host(), good_https_url.host()); |
| |
| auto* tab = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| auto* profile = Profile::FromBrowserContext(tab->GetBrowserContext()); |
| auto* state = static_cast<StatefulSSLHostStateDelegate*>( |
| profile->GetSSLHostStateDelegate()); |
| |
| // First check that main frame requests revoke the decision. |
| |
| // Navigate to `http_url`, which will get upgraded to `bad_https_url`. |
| NavigateAndWaitForFallback(tab, http_url); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| ASSERT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(tab)); |
| ProceedThroughInterstitial(tab); |
| } |
| |
| EXPECT_TRUE(state->HasAllowException( |
| http_url.host(), tab->GetPrimaryMainFrame()->GetStoragePartition())); |
| |
| EXPECT_TRUE(content::NavigateToURL(tab, good_https_url)); |
| EXPECT_FALSE(state->HasAllowException( |
| http_url.host(), tab->GetPrimaryMainFrame()->GetStoragePartition())); |
| |
| // Rarely, an open connection with the bad cert might be reused for the next |
| // navigation, which is supposed to show an interstitial. Close open |
| // connections to ensure a fresh connection (and certificate validation) for |
| // the next navigation. See https://crbug.com/1150592. A deeper fix for this |
| // issue would be to unify certificate bypass logic which is currently split |
| // between the net stack and content layer; see https://crbug.com/488043. |
| // See also: SSLUITest.BadCertFollowedByGoodCert. |
| state->RevokeUserAllowExceptionsHard(http_url.host()); |
| |
| // Now check that subresource requests revoke the decision. |
| |
| // Navigate to `http_url`, which will get upgraded to `bad_https_url`. |
| NavigateAndWaitForFallback(tab, http_url); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| ASSERT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial(tab)); |
| ProceedThroughInterstitial(tab); |
| } |
| |
| EXPECT_TRUE(state->HasAllowException( |
| http_url.host(), tab->GetPrimaryMainFrame()->GetStoragePartition())); |
| |
| // Load "logo.gif" as an image on the page. |
| GURL image = https_server()->GetURL("foo.com", "/ssl/google_files/logo.gif"); |
| |
| // TODO(crbug.com/422956041): this fetch generates an LNA request, its unclear |
| // why. Investigation is required to see if this is an LNA bug or not. |
| EXPECT_EQ( |
| true, |
| EvalJs(tab, |
| std::string("var img = document.createElement('img');img.src ='") + |
| image.spec() + |
| "';" |
| "new Promise(resolve => {" |
| " img.onload=function() { " |
| " resolve(true); };" |
| " document.body.appendChild(img);" |
| "});")); |
| |
| EXPECT_FALSE(state->HasAllowException( |
| http_url.host(), tab->GetPrimaryMainFrame()->GetStoragePartition())); |
| } |
| |
| // Tests that clicking the "Go back" button in the HTTPS-First Mode interstitial |
| // navigates back to the previous page (about:blank in this case). |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, InterstitialGoBack) { |
| // This test is only relevant to HTTPS-First Mode. |
| if (!IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| return; |
| } |
| |
| GURL http_url = http_server()->GetURL("foo.com", "/close-socket"); |
| GURL https_url = https_server()->GetURL("foo.com", "/close-socket"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Simulate clicking the "Go back" button. |
| DontProceedThroughInterstitial(contents); |
| |
| EXPECT_EQ(GURL("about:blank"), contents->GetLastCommittedURL()); |
| |
| // Verify that the interstitial metrics were correctly recorded. |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::SHOW, 1); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::DONT_PROCEED, 1); |
| |
| ExpectUKMEntry(http_url, BlockingResult::kInterstitialDontProceed); |
| } |
| |
| // Tests that closing the tab of the HTTPS-First Mode interstitial counts as |
| // not proceeding through the interstitial for metrics. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, CloseInterstitialTab) { |
| // This test is only relevant to HTTPS-First Mode. |
| if (!IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| return; |
| } |
| |
| GURL http_url = http_server()->GetURL("foo.com", "/close-socket"); |
| GURL https_url = https_server()->GetURL("foo.com", "/close-socket"); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Leave the interstitial by closing the tab. |
| chrome::CloseWebContents(GetBrowser(), contents, false); |
| |
| // Verify that the interstitial metrics were correctly recorded. |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::SHOW, 1); |
| histograms()->ExpectBucketCount( |
| "interstitial.https_first_mode.decision", |
| security_interstitials::MetricsHelper::Decision::DONT_PROCEED, 1); |
| |
| ExpectUKMEntry(http_url, BlockingResult::kInterstitialDontProceed); |
| } |
| |
| // Tests that if a user allowlists a host and then does not visit it again for |
| // seven days (the expiration period), then the interstitial will be shown again |
| // the next time they visit the host. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, AllowlistEntryExpires) { |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); |
| |
| // Set a testing clock on the StatefulSSLHostStateDelegate, keeping a pointer |
| // to the clock object around so the test can manipulate time. `chrome_state` |
| // takes ownership of `clock`. |
| auto clock = std::make_unique<base::SimpleTestClock>(); |
| auto* clock_ptr = clock.get(); |
| StatefulSSLHostStateDelegate* chrome_state = |
| static_cast<StatefulSSLHostStateDelegate*>(state); |
| chrome_state->SetClockForTesting(std::move(clock)); |
| |
| // Start the clock at standard system time. |
| clock_ptr->SetNow(base::Time::NowFromSystemTime()); |
| |
| // Visit a host that doesn't support HTTPS for the first time, and click |
| // through the HTTPS-First Mode interstitial to allowlist the host. |
| GURL http_url = http_server()->GetURL("bad-https.com", "/simple.html"); |
| NavigateAndWaitForFallback(contents, http_url); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| ProceedThroughInterstitial(contents); |
| } |
| |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_TRUE(state->IsHttpAllowedForHost( |
| http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); |
| |
| // Simulate the clock advancing by 16 days, which is past the expiration |
| // point. |
| clock_ptr->Advance(base::Days(16)); |
| |
| // The host should no longer be allowlisted, and the interstitial should |
| // trigger again. |
| EXPECT_FALSE(state->IsHttpAllowedForHost( |
| http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); |
| NavigateAndWaitForFallback(contents, http_url); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| } |
| |
| // Tests that re-visiting an allowlisted host bumps the expiration time to a new |
| // seven days in the future from now. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, RevisitingBumpsExpiration) { |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); |
| |
| // Set a testing clock on the StatefulSSLHostStateDelegate, keeping a pointer |
| // to the clock object around so the test can manipulate time. `chrome_state` |
| // takes ownership of `clock`. |
| auto clock = std::make_unique<base::SimpleTestClock>(); |
| auto* clock_ptr = clock.get(); |
| StatefulSSLHostStateDelegate* chrome_state = |
| static_cast<StatefulSSLHostStateDelegate*>(state); |
| chrome_state->SetClockForTesting(std::move(clock)); |
| |
| // Start the clock at standard system time. |
| clock_ptr->SetNow(base::Time::NowFromSystemTime()); |
| |
| // Visit a host that doesn't support HTTPS for the first time, and click |
| // through the HTTPS-First Mode interstitial to allowlist the host. |
| GURL http_url = http_server()->GetURL("bad-https.com", "/simple.html"); |
| NavigateAndWaitForFallback(contents, http_url); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| ProceedThroughInterstitial(contents); |
| } |
| |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| EXPECT_TRUE(state->IsHttpAllowedForHost( |
| http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); |
| |
| // Simulate the clock advancing by ten days. |
| clock_ptr->Advance(base::Days(10)); |
| |
| // Navigate to the host again; this will reset the allowlist expiration to |
| // now + 7 days. |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| |
| // Simulate the clock advancing another ten days. This will be _after_ the |
| // initial expiration date of the allowlist entry, but _before_ the bumped |
| // expiration date from the second navigation. |
| clock_ptr->Advance(base::Days(10)); |
| EXPECT_TRUE(state->IsHttpAllowedForHost( |
| http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| // Tests that if a hostname has an HSTS entry registered, then HTTPS-First Mode |
| // should not try to upgrade it (instead allowing HSTS to handle the upgrade as |
| // it is more strict). |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, PreferHstsOverHttpsFirstMode) { |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| |
| // URL for HTTPS server that will result in a certificate error. |
| GURL https_url = https_server()->GetURL("bad-https.com", "/simple.html"); |
| |
| // HTTP version of that URL that will get upgraded to HTTPS (but with the |
| // correct port for the HTTPS server -- the test code can configure |
| // HTTPS-First Mode to be aware of the different ports, but can't do that for |
| // HSTS). |
| GURL::Replacements downgrade_scheme_to_http; |
| downgrade_scheme_to_http.SetSchemeStr(url::kHttpScheme); |
| GURL http_url = https_url.ReplaceComponents(downgrade_scheme_to_http); |
| |
| // Set HTTP testing port to match `http_url`. |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(http_url.EffectiveIntPort()); |
| |
| // Add hostname to the TransportSecurityState. |
| base::Time expiry = base::Time::Now() + base::Days(100); |
| bool include_subdomains = false; |
| auto* network_context = |
| profile->GetDefaultStoragePartition()->GetNetworkContext(); |
| base::RunLoop run_loop; |
| network_context->AddHSTS(http_url.host(), expiry, include_subdomains, |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| // Navigate to the HTTP URL. It should get upgraded to HTTPS and trigger a |
| // fatal certificate error (because of HTTPS) instead of falling back to the |
| // HTTPS-First Mode interstitial. |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingSSLInterstitial(contents)); |
| |
| // Verify that no HFM event histograms were emitted (to check that HFM did not |
| // trigger for this navigation at all). |
| histograms()->ExpectTotalCount(kEventHistogram, 0); |
| |
| // Verify that general navigation request metrics were recorded. |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 2); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kHstsUpgraded, |
| 1); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSecure, 1); |
| } |
| |
| // Regression test for crbug.com/1272781. Previously, performing back/forward |
| // navigations around the HTTPS-First Mode interstitial could cause history |
| // entries to dropped. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| InterstitialFallbackMaintainsHistory) { |
| // This test only applies to HTTPS-First Mode. |
| if (!IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| return; |
| } |
| |
| GURL good_https_url = https_server()->GetURL("site1.com", "/defaultresponse"); |
| |
| // Set up a new test server instance so it can have a custom handler. |
| net::EmbeddedTestServer downgrading_server{ |
| net::EmbeddedTestServer::TYPE_HTTPS}; |
| // Downgrade by swapping the scheme for HTTP. HTTPS-First Mode will upgrade it |
| // back to HTTPS. |
| downgrading_server.RegisterRequestHandler(base::BindLambdaForTesting( |
| [&](const net::test_server::HttpRequest& request) |
| -> std::unique_ptr<net::test_server::HttpResponse> { |
| GURL::Replacements http_downgrade; |
| http_downgrade.SetSchemeStr(url::kHttpScheme); |
| // The HttpRequest will by default refer to the test server by the |
| // loopback address rather than any hostname in the navigation (i.e., |
| // the EmbeddedTestServer has no notion of virtual hosts). This |
| // explicitly sets the hostname back to the test host so that this |
| // doesn't fail due to the exception for localhost. |
| http_downgrade.SetHostStr("site2.com"); |
| auto redirect_url = request.GetURL().ReplaceComponents(http_downgrade); |
| auto response = std::make_unique<net::test_server::BasicHttpResponse>(); |
| response->set_code(net::HTTP_TEMPORARY_REDIRECT); |
| response->AddCustomHeader("Location", redirect_url.spec()); |
| return response; |
| })); |
| ASSERT_TRUE(downgrading_server.Start()); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(downgrading_server.port()); |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(downgrading_server.port()); |
| |
| GURL downgrading_https_url = downgrading_server.GetURL("site2.com", "/"); |
| GURL::Replacements swap_http_scheme; |
| swap_http_scheme.SetSchemeStr(url::kHttpScheme); |
| GURL downgrading_http_url = |
| downgrading_https_url.ReplaceComponents(swap_http_scheme); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Navigate to a "good" HTTPS site. |
| EXPECT_TRUE(content::NavigateToURL(contents, good_https_url)); |
| |
| // Navigate to the HTTP version of `downgrading_https_url`, which will get |
| // upgraded to HTTPS and fail, triggering the HTTPS-First Mode |
| // interstitial. |
| content::NavigateToURLBlockUntilNavigationsComplete(contents, |
| downgrading_http_url, 1); |
| EXPECT_EQ(downgrading_http_url, contents->GetLastCommittedURL()); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Simulate clicking the browser "back" button. |
| EXPECT_TRUE(content::HistoryGoBack(contents)); |
| EXPECT_EQ(good_https_url, contents->GetLastCommittedURL()); |
| auto* helper = SecurityStateTabHelper::FromWebContents(contents); |
| EXPECT_EQ(security_state::SECURE, helper->GetSecurityLevel()); |
| |
| // Simulate clicking the browser "forward" button. The HistoryGoForward() |
| // call returns `false` because it is an error page. |
| EXPECT_FALSE(content::HistoryGoForward(contents)); |
| EXPECT_EQ(downgrading_http_url, contents->GetLastCommittedURL()); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // No forward entry should be present. |
| EXPECT_FALSE(contents->GetController().CanGoForward()); |
| |
| // Simulate clicking the browser "back" button again. Previously this would |
| // result in `about:blank` being shown. |
| EXPECT_TRUE(content::HistoryGoBack(contents)); |
| EXPECT_EQ(good_https_url, contents->GetLastCommittedURL()); |
| |
| // Repeat forward one last time. (Previously the user would no longer be able |
| // to go back any more as the history entries were lost.) |
| EXPECT_FALSE(content::HistoryGoForward(contents)); // error page -> false |
| EXPECT_EQ(downgrading_http_url, contents->GetLastCommittedURL()); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| EXPECT_TRUE(contents->GetController().CanGoBack()); |
| } |
| |
| // Tests that if the HttpAllowlist enterprise policy is set, then HTTPS upgrades |
| // are skipped for hosts in the allowlist. Includes simple hostname, wildcard |
| // hostname pattern, and bare IP address cases. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| EnterpriseAllowlistDisablesUpgrades) { |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Without any policy allowlist, navigate to HTTP URL on foo.com. It *should* |
| // get upgraded to HTTPS. |
| auto http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| auto https_url = https_server()->GetURL("foo.com", "/simple.html"); |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| |
| // Artificially add the pref that gets mapped from the enterprise policy. |
| auto* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| auto* prefs = profile->GetPrefs(); |
| base::Value::List allowlist; |
| allowlist.Append("foo.com"); |
| allowlist.Append("[*.]bar.com"); |
| allowlist.Append(http_server()->GetIPLiteralString()); |
| // These cases should not work, but the policy->pref mapping won't immediately |
| // reject them. |
| allowlist.Append("[*]"); |
| allowlist.Append("*"); |
| prefs->SetList(prefs::kHttpAllowlist, std::move(allowlist)); |
| |
| // Navigate to HTTP URL on foo.com. It should not get upgraded to HTTPS and |
| // no interstitial should be shown. |
| http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| https_url = https_server()->GetURL("foo.com", "/simple.html"); |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Navigate to HTTP URL on bar.com. Same result. |
| http_url = http_server()->GetURL("bar.com", "/simple.html"); |
| https_url = https_server()->GetURL("bar.com", "/simple.html"); |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Navigate to HTTP URL on bar.bar.com. Same result as subdomain wildcard |
| // was specified. |
| http_url = http_server()->GetURL("bar.bar.com", "/simple.html"); |
| https_url = https_server()->GetURL("bar.bar.com", "/simple.html"); |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Navigate to HTTP URL on foo.foo.com. Subdomains of foo.com should not be |
| // considered as being in the allowlist as no wildcard was specified. This |
| // should get upgraded to HTTPS. |
| http_url = http_server()->GetURL("foo.foo.com", "/simple.html"); |
| https_url = https_server()->GetURL("foo.foo.com", "/simple.html"); |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| |
| // Navigate to HTTP URL on baz.com, which is not on the allowlist. Should get |
| // upgraded to HTTPS. |
| http_url = http_server()->GetURL("baz.com", "/simple.html"); |
| https_url = https_server()->GetURL("baz.com", "/simple.html"); |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| |
| // Navigate to HTTP URL on the HTTP test server's IP address. It should not |
| // get upgraded to HTTPS and no interstitial should be shown. |
| http_url = http_server()->GetURL("/simple.html"); |
| https_url = https_server()->GetURL("/simple.html"); |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| // Tests that if the HttpAllowlist enterprise policy is set, then HTTPS upgrades |
| // and HTTPS-First Mode For Site Engagement checks are skipped for hosts in the |
| // allowlist. |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| EnterpriseAllowlistDisablesHttpsFirstModeForSiteEngagament) { |
| // Skip this test when HTTPS-First Mode for Site Engagement isn't enabled. |
| if (!IsSiteEngagementHeuristicEnabled()) { |
| return; |
| } |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| auto url_loader_interceptor = MakeInterceptorForSiteEngagementHeuristic(); |
| |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| auto* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| |
| // Without any policy allowlist, navigate to an HTTP URL. It should show the |
| // HFM+SE interstitial. |
| GURL http_url("http://bad-https.com"); |
| GURL https_url("https://bad-https.com"); |
| SetSiteEngagementScore(http_url, kLowSiteEngagementScore); |
| SetSiteEngagementScore(https_url, kHighSiteEnagementScore); |
| |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(profile); |
| MaybeEnableHttpsFirstModeForEngagedSitesAndWait(hfm_service); |
| |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Artificially add the pref that gets mapped from the enterprise policy. |
| auto* prefs = profile->GetPrefs(); |
| base::Value::List allowlist; |
| allowlist.Append("bad-https.com"); |
| prefs->SetList(prefs::kHttpAllowlist, std::move(allowlist)); |
| |
| // Navigate to the same URL. It should not get upgraded to HTTPS and |
| // no interstitial should be shown. |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| EnterprisePolicyDisablesUpgrades) { |
| // Disable HTTPS-Upgrades via enterprise policy. |
| auto* prefs = browser()->profile()->GetPrefs(); |
| prefs->SetBoolean(prefs::kHttpsUpgradesEnabled, false); |
| |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| GURL http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("foo.com", "/simple.html"); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| // HTTPS-First Mode should supercede HTTPS-Upgrades and upgrade the |
| // navigation despite the HttpsUpgradeMode policy setting. |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 1); |
| } else { |
| // If HTTPS-First Mode is not enabled but upgrading is, then the policy |
| // should prevent the upgrade. |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kAllowlisted, 1); |
| } |
| } |
| |
| // Test that HTTPS Upgrades are skipped if the "Insecure content" site setting |
| // is set to "allow". |
| // MIXED_SCRIPT isn't enabled as a content setting on Android. |
| #if BUILDFLAG(IS_ANDROID) |
| #define MAYBE_InsecureContentSettingDisablesUpgrades \ |
| DISABLED_InsecureContentSettingDisablesUpgrades |
| #else |
| #define MAYBE_InsecureContentSettingDisablesUpgrades \ |
| InsecureContentSettingDisablesUpgrades |
| #endif |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| MAYBE_InsecureContentSettingDisablesUpgrades) { |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| GURL http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("foo.com", "/simple.html"); |
| auto* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| auto* host_content_settings_map = |
| HostContentSettingsMapFactory::GetForProfile(profile); |
| |
| // Set insecure content setting to allowed for `http_url`. |
| host_content_settings_map->SetContentSettingDefaultScope( |
| http_url, GURL(), ContentSettingsType::MIXEDSCRIPT, |
| CONTENT_SETTING_ALLOW); |
| |
| if (IsHttpsFirstModePrefEnabled()) { |
| // If HTTPS-First Mode is enabled, upgrades should still be applied. |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 1); |
| } else { |
| // Otherwise, the upgrades should be skipped. |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kAllowlisted, 1); |
| } |
| |
| // Unset the content settings. |
| host_content_settings_map->ClearSettingsForOneType( |
| ContentSettingsType::MIXEDSCRIPT); |
| |
| // Set insecure content setting to allowed for `https_url`. |
| HostContentSettingsMapFactory::GetForProfile(profile) |
| ->SetContentSettingDefaultScope(https_url, GURL(), |
| ContentSettingsType::MIXEDSCRIPT, |
| CONTENT_SETTING_ALLOW); |
| if (IsHttpsFirstModePrefEnabled()) { |
| // If HTTPS-First Mode is enabled, upgrades should still be applied. |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 2); |
| } else { |
| // Otherwise, the upgrades should be skipped. |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kAllowlisted, 2); |
| } |
| } |
| |
| // Test that HTTPS Upgrades are skipped if the "Insecure content" site setting |
| // is set to "allow". |
| // MIXED_SCRIPT isn't enabled as a content setting on Android. |
| // This test is identical to InsecureContentSettingDisablesUpgrades except it |
| // sets a high site engagement score for the https URL and checks an additional |
| // histogram. |
| #if BUILDFLAG(IS_ANDROID) |
| #define MAYBE_InsecureContentSettingDisablesHFMForEngagedSites \ |
| DISABLED_InsecureContentSettingDisablesHFMForEngagedSites |
| #else |
| #define MAYBE_InsecureContentSettingDisablesHFMForEngagedSites \ |
| InsecureContentSettingDisablesHFMForEngagedSites |
| #endif |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| MAYBE_InsecureContentSettingDisablesHFMForEngagedSites) { |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| GURL http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("foo.com", "/simple.html"); |
| auto* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| auto* host_content_settings_map = |
| HostContentSettingsMapFactory::GetForProfile(profile); |
| |
| // Setting a high engagement score on the HTTPS URL enables HFM on the site |
| // if the HFM+SE feature is enabled, but an Insecure Content entry disables |
| // HFM+SE on the site. |
| SetSiteEngagementScore(http_url, kLowSiteEngagementScore); |
| SetSiteEngagementScore(https_url, kHighSiteEnagementScore); |
| |
| // Set insecure content setting to allowed for `http_url`. |
| host_content_settings_map->SetContentSettingDefaultScope( |
| http_url, GURL(), ContentSettingsType::MIXEDSCRIPT, |
| CONTENT_SETTING_ALLOW); |
| |
| if (IsHttpsFirstModePrefEnabled()) { |
| // If HTTPS-First Mode is fully enabled, upgrades should still be applied. |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 1); |
| } else { |
| // Otherwise, the upgrades should be skipped. |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kAllowlisted, 1); |
| } |
| // In both cases, HFM+SE events shouldn't be recorded because of the Insecure |
| // content setting. |
| histograms()->ExpectTotalCount(kEventHistogramWithEngagementHeuristic, 0); |
| |
| // Unset the content settings. |
| host_content_settings_map->ClearSettingsForOneType( |
| ContentSettingsType::MIXEDSCRIPT); |
| |
| // Set insecure content setting to allowed for `https_url`. |
| HostContentSettingsMapFactory::GetForProfile(profile) |
| ->SetContentSettingDefaultScope(https_url, GURL(), |
| ContentSettingsType::MIXEDSCRIPT, |
| CONTENT_SETTING_ALLOW); |
| if (IsHttpsFirstModePrefEnabled()) { |
| // If HTTPS-First Mode is enabled, upgrades should still be applied. |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 2); |
| } else { |
| // Otherwise, the upgrades should be skipped. |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kAllowlisted, 2); |
| } |
| // In both cases, HFM+SE events shouldn't be recorded because of the Insecure |
| // content setting. |
| histograms()->ExpectTotalCount(kEventHistogramWithEngagementHeuristic, 0); |
| } |
| |
| // Regression test for crbug.com/1431026. Triggers a navigation where HTTPS |
| // upgrades applied multiple times across redirects to different sites. |
| // Should not crash when DCHECKS are enabled. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, crbug1431026) { |
| GURL www_bad_https_url = |
| https_server()->GetURL("www.bad-https.com", "/simple.html"); |
| GURL www_http_url = |
| http_server()->GetURL("www.bad-https.com", "/simple.html"); |
| |
| // Configure HTTP and bad-HTTPS URLs which redirect to www. subdomain. |
| std::string www_redirect_path = |
| base::StrCat({"/server-redirect?", www_http_url.spec()}); |
| GURL redirecting_bad_https_url = |
| https_server()->GetURL("bad-https.com", www_redirect_path); |
| GURL redirecting_http_url = |
| http_server()->GetURL("bad-https.com", www_redirect_path); |
| |
| // A good HTTPS URL which redirects to an HTTP URL, which also redirects. |
| GURL initial_redirecting_good_https_url = https_server()->GetURL( |
| "good-https.com", |
| base::StrCat({"/server-redirect-301?", redirecting_http_url.spec()})); |
| |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_FALSE( |
| content::NavigateToURL(contents, initial_redirecting_good_https_url)); |
| |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| // Should be showing interstitial on http://bad-https.com/. |
| EXPECT_EQ(redirecting_http_url, contents->GetLastCommittedURL()); |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } else { |
| // Either due to no upgrades, or due to fast fallback to HTTP, this should |
| // end up on http://www.bad-https.com. |
| EXPECT_EQ(www_http_url, contents->GetLastCommittedURL()); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| } |
| |
| // Tests that when the HTTPS-First Mode setting is toggled on or off, the |
| // HTTP allowlist is cleared. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| TogglingSettingClearsAllowlist) { |
| // The allowlist in an Incognito window is in-memory only, and is not cleared |
| // when the main profile's pref changes. |
| // TODO(crbug.com/40937027): Add a test to cover the Incognito allowlisting |
| // behavior explicitly. |
| if (IsIncognito()) { |
| return; |
| } |
| |
| auto http_url = http_server()->GetURL("bad-https.com", "/simple.html"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| |
| // Start by enabling HTTPS-First Mode. |
| SetPref(true); |
| |
| // Navigate to a URL that will fail upgrades, and click through the |
| // interstitial to add it to the allowlist. |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| ProceedThroughInterstitial(contents); |
| |
| // Disable the HTTPS-First Mode pref. This should clear the allowlist. |
| SetPref(false); |
| |
| if (InBalancedMode()) { |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_TRUE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| |
| // Proceed through the interstitial and add the host to the allowlist. |
| ProceedThroughInterstitial(contents); |
| } else { |
| // With HTTPS-Upgrades enabled, navigating again should cause the site to |
| // get added back to the allowlist. |
| EXPECT_TRUE(content::NavigateToURL(contents, http_url)); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| // Re-enable the HTTPS-First Mode pref. The allowlist should be cleared again. |
| SetPref(true); |
| |
| // Navigate to a URL that will fail upgrades, and the interstitial should be |
| // shown again as the allowlist was cleared. |
| EXPECT_FALSE(content::NavigateToURL(contents, http_url)); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| // Main window HTTP allowlist should not apply to Incognito window. |
| // Regression test for crbug.com/40949400. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| IncognitoHasSeparateAllowlist) { |
| // This test only covers the case of HFM-in-Incognito. |
| if (!IsIncognito()) { |
| return; |
| } |
| // In a regular window, add a host to the HTTP allowlist. |
| // Note: This is explicitly done with HTTPS-First Mode disabled as that is the |
| // specific regression case for crbug.com/40949400, but HTTPS-First Mode and |
| // HTTPS-Upgrades may eventually have separate allowlists. |
| SetPref(false); |
| auto http_url = http_server()->GetURL("bad-https.com", "/simple.html"); |
| auto* normal_tab = browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_TRUE(content::NavigateToURL(normal_tab, http_url)); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| normal_tab)); |
| |
| // In an Incognito window, navigating to that same host should still trigger |
| // the HTTP interstitial, as the allowlist is not inherited. |
| auto* incognito_tab = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_FALSE(content::NavigateToURL(incognito_tab, http_url)); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| incognito_tab)); |
| } |
| |
| // Tests that URLs typed with an explicit http:// scheme are opted out from |
| // upgrades. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| URLsTypedWithHttpSchemeNoUpgrades) { |
| GURL http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("foo.com", "/simple.html"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| OmniboxClient* omnibox_client = GetBrowser() |
| ->window() |
| ->GetLocationBar() |
| ->GetOmniboxView() |
| ->controller() |
| ->client(); |
| |
| // Simulate the full URL was typed with an http scheme. |
| content::TestNavigationObserver nav_observer(contents, 1); |
| omnibox_client->OnAutocompleteAccept( |
| http_url, nullptr, WindowOpenDisposition::CURRENT_TAB, |
| ui::PAGE_TRANSITION_TYPED, AutocompleteMatchType::URL_WHAT_YOU_TYPED, |
| base::TimeTicks(), false, true, std::u16string(), AutocompleteMatch(), |
| AutocompleteMatch()); |
| nav_observer.Wait(); |
| |
| if (IsHttpsFirstModePrefEnabled() || IsIncognito()) { |
| // Typed http URLs don't opt out of upgrades in HFM. |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| } else { |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 1); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kExplicitHttpScheme, 1); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| } |
| } |
| |
| // Tests that URLs with an explicit http:// scheme are upgraded if they were |
| // autocompleted. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| URLsAutocompletedWithHttpSchemeAreUpgraded) { |
| GURL http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("foo.com", "/simple.html"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| OmniboxClient* omnibox_client = GetBrowser() |
| ->window() |
| ->GetLocationBar() |
| ->GetOmniboxView() |
| ->controller() |
| ->client(); |
| |
| // Simulate the full URL was autocompleted with an http scheme. |
| content::TestNavigationObserver nav_observer(contents, 1); |
| omnibox_client->OnAutocompleteAccept( |
| http_url, nullptr, WindowOpenDisposition::CURRENT_TAB, |
| ui::PAGE_TRANSITION_TYPED, AutocompleteMatchType::NAVSUGGEST, |
| base::TimeTicks(), false, false, std::u16string(), AutocompleteMatch(), |
| AutocompleteMatch()); |
| nav_observer.Wait(); |
| |
| EXPECT_EQ(https_url, contents->GetLastCommittedURL()); |
| } |
| |
| // Tests that URLs typed with an explicit http:// scheme that result in an |
| // opt-out cause the url to be added to the allowlist. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, |
| URLsTypedWithHttpSchemeNoUpgradesAllowlist) { |
| if (IsHttpsFirstModeInterstitialEnabledAcrossSites()) { |
| return; |
| } |
| GURL http_url = http_server()->GetURL("foo.com", "/simple.html"); |
| GURL https_url = https_server()->GetURL("foo.com", "/simple.html"); |
| auto* contents = GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| OmniboxClient* omnibox_client = GetBrowser() |
| ->window() |
| ->GetLocationBar() |
| ->GetOmniboxView() |
| ->controller() |
| ->client(); |
| |
| Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext()); |
| content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); |
| |
| // Site should not yet be in the allowlist. |
| EXPECT_FALSE(state->IsHttpAllowedForHost( |
| http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); |
| |
| // Simulate the full URL was typed with an http scheme. |
| content::TestNavigationObserver nav_observer(contents, 1); |
| omnibox_client->OnAutocompleteAccept( |
| http_url, nullptr, WindowOpenDisposition::CURRENT_TAB, |
| ui::PAGE_TRANSITION_TYPED, AutocompleteMatchType::URL_WHAT_YOU_TYPED, |
| base::TimeTicks(), false, true, std::u16string(), AutocompleteMatch(), |
| AutocompleteMatch()); |
| nav_observer.Wait(); |
| |
| // URL should not have been upgraded, and site should now be in the allowlist. |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| EXPECT_TRUE(state->IsHttpAllowedForHost( |
| http_url.host(), contents->GetPrimaryMainFrame()->GetStoragePartition())); |
| } |
| |
| // Url used to detect the presence of a captive portal. |
| constexpr char kCaptivePortalPingUrl[] = "http://captive-portal-ping-url.com/"; |
| // HTTPS version of the same URL. |
| constexpr char kCaptivePortalPingUrlHttps[] = |
| "https://captive-portal-ping-url.com/"; |
| |
| // Returns a URL loader interceptor that responds to HTTPS URLs with a cert |
| // error and to HTTP URLs with a good response. |
| std::unique_ptr<content::URLLoaderInterceptor> MakeCaptivePortalInterceptor( |
| bool login_page_has_valid_https) { |
| return std::make_unique<content::URLLoaderInterceptor>( |
| base::BindLambdaForTesting( |
| [login_page_has_valid_https]( |
| content::URLLoaderInterceptor::RequestParams* params) { |
| if (params->url_request.url == GURL(kCaptivePortalPingUrl) || |
| (login_page_has_valid_https && |
| params->url_request.url == GURL(kCaptivePortalPingUrlHttps))) { |
| // Return a non-204 response for the captive portal ping URL |
| // so that the portal is detected. |
| content::URLLoaderInterceptor::WriteResponse( |
| "HTTP/1.1 200 OK\nContent-type: text/html\n\n", |
| "<html>Non-204 response to trigger captive portal " |
| "detection</html>", |
| params->client.get()); |
| return true; |
| } |
| |
| if (params->url_request.url.SchemeIs("https")) { |
| // Fail with an SSL error so that a fallback is triggered. |
| network::URLLoaderCompletionStatus status; |
| status.error_code = net::ERR_CERT_COMMON_NAME_INVALID; |
| status.ssl_info = net::SSLInfo(); |
| status.ssl_info->cert_status = |
| net::CERT_STATUS_COMMON_NAME_INVALID; |
| // The cert doesn't matter. |
| status.ssl_info->cert = net::ImportCertFromFile( |
| net::GetTestCertsDirectory(), "ok_cert.pem"); |
| status.ssl_info->unverified_cert = status.ssl_info->cert; |
| params->client->OnComplete(status); |
| return true; |
| } |
| content::URLLoaderInterceptor::WriteResponse( |
| "HTTP/1.1 200 OK\nContent-type: text/html\n\n", |
| "<html>Done</html>", params->client.get()); |
| return true; |
| })); |
| } |
| |
| void HttpsUpgradesBrowserTest::EnableCaptivePortalDetection(Browser* browser) { |
| captive_portal::CaptivePortalService* captive_portal_service = |
| CaptivePortalServiceFactory::GetForProfile(browser->profile()); |
| captive_portal_service->set_test_url(GURL(kCaptivePortalPingUrl)); |
| |
| captive_portal::CaptivePortalService::set_state_for_testing( |
| captive_portal::CaptivePortalService::NOT_TESTING); |
| browser->profile()->GetPrefs()->SetBoolean( |
| embedder_support::kAlternateErrorPagesEnabled, true); |
| } |
| |
| // Checks that an automatically opened captive portal login page is not upgraded |
| // to HTTPS unless the interstitial is enabled. The captive portal's login |
| // page supports https. |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| CaptivePortal_LoginPageWithValidSSL_ShouldNotUpgradeUnlessInterstitialEnabled) { |
| if (https_upgrades_test_type() == |
| HttpsUpgradesTestType::kHttpsFirstModeIncognito) { |
| return; |
| } |
| auto interceptor = |
| MakeCaptivePortalInterceptor(/*login_page_has_valid_https=*/true); |
| |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| EnableCaptivePortalDetection(browser()); |
| |
| auto* tab_strip = GetBrowser()->tab_strip_model(); |
| auto* contents = tab_strip->GetActiveWebContents(); |
| size_t tab_count = tab_strip->count(); |
| |
| // Go to an HTTPS URL. The navigation will fail and trigger a captive portal |
| // detection. |
| ui_test_utils::TabAddedWaiter waiter(browser()); |
| NavigateAndWaitForFallback(contents, |
| GURL("https://ssl-error-for-captive-portal.com/")); |
| waiter.Wait(); |
| |
| // Captive portal login page should not be upgraded. |
| content::WebContents* login_page = tab_strip->GetWebContentsAt(tab_count); |
| content::WaitForLoadStop(login_page); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| login_page)); |
| |
| if (IsHttpsFirstModePrefEnabled()) { |
| // If the interstitial is enabled, captive portal login page should also be |
| // upgraded to HTTPS. |
| EXPECT_EQ(GURL(kCaptivePortalPingUrlHttps), |
| login_page->GetLastCommittedURL()); |
| |
| // Should only attempt an upgrade for the original page. |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 3); |
| // The original page serves bad HTTPS, but any HTTPS URL is counted as |
| // secure in this histogram. Captive portal login page is valid HTTPS, so |
| // it's also counted here. |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSecure, 2); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 1); |
| } else { |
| // Captive portal login page should not be upgraded to HTTPS. |
| EXPECT_EQ(GURL(kCaptivePortalPingUrl), login_page->GetLastCommittedURL()); |
| |
| // Should only attempt an upgrade for the original page. |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 2); |
| // The original page serves bad HTTPS, but any HTTPS URL is counted as |
| // secure in this histogram: |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSecure, 1); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kCaptivePortalLogin, 1); |
| } |
| } |
| |
| // Same as |
| // CaptivePortal_LoginPageWithValidSSL_ShouldNotUpgradeUnlessInterstitialEnabled |
| // but the captive portal's login page serves bad SSL. |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesBrowserTest, |
| CaptivePortal_LoginPageWithoutValidSSL_ShouldNotUpgradeUnlessInterstitialEnabled) { |
| if (https_upgrades_test_type() == |
| HttpsUpgradesTestType::kHttpsFirstModeIncognito) { |
| return; |
| } |
| auto interceptor = |
| MakeCaptivePortalInterceptor(/*login_page_has_valid_https=*/true); |
| |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| EnableCaptivePortalDetection(browser()); |
| |
| auto* tab_strip = GetBrowser()->tab_strip_model(); |
| auto* contents = tab_strip->GetActiveWebContents(); |
| size_t tab_count = tab_strip->count(); |
| |
| // Go to an HTTPS URL. The navigation will fail and trigger a captive portal |
| // detection. |
| ui_test_utils::TabAddedWaiter waiter(browser()); |
| NavigateAndWaitForFallback(contents, |
| GURL("https://ssl-error-for-captive-portal.com/")); |
| waiter.Wait(); |
| |
| content::WebContents* login_page = tab_strip->GetWebContentsAt(tab_count); |
| content::WaitForLoadStop(login_page); |
| |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| login_page)); |
| |
| if (IsHttpsFirstModePrefEnabled()) { |
| // If the interstitial is enabled, captive portal login page should also be |
| // upgraded to HTTPS. |
| EXPECT_EQ(GURL(kCaptivePortalPingUrlHttps), |
| login_page->GetLastCommittedURL()); |
| |
| // Should only attempt an upgrade for the original page. |
| histograms()->ExpectTotalCount(kNavigationRequestSecurityLevelHistogram, 3); |
| // The original page serves bad HTTPS, but any HTTPS URL is counted as |
| // secure in this histogram. Captive portal login page is valid HTTPS, so |
| // it's also counted here. |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSecure, 2); |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kUpgraded, |
| 1); |
| } else { |
| // Captive portal login page should not be upgraded to HTTPS. |
| EXPECT_EQ(GURL(kCaptivePortalPingUrl), login_page->GetLastCommittedURL()); |
| |
| // The original page serves bad HTTPS, but any HTTPS URL is counted as |
| // secure in this histogram: |
| histograms()->ExpectBucketCount(kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kSecure, 1); |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kCaptivePortalLogin, 1); |
| } |
| } |
| |
| // A simple test fixture that constructs a HistogramTester (so that it gets |
| // initialized before browser startup). Used for testing pref tracking logic. |
| class HttpsUpgradesPrefsBrowserTest : public InProcessBrowserTest { |
| public: |
| HttpsUpgradesPrefsBrowserTest() { |
| feature_list_.InitWithFeatures( |
| /*enabled_features=*/{}, |
| /*disabled_features=*/{features::kHttpsFirstModeIncognito, |
| features::kHttpsFirstBalancedMode}); |
| } |
| ~HttpsUpgradesPrefsBrowserTest() override = default; |
| |
| protected: |
| void SetUISetting(HttpsFirstModeSetting setting) { |
| extensions::settings_private::GeneratedPrefs prefs(browser()->profile()); |
| prefs.SetPref( |
| kGeneratedHttpsFirstModePref, |
| std::make_unique<base::Value>(static_cast<int>(setting)).get()); |
| } |
| |
| bool GetPref() const { |
| auto* prefs = browser()->profile()->GetPrefs(); |
| return prefs->GetBoolean(prefs::kHttpsOnlyModeEnabled); |
| } |
| |
| base::HistogramTester* histograms() { return &histograms_; } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| base::HistogramTester histograms_; |
| }; |
| |
| // Tests that the HTTPS-First Mode state is recorded at startup and when |
| // changed. This test requires restarting the browser to test the "at startup" |
| // metric in order for the preference state to be set up before the |
| // HttpsFirstModeService is created. |
| IN_PROC_BROWSER_TEST_F(HttpsUpgradesPrefsBrowserTest, PRE_PrefStatesRecorded) { |
| // The default pref state is `false`, which should get recorded when the |
| // initial browser instance is started here. |
| histograms()->ExpectUniqueSample( |
| "Security.HttpsFirstMode.SettingEnabledAtStartup2", |
| HttpsFirstModeSetting::kDisabled, 1); |
| |
| EXPECT_TRUE(variations::IsInSyntheticTrialGroup("HttpsFirstModeClientSetting", |
| "Disabled")); |
| |
| // Emulate changing the UI setting to Enabled. This should get recorded |
| // in the histogram. |
| SetUISetting(HttpsFirstModeSetting::kEnabledFull); |
| histograms()->ExpectUniqueSample("Security.HttpsFirstMode.SettingChanged", |
| true, 1); |
| EXPECT_TRUE(variations::IsInSyntheticTrialGroup("HttpsFirstModeClientSetting", |
| "Enabled")); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(HttpsUpgradesPrefsBrowserTest, PrefStatesRecorded) { |
| // Restarting the browser from the PRE_ test should record the startup setting |
| // histogram. Checking the unique count also ensures that other profile |
| // types (e.g. the ChromeOS sign-in profile) don't cause double-counting. |
| EXPECT_TRUE(GetPref()); |
| histograms()->ExpectUniqueSample( |
| "Security.HttpsFirstMode.SettingEnabledAtStartup2", |
| HttpsFirstModeSetting::kEnabledFull, 1); |
| EXPECT_TRUE(variations::IsInSyntheticTrialGroup("HttpsFirstModeClientSetting", |
| "Enabled")); |
| |
| // Open an Incognito window. Startup metrics should not get recorded. |
| CreateIncognitoBrowser(); |
| histograms()->ExpectTotalCount( |
| "Security.HttpsFirstMode.SettingEnabledAtStartup2", 1); |
| } |
| |
| enum class BalancedModeParam { |
| kNotAutoEnabled, |
| kAutoEnabled, |
| }; |
| |
| // A simple test fixture that constructs a HistogramTester (so that it gets |
| // initialized before browser startup). Used for testing pref tracking logic. |
| // Variant of HttpsUpgradesPrefsBrowserTest but with the |
| // HttpsFirstBalancedMode feature enabled. |
| class HttpsUpgradesBalancedModePrefsBrowserTest |
| : public testing::WithParamInterface<BalancedModeParam>, |
| public InProcessBrowserTest { |
| protected: |
| void SetUp() override { |
| // Feature flag must be enabled before SetUp() continues. |
| switch (GetParam()) { |
| case BalancedModeParam::kNotAutoEnabled: |
| feature_list()->InitWithFeatures( |
| /*enabled_features=*/{features::kHttpsFirstBalancedMode}, |
| /*disabled_features=*/{ |
| features::kHttpsFirstBalancedModeAutoEnable}); |
| break; |
| |
| case BalancedModeParam::kAutoEnabled: |
| feature_list()->InitWithFeatures( |
| /*enabled_features=*/{features::kHttpsFirstBalancedMode, |
| features::kHttpsFirstBalancedModeAutoEnable}, |
| /*disabled_features=*/{}); |
| break; |
| } |
| InProcessBrowserTest::SetUp(); |
| } |
| |
| void SetUISetting(HttpsFirstModeSetting setting) { |
| extensions::settings_private::GeneratedPrefs prefs(browser()->profile()); |
| prefs.SetPref( |
| kGeneratedHttpsFirstModePref, |
| std::make_unique<base::Value>(static_cast<int>(setting)).get()); |
| } |
| |
| bool GetPref() const { |
| auto* prefs = browser()->profile()->GetPrefs(); |
| return prefs->GetBoolean(prefs::kHttpsFirstBalancedMode); |
| } |
| |
| base::test::ScopedFeatureList* feature_list() { return &feature_list_; } |
| base::HistogramTester* histograms() { return &histograms_; } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| base::HistogramTester histograms_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* no prefix */, |
| HttpsUpgradesBalancedModePrefsBrowserTest, |
| ::testing::Values(BalancedModeParam::kNotAutoEnabled, |
| BalancedModeParam::kAutoEnabled), |
| // Map param to a human-readable string for better test output. |
| [](testing::TestParamInfo<BalancedModeParam> input_type) -> std::string { |
| switch (input_type.param) { |
| case BalancedModeParam::kNotAutoEnabled: |
| return "BalancedModeNotAutoEnabled"; |
| case BalancedModeParam::kAutoEnabled: |
| return "BalancedModeAutoEnabled"; |
| } |
| }); |
| |
| // Tests that the HTTPS-First Mode setting is recorded at startup and when |
| // changed, when the HFM-Balanced-Mode feature flag is enabled. This test |
| // requires restarting the browser to test the "at startup" metric in order |
| // for the preference state to be set up before the HttpsFirstModeService is |
| // created. |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBalancedModePrefsBrowserTest, |
| PRE_PrefStatesRecorded) { |
| if (GetParam() == BalancedModeParam::kNotAutoEnabled) { |
| // The default Balanced Mode pref state is false, which should get recorded |
| // when the initial browser instance is started here. |
| histograms()->ExpectUniqueSample( |
| "Security.HttpsFirstMode.SettingEnabledAtStartup2", |
| HttpsFirstModeSetting::kDisabled, 1); |
| |
| EXPECT_TRUE(variations::IsInSyntheticTrialGroup( |
| "HttpsFirstModeClientSetting", "Disabled")); |
| } else if (GetParam() == BalancedModeParam::kAutoEnabled) { |
| // The default Balanced Mode pref state is false, but Balanced Mode is auto |
| // enabled. |
| histograms()->ExpectUniqueSample( |
| "Security.HttpsFirstMode.SettingEnabledAtStartup2", |
| HttpsFirstModeSetting::kEnabledBalanced, 1); |
| |
| EXPECT_TRUE(variations::IsInSyntheticTrialGroup( |
| "HttpsFirstModeClientSetting", "Balanced")); |
| } |
| |
| // Emulate changing the UI setting to Balanced Mode. This should get recorded |
| // in the histogram. |
| SetUISetting(HttpsFirstModeSetting::kEnabledBalanced); |
| EXPECT_TRUE(GetPref()); |
| histograms()->ExpectUniqueSample("Security.HttpsFirstMode.SettingChanged2", |
| HttpsFirstModeSetting::kEnabledBalanced, 1); |
| EXPECT_TRUE(variations::IsInSyntheticTrialGroup("HttpsFirstModeClientSetting", |
| "Balanced")); |
| } |
| |
| IN_PROC_BROWSER_TEST_P(HttpsUpgradesBalancedModePrefsBrowserTest, |
| PrefStatesRecorded) { |
| // Restarting the browser from the PRE_ test should record the startup setting |
| // histogram. Checking the unique count also ensures that other profile |
| // types (e.g. the ChromeOS sign-in profile) don't cause double-counting. |
| EXPECT_TRUE(GetPref()); |
| histograms()->ExpectUniqueSample( |
| "Security.HttpsFirstMode.SettingEnabledAtStartup2", |
| HttpsFirstModeSetting::kEnabledBalanced, 1); |
| EXPECT_TRUE(variations::IsInSyntheticTrialGroup("HttpsFirstModeClientSetting", |
| "Balanced")); |
| |
| // Open an Incognito window. Startup metrics should not get recorded. |
| CreateIncognitoBrowser(); |
| histograms()->ExpectTotalCount( |
| "Security.HttpsFirstMode.SettingEnabledAtStartup2", 1); |
| } |
| |
| using TypicallySecureUserBrowserTest = InProcessBrowserTest; |
| |
| IN_PROC_BROWSER_TEST_F(TypicallySecureUserBrowserTest, |
| PRE_RestoreCountsOnStartup_OneNavigation) { |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(browser()->profile()); |
| hfm_service->IncrementRecentNavigationCount(); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(TypicallySecureUserBrowserTest, |
| RestoreCountsOnStartup_OneNavigation) { |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(browser()->profile()); |
| // A single navigation will not be persisted to the pref and won't be |
| // restored on startup because navigations are persisted in batches of 10. |
| EXPECT_EQ(0u, hfm_service->GetRecentNavigationCount()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(TypicallySecureUserBrowserTest, |
| PRE_RestoreCountsOnStartup_TenNavigations) { |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(browser()->profile()); |
| // Increment repeatedly to force the counts to be persisted to the pref. |
| for (size_t i = 0; i < 10; i++) { |
| hfm_service->IncrementRecentNavigationCount(); |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_F(TypicallySecureUserBrowserTest, |
| RestoreCountsOnStartup_TenNavigations) { |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(browser()->profile()); |
| EXPECT_EQ(10u, hfm_service->GetRecentNavigationCount()); |
| } |
| |
| // Tests for HFM heuristics without Balanced Mode enabled. These are unusual |
| // configurations and shouldn't appear in production. |
| // TODO(crbug.com/349860796): Remove after balanced mode is fully launched. |
| using HttpsUpgradesHeuristicsWithoutBalancedModeBrowserTest = |
| HttpsUpgradesBrowserTest; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| /* no prefix */, |
| HttpsUpgradesHeuristicsWithoutBalancedModeBrowserTest, |
| ::testing::Values( |
| HttpsUpgradesTestType:: |
| kHttpsFirstModeWithSiteEngagementWithoutBalancedMode)); |
| |
| // Test that Site Engagement Heuristic without Balanced Mode is a no-op. |
| IN_PROC_BROWSER_TEST_P( |
| HttpsUpgradesHeuristicsWithoutBalancedModeBrowserTest, |
| UrlWithHttpScheme_BrokenSSL_SiteEngagementHeuristicWithoutBalancedMode_ShouldIgnore) { |
| ASSERT_FALSE(IsIncognito() || IsHttpsFirstModePrefEnabled()); |
| |
| // Disable the testing port configuration, as this test doesn't use the |
| // EmbeddedTestServer. |
| HttpsUpgradesInterceptor::SetHttpsPortForTesting(0); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting(0); |
| auto url_loader_interceptor = MakeInterceptorForSiteEngagementHeuristic(); |
| |
| content::WebContents* contents = |
| GetBrowser()->tab_strip_model()->GetActiveWebContents(); |
| Profile* profile = GetBrowser()->profile(); |
| content::SSLHostStateDelegate* state = profile->GetSSLHostStateDelegate(); |
| |
| // Set test clock. |
| auto clock = std::make_unique<base::SimpleTestClock>(); |
| auto* clock_ptr = clock.get(); |
| StatefulSSLHostStateDelegate* chrome_state = |
| static_cast<StatefulSSLHostStateDelegate*>(state); |
| chrome_state->SetClockForTesting(std::move(clock)); |
| |
| // Start the clock at standard system time. |
| clock_ptr->SetNow(base::Time::NowFromSystemTime()); |
| |
| // Set site engagement scores so that this site would have HFM enabled if |
| // HFM+SE kicked in. |
| GURL http_url("http://example.com"); |
| GURL https_url("https://example.com"); |
| |
| SetSiteEngagementScore(http_url, kLowSiteEngagementScore); |
| SetSiteEngagementScore(https_url, kHighSiteEnagementScore); |
| HttpsFirstModeService* hfm_service = |
| HttpsFirstModeServiceFactory::GetForProfile(profile); |
| MaybeEnableHttpsFirstModeForEngagedSitesAndWait(hfm_service); |
| |
| // This URL should be upgraded by HTTPS-Upgrades and should fall back to HTTP, |
| // but not have HFM auto-enabled on it because balanced mode isn't enabled. |
| NavigateAndWaitForFallback(contents, http_url); |
| EXPECT_EQ(http_url, contents->GetLastCommittedURL()); |
| |
| EXPECT_EQ(HFMInterstitialType::kNone, |
| chrome_browser_interstitials::GetHFMInterstitialType(contents)); |
| |
| // Verify that navigation event metrics were correctly recorded. |
| histograms()->ExpectTotalCount(kEventHistogram, 3); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeAttempted, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeFailed, 1); |
| histograms()->ExpectBucketCount(kEventHistogram, Event::kUpgradeCertError, 1); |
| |
| // Engagement heuristic shouldn't handle any navigation events. |
| histograms()->ExpectTotalCount(kEventHistogramWithEngagementHeuristic, 0); |
| |
| // Security level histogram should not record kHttpsEnforcedOnHostname. |
| histograms()->ExpectBucketCount( |
| kNavigationRequestSecurityLevelHistogram, |
| NavigationRequestSecurityLevel::kHttpsEnforcedOnHostname, 0); |
| } |
| |
| // Minimal test fixture for testing the interaction between the |
| // SecureOriginAllowlist (set via the command-line switch |
| // `switches::kUnsafelyTreatInsecureOriginAsSecure`) and HTTPS-First Balanced |
| // Mode. |
| class HttpsUpgradesSecureOriginAllowlistBrowserTest |
| : public InProcessBrowserTest { |
| public: |
| HttpsUpgradesSecureOriginAllowlistBrowserTest() { |
| feature_list_.InitWithFeatures( |
| {features::kHttpsFirstBalancedModeAutoEnable}, |
| // TODO(crbug.com/351990829): Update these tests to work with the new |
| // native dialog UI, and then re-enable this feature. |
| {features::kHttpsFirstDialogUi}); |
| } |
| ~HttpsUpgradesSecureOriginAllowlistBrowserTest() override = default; |
| |
| void SetUpOnMainThread() override { |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| embedded_test_server()->AddDefaultHandlers(GetChromeTestDataDir()); |
| ASSERT_TRUE(embedded_test_server()->Start()); |
| HttpsUpgradesInterceptor::SetHttpPortForTesting( |
| embedded_test_server()->port()); |
| } |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| // This sets a wildcard entry in the allowlist, because we can't specify |
| // an exact origin as we don't yet have the embedded test server's port |
| // (it isn't started until later). |
| command_line->AppendSwitchASCII( |
| network::switches::kUnsafelyTreatInsecureOriginAsSecure, |
| "*.example.com"); |
| } |
| |
| private: |
| base::test::ScopedFeatureList feature_list_; |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(HttpsUpgradesSecureOriginAllowlistBrowserTest, |
| HostInAllowlistExemptedFromHttpsFirstMode) { |
| GURL url_in_allowlist = |
| embedded_test_server()->GetURL("test.example.com", "/simple.html"); |
| content::WebContents* contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_TRUE(content::NavigateToURL(contents, url_in_allowlist)); |
| EXPECT_FALSE( |
| chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(HttpsUpgradesSecureOriginAllowlistBrowserTest, |
| HostNotInAllowlistShowWarning) { |
| GURL url_not_in_allowlist = |
| embedded_test_server()->GetURL("not-example.com", "/simple.html"); |
| content::WebContents* contents = |
| browser()->tab_strip_model()->GetActiveWebContents(); |
| EXPECT_FALSE(content::NavigateToURL(contents, url_not_in_allowlist)); |
| EXPECT_TRUE(chrome_browser_interstitials::IsShowingHttpsFirstModeInterstitial( |
| contents)); |
| } |