| // 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 |