blob: bc5a32137ee8c79a5fe7f67a14e8fc00ddcc54a5 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/browsing_data/clear_site_data_handler.h"
#include <optional>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "content/browser/buckets/bucket_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition_config.h"
#include "content/public/browser/web_contents.h"
#include "net/base/load_flags.h"
#include "net/url_request/clear_site_data.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/features_generated.h"
namespace content {
namespace {
// Pretty-printed log output.
const char kConsoleMessageTemplate[] = "Clear-Site-Data header on '%s': %s";
const char kConsoleMessageCleared[] = "Cleared data types: %s.";
const char kConsoleMessageDatatypeSeparator[] = ", ";
enum LoggableEventMask {
CLEAR_SITE_DATA_NO_RECOGNIZABLE_TYPES = 0,
CLEAR_SITE_DATA_COOKIES = 1 << 0,
CLEAR_SITE_DATA_STORAGE = 1 << 1,
CLEAR_SITE_DATA_CACHE = 1 << 2,
CLEAR_SITE_DATA_BUCKETS = 1 << 3,
CLEAR_SITE_DATA_CLIENT_HINTS = 1 << 4,
CLEAR_SITE_DATA_PREFETCH_CACHE = 1 << 5,
CLEAR_SITE_DATA_PRERENDER_CACHE = 1 << 6,
CLEAR_SITE_DATA_MAX_VALUE = 1 << 7,
};
void LogEvent(int event) {
UMA_HISTOGRAM_ENUMERATION("Storage.ClearSiteDataHeader.Parameters", event,
static_cast<int>(CLEAR_SITE_DATA_MAX_VALUE));
}
// Represents the parameters as a single number to be recorded in a histogram.
int ParametersMask(const ClearSiteDataTypeSet clear_site_data_types,
bool has_buckets) {
int mask = CLEAR_SITE_DATA_NO_RECOGNIZABLE_TYPES;
if (clear_site_data_types.Has(ClearSiteDataType::kCookies)) {
mask = mask | CLEAR_SITE_DATA_COOKIES;
}
if (clear_site_data_types.Has(ClearSiteDataType::kStorage)) {
mask = mask | CLEAR_SITE_DATA_STORAGE;
}
if (clear_site_data_types.Has(ClearSiteDataType::kCache)) {
mask = mask | CLEAR_SITE_DATA_CACHE;
}
if (has_buckets) {
mask = mask | CLEAR_SITE_DATA_BUCKETS;
}
if (clear_site_data_types.Has(ClearSiteDataType::kClientHints)) {
mask = mask | CLEAR_SITE_DATA_CLIENT_HINTS;
}
if (clear_site_data_types.Has(ClearSiteDataType::kPrefetchCache)) {
mask = mask | CLEAR_SITE_DATA_PREFETCH_CACHE;
}
if (clear_site_data_types.Has(ClearSiteDataType::kPrerenderCache)) {
mask = mask | CLEAR_SITE_DATA_PRERENDER_CACHE;
}
return mask;
}
// Outputs a single |formatted_message| on the UI thread.
void OutputFormattedMessage(WebContents* web_contents,
blink::mojom::ConsoleMessageLevel level,
const std::string& formatted_text) {
if (web_contents)
web_contents->GetPrimaryMainFrame()->AddMessageToConsole(level,
formatted_text);
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// ConsoleMessagesDelegate
ClearSiteDataHandler::ConsoleMessagesDelegate::ConsoleMessagesDelegate()
: output_formatted_message_function_(
base::BindRepeating(&OutputFormattedMessage)) {}
ClearSiteDataHandler::ConsoleMessagesDelegate::~ConsoleMessagesDelegate() {}
void ClearSiteDataHandler::ConsoleMessagesDelegate::AddMessage(
const GURL& url,
const std::string& text,
blink::mojom::ConsoleMessageLevel level) {
messages_.push_back({url, text, level});
}
void ClearSiteDataHandler::ConsoleMessagesDelegate::OutputMessages(
base::WeakPtr<WebContents> web_contents) {
if (messages_.empty())
return;
for (const auto& message : messages_) {
// Prefix each message with |kConsoleMessageTemplate|.
output_formatted_message_function_.Run(
web_contents.get(), message.level,
base::StringPrintf(kConsoleMessageTemplate, message.url.spec().c_str(),
message.text.c_str()));
}
messages_.clear();
}
void ClearSiteDataHandler::ConsoleMessagesDelegate::
SetOutputFormattedMessageFunctionForTesting(
const OutputFormattedMessageFunction& function) {
output_formatted_message_function_ = function;
}
////////////////////////////////////////////////////////////////////////////////
// ClearSiteDataHandler
// static
void ClearSiteDataHandler::HandleHeader(
base::WeakPtr<BrowserContext> browser_context,
base::WeakPtr<WebContents> web_contents,
const StoragePartitionConfig& storage_partition_config,
const GURL& url,
const std::string& header_value,
int load_flags,
const std::optional<net::CookiePartitionKey> cookie_partition_key,
const std::optional<blink::StorageKey> storage_key,
bool partitioned_state_allowed_only,
base::OnceClosure callback) {
ClearSiteDataHandler handler(
browser_context, web_contents, storage_partition_config, url,
header_value, load_flags, cookie_partition_key, storage_key,
partitioned_state_allowed_only, std::move(callback),
std::make_unique<ConsoleMessagesDelegate>());
handler.HandleHeaderAndOutputConsoleMessages();
}
// static
bool ClearSiteDataHandler::ParseHeaderForTesting(
const std::string& header,
ClearSiteDataTypeSet* clear_site_data_types,
std::set<std::string>* storage_buckets_to_remove,
ConsoleMessagesDelegate* delegate,
const GURL& current_url) {
return ClearSiteDataHandler::ParseHeader(header, clear_site_data_types,
storage_buckets_to_remove, delegate,
current_url);
}
ClearSiteDataHandler::ClearSiteDataHandler(
base::WeakPtr<BrowserContext> browser_context,
base::WeakPtr<WebContents> web_contents,
const StoragePartitionConfig& storage_partition_config,
const GURL& url,
const std::string& header_value,
int load_flags,
const std::optional<net::CookiePartitionKey> cookie_partition_key,
const std::optional<blink::StorageKey> storage_key,
bool partitioned_state_allowed_only,
base::OnceClosure callback,
std::unique_ptr<ConsoleMessagesDelegate> delegate)
: browser_context_(browser_context),
web_contents_(web_contents),
storage_partition_config_(storage_partition_config),
url_(url),
header_value_(header_value),
load_flags_(load_flags),
cookie_partition_key_(cookie_partition_key),
storage_key_(storage_key),
partitioned_state_allowed_only_(partitioned_state_allowed_only),
callback_(std::move(callback)),
delegate_(std::move(delegate)) {
DCHECK(delegate_);
}
ClearSiteDataHandler::~ClearSiteDataHandler() = default;
bool ClearSiteDataHandler::HandleHeaderAndOutputConsoleMessages() {
bool deferred = Run();
// If the redirect is deferred, wait until it is resumed.
// TODO(crbug.com/41409604): Delay output until next frame for navigations.
if (!deferred) {
OutputConsoleMessages();
RunCallbackNotDeferred();
}
return deferred;
}
bool ClearSiteDataHandler::Run() {
// Only accept the header on secure non-unique origins.
if (!network::IsUrlPotentiallyTrustworthy(url_)) {
delegate_->AddMessage(url_, "Not supported for insecure origins.",
blink::mojom::ConsoleMessageLevel::kError);
return false;
}
url::Origin origin = url::Origin::Create(url_);
if (origin.opaque()) {
delegate_->AddMessage(url_, "Not supported for unique origins.",
blink::mojom::ConsoleMessageLevel::kError);
return false;
}
// The LOAD_DO_NOT_SAVE_COOKIES flag prohibits the request from doing any
// modification to cookies. Clear-Site-Data applies this restriction to other
// data types as well.
// TODO(msramek): Consider showing a blocked icon via
// PageSpecificContentSettings and reporting the action in the "Blocked"
// section of the cookies dialog in OIB.
if (load_flags_ & net::LOAD_DO_NOT_SAVE_COOKIES) {
delegate_->AddMessage(
url_,
"The request's credentials mode prohibits modifying cookies "
"and other local data.",
blink::mojom::ConsoleMessageLevel::kError);
return false;
}
ClearSiteDataTypeSet clear_site_data_types;
std::set<std::string> storage_buckets_to_remove;
if (!ClearSiteDataHandler::ParseHeader(header_value_, &clear_site_data_types,
&storage_buckets_to_remove,
delegate_.get(), url_)) {
return false;
}
ExecuteClearingTask(
origin, clear_site_data_types, storage_buckets_to_remove,
base::BindOnce(&ClearSiteDataHandler::TaskFinished,
base::TimeTicks::Now(), std::move(delegate_),
web_contents_, std::move(callback_)));
return true;
}
// static
bool ClearSiteDataHandler::ParseHeader(
const std::string& header,
ClearSiteDataTypeSet* clear_site_data_types,
std::set<std::string>* storage_buckets_to_remove,
ConsoleMessagesDelegate* delegate,
const GURL& current_url) {
DCHECK(clear_site_data_types);
DCHECK(storage_buckets_to_remove);
DCHECK(delegate);
if (!base::IsStringASCII(header)) {
delegate->AddMessage(current_url, "Must only contain ASCII characters.",
blink::mojom::ConsoleMessageLevel::kWarning);
LogEvent(CLEAR_SITE_DATA_NO_RECOGNIZABLE_TYPES);
return false;
}
clear_site_data_types->Clear();
std::vector<std::string> input_types =
net::ClearSiteDataHeaderContents(header);
std::string output_types;
if (base::Contains(input_types, net::kDatatypeWildcard)) {
input_types.push_back(net::kDatatypeCookies);
input_types.push_back(net::kDatatypeStorage);
input_types.push_back(net::kDatatypeCache);
input_types.push_back(net::kDatatypeClientHints);
}
for (auto& input_type : input_types) {
// Match here if the beginning is '"storage:' and ends with '"'.
if (base::FeatureList::IsEnabled(blink::features::kStorageBuckets) &&
base::StartsWith(input_type, net::kDatatypeStorageBucketPrefix) &&
base::EndsWith(input_type, net::kDatatypeStorageBucketSuffix)) {
const int prefix_len = strlen(net::kDatatypeStorageBucketPrefix);
const std::string bucket_name = input_type.substr(
prefix_len,
input_type.length() -
(prefix_len + strlen(net::kDatatypeStorageBucketSuffix)));
if (IsValidBucketName(bucket_name))
storage_buckets_to_remove->insert(bucket_name);
// Exit the loop and continue since for buckets, there are no booleans
// and the logic later would cause a crash.
continue;
}
ClearSiteDataType data_type = ClearSiteDataType::kUndefined;
if (input_type == net::kDatatypeCookies) {
data_type = ClearSiteDataType::kCookies;
} else if (input_type == net::kDatatypeStorage) {
data_type = ClearSiteDataType::kStorage;
} else if (input_type == net::kDatatypeCache) {
data_type = ClearSiteDataType::kCache;
} else if (input_type == net::kDatatypeClientHints) {
data_type = ClearSiteDataType::kClientHints;
} else if (input_type == net::kDatatypePrefetchCache) {
data_type = ClearSiteDataType::kPrefetchCache;
} else if (input_type == net::kDatatypePrerenderCache) {
data_type = ClearSiteDataType::kPrerenderCache;
} else if (input_type == net::kDatatypeWildcard) {
continue;
} else {
delegate->AddMessage(
current_url,
base::StringPrintf("Unrecognized type: %s.", input_type.c_str()),
blink::mojom::ConsoleMessageLevel::kWarning);
continue;
}
if (!base::FeatureList::IsEnabled(
blink::features::kClearSiteDataPrefetchPrerenderCache)) {
if (data_type == ClearSiteDataType::kPrefetchCache ||
data_type == ClearSiteDataType::kPrerenderCache) {
delegate->AddMessage(
current_url,
base::StringPrintf(
"prefetchCache and prerenderCache not enabled: %s.",
input_type.c_str()),
blink::mojom::ConsoleMessageLevel::kWarning);
continue;
}
}
DCHECK_NE(data_type, ClearSiteDataType::kUndefined);
if (clear_site_data_types->Has(data_type)) {
continue;
}
clear_site_data_types->Put(data_type);
if (!output_types.empty())
output_types += kConsoleMessageDatatypeSeparator;
output_types += input_type;
}
if (clear_site_data_types->empty() && storage_buckets_to_remove->empty()) {
delegate->AddMessage(current_url, "No recognized types specified.",
blink::mojom::ConsoleMessageLevel::kWarning);
LogEvent(CLEAR_SITE_DATA_NO_RECOGNIZABLE_TYPES);
return false;
}
if (clear_site_data_types->Has(ClearSiteDataType::kStorage) &&
!storage_buckets_to_remove->empty()) {
// `clear_storage` and `clear_storage_buckets` cannot both be true. When
// that happens, `clear_storage` stays true and we empty `storage_buckets
// _to_remove`
delegate->AddMessage(current_url,
"All the buckets related to this url will be "
"cleared. When passing type 'storage', any "
"additional buckets specifiers are ignored.",
blink::mojom::ConsoleMessageLevel::kWarning);
storage_buckets_to_remove->clear();
}
// Pretty-print which types are to be cleared.
// TODO(crbug.com/41363015): Remove the disclaimer about cookies.
std::string console_output =
base::StringPrintf(kConsoleMessageCleared, output_types.c_str());
if (clear_site_data_types->Has(ClearSiteDataType::kCookies)) {
console_output +=
" Clearing channel IDs and HTTP authentication cache is currently not"
" supported, as it breaks active network connections.";
}
delegate->AddMessage(current_url, console_output,
blink::mojom::ConsoleMessageLevel::kInfo);
// Note that presence of headers is also logged in WebRequest.ResponseHeader
LogEvent(ParametersMask(*clear_site_data_types,
!storage_buckets_to_remove->empty()));
return true;
}
void ClearSiteDataHandler::ExecuteClearingTask(
const url::Origin& origin,
const ClearSiteDataTypeSet clear_site_data_types,
const std::set<std::string>& storage_buckets_to_remove,
base::OnceClosure callback) {
ClearSiteData(browser_context_, storage_partition_config_, origin,
clear_site_data_types, storage_buckets_to_remove,
/*avoid_closing_connections=*/true, cookie_partition_key_,
storage_key_, partitioned_state_allowed_only_,
std::move(callback));
}
// static
void ClearSiteDataHandler::TaskFinished(
base::TimeTicks clearing_started,
std::unique_ptr<ConsoleMessagesDelegate> delegate,
base::WeakPtr<WebContents> web_contents,
base::OnceClosure callback) {
DCHECK(!clearing_started.is_null());
// TODO(crbug.com/41409604): Delay output until next frame for navigations.
delegate->OutputMessages(web_contents);
std::move(callback).Run();
}
void ClearSiteDataHandler::OutputConsoleMessages() {
delegate_->OutputMessages(web_contents_);
}
void ClearSiteDataHandler::RunCallbackNotDeferred() {
std::move(callback_).Run();
}
} // namespace content