blob: 7149cf9ecef91f756955e777ca076e5a687f1429 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "chrome/browser/browsing_data/counters/site_data_counting_helper.h"
#include "chrome/browser/content_settings/cookie_settings_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/browsing_data/core/counters/browsing_data_counter.h"
#include "components/content_settings/core/browser/cookie_settings.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_devtools_protocol_client.h"
#include "net/dns/mock_host_resolver.h"
#include "net/extras/shared_dictionary/shared_dictionary_usage_info.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/shared_dictionary_encoding_names.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "services/network/public/mojom/shared_dictionary_access_observer.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom.h"
#include "url/url_constants.h"
namespace {
constexpr std::string_view kTestDictionaryString = "A dictionary";
constexpr std::string_view kTestDictionaryHashBase64 =
":CqNpAU9/qzcL6UB0aYVFx7uTLsRhJSePN780qwKjWuw=:";
constexpr std::string_view kCompressedDataOriginalString =
"This is compressed test data using a test dictionary";
// kBrotliCompressedData is generated using the following commands:
// $ echo -n "A dictionary" > /tmp/dict
// $ echo -n "This is compressed test data using a test dictionary" > /tmp/data
// $ ./brotli -o /tmp/out.sbr -D /tmp/dict /tmp/data
// $ xxd -i /tmp/out.sbr
constexpr uint8_t kBrotliCompressedData[] = {
0xa1, 0x98, 0x01, 0x80, 0x22, 0xe0, 0x26, 0x4b, 0x95, 0x5c, 0x19,
0x18, 0x9d, 0xc1, 0xc3, 0x44, 0x0e, 0x5c, 0x6a, 0x09, 0x9d, 0xf0,
0xb0, 0x01, 0x47, 0x14, 0x87, 0x14, 0x6d, 0xfb, 0x60, 0x96, 0xdb,
0xae, 0x9e, 0x79, 0x54, 0xe3, 0x69, 0x03, 0x29};
// NOLINTNEXTLINE(runtime/string)
const std::string kBrotliCompressedDataString =
std::string(reinterpret_cast<const char*>(kBrotliCompressedData),
sizeof(kBrotliCompressedData));
// kZstdCompressedData is generated using the following commands:
// $ echo -n "A dictionary" > /tmp/dict
// $ echo -n "This is compressed test data using a test dictionary" > /tmp/data
// $ zstd -o /tmp/out.szstd -D /tmp/dict /tmp/data
// $ xxd -i /tmp/out.szstd
constexpr uint8_t kZstdCompressedData[] = {
0x28, 0xb5, 0x2f, 0xfd, 0x24, 0x34, 0xa1, 0x01, 0x00, 0x54, 0x68,
0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x63, 0x6f, 0x6d, 0x70, 0x72,
0x65, 0x73, 0x73, 0x65, 0x64, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20,
0x64, 0x61, 0x74, 0x61, 0x20, 0x75, 0x73, 0x69, 0x6e, 0x67, 0x20,
0x61, 0x20, 0x74, 0x65, 0x73, 0x74, 0x20, 0x64, 0x69, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x61, 0x72, 0x79, 0x9e, 0x99, 0xf2, 0xbc};
// NOLINTNEXTLINE(runtime/string)
const std::string kZstdCompressedDataString =
std::string(reinterpret_cast<const char*>(kZstdCompressedData),
sizeof(kZstdCompressedData));
class SharedDictionaryAccessObserver : public content::WebContentsObserver {
public:
SharedDictionaryAccessObserver(content::WebContents* web_contents,
base::RepeatingClosure on_accessed_callback)
: WebContentsObserver(web_contents),
on_accessed_callback_(std::move(on_accessed_callback)) {}
const network::mojom::SharedDictionaryAccessDetailsPtr& details() const {
return details_;
}
private:
// WebContentsObserver overrides:
void OnSharedDictionaryAccessed(
content::NavigationHandle* navigation,
const network::mojom::SharedDictionaryAccessDetails& details) override {
details_ = details.Clone();
on_accessed_callback_.Run();
}
void OnSharedDictionaryAccessed(
content::RenderFrameHost* rfh,
const network::mojom::SharedDictionaryAccessDetails& details) override {
details_ = details.Clone();
on_accessed_callback_.Run();
}
base::RepeatingClosure on_accessed_callback_;
network::mojom::SharedDictionaryAccessDetailsPtr details_;
};
std::optional<std::string> GetAvailableDictionary(
const net::test_server::HttpRequest::HeaderMap& headers) {
auto it = headers.find("available-dictionary");
return it == headers.end() ? std::nullopt
: std::make_optional(it->second);
}
void CheckSharedDictionaryUseCounter(
base::HistogramTester& histograms,
int expected_used_count_with_sbr,
int expected_used_count_with_zstd_d,
int expected_used_for_navigation_count,
int expected_used_for_main_frame_navigation_count,
int expected_used_for_sub_frame_navigation_count,
int expected_used_for_subresource_count) {
histograms.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kSharedDictionaryUsed,
expected_used_count_with_sbr + expected_used_count_with_zstd_d);
histograms.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kSharedDictionaryUsedForNavigation,
expected_used_for_navigation_count);
histograms.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kSharedDictionaryUsedForMainFrameNavigation,
expected_used_for_main_frame_navigation_count);
histograms.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kSharedDictionaryUsedForSubFrameNavigation,
expected_used_for_sub_frame_navigation_count);
histograms.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kSharedDictionaryUsedForSubresource,
expected_used_for_subresource_count);
histograms.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kSharedDictionaryUsedWithSharedBrotli,
expected_used_count_with_sbr);
histograms.ExpectBucketCount(
"Blink.UseCounter.Features",
blink::mojom::WebFeature::kSharedDictionaryUsedWithSharedZstd,
expected_used_count_with_zstd_d);
}
std::string FetchUrlScript(const GURL& url) {
return content::JsReplace(R"(
(async () => {
try {
await fetch($1);
} catch (e) {
}
})();
)",
url);
}
std::string FetchUrlWithNoCorsModeScript(const GURL& url) {
return content::JsReplace(R"(
(async () => {
try {
await fetch($1, {mode: 'no-cors'});
} catch (e) {
}
})();
)",
url);
}
} // namespace
// `ChromeSharedDictionaryBrowserTest` is required to test Chrome
// specific code such as Site Settings.
// See `SharedDictionaryBrowserTest` for content's version of tests.
class ChromeSharedDictionaryBrowserTest : public InProcessBrowserTest {
public:
ChromeSharedDictionaryBrowserTest() {
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/
{network::features::kCompressionDictionaryTransportBackend,
network::features::kCompressionDictionaryTransport,
network::features::kSharedZstd},
/*disabled_features=*/{});
embedded_test_server()->RegisterRequestHandler(
base::BindRepeating(&ChromeSharedDictionaryBrowserTest::RequestHandler,
base::Unretained(this)));
EXPECT_TRUE(embedded_test_server()->Start());
cross_origin_server_ = std::make_unique<net::EmbeddedTestServer>();
cross_origin_server()->RegisterRequestHandler(
base::BindRepeating(&ChromeSharedDictionaryBrowserTest::RequestHandler,
base::Unretained(this)));
CHECK(cross_origin_server()->Start());
}
ChromeSharedDictionaryBrowserTest(const ChromeSharedDictionaryBrowserTest&) =
delete;
ChromeSharedDictionaryBrowserTest& operator=(
const ChromeSharedDictionaryBrowserTest&) = delete;
protected:
net::EmbeddedTestServer* cross_origin_server() {
return cross_origin_server_.get();
}
std::string FetchStringInActiveContents(const GURL& url) {
return EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
content::JsReplace(R"(
(async () => {
return await (await fetch($1)).text();
})();
)",
url))
.ExtractString();
}
bool TryRegisterDictionary(const net::EmbeddedTestServer& server) {
base::RunLoop loop;
SharedDictionaryAccessObserver observer(
browser()->tab_strip_model()->GetActiveWebContents(),
loop.QuitClosure());
EXPECT_EQ(kTestDictionaryString,
FetchStringInActiveContents(server.GetURL("/dictionary")));
loop.Run();
CHECK(observer.details());
EXPECT_EQ(network::mojom::SharedDictionaryAccessDetails::Type::kWrite,
observer.details()->type);
return !observer.details()->is_blocked;
}
bool CheckDictionaryHeader(const net::EmbeddedTestServer& server,
bool expect_blocked) {
base::RunLoop loop;
SharedDictionaryAccessObserver observer(
browser()->tab_strip_model()->GetActiveWebContents(),
loop.QuitClosure());
return CheckResultOfDictionaryHeaderAndObserver(
FetchStringInActiveContents(server.GetURL("/path/check_header")), loop,
observer, expect_blocked);
}
bool CheckDictionaryHeaderByNavigation(const GURL& url, bool expect_blocked) {
base::RunLoop loop;
SharedDictionaryAccessObserver observer(
browser()->tab_strip_model()->GetActiveWebContents(),
loop.QuitClosure());
CHECK(ui_test_utils::NavigateToURL(browser(), url));
return CheckResultOfDictionaryHeaderAndObserver(
EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
"document.body.innerText")
.ExtractString(),
loop, observer, expect_blocked);
}
bool CheckDictionaryHeaderByIframeNavigation(const GURL& url,
bool expect_blocked) {
base::RunLoop loop;
SharedDictionaryAccessObserver observer(
browser()->tab_strip_model()->GetActiveWebContents(),
loop.QuitClosure());
return CheckResultOfDictionaryHeaderAndObserver(
EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
content::JsReplace(R"(
(async () => {
const iframe = document.createElement('iframe');
iframe.src = $1;
const promise =
new Promise(resolve => { iframe.addEventListener('load', resolve); });
document.body.appendChild(iframe);
await promise;
return iframe.contentDocument.body.innerText;
})()
)",
url))
.ExtractString(),
loop, observer, expect_blocked);
}
bool CheckResultOfDictionaryHeaderAndObserver(
const std::string& check_result,
base::RunLoop& loop,
SharedDictionaryAccessObserver& observer,
bool expect_blocked) {
if (check_result == "Dictionary header available") {
loop.Run();
CHECK(observer.details());
EXPECT_EQ(network::mojom::SharedDictionaryAccessDetails::Type::kRead,
observer.details()->type);
EXPECT_FALSE(observer.details()->is_blocked);
EXPECT_FALSE(expect_blocked);
return true;
}
if (expect_blocked) {
loop.Run();
CHECK(observer.details());
EXPECT_EQ(network::mojom::SharedDictionaryAccessDetails::Type::kRead,
observer.details()->type);
EXPECT_TRUE(observer.details()->is_blocked);
}
return false;
}
void WaitForDictionaryReady(net::EmbeddedTestServer& server) {
// Need the polling because writing the dictionary to the storage and the
// database may take time.
while (!CheckDictionaryHeader(server, /*expect_blocked=*/false)) {
base::PlatformThread::Sleep(base::Milliseconds(5));
}
}
int GetSiteDataCount(base::Time begin_time, base::Time end_time) {
base::test::TestFuture<int> result;
auto* helper = new SiteDataCountingHelper(browser()->profile(), begin_time,
end_time, result.GetCallback());
helper->CountAndDestroySelfWhenFinished();
return result.Get();
;
}
private:
std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
const net::test_server::HttpRequest& request) {
auto response = std::make_unique<net::test_server::BasicHttpResponse>();
response->set_code(net::HTTP_OK);
response->AddCustomHeader("access-control-allow-origin", "*");
if (request.relative_url == "/dictionary") {
response->set_content_type("text/plain");
response->AddCustomHeader("use-as-dictionary", "match=\"/path/*\"");
response->AddCustomHeader("cache-control", "max-age=3600");
response->set_content(kTestDictionaryString);
return response;
} else if (request.relative_url == "/path/check_header") {
response->set_content_type("text/plain");
std::optional<std::string> dict_hash =
GetAvailableDictionary(request.headers);
response->set_content(dict_hash ? "Dictionary header available"
: "Dictionary header not available");
return response;
} else if (request.relative_url == "/path/check_header1.html" ||
request.relative_url == "/path/check_header2.html") {
response->set_content_type("text/html");
std::optional<std::string> dict_hash =
GetAvailableDictionary(request.headers);
response->set_content(dict_hash ? "Dictionary header available"
: "Dictionary header not available");
return response;
} else if (request.relative_url == "/path/brotli_compressed") {
CHECK(GetAvailableDictionary(request.headers));
response->set_content_type("text/html");
response->AddCustomHeader("content-encoding",
network::GetSharedBrotliContentEncodingName());
response->AddCustomHeader("content-dictionary",
kTestDictionaryHashBase64);
response->set_content(kBrotliCompressedDataString);
return response;
} else if (request.relative_url == "/path/zstd_compressed") {
CHECK(GetAvailableDictionary(request.headers));
response->set_content_type("text/html");
response->AddCustomHeader("content-encoding",
network::GetSharedZstdContentEncodingName());
response->AddCustomHeader("content-dictionary",
kTestDictionaryHashBase64);
response->set_content(kZstdCompressedDataString);
return response;
}
return nullptr;
}
std::unique_ptr<net::EmbeddedTestServer> cross_origin_server_;
base::test::ScopedFeatureList scoped_feature_list_;
};
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest, BlockWriting) {
content_settings::CookieSettings* settings =
CookieSettingsFactory::GetForProfile(browser()->profile()).get();
settings->SetCookieSetting(embedded_test_server()->GetURL("/"),
CONTENT_SETTING_BLOCK);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_FALSE(TryRegisterDictionary(*embedded_test_server()));
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest,
BlockWritingCrossOrigin) {
content_settings::CookieSettings* settings =
CookieSettingsFactory::GetForProfile(browser()->profile()).get();
settings->SetCookieSetting(cross_origin_server()->GetURL("/"),
CONTENT_SETTING_BLOCK);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_FALSE(TryRegisterDictionary(*cross_origin_server()));
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest, BlockReading) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_TRUE(TryRegisterDictionary(*embedded_test_server()));
WaitForDictionaryReady(*embedded_test_server());
content_settings::CookieSettings* settings =
CookieSettingsFactory::GetForProfile(browser()->profile()).get();
settings->SetCookieSetting(embedded_test_server()->GetURL("/"),
CONTENT_SETTING_BLOCK);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html?")));
EXPECT_FALSE(
CheckDictionaryHeader(*embedded_test_server(), /*expect_blocked=*/true));
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest,
BlockReadingCrossOrigin) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_TRUE(TryRegisterDictionary(*cross_origin_server()));
WaitForDictionaryReady(*cross_origin_server());
content_settings::CookieSettings* settings =
CookieSettingsFactory::GetForProfile(browser()->profile()).get();
settings->SetCookieSetting(cross_origin_server()->GetURL("/"),
CONTENT_SETTING_BLOCK);
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html?")));
EXPECT_FALSE(
CheckDictionaryHeader(*cross_origin_server(), /*expect_blocked=*/true));
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest,
BlockReadingWhileNavigation) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_TRUE(TryRegisterDictionary(*embedded_test_server()));
WaitForDictionaryReady(*embedded_test_server());
EXPECT_TRUE(CheckDictionaryHeaderByNavigation(
embedded_test_server()->GetURL("/path/check_header1.html"),
/*expect_blocked=*/false));
content_settings::CookieSettings* settings =
CookieSettingsFactory::GetForProfile(browser()->profile()).get();
settings->SetCookieSetting(embedded_test_server()->GetURL("/"),
CONTENT_SETTING_BLOCK);
EXPECT_FALSE(CheckDictionaryHeaderByNavigation(
embedded_test_server()->GetURL("/path/check_header2.html"),
/*expect_blocked=*/true));
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest,
BlockReadingWhileIframeNavigation) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_TRUE(TryRegisterDictionary(*embedded_test_server()));
WaitForDictionaryReady(*embedded_test_server());
EXPECT_TRUE(CheckDictionaryHeaderByIframeNavigation(
embedded_test_server()->GetURL("/path/check_header1.html"),
/*expect_blocked=*/false));
content_settings::CookieSettings* settings =
CookieSettingsFactory::GetForProfile(browser()->profile()).get();
settings->SetCookieSetting(embedded_test_server()->GetURL("/"),
CONTENT_SETTING_BLOCK);
EXPECT_FALSE(CheckDictionaryHeaderByIframeNavigation(
embedded_test_server()->GetURL("/path/check_header2.html"),
/*expect_blocked=*/true));
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest,
UseCounterMainFrameNavigation) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_TRUE(TryRegisterDictionary(*embedded_test_server()));
WaitForDictionaryReady(*embedded_test_server());
// Navigate away in order to flush use counters.
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
base::HistogramTester histograms;
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/path/brotli_compressed")));
EXPECT_EQ(kCompressedDataOriginalString,
EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
"document.body.innerText")
.ExtractString());
// Navigate away in order to flush use counters.
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
CheckSharedDictionaryUseCounter(
histograms,
/*expected_used_count_with_sbr=*/1,
/*expected_used_count_with_zstd_d=*/0,
/*expected_used_for_navigation_count=*/1,
/*expected_used_for_main_frame_navigation_count=*/1,
/*expected_used_for_sub_frame_navigation_count=*/0,
/*expected_used_for_subresource_count=*/0);
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest,
UseCounterSubFrameNavigation) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_TRUE(TryRegisterDictionary(*embedded_test_server()));
WaitForDictionaryReady(*embedded_test_server());
// Navigate away in order to flush use counters.
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
base::HistogramTester histograms;
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_EQ(kCompressedDataOriginalString,
EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
R"(
(async () => {
const iframe = document.createElement('iframe');
iframe.src = '/path/brotli_compressed';
const promise =
new Promise(resolve => { iframe.addEventListener('load', resolve); });
document.body.appendChild(iframe);
await promise;
return iframe.contentDocument.body.innerText;
})()
)")
.ExtractString());
// Navigate away in order to flush use counters.
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
CheckSharedDictionaryUseCounter(
histograms,
/*expected_used_count_with_sbr=*/1,
/*expected_used_count_with_zstd_d=*/0,
/*expected_used_for_navigation_count=*/1,
/*expected_used_for_main_frame_navigation_count=*/0,
/*expected_used_for_sub_frame_navigation_count=*/1,
/*expected_used_for_subresource_count=*/0);
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest,
UseCounterSubresource) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_TRUE(TryRegisterDictionary(*embedded_test_server()));
WaitForDictionaryReady(*embedded_test_server());
// Navigate away in order to flush use counters.
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
base::HistogramTester histograms;
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_EQ(kCompressedDataOriginalString,
EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
R"(
(async () => {
return await (await fetch('path/brotli_compressed')).text();
})()
)")
.ExtractString());
// Navigate away in order to flush use counters.
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
CheckSharedDictionaryUseCounter(
histograms,
/*expected_used_count_with_sbr=*/1,
/*expected_used_count_with_zstd_d=*/0,
/*expected_used_for_navigation_count=*/0,
/*expected_used_for_main_frame_navigation_count=*/0,
/*expected_used_for_sub_frame_navigation_count=*/0,
/*expected_used_for_subresource_count=*/1);
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest,
UseCounterZstdMainFrameNavigation) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_TRUE(TryRegisterDictionary(*embedded_test_server()));
WaitForDictionaryReady(*embedded_test_server());
// Navigate away in order to flush use counters.
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
base::HistogramTester histograms;
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/path/zstd_compressed")));
EXPECT_EQ(kCompressedDataOriginalString,
EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
"document.body.innerText")
.ExtractString());
// Navigate away in order to flush use counters.
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
CheckSharedDictionaryUseCounter(
histograms,
/*expected_used_count_with_sbr=*/0,
/*expected_used_count_with_zstd_d=*/1,
/*expected_used_for_navigation_count=*/1,
/*expected_used_for_main_frame_navigation_count=*/1,
/*expected_used_for_sub_frame_navigation_count=*/0,
/*expected_used_for_subresource_count=*/0);
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest,
UseCounterZstdSubresource) {
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_TRUE(TryRegisterDictionary(*embedded_test_server()));
WaitForDictionaryReady(*embedded_test_server());
// Navigate away in order to flush use counters.
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
base::HistogramTester histograms;
EXPECT_TRUE(ui_test_utils::NavigateToURL(
browser(), embedded_test_server()->GetURL("/title1.html")));
EXPECT_EQ(kCompressedDataOriginalString,
EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
R"(
(async () => {
return await (await fetch('path/zstd_compressed')).text();
})()
)")
.ExtractString());
// Navigate away in order to flush use counters.
EXPECT_TRUE(
ui_test_utils::NavigateToURL(browser(), GURL(url::kAboutBlankURL)));
CheckSharedDictionaryUseCounter(
histograms,
/*expected_used_count_with_sbr=*/0,
/*expected_used_count_with_zstd_d=*/1,
/*expected_used_for_navigation_count=*/0,
/*expected_used_for_main_frame_navigation_count=*/0,
/*expected_used_for_sub_frame_navigation_count=*/0,
/*expected_used_for_subresource_count=*/1);
}
IN_PROC_BROWSER_TEST_F(ChromeSharedDictionaryBrowserTest, SiteDataCount) {
const GURL url = embedded_test_server()->GetURL("/title1.html");
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
EXPECT_TRUE(TryRegisterDictionary(*embedded_test_server()));
WaitForDictionaryReady(*embedded_test_server());
base::RunLoop loop;
std::vector<network::mojom::SharedDictionaryInfoPtr> dictionaries;
browser()
->profile()
->GetDefaultStoragePartition()
->GetNetworkContext()
->GetSharedDictionaryInfo(
net::SharedDictionaryIsolationKey(url::Origin::Create(url),
net::SchemefulSite(url)),
base::BindLambdaForTesting(
[&](std::vector<network::mojom::SharedDictionaryInfoPtr> result) {
EXPECT_FALSE(result.empty());
dictionaries = std::move(result);
loop.Quit();
}));
loop.Run();
ASSERT_EQ(dictionaries.size(), 1u);
ASSERT_TRUE(dictionaries[0]);
const base::Time response_time = dictionaries[0]->response_time;
EXPECT_EQ(0,
GetSiteDataCount(response_time - base::Seconds(1), response_time));
EXPECT_EQ(1,
GetSiteDataCount(response_time, response_time + base::Seconds(1)));
}
class SharedDictionaryDevToolsBrowserTest
: public InProcessBrowserTest,
public content::TestDevToolsProtocolClient {
public:
explicit SharedDictionaryDevToolsBrowserTest(bool enable_feature = true) {
if (enable_feature) {
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/
{network::features::kCompressionDictionaryTransportBackend,
network::features::kCompressionDictionaryTransport},
/*disabled_features=*/
{});
} else {
scoped_feature_list_.InitWithFeatures(
/*enabled_features=*/
{network::features::kCompressionDictionaryTransportBackend},
/*disabled_features=*/
{network::features::kCompressionDictionaryTransport});
}
}
~SharedDictionaryDevToolsBrowserTest() override = default;
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
host_resolver()->AddRule("*", "127.0.0.1");
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
embedded_https_test_server().ServeFilesFromSourceDirectory(
"content/test/data");
}
void TearDownOnMainThread() override {
DetachProtocolClient();
InProcessBrowserTest::TearDownOnMainThread();
}
protected:
content::RenderFrameHost* GetPrimaryMainFrame() {
return browser()
->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame();
}
void NavigateAndEnableAudits(const GURL& url) {
EXPECT_TRUE(NavigateToURL(
browser()->tab_strip_model()->GetActiveWebContents(), url));
AttachToWebContents(browser()->tab_strip_model()->GetActiveWebContents());
SendCommandSync("Network.enable");
SendCommandSync("Audits.enable");
}
base::Value::Dict WaitForSharedDictionaryIssueAdded(
const std::string& expected_error_type) {
auto matcher = [](const base::Value::Dict& params) {
const std::string* maybe_issue_code =
params.FindStringByDottedPath("issue.code");
return maybe_issue_code && *maybe_issue_code == "SharedDictionaryIssue";
};
base::Value::Dict notification = WaitForMatchingNotification(
"Audits.issueAdded", base::BindRepeating(matcher));
EXPECT_EQ(*notification.FindStringByDottedPath("issue.code"),
"SharedDictionaryIssue");
const std::string* maybe_error_type = notification.FindStringByDottedPath(
"issue.details.sharedDictionaryIssueDetails.sharedDictionaryError");
CHECK(maybe_error_type);
EXPECT_EQ(*maybe_error_type, expected_error_type);
return notification;
}
void RunCustomHeaderTest(
const std::string& expected_erorr_type,
const std::string& use_as_dictionary_header,
const std::string& cache_control_header = "max-age=3600") {
embedded_https_test_server().RegisterRequestHandler(
base::BindLambdaForTesting(
[&](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
if (request.relative_url == "/test.dict") {
auto response =
std::make_unique<net::test_server::BasicHttpResponse>();
response->AddCustomHeader("use-as-dictionary",
use_as_dictionary_header);
response->AddCustomHeader("cache-control",
cache_control_header);
response->set_content("dict");
return response;
}
return nullptr;
}));
auto handle = embedded_https_test_server().StartAndReturnHandle();
ASSERT_TRUE(handle);
NavigateAndEnableAudits(
embedded_https_test_server().GetURL("/shared_dictionary/blank.html"));
EXPECT_TRUE(ExecJs(
GetPrimaryMainFrame(),
FetchUrlScript(embedded_https_test_server().GetURL("/test.dict"))));
WaitForSharedDictionaryIssueAdded(expected_erorr_type);
}
std::vector<net::SharedDictionaryUsageInfo> GetSharedDictionaryUsageInfo() {
base::test::TestFuture<const std::vector<net::SharedDictionaryUsageInfo>&>
result;
browser()
->profile()
->GetDefaultStoragePartition()
->GetNetworkContext()
->GetSharedDictionaryUsageInfo(result.GetCallback());
return result.Get();
}
void WaitUntilDictionaryRegistered() {
while (GetSharedDictionaryUsageInfo().empty()) {
base::PlatformThread::Sleep(base::Milliseconds(10));
}
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
class DevToolsSharedDictionaryFeatureDisabledBrowserTest
: public SharedDictionaryDevToolsBrowserTest {
public:
DevToolsSharedDictionaryFeatureDisabledBrowserTest()
: SharedDictionaryDevToolsBrowserTest(/*enable_feature=*/false) {}
~DevToolsSharedDictionaryFeatureDisabledBrowserTest() override = default;
};
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
UseErrorCrossOriginNoCorsRequest) {
const std::string kHostName = "www.example.com";
const std::string kCrossOriginHostName = "other.example.com";
embedded_https_test_server().SetCertHostnames(
{kHostName, kCrossOriginHostName});
ASSERT_TRUE(embedded_https_test_server().Start());
NavigateAndEnableAudits(embedded_https_test_server().GetURL(
kHostName, "/shared_dictionary/blank.html"));
content::RenderFrameHost* rfh = GetPrimaryMainFrame();
EXPECT_TRUE(
ExecJs(rfh, FetchUrlScript(embedded_https_test_server().GetURL(
kCrossOriginHostName, "/shared_dictionary/test.dict"))));
WaitUntilDictionaryRegistered();
EXPECT_TRUE(ExecJs(
rfh, FetchUrlWithNoCorsModeScript(embedded_https_test_server().GetURL(
kCrossOriginHostName, "/shared_dictionary/path/target"))));
WaitForSharedDictionaryIssueAdded("UseErrorCrossOriginNoCorsRequest");
}
// Can't cause the dictionary load failure by deletaing the disk cache directory
// on Windows.
#if !BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
UseErrorDictionaryLoadFailure) {
ASSERT_TRUE(embedded_test_server()->Start());
NavigateAndEnableAudits(
embedded_test_server()->GetURL("/shared_dictionary/blank.html"));
content::RenderFrameHost* rfh = GetPrimaryMainFrame();
EXPECT_TRUE(ExecJs(rfh, FetchUrlScript(embedded_test_server()->GetURL(
"/shared_dictionary/test.dict"))));
WaitUntilDictionaryRegistered();
{
base::ScopedAllowBlockingForTesting allow_blocking;
EXPECT_TRUE(base::DeletePathRecursively(
browser()->profile()->GetDefaultStoragePartition()->GetPath().Append(
FILE_PATH_LITERAL("Shared Dictionary/cache/"))));
}
EXPECT_TRUE(ExecJs(rfh, FetchUrlScript(embedded_test_server()->GetURL(
"/shared_dictionary/path/compressed.data"))));
WaitForSharedDictionaryIssueAdded("UseErrorDictionaryLoadFailure");
}
#endif // !BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
UseErrorMatchingDictionaryNotUsed) {
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
[](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
if (request.relative_url == "/shared_dictionary/path/target") {
auto response =
std::make_unique<net::test_server::BasicHttpResponse>();
response->set_content("data");
return response;
}
return nullptr;
}));
ASSERT_TRUE(embedded_test_server()->Start());
NavigateAndEnableAudits(
embedded_test_server()->GetURL("/shared_dictionary/blank.html"));
content::RenderFrameHost* rfh = GetPrimaryMainFrame();
EXPECT_TRUE(ExecJs(rfh, FetchUrlScript(embedded_test_server()->GetURL(
"/shared_dictionary/test.dict"))));
WaitUntilDictionaryRegistered();
EXPECT_TRUE(ExecJs(rfh, FetchUrlScript(embedded_test_server()->GetURL(
"/shared_dictionary/path/target"))));
WaitForSharedDictionaryIssueAdded("UseErrorMatchingDictionaryNotUsed");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
UseErrorUnexpectedContentDictionaryHeader) {
embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
[](const net::test_server::HttpRequest& request)
-> std::unique_ptr<net::test_server::HttpResponse> {
if (request.relative_url == "/shared_dictionary/path/target") {
auto response =
std::make_unique<net::test_server::BasicHttpResponse>();
response->AddCustomHeader("Content-Encoding", "br-d");
response->AddCustomHeader("Content-Dictionary",
"Invalid Content-Dictionary");
return response;
}
return nullptr;
}));
ASSERT_TRUE(embedded_test_server()->Start());
NavigateAndEnableAudits(
embedded_test_server()->GetURL("/shared_dictionary/blank.html"));
content::RenderFrameHost* rfh = GetPrimaryMainFrame();
EXPECT_TRUE(ExecJs(rfh, FetchUrlScript(embedded_test_server()->GetURL(
"/shared_dictionary/test.dict"))));
WaitUntilDictionaryRegistered();
EXPECT_TRUE(ExecJs(rfh, FetchUrlScript(embedded_test_server()->GetURL(
"/shared_dictionary/path/target"))));
WaitForSharedDictionaryIssueAdded(
"UseErrorUnexpectedContentDictionaryHeader");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorCossOriginNoCorsRequest) {
const std::string kCrossOriginHostName = "example.com";
ASSERT_TRUE(embedded_test_server()->Start());
NavigateAndEnableAudits(
embedded_test_server()->GetURL("/shared_dictionary/blank.html"));
EXPECT_TRUE(
ExecJs(GetPrimaryMainFrame(),
FetchUrlWithNoCorsModeScript(embedded_test_server()->GetURL(
kCrossOriginHostName, "/shared_dictionary/test.dict"))));
WaitForSharedDictionaryIssueAdded("WriteErrorCossOriginNoCorsRequest");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorDisallowedBySettings) {
const std::string kHostName = "www.example.com";
const std::string kCrossOriginHostName = "other.example.com";
embedded_https_test_server().SetCertHostnames(
{kHostName, kCrossOriginHostName});
ASSERT_TRUE(embedded_https_test_server().Start());
content_settings::CookieSettings* settings =
CookieSettingsFactory::GetForProfile(browser()->profile()).get();
settings->SetCookieSetting(
embedded_https_test_server().GetURL(kCrossOriginHostName, "/"),
CONTENT_SETTING_BLOCK);
NavigateAndEnableAudits(embedded_https_test_server().GetURL(
kHostName, "/shared_dictionary/blank.html"));
EXPECT_TRUE(
ExecJs(browser()
->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame(),
FetchUrlScript(embedded_https_test_server().GetURL(
kCrossOriginHostName, "/shared_dictionary/test.dict"))));
WaitForSharedDictionaryIssueAdded("WriteErrorDisallowedBySettings");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorExpiredResponse) {
RunCustomHeaderTest("WriteErrorExpiredResponse",
/*use_as_dictionary_header=*/"match=\"/test/*\"",
/*cache_control_header=*/"no-store");
}
IN_PROC_BROWSER_TEST_F(DevToolsSharedDictionaryFeatureDisabledBrowserTest,
WriteErrorFeatureDisabled) {
ASSERT_TRUE(embedded_test_server()->Start());
NavigateAndEnableAudits(
embedded_test_server()->GetURL("/shared_dictionary/blank.html"));
EXPECT_TRUE(ExecJs(GetPrimaryMainFrame(),
FetchUrlScript(embedded_test_server()->GetURL(
"/shared_dictionary/test.dict"))));
WaitForSharedDictionaryIssueAdded("WriteErrorFeatureDisabled");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorInvalidMatchField) {
RunCustomHeaderTest("WriteErrorInvalidMatchField", "match=\"(\"");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorInvalidStructuredHeader) {
RunCustomHeaderTest("WriteErrorInvalidStructuredHeader", "match=\"");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorNavigationRequest) {
ASSERT_TRUE(embedded_test_server()->Start());
NavigateAndEnableAudits(
embedded_test_server()->GetURL("/shared_dictionary/blank.html"));
EXPECT_TRUE(NavigateToURL(
browser()->tab_strip_model()->GetActiveWebContents(),
embedded_test_server()->GetURL("/shared_dictionary/test_dict.html")));
WaitForSharedDictionaryIssueAdded("WriteErrorNavigationRequest");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorNoMatchField) {
RunCustomHeaderTest("WriteErrorNoMatchField", "id=\"dict_id\"");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorNonListMatchDestField) {
RunCustomHeaderTest("WriteErrorNonListMatchDestField",
"match=\"/test/*\", match-dest=document");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorNonSecureContext) {
const std::string kHostName = "example.com";
ASSERT_TRUE(embedded_test_server()->Start());
NavigateAndEnableAudits(embedded_test_server()->GetURL(
kHostName, "/shared_dictionary/blank.html"));
EXPECT_TRUE(ExecJs(GetPrimaryMainFrame(),
FetchUrlScript(embedded_test_server()->GetURL(
kHostName, "/shared_dictionary/test.dict"))));
WaitForSharedDictionaryIssueAdded("WriteErrorNonSecureContext");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorNonStringIdField) {
RunCustomHeaderTest("WriteErrorNonStringIdField",
"match=\"/test/*\", id=dict_id");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorNonStringInMatchDestList) {
RunCustomHeaderTest("WriteErrorNonStringInMatchDestList",
"match=\"/test/*\", match-dest=(document)");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorNonStringMatchField) {
RunCustomHeaderTest("WriteErrorNonStringMatchField", "match=test");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorNonTokenTypeField) {
RunCustomHeaderTest("WriteErrorNonTokenTypeField",
"match=\"/test/*\", type=\"raw\"");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorRequestAborted) {
auto dictionary_response =
std::make_unique<net::test_server::ControllableHttpResponse>(
embedded_test_server(), "/test.dict");
ASSERT_TRUE(embedded_test_server()->Start());
NavigateAndEnableAudits(
embedded_test_server()->GetURL("/shared_dictionary/blank.html"));
content::RenderFrameHost* rfh = GetPrimaryMainFrame();
EXPECT_TRUE(ExecJs(
rfh, content::JsReplace(R"(
(() => {
const controller = new AbortController();
window.FETCH_ABORT_CONTROLLER = controller;
window.FETCH_RESULT = fetch($1, {signal: controller.signal});
})();
)",
embedded_test_server()->GetURL("/test.dict"))));
dictionary_response->WaitForRequest();
dictionary_response->Send(
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Use-As-Dictionary: match=\"/test/*\"\r\n"
"Cache-Control: max-age=3600\r\n"
"\r\n"
"dict");
EXPECT_TRUE(ExecJs(rfh, R"(
(async () => {
try {
const response = await window.FETCH_RESULT;
const reader = response.body.getReader();
const result = await reader.read();
window.FETCH_ABORT_CONTROLLER.abort();
} catch (e) {
}
})();
)"));
WaitForSharedDictionaryIssueAdded("WriteErrorRequestAborted");
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorTooLongIdField) {
RunCustomHeaderTest(
"WriteErrorTooLongIdField",
base::StrCat({"match=\"/test/*\", id=\"", std::string(1025, 'a'), "\""}));
}
IN_PROC_BROWSER_TEST_F(SharedDictionaryDevToolsBrowserTest,
WriteErrorUnsupportedType) {
RunCustomHeaderTest("WriteErrorUnsupportedType",
"match=\"/test/*\", type=unsupported");
}