blob: 73a6ec788998cf71053272920594174afd19ed67 [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 "services/network/shared_dictionary/shared_dictionary_storage.h"
#include <algorithm>
#include <optional>
#include <string_view>
#include <vector>
#include "base/feature_list.h"
#include "base/strings/pattern.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "components/url_pattern/simple_url_pattern_matcher.h"
#include "net/base/io_buffer.h"
#include "net/http/http_response_headers.h"
#include "net/http/structured_headers.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/request_destination.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/shared_dictionary_error.mojom.h"
#include "services/network/shared_dictionary/shared_dictionary_constants.h"
#include "services/network/shared_dictionary/shared_dictionary_writer.h"
#include "url/gurl.h"
#include "url/scheme_host_port.h"
namespace network {
namespace {
constexpr std::string_view kDefaultTypeRaw = "raw";
class DictionaryHeaderInfo {
public:
DictionaryHeaderInfo(std::string match,
std::set<network::mojom::RequestDestination> match_dest,
std::string type,
std::string id,
std::optional<base::TimeDelta> ttl)
: match(std::move(match)),
match_dest(std::move(match_dest)),
type(std::move(type)),
id(std::move(id)),
ttl(std::move(ttl)) {}
~DictionaryHeaderInfo() = default;
std::string match;
std::set<network::mojom::RequestDestination> match_dest;
std::string type;
std::string id;
std::optional<base::TimeDelta> ttl;
};
base::TimeDelta CalculateExpiration(const net::HttpResponseHeaders& headers,
const base::Time request_time,
const base::Time response_time,
const std::optional<base::TimeDelta>& ttl) {
// An explicit "ttl" value overrides the calculated expiration from the HTTP
// caching headers.
if (ttl) {
return ttl.value();
}
// Use the freshness lifetime calculated 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.
return lifetimes.freshness + lifetimes.staleness - age_on_response_time;
}
base::expected<DictionaryHeaderInfo, mojom::SharedDictionaryError>
ParseDictionaryHeaderInfo(const std::string& use_as_dictionary_header) {
std::optional<net::structured_headers::Dictionary> dictionary =
net::structured_headers::ParseDictionary(use_as_dictionary_header);
if (!dictionary) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorInvalidStructuredHeader);
}
std::optional<std::string> match_value;
// Maybe we don't need to support multiple match-dest.
// https://github.com/httpwg/http-extensions/issues/2722
std::set<network::mojom::RequestDestination> match_dest_values;
std::string type_value = std::string(kDefaultTypeRaw);
std::string id_value;
std::optional<base::TimeDelta> ttl;
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 base::unexpected(
mojom::SharedDictionaryError::kWriteErrorNonStringMatchField);
}
match_value = entry.second.member.front().item.GetString();
} else if (entry.first == shared_dictionary::kOptionNameMatchDest) {
if (!entry.second.member_is_inner_list) {
// `match-dest` must be a list.
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorNonListMatchDestField);
}
for (const auto& item : entry.second.member) {
if (!item.item.is_string()) {
return base::unexpected(mojom::SharedDictionaryError::
kWriteErrorNonStringInMatchDestList);
}
// We use the empty string "" for RequestDestination::kEmpty in
// `match-dest`.
std::optional<mojom::RequestDestination> dest_value =
RequestDestinationFromString(
item.item.GetString(),
EmptyRequestDestinationOption::kUseTheEmptyString);
if (dest_value) {
match_dest_values.insert(*dest_value);
}
}
} else if (entry.first == shared_dictionary::kOptionNameType) {
if ((entry.second.member.size() != 1u) ||
!entry.second.member.front().item.is_token()) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorNonTokenTypeField);
}
type_value = entry.second.member.front().item.GetString();
} else if (entry.first == shared_dictionary::kOptionNameId) {
if ((entry.second.member.size() != 1u) ||
!entry.second.member.front().item.is_string()) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorNonStringIdField);
}
id_value = entry.second.member.front().item.GetString();
if (id_value.size() > shared_dictionary::kDictionaryIdMaxLength) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorTooLongIdField);
}
} else if (entry.first == shared_dictionary::kOptionNameTTL &&
base::FeatureList::IsEnabled(
features::kCompressionDictionaryTTL)) {
if ((entry.second.member.size() != 1u) ||
!entry.second.member.front().item.is_integer()) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorNonIntegerTTLField);
}
int64_t ttl_seconds = entry.second.member.front().item.GetInteger();
if (ttl_seconds <= 0) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorInvalidTTLField);
}
ttl = base::Seconds(ttl_seconds);
}
}
if (!match_value) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorNoMatchField);
}
return DictionaryHeaderInfo(
std::move(*match_value), std::move(match_dest_values),
std::move(type_value), std::move(id_value), std::move(ttl));
}
} // namespace
SharedDictionaryStorage::SharedDictionaryStorage() = default;
SharedDictionaryStorage::~SharedDictionaryStorage() = default;
// static
base::expected<scoped_refptr<SharedDictionaryWriter>,
mojom::SharedDictionaryError>
SharedDictionaryStorage::MaybeCreateWriter(
const std::string& use_as_dictionary_header,
bool shared_dictionary_writer_enabled,
SharedDictionaryStorage* storage,
mojom::RequestMode request_mode,
mojom::FetchResponseType response_tainting,
const GURL& url,
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) {
// Supports storing dictionaries if the request was fetched by cors enabled
// mode request or same-origin mode request or no-cors mode same origin
// request.
switch (request_mode) {
case mojom::RequestMode::kSameOrigin:
break;
case mojom::RequestMode::kNoCors:
// Basic `response_tainting` for no-cors request means that the response
// is from same origin without any cross origin redirect.
if (response_tainting != mojom::FetchResponseType::kBasic) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorCossOriginNoCorsRequest);
}
break;
case mojom::RequestMode::kCors:
break;
case mojom::RequestMode::kCorsWithForcedPreflight:
break;
case mojom::RequestMode::kNavigate:
break;
}
if (!shared_dictionary_writer_enabled) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorFeatureDisabled);
}
if (!storage) {
// CorsURLLoader passes a null `storage`, when the request is not from
// secure context.
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorNonSecureContext);
}
// Opaque response tainting requests should not trigger dictionary
// registration.
CHECK_NE(mojom::FetchResponseType::kOpaque, response_tainting);
base::expected<DictionaryHeaderInfo, mojom::SharedDictionaryError> info =
ParseDictionaryHeaderInfo(use_as_dictionary_header);
if (!info.has_value()) {
return base::unexpected(info.error());
}
base::TimeDelta expiration =
CalculateExpiration(headers, request_time, response_time, info->ttl);
if (expiration <= base::TimeDelta()) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorExpiredResponse);
}
if (info->type != kDefaultTypeRaw) {
// Currently we only support `raw` type.
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorUnsupportedType);
}
base::Time last_fetch_time = base::Time::Now();
// Do not write an existing shared dictionary from the HTTP caches to the
// shared dictionary storage. Note that IsAlreadyRegistered() can return false
// even when `was_fetched_via_cache` is true. This is because the shared
// dictionary storage has its own cache eviction logic, which is different
// from the HTTP Caches's eviction logic.
if (was_fetched_via_cache &&
storage->UpdateLastFetchTimeIfAlreadyRegistered(
url, response_time, expiration, info->match, info->match_dest,
info->id, info->ttl, last_fetch_time)) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorAlreadyRegistered);
}
if (!std::move(access_allowed_check_callback).Run()) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorDisallowedBySettings);
}
auto matcher_create_result =
url_pattern::SimpleUrlPatternMatcher::Create(info->match, &url);
if (!matcher_create_result.has_value()) {
return base::unexpected(
mojom::SharedDictionaryError::kWriteErrorInvalidMatchField);
}
return storage->CreateWriter(url, last_fetch_time, response_time, expiration,
info->match, info->match_dest, info->id,
std::move(matcher_create_result.value()));
}
} // namespace network