diff --git a/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle.cc b/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle.cc index cca8127986c23f..bf0026b6f76bb6 100644 --- a/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle.cc +++ b/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle.cc @@ -5,38 +5,67 @@ #include "chrome/browser/ui/passwords/well_known_change_password_navigation_throttle.h" #include "base/logging.h" -#include "chrome/browser/profiles/profile.h" +#include "chrome/common/url_constants.h" #include "components/password_manager/core/common/password_manager_features.h" #include "content/public/browser/browser_context.h" #include "content/public/browser/navigation_handle.h" +#include "content/public/browser/page_navigator.h" #include "content/public/browser/storage_partition.h" #include "content/public/browser/web_contents.h" +#include "content/public/browser/web_contents_user_data.h" #include "net/base/load_flags.h" +#include "net/http/http_status_code.h" #include "services/network/public/cpp/shared_url_loader_factory.h" #include "services/network/public/cpp/simple_url_loader.h" #include "url/gurl.h" namespace { +using chrome::kWellKnownChangePasswordPath; +using chrome::kWellKnownNotExistingResourcePath; using content::NavigationHandle; using content::NavigationThrottle; +using content::WebContents; -// This path should return 404. This enables us to check whether -// we can trust the server's response code. -// https://wicg.github.io/change-password-url/response-code-reliability.html#iana -constexpr char kNotExistingResourcePath[] = - ".well-known/" - "resource-that-should-not-exist-whose-status-code-should-not-be-200"; +// Used to scope the posted navigation task to the lifetime of |web_contents|. +class WebContentsLifetimeHelper + : public content::WebContentsUserData { + public: + explicit WebContentsLifetimeHelper(WebContents* web_contents) + : web_contents_(web_contents) {} + + base::WeakPtr GetWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + + void NavigateTo(const content::OpenURLParams& url_params) { + web_contents_->OpenURL(url_params); + } + + private: + friend class content::WebContentsUserData; + + WebContents* const web_contents_; + base::WeakPtrFactory weak_factory_{this}; + + WEB_CONTENTS_USER_DATA_KEY_DECL(); +}; + +WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsLifetimeHelper) bool IsWellKnownChangePasswordUrl(const GURL& url) { - return url.is_valid() && url.has_path() && - (url.PathForRequest() == "/.well-known/change-password" || - url.PathForRequest() == "/.well-known/change-password/"); + if (!url.is_valid() || !url.has_path()) + return false; + base::StringPiece path = url.PathForRequestPiece(); + // remove trailing slash if there + if (path.ends_with("/")) + path = path.substr(0, path.size() - 1); + return path == kWellKnownChangePasswordPath; } GURL CreateNonExistingResourceURL(const GURL& url) { GURL::Replacements replacement; - replacement.SetPathStr(kNotExistingResourcePath); + replacement.SetPathStr(kWellKnownNotExistingResourcePath); return url.GetOrigin().ReplaceComponents(replacement); } @@ -75,6 +104,7 @@ WellKnownChangePasswordNavigationThrottle::WillStartRequest() { NavigationThrottle::ThrottleCheckResult WellKnownChangePasswordNavigationThrottle::WillFailRequest() { + url_loader_.reset(); return NavigationThrottle::PROCEED; } @@ -82,8 +112,8 @@ NavigationThrottle::ThrottleCheckResult WellKnownChangePasswordNavigationThrottle::WillProcessResponse() { change_password_response_code_ = navigation_handle()->GetResponseHeaders()->response_code(); - // TODO(crbug.com/927473) handle processing - return NavigationThrottle::PROCEED; + return BothRequestsFinished() ? ContinueProcessing() + : NavigationThrottle::DEFER; } const char* WellKnownChangePasswordNavigationThrottle::GetNameForLogging() { @@ -124,11 +154,9 @@ void WellKnownChangePasswordNavigationThrottle::FetchNonExistingResource( setting: "This feature cannot be disabled." policy_exception_justification: "Essential for navigation." })"); - auto url_loader_factory = - content::BrowserContext::GetDefaultStoragePartition( - Profile::FromBrowserContext( - handle->GetWebContents()->GetBrowserContext())) - ->GetURLLoaderFactoryForBrowserProcess(); + auto url_loader_factory = content::BrowserContext::GetDefaultStoragePartition( + handle->GetWebContents()->GetBrowserContext()) + ->GetURLLoaderFactoryForBrowserProcess(); url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request), traffic_annotation); // Binding the callback to |this| is safe, because the navigationthrottle @@ -144,6 +172,58 @@ void WellKnownChangePasswordNavigationThrottle::FetchNonExistingResource( void WellKnownChangePasswordNavigationThrottle:: FetchNonExistingResourceCallback( scoped_refptr headers) { + if (!headers) { + non_existing_resource_response_code_ = -1; + return; + } non_existing_resource_response_code_ = headers->response_code(); - // TODO(crbug.com/927473) handle processing + if (BothRequestsFinished()) { + ThrottleAction action = ContinueProcessing(); + if (action == NavigationThrottle::PROCEED) { + Resume(); + } + } +} + +NavigationThrottle::ThrottleAction +WellKnownChangePasswordNavigationThrottle::ContinueProcessing() { + DCHECK(BothRequestsFinished()); + if (SupportsChangePasswordUrl()) { + return NavigationThrottle::PROCEED; + } else { + // TODO(crbug.com/1086141): Integrate Service that provides URL overrides + Redirect(navigation_handle()->GetURL().GetOrigin()); + return NavigationThrottle::CANCEL; + } +} + +void WellKnownChangePasswordNavigationThrottle::Redirect(const GURL& url) { + content::OpenURLParams params = + content::OpenURLParams::FromNavigationHandle(navigation_handle()); + params.url = url; + params.transition = ui::PAGE_TRANSITION_CLIENT_REDIRECT; + + WebContents* web_contents = navigation_handle()->GetWebContents(); + if (!web_contents) + return; + + WebContentsLifetimeHelper::CreateForWebContents(web_contents); + WebContentsLifetimeHelper* helper = + WebContentsLifetimeHelper::FromWebContents(web_contents); + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&WebContentsLifetimeHelper::NavigateTo, + helper->GetWeakPtr(), std::move(params))); +} + +bool WellKnownChangePasswordNavigationThrottle::BothRequestsFinished() const { + return non_existing_resource_response_code_ != 0 && + change_password_response_code_ != 0; +} + +bool WellKnownChangePasswordNavigationThrottle::SupportsChangePasswordUrl() + const { + DCHECK(BothRequestsFinished()); + return 200 <= change_password_response_code_ && + change_password_response_code_ < 300 && + non_existing_resource_response_code_ == net::HTTP_NOT_FOUND; } diff --git a/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle.h b/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle.h index 5c7ae4fe3b4787..74a71e63340ce8 100644 --- a/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle.h +++ b/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle.h @@ -9,6 +9,7 @@ #include "content/public/browser/navigation_throttle.h" +class GURL; namespace content { class NavigationHandle; } // namespace content @@ -52,6 +53,15 @@ class WellKnownChangePasswordNavigationThrottle // Callback for the request to the "not exist" path. void FetchNonExistingResourceCallback( scoped_refptr headers); + // Function is called when both requests are finished. Decides to continue or + // redirect to homepage. + ThrottleAction ContinueProcessing(); + // Redirects to a given URL in the same tab. + void Redirect(const GURL& url); + // Checks if both requests are finished. + bool BothRequestsFinished() const; + // Checks the status codes and returns if change password is supported. + bool SupportsChangePasswordUrl() const; int non_existing_resource_response_code_ = 0; int change_password_response_code_ = 0; diff --git a/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle_browsertest.cc b/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle_browsertest.cc new file mode 100644 index 00000000000000..d3b91f3024359b --- /dev/null +++ b/chrome/browser/ui/passwords/well_known_change_password_navigation_throttle_browsertest.cc @@ -0,0 +1,265 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/ui/passwords/well_known_change_password_navigation_throttle.h" + +#include +#include +#include "base/run_loop.h" +#include "base/test/scoped_feature_list.h" +#include "base/time/time.h" +#include "chrome/browser/ssl/cert_verifier_browser_test.h" +#include "chrome/browser/ui/browser_navigator.h" +#include "chrome/browser/ui/browser_navigator_params.h" +#include "chrome/common/url_constants.h" +#include "components/password_manager/core/common/password_manager_features.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/mock_navigation_handle.h" +#include "content/public/test/test_navigation_observer.h" +#include "content/public/test/test_utils.h" +#include "net/cert/x509_certificate.h" +#include "net/http/http_status_code.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/http_response.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "url/gurl.h" + +namespace { +using chrome::kWellKnownChangePasswordPath; +using chrome::kWellKnownNotExistingResourcePath; +using content::NavigationThrottle; +using content::TestNavigationObserver; +using net::test_server::BasicHttpResponse; +using net::test_server::DelayedHttpResponse; +using net::test_server::EmbeddedTestServer; +using net::test_server::EmbeddedTestServerHandle; +using net::test_server::HttpRequest; +using net::test_server::HttpResponse; + +// ServerResponse describes how a server should respond to a given path. +struct ServerResponse { + net::HttpStatusCode status_code; + std::vector> headers; + int resolve_time_in_milliseconds; +}; + +// The NavigationThrottle is making 2 requests in parallel. With this config we +// simulate the different orders for the arrival of the responses. The value +// represents the delay in milliseconds. +struct ResponseDelayParams { + int change_password_delay; + int not_exist_delay; +}; + +} // namespace + +class WellKnownChangePasswordNavigationThrottleBrowserTest + : public CertVerifierBrowserTest, + public testing::WithParamInterface { + public: + WellKnownChangePasswordNavigationThrottleBrowserTest() { + feature_list_.InitAndEnableFeature( + password_manager::features::kWellKnownChangePassword); + test_server_->RegisterRequestHandler(base::BindRepeating( + &WellKnownChangePasswordNavigationThrottleBrowserTest::HandleRequest, + base::Unretained(this))); + } + + void SetUpOnMainThread() override { + ASSERT_TRUE(test_server_->InitializeAndListen()); + test_server_->StartAcceptingConnections(); + } + + protected: + // Navigates to |kWellKnownChangePasswordPath| from the mock server. It waits + // until the navigation to |expected_path| happened. + void TestNavigationThrottle(const std::string& expected_path); + + // Whitelist all https certs for the |test_server_|. + void AddHttpsCertificate() { + auto cert = test_server_->GetCertificate(); + net::CertVerifyResult verify_result; + verify_result.cert_status = 0; + verify_result.verified_cert = cert; + mock_cert_verifier()->AddResultForCert(cert.get(), verify_result, net::OK); + } + + // Maps a path to a ServerResponse config object. + base::flat_map path_response_map_; + std::unique_ptr test_server_ = + std::make_unique(EmbeddedTestServer::TYPE_HTTPS); + + private: + // Returns a response for the given request. Uses |path_response_map_| to + // construct the response. Returns nullptr when the path is not defined in + // |path_response_map_|. + std::unique_ptr HandleRequest(const HttpRequest& request); + + base::test::ScopedFeatureList feature_list_; +}; + +std::unique_ptr +WellKnownChangePasswordNavigationThrottleBrowserTest::HandleRequest( + const HttpRequest& request) { + GURL absolute_url = test_server_->GetURL(request.relative_url); + std::string path = absolute_url.path(); + auto it = path_response_map_.find(absolute_url.path_piece()); + if (it == path_response_map_.end()) + return nullptr; + const ServerResponse& config = it->second; + auto http_response = std::make_unique( + base::TimeDelta::FromMilliseconds(config.resolve_time_in_milliseconds)); + http_response->set_code(config.status_code); + http_response->set_content_type("text/plain"); + for (auto header_pair : config.headers) { + http_response->AddCustomHeader(header_pair.first, header_pair.second); + } + return http_response; +} + +void WellKnownChangePasswordNavigationThrottleBrowserTest:: + TestNavigationThrottle(const std::string& expected_path) { + AddHttpsCertificate(); + GURL url = test_server_->GetURL(kWellKnownChangePasswordPath); + GURL expected_url = test_server_->GetURL(expected_path); + + NavigateParams params(browser(), url, + ui::PageTransition::PAGE_TRANSITION_LINK); + TestNavigationObserver observer(expected_url); + observer.WatchExistingWebContents(); + Navigate(¶ms); + observer.Wait(); + + EXPECT_EQ(observer.last_navigation_url(), expected_url); +} + +IN_PROC_BROWSER_TEST_P(WellKnownChangePasswordNavigationThrottleBrowserTest, + SupportForChangePassword) { + auto response_delays = GetParam(); + path_response_map_[kWellKnownChangePasswordPath] = { + net::HTTP_OK, {}, response_delays.change_password_delay}; + path_response_map_[kWellKnownNotExistingResourcePath] = { + net::HTTP_NOT_FOUND, {}, response_delays.not_exist_delay}; + + TestNavigationThrottle(kWellKnownChangePasswordPath); +} + +IN_PROC_BROWSER_TEST_P(WellKnownChangePasswordNavigationThrottleBrowserTest, + SupportForChangePassword_WithRedirect) { + auto response_delays = GetParam(); + path_response_map_[kWellKnownChangePasswordPath] = { + net::HTTP_PERMANENT_REDIRECT, + {std::make_pair("Location", "/change-password")}, + response_delays.change_password_delay}; + path_response_map_[kWellKnownNotExistingResourcePath] = { + net::HTTP_NOT_FOUND, {}, response_delays.not_exist_delay}; + path_response_map_["/change-password"] = {net::HTTP_OK, {}, 0}; + + TestNavigationThrottle(/*expected_path=*/"/change-password"); +} + +IN_PROC_BROWSER_TEST_P(WellKnownChangePasswordNavigationThrottleBrowserTest, + SupportForChangePassword_PartialContent) { + auto response_delays = GetParam(); + path_response_map_[kWellKnownChangePasswordPath] = { + net::HTTP_PARTIAL_CONTENT, {}, response_delays.change_password_delay}; + path_response_map_[kWellKnownNotExistingResourcePath] = { + net::HTTP_NOT_FOUND, {}, response_delays.not_exist_delay}; + + TestNavigationThrottle(/*expected_path=*/kWellKnownChangePasswordPath); +} + +IN_PROC_BROWSER_TEST_P(WellKnownChangePasswordNavigationThrottleBrowserTest, + SupportForChangePassword_WithRedirectToNotFoundPage) { + auto response_delays = GetParam(); + path_response_map_[kWellKnownChangePasswordPath] = { + net::HTTP_PERMANENT_REDIRECT, + {std::make_pair("Location", "/change-password")}, + response_delays.change_password_delay}; + path_response_map_[kWellKnownNotExistingResourcePath] = { + net::HTTP_PERMANENT_REDIRECT, + {std::make_pair("Location", "/not-found")}, + response_delays.not_exist_delay}; + path_response_map_["/change-password"] = {net::HTTP_OK, {}, 0}; + path_response_map_["/not-found"] = {net::HTTP_NOT_FOUND, {}, 0}; + + TestNavigationThrottle(/*expected_path=*/"/change-password"); +} + +IN_PROC_BROWSER_TEST_P(WellKnownChangePasswordNavigationThrottleBrowserTest, + NoSupportForChangePassword_NotFound) { + auto response_delays = GetParam(); + path_response_map_[kWellKnownChangePasswordPath] = { + net::HTTP_NOT_FOUND, {}, response_delays.change_password_delay}; + path_response_map_[kWellKnownNotExistingResourcePath] = { + net::HTTP_NOT_FOUND, {}, response_delays.not_exist_delay}; + path_response_map_["/"] = {net::HTTP_OK, {}, 0}; + + TestNavigationThrottle(/*expected_path=*/"/"); +} + +// Single page applications often return 200 for all paths +IN_PROC_BROWSER_TEST_P(WellKnownChangePasswordNavigationThrottleBrowserTest, + NoSupportForChangePassword_Ok) { + auto response_delays = GetParam(); + path_response_map_[kWellKnownChangePasswordPath] = { + net::HTTP_OK, {}, response_delays.change_password_delay}; + path_response_map_[kWellKnownNotExistingResourcePath] = { + net::HTTP_OK, {}, response_delays.not_exist_delay}; + path_response_map_["/"] = {net::HTTP_OK, {}, 0}; + + TestNavigationThrottle(/*expected_path=*/"/"); +} + +IN_PROC_BROWSER_TEST_P(WellKnownChangePasswordNavigationThrottleBrowserTest, + NoSupportForChangePassword_WithRedirectToNotFoundPage) { + auto response_delays = GetParam(); + path_response_map_[kWellKnownChangePasswordPath] = { + net::HTTP_PERMANENT_REDIRECT, + {std::make_pair("Location", "/not-found")}, + response_delays.change_password_delay}; + path_response_map_[kWellKnownNotExistingResourcePath] = { + net::HTTP_PERMANENT_REDIRECT, + {std::make_pair("Location", "/not-found")}, + response_delays.not_exist_delay}; + path_response_map_["/"] = {net::HTTP_OK, {}, 0}; + path_response_map_["/not-found"] = {net::HTTP_NOT_FOUND, {}, 0}; + + TestNavigationThrottle(/*expected_path=*/"/"); +} + +IN_PROC_BROWSER_TEST_P(WellKnownChangePasswordNavigationThrottleBrowserTest, + NoSupportForChangePassword_WillFailRequest) { + auto response_delays = GetParam(); + path_response_map_[kWellKnownChangePasswordPath] = { + net::HTTP_PERMANENT_REDIRECT, + {std::make_pair("Location", "/change-password")}, + response_delays.change_password_delay}; + path_response_map_[kWellKnownNotExistingResourcePath] = { + net::HTTP_NOT_FOUND, {}, response_delays.not_exist_delay}; + + // Make request fail. + scoped_refptr cert = test_server_->GetCertificate(); + net::CertVerifyResult verify_result; + verify_result.cert_status = 0; + verify_result.verified_cert = cert; + mock_cert_verifier()->AddResultForCert(cert.get(), verify_result, + net::ERR_BLOCKED_BY_CLIENT); + + GURL url = test_server_->GetURL(kWellKnownChangePasswordPath); + NavigateParams params(browser(), url, + ui::PageTransition::PAGE_TRANSITION_LINK); + Navigate(¶ms); + TestNavigationObserver observer(params.navigated_or_inserted_contents); + observer.Wait(); + + EXPECT_EQ(observer.last_navigation_url(), url); +} + +constexpr ResponseDelayParams kDelayParams[] = {{0, 1}, {1, 0}}; + +INSTANTIATE_TEST_SUITE_P(All, + WellKnownChangePasswordNavigationThrottleBrowserTest, + ::testing::ValuesIn(kDelayParams)); diff --git a/chrome/common/url_constants.cc b/chrome/common/url_constants.cc index 99d38846f95a02..733b1e74c4e094 100644 --- a/chrome/common/url_constants.cc +++ b/chrome/common/url_constants.cc @@ -282,6 +282,12 @@ const char kWhoIsMyAdministratorHelpURL[] = const char kChromeFlashRoadmapURL[] = "https://www.chromium.org/flash-roadmap/"; +const char kWellKnownChangePasswordPath[] = "/.well-known/change-password"; + +const char kWellKnownNotExistingResourcePath[] = + "/.well-known/" + "resource-that-should-not-exist-whose-status-code-should-not-be-200"; + #if defined(OS_ANDROID) const char kAndroidAppScheme[] = "android-app"; #endif diff --git a/chrome/common/url_constants.h b/chrome/common/url_constants.h index ce93bf7963a2cd..b2a182d6912843 100644 --- a/chrome/common/url_constants.h +++ b/chrome/common/url_constants.h @@ -237,6 +237,15 @@ extern const char kWhoIsMyAdministratorHelpURL[]; // Link to the flash roadmap extern const char kChromeFlashRoadmapURL[]; +// Path for Well-Known change password url +// Spec: https://wicg.github.io/change-password-url/ +extern const char kWellKnownChangePasswordPath[]; + +// This path should return 404. This enables us to check whether +// we can trust the server's Well-Known response codes. +// https://wicg.github.io/change-password-url/response-code-reliability.html#iana +extern const char kWellKnownNotExistingResourcePath[]; + #if defined(OS_ANDROID) extern const char kAndroidAppScheme[]; #endif diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn index 9d9b0c3819c399..2ecb06fb592730 100644 --- a/chrome/test/BUILD.gn +++ b/chrome/test/BUILD.gn @@ -1253,6 +1253,7 @@ if (!is_android) { "../browser/ui/passwords/manage_passwords_test.cc", "../browser/ui/passwords/password_generation_popup_view_browsertest.cc", "../browser/ui/passwords/password_generation_popup_view_tester.h", + "../browser/ui/passwords/well_known_change_password_navigation_throttle_browsertest.cc", "../browser/ui/permission_bubble/permission_bubble_browser_test_util.cc", "../browser/ui/permission_bubble/permission_bubble_browser_test_util.h", "../browser/ui/popup_browsertest.cc",