| // 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/origin_trials.h" |
| |
| #include <algorithm> |
| #include <string_view> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_map.h" |
| #include "base/containers/flat_set.h" |
| #include "components/origin_trials/common/persisted_trial_token.h" |
| #include "net/base/schemeful_site.h" |
| #include "third_party/blink/public/common/origin_trials/origin_trials.h" |
| #include "third_party/blink/public/common/origin_trials/trial_token.h" |
| #include "third_party/blink/public/common/origin_trials/trial_token_result.h" |
| #include "third_party/blink/public/common/origin_trials/trial_token_validator.h" |
| #include "url/origin.h" |
| |
| namespace origin_trials { |
| |
| namespace { |
| // A string to denote opaque origins for partitioning. It should not be |
| // possible to have a valid origin serialize to this value. |
| const char kOpaqueOriginPartitionKey[] = ":opaque"; |
| } // namespace |
| |
| OriginTrials::OriginTrials( |
| std::unique_ptr<OriginTrialsPersistenceProvider> persistence_provider, |
| std::unique_ptr<blink::TrialTokenValidator> token_validator) |
| : persistence_provider_(std::move(persistence_provider)), |
| trial_token_validator_(std::move(token_validator)) {} |
| |
| OriginTrials::~OriginTrials() = default; |
| |
| void OriginTrials::AddObserver(Observer* observer) { |
| CHECK(observer); |
| observer_map_[observer->trial_name()].AddObserver(observer); |
| } |
| |
| void OriginTrials::RemoveObserver(Observer* observer) { |
| CHECK(observer); |
| auto it = observer_map_.find(observer->trial_name()); |
| if (it == observer_map_.end()) { |
| return; |
| } |
| it->second.RemoveObserver(observer); |
| if (it->second.empty()) { |
| observer_map_.erase(it); |
| } |
| } |
| |
| void OriginTrials::NotifyStatusChange( |
| const std::string& trial, |
| const OriginTrialStatusChangeDetails& details) { |
| const auto find_it = observer_map_.find(trial); |
| if (find_it == observer_map_.end()) { |
| return; |
| } |
| |
| for (Observer& observer : find_it->second) { |
| observer.OnStatusChanged(details); |
| } |
| } |
| |
| void OriginTrials::NotifyPersistedTokensCleared() { |
| for (const auto& it : observer_map_) { |
| for (Observer& observer : it.second) { |
| observer.OnPersistedTokensCleared(); |
| } |
| } |
| } |
| |
| bool OriginTrials::MatchesTokenOrigin(const url::Origin& token_origin, |
| bool match_subdomains, |
| const url::Origin& origin) const { |
| if (match_subdomains) { |
| return origin.scheme() == token_origin.scheme() && |
| origin.DomainIs(token_origin.host()) && |
| origin.port() == token_origin.port(); |
| } |
| |
| return origin == token_origin; |
| } |
| |
| base::flat_set<std::string> OriginTrials::GetPersistedTrialsForOrigin( |
| const url::Origin& origin, |
| const url::Origin& partition_origin, |
| const base::Time current_time) { |
| return GetPersistedTrialsForOriginWithMatch(origin, partition_origin, |
| current_time, std::nullopt); |
| } |
| |
| bool OriginTrials::IsFeaturePersistedForOrigin( |
| const url::Origin& origin, |
| const url::Origin& partition_origin, |
| blink::mojom::OriginTrialFeature feature, |
| const base::Time current_time) { |
| return !GetPersistedTrialsForOriginWithMatch(origin, partition_origin, |
| current_time, feature) |
| .empty(); |
| } |
| |
| void OriginTrials::PersistTrialsFromTokens( |
| const url::Origin& origin, |
| const url::Origin& partition_origin, |
| const base::span<const std::string> header_tokens, |
| const base::Time current_time, |
| std::optional<ukm::SourceId> source_id) { |
| PersistTokensInternal(origin, partition_origin, /*script_origins=*/{}, |
| header_tokens, current_time, source_id, |
| /*append_only=*/false); |
| } |
| |
| void OriginTrials::PersistAdditionalTrialsFromTokens( |
| const url::Origin& origin, |
| const url::Origin& partition_origin, |
| base::span<const url::Origin> script_origins, |
| const base::span<const std::string> header_tokens, |
| const base::Time current_time, |
| std::optional<ukm::SourceId> source_id) { |
| PersistTokensInternal(origin, partition_origin, script_origins, header_tokens, |
| current_time, source_id, |
| /*append_only=*/true); |
| } |
| |
| void OriginTrials::PersistTokensInternal( |
| const url::Origin& origin, |
| const url::Origin& partition_origin, |
| base::span<const url::Origin> script_origins, |
| const base::span<const std::string> header_tokens, |
| const base::Time current_time, |
| std::optional<ukm::SourceId> source_id, |
| bool append_only) { |
| if (origin.opaque()) { |
| return; |
| } |
| |
| base::flat_map<url::Origin, std::vector<blink::TrialToken>> valid_tokens; |
| if (!append_only) { |
| // Explicitly initialize the entry for the first-party origin since an empty |
| // vector means tokens should be cleared if `append_only` is false. |
| valid_tokens[origin] = {}; |
| } |
| |
| // Parse the provided tokens |
| for (const std::string_view token : header_tokens) { |
| blink::TrialTokenResult validation_result = |
| trial_token_validator_->ValidateTokenAndTrial( |
| token, origin, script_origins, current_time); |
| |
| const blink::TrialToken* parsed_token = validation_result.ParsedToken(); |
| |
| if (validation_result.Status() != blink::OriginTrialTokenStatus::kSuccess) { |
| continue; |
| } |
| if (!blink::origin_trials::IsTrialPersistentToNextResponse( |
| parsed_token->feature_name())) { |
| continue; |
| } |
| // TODO(crbug.com/40189223): Should be part of general validation logic. |
| if (!trial_token_validator_->TrialEnablesFeaturesForOS( |
| parsed_token->feature_name())) { |
| continue; |
| } |
| if (parsed_token->is_third_party()) { |
| // TODO(crbug.com/40257643): Support for all third-party tokens. |
| // Only accept deprecation trials as third-party for now. |
| bool deprecation_trial = false; |
| for (const blink::mojom::OriginTrialFeature feature : |
| blink::origin_trials::FeaturesForTrial( |
| parsed_token->feature_name())) { |
| deprecation_trial |= blink::origin_trials::GetTrialType(feature) == |
| blink::OriginTrialType::kDeprecation; |
| } |
| if (deprecation_trial) { |
| // Valid third-party tokens are saved using the origin stored in the |
| // token. |
| valid_tokens[parsed_token->origin()].push_back( |
| std::move(*parsed_token)); |
| } |
| } else { |
| valid_tokens[parsed_token->origin()].push_back(std::move(*parsed_token)); |
| } |
| } |
| std::string partition_site = GetTokenPartitionSite(partition_origin); |
| for (const auto& origin_token_pair : valid_tokens) { |
| UpdatePersistedTokenSet(origin, origin_token_pair.first, |
| origin_token_pair.second, partition_site, source_id, |
| append_only); |
| } |
| } |
| |
| base::flat_set<std::string> OriginTrials::GetPersistedTrialsForOriginWithMatch( |
| const url::Origin& origin, |
| const url::Origin& partition_origin, |
| const base::Time current_time, |
| const std::optional<blink::mojom::OriginTrialFeature> trial_feature_match) |
| const { |
| if (origin.opaque()) { |
| return {}; |
| } |
| |
| SiteOriginTrialTokens potential_tokens = |
| persistence_provider_->GetPotentialPersistentTrialTokens(origin); |
| |
| base::flat_set<std::string> enabled_trials; |
| for (const auto& [token_origin, saved_tokens] : potential_tokens) { |
| for (const PersistedTrialToken& token : saved_tokens) { |
| if (trial_feature_match && |
| // TODO(crbug.com/40189223): FeaturesEnabledByTrial should be part of |
| // general validation logic. |
| !base::Contains( |
| trial_token_validator_->FeaturesEnabledByTrial(token.trial_name), |
| trial_feature_match.value())) { |
| continue; |
| } |
| if (!MatchesTokenOrigin(token_origin, token.match_subdomains, origin)) { |
| continue; |
| } |
| |
| bool valid = trial_token_validator_->RevalidateTokenAndTrial( |
| token.trial_name, token.token_expiry, token.usage_restriction, |
| token.token_signature, current_time); |
| bool persistent = blink::origin_trials::IsTrialPersistentToNextResponse( |
| token.trial_name); |
| if (valid && persistent && |
| token.partition_sites.contains( |
| GetTokenPartitionSite(partition_origin))) { |
| // Move the string into the flat_set to avoid extra heap allocations |
| enabled_trials.insert(std::move(token.trial_name)); |
| } |
| } |
| } |
| |
| return enabled_trials; |
| } |
| |
| void OriginTrials::ClearPersistedTokens() { |
| persistence_provider_->ClearPersistedTokens(); |
| NotifyPersistedTokensCleared(); |
| } |
| |
| // static |
| std::string OriginTrials::GetTokenPartitionSite(const url::Origin& origin) { |
| if (origin.opaque()) { |
| return kOpaqueOriginPartitionKey; |
| } |
| return net::SchemefulSite(origin).Serialize(); |
| } |
| |
| void OriginTrials::UpdatePersistedTokenSet( |
| const url::Origin& document_origin, |
| const url::Origin& token_origin, |
| base::span<const blink::TrialToken> new_tokens, |
| const std::string& partition_site, |
| std::optional<ukm::SourceId> source_id, |
| bool append_only) { |
| if (append_only && new_tokens.empty()) { |
| return; // Nothing to do. |
| } |
| |
| base::flat_set<PersistedTrialToken> token_set = |
| persistence_provider_->GetPersistentTrialTokens(token_origin); |
| |
| if (!append_only) { |
| // First, clean up token registrations for this origin and partition |
| // by removing any trials in the active partition that aren't being set |
| // by the new parameters. |
| for (PersistedTrialToken& token : token_set) { |
| const auto new_token_iter = |
| std::find_if(new_tokens.begin(), new_tokens.end(), |
| [&token](const blink::TrialToken& trial_token) { |
| return token.Matches(trial_token); |
| }); |
| |
| // Remove registration of the token for the first party or top-level site |
| // partition. |
| if (new_token_iter == new_tokens.end()) { |
| // Tokens that match subdomains should only be removed when not included |
| // by a document loaded from the token origin. |
| if (!token.match_subdomains || (document_origin == token_origin)) { |
| token.RemoveFromPartition(partition_site); |
| NotifyStatusChange( |
| token.trial_name, |
| OriginTrialStatusChangeDetails(token_origin, partition_site, |
| token.match_subdomains, |
| /*enabled=*/false, source_id)); |
| } |
| } |
| } |
| // Cleanup of tokens no longer in any partitions. |
| base::EraseIf(token_set, [](const PersistedTrialToken& token) { |
| return !token.InAnyPartition(); |
| }); |
| } |
| |
| // Update the set with new partition information. |
| for (const blink::TrialToken& new_token : new_tokens) { |
| const auto found_token = |
| std::find_if(token_set.begin(), token_set.end(), |
| [&new_token](const PersistedTrialToken& existing_token) { |
| return existing_token.Matches(new_token); |
| }); |
| |
| if (found_token != token_set.end()) { |
| // The status of the trial for `origin` under `partition_site` (with this |
| // token/signature) only changes if the token was not already logically in |
| // the top-level partition of `partition_site`. |
| // NOTE: This is because `found_token` can "match" `new_token` without |
| // `found_token->partition_sites` containing `partition_site`. |
| if (!found_token->partition_sites.contains(partition_site)) { |
| NotifyStatusChange( |
| found_token->trial_name, |
| OriginTrialStatusChangeDetails(token_origin, partition_site, |
| found_token->match_subdomains, |
| /*enabled=*/true, source_id)); |
| } |
| |
| // Update the existing stored trial token with the metadata fields, as it |
| // may be a newly issued token. |
| found_token->AddToPartition(partition_site); |
| } else { |
| token_set.emplace(new_token, partition_site); |
| NotifyStatusChange( |
| new_token.feature_name(), |
| OriginTrialStatusChangeDetails(token_origin, partition_site, |
| new_token.match_subdomains(), |
| /*enabled=*/true, source_id)); |
| } |
| } |
| persistence_provider_->SavePersistentTrialTokens(token_origin, |
| std::move(token_set)); |
| } |
| |
| } // namespace origin_trials |