[go: nahoru, domu]

blob: 387946681b3e42b85ced16633ae90db3e13a55fe [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/extensions/cws_info_service.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/queue.h"
#include "base/i18n/time_formatting.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "chrome/browser/extensions/cws_info_service_factory.h"
#include "chrome/browser/extensions/cws_item_service.pb.h"
#include "chrome/browser/extensions/extension_management.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/pref_names.h"
#include "google_apis/google_api_keys.h"
#include "net/base/load_flags.h"
#include "net/http/http_status_code.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
namespace {
constexpr int kMaxExtensionIdsPerRequest = 3;
constexpr int kMaxRetriesPerRequest = 2;
// Default check and fetch intervals.
constexpr int kCheckIntervalSeconds = 1 * 60 * 60;
constexpr int kFetchIntervalSeconds = 24 * 60 * 60;
// Fast mode check and fetch intervals. These intervals are used to
// facilitate end-end testing.
constexpr int kFastStartupCheckDelaySeconds = 30;
constexpr int kFastCheckIntervalSeconds = 1 * 60;
constexpr int kFastFetchIntervalSeconds = 3 * 60;
constexpr char kRequestUrl[] =
"https://chromewebstore.googleapis.com/v2/items/-/storeMetadata:batchGet";
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("cws_info_service", R"(
semantics {
sender: "CWS Info Service"
description:
"Sends ids of currently installed extensions that update from the "
"the Chrome Web Store to fetch their store metadata. The metadata "
"includes information such as an extension's current publish status "
"which is used to enforce the ExtensionUnpublishedAvailability "
"policy to disable the extension. "
trigger:
"Periodic fetch of metadata information once every 24 hours. A fetch "
"is also triggered at Chrome or profile startup and when the "
"ExtensionUnpublishedAvailability policy setting changes."
user_data {
type: PROFILE_DATA
}
data:
"Ids of the currently installed extensions that update from the "
"Chrome Web Store."
destination: GOOGLE_OWNED_SERVICE
last_reviewed: "2023-04-06"
internal {
contacts {
email: "anunoy@chromium.org"
}
}
}
policy {
cookies_allowed: NO
setting:
"This feature cannot be disabled in settings. It will only be "
"triggered if the user has installed extensions from the store."
policy_exception_justification: "Not implemented."
})");
// CWS Info pref keys.
constexpr char kCWSInfo[] = "cws-info";
constexpr char kIsPresent[] = "is-present";
constexpr char kIsLive[] = "is-live";
constexpr char kLastUpdateTimeMillis[] = "last-updated-time-millis";
constexpr char kViolationType[] = "violation-type";
constexpr char kUnpublishedLongAgo[] = "unpublished-long-ago";
constexpr char kNoPrivacyPractice[] = "no-privacy-practice";
constexpr const char* kLabels[] = {kUnpublishedLongAgo, kNoPrivacyPractice};
// Proto conversion helpers.
// Helpers to convert extension id <-> name field in protos.
// name format: items/{itemId}/storeMetadata
std::string GetIdFromName(const std::string& name) {
std::string id;
base::StringTokenizer t(name, "/");
if (t.GetNext() && t.GetNext()) {
id = t.token();
}
return id;
}
std::string GetNameFromId(const std::string& id) {
return "items/" + id + "/storeMetadata";
}
// Histogram helpers.
void RecordFetchSuccess(bool success) {
base::UmaHistogramBoolean("Extensions.CWSInfoService.FetchSuccess", success);
}
void RecordMetadataChanged(bool changed) {
base::UmaHistogramBoolean("Extensions.CWSInfoService.MetadataChanged",
changed);
}
void RecordNumRequestsInFetch(int num_requests) {
base::UmaHistogramCounts100("Extensions.CWSInfoService.NumRequestsInFetch",
num_requests);
}
void RecordNetworkHistograms(const network::SimpleURLLoader* url_loader) {
int net_error = url_loader->NetError();
int response_code = 0;
if (url_loader->ResponseInfo() && url_loader->ResponseInfo()->headers) {
response_code = url_loader->ResponseInfo()->headers->response_code();
}
base::UmaHistogramSparse(
"Extensions.CWSInfoService.NetworkResponseCodeOrError",
net_error == net::OK || net_error == net::ERR_HTTP_RESPONSE_CODE_FAILURE
? response_code
: net_error);
if (net_error == net::OK && response_code == net::HTTP_OK) {
base::UmaHistogramExactLinear(
"Extensions.CWSInfoService.NetworkRetriesTillSuccess",
url_loader->GetNumRetries(), kMaxRetriesPerRequest + 1);
} else {
DVLOG(1) << "Request net error:" << net_error
<< ", response code:" << response_code;
}
}
} // namespace
namespace extensions {
// Allow periodic retrieval of extensions metadata from the Chrome Web Store
// (CWS). This is effectively a kill-switch for the feature.
BASE_FEATURE(kCWSInfoService,
"CWSInfoService",
base::FEATURE_ENABLED_BY_DEFAULT);
// Increase the frequency of periodic retrieval of extensions metadata from
// CWS. This feature is used only for testing purposes.
BASE_FEATURE(kCWSInfoFastCheck,
"CWSInfoFastCheck",
base::FEATURE_DISABLED_BY_DEFAULT);
namespace {
base::Value::Dict GetDictFromStoreMetadataProto(const StoreMetadata* metadata) {
base::Value::Dict dict;
if (!metadata) {
dict.Set(kIsPresent, false);
} else {
dict.Set(kIsPresent, true);
dict.Set(kIsLive, metadata->is_live());
dict.Set(kLastUpdateTimeMillis,
base::NumberToString(metadata->last_update_time_millis()));
dict.Set(kViolationType,
static_cast<int>(CWSInfoService::GetViolationTypeFromString(
metadata->violation_type())));
const auto& proto_labels = metadata->labels();
for (const auto* label : kLabels) {
dict.Set(label, base::Contains(proto_labels, label));
}
}
return dict;
}
// Saves CWS info if it is different from that currently saved in extension
// prefs.
bool SaveInfoIfChanged(ExtensionPrefs* extension_prefs,
const std::string& id,
const StoreMetadata* new_info) {
bool saved = false;
const base::Value::Dict* saved_dict =
extension_prefs->ReadPrefAsDict(id, kCWSInfo);
base::Value::Dict new_dict = GetDictFromStoreMetadataProto(new_info);
if (!saved_dict || *saved_dict != new_dict) {
// The metadata is new or is different from that saved in extension prefs.
saved = true;
extension_prefs->SetDictionaryPref(
id, {kCWSInfo, kDictionary, PrefScope::kExtensionSpecific},
std::move(new_dict));
}
return saved;
}
int GetNextFetchInterval() {
// jitter fetch interval by +/- 25%
double jitter_factor = base::RandDouble() * 0.5 + 0.75;
return base::FeatureList::IsEnabled(kCWSInfoFastCheck)
? kFastFetchIntervalSeconds
: kFetchIntervalSeconds * jitter_factor;
}
} // namespace
// Stores context information about a CWS info fetch operation.
struct CWSInfoService::FetchContext {
struct Request {
ExtensionIdSet ids;
BatchGetStoreMetadatasRequest proto;
};
base::queue<Request> requests;
// Indicates if the metadata retrieved is different from that currently saved.
bool metadata_changed = false;
};
// static
CWSInfoService* CWSInfoService::Get(Profile* profile) {
return CWSInfoServiceFactory::GetInstance()->GetForProfile(profile);
}
CWSInfoService::CWSInfoService(Profile* profile)
: profile_(profile),
pref_service_(profile->GetPrefs()),
extension_prefs_(ExtensionPrefs::Get(profile)),
extension_registry_(ExtensionRegistry::Get(profile)),
url_loader_factory_(profile->GetURLLoaderFactory()),
max_ids_per_request_(kMaxExtensionIdsPerRequest),
current_fetch_interval_secs_(GetNextFetchInterval()) {
// Vary the startup check out between 30s to 10min, unless FastCheck
// option is enabled.
startup_delay_secs_ = base::FeatureList::IsEnabled(kCWSInfoFastCheck)
? kFastStartupCheckDelaySeconds
: base::RandInt(/*min=*/30, /*max=*/600);
ScheduleCheck(startup_delay_secs_);
}
CWSInfoService::CWSInfoService() = default;
CWSInfoService::~CWSInfoService() = default;
void CWSInfoService::Shutdown() {
info_check_timer_.Stop();
}
absl::optional<bool> CWSInfoService::IsLiveInCWS(
const Extension& extension) const {
const base::Value::Dict* cws_info_dict =
extension_prefs_->ReadPrefAsDict(extension.id(), kCWSInfo);
if (cws_info_dict == nullptr) {
return absl::nullopt;
}
if (!cws_info_dict->FindBool(kIsPresent).value_or(false)) {
return false;
}
return cws_info_dict->FindBool(kIsLive).value_or(false);
}
absl::optional<CWSInfoService::CWSInfo> CWSInfoService::GetCWSInfo(
const Extension& extension) const {
const base::Value::Dict* cws_info_dict =
extension_prefs_->ReadPrefAsDict(extension.id(), kCWSInfo);
if (cws_info_dict == nullptr) {
return absl::nullopt;
}
CWSInfo info;
info.is_present = cws_info_dict->FindBool(kIsPresent).value_or(false);
if (info.is_present) {
info.is_live = cws_info_dict->FindBool(kIsLive).value_or(false);
const std::string* last_update_time_millis_str =
cws_info_dict->FindString(kLastUpdateTimeMillis);
int64_t last_update_time_millis = 0;
if (last_update_time_millis_str &&
base::StringToInt64(*last_update_time_millis_str,
&last_update_time_millis)) {
info.last_update_time = base::Time::FromJavaTime(last_update_time_millis);
}
info.violation_type = static_cast<CWSViolationType>(
cws_info_dict->FindInt(kViolationType).value_or(0));
info.unpublished_long_ago =
cws_info_dict->FindBool(kUnpublishedLongAgo).value_or(false);
info.no_privacy_practice =
cws_info_dict->FindBool(kNoPrivacyPractice).value_or(false);
}
return absl::make_optional<CWSInfo>(info);
}
void CWSInfoService::CheckAndMaybeFetchInfo() {
CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
// If a fetch is already in progress, don't do anything.
if (active_fetch_) {
return;
}
if (CanFetchInfo()) {
base::TimeDelta elapsed_time =
base::Time::Now() - pref_service_->GetTime(prefs::kCWSInfoTimestamp);
// Enough time has elapsed since the last fetch.
bool data_refresh_needed =
elapsed_time >= base::Seconds(current_fetch_interval_secs_);
bool new_info_requested = false;
std::unique_ptr<FetchContext> fetch_context =
CreateRequests(new_info_requested);
if ((data_refresh_needed || new_info_requested) && fetch_context) {
// Stop the check timer in case it is running. This can happen if we got
// here because of an out-of-cycle fetch.
info_check_timer_.Stop();
// Save the fetch context and send the (first) request.
active_fetch_ = std::move(fetch_context);
RecordNumRequestsInFetch(active_fetch_->requests.size());
current_fetch_interval_secs_ = GetNextFetchInterval();
SendRequest();
return;
}
}
// No info request necessary at this time. Schedule the next check.
int check_interval_seconds = base::FeatureList::IsEnabled(kCWSInfoFastCheck)
? kFastCheckIntervalSeconds
: kCheckIntervalSeconds;
ScheduleCheck(check_interval_seconds);
}
bool CWSInfoService::CanFetchInfo() const {
// TODO(anunoy): These two checks are needed to support the enterprise policy
// and safety check extensions module respectively. Once safety check is
// launched, we can remove this method completely.
return pref_service_->GetInteger(
pref_names::kExtensionUnpublishedAvailability) == 1 ||
base::FeatureList::IsEnabled(features::kSafetyCheckExtensions);
}
void CWSInfoService::ScheduleCheck(int seconds) {
info_check_timer_.Start(FROM_HERE, base::Seconds(seconds), this,
&CWSInfoService::CheckAndMaybeFetchInfo);
}
std::unique_ptr<CWSInfoService::FetchContext> CWSInfoService::CreateRequests(
bool& new_info_requested) {
new_info_requested = false;
auto* extension_mgmt =
extensions::ExtensionManagementFactory::GetForBrowserContext(profile_);
if (!extension_mgmt) {
return nullptr;
}
extensions::ExtensionSet installed_extensions =
extension_registry_->GenerateInstalledExtensionsSet();
if (installed_extensions.empty()) {
return nullptr;
}
auto fetch_context = std::make_unique<FetchContext>();
FetchContext::Request* request = nullptr;
int num_ids_added_in_request = 0;
for (const auto& extension : installed_extensions) {
if (extension_mgmt->UpdatesFromWebstore(*extension) == false) {
continue;
}
if (extension_prefs_->ReadPrefAsDict(extension->id(), kCWSInfo) ==
nullptr) {
// This extension does not already have CWS info saved. Flag this as a new
// info request.
new_info_requested = true;
}
if (num_ids_added_in_request == 0) {
// Create a new request context.
fetch_context->requests.emplace();
request = &fetch_context->requests.back();
request->proto.set_parent("items/-");
}
request->proto.add_names(GetNameFromId(extension->id()));
request->ids.emplace(extension->id());
num_ids_added_in_request++;
if (num_ids_added_in_request == max_ids_per_request_) {
// Max ids reached for the request context. Reset the count to create
// a new context for the remaining ids.
num_ids_added_in_request = 0;
}
}
if (fetch_context->requests.empty()) {
// No extensions require a CWS info fetch.
return nullptr;
}
// Return the fetch context - contains information about number of requests to
// send and which ids are included in each request.
return fetch_context;
}
void CWSInfoService::SendRequest() {
auto resource_request = std::make_unique<network::ResourceRequest>();
resource_request->url = GURL(kRequestUrl);
// A POST request is sent with an override to GET due to server requirements.
resource_request->method = "POST";
resource_request->load_flags = net::LOAD_DISABLE_CACHE;
resource_request->headers.SetHeader("X-HTTP-Method-Override", "GET");
resource_request->headers.SetHeader("X-Goog-Api-Key",
google_apis::GetAPIKey());
resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
kTrafficAnnotation);
url_loader_->SetRetryOptions(kMaxRetriesPerRequest,
network::SimpleURLLoader::RETRY_ON_5XX);
std::string request_str =
active_fetch_->requests.front().proto.SerializeAsString();
url_loader_->AttachStringForUpload(request_str, "application/x-protobuf");
url_loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie(
url_loader_factory_.get(),
base::BindOnce(&CWSInfoService::OnResponseReceived,
weak_factory_.GetWeakPtr()));
info_requests_++;
}
void CWSInfoService::OnResponseReceived(std::unique_ptr<std::string> response) {
CHECK(url_loader_);
RecordNetworkHistograms(url_loader_.get());
bool error = false;
if (response) {
BatchGetStoreMetadatasResponse response_proto;
if (response_proto.ParseFromString(*response) == true) {
info_responses_++;
if (MaybeSaveResponseToPrefs(response_proto)) {
info_changes_++;
active_fetch_->metadata_changed = true;
}
} else {
DVLOG(1) << "Failed to parse response: " << *response;
info_errors_++;
error = true;
}
} else {
info_errors_++;
error = true;
}
if (!error) {
// Info response received without any errors. Remove the request object
// from the request queue.
active_fetch_->requests.pop();
if (!active_fetch_->requests.empty()) {
// Request info for the next batch of extension ids.
SendRequest();
return;
}
// All requests completed. Store "freshness" timestamp in global extension
// prefs.
pref_service_->SetTime(prefs::kCWSInfoTimestamp, base::Time::Now());
RecordMetadataChanged(active_fetch_->metadata_changed);
if (active_fetch_->metadata_changed) {
// Notify observers if the metadata changed.
for (auto& observer : observers_) {
observer.OnCWSInfoChanged();
}
}
}
// All requests completed successfully OR a request failed. In either case,
// schedule the next check.
RecordFetchSuccess(!error);
active_fetch_.reset();
int check_interval_seconds = base::FeatureList::IsEnabled(kCWSInfoFastCheck)
? kFastCheckIntervalSeconds
: kCheckIntervalSeconds;
ScheduleCheck(check_interval_seconds);
}
bool CWSInfoService::MaybeSaveResponseToPrefs(
const BatchGetStoreMetadatasResponse& response_proto) {
bool store_metadata_changed = false;
for (const auto& metadata : response_proto.store_metadatas()) {
std::string id = GetIdFromName(metadata.name());
active_fetch_->requests.front().ids.erase(id);
if (extension_prefs_->HasPrefForExtension(id) == false) {
continue;
}
if (SaveInfoIfChanged(extension_prefs_, id, &metadata)) {
store_metadata_changed = true;
}
}
// Process any resquested ids missing from the response. These ids represent
// extensions that are no longer available from the store.
for (const auto& id : active_fetch_->requests.front().ids) {
if (extension_prefs_->HasPrefForExtension(id) == false) {
continue;
}
if (SaveInfoIfChanged(extension_prefs_, id, nullptr)) {
store_metadata_changed = true;
}
}
return store_metadata_changed;
}
void CWSInfoService::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void CWSInfoService::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
static_assert(static_cast<int>(
extensions::CWSInfoService::CWSViolationType::kUnknown) == 4,
"GetViolationTypeFromString needs to be updated to match "
"CWSInfoService::CWSViolationType");
// static:
CWSInfoService::CWSViolationType CWSInfoService::GetViolationTypeFromString(
const std::string& violation_type_str) {
static constexpr auto violation_type_str_map =
base::MakeFixedFlatMap<base::StringPiece,
CWSInfoService::CWSViolationType>(
{{"none", CWSInfoService::CWSViolationType::kNone},
{"malware", CWSInfoService::CWSViolationType::kMalware},
{"policy-violation", CWSInfoService::CWSViolationType::kPolicy},
{"minor-policy-violation",
CWSInfoService::CWSViolationType::kMinorPolicy}});
const auto* it = violation_type_str_map.find(violation_type_str);
return it != violation_type_str_map.end() ? it->second
: CWSViolationType::kUnknown;
}
void CWSInfoService::SetMaxExtensionIdsPerRequestForTesting(int max) {
max_ids_per_request_ = max;
}
std::string CWSInfoService::GetRequestURLForTesting() const {
return kRequestUrl;
}
int CWSInfoService::GetFetchIntervalForTesting() const {
return current_fetch_interval_secs_;
}
int CWSInfoService::GetStartupDelayForTesting() const {
return startup_delay_secs_;
}
int CWSInfoService::GetCheckIntervalForTesting() const {
return kCheckIntervalSeconds;
}
base::Time CWSInfoService::GetCWSInfoTimestampForTesting() const {
return pref_service_->GetTime(prefs::kCWSInfoTimestamp);
}
} // namespace extensions