ceftests: Add HSTS redirect test (see issue #3336, see issue #3348)

This commit is contained in:
Marshall Greenblatt 2022-08-02 16:30:37 -04:00
parent 952f2b0829
commit 46e1c4f177
14 changed files with 404 additions and 52 deletions

View File

@ -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',

View File

@ -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<int>(port));
if (!test_server_handle_) {

View File

@ -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<CefBrowser> 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)

View File

@ -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")

View File

@ -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")

View File

@ -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<CefBrowser> 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<CefBrowser> browser) {
class Visitor : public CefStringVisitor {
public:
explicit Visitor(CefRefPtr<CefBrowser> browser) : browser_(browser) {}
virtual void Visit(const CefString& string) override {
std::string source = StringReplace(string, "<", "&lt;");
source = StringReplace(source, ">", "&gt;");
std::string source = AsciiStrReplace(string, "<", "&lt;");
source = AsciiStrReplace(source, ">", "&gt;");
std::stringstream ss;
ss << "<html><body bgcolor=\"white\">Source:<pre>" << source
<< "</pre></body></html>";
@ -98,8 +81,8 @@ void RunGetTextTest(CefRefPtr<CefBrowser> browser) {
public:
explicit Visitor(CefRefPtr<CefBrowser> browser) : browser_(browser) {}
virtual void Visit(const CefString& string) override {
std::string text = StringReplace(string, "<", "&lt;");
text = StringReplace(text, ">", "&gt;");
std::string text = AsciiStrReplace(string, "<", "&lt;");
text = AsciiStrReplace(text, ">", "&gt;");
std::stringstream ss;
ss << "<html><body bgcolor=\"white\">Text:<pre>" << text
<< "</pre></body></html>";
@ -649,8 +632,7 @@ CefRefPtr<CefStreamReader> 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<CefBrowser> 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<CefFrame> frame = browser->GetMainFrame();

View File

@ -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;
}
}

View File

@ -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<void(const std::string& url)>;
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<CefRequest> 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 = "<html><body>Test1</body></html>";
}
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<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request,
CefRefPtr<CefResponse> 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<CefBrowser> browser,
CefRefPtr<CefFrame> 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<HSTSRedirectTest> handler = new HSTSRedirectTest();
handler->ExecuteTest();
ReleaseAndWaitForDestructor(handler);
}

View File

@ -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;

View File

@ -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

View File

@ -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;
}

View File

@ -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;

View File

@ -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 <algorithm>
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

View File

@ -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 <string>
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_