Skip to content

Commit

Permalink
Revamp HTTPS-First Mode fallback mechanism
Browse files Browse the repository at this point in the history
This CL changes how HTTPS-First Mode and HTTPS Upgrades trigger a
fallback navigation to HTTP. Before this change, the NavigationThrottle
would cancel the failed navigation and post an async task to start a
new navigation to the fallback URL. This generally worked, but had
(at least) three issues: (1) this could cause "double counting" of
navigations, (2) it's hard to create a new navigation out of an old one
without missing important parameters (and maintain that over time), and
(3) it's possible for race conditions to occur on the frame being
navigated.

To avoid these issues, this CL changes the fallback mechanism to
instead intercept the failing navigation via
NavigationLoaderInterceptor::MaybeCreateLoaderForResponse() to serve an
artificial redirect back to the fallback URL directly, as part of the
same overall navigation. This means no new navigation is created
(avoiding double counting), no state is lost on the navigation (it's
just like the site itself served a downgrade redirect), and there is
no period where one navigation is canceled and another is not yet
created in which races could occur.

As the HTTPS-First Mode/HTTPS Upgrades logic is currently implemented
in chrome/, this exposes the MaybeCreateLoaderForResponse() API on
URLLoaderRequestInterceptor and adds an implementation to
HttpsUpgradesInterceptor. As a result, a lot of the logic from
HttpsUpgradesNavigationThrottle moves into HttpsUpgradesInterceptor.

The SetNavigationTimeout() implementation also needs to be modified to
trigger NavigationURLLoaderImpl::OnComplete() rather than
NotifyRequestFailed(), as the latter skips directly to the failure
state and does not trigger the interceptor's
MaybeCreateLoaderForResponse().

There are still known issues with how the new fallback implementation
interacts with (1) redirect loops and (2) tracking fallback state
across navigations. The first can result in briefly seeing a network
error for TOO_MANY_REDIRECTS before the HTTPS-First Mode interstitial
is triggered. The second can result in an incorrect security state
being computed for a tab where a fallback to HTTP had previously
occurred. Fixes for these issues will be handled in followup CLs.

Bug: 1394910
Change-Id: Ib53c8f873a30e6b2ebb933ae527a5c7f5fcd9bcd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4199590
Commit-Queue: Chris Thompson <cthomp@chromium.org>
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Mustafa Emre Acer <meacer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1105469}
  • Loading branch information
christhompson authored and Chromium LUCI CQ committed Feb 15, 2023
1 parent a974be1 commit 824cb37
Show file tree
Hide file tree
Showing 20 changed files with 501 additions and 174 deletions.
7 changes: 5 additions & 2 deletions chrome/browser/chrome_content_browser_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5971,8 +5971,11 @@ ChromeContentBrowserClient::WillCreateURLLoaderRequestInterceptors(
std::make_unique<SearchPrefetchURLLoaderInterceptor>(frame_tree_node_id));

if (base::FeatureList::IsEnabled(features::kHttpsFirstModeV2)) {
interceptors.push_back(
std::make_unique<HttpsUpgradesInterceptor>(frame_tree_node_id));
auto https_upgrades_interceptor =
HttpsUpgradesInterceptor::MaybeCreateInterceptor(frame_tree_node_id);
if (https_upgrades_interceptor) {
interceptors.push_back(std::move(https_upgrades_interceptor));
}
} else {
interceptors.push_back(
std::make_unique<HttpsOnlyModeUpgradeInterceptor>(frame_tree_node_id));
Expand Down
41 changes: 20 additions & 21 deletions chrome/browser/ssl/https_upgrades_browsertest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/https_only_mode_navigation_throttle.h"
#include "chrome/browser/ssl/https_only_mode_upgrade_interceptor.h"
#include "chrome/browser/ssl/https_upgrades_interceptor.h"
#include "chrome/browser/ssl/https_upgrades_navigation_throttle.h"
#include "chrome/browser/ssl/security_state_tab_helper.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_tabstrip.h"
Expand Down Expand Up @@ -95,10 +97,8 @@ class HttpsUpgradesBrowserTest
ASSERT_TRUE(http_server_.Start());
ASSERT_TRUE(https_server_.Start());

HttpsOnlyModeUpgradeInterceptor::SetHttpsPortForTesting(
https_server()->port());
HttpsOnlyModeUpgradeInterceptor::SetHttpPortForTesting(
http_server()->port());
HttpsUpgradesInterceptor::SetHttpsPortForTesting(https_server()->port());
HttpsUpgradesInterceptor::SetHttpPortForTesting(http_server()->port());

// For the kHttpsUpgradesOnly test variant, don't enable the HTTPS-First
// Mode pref.
Expand Down Expand Up @@ -150,10 +150,9 @@ class HttpsUpgradesBrowserTest
}

void NavigateAndWaitForFallback(content::WebContents* tab, const GURL& url) {
// Fallback to HTTP (and showing the HTTPS-First Mode interstitial, if
// enabled) is a new navigation, so navigate to the initial URL and wait
// for *two* navigations to complete.
content::NavigateToURLBlockUntilNavigationsComplete(tab, url, 2);
// TODO(crbug.com/1394910): With fallback as part of the same navigation,
// this helper is no longer particularly useful. Consider updating callers.
content::NavigateToURLBlockUntilNavigationsComplete(tab, url, 1);
}

// Whether the tests should run steps that assume the HTTP interstitial will
Expand Down Expand Up @@ -411,7 +410,7 @@ IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest,
// HTTPS-Only Mode interstitial.
IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, SlowHttps_ShouldInterstitial) {
// Set timeout to zero so that HTTPS upgrades immediately timeout.
HttpsOnlyModeNavigationThrottle::set_timeout_for_testing(0);
HttpsUpgradesNavigationThrottle::set_timeout_for_testing(0);

// Set up a custom HTTPS server that times out without sending a response.
net::EmbeddedTestServer timeout_server{net::EmbeddedTestServer::TYPE_HTTPS};
Expand All @@ -422,8 +421,7 @@ IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, SlowHttps_ShouldInterstitial) {
return std::make_unique<net::test_server::HungResponse>();
}));
ASSERT_TRUE(timeout_server.Start());
HttpsOnlyModeUpgradeInterceptor::SetHttpsPortForTesting(
timeout_server.port());
HttpsUpgradesInterceptor::SetHttpsPortForTesting(timeout_server.port());

const GURL http_url = http_server()->GetURL("foo.test", "/simple.html");
auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
Expand Down Expand Up @@ -453,7 +451,7 @@ IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest, HttpPageHttpPost_NotUpgraded) {
// Navigate to the page hosting the form on "foo.test".
auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
content::NavigateToURLBlockUntilNavigationsComplete(
contents, http_server()->GetURL("bad-https.test", replacement_path), 2);
contents, http_server()->GetURL("bad-https.test", replacement_path), 1);

if (IsHttpInterstitialEnabled()) {
// The HTTPS-Only Mode interstitial should trigger.
Expand Down Expand Up @@ -513,8 +511,10 @@ IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest,
// 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 the
// redirect loop hitting the redirect limit).
// TODO(crbug.com/1394910): Re-enable once redirect loops are handled by the
// interceptor rather than relying on the net error.
IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest,
RedirectLoop_ShouldInterstitial) {
DISABLED_RedirectLoop_ShouldInterstitial) {
// Set up a new test server instance so it can have a custom handler.
net::EmbeddedTestServer downgrading_server{
net::EmbeddedTestServer::TYPE_HTTPS};
Expand All @@ -538,8 +538,7 @@ IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest,
return response;
}));
ASSERT_TRUE(downgrading_server.Start());
HttpsOnlyModeUpgradeInterceptor::SetHttpsPortForTesting(
downgrading_server.port());
HttpsUpgradesInterceptor::SetHttpsPortForTesting(downgrading_server.port());

GURL url = downgrading_server.GetURL("foo.test", "/");
auto* contents = browser()->tab_strip_model()->GetActiveWebContents();
Expand Down Expand Up @@ -584,7 +583,9 @@ IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest,

// The HTTP site results in a net error, which should have security level NONE
// (as no connection was made).
EXPECT_EQ(security_state::NONE, helper->GetSecurityLevel());
// TODO(crbug.com/1394910): 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
Expand Down Expand Up @@ -640,12 +641,11 @@ IN_PROC_BROWSER_TEST_P(HttpsUpgradesBrowserTest,
"Location",
"https://bad-https.test:" +
base::NumberToString(
HttpsOnlyModeUpgradeInterceptor::GetHttpsPortForTesting()) +
HttpsUpgradesInterceptor::GetHttpsPortForTesting()) +
"/simple.html");
return response;
}));
HttpsOnlyModeUpgradeInterceptor::SetHttpPortForTesting(
upgrading_server.port());
HttpsUpgradesInterceptor::SetHttpPortForTesting(upgrading_server.port());
ASSERT_TRUE(upgrading_server.Start());

GURL http_url = upgrading_server.GetURL("bad-https.test", "/simple.html");
Expand Down Expand Up @@ -1071,8 +1071,7 @@ IN_PROC_BROWSER_TEST_F(HttpsUpgradesBrowserTest,
return response;
}));
ASSERT_TRUE(downgrading_server.Start());
HttpsOnlyModeUpgradeInterceptor::SetHttpsPortForTesting(
downgrading_server.port());
HttpsUpgradesInterceptor::SetHttpsPortForTesting(downgrading_server.port());

GURL downgrading_https_url = downgrading_server.GetURL("site2.test", "/");
GURL::Replacements swap_http_scheme;
Expand Down

0 comments on commit 824cb37

Please sign in to comment.