#include <fuchsia/web/cpp/fidl.h>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/client_hints_controller_delegate.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "fuchsia_web/common/test/frame_for_test.h"
#include "fuchsia_web/common/test/frame_test_util.h"
#include "fuchsia_web/common/test/test_navigation_listener.h"
#include "fuchsia_web/webengine/browser/context_impl.h"
#include "fuchsia_web/webengine/browser/frame_impl.h"
#include "fuchsia_web/webengine/browser/frame_impl_browser_test_base.h"
#include "fuchsia_web/webengine/test/test_data.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 "services/network/public/mojom/web_client_hints_types.mojom-shared.h"
namespace {
constexpr const char kHeaderNotPresent[] = "None";
constexpr const char kRoundTripTimeCH[] = "RTT";
constexpr const char kDeviceMemoryCH[] = "Sec-CH-Device-Memory";
constexpr const char kUserAgentCH[] = "Sec-CH-UA";
constexpr const char kFullVersionListCH[] = "Sec-CH-UA-Full-Version-List";
constexpr const char kArchCH[] = "Sec-CH-UA-Arch";
constexpr const char kBitnessCH[] = "Sec-CH-UA-Bitness";
constexpr const char kPlatformCH[] = "Sec-CH-UA-Platform";
constexpr const char k64Bitness[] = "\"64\"";
constexpr const char kFuchsiaPlatform[] = "\"Fuchsia\"";
void ExpectStringIsNonNegativeNumber(std::string& str) {
double str_double;
EXPECT_TRUE(base::StringToDouble(str, &str_double));
EXPECT_GE(str_double, 0);
}
}
class ClientHintsTest : public FrameImplTestBaseWithServer {
public:
ClientHintsTest() = default;
~ClientHintsTest() override = default;
ClientHintsTest(const ClientHintsTest&) = delete;
ClientHintsTest& operator=(const ClientHintsTest&) = delete;
void SetUpOnMainThread() override {
FrameImplTestBaseWithServer::SetUpOnMainThread();
frame_for_test_ = FrameForTest::Create(context(), {});
}
void TearDownOnMainThread() override {
frame_for_test_ = {};
FrameImplTestBaseWithServer::TearDownOnMainThread();
}
protected:
void SetClientHintsForTestServerToRequest(const std::string& hint_types) {
GURL url = embedded_test_server()->GetURL(
std::string("/set-header?Accept-CH: ") + hint_types);
LoadUrlAndExpectResponse(frame_for_test_.GetNavigationController(), {},
url.spec());
frame_for_test_.navigation_listener().RunUntilUrlEquals(url);
}
std::string GetNavRequestHeaderValue(const std::string& header) {
GURL url =
embedded_test_server()->GetURL(std::string("/echoheader?") + header);
LoadUrlAndExpectResponse(frame_for_test_.GetNavigationController(), {},
url.spec());
frame_for_test_.navigation_listener().RunUntilUrlEquals(url);
std::optional<base::Value> value =
ExecuteJavaScript(frame_for_test_.get(), "document.body.innerText;");
return value->GetString();
}
std::string GetXHRRequestHeaderValue(const std::string& header) {
constexpr char kScript[] = R"(
new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.open("GET", "/echoheader?" + $1);
xhr.onload = () => {
resolve(xhr.response);
};
xhr.send();
})
)";
FrameImpl* frame_impl =
context_impl()->GetFrameImplForTest(&frame_for_test_.ptr());
content::WebContents* web_contents = frame_impl->web_contents_for_test();
return content::EvalJs(web_contents, content::JsReplace(kScript, header))
.ExtractString();
}
void GetAndVerifyClientHint(
const std::string& hint_type,
base::RepeatingCallback<void(std::string&)> verify_callback) {
std::string result = GetNavRequestHeaderValue(hint_type);
verify_callback.Run(result);
result = GetXHRRequestHeaderValue(hint_type);
verify_callback.Run(result);
}
FrameForTest frame_for_test_;
};
IN_PROC_BROWSER_TEST_F(ClientHintsTest, NumericalClientHints) {
SetClientHintsForTestServerToRequest(std::string(kRoundTripTimeCH) + "," +
std::string(kDeviceMemoryCH));
GetAndVerifyClientHint(kRoundTripTimeCH,
base::BindRepeating(&ExpectStringIsNonNegativeNumber));
GetAndVerifyClientHint(kDeviceMemoryCH,
base::BindRepeating(&ExpectStringIsNonNegativeNumber));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest, InvalidClientHint) {
SetClientHintsForTestServerToRequest("not-a-client-hint");
GetAndVerifyClientHint("not-a-client-hint",
base::BindRepeating([](std::string& str) {
EXPECT_EQ(str, kHeaderNotPresent);
}));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest, LowEntropyClientHintsAreSentByDefault) {
GetAndVerifyClientHint(
kUserAgentCH, base::BindRepeating([](std::string& str) {
EXPECT_TRUE(base::Contains(str, "Chromium"));
EXPECT_TRUE(base::Contains(str, version_info::GetMajorVersionNumber()));
}));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest,
LowEntropyClientHintsAreSentWhenRequested) {
SetClientHintsForTestServerToRequest(kUserAgentCH);
GetAndVerifyClientHint(
kUserAgentCH, base::BindRepeating([](std::string& str) {
EXPECT_TRUE(base::Contains(str, "Chromium"));
EXPECT_TRUE(base::Contains(str, version_info::GetMajorVersionNumber()));
}));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest,
HighEntropyClientHintsAreNotSentByDefault) {
GetAndVerifyClientHint(kFullVersionListCH,
base::BindRepeating([](std::string& str) {
EXPECT_EQ(str, kHeaderNotPresent);
}));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest,
HighEntropyClientHintsAreSentWhenRequested) {
SetClientHintsForTestServerToRequest(kFullVersionListCH);
GetAndVerifyClientHint(
kFullVersionListCH, base::BindRepeating([](std::string& str) {
EXPECT_TRUE(base::Contains(str, "Chromium"));
EXPECT_TRUE(base::Contains(str, version_info::GetVersionNumber()));
}));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest, ArchitectureIsArmOrX86) {
SetClientHintsForTestServerToRequest(kArchCH);
GetAndVerifyClientHint(kArchCH, base::BindRepeating([](std::string& str) {
#if defined(ARCH_CPU_X86_64)
EXPECT_EQ(str, "\"x86\"");
#elif defined(ARCH_CPU_ARM64)
EXPECT_EQ(str, "\"arm\"");
#else
#error Unsupported CPU architecture
#endif
}));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest, BitnessIs64) {
SetClientHintsForTestServerToRequest(kBitnessCH);
GetAndVerifyClientHint(kBitnessCH, base::BindRepeating([](std::string& str) {
EXPECT_EQ(str, k64Bitness);
}));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest, PlatformIsFuchsia) {
GetAndVerifyClientHint(kPlatformCH, base::BindRepeating([](std::string& str) {
EXPECT_EQ(str, kFuchsiaPlatform);
}));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest, RemoveClientHint) {
SetClientHintsForTestServerToRequest(std::string(kRoundTripTimeCH) + "," +
std::string(kDeviceMemoryCH));
GetAndVerifyClientHint(kDeviceMemoryCH,
base::BindRepeating(&ExpectStringIsNonNegativeNumber));
SetClientHintsForTestServerToRequest(kRoundTripTimeCH);
GetAndVerifyClientHint(kDeviceMemoryCH,
base::BindRepeating([](std::string& str) {
EXPECT_EQ(str, kHeaderNotPresent);
}));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest, AdditionalClientHintsAreAlwaysSent) {
SetClientHintsForTestServerToRequest(kRoundTripTimeCH);
auto* client_hints_delegate =
context_impl()->browser_context()->GetClientHintsControllerDelegate();
client_hints_delegate->SetAdditionalClientHints(
{network::mojom::WebClientHintsType::kDeviceMemory});
GetAndVerifyClientHint(kRoundTripTimeCH,
base::BindRepeating(&ExpectStringIsNonNegativeNumber));
GetAndVerifyClientHint(kDeviceMemoryCH,
base::BindRepeating(&ExpectStringIsNonNegativeNumber));
client_hints_delegate->ClearAdditionalClientHints();
GetAndVerifyClientHint(kRoundTripTimeCH,
base::BindRepeating(&ExpectStringIsNonNegativeNumber));
GetAndVerifyClientHint(kDeviceMemoryCH,
base::BindRepeating([](std::string& str) {
EXPECT_EQ(str, kHeaderNotPresent);
}));
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest, WithUrlRedirectRules) {
net::EmbeddedTestServer http2_server(
net::test_server::EmbeddedTestServer::TYPE_HTTPS,
net::test_server::HttpConnection::Protocol::kHttp2);
http2_server.ServeFilesFromSourceDirectory(kTestServerRoot);
http2_server.SetAlpsAcceptCH(
"", base::JoinString({kBitnessCH, kPlatformCH}, ","));
http2_server.RegisterRequestMonitor(
base::BindRepeating([](const net::test_server::HttpRequest& request) {
EXPECT_TRUE(request.headers.contains(kBitnessCH));
EXPECT_EQ(request.headers.at(kBitnessCH), k64Bitness);
EXPECT_TRUE(request.headers.contains(kPlatformCH));
EXPECT_EQ(request.headers.at(kPlatformCH), kFuchsiaPlatform);
}));
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle = http2_server.StartAndReturnHandle());
fuchsia::web::UrlRequestRewriteAppendToQuery append_to_query;
append_to_query.set_query("foo=1&bar=2");
fuchsia::web::UrlRequestRewrite rewrite;
rewrite.set_append_to_query(std::move(append_to_query));
fuchsia::web::UrlRequestRewriteRule rule;
rule.set_hosts_filter({http2_server.base_url().GetHost()});
rule.set_schemes_filter({http2_server.base_url().GetScheme()});
rule.mutable_rewrites()->push_back(std::move(rewrite));
std::vector<fuchsia::web::UrlRequestRewriteRule> rules;
rules.push_back(std::move(rule));
base::RunLoop run_loop;
frame_for_test_->SetUrlRequestRewriteRules(
std::move(rules), [&run_loop]() { run_loop.Quit(); });
run_loop.Run();
GURL url = http2_server.GetURL("/title1.html");
EXPECT_TRUE(LoadUrlAndExpectResponse(
frame_for_test_.GetNavigationController(), {}, url.spec()));
frame_for_test_.navigation_listener().RunUntilLoaded();
EXPECT_EQ(frame_for_test_.navigation_listener().current_state()->url(),
url.spec() + "?foo=1&bar=2");
}
std::unique_ptr<net::test_server::HttpResponse>
SandboxedClientHintsRequestHandler(
const std::string client_hint_type,
const net::test_server::HttpRequest& request) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->AddCustomHeader("Content-Security-Policy", "sandbox allow-scripts");
if (request.relative_url == "/set") {
response->AddCustomHeader("Accept-CH", client_hint_type);
} else if (request.relative_url == "/get") {
auto it = request.headers.find(client_hint_type);
if (it != request.headers.end()) {
response->set_content(it->second);
}
} else {
return nullptr;
}
return std::move(response);
}
IN_PROC_BROWSER_TEST_F(ClientHintsTest, HintsAreSentFromSandboxedPage) {
net::EmbeddedTestServer http_server;
http_server.RegisterRequestHandler(
base::BindRepeating(&SandboxedClientHintsRequestHandler, kBitnessCH));
net::test_server::EmbeddedTestServerHandle test_server_handle;
ASSERT_TRUE(test_server_handle = http_server.StartAndReturnHandle());
GURL url = http_server.GetURL("/set");
LoadUrlAndExpectResponse(frame_for_test_.GetNavigationController(), {},
url.spec());
frame_for_test_.navigation_listener().RunUntilUrlEquals(url);
url = http_server.GetURL("/get");
LoadUrlAndExpectResponse(frame_for_test_.GetNavigationController(), {},
url.spec());
frame_for_test_.navigation_listener().RunUntilUrlEquals(url);
std::optional<base::Value> value =
ExecuteJavaScript(frame_for_test_.get(), "document.body.innerText;");
EXPECT_EQ(value->GetString(), k64Bitness);
}