blob: 5ceea4d53d6ebeb5d8fbf9568fbe1ee75bebc83c [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/http/http_cache_util.h"
#include <array>
#include <optional>
#include <string_view>
#include "base/containers/span.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/strings/string_util.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
namespace net::http_cache_util {
namespace {
// If the request includes one of these request headers, then avoid caching
// to avoid getting confused.
struct HeaderNameAndValue {
std::string_view name;
std::optional<std::string_view> value;
};
// If the request includes one of these request headers, then avoid caching
// to avoid getting confused.
constexpr auto kPassThroughHeaders = std::to_array(
{HeaderNameAndValue{"if-unmodified-since",
std::nullopt}, // causes unexpected 412s
HeaderNameAndValue{"if-match", std::nullopt}, // causes unexpected 412s
HeaderNameAndValue{"if-range", std::nullopt}});
// If the request includes one of these request headers, then avoid reusing
// our cached copy if any.
constexpr auto kForceFetchHeaders =
std::to_array({HeaderNameAndValue{"cache-control", "no-cache"},
HeaderNameAndValue{"pragma", "no-cache"}});
// If the request includes one of these request headers, then force our
// cached copy (if any) to be revalidated before reusing it.
constexpr auto kForceValidateHeaders =
std::to_array({HeaderNameAndValue{"cache-control", "max-age=0"}});
bool HeaderMatches(const HttpRequestHeaders& headers,
base::span<const HeaderNameAndValue> search_headers) {
for (const auto& search_header : search_headers) {
std::optional<std::string> header_value =
headers.GetHeader(search_header.name);
if (!header_value) {
continue;
}
if (!search_header.value) {
return true;
}
HttpUtil::ValuesIterator v(*header_value, ',');
while (v.GetNext()) {
if (base::EqualsCaseInsensitiveASCII(v.value(), *search_header.value)) {
return true;
}
}
}
return false;
}
struct ValidationHeaderInfo {
std::string_view request_header_name;
std::string_view related_response_header_name;
};
constexpr auto kValidationHeaders = std::to_array<ValidationHeaderInfo>(
{{"if-modified-since", "last-modified"}, {"if-none-match", "etag"}});
} // namespace
int GetLoadFlagsForExtraHeaders(const HttpRequestHeaders& extra_headers) {
// Some headers imply load flags. The order here is significant.
//
// LOAD_DISABLE_CACHE : no cache read or write
// LOAD_BYPASS_CACHE : no cache read
// LOAD_VALIDATE_CACHE : no cache read unless validation
//
// The former modes trump latter modes, so if we find a matching header we
// can stop iterating kSpecialHeaders.
static const struct {
// RAW_PTR_EXCLUSION: Never allocated by PartitionAlloc (always points to
// constexpr tables), so there is no benefit to using a raw_ptr, only cost.
RAW_PTR_EXCLUSION const base::span<const HeaderNameAndValue> search;
int load_flag;
} kSpecialHeaders[] = {
{kPassThroughHeaders, LOAD_DISABLE_CACHE},
{kForceFetchHeaders, LOAD_BYPASS_CACHE},
{kForceValidateHeaders, LOAD_VALIDATE_CACHE},
};
for (const auto& special_header : kSpecialHeaders) {
if (HeaderMatches(extra_headers, special_header.search)) {
return special_header.load_flag;
}
}
static_assert(LOAD_NORMAL == 0);
return LOAD_NORMAL;
}
// static
base::expected<std::optional<ValidationHeaders>, std::string_view>
ValidationHeaders::MaybeCreate(const HttpRequestHeaders& extra_headers) {
static_assert(kNumValidationHeaders == std::size(kValidationHeaders),
"invalid number of validation headers");
ValidationHeaderValues values;
bool validation_header_found = false;
// Check for conditionalization headers which may correspond with a
// cache validation request.
for (size_t i = 0; i < std::size(kValidationHeaders); ++i) {
const ValidationHeaderInfo& info = kValidationHeaders[i];
if (std::optional<std::string> validation_value =
extra_headers.GetHeader(info.request_header_name)) {
if (validation_value->empty()) {
return base::unexpected("Empty validation header value found");
}
values[i] = std::move(*validation_value);
validation_header_found = true;
}
}
if (validation_header_found) {
return ValidationHeaders(std::move(values));
}
return std::nullopt;
}
ValidationHeaders::ValidationHeaders(ValidationHeaderValues values)
: values_(std::move(values)) {}
ValidationHeaders::~ValidationHeaders() = default;
ValidationHeaders::ValidationHeaders(ValidationHeaders&&) = default;
ValidationHeaders& ValidationHeaders::operator=(ValidationHeaders&&) = default;
bool ValidationHeaders::Match(
const HttpResponseHeaders& response_headers) const {
for (size_t i = 0; i < std::size(kValidationHeaders); i++) {
if (values_[i].empty()) {
continue;
}
// Retrieve either the cached response's "etag" or "last-modified" header.
std::optional<std::string_view> validator =
response_headers.EnumerateHeader(
nullptr, kValidationHeaders[i].related_response_header_name);
if (validator && *validator != values_[i]) {
return false;
}
}
return true;
}
} // namespace net::http_cache_util