From 46e1c4f177d720b49f7dce859cab1d974029db9b Mon Sep 17 00:00:00 2001 From: Marshall Greenblatt Date: Tue, 2 Aug 2022 16:30:37 -0400 Subject: [PATCH] ceftests: Add HSTS redirect test (see issue #3336, see issue #3348) --- cef_paths2.gypi | 3 + libcef/browser/test/test_server_impl.cc | 7 + tests/cefclient/browser/client_handler.cc | 8 +- tests/cefclient/browser/image_cache.cc | 3 +- tests/cefclient/browser/main_context_impl.cc | 6 +- tests/cefclient/browser/test_runner.cc | 34 +- tests/ceftests/cookie_unittest.cc | 3 +- tests/ceftests/hsts_redirect_unittest.cc | 312 +++++++++++++++++++ tests/ceftests/test_util.cc | 11 +- tests/ceftests/test_util.h | 2 - tests/ceftests/urlrequest_unittest.cc | 3 +- tests/shared/browser/extension_util.cc | 7 +- tests/shared/common/string_util.cc | 34 ++ tests/shared/common/string_util.h | 23 ++ 14 files changed, 404 insertions(+), 52 deletions(-) create mode 100644 tests/ceftests/hsts_redirect_unittest.cc create mode 100644 tests/shared/common/string_util.cc create mode 100644 tests/shared/common/string_util.h diff --git a/cef_paths2.gypi b/cef_paths2.gypi index a24501279..8f82ad714 100644 --- a/cef_paths2.gypi +++ b/cef_paths2.gypi @@ -186,6 +186,8 @@ 'tests/shared/common/client_app_other.h', 'tests/shared/common/client_switches.cc', 'tests/shared/common/client_switches.h', + 'tests/shared/common/string_util.cc', + 'tests/shared/common/string_util.h', ], 'shared_sources_renderer': [ 'tests/shared/renderer/client_app_renderer.cc', @@ -476,6 +478,7 @@ 'tests/ceftests/file_util_unittest.cc', 'tests/ceftests/frame_handler_unittest.cc', 'tests/ceftests/frame_unittest.cc', + 'tests/ceftests/hsts_redirect_unittest.cc', 'tests/ceftests/image_unittest.cc', 'tests/ceftests/image_util.cc', 'tests/ceftests/image_util.h', diff --git a/libcef/browser/test/test_server_impl.cc b/libcef/browser/test/test_server_impl.cc index 582f7066e..8861df4dc 100644 --- a/libcef/browser/test/test_server_impl.cc +++ b/libcef/browser/test/test_server_impl.cc @@ -184,6 +184,13 @@ class CefTestServerImpl::Context { test_server_->RegisterRequestHandler( base::BindRepeating(&Context::HandleRequest, base::Unretained(this))); + if (https_server) { + // Use a "localhost" domain certificate instead of IP address. This is + // required for HSTS tests (see https://crbug.com/456712). + test_server_->SetSSLConfig( + EmbeddedTestServer::CERT_COMMON_NAME_IS_DOMAIN); + } + test_server_handle_ = test_server_->StartAndReturnHandle(static_cast(port)); if (!test_server_handle_) { diff --git a/tests/cefclient/browser/client_handler.cc b/tests/cefclient/browser/client_handler.cc index ed3f6d53b..3e8b319d8 100644 --- a/tests/cefclient/browser/client_handler.cc +++ b/tests/cefclient/browser/client_handler.cc @@ -27,6 +27,7 @@ #include "tests/shared/browser/resource_util.h" #include "tests/shared/common/binary_value_utils.h" #include "tests/shared/common/client_switches.h" +#include "tests/shared/common/string_util.h" namespace client { @@ -1047,12 +1048,9 @@ void ClientHandler::OnRenderProcessTerminated(CefRefPtr browser, if (url.empty()) return; - std::string start_url = startup_url_; - // Convert URLs to lowercase for easier comparison. - std::transform(url.begin(), url.end(), url.begin(), tolower); - std::transform(start_url.begin(), start_url.end(), start_url.begin(), - tolower); + url = AsciiStrToLower(url); + const std::string& start_url = AsciiStrToLower(startup_url_); // Don't reload the URL that just resulted in termination. if (url.find(start_url) == 0) diff --git a/tests/cefclient/browser/image_cache.cc b/tests/cefclient/browser/image_cache.cc index ea0ee5516..efbe67b72 100644 --- a/tests/cefclient/browser/image_cache.cc +++ b/tests/cefclient/browser/image_cache.cc @@ -8,6 +8,7 @@ #include "tests/shared/browser/file_util.h" #include "tests/shared/browser/resource_util.h" +#include "tests/shared/common/string_util.h" namespace client { @@ -158,7 +159,7 @@ ImageCache::ImageType ImageCache::GetImageType(const std::string& path) { if (ext.empty()) return TYPE_NONE; - std::transform(ext.begin(), ext.end(), ext.begin(), tolower); + ext = AsciiStrToLower(ext); if (ext == "png") return TYPE_PNG; if (ext == "jpg" || ext == "jpeg") diff --git a/tests/cefclient/browser/main_context_impl.cc b/tests/cefclient/browser/main_context_impl.cc index cc41ddf50..5c8b4fc2e 100644 --- a/tests/cefclient/browser/main_context_impl.cc +++ b/tests/cefclient/browser/main_context_impl.cc @@ -9,6 +9,7 @@ #include "include/cef_parser.h" #include "tests/shared/browser/client_app_browser.h" #include "tests/shared/common/client_switches.h" +#include "tests/shared/common/string_util.h" namespace client { @@ -19,10 +20,7 @@ const char kDefaultUrl[] = "http://www.google.com"; // Returns the ARGB value for |color|. cef_color_t ParseColor(const std::string& color) { - std::string colorToLower; - colorToLower.resize(color.size()); - std::transform(color.begin(), color.end(), colorToLower.begin(), ::tolower); - + const std::string& colorToLower = AsciiStrToLower(color); if (colorToLower == "black") return CefColorSetARGB(255, 0, 0, 0); else if (colorToLower == "blue") diff --git a/tests/cefclient/browser/test_runner.cc b/tests/cefclient/browser/test_runner.cc index 68665ffad..1693ede2c 100644 --- a/tests/cefclient/browser/test_runner.cc +++ b/tests/cefclient/browser/test_runner.cc @@ -29,6 +29,7 @@ #include "tests/cefclient/browser/urlrequest_test.h" #include "tests/cefclient/browser/window_test.h" #include "tests/shared/browser/resource_util.h" +#include "tests/shared/common/string_util.h" namespace client { namespace test_runner { @@ -54,31 +55,13 @@ void LoadStringResourcePage(CefRefPtr browser, browser->GetMainFrame()->LoadURL(kTestOrigin + page); } -// Replace all instances of |from| with |to| in |str|. -std::string StringReplace(const std::string& str, - const std::string& from, - const std::string& to) { - std::string result = str; - std::string::size_type pos = 0; - std::string::size_type from_len = from.length(); - std::string::size_type to_len = to.length(); - do { - pos = result.find(from, pos); - if (pos != std::string::npos) { - result.replace(pos, from_len, to); - pos += to_len; - } - } while (pos != std::string::npos); - return result; -} - void RunGetSourceTest(CefRefPtr browser) { class Visitor : public CefStringVisitor { public: explicit Visitor(CefRefPtr browser) : browser_(browser) {} virtual void Visit(const CefString& string) override { - std::string source = StringReplace(string, "<", "<"); - source = StringReplace(source, ">", ">"); + std::string source = AsciiStrReplace(string, "<", "<"); + source = AsciiStrReplace(source, ">", ">"); std::stringstream ss; ss << "Source:
" << source
          << "
"; @@ -98,8 +81,8 @@ void RunGetTextTest(CefRefPtr browser) { public: explicit Visitor(CefRefPtr browser) : browser_(browser) {} virtual void Visit(const CefString& string) override { - std::string text = StringReplace(string, "<", "<"); - text = StringReplace(text, ">", ">"); + std::string text = AsciiStrReplace(string, "<", "<"); + text = AsciiStrReplace(text, ">", ">"); std::stringstream ss; ss << "Text:
" << text
          << "
"; @@ -649,8 +632,7 @@ CefRefPtr GetDumpResponse( CefRequest::HeaderMap::const_iterator it = requestMap.begin(); for (; it != requestMap.end(); ++it) { - std::string key = it->first; - std::transform(key.begin(), key.end(), key.begin(), ::tolower); + const std::string& key = AsciiStrToLower(it->first); if (key == "origin") { origin = it->second; break; @@ -801,8 +783,8 @@ void Alert(CefRefPtr browser, const std::string& message) { } // Escape special characters in the message. - std::string msg = StringReplace(message, "\\", "\\\\"); - msg = StringReplace(msg, "'", "\\'"); + std::string msg = AsciiStrReplace(message, "\\", "\\\\"); + msg = AsciiStrReplace(msg, "'", "\\'"); // Execute a JavaScript alert(). CefRefPtr frame = browser->GetMainFrame(); diff --git a/tests/ceftests/cookie_unittest.cc b/tests/ceftests/cookie_unittest.cc index 1744e1090..7985b4e21 100644 --- a/tests/ceftests/cookie_unittest.cc +++ b/tests/ceftests/cookie_unittest.cc @@ -21,6 +21,7 @@ #include "tests/ceftests/test_suite.h" #include "tests/ceftests/test_util.h" #include "tests/gtest/include/gtest/gtest.h" +#include "tests/shared/common/string_util.h" namespace { @@ -1113,7 +1114,7 @@ class CookieAccessResponseHandler { std::string GetHeaderValue(const CefServer::HeaderMap& header_map, const std::string& header_name_lower) { for (const auto& [name, value] : header_map) { - if (AsciiStrToLower(name) == header_name_lower) { + if (client::AsciiStrToLower(name) == header_name_lower) { return value; } } diff --git a/tests/ceftests/hsts_redirect_unittest.cc b/tests/ceftests/hsts_redirect_unittest.cc new file mode 100644 index 000000000..5577109e1 --- /dev/null +++ b/tests/ceftests/hsts_redirect_unittest.cc @@ -0,0 +1,312 @@ +// Copyright (c) 2022 The Chromium Embedded Framework 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 "include/base/cef_callback.h" +#include "include/wrapper/cef_closure_task.h" +#include "tests/ceftests/test_handler.h" +#include "tests/ceftests/test_server.h" +#include "tests/ceftests/test_server_observer.h" +#include "tests/gtest/include/gtest/gtest.h" +#include "tests/shared/common/string_util.h" + +namespace { + +// Set the "Strict-Transport-Security" header on an HTTPS response to enable +// HSTS redirects for follow-up HTTP requests to the same origin. See +// https://www.chromium.org/hsts/. +// +// HSTS is implemented in the network service so real servers are required to +// test the redirect behavior. It also requires a "localhost" domain certificate +// instead of IP address (see https://crbug.com/456712). See additional comments +// in OnResourceRedirect about redirect behavior with non-standard port numbers. +// +// The test works as follows: +// 1. Start HTTP and HTTPS servers. +// 2. Load an HTTP URL that redirects to an HTTPS URL. +// 3. Set the "Strict-Transport-Security" header in response to the first HTTPS +// request. +// 4. Load the same HTTP URL additional times to trigger the internal HTTP to +// HTTPS redirect. + +// Number of times to load the same HTTP URL. Must be > 1. +constexpr size_t kHSTSLoadCount = 3; + +constexpr char kHSTSURLPath[] = "/index.html"; + +// Used to observe HTTP and HTTPS server requests. +class HSTSTestServerObserver : public test_server::ObserverHelper { + public: + using ReadyCallback = base::OnceCallback; + + HSTSTestServerObserver(bool https_server, + size_t& nav_ct, + TrackCallback (&got_request)[kHSTSLoadCount], + ReadyCallback ready_callback, + base::OnceClosure done_callback) + : https_server_(https_server), + nav_ct_(nav_ct), + got_request_(got_request), + ready_callback_(std::move(ready_callback)), + done_callback_(std::move(done_callback)) { + Initialize(https_server); + } + + void OnInitialized(const std::string& server_origin) override { + EXPECT_UI_THREAD(); + + origin_ = ToLocalhostOrigin(server_origin); + url_ = origin_ + kHSTSURLPath; + + std::move(ready_callback_).Run(url_); + } + + void OnShutdown() override { + EXPECT_UI_THREAD(); + std::move(done_callback_).Run(); + delete this; + } + + bool OnTestServerRequest(CefRefPtr request, + const ResponseCallback& response_callback) override { + EXPECT_UI_THREAD(); + + // At most 1 request per load. + EXPECT_FALSE(got_request_[nav_ct_]) << nav_ct_; + got_request_[nav_ct_].yes(); + + const std::string& url = ToLocalhostOrigin(request->GetURL()); + + auto response = CefResponse::Create(); + response->SetMimeType("text/html"); + std::string response_body; + + if (!https_server_) { + // Redirect to the HTTPS URL. + EXPECT_STREQ(url_.c_str(), url.c_str()) << nav_ct_; + response->SetStatus(301); // Permanent Redirect + response->SetHeaderByName("Location", + GetLocalhostURL(/*https_server=*/true), + /*overwrite=*/true); + } else { + // Normal response after an HTTP to HTTPS redirect. + EXPECT_STREQ(url_.c_str(), url.c_str()) << nav_ct_; + response->SetStatus(200); + + if (nav_ct_ == 0) { + // Set the "Strict-Transport-Security" header in response to the first + // HTTPS request. + response->SetHeaderByName("Strict-Transport-Security", + "max-age=16070400", + /*overwrite=*/true); + } + + // Don't cache the HTTPS response (so we see all the requests). + response->SetHeaderByName("Cache-Control", "no-cache", + /*overwrite=*/true); + + response_body = "Test1"; + } + + response_callback.Run(response, response_body); + + // Stop propagating the callback. + return true; + } + + private: + static std::string ToLocalhostOrigin(const std::string& origin) { + // Need to explicitly use the "localhost" domain instead of the IP address. + // HTTPS URLs will already be using "localhost". + return client::AsciiStrReplace(origin, "127.0.0.1", "localhost"); + } + + static std::string GetLocalhostOrigin(bool https_server) { + return ToLocalhostOrigin(test_server::GetOrigin(https_server)); + } + + static std::string GetLocalhostURL(bool https_server) { + return GetLocalhostOrigin(/*https_server=*/true) + kHSTSURLPath; + } + + const bool https_server_; + + size_t& nav_ct_; + TrackCallback (&got_request_)[kHSTSLoadCount]; + ReadyCallback ready_callback_; + base::OnceClosure done_callback_; + + std::string origin_; + std::string url_; + + DISALLOW_COPY_AND_ASSIGN(HSTSTestServerObserver); +}; + +class HSTSRedirectTest : public TestHandler { + public: + HSTSRedirectTest() = default; + + void RunTest() override { + SetTestTimeout(); + CefPostTask(TID_UI, + base::BindOnce(&HSTSRedirectTest::StartHttpServer, this)); + } + + void OnResourceRedirect(CefRefPtr browser, + CefRefPtr frame, + CefRefPtr request, + CefRefPtr response, + CefString& new_url) override { + EXPECT_IO_THREAD(); + + EXPECT_FALSE(got_redirect_[nav_ct_]) << nav_ct_; + got_redirect_[nav_ct_].yes(); + + EXPECT_STREQ(http_url_.c_str(), request->GetURL().ToString().c_str()) + << nav_ct_; + + if (nav_ct_ == 0) { + // Initial HTTP to HTTPS redirect. + EXPECT_STREQ(https_url_.c_str(), new_url.ToString().c_str()) << nav_ct_; + } else { + // HSTS HTTP to HTTPS redirect. This will use the wrong "localhost" port + // number, per spec. From RFC 6797: + // The UA MUST replace the URI scheme with "https" [RFC2818], and if the + // URI contains an explicit port component of "80", then the UA MUST + // convert the port component to be "443", or if the URI contains an + // explicit port component that is not equal to "80", the port component + // value MUST be preserved; otherwise, if the URI does not contain an + // explicit port component, the UA MUST NOT add one. + const std::string& expected_https_url = + client::AsciiStrReplace(http_url_, "http:", "https:"); + EXPECT_STREQ(expected_https_url.c_str(), new_url.ToString().c_str()) + << nav_ct_; + + // Redirect to the correct HTTPS URL instead. + new_url = https_url_; + } + } + + void OnLoadEnd(CefRefPtr browser, + CefRefPtr frame, + int httpStatusCode) override { + EXPECT_UI_THREAD(); + + TestHandler::OnLoadEnd(browser, frame, httpStatusCode); + + EXPECT_FALSE(got_load_end_[nav_ct_]) << nav_ct_; + got_load_end_[nav_ct_].yes(); + + // Expect only the HTTPS URL to load. + EXPECT_STREQ(https_url_.c_str(), frame->GetURL().ToString().c_str()) + << nav_ct_; + + if (++nav_ct_ == kHSTSLoadCount) { + StopHttpServer(); + } else { + // Load the same HTTP URL again. + browser->GetMainFrame()->LoadURL(http_url_); + } + } + + void DestroyTest() override { + EXPECT_FALSE(http_server_); + EXPECT_FALSE(https_server_); + + EXPECT_EQ(kHSTSLoadCount, nav_ct_); + for (size_t i = 0; i < kHSTSLoadCount; ++i) { + EXPECT_TRUE(got_redirect_[i]) << i; + EXPECT_TRUE(got_load_end_[i]) << i; + } + + for (size_t i = 0; i < kHSTSLoadCount; ++i) { + // Should only see the 1st HTTP request due to the internal HSTS redirect + // for the 2nd+ requests. + EXPECT_EQ(i == 0, got_http_request_[i]) << i; + + // Should see all HTTPS requests. + EXPECT_TRUE(got_https_request_[i]) << i; + } + + TestHandler::DestroyTest(); + } + + private: + void StartHttpServer() { + EXPECT_UI_THREAD(); + + // Will delete itself after the server stops. + http_server_ = new HSTSTestServerObserver( + /*https_server=*/false, nav_ct_, got_http_request_, + base::BindOnce(&HSTSRedirectTest::StartedHttpServer, this), + base::BindOnce(&HSTSRedirectTest::StoppedHttpServer, this)); + } + + void StartedHttpServer(const std::string& url) { + EXPECT_UI_THREAD(); + + http_url_ = url; + EXPECT_TRUE(http_url_.find("http://localhost:") == 0); + + // Start the HTTPS server. Will delete itself after the server stops. + https_server_ = new HSTSTestServerObserver( + /*https_server=*/true, nav_ct_, got_https_request_, + base::BindOnce(&HSTSRedirectTest::StartedHttpsServer, this), + base::BindOnce(&HSTSRedirectTest::StoppedHttpsServer, this)); + } + + void StartedHttpsServer(const std::string& url) { + EXPECT_UI_THREAD(); + + https_url_ = url; + EXPECT_TRUE(https_url_.find("https://localhost:") == 0); + + CreateBrowser(http_url_); + } + + void StopHttpServer() { + EXPECT_UI_THREAD(); + + // Results in a call to StoppedHttpServer(). + http_server_->Shutdown(); + } + + void StoppedHttpServer() { + EXPECT_UI_THREAD(); + + http_server_ = nullptr; + + // Stop the HTTPS server. Results in a call to StoppedHttpsServer(). + https_server_->Shutdown(); + } + + void StoppedHttpsServer() { + EXPECT_UI_THREAD(); + + https_server_ = nullptr; + + DestroyTest(); + } + + HSTSTestServerObserver* http_server_ = nullptr; + std::string http_url_; + + HSTSTestServerObserver* https_server_ = nullptr; + std::string https_url_; + + size_t nav_ct_ = 0U; + TrackCallback got_http_request_[kHSTSLoadCount]; + TrackCallback got_https_request_[kHSTSLoadCount]; + TrackCallback got_load_end_[kHSTSLoadCount]; + TrackCallback got_redirect_[kHSTSLoadCount]; + + IMPLEMENT_REFCOUNTING(HSTSRedirectTest); +}; + +} // namespace + +TEST(HSTSRedirectTest, Redirect) { + CefRefPtr handler = new HSTSRedirectTest(); + handler->ExecuteTest(); + ReleaseAndWaitForDestructor(handler); +} diff --git a/tests/ceftests/test_util.cc b/tests/ceftests/test_util.cc index b63b04c7e..7eb3b4f87 100644 --- a/tests/ceftests/test_util.cc +++ b/tests/ceftests/test_util.cc @@ -10,12 +10,7 @@ #include "include/cef_command_line.h" #include "include/cef_request_context_handler.h" #include "tests/gtest/include/gtest/gtest.h" - -std::string AsciiStrToLower(const std::string& str) { - std::string lowerStr = str; - std::transform(lowerStr.begin(), lowerStr.end(), lowerStr.begin(), ::tolower); - return lowerStr; -} +#include "tests/shared/common/string_util.h" void TestMapEqual(const CefRequest::HeaderMap& map1, const CefRequest::HeaderMap& map2, @@ -31,9 +26,9 @@ void TestMapEqual(const CefRequest::HeaderMap& map1, for (it1 = map1.begin(); it1 != map1.end(); ++it1) { bool found = false; - std::string name1 = AsciiStrToLower(it1->first); + std::string name1 = client::AsciiStrToLower(it1->first); for (it2 = map2.begin(); it2 != map2.end(); ++it2) { - std::string name2 = AsciiStrToLower(it2->first); + std::string name2 = client::AsciiStrToLower(it2->first); if (name1 == name2 && it1->second == it2->second) { found = true; break; diff --git a/tests/ceftests/test_util.h b/tests/ceftests/test_util.h index 13746e92d..e066bc4fa 100644 --- a/tests/ceftests/test_util.h +++ b/tests/ceftests/test_util.h @@ -16,8 +16,6 @@ CefTime CefTimeFrom(CefBaseTime value); CefBaseTime CefBaseTimeFrom(const CefTime& value); -std::string AsciiStrToLower(const std::string& str); - // Test that CefRequest::HeaderMap objects are equal. Multiple values with the // same key are allowed, but not duplicate entries with the same key/value. If // |allowExtras| is true then additional header fields will be allowed in diff --git a/tests/ceftests/urlrequest_unittest.cc b/tests/ceftests/urlrequest_unittest.cc index e2ab02561..bd5998468 100644 --- a/tests/ceftests/urlrequest_unittest.cc +++ b/tests/ceftests/urlrequest_unittest.cc @@ -25,6 +25,7 @@ #include "tests/gtest/include/gtest/gtest.h" #include "tests/shared/browser/client_app_browser.h" #include "tests/shared/browser/file_util.h" +#include "tests/shared/common/string_util.h" #include "tests/shared/renderer/client_app_renderer.h" using client::ClientAppRenderer; @@ -453,7 +454,7 @@ std::string GetHeaderValue(const CefRequest::HeaderMap& header_map, const std::string& header_name_lower) { CefRequest::HeaderMap::const_iterator it = header_map.begin(); for (; it != header_map.end(); ++it) { - std::string name = AsciiStrToLower(it->first); + std::string name = client::AsciiStrToLower(it->first); if (name == header_name_lower) return it->second; } diff --git a/tests/shared/browser/extension_util.cc b/tests/shared/browser/extension_util.cc index 6163040e6..e6e6eae89 100644 --- a/tests/shared/browser/extension_util.cc +++ b/tests/shared/browser/extension_util.cc @@ -14,6 +14,7 @@ #include "include/wrapper/cef_closure_task.h" #include "tests/shared/browser/file_util.h" #include "tests/shared/browser/resource_util.h" +#include "tests/shared/common/string_util.h" namespace client { namespace extension_util { @@ -36,10 +37,8 @@ std::string GetInternalPath(const std::string& extension_path) { #if defined(OS_WIN) // Convert to lower-case, since Windows paths are case-insensitive. - std::transform(resources_path_lower.begin(), resources_path_lower.end(), - resources_path_lower.begin(), ::tolower); - std::transform(extension_path_lower.begin(), extension_path_lower.end(), - extension_path_lower.begin(), ::tolower); + resources_path_lower = AsciiStrToLower(resources_path_lower); + extension_path_lower = AsciiStrToLower(extension_path_lower); #endif std::string internal_path; diff --git a/tests/shared/common/string_util.cc b/tests/shared/common/string_util.cc new file mode 100644 index 000000000..7bd5ada83 --- /dev/null +++ b/tests/shared/common/string_util.cc @@ -0,0 +1,34 @@ +// Copyright (c) 2022 The Chromium Embedded Framework 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 "tests/shared/common/string_util.h" + +#include + +namespace client { + +std::string AsciiStrToLower(const std::string& str) { + std::string lowerStr = str; + std::transform(lowerStr.begin(), lowerStr.end(), lowerStr.begin(), ::tolower); + return lowerStr; +} + +std::string AsciiStrReplace(const std::string& str, + const std::string& from, + const std::string& to) { + std::string result = str; + std::string::size_type pos = 0; + std::string::size_type from_len = from.length(); + std::string::size_type to_len = to.length(); + do { + pos = result.find(from, pos); + if (pos != std::string::npos) { + result.replace(pos, from_len, to); + pos += to_len; + } + } while (pos != std::string::npos); + return result; +} + +} // namespace client diff --git a/tests/shared/common/string_util.h b/tests/shared/common/string_util.h new file mode 100644 index 000000000..5ae4e2c31 --- /dev/null +++ b/tests/shared/common/string_util.h @@ -0,0 +1,23 @@ +// Copyright (c) 2022 The Chromium Embedded Framework Authors. All rights +// reserved. Use of this source code is governed by a BSD-style license that +// can be found in the LICENSE file. + +#ifndef CEF_TESTS_SHARED_COMMON_STRING_UTIL_H_ +#define CEF_TESTS_SHARED_COMMON_STRING_UTIL_H_ +#pragma once + +#include + +namespace client { + +// Convert |str| to lowercase. +std::string AsciiStrToLower(const std::string& str); + +// Replace all instances of |from| with |to| in |str|. +std::string AsciiStrReplace(const std::string& str, + const std::string& from, + const std::string& to); + +} // namespace client + +#endif // CEF_TESTS_SHARED_COMMON_STRING_UTIL_H_