0

Stop checking "expires" option in Use-As-Dictionary response header

Currently Chromium is using "expires" option in Use-As-Dictionary
response  header for the dictionary lifetime. The option name has been
renamed to "ttl" in the spec discussed in IETF HTTPWG.

And the new change in the spac [1] removed this, and the lifetime should
be calculated from the response's freshness [2].

After this CL, when V2 backend is enabled, Chromium will not check
"expires" option, and it will check the response's freshness.

[1]: https://github.com/httpwg/http-extensions/pull/2709
[2]: https://datatracker.ietf.org/doc/html/rfc9111#name-freshness

Bug: 1413922
Change-Id: Idd2cdeb526099a1f2854ea0094541cedc0a64af8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5227360
Commit-Queue: Tsuyoshi Horo <horo@chromium.org>
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1251343}
This commit is contained in:
Tsuyoshi Horo
2024-01-24 11:47:09 +00:00
committed by Chromium LUCI CQ
parent 14be928e11
commit 47afd65f83
13 changed files with 427 additions and 158 deletions

@ -336,6 +336,7 @@ class ChromeSharedDictionaryBrowserTest
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") {

@ -1176,6 +1176,7 @@ IN_PROC_BROWSER_TEST_P(SharedDictionaryFeatureStateBrowserTest,
// We want to check the behavior of kBackendOnly and kFullyEnabled.
return;
}
base::Time test_start_time = base::Time::Now();
RunWriteDictionaryTest(
FetchType::kLinkRelDictionary,
GURL("https://shared-dictionary.test/blank.html?ot=enabled"),
@ -1190,10 +1191,26 @@ IN_PROC_BROWSER_TEST_P(SharedDictionaryFeatureStateBrowserTest,
base::BindLambdaForTesting(
[&](std::vector<network::mojom::SharedDictionaryInfoPtr> result) {
ASSERT_EQ(1u, result.size());
EXPECT_EQ(GetFeatureState() == FeatureState::kBackendOnly
? base::Days(30)
: base::Seconds(31536000),
result[0]->expiration);
switch (GetVersion()) {
case network::features::
CompressionDictionaryTransportBackendVersion::kV1:
EXPECT_EQ(GetFeatureState() == FeatureState::kBackendOnly
? base::Days(30)
: base::Seconds(31536000),
result[0]->expiration);
break;
case network::features::
CompressionDictionaryTransportBackendVersion::kV2:
// The dictionary response was treated as somewhat aged
// (response time - request time). So the expiration is a bit
// smaller than 3600 seconds which is set in the Cache-Control
// header's max-age directive.
EXPECT_LT(result[0]->expiration, base::Seconds(3600));
EXPECT_GT(result[0]->expiration,
base::Seconds(3600) -
(result[0]->response_time - test_start_time));
break;
}
loop.Quit();
}));
loop.Run();

@ -1,3 +1,4 @@
HTTP/1.1 200 OK
Use-As-Dictionary: match="/shared_dictionary/path/*"
Access-Control-Allow-Origin: *
Cache-Control: max-age=3600

@ -1,4 +1,5 @@
HTTP/1.1 200 OK
Use-As-Dictionary: match="/shared_dictionary/path/*"
Access-Control-Allow-Origin: *
Cache-Control: max-age=3600
Content-Type: text/html

@ -1,2 +1,3 @@
HTTP/1.1 200 OK
Use-As-Dictionary: match="/shared_dictionary/path/*"
Cache-Control: max-age=3600

@ -204,8 +204,9 @@ construction to catch up the following spec changes:
- Stauts: Not yet implemented.
- Added support for a server-provided dictionary id
- Stauts: Not yet implemented.
- Updated the default dictionary ttl to 14 days since last fetched
- Stauts: Not yet implemented.
- Stop using "expires" value of "Use-As-Dictionary" header, and use the cache
expiration time calculated from the response's freshness instead.
- Stauts: Removed by https://crrev.com/c/5227360.
- Removed support for hash negotiation and force use of sha-256
- Stauts: Removed by https://crrev.com/c/5223985.
- Added the dictionary hash to the compressed response

@ -617,8 +617,8 @@ void CorsURLLoader::OnReceiveResponse(
// registration.
CHECK_NE(mojom::FetchResponseType::kOpaque, response_tainting_);
auto writer = shared_dictionary_storage_->MaybeCreateWriter(
request_.url, response_head->response_time, *response_head->headers,
response_head->was_fetched_via_cache,
request_.url, response_head->request_time, response_head->response_time,
*response_head->headers, response_head->was_fetched_via_cache,
base::BindOnce(
&SharedDictionaryAccessChecker::CheckAllowedToWriteAndReport,
std::make_unique<SharedDictionaryAccessChecker>(

@ -100,6 +100,7 @@ class CorsURLLoaderSharedDictionaryTest : public CorsURLLoaderTestBase {
extra_headers.emplace_back(
network::shared_dictionary::kUseAsDictionaryHeaderName,
"match=\"/path*\"");
extra_headers.emplace_back("cache-control", "max-age=2592000");
NotifyLoaderClientOnReceiveResponse(extra_headers,
std::move(consumer_handle_));
}

@ -15,6 +15,7 @@ namespace network::shared_dictionary {
// The default value (1 year) of expiration time in "use-as-dictionary"
// HTTP header.
// This will not be used when V2 backend is enabled.
constexpr base::TimeDelta kDefaultExpiration = base::Seconds(31536000);
// The max expiration time (30 days) for Origin Trial. This is used when
@ -55,6 +56,7 @@ extern const char kContentDictionaryHeaderName[];
COMPONENT_EXPORT(NETWORK_SERVICE) extern const char kOptionNameMatch[];
// The dictionary option name of "expires".
// This will not be used when V2 backend is enabled.
COMPONENT_EXPORT(NETWORK_SERVICE) extern const char kOptionNameExpires[];
// The dictionary option name of "type".

@ -4,6 +4,7 @@
#include "services/network/shared_dictionary/shared_dictionary_manager_on_disk.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
@ -13,6 +14,7 @@
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_file_util.h"
#include "base/time/time.h"
@ -27,6 +29,7 @@
#include "net/disk_cache/disk_cache_test_util.h"
#include "net/extras/shared_dictionary/shared_dictionary_info.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/cpp/features.h"
#include "services/network/shared_dictionary/shared_dictionary.h"
#include "services/network/shared_dictionary/shared_dictionary_constants.h"
#include "services/network/shared_dictionary/shared_dictionary_disk_cache.h"
@ -54,6 +57,10 @@ const net::SchemefulSite kSite(kUrl);
const std::string kTestData1 = "Hello world";
const std::string kTestData2 = "Bonjour le monde";
// Default cache control header for dictionary entries which expires in 30 days.
const std::string kDefaultCacheControlHeader =
"cache-control: max-age=2592000\n";
const int kCurrentVersionNumber = 1;
base::OnceCallback<bool()> DummyAccessAllowedCheckCallback() {
@ -67,10 +74,11 @@ void WriteDictionary(SharedDictionaryStorage* storage,
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"/", match, "\"\n\n"}));
": match=\"/", match, "\"\n", kDefaultCacheControlHeader, "\n"}));
ASSERT_TRUE(headers);
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
dictionary_url, base::Time::Now(), *headers,
dictionary_url, /*request_time=*/base::Time::Now(),
/*response_time=*/base::Time::Now(), *headers,
/*was_fetched_via_cache=*/false, DummyAccessAllowedCheckCallback());
ASSERT_TRUE(writer);
writer->Append(data.c_str(), data.size());
@ -85,10 +93,13 @@ void WriteDictionaryWithExpiry(SharedDictionaryStorage* storage,
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"/", match,
"\", expires=", base::NumberToString(expires.InSeconds()), "\n\n"}));
"\", expires=", base::NumberToString(expires.InSeconds()), "\n",
"cache-control: max-age=", base::NumberToString(expires.InSeconds()),
"\n\n"}));
ASSERT_TRUE(headers);
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
dictionary_url, base::Time::Now(), *headers,
dictionary_url, /*request_time=*/base::Time::Now(),
/*response_time=*/base::Time::Now(), *headers,
/*was_fetched_via_cache=*/false, DummyAccessAllowedCheckCallback());
ASSERT_TRUE(writer);
writer->Append(data.c_str(), data.size());
@ -163,9 +174,20 @@ base::UnguessableToken GetDiskCacheKeyTokenOfFirstDictionary(
} // namespace
class SharedDictionaryManagerOnDiskTest : public ::testing::Test {
class SharedDictionaryManagerOnDiskTest
: public ::testing::Test,
public testing::WithParamInterface<
features::CompressionDictionaryTransportBackendVersion> {
public:
SharedDictionaryManagerOnDiskTest() = default;
SharedDictionaryManagerOnDiskTest() {
std::vector<base::test::FeatureRefAndParams> enabled_features;
enabled_features.emplace_back(base::test::FeatureRefAndParams(
features::kCompressionDictionaryTransportBackend,
{{features::kCompressionDictionaryTransportBackendVersion.name,
features::kCompressionDictionaryTransportBackendVersion.GetName(
GetVersion())}}));
scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features, {});
}
~SharedDictionaryManagerOnDiskTest() override = default;
SharedDictionaryManagerOnDiskTest(const SharedDictionaryManagerOnDiskTest&) =
@ -182,6 +204,9 @@ class SharedDictionaryManagerOnDiskTest : public ::testing::Test {
void TearDown() override { FlushCacheTasks(); }
protected:
features::CompressionDictionaryTransportBackendVersion GetVersion() const {
return GetParam();
}
std::unique_ptr<SharedDictionaryManager> CreateSharedDictionaryManager(
uint64_t cache_max_size = 0,
uint64_t cache_max_count =
@ -242,9 +267,26 @@ class SharedDictionaryManagerOnDiskTest : public ::testing::Test {
// `file_permissions_restorer_` must be below `tmp_directory_` to restore the
// file permission correctly.
std::unique_ptr<base::FilePermissionRestorer> file_permissions_restorer_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(SharedDictionaryManagerOnDiskTest, ReusingRefCountedSharedDictionary) {
INSTANTIATE_TEST_SUITE_P(
All,
SharedDictionaryManagerOnDiskTest,
testing::ValuesIn(
{features::CompressionDictionaryTransportBackendVersion::kV1,
features::CompressionDictionaryTransportBackendVersion::kV2}),
[](const testing::TestParamInfo<
features::CompressionDictionaryTransportBackendVersion>& info) {
switch (info.param) {
case features::CompressionDictionaryTransportBackendVersion::kV1:
return "V1";
case features::CompressionDictionaryTransportBackendVersion::kV2:
return "V2";
}
});
TEST_P(SharedDictionaryManagerOnDiskTest, ReusingRefCountedSharedDictionary) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
@ -287,7 +329,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, ReusingRefCountedSharedDictionary) {
dict1->size()));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
MaybeCreateWriterAfterManagerDeleted) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
@ -302,17 +344,18 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"/testfile*\"\n\n"}));
": match=\"/testfile*\"\n", kDefaultCacheControlHeader, "\n"}));
ASSERT_TRUE(headers);
// MaybeCreateWriter() must return nullptr, after `manager` was deleted.
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
GURL("https://origin.test/dict"), base::Time::Now(), *headers,
GURL("https://origin.test/dict"), /*request_time=*/base::Time::Now(),
/*response_time=*/base::Time::Now(), *headers,
/*was_fetched_via_cache=*/false, DummyAccessAllowedCheckCallback());
EXPECT_FALSE(writer);
}
TEST_F(SharedDictionaryManagerOnDiskTest, GetDictionaryAfterManagerDeleted) {
TEST_P(SharedDictionaryManagerOnDiskTest, GetDictionaryAfterManagerDeleted) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
@ -329,7 +372,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, GetDictionaryAfterManagerDeleted) {
EXPECT_FALSE(dict);
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
DictionaryWrittenInDiskCacheAfterManagerDeleted) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
@ -347,7 +390,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
FlushCacheTasks();
}
TEST_F(SharedDictionaryManagerOnDiskTest, OverridingDictionary) {
TEST_P(SharedDictionaryManagerOnDiskTest, OverridingDictionary) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
@ -419,7 +462,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, OverridingDictionary) {
dict1->size()));
}
TEST_F(SharedDictionaryManagerOnDiskTest, MultipleDictionaries) {
TEST_P(SharedDictionaryManagerOnDiskTest, MultipleDictionaries) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -502,7 +545,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, MultipleDictionaries) {
dict2->size()));
}
TEST_F(SharedDictionaryManagerOnDiskTest, GetDictionary) {
TEST_P(SharedDictionaryManagerOnDiskTest, GetDictionary) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -556,7 +599,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, GetDictionary) {
// Test that corruptted disk cache doesn't cause crash.
// CorruptDiskCache() doesn't work on Fuchsia. So disabling the following tests
// on Fuchsia.
TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDiskCacheAndWriteData) {
TEST_P(SharedDictionaryManagerOnDiskTest, CorruptedDiskCacheAndWriteData) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -594,7 +637,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDiskCacheAndWriteData) {
}
}
TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDiskCacheAndGetData) {
TEST_P(SharedDictionaryManagerOnDiskTest, CorruptedDiskCacheAndGetData) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -643,7 +686,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDiskCacheAndGetData) {
}
#endif // !BUILDFLAG(IS_FUCHSIA)
TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDatabase) {
TEST_P(SharedDictionaryManagerOnDiskTest, CorruptedDatabase) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -730,7 +773,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDatabase) {
}
}
TEST_F(SharedDictionaryManagerOnDiskTest, MetadataBrokenDatabase) {
TEST_P(SharedDictionaryManagerOnDiskTest, MetadataBrokenDatabase) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -799,7 +842,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, MetadataBrokenDatabase) {
}
}
TEST_F(SharedDictionaryManagerOnDiskTest, LastUsedTime) {
TEST_P(SharedDictionaryManagerOnDiskTest, LastUsedTime) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
base::Time last_used_time_after_second_get_dict;
@ -871,7 +914,7 @@ MATCHER_P(DictionaryUrlIs,
return arg.url().spec() == url;
}
TEST_F(SharedDictionaryManagerOnDiskTest, ClearData) {
TEST_P(SharedDictionaryManagerOnDiskTest, ClearData) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
{
@ -973,7 +1016,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, ClearData) {
Pair("/p4*", DictionaryUrlIs("https://target.test/4"))))));
}
TEST_F(SharedDictionaryManagerOnDiskTest, ClearDataSerializedOperation) {
TEST_P(SharedDictionaryManagerOnDiskTest, ClearDataSerializedOperation) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
std::unique_ptr<SharedDictionaryManager> manager =
@ -1024,7 +1067,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, ClearDataSerializedOperation) {
EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty());
}
TEST_F(SharedDictionaryManagerOnDiskTest, ClearDataForIsolationKey) {
TEST_P(SharedDictionaryManagerOnDiskTest, ClearDataForIsolationKey) {
net::SharedDictionaryIsolationKey isolation_key1(url::Origin::Create(kUrl),
kSite);
net::SharedDictionaryIsolationKey isolation_key2(
@ -1096,7 +1139,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, ClearDataForIsolationKey) {
"/p*", DictionaryUrlIs("https://origin1.test/d"))))));
}
TEST_F(SharedDictionaryManagerOnDiskTest, ExpiredDictionaryDeletionOnReload) {
TEST_P(SharedDictionaryManagerOnDiskTest, ExpiredDictionaryDeletionOnReload) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
base::UnguessableToken token1, token2;
@ -1143,7 +1186,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, ExpiredDictionaryDeletionOnReload) {
EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token2));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
ExpiredDictionaryDeletionOnNewDictionary) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -1188,7 +1231,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token2));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
ExpiredDictionaryDeletionOnSetCacheMaxSize) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -1224,7 +1267,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
ExpiredDictionaryDeletionOnClearData) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -1263,7 +1306,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
ExpiredDictionaryDeletionOnClearDataForIsolationKey) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -1303,7 +1346,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token));
}
TEST_F(SharedDictionaryManagerOnDiskTest, CacheEvictionOnReload) {
TEST_P(SharedDictionaryManagerOnDiskTest, CacheEvictionOnReload) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
base::UnguessableToken token1, token2, token3;
@ -1355,7 +1398,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, CacheEvictionOnReload) {
EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token3));
}
TEST_F(SharedDictionaryManagerOnDiskTest, CacheEvictionOnSetCacheMaxSize) {
TEST_P(SharedDictionaryManagerOnDiskTest, CacheEvictionOnSetCacheMaxSize) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
std::unique_ptr<SharedDictionaryManager> manager =
@ -1397,7 +1440,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, CacheEvictionOnSetCacheMaxSize) {
EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token3));
}
TEST_F(SharedDictionaryManagerOnDiskTest, CacheEvictionOnNewDictionary) {
TEST_P(SharedDictionaryManagerOnDiskTest, CacheEvictionOnNewDictionary) {
const net::SchemefulSite site1(GURL("https://site1.test"));
const net::SchemefulSite site2(GURL("https://site2.test"));
const net::SchemefulSite site3(GURL("https://site3.test"));
@ -1473,7 +1516,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest, CacheEvictionOnNewDictionary) {
EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token3));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
CacheEvictionPerSiteExceededSizeLimit) {
const net::SchemefulSite site1(GURL("https://site1.test"));
const net::SchemefulSite site2(GURL("https://site2.test"));
@ -1553,7 +1596,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token2));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
CacheEvictionPerSiteExceededCountLimit) {
const net::SchemefulSite site1(GURL("https://site1.test"));
const net::SchemefulSite site2(GURL("https://site2.test"));
@ -1655,7 +1698,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token3));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
CacheEvictionAfterUpdatingLastUsedTime) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -1727,7 +1770,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token4));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
MismatchingEntryDeletionMetadataUnavailableDictionary) {
const base::UnguessableToken token = base::UnguessableToken::Create();
const std::string entry_key = token.ToString();
@ -1757,7 +1800,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
0, 1);
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
MismatchingEntryDeletionInvalidDiskCacheEntry) {
const std::string kTestKey = "test";
const std::string kTestData = "Hello world";
@ -1786,7 +1829,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
0, 1);
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
MismatchingEntryDeletionDiskCacheEntryUnavailableDictionary) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -1853,7 +1896,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty());
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
MismatchingEntryDeletionCanBeTriggeredOnlyOnce) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
@ -1894,7 +1937,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
EXPECT_TRUE(DiskCacheEntryExists(manager.get(), entry_key));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
MismatchingEntryDeletionWritingEntryMustNotBeDeleted) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);
@ -1908,10 +1951,11 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"/p*\"\n\n"}));
": match=\"/p*\"\n", kDefaultCacheControlHeader, "\n"}));
ASSERT_TRUE(headers);
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
GURL("https://target1.test/d"), base::Time::Now(), *headers,
GURL("https://target1.test/d"), /*request_time=*/base::Time::Now(),
/*response_time=*/base::Time::Now(), *headers,
/*was_fetched_via_cache=*/false, DummyAccessAllowedCheckCallback());
ASSERT_TRUE(writer);
writer->Append(kTestData1.c_str(), kTestData1.size());
@ -1957,7 +2001,7 @@ TEST_F(SharedDictionaryManagerOnDiskTest,
"/p*", DictionaryUrlIs("https://target1.test/d"))))));
}
TEST_F(SharedDictionaryManagerOnDiskTest,
TEST_P(SharedDictionaryManagerOnDiskTest,
MismatchingEntryDeletionWritingDiskCacheEntryMustNotBeDeleted) {
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl),
kSite);

@ -4,6 +4,8 @@
#include "services/network/shared_dictionary/shared_dictionary_manager.h"
#include <optional>
#include <string>
#include <vector>
#include "base/feature_list.h"
@ -14,6 +16,7 @@
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
@ -76,6 +79,29 @@ const net::SHA256HashValue kTestData2Hash = {
const size_t kCacheMaxCount = 100;
// Default cache control header for dictionary entries which expires in 30 days.
const std::string kDefaultCacheControlHeader =
"cache-control: max-age=2592000\n";
std::string ToString(TestManagerType type) {
switch (type) {
case TestManagerType::kInMemory:
return "InMemory";
case TestManagerType::kOnDisk:
return "OnDisk";
}
}
std::string ToString(
features::CompressionDictionaryTransportBackendVersion version) {
switch (version) {
case features::CompressionDictionaryTransportBackendVersion::kV1:
return "V1";
case features::CompressionDictionaryTransportBackendVersion::kV2:
return "V2";
}
}
void CheckDiskCacheEntryDataEquals(
SharedDictionaryDiskCache& disk_cache,
const base::UnguessableToken& disk_cache_key_token,
@ -104,18 +130,22 @@ void CheckDiskCacheEntryDataEquals(
read_buffer->size()));
}
void WriteDictionary(SharedDictionaryStorage* storage,
const GURL& dictionary_url,
const std::string& match,
const std::vector<std::string>& data_list,
const std::string& additional_options = std::string()) {
void WriteDictionary(
SharedDictionaryStorage* storage,
const GURL& dictionary_url,
const std::string& match,
const std::vector<std::string>& data_list,
const std::string& additional_options = std::string(),
const std::string& additional_header = kDefaultCacheControlHeader) {
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"/", match, "\"", additional_options, "\n\n"}));
": match=\"/", match, "\"", additional_options, "\n",
additional_header, "\n"}));
ASSERT_TRUE(headers);
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
dictionary_url, base::Time::Now(), *headers,
dictionary_url, /*request_time=*/base::Time::Now(),
/*response_time=*/base::Time::Now(), *headers,
/*was_fetched_via_cache=*/false,
/*access_allowed_check_callback=*/base::BindOnce([]() { return true; }));
ASSERT_TRUE(writer);
@ -126,8 +156,11 @@ void WriteDictionary(SharedDictionaryStorage* storage,
}
base::TimeDelta GetDefaultExpiration() {
return base::FeatureList::IsEnabled(
network::features::kCompressionDictionaryTransport)
if (features::kCompressionDictionaryTransportBackendVersion.Get() ==
features::CompressionDictionaryTransportBackendVersion::kV2) {
return base::Seconds(2592000); // See kDefaultCacheControlHeader
}
return base::FeatureList::IsEnabled(features::kCompressionDictionaryTransport)
? shared_dictionary::kDefaultExpiration
: shared_dictionary::kMaxExpirationForOriginTrial;
}
@ -136,9 +169,19 @@ base::TimeDelta GetDefaultExpiration() {
class SharedDictionaryManagerTest
: public ::testing::Test,
public testing::WithParamInterface<TestManagerType> {
public ::testing::WithParamInterface<
std::tuple<TestManagerType,
features::CompressionDictionaryTransportBackendVersion>> {
public:
SharedDictionaryManagerTest() = default;
SharedDictionaryManagerTest() {
std::vector<base::test::FeatureRefAndParams> enabled_features;
enabled_features.emplace_back(base::test::FeatureRefAndParams(
features::kCompressionDictionaryTransportBackend,
{{features::kCompressionDictionaryTransportBackendVersion.name,
features::kCompressionDictionaryTransportBackendVersion.GetName(
GetVersion())}}));
scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features, {});
}
~SharedDictionaryManagerTest() override = default;
SharedDictionaryManagerTest(const SharedDictionaryManagerTest&) = delete;
@ -146,7 +189,7 @@ class SharedDictionaryManagerTest
delete;
void SetUp() override {
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
ASSERT_TRUE(tmp_directory_.CreateUniqueTempDir());
database_path_ = tmp_directory_.GetPath().Append(FILE_PATH_LITERAL("db"));
cache_directory_path_ =
@ -154,14 +197,18 @@ class SharedDictionaryManagerTest
}
}
void TearDown() override {
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
}
protected:
TestManagerType GetManagerType() const { return std::get<0>(GetParam()); }
features::CompressionDictionaryTransportBackendVersion GetVersion() const {
return std::get<1>(GetParam());
}
std::unique_ptr<SharedDictionaryManager> CreateSharedDictionaryManager() {
switch (GetParam()) {
switch (GetManagerType()) {
case TestManagerType::kInMemory:
return SharedDictionaryManager::CreateInMemory(/*cache_max_size=*/0,
kCacheMaxCount);
@ -217,19 +264,22 @@ class SharedDictionaryManagerTest
base::ScopedTempDir tmp_directory_;
base::FilePath database_path_;
base::FilePath cache_directory_path_;
base::test::ScopedFeatureList scoped_feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
All,
SharedDictionaryManagerTest,
testing::ValuesIn({TestManagerType::kInMemory, TestManagerType::kOnDisk}),
[](const testing::TestParamInfo<TestManagerType>& info) {
switch (info.param) {
case TestManagerType::kInMemory:
return "InMemory";
case TestManagerType::kOnDisk:
return "OnDisk";
}
::testing::Combine(
testing::Values(TestManagerType::kInMemory, TestManagerType::kOnDisk),
testing::Values(
features::CompressionDictionaryTransportBackendVersion::kV1,
features::CompressionDictionaryTransportBackendVersion::kV2)),
[](const testing::TestParamInfo<std::tuple<
TestManagerType,
features::CompressionDictionaryTransportBackendVersion>>& info) {
return ToString(std::get<0>(info.param)) + "_" +
ToString(std::get<1>(info.param));
});
TEST_P(SharedDictionaryManagerTest, SameStorageForSameIsolationKey) {
@ -285,7 +335,7 @@ TEST_P(SharedDictionaryManagerTest, CachedStorage) {
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "p*",
{"Hello"});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -311,7 +361,7 @@ TEST_P(SharedDictionaryManagerTest, CachedStorageEvicted) {
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "p*",
{"Hello"});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -364,7 +414,7 @@ TEST_P(SharedDictionaryManagerTest,
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "p*",
{"Hello"});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -395,7 +445,7 @@ TEST_P(SharedDictionaryManagerTest,
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "p*",
{"Hello"});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -421,7 +471,7 @@ TEST_P(SharedDictionaryManagerTest,
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "p*",
{"Hello"});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -452,7 +502,7 @@ TEST_P(SharedDictionaryManagerTest,
// Write the test data to the dictionary.
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "p*",
{"Hello"});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -486,7 +536,9 @@ TEST_P(SharedDictionaryManagerTest, NoWriterForNoUseAsDictionaryHeader) {
net::HttpResponseHeaders::TryToCreate("HTTP/1.1 200 OK\n");
ASSERT_TRUE(headers);
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
GURL("https://origin1.test/testfile.txt"), base::Time::Now(), *headers,
GURL("https://origin1.test/testfile.txt"),
/*request_time=*/base::Time::Now(), /*response_time=*/base::Time::Now(),
*headers,
/*was_fetched_via_cache=*/false,
/*access_allowed_check_callback=*/base::BindOnce([]() { return true; }));
EXPECT_FALSE(writer);
@ -525,13 +577,6 @@ TEST_P(SharedDictionaryManagerTest, WriterForUseAsDictionaryHeader) {
// Token `match` value is not supported.
{"match=test", false},
// Valid `expires` value.
{"match=\"test\", expires=1000", true},
// List `expires` value is not supported.
{"match=\"test\", expires=(1000 2000)", false},
// String `expires` value is not supported.
{"match=\"test\", expires=PI", false},
// We support `raw` type.
{"match=\"test\", type=raw", true},
{"match=\"test\", type=(raw)", true},
@ -548,10 +593,13 @@ TEST_P(SharedDictionaryManagerTest, WriterForUseAsDictionaryHeader) {
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": ", testcase.header_string, "\n\n"}));
": ", testcase.header_string, "\n", kDefaultCacheControlHeader,
"\n"}));
ASSERT_TRUE(headers);
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
GURL("https://origin1.test/testfile.txt"), base::Time::Now(), *headers,
GURL("https://origin1.test/testfile.txt"),
/*request_time=*/base::Time::Now(), /*response_time=*/base::Time::Now(),
*headers,
/*was_fetched_via_cache=*/false,
/*access_allowed_check_callback=*/base::BindOnce([]() {
return true;
@ -560,6 +608,114 @@ TEST_P(SharedDictionaryManagerTest, WriterForUseAsDictionaryHeader) {
}
}
TEST_P(SharedDictionaryManagerTest,
WriterForUseAsDictionaryHeaderExpiresOption) {
// We don't support `expires` option in the V2 backend.
if (GetVersion() ==
features::CompressionDictionaryTransportBackendVersion::kV2) {
return;
}
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl1),
kSite1);
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
struct {
std::string header_string;
bool expect_success;
} kTestCases[] = {
// Valid `expires` value.
{"match=\"test\", expires=1000", true},
// List `expires` value is not supported.
{"match=\"test\", expires=(1000 2000)", false},
// String `expires` value is not supported.
{"match=\"test\", expires=PI", false},
};
for (const auto& testcase : kTestCases) {
SCOPED_TRACE(base::StringPrintf("header_string: %s",
testcase.header_string.c_str()));
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": ", testcase.header_string, "\n\n"}));
ASSERT_TRUE(headers);
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
GURL("https://origin1.test/testfile.txt"),
/*request_time=*/base::Time::Now(), /*response_time=*/base::Time::Now(),
*headers,
/*was_fetched_via_cache=*/false,
/*access_allowed_check_callback=*/base::BindOnce([]() {
return true;
}));
EXPECT_EQ(testcase.expect_success, !!writer);
}
}
TEST_P(SharedDictionaryManagerTest, DictionaryLifetimeFromCacheControlHeader) {
// We don't use the cache conterol header for the lifetime of the dictionary
// in the V1 backend.
if (GetVersion() ==
features::CompressionDictionaryTransportBackendVersion::kV1) {
return;
}
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
net::SharedDictionaryIsolationKey isolation_key(url::Origin::Create(kUrl1),
kSite1);
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
ASSERT_TRUE(storage);
struct {
std::string header_string;
std::optional<base::TimeDelta> expected_expiration;
} kTestCases[] = {
// Empty
{"", std::nullopt},
{"cache-control:max-age=100", base::Seconds(100)},
{"cache-control:max-age=100, stale-while-revalidate=50",
base::Seconds(150)},
{"cache-control:max-age=100\nage:10", base::Seconds(90)},
};
for (const auto& testcase : kTestCases) {
SCOPED_TRACE(base::StringPrintf("header_string: %s",
testcase.header_string.c_str()));
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"test\"\n", testcase.header_string, "\n"}));
ASSERT_TRUE(headers);
const base::Time request_time = base::Time::Now();
const base::Time response_time = request_time;
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
GURL("https://origin1.test/testfile.txt"), request_time, response_time,
*headers,
/*was_fetched_via_cache=*/false,
/*access_allowed_check_callback=*/base::BindOnce([]() {
return true;
}));
EXPECT_EQ(!!testcase.expected_expiration, !!writer);
if (!writer) {
continue;
}
writer->Append(kTestData1.c_str(), kTestData1.size());
writer->Finish();
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
std::vector<network::mojom::SharedDictionaryInfoPtr> result =
GetSharedDictionaryInfo(manager.get(), isolation_key);
ASSERT_EQ(1u, result.size());
EXPECT_EQ(*testcase.expected_expiration, result[0]->expiration);
}
}
TEST_P(SharedDictionaryManagerTest, AccessAllowedCheckReturnTrue) {
std::unique_ptr<SharedDictionaryManager> manager =
CreateSharedDictionaryManager();
@ -572,11 +728,13 @@ TEST_P(SharedDictionaryManagerTest, AccessAllowedCheckReturnTrue) {
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"test\"\n\n"}));
": match=\"test\"\ncache-control:max-age=100\n\n"}));
ASSERT_TRUE(headers);
bool callback_called = false;
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
GURL("https://origin1.test/testfile.txt"), base::Time::Now(), *headers,
GURL("https://origin1.test/testfile.txt"),
/*request_time=*/base::Time::Now(), /*response_time=*/base::Time::Now(),
*headers,
/*was_fetched_via_cache=*/false,
/*access_allowed_check_callback=*/base::BindLambdaForTesting([&]() {
callback_called = true;
@ -598,11 +756,13 @@ TEST_P(SharedDictionaryManagerTest, AccessAllowedCheckReturnFalse) {
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"test\"\n\n"}));
": match=\"test\"\ncache-control:max-age=100\n\n"}));
ASSERT_TRUE(headers);
bool callback_called = false;
scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter(
GURL("https://origin1.test/testfile.txt"), base::Time::Now(), *headers,
GURL("https://origin1.test/testfile.txt"),
/*request_time=*/base::Time::Now(), /*response_time=*/base::Time::Now(),
*headers,
/*was_fetched_via_cache=*/false,
/*access_allowed_check_callback=*/base::BindLambdaForTesting([&]() {
callback_called = true;
@ -626,10 +786,11 @@ TEST_P(SharedDictionaryManagerTest, SameDictionaryFromDiskCache) {
scoped_refptr<net::HttpResponseHeaders> headers =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"test\"\n\n"}));
": match=\"test\"\ncache-control:max-age=100\n\n"}));
ASSERT_TRUE(headers);
scoped_refptr<SharedDictionaryWriter> writer1 = storage->MaybeCreateWriter(
dictionary_url, response_time, *headers,
dictionary_url, /*request_time=*/response_time,
/*response_time=*/response_time, *headers,
/*was_fetched_via_cache=*/false,
/*access_allowed_check_callback=*/base::BindLambdaForTesting([&]() {
return true;
@ -637,11 +798,12 @@ TEST_P(SharedDictionaryManagerTest, SameDictionaryFromDiskCache) {
ASSERT_TRUE(writer1);
writer1->Append(kTestData1.c_str(), kTestData1.size());
writer1->Finish();
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
scoped_refptr<SharedDictionaryWriter> writer2 = storage->MaybeCreateWriter(
dictionary_url, response_time, *headers,
dictionary_url, /*request_time=*/response_time,
/*response_time=*/response_time, *headers,
/*was_fetched_via_cache=*/true,
/*access_allowed_check_callback=*/base::BindLambdaForTesting([&]() {
return true;
@ -665,10 +827,11 @@ TEST_P(SharedDictionaryManagerTest, DifferentDictionaryFromDiskCache) {
scoped_refptr<net::HttpResponseHeaders> headers1 =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"test1\"\n\n"}));
": match=\"test1\"\ncache-control:max-age=100\n\n"}));
ASSERT_TRUE(headers1);
scoped_refptr<SharedDictionaryWriter> writer1 = storage->MaybeCreateWriter(
dictionary_url, response_time, *headers1,
dictionary_url, /*request_time=*/response_time,
/*response_time=*/response_time, *headers1,
/*was_fetched_via_cache=*/false,
/*access_allowed_check_callback=*/base::BindLambdaForTesting([&]() {
return true;
@ -676,17 +839,18 @@ TEST_P(SharedDictionaryManagerTest, DifferentDictionaryFromDiskCache) {
ASSERT_TRUE(writer1);
writer1->Append(kTestData1.c_str(), kTestData1.size());
writer1->Finish();
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
scoped_refptr<net::HttpResponseHeaders> headers2 =
net::HttpResponseHeaders::TryToCreate(base::StrCat(
{"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName,
": match=\"test2\"\n\n"}));
": match=\"test2\"\ncache-control:max-age=100\n\n"}));
ASSERT_TRUE(headers1);
scoped_refptr<SharedDictionaryWriter> writer2 = storage->MaybeCreateWriter(
dictionary_url, response_time, *headers2,
dictionary_url, /*request_time=*/response_time,
/*response_time=*/response_time, *headers2,
/*was_fetched_via_cache=*/true,
/*access_allowed_check_callback=*/base::BindLambdaForTesting([&]() {
return true;
@ -706,7 +870,7 @@ TEST_P(SharedDictionaryManagerTest, WriteAndGetDictionary) {
ASSERT_TRUE(storage);
WriteDictionary(storage.get(), GURL("https://origin1.test/dict"), "testfile*",
{"hello world"});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -743,7 +907,7 @@ TEST_P(SharedDictionaryManagerTest, WriteAndReadDictionary) {
net::SHA256HashValue sha256;
secure_hash->Finish(sha256.data, sizeof(sha256.data));
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -755,7 +919,7 @@ TEST_P(SharedDictionaryManagerTest, WriteAndReadDictionary) {
EXPECT_EQ(sha256, dict->hash());
// Read and check the dictionary binary.
switch (GetParam()) {
switch (GetManagerType()) {
case TestManagerType::kInMemory: {
EXPECT_EQ(net::OK,
dict->ReadAll(base::BindOnce([](int rv) { NOTREACHED(); })));
@ -776,7 +940,7 @@ TEST_P(SharedDictionaryManagerTest, WriteAndReadDictionary) {
ASSERT_TRUE(dict->data());
EXPECT_EQ(data1 + data2, std::string(dict->data()->data(), dict->size()));
switch (GetParam()) {
switch (GetManagerType()) {
case TestManagerType::kInMemory: {
// Check the internal state of SharedDictionaryStorageInMemory.
const auto& dictionary_map = GetInMemoryDictionaryMap(storage.get());
@ -839,7 +1003,7 @@ TEST_P(SharedDictionaryManagerTest, OverrideDictionary) {
// Write a test dictionary.
WriteDictionary(storage.get(), url1, match, {data1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -851,7 +1015,7 @@ TEST_P(SharedDictionaryManagerTest, OverrideDictionary) {
// Write another dictionary with same `match`.
WriteDictionary(storage.get(), url2, match, {data2});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -898,7 +1062,7 @@ TEST_P(SharedDictionaryManagerTest,
WriteDictionary(storage.get(), GURL("https://origin3.test/d1"), "p3*",
{kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -906,7 +1070,7 @@ TEST_P(SharedDictionaryManagerTest,
manager->SetCacheMaxSize(/*cache_max_size=*/kTestData1.size() * 2);
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -935,7 +1099,7 @@ TEST_P(SharedDictionaryManagerTest, CacheEvictionZeroMaxSizeCountExceeded) {
storage.get(),
GURL(base::StringPrintf("https://origin.test/d%03" PRIuS, i)),
base::StringPrintf("p%03" PRIuS, i), {kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
task_environment_.FastForwardBy(base::Seconds(1));
@ -962,7 +1126,7 @@ TEST_P(SharedDictionaryManagerTest, CacheEvictionZeroMaxSizeCountExceeded) {
kCacheMaxCount)),
base::StringPrintf("p%03" PRIuS, kCacheMaxCount),
{kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
task_environment_.FastForwardBy(base::Seconds(1));
@ -1009,7 +1173,7 @@ TEST_P(SharedDictionaryManagerTest,
{kTestData1});
WriteDictionary(storage2.get(), GURL("https://origin2.test/d2"), "p2*",
{kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
EXPECT_TRUE(storage1->GetDictionarySync(GURL("https://origin1.test/p1?")));
@ -1018,7 +1182,7 @@ TEST_P(SharedDictionaryManagerTest,
task_environment_.FastForwardBy(base::Seconds(1));
WriteDictionary(storage3.get(), GURL("https://origin3.test/d1"), "p3*",
{kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
EXPECT_FALSE(storage1->GetDictionarySync(GURL("https://origin1.test/p1?")));
@ -1057,7 +1221,7 @@ TEST_P(SharedDictionaryManagerTest, CacheEvictionAfterUpdatingLastUsedTime) {
WriteDictionary(storage2.get(), GURL("https://origin2.test/d2"), "p2*",
{kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -1072,7 +1236,7 @@ TEST_P(SharedDictionaryManagerTest, CacheEvictionAfterUpdatingLastUsedTime) {
// kTestData1.size() * 2.7 (3 * 0.9).
manager->SetCacheMaxSize(/*cache_max_size=*/kTestData1.size() * 3);
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -1108,7 +1272,7 @@ TEST_P(SharedDictionaryManagerTest, CacheEvictionPerSiteSizeExceeded) {
{kTestData1});
WriteDictionary(storage3.get(), GURL("https://origin3.test/d"), "p*",
{kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
EXPECT_TRUE(storage1->GetDictionarySync(GURL("https://origin1.test/p?")));
@ -1120,7 +1284,7 @@ TEST_P(SharedDictionaryManagerTest, CacheEvictionPerSiteSizeExceeded) {
WriteDictionary(storage1.get(), GURL("https://origin4.test/d"), "p*",
{kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
EXPECT_FALSE(storage1->GetDictionarySync(GURL("https://origin1.test/p?")));
@ -1145,7 +1309,7 @@ TEST_P(SharedDictionaryManagerTest,
storage.get(),
GURL(base::StringPrintf("https://origin.test/d%03" PRIuS, i)),
base::StringPrintf("p%03" PRIuS, i), {kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
task_environment_.FastForwardBy(base::Seconds(1));
@ -1163,7 +1327,7 @@ TEST_P(SharedDictionaryManagerTest,
cache_max_count_per_site)),
base::StringPrintf("p%03" PRIuS, cache_max_count_per_site),
{kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
task_environment_.FastForwardBy(base::Seconds(1));
@ -1195,7 +1359,7 @@ TEST_P(SharedDictionaryManagerTest,
storage.get(),
GURL(base::StringPrintf("https://origin.test/d%03" PRIuS, i)),
base::StringPrintf("p%03" PRIuS, i), {kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
task_environment_.FastForwardBy(base::Seconds(1));
@ -1213,7 +1377,7 @@ TEST_P(SharedDictionaryManagerTest,
cache_max_count_per_site)),
base::StringPrintf("p%03" PRIuS, cache_max_count_per_site),
{kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
task_environment_.FastForwardBy(base::Seconds(1));
@ -1245,7 +1409,7 @@ TEST_P(SharedDictionaryManagerTest,
storage.get(),
GURL(base::StringPrintf("https://origin.test/d%03" PRIuS, i)),
base::StringPrintf("p%03" PRIuS, i), {kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
task_environment_.FastForwardBy(base::Seconds(1));
@ -1263,7 +1427,7 @@ TEST_P(SharedDictionaryManagerTest,
cache_max_count_per_site)),
base::StringPrintf("p%03" PRIuS, cache_max_count_per_site),
{kTestData1, kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
task_environment_.FastForwardBy(base::Seconds(1));
@ -1438,7 +1602,7 @@ TEST_P(SharedDictionaryManagerTest, ClearDataDoNotInvalidateActiveDictionary) {
// Move the clock forward by 12 hours.
task_environment_.FastForwardBy(base::Hours(12));
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -1490,7 +1654,7 @@ TEST_P(SharedDictionaryManagerTest, ClearDataForIsolationKey) {
WriteDictionary(storage2.get(), GURL("https://origin2.test/2"), "p2*",
{kTestData1});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -1533,7 +1697,7 @@ TEST_P(SharedDictionaryManagerTest, GetUsageInfo) {
WriteDictionary(storage2.get(), GURL("https://origin2.test/2"), "p2*",
{kTestData2});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -1592,9 +1756,10 @@ TEST_P(SharedDictionaryManagerTest, GetSharedDictionaryInfo) {
manager->GetStorage(isolation_key2);
task_environment_.FastForwardBy(base::Seconds(1));
WriteDictionary(storage2.get(), GURL("https://origin2.test/d"), "p*",
{kTestData2}, /*additional_options=*/",expires=123456");
{kTestData2}, /*additional_options=*/",expires=123456",
/*additional_header=*/"cache-control:max-age=123456\n");
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -1672,7 +1837,7 @@ TEST_P(SharedDictionaryManagerTest, GetTotalSizeAndOrigins) {
WriteDictionary(storage2.get(), GURL("https://origin2.test/d"), "p*",
{kTestData2});
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}
@ -1704,14 +1869,16 @@ TEST_P(SharedDictionaryManagerTest, DeleteExpiredDictionariesOnGetDictionary) {
scoped_refptr<SharedDictionaryStorage> storage =
manager->GetStorage(isolation_key);
WriteDictionary(storage.get(), GURL("https://origin.test/d1"), "p1*",
{kTestData1}, /*additional_options=*/",expires=20");
{kTestData1}, /*additional_options=*/",expires=20",
/*additional_header=*/"cache-control:max-age=20\n");
task_environment_.FastForwardBy(base::Seconds(10));
WriteDictionary(storage.get(), GURL("https://origin.test/d1"), "p2*",
{kTestData2}, /*additional_options=*/",expires=5");
{kTestData2}, /*additional_options=*/",expires=5",
/*additional_header=*/"cache-control:max-age=5\n");
if (GetParam() == TestManagerType::kOnDisk) {
if (GetManagerType() == TestManagerType::kOnDisk) {
FlushCacheTasks();
}

@ -5,6 +5,7 @@
#include "services/network/shared_dictionary/shared_dictionary_storage.h"
#include <algorithm>
#include <optional>
#include <string_view>
#include "base/containers/contains.h"
@ -18,7 +19,6 @@
#include "services/network/public/cpp/features.h"
#include "services/network/shared_dictionary/shared_dictionary_constants.h"
#include "services/network/shared_dictionary/shared_dictionary_writer.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "url/gurl.h"
#include "url/scheme_host_port.h"
@ -28,65 +28,94 @@ namespace {
constexpr std::string_view kDefaultTypeRaw = "raw";
class UseAsDictionaryHeaderInfo {
class DictionaryHeaderInfo {
public:
UseAsDictionaryHeaderInfo(std::string match,
absl::optional<base::TimeDelta> expiration,
std::string type)
DictionaryHeaderInfo(std::string match,
std::optional<base::TimeDelta> expiration,
std::string type)
: match(std::move(match)),
expiration(expiration),
type(std::move(type)) {}
~UseAsDictionaryHeaderInfo() = default;
~DictionaryHeaderInfo() = default;
std::string match;
absl::optional<base::TimeDelta> expiration;
// TODO(crbug.com/1413922): Stop using std::optional when we remove V1 backend
// support.
std::optional<base::TimeDelta> expiration;
std::string type;
};
absl::optional<UseAsDictionaryHeaderInfo> ParseUseAsDictionaryHeaderInfo(
const net::HttpResponseHeaders& headers) {
std::optional<DictionaryHeaderInfo> ParseDictionaryHeaderInfo(
const net::HttpResponseHeaders& headers,
const base::Time request_time,
const base::Time response_time) {
std::string use_as_dictionary_header;
if (!headers.GetNormalizedHeader(
shared_dictionary::kUseAsDictionaryHeaderName,
&use_as_dictionary_header)) {
return absl::nullopt;
return std::nullopt;
}
absl::optional<net::structured_headers::Dictionary> dictionary =
std::optional<net::structured_headers::Dictionary> dictionary =
net::structured_headers::ParseDictionary(use_as_dictionary_header);
if (!dictionary) {
return absl::nullopt;
return std::nullopt;
}
absl::optional<std::string> match_value;
absl::optional<base::TimeDelta> expires_value;
// Don't use the value of `expires` in the `Use-As-Dictionary` response header
// when V2 backend is enabled.
const bool check_expires_dictionary_value =
features::kCompressionDictionaryTransportBackendVersion.Get() ==
features::CompressionDictionaryTransportBackendVersion::kV1;
std::optional<std::string> match_value;
std::optional<base::TimeDelta> expires_value;
std::string type_value = std::string(kDefaultTypeRaw);
for (const auto& entry : dictionary.value()) {
if (entry.first == shared_dictionary::kOptionNameMatch) {
if ((entry.second.member.size() != 1u) ||
!entry.second.member.front().item.is_string()) {
return absl::nullopt;
return std::nullopt;
}
match_value = entry.second.member.front().item.GetString();
} else if (entry.first == shared_dictionary::kOptionNameExpires) {
} else if (check_expires_dictionary_value &&
entry.first == shared_dictionary::kOptionNameExpires) {
if ((entry.second.member.size() != 1u) ||
!entry.second.member.front().item.is_integer()) {
return absl::nullopt;
return std::nullopt;
}
expires_value =
base::Seconds(entry.second.member.front().item.GetInteger());
} else if (entry.first == shared_dictionary::kOptionNameType) {
if ((entry.second.member.size() != 1u) ||
!entry.second.member.front().item.is_token()) {
return absl::nullopt;
return std::nullopt;
}
type_value = entry.second.member.front().item.GetString();
}
}
if (!match_value) {
return absl::nullopt;
if (!check_expires_dictionary_value) {
// Use the fressness lifetime caliculated from the response header.
net::HttpResponseHeaders::FreshnessLifetimes lifetimes =
headers.GetFreshnessLifetimes(response_time);
// We calculate `expires_value` which is a delta from the response time to
// the expiration time. So we get the age of the response on the response
// time by setting `current_time` argument to `response_time`.
base::TimeDelta age_on_response_time =
headers.GetCurrentAge(request_time, response_time,
/*current_time=*/response_time);
// We can use `freshness + staleness - current_age` as the expiration time.
expires_value =
lifetimes.freshness + lifetimes.staleness - age_on_response_time;
if (*expires_value <= base::TimeDelta()) {
return std::nullopt;
}
}
return UseAsDictionaryHeaderInfo(*match_value, std::move(expires_value),
type_value);
if (!match_value) {
return std::nullopt;
}
return DictionaryHeaderInfo(std::move(*match_value), std::move(expires_value),
std::move(type_value));
}
} // namespace
@ -98,15 +127,18 @@ SharedDictionaryStorage::~SharedDictionaryStorage() = default;
scoped_refptr<SharedDictionaryWriter>
SharedDictionaryStorage::MaybeCreateWriter(
const GURL& url,
base::Time response_time,
const base::Time request_time,
const base::Time response_time,
const net::HttpResponseHeaders& headers,
bool was_fetched_via_cache,
base::OnceCallback<bool()> access_allowed_check_callback) {
absl::optional<UseAsDictionaryHeaderInfo> info =
ParseUseAsDictionaryHeaderInfo(headers);
std::optional<DictionaryHeaderInfo> info =
ParseDictionaryHeaderInfo(headers, request_time, response_time);
if (!info) {
return nullptr;
}
// TODO(crubg.com/1413922) Stop using kDefaultExpiration when we remove V1
// backend support.
base::TimeDelta expiration = shared_dictionary::kDefaultExpiration;
if (info->expiration) {
expiration = *info->expiration;

@ -54,7 +54,8 @@ class COMPONENT_EXPORT(NETWORK_SERVICE) SharedDictionaryStorage
// returns true,
scoped_refptr<SharedDictionaryWriter> MaybeCreateWriter(
const GURL& url,
base::Time response_time,
const base::Time request_time,
const base::Time response_time,
const net::HttpResponseHeaders& headers,
bool was_fetched_via_cache,
base::OnceCallback<bool()> access_allowed_check_callback);