blob: bc29f345e9ff0d45921e53fb72c15ffbdae714a6 [file]
// Copyright 2026 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/public/cpp/shared_http_cache_client.h"
#include <optional>
#include <utility>
#include <vector>
#include "base/containers/hashing_lru_cache.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/hash/hash.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "base/synchronization/lock.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/sequence_bound.h"
#include "components/sqlite_vfs/pending_file_set.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "net/base/url_util.h"
#include "net/disk_cache/sql/sql_shared_cache_isolated_database_reader.h"
#include "net/filter/filter_source_stream.h"
#include "net/http/http_response_info.h"
#include "services/network/public/cpp/content_decoding_util.h"
#include "services/network/public/cpp/data_buffer_factory.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_http_cache_util.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "url/gurl.h"
namespace network {
namespace {
// A thread-safe LRU cache set of persistent URL hashes.
// Used to quickly determine whether a requested URL might exist in the
// shared HTTP cache without thread hopping or disk I/O.
class ThreadSafeSet : public base::RefCountedThreadSafe<ThreadSafeSet> {
public:
explicit ThreadSafeSet(size_t max_size);
ThreadSafeSet(const ThreadSafeSet&) = delete;
ThreadSafeSet& operator=(const ThreadSafeSet&) = delete;
void Initialize();
void Insert(const std::vector<uint32_t>& new_hashes);
void OnDisconnected();
bool IsDisconnected();
bool ShouldEarlyReturn(uint32_t hash);
private:
friend class base::RefCountedThreadSafe<ThreadSafeSet>;
~ThreadSafeSet();
const size_t max_size_;
base::Lock lock_;
std::optional<base::HashingLRUCacheSet<uint32_t>> set_ GUARDED_BY(lock_);
bool init_called_ GUARDED_BY(lock_) = false;
bool disconnected_ GUARDED_BY(lock_) = false;
};
// Implements network::mojom::SharedHttpCacheClient on `client_task_runner_`.
// Receives cache hash updates and inserts them into `shared_state_`.
class CacheClient : public network::mojom::SharedHttpCacheClient {
public:
CacheClient(mojo::PendingReceiver<network::mojom::SharedHttpCacheClient>
pending_receiver,
scoped_refptr<ThreadSafeSet> shared_state,
base::OnceClosure on_disconnect_callback)
: receiver_(this, std::move(pending_receiver)),
shared_state_(std::move(shared_state)),
on_disconnect_callback_(std::move(on_disconnect_callback)) {
receiver_.set_disconnect_handler(
base::BindOnce(&CacheClient::OnMojoDisconnect, base::Unretained(this)));
}
~CacheClient() override = default;
CacheClient(const CacheClient&) = delete;
CacheClient& operator=(const CacheClient&) = delete;
// network::mojom::SharedHttpCacheClient:
void OnResourcesAdded(const std::vector<uint32_t>& new_hashes) override {
shared_state_->Insert(new_hashes);
}
private:
void OnMojoDisconnect() {
shared_state_->OnDisconnected();
if (on_disconnect_callback_) {
std::move(on_disconnect_callback_).Run();
}
}
mojo::Receiver<network::mojom::SharedHttpCacheClient> receiver_;
const scoped_refptr<ThreadSafeSet> shared_state_;
base::OnceClosure on_disconnect_callback_;
};
// Implements network::mojom::SharedHttpCacheClientFactory and executes SQLite
// database reads on `database_task_runner_`.
class DatabaseBackend : public network::mojom::SharedHttpCacheClientFactory {
public:
DatabaseBackend(
mojo::PendingReceiver<network::mojom::SharedHttpCacheClientFactory>
pending_receiver,
scoped_refptr<base::SequencedTaskRunner> client_task_runner,
scoped_refptr<ThreadSafeSet> shared_state,
base::OnceClosure on_db_reader_initialized_callback)
: receiver_(this, std::move(pending_receiver)),
client_task_runner_(std::move(client_task_runner)),
shared_state_(std::move(shared_state)),
on_db_reader_initialized_callback_(
std::move(on_db_reader_initialized_callback)) {
receiver_.set_disconnect_handler(base::BindOnce(
&DatabaseBackend::OnMojoDisconnect, base::Unretained(this)));
}
~DatabaseBackend() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
DatabaseBackend(const DatabaseBackend&) = delete;
DatabaseBackend& operator=(const DatabaseBackend&) = delete;
// network::mojom::SharedHttpCacheClientFactory:
void CreateClient(sqlite_vfs::PendingFileSet pending_file_set,
mojo::PendingReceiver<network::mojom::SharedHttpCacheClient>
client_receiver) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (client_created_) {
receiver_.ReportBadMessage("Duplicate CreateClient call");
return;
}
client_created_ = true;
// Once `CreateClient` is called, registration lifetime transfers to
// `client_receiver`. Clear `receiver_`'s disconnect handler so closing the
// factory pipe does not invalidate the client.
receiver_.set_disconnect_handler(base::NullCallback());
db_reader_ =
std::make_unique<disk_cache::SqlSharedCacheIsolatedDatabaseReader>(
std::move(pending_file_set));
shared_state_->Initialize();
cache_client_.emplace(
client_task_runner_, std::move(client_receiver), shared_state_,
base::BindPostTaskToCurrentDefault(base::BindOnce(
&DatabaseBackend::OnMojoDisconnect, weak_factory_.GetWeakPtr())));
if (on_db_reader_initialized_callback_) {
std::move(on_db_reader_initialized_callback_).Run();
}
}
void FindResource(
const GURL& url,
base::TimeTicks request_start,
base::Time request_start_time,
scoped_refptr<DataBufferFactory> data_buffer_factory,
base::OnceCallback<void(std::optional<SharedHttpCacheClient::Response>)>
callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (shared_state_->IsDisconnected()) {
std::move(callback).Run(std::nullopt);
return;
}
CHECK(db_reader_);
const base::TimeTicks send_start = base::TimeTicks::Now();
const base::TimeTicks send_end = send_start;
auto response = db_reader_->ReadResponse(url.spec());
if (!response.has_value()) {
std::move(callback).Run(std::nullopt);
return;
}
CHECK(data_buffer_factory);
const auto body_size = response->GetBodySize();
auto body = data_buffer_factory->AllocateDataBuffer(body_size);
if (!response->ReadBody(body->data())) {
std::move(callback).Run(std::nullopt);
return;
}
base::ThreadPool::PostTask(
FROM_HERE, {base::TaskPriority::USER_BLOCKING},
base::BindOnce(
&DatabaseBackend::ParseAndDecode, std::move(data_buffer_factory),
request_start, request_start_time, send_start, send_end,
response->TakeHeaders(), std::move(body), std::move(callback)));
}
private:
void OnMojoDisconnect() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
shared_state_->OnDisconnected();
db_reader_.reset();
cache_client_.Reset();
}
static void ParseAndDecode(
scoped_refptr<DataBufferFactory> data_buffer_factory,
base::TimeTicks request_start,
base::Time request_start_time,
base::TimeTicks send_start,
base::TimeTicks send_end,
std::vector<uint8_t> headers_pickle,
std::unique_ptr<DataBuffer> body,
base::OnceCallback<void(std::optional<SharedHttpCacheClient::Response>)>
callback);
mojo::Receiver<network::mojom::SharedHttpCacheClientFactory> receiver_
GUARDED_BY_CONTEXT(sequence_checker_);
const scoped_refptr<base::SequencedTaskRunner> client_task_runner_;
const scoped_refptr<ThreadSafeSet> shared_state_;
base::OnceClosure on_db_reader_initialized_callback_
GUARDED_BY_CONTEXT(sequence_checker_);
bool client_created_ GUARDED_BY_CONTEXT(sequence_checker_) = false;
base::SequenceBound<CacheClient> cache_client_
GUARDED_BY_CONTEXT(sequence_checker_);
std::unique_ptr<disk_cache::SqlSharedCacheIsolatedDatabaseReader> db_reader_
GUARDED_BY_CONTEXT(sequence_checker_);
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<DatabaseBackend> weak_factory_{this};
};
// static
void DatabaseBackend::ParseAndDecode(
scoped_refptr<DataBufferFactory> data_buffer_factory,
base::TimeTicks request_start,
base::Time request_start_time,
base::TimeTicks send_start,
base::TimeTicks send_end,
std::vector<uint8_t> headers_pickle,
std::unique_ptr<DataBuffer> body,
base::OnceCallback<void(std::optional<SharedHttpCacheClient::Response>)>
callback) {
CHECK(body);
const base::TimeTicks receive_headers_start = base::TimeTicks::Now();
base::PickleIterator pickle_iter = base::PickleIterator::WithData(
base::as_bytes(base::span(headers_pickle)));
auto response_info = std::make_unique<net::HttpResponseInfo>();
bool response_truncated = false;
if (!response_info->InitFromPickle(pickle_iter, &response_truncated) ||
!response_info->headers) {
std::move(callback).Run(std::nullopt);
return;
}
const base::TimeTicks receive_headers_end = base::TimeTicks::Now();
// TODO(crbug.com/473666511): Add freshness check (e.g. Cache-Control:
// max-age, Expires, Age, Date headers) so that stale entries are not served
// directly and are instead revalidated via the network service.
//
// TODO(crbug.com/473666511): Support Stale-While-Revalidate (SWR) by serving
// the cached response while triggering an asynchronous revalidation request
// to the network service.
//
// TODO(crbug.com/473666511): Implement read-time security checks (CORS,
// ORB, CORP, SRI) to ensure responses stored under different request modes
// or before ORB rule updates are safe to serve for the current request.
network::mojom::URLResponseHeadPtr head =
network::mojom::URLResponseHead::New();
head->headers = response_info->headers;
head->request_time = response_info->request_time;
head->response_time = response_info->response_time;
head->original_response_time = response_info->original_response_time;
head->content_length = body->data().size();
head->headers->GetMimeTypeAndCharset(&head->mime_type, &head->charset);
head->was_fetched_via_cache = true;
head->remote_endpoint = response_info->remote_endpoint;
head->was_fetched_via_spdy = response_info->was_fetched_via_spdy;
head->was_alpn_negotiated = response_info->was_alpn_negotiated;
head->alpn_negotiated_protocol = response_info->alpn_negotiated_protocol;
head->alternate_protocol_usage = response_info->alternate_protocol_usage;
head->connection_info = response_info->connection_info;
head->network_accessed = false;
head->request_start = request_start;
head->response_start = receive_headers_end;
head->encoded_body_length = network::mojom::EncodedBodyLength::New(
response_info->encoded_body_size.has_value()
? response_info->encoded_body_size->InBytes()
: body->data().size());
head->encoded_data_length = response_info->headers->raw_headers().size() +
head->encoded_body_length->value;
head->load_timing.request_start = request_start;
head->load_timing.request_start_time = request_start_time;
head->load_timing.send_start = send_start;
head->load_timing.send_end = send_end;
head->load_timing.receive_headers_start = receive_headers_start;
head->load_timing.receive_headers_end = receive_headers_end;
const auto content_encoding_types =
net::FilterSourceStream::GetContentEncodingTypes(*response_info->headers);
if (content_encoding_types.empty()) {
auto buffers = data_buffer_factory->CreateDataBufferList();
CHECK(buffers);
if (!body->data().empty()) {
buffers->Append(std::move(body));
}
std::move(callback).Run(
SharedHttpCacheClient::Response(std::move(head), std::move(buffers)));
return;
}
auto buffers = ContentDecodingUtil::Decode(
body->data(), content_encoding_types, *data_buffer_factory.get());
if (!buffers) {
std::move(callback).Run(std::nullopt);
return;
}
std::move(callback).Run(
SharedHttpCacheClient::Response(std::move(head), std::move(buffers)));
}
class SharedHttpCacheClientImpl : public SharedHttpCacheClient {
public:
SharedHttpCacheClientImpl(
scoped_refptr<base::SequencedTaskRunner> client_task_runner,
scoped_refptr<base::SequencedTaskRunner> database_task_runner,
size_t max_cached_url_hashes);
SharedHttpCacheClientImpl(const SharedHttpCacheClientImpl&) = delete;
SharedHttpCacheClientImpl& operator=(const SharedHttpCacheClientImpl&) =
delete;
void Init(mojo::PendingReceiver<network::mojom::SharedHttpCacheClientFactory>
pending_receiver,
base::OnceClosure on_db_reader_initialized_callback);
void Find(
const network::ResourceRequest& request,
scoped_refptr<DataBufferFactory> data_buffer_factory,
base::OnceCallback<void(std::optional<Response>)> callback,
scoped_refptr<base::SequencedTaskRunner> callback_task_runner) override;
private:
friend class base::RefCountedThreadSafe<SharedHttpCacheClientImpl>;
~SharedHttpCacheClientImpl() override;
bool ShouldEarlyReturn(const GURL& url);
const scoped_refptr<base::SequencedTaskRunner> client_task_runner_;
const scoped_refptr<base::SequencedTaskRunner> database_task_runner_;
const scoped_refptr<ThreadSafeSet> shared_state_;
base::SequenceBound<DatabaseBackend> database_backend_;
};
ThreadSafeSet::ThreadSafeSet(size_t max_size) : max_size_(max_size) {
CHECK_GT(max_size_, 0u);
}
ThreadSafeSet::~ThreadSafeSet() = default;
void ThreadSafeSet::Initialize() {
base::AutoLock auto_lock(lock_);
init_called_ = true;
}
void ThreadSafeSet::Insert(const std::vector<uint32_t>& new_hashes) {
base::AutoLock auto_lock(lock_);
if (!set_.has_value()) {
set_.emplace(max_size_);
}
for (uint32_t hash : new_hashes) {
// `LRUCacheBase::Put(value_type&&)` only accepts rvalues.
set_->Put(std::move(hash));
}
}
void ThreadSafeSet::OnDisconnected() {
base::AutoLock auto_lock(lock_);
disconnected_ = true;
set_.reset();
}
bool ThreadSafeSet::IsDisconnected() {
base::AutoLock auto_lock(lock_);
return disconnected_;
}
bool ThreadSafeSet::ShouldEarlyReturn(uint32_t hash) {
base::AutoLock auto_lock(lock_);
if (disconnected_ || !init_called_) {
// TODO(crbug.com/473666511): Consider making early-return vs queuing before
// database initialization configurable via a base::FeatureParam.
return true;
}
if (!set_.has_value()) {
return false;
}
return set_->Get(hash) == set_->end();
}
SharedHttpCacheClientImpl::SharedHttpCacheClientImpl(
scoped_refptr<base::SequencedTaskRunner> client_task_runner,
scoped_refptr<base::SequencedTaskRunner> database_task_runner,
size_t max_cached_url_hashes)
: client_task_runner_(std::move(client_task_runner)),
database_task_runner_(std::move(database_task_runner)),
shared_state_(
base::MakeRefCounted<ThreadSafeSet>(max_cached_url_hashes)) {
CHECK(client_task_runner_);
CHECK(database_task_runner_);
}
SharedHttpCacheClientImpl::~SharedHttpCacheClientImpl() = default;
void SharedHttpCacheClientImpl::Init(
mojo::PendingReceiver<network::mojom::SharedHttpCacheClientFactory>
pending_receiver,
base::OnceClosure on_db_reader_initialized_callback) {
CHECK(pending_receiver);
database_backend_ = base::SequenceBound<DatabaseBackend>(
database_task_runner_, std::move(pending_receiver), client_task_runner_,
shared_state_, std::move(on_db_reader_initialized_callback));
}
bool SharedHttpCacheClientImpl::ShouldEarlyReturn(const GURL& url) {
if (!url.is_valid()) {
return true;
}
const auto hash =
base::PersistentHash(net::SimplifyUrlForRequest(url).spec());
return shared_state_->ShouldEarlyReturn(hash);
}
void SharedHttpCacheClientImpl::Find(
const network::ResourceRequest& request,
scoped_refptr<DataBufferFactory> data_buffer_factory,
base::OnceCallback<void(std::optional<Response>)> callback,
scoped_refptr<base::SequencedTaskRunner> callback_task_runner) {
if (!IsRequestEligibleForSharedHttpCacheLookup(request)) {
std::move(callback).Run(std::nullopt);
return;
}
const base::TimeTicks request_start = base::TimeTicks::Now();
const base::Time request_start_time = base::Time::Now();
if (ShouldEarlyReturn(request.url)) {
std::move(callback).Run(std::nullopt);
return;
}
database_backend_.AsyncCall(&DatabaseBackend::FindResource)
.WithArgs(request.url, request_start, request_start_time,
std::move(data_buffer_factory),
base::BindPostTask(std::move(callback_task_runner),
std::move(callback)));
}
} // namespace
SharedHttpCacheClient::Response::Response(
network::mojom::URLResponseHeadPtr head,
std::unique_ptr<DataBufferList> body)
: head(std::move(head)), body(std::move(body)) {}
SharedHttpCacheClient::Response::~Response() = default;
SharedHttpCacheClient::Response::Response(Response&&) = default;
SharedHttpCacheClient::Response& SharedHttpCacheClient::Response::operator=(
Response&&) = default;
// static
scoped_refptr<SharedHttpCacheClient> SharedHttpCacheClient::CreateAndInit(
mojo::PendingReceiver<network::mojom::SharedHttpCacheClientFactory>
pending_receiver,
scoped_refptr<base::SequencedTaskRunner> client_task_runner,
scoped_refptr<base::SequencedTaskRunner> database_task_runner,
base::OnceClosure on_db_reader_initialized_callback,
size_t max_cached_url_hashes) {
auto impl = base::MakeRefCounted<SharedHttpCacheClientImpl>(
std::move(client_task_runner), std::move(database_task_runner),
max_cached_url_hashes);
impl->Init(std::move(pending_receiver),
std::move(on_db_reader_initialized_callback));
return impl;
}
} // namespace network