| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/origin_trials/browser/leveldb_persistence_provider.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/flat_map.h" |
| #include "base/containers/flat_set.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_util.h" |
| #include "base/synchronization/lock.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/platform_thread.h" |
| #include "base/time/time.h" |
| #include "components/leveldb_proto/public/proto_database_provider.h" |
| #include "components/leveldb_proto/public/shared_proto_database_client_list.h" |
| #include "components/origin_trials/common/persisted_trial_token.h" |
| #include "components/origin_trials/proto/db_trial_token.pb.h" |
| #include "components/origin_trials/proto/proto_util.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "net/base/schemeful_site.h" |
| #include "third_party/blink/public/common/origin_trials/trial_token_validator.h" |
| #include "url/origin.h" |
| |
| namespace origin_trials { |
| |
| namespace { |
| |
| // Check to see if |token_origin_host| has any of the strings in |
| // |partition_sites| as a suffix. |
| bool HasFirstPartyPartition( |
| const url::Origin token_origin_host, |
| const google::protobuf::RepeatedPtrField<std::string>& partition_sites) { |
| std::string host_key = net::SchemefulSite(token_origin_host).Serialize(); |
| for (const std::string& site : partition_sites) { |
| if (site == host_key) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| const base::FilePath::StringViewType kPersistentTrialTokenDbPath = |
| FILE_PATH_LITERAL("PersistentOriginTrials"); |
| |
| std::unique_ptr<LevelDbPersistenceProvider::DbLoadResult> BuildMapFromDb( |
| std::unique_ptr<std::vector<origin_trials_pb::TrialTokenDbEntries>> |
| entries) { |
| // Build new maps |
| std::unique_ptr<OriginTrialMap> new_origin_trial_map = |
| std::make_unique<OriginTrialMap>(); |
| std::unique_ptr<SiteOriginsMap> new_site_origins_map = |
| std::make_unique<SiteOriginsMap>(); |
| |
| // Keep track of values to clean up |
| blink::TrialTokenValidator validator; |
| std::unique_ptr<ProtoKeyVector> keys_to_delete = |
| std::make_unique<ProtoKeyVector>(); |
| std::unique_ptr<OriginTrialMap> entries_to_update = |
| std::make_unique<OriginTrialMap>(); |
| base::Time current_time = base::Time::Now(); |
| |
| // Move values out of the protobuffer to avoid allocating new strings |
| for (auto& entry : *entries) { |
| origin_trials_pb::OriginMessage* source_origin = entry.mutable_origin(); |
| url::Origin origin = url::Origin::CreateFromNormalizedTuple( |
| std::move(*source_origin->mutable_scheme()), |
| std::move(*source_origin->mutable_host()), source_origin->port()); |
| SiteKey mapKey = LevelDbPersistenceProvider::GetSiteKey(origin); |
| base::flat_set<PersistedTrialToken> new_tokens; |
| auto* mutable_tokens = entry.mutable_tokens(); |
| size_t stored_count = entry.mutable_tokens()->size(); |
| |
| for (auto it = mutable_tokens->begin(); it < mutable_tokens->end(); ++it) { |
| base::Time token_expiry = |
| origin_trials_pb::DeserializeTime(it->token_expiry()); |
| auto usage_restriction = static_cast<blink::TrialToken::UsageRestriction>( |
| it->usage_restriction()); |
| bool valid = validator.RevalidateTokenAndTrial( |
| it->trial_name(), token_expiry, usage_restriction, |
| it->token_signature(), current_time); |
| |
| if (valid) { |
| UMA_HISTOGRAM_COUNTS_100( |
| "OriginTrials.PersistentOriginTrial.PartitionSetSize", |
| it->partition_sites().size()); |
| UMA_HISTOGRAM_BOOLEAN( |
| "OriginTrials.PersistentOriginTrial.TokenHasFirstPartyPartition", |
| HasFirstPartyPartition(origin, it->partition_sites())); |
| // Move the strings out of the protobuffer to avoid allocations |
| base::flat_set<std::string> partition_sites; |
| for (std::string& site : *it->mutable_partition_sites()) { |
| partition_sites.insert(std::move(site)); |
| } |
| new_tokens.emplace(it->match_subdomains(), |
| std::move(*it->mutable_trial_name()), token_expiry, |
| usage_restriction, |
| std::move(*it->mutable_token_signature()), |
| std::move(partition_sites)); |
| } |
| } |
| |
| if (new_tokens.empty()) { |
| keys_to_delete->push_back(origin.Serialize()); |
| } else if (new_tokens.size() < stored_count) { |
| entries_to_update->insert_or_assign(origin, new_tokens); |
| } |
| |
| if (!new_tokens.empty()) { |
| new_origin_trial_map->insert_or_assign(origin, std::move(new_tokens)); |
| |
| SiteKey site_key = LevelDbPersistenceProvider::GetSiteKey(origin); |
| auto find_it = new_site_origins_map->find(site_key); |
| if (find_it == new_site_origins_map->end()) { |
| new_site_origins_map->insert_or_assign( |
| std::move(site_key), |
| base::flat_set<url::Origin>({std::move(origin)})); |
| } else { |
| find_it->second.insert(std::move(origin)); |
| } |
| } |
| } |
| return std::make_unique<LevelDbPersistenceProvider::DbLoadResult>( |
| std::move(new_origin_trial_map), std::move(keys_to_delete), |
| std::move(entries_to_update), std::move(new_site_origins_map)); |
| } |
| |
| } // namespace |
| |
| // Public constructor |
| LevelDbPersistenceProvider::LevelDbPersistenceProvider( |
| const base::FilePath& profile_dir, |
| leveldb_proto::ProtoDatabaseProvider* db_provider) |
| : LevelDbPersistenceProvider( |
| db_provider->GetDB<origin_trials_pb::TrialTokenDbEntries>( |
| leveldb_proto::ProtoDbType::PERSISTENT_ORIGIN_TRIALS, |
| profile_dir.Append(kPersistentTrialTokenDbPath), |
| base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::USER_BLOCKING}))) {} |
| |
| // Private constructor |
| LevelDbPersistenceProvider::LevelDbPersistenceProvider( |
| std::unique_ptr< |
| leveldb_proto::ProtoDatabase<origin_trials_pb::TrialTokenDbEntries>> |
| database) |
| : db_loaded_(false), |
| db_(std::move(database)), |
| trial_status_cache_(std::make_unique<OriginTrialMap>()), |
| site_origins_map_(std::make_unique<SiteOriginsMap>()), |
| database_load_start_(base::TimeTicks::Now()), |
| lookups_before_db_loaded_(0) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| db_->Init(base::BindOnce(&LevelDbPersistenceProvider::OnDbInitialized, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| // static |
| std::unique_ptr<LevelDbPersistenceProvider> |
| LevelDbPersistenceProvider::CreateForTesting( |
| std::unique_ptr< |
| leveldb_proto::ProtoDatabase<origin_trials_pb::TrialTokenDbEntries>> |
| db) { |
| return base::WrapUnique(new LevelDbPersistenceProvider(std::move(db))); |
| } |
| |
| LevelDbPersistenceProvider::~LevelDbPersistenceProvider() = default; |
| |
| void LevelDbPersistenceProvider::OnDbInitialized( |
| leveldb_proto::Enums::InitStatus status) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| base::UmaHistogramBoolean( |
| "OriginTrials.PersistentOriginTrial.LevelDbInitSuccess", |
| status == leveldb_proto::Enums::kOK); |
| if (status != leveldb_proto::Enums::kOK) { |
| DLOG(ERROR) |
| << "PersistentTrialToken database initialization failed with status: " |
| << status; |
| return; |
| } |
| // Trigger a cache load from the DB |
| db_->LoadEntries(base::BindOnce(&LevelDbPersistenceProvider::OnDbLoad, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void LevelDbPersistenceProvider::OnDbLoad( |
| bool success, |
| std::unique_ptr<std::vector<origin_trials_pb::TrialTokenDbEntries>> |
| entries) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| base::UmaHistogramBoolean( |
| "OriginTrials.PersistentOriginTrial.LevelDbLoadSuccess", success); |
| if (!success) |
| return; |
| |
| base::UmaHistogramCounts1000( |
| "OriginTrials.PersistentOriginTrial.LevelDbLoadSize", entries->size()); |
| |
| // Converting the proto data to a flat_map is potentially expensive, so do it |
| // asynchronously. |
| base::ThreadPool::PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&BuildMapFromDb, std::move(entries)), |
| base::BindOnce(&LevelDbPersistenceProvider::OnMapBuild, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void LevelDbPersistenceProvider::OnMapBuild( |
| std::unique_ptr<DbLoadResult> result) { |
| base::UmaHistogramCounts100( |
| "OriginTrials.PersistentOriginTrial.OriginsAddedBeforeDbLoad", |
| trial_status_cache_->size()); |
| |
| // Add/update any keys that were inserted after the data load was triggered. |
| MergeCacheIntoLoadResult(*result); |
| |
| // Activate the new cache. |
| trial_status_cache_.swap(result->result_origin_trial_map); |
| site_origins_map_.swap(result->result_site_origins_map); |
| |
| // Update the database with any changes or deletions caused by expired |
| // tokens or pre-load writes. |
| if (!result->updated_entries->empty() || !result->expired_keys->empty()) { |
| std::unique_ptr<ProtoKeyEntryVector> update_vector = |
| std::make_unique<ProtoKeyEntryVector>(); |
| for (auto& update : *(result->updated_entries)) { |
| update_vector->emplace_back( |
| update.first.Serialize(), |
| origin_trials_pb::ProtoFromTokens(update.first, update.second)); |
| } |
| db_->UpdateEntries(std::move(update_vector), |
| std::move(result->expired_keys), base::DoNothing()); |
| } |
| |
| // Done loading, record how long it took. |
| base::UmaHistogramTimes("OriginTrials.PersistentOriginTrial.LevelDbLoadTime", |
| base::TimeTicks::Now() - database_load_start_); |
| base::UmaHistogramCounts100( |
| "OriginTrials.PersistentOriginTrial.OriginLookupsBeforeDbLoad", |
| lookups_before_db_loaded_); |
| db_loaded_ = true; // Stop counting |
| } |
| |
| void LevelDbPersistenceProvider::MergeCacheIntoLoadResult( |
| DbLoadResult& result) { |
| // Merge the `site_origins_map_` into the `result.result_site_origins_map` |
| // to update the mappings created loading the token entries from the database. |
| for (const auto& [cache_site, cache_origins] : *site_origins_map_) { |
| auto find_iter = result.result_site_origins_map->find(cache_site); |
| if (find_iter == result.result_site_origins_map->end()) { |
| // This is a completely new site, add it directly. |
| (*result.result_site_origins_map)[cache_site] = cache_origins; |
| |
| } else { |
| // The `result.result_site_origins_map` already has a set of origins for |
| // the `cache_site`, merge the `cache_origins` into it. |
| find_iter->second.insert(cache_origins.begin(), cache_origins.end()); |
| } |
| } |
| |
| // Merge the |trial_status_cache_| into the |result.result_origin_trial_map| |
| // to update the entries loaded from the database. |
| for (const auto& [cache_origin, cache_tokens] : *trial_status_cache_) { |
| auto find_iter = result.result_origin_trial_map->find(cache_origin); |
| if (find_iter == result.result_origin_trial_map->end()) { |
| // This is a completely new origin, add it directly. |
| (*result.result_origin_trial_map)[cache_origin] = cache_tokens; |
| |
| } else { |
| // The |result.result_origin_trial_map| already has a set of tokens for |
| // the |cache_origin|, merge the |cache_tokens| into the |
| // |result.result_origin_trial_map|. |
| base::flat_set<PersistedTrialToken>& destination_set = find_iter->second; |
| for (const PersistedTrialToken& token : cache_tokens) { |
| auto token_iter = destination_set.find(token); |
| if (token_iter == destination_set.end()) { |
| // No comparable token exists in the set, insert the new one |
| destination_set.insert(token); |
| } else { |
| // The token already exists, insert the cached partition sites. |
| token_iter->partition_sites.insert(token.partition_sites.begin(), |
| token.partition_sites.end()); |
| } |
| } |
| } |
| } |
| |
| // Ensure changes are written back to the database as well by updating |
| // modified entries in |result.updated_entries|. |
| for (const auto& [cache_origin, ignored] : *trial_status_cache_) { |
| (*result.updated_entries)[cache_origin] = |
| (*result.result_origin_trial_map)[cache_origin]; |
| } |
| } |
| |
| void LevelDbPersistenceProvider::UpdateSiteToOriginsMap( |
| const url::Origin& origin, |
| bool insert) { |
| // If `insert` is false, removes `origin` from the set of origins for its |
| // SiteKey. If `insert` is true, adds `origin` to the set of origins for its |
| // SiteKey. |
| SiteKey site_key = GetSiteKey(origin); |
| auto find_it = site_origins_map_->find(site_key); |
| bool site_mapped = (find_it != site_origins_map_->end()); |
| bool origin_mapped = (site_mapped && find_it->second.contains(origin)); |
| |
| bool needs_create = insert && !site_mapped; |
| bool needs_update = insert && (site_mapped && !origin_mapped); |
| bool needs_delete = !insert && origin_mapped; |
| |
| if (needs_delete) { |
| // There exists an origin set for `site_key` and `origin` needs to be |
| // removed from it. |
| find_it->second.erase(origin); |
| |
| if (find_it->second.empty()) { |
| site_origins_map_->erase(find_it); |
| } |
| } else if (needs_update) { |
| // There exists an origin set for `site_key` and `origin` needs to be |
| // added to it. |
| find_it->second.insert(origin); |
| } else if (needs_create) { |
| // There is not an existing origin set for `site_key`, but one needs to be |
| // created (containing `origin`). |
| site_origins_map_->insert_or_assign(find_it, site_key, |
| base::flat_set<url::Origin>({origin})); |
| } |
| } |
| |
| base::flat_set<PersistedTrialToken> |
| LevelDbPersistenceProvider::GetPersistentTrialTokens( |
| const url::Origin& origin) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| if (!db_loaded_) |
| lookups_before_db_loaded_++; |
| |
| auto find_it = trial_status_cache_->find(origin); |
| if (find_it != trial_status_cache_->end()) |
| return find_it->second; |
| return base::flat_set<PersistedTrialToken>(); |
| } |
| |
| SiteOriginTrialTokens |
| LevelDbPersistenceProvider::GetPotentialPersistentTrialTokens( |
| const url::Origin& origin) { |
| SiteOriginTrialTokens site_tokens; |
| auto site_iter = site_origins_map_->find(GetSiteKey(origin)); |
| |
| if (site_iter == site_origins_map_->end()) { |
| return site_tokens; |
| } |
| |
| for (const auto& token_origin : site_iter->second) { |
| auto token_origin_iter = trial_status_cache_->find(token_origin); |
| if (token_origin_iter != trial_status_cache_->end()) { |
| site_tokens.emplace_back(token_origin, token_origin_iter->second); |
| } |
| } |
| |
| return site_tokens; |
| } |
| |
| void LevelDbPersistenceProvider::SavePersistentTrialTokens( |
| const url::Origin& origin, |
| const base::flat_set<PersistedTrialToken>& tokens) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| // Check if we need to update or delete the entry |
| auto find_it = trial_status_cache_->find(origin); |
| bool needs_delete = tokens.empty() && find_it != trial_status_cache_->end(); |
| bool needs_update = |
| !tokens.empty() && |
| (find_it == trial_status_cache_->end() || find_it->second != tokens); |
| |
| if (!(needs_delete || needs_update)) |
| return; |
| |
| if (needs_update) |
| trial_status_cache_->insert_or_assign(find_it, origin, tokens); |
| else |
| trial_status_cache_->erase(find_it); |
| |
| UpdateSiteToOriginsMap(origin, !needs_delete); |
| |
| if (db_loaded_) { |
| std::string origin_key = origin.Serialize(); |
| std::unique_ptr<ProtoKeyEntryVector> entries_to_save = |
| std::make_unique<ProtoKeyEntryVector>(); |
| std::unique_ptr<ProtoKeyVector> keys_to_remove = |
| std::make_unique<ProtoKeyVector>(); |
| |
| if (needs_update) { |
| entries_to_save->emplace_back( |
| origin_key, origin_trials_pb::ProtoFromTokens(origin, tokens)); |
| } else |
| keys_to_remove->push_back(origin_key); |
| |
| db_->UpdateEntries(std::move(entries_to_save), std::move(keys_to_remove), |
| base::DoNothing()); |
| } |
| } |
| |
| void LevelDbPersistenceProvider::ClearPersistedTokens() { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| trial_status_cache_ = std::make_unique<OriginTrialMap>(); |
| site_origins_map_ = std::make_unique<SiteOriginsMap>(); |
| db_->UpdateEntriesWithRemoveFilter( |
| std::make_unique<ProtoKeyEntryVector>(), |
| base::BindRepeating([](const std::string& key) { return true; }), |
| base::DoNothing()); |
| } |
| |
| LevelDbPersistenceProvider::DbLoadResult::DbLoadResult( |
| std::unique_ptr<OriginTrialMap> new_origin_trial_map, |
| std::unique_ptr<ProtoKeyVector> keys_to_delete, |
| std::unique_ptr<OriginTrialMap> entries_to_update, |
| std::unique_ptr<SiteOriginsMap> new_site_origins_map) |
| : result_origin_trial_map(std::move(new_origin_trial_map)), |
| expired_keys(std::move(keys_to_delete)), |
| updated_entries(std::move(entries_to_update)), |
| result_site_origins_map(std::move(new_site_origins_map)) {} |
| |
| LevelDbPersistenceProvider::DbLoadResult::~DbLoadResult() = default; |
| |
| } // namespace origin_trials |