blob: c254f02195951b18a26b3bfc43dfd2b4e62cb037 [file] [log] [blame]
/*
Copyright (C) 1998 Lars Knoll (knoll@mpi-hd.mpg.de)
Copyright (C) 2001 Dirk Mueller (mueller@kde.org)
Copyright (C) 2002 Waldo Bastian (bastian@kde.org)
Copyright (C) 2006 Samuel Weinig (sam.weinig@gmail.com)
Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc. All
rights reserved.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public License
along with this library; see the file COPYING.LIB. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "third_party/blink/renderer/platform/loader/fetch/resource.h"
#include <stdint.h>
#include <algorithm>
#include <cassert>
#include <memory>
#include <utility>
#include <variant>
#include "base/feature_list.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/default_clock.h"
#include "build/build_config.h"
#include "services/network/public/mojom/fetch_api.mojom-blink.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-shared.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/web_security_origin.h"
#include "third_party/blink/renderer/platform/heap/collection_support/heap_vector.h"
#include "third_party/blink/renderer/platform/instrumentation/histogram.h"
#include "third_party/blink/renderer/platform/instrumentation/instance_counters.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/loader/cors/cors.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_context.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_initiator_type_names.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_parameters.h"
#include "third_party/blink/renderer/platform/loader/fetch/integrity_metadata.h"
#include "third_party/blink/renderer/platform/loader/fetch/memory_cache.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_client.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_client_walker.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_finish_observer.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_load_timing.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_loader.h"
#include "third_party/blink/renderer/platform/loader/fetch/url_loader/background_response_processor.h"
#include "third_party/blink/renderer/platform/network/http_parsers.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/math_extras.h"
#include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
#include "third_party/blink/renderer/platform/wtf/text/strcat.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink {
namespace {
void NotifyFinishObservers(
GCedHeapHashSet<WeakMember<ResourceFinishObserver>>* observers) {
for (const auto& observer : *observers)
observer->NotifyFinished();
}
void GetSharedBufferMemoryDump(SharedBuffer* buffer,
const String& dump_prefix,
WebProcessMemoryDump* memory_dump) {
size_t dump_size;
String dump_name;
buffer->GetMemoryDumpNameAndSize(dump_name, dump_size);
WebMemoryAllocatorDump* dump =
memory_dump->CreateMemoryAllocatorDump(StrCat({dump_prefix, dump_name}));
dump->AddScalar("size", "bytes", dump_size);
memory_dump->AddSuballocation(dump->Guid(),
String(Partitions::kAllocatedObjectPoolName));
}
// These response headers are not copied from a revalidated response to the
// cached response headers. For compatibility, this list is based on Chromium's
// net/http/http_response_headers.cc.
constexpr auto kHeadersToIgnoreAfterRevalidation = std::to_array<const char*>({
"allow",
"connection",
"etag",
"expires",
"keep-alive",
"last-modified",
"proxy-authenticate",
"proxy-connection",
"trailer",
"transfer-encoding",
"upgrade",
"www-authenticate",
"x-frame-options",
"x-xss-protection",
});
// Some header prefixes mean "Don't copy this header from a 304 response.".
// Rather than listing all the relevant headers, we can consolidate them into
// this list, also grabbed from Chromium's net/http/http_response_headers.cc.
const auto kHeaderPrefixesToIgnoreAfterRevalidation =
std::to_array<const char*>({"content-", "x-content-", "x-webkit-"});
inline bool ShouldUpdateHeaderAfterRevalidation(const AtomicString& header) {
for (const auto* header_to_ignore : kHeadersToIgnoreAfterRevalidation) {
if (EqualIgnoringASCIICase(header, header_to_ignore)) {
return false;
}
}
for (const auto* header_prefix_to_ignore :
kHeaderPrefixesToIgnoreAfterRevalidation) {
if (header.StartsWithIgnoringASCIICase(header_prefix_to_ignore)) {
return false;
}
}
return true;
}
const base::Clock* g_clock_for_testing = nullptr;
} // namespace
static inline base::Time Now() {
const base::Clock* clock = g_clock_for_testing
? g_clock_for_testing
: base::DefaultClock::GetInstance();
return clock->Now();
}
Resource::Resource(const ResourceRequestHead& request,
ResourceType type,
const ResourceLoaderOptions& options)
: type_(type),
cache_identifier_(MemoryCache::DefaultCacheIdentifier()),
options_(options),
response_timestamp_(Now()),
resource_request_(request),
overhead_size_(CalculateOverheadSize()) {
InstanceCounters::IncrementCounter(InstanceCounters::kResourceCounter);
if (IsMainThread())
MemoryPressureListenerRegistry::Instance().RegisterClient(this);
}
Resource::~Resource() {
InstanceCounters::DecrementCounter(InstanceCounters::kResourceCounter);
}
void Resource::Trace(Visitor* visitor) const {
visitor->Trace(loader_);
visitor->Trace(clients_);
visitor->Trace(clients_awaiting_callback_);
visitor->Trace(finished_clients_);
visitor->Trace(finish_observers_);
visitor->Trace(options_);
MemoryPressureListener::Trace(visitor);
}
void Resource::SetLoader(ResourceLoader* loader) {
CHECK(!loader_);
DCHECK(StillNeedsLoad());
loader_ = loader;
}
void Resource::CheckResourceIntegrity() {
// Skip the check and reuse the previous check result, especially on
// successful revalidation.
if (integrity_disposition_ != ResourceIntegrityDisposition::kNotChecked) {
return;
}
// Loading error occurred? Then result is uncheckable.
integrity_report_.Clear();
if (ErrorOccurred()) {
CHECK(!Data());
integrity_disposition_ = ResourceIntegrityDisposition::kNetworkError;
return;
}
// Check `Unencoded-Digest` headers. If the digest doesn't match, fail.
// Otherwise, fall through to validating SRI.
const FeatureContext* feature_context =
loader_ ? loader_->GetFeatureContext() : nullptr;
if (RuntimeEnabledFeatures::UnencodedDigestEnabled(feature_context) &&
!SubresourceIntegrity::CheckUnencodedDigests(
GetResponse().GetUnencodedDigests(), Data())) {
integrity_disposition_ =
ResourceIntegrityDisposition::kFailedUnencodedDigest;
integrity_report_.AddConsoleErrorMessage(StrCat(
{"The resource '", Url().ElidedString(),
"' has an `unencoded-digest` header which asserts a digest which does "
"not match the resource's body."}));
return;
}
HashMap<HashAlgorithm, String> integrity_hashes;
bool is_cors_same_origin = response_.IsCorsSameOrigin();
HashSet<HashAlgorithm> csp_hash_reports_needed;
if ((type_ == ResourceType::kScript) && loader_) {
csp_hash_reports_needed = loader_->Fetcher()->Context().CSPHashesToReport();
}
if (IntegrityMetadata().empty()) {
// No integrity attributes to check? Then we're passing.
integrity_disposition_ = ResourceIntegrityDisposition::kPassed;
} else {
if (SubresourceIntegrity::CheckSubresourceIntegrity(
IntegrityMetadata(), Data(), Url(), *this, feature_context,
integrity_report_, &integrity_hashes)) {
integrity_disposition_ = ResourceIntegrityDisposition::kPassed;
} else {
integrity_disposition_ =
ResourceIntegrityDisposition::kFailedIntegrityMetadata;
// The resource was blocked so there's nothing to report.
csp_hash_reports_needed = HashSet<HashAlgorithm>();
}
}
if (csp_hash_reports_needed.size()) {
if (is_cors_same_origin) {
for (HashAlgorithm algorithm : csp_hash_reports_needed) {
if (integrity_hashes.Contains(algorithm)) {
continue;
}
if (auto calculated_integrity_hash =
SubresourceIntegrity::GetSubresourceIntegrityHash(Data(),
algorithm)) {
integrity_hashes.insert(algorithm, calculated_integrity_hash);
}
}
}
loader_->Fetcher()->Context().AddCSPHashReport(Url().GetString(),
integrity_hashes);
}
CHECK_NE(integrity_disposition_, ResourceIntegrityDisposition::kNotChecked);
}
void Resource::NotifyFinished() {
CHECK(IsLoaded());
ResourceClientWalker<ResourceClient> w(clients_);
while (ResourceClient* c = w.Next()) {
MarkClientFinished(c);
c->NotifyFinished(this);
}
}
void Resource::MarkClientFinished(ResourceClient* client) {
if (clients_.Contains(client)) {
finished_clients_.insert(client);
clients_.erase(client);
}
}
void Resource::AppendData(
std::variant<SegmentedBuffer, base::span<const char>> data) {
DCHECK(!IsCacheValidator());
DCHECK(!ErrorOccurred());
if (std::holds_alternative<SegmentedBuffer>(data)) {
AppendDataImpl(std::move(std::get<SegmentedBuffer>(data)));
} else {
CHECK(std::holds_alternative<base::span<const char>>(data));
AppendDataImpl(std::get<base::span<const char>>(data));
}
}
void Resource::AppendDataImpl(SegmentedBuffer&& buffer) {
TRACE_EVENT1("blink", "Resource::appendData", "length", buffer.size());
SegmentedBuffer* data_ptr = &buffer;
if (options_.data_buffering_policy == kBufferData) {
CHECK(!data_);
data_ = SharedBuffer::Create(std::move(buffer));
data_ptr = data_.get();
SetEncodedSize(data_->size());
}
for (const auto& span : *data_ptr) {
NotifyDataReceived(span);
}
}
void Resource::AppendDataImpl(base::span<const char> data) {
TRACE_EVENT1("blink", "Resource::appendData", "length", data.size());
if (options_.data_buffering_policy == kBufferData) {
if (!data_) {
data_ = SharedBuffer::Create();
}
data_->Append(data);
SetEncodedSize(data_->size());
}
NotifyDataReceived(data);
}
void Resource::NotifyDataReceived(base::span<const char> data) {
ResourceClientWalker<ResourceClient> w(Clients());
while (ResourceClient* c = w.Next())
c->DataReceived(this, data);
}
void Resource::SetResourceBuffer(scoped_refptr<SharedBuffer> resource_buffer) {
DCHECK(!IsCacheValidator());
DCHECK(!ErrorOccurred());
DCHECK_EQ(options_.data_buffering_policy, kBufferData);
data_ = std::move(resource_buffer);
SetEncodedSize(data_->size());
}
void Resource::ClearData() {
data_ = nullptr;
}
void Resource::TriggerNotificationForFinishObservers(
base::SingleThreadTaskRunner* task_runner) {
if (finish_observers_.empty())
return;
auto* new_collections =
MakeGarbageCollected<GCedHeapHashSet<WeakMember<ResourceFinishObserver>>>(
std::move(finish_observers_));
finish_observers_.clear();
task_runner->PostTask(FROM_HERE, BindOnce(&NotifyFinishObservers,
WrapPersistent(new_collections)));
DidRemoveClientOrObserver();
}
void Resource::SetDataBufferingPolicy(
DataBufferingPolicy data_buffering_policy) {
options_.data_buffering_policy = data_buffering_policy;
ClearData();
SetEncodedSize(0);
}
static bool NeedsSynchronousCacheHit(ResourceType type,
const ResourceLoaderOptions& options) {
// Synchronous requests must always succeed or fail synchronously.
if (options.synchronous_policy == kRequestSynchronously)
return true;
// Some resources types default to return data synchronously. For most of
// these, it's because there are web tests that expect data to return
// synchronously in case of cache hit. In the case of fonts, there was a
// performance regression.
// FIXME: Get to the point where we don't need to special-case sync/async
// behavior for different resource types.
if (type == ResourceType::kCSSStyleSheet)
return true;
if (type == ResourceType::kScript)
return true;
if (type == ResourceType::kFont)
return true;
return false;
}
void Resource::FinishAsError(const ResourceError& error,
base::SingleThreadTaskRunner* task_runner) {
error_ = error;
revalidation_status_ = RevalidationStatus::kNoRevalidatingOrFailed;
if (IsMainThread())
MemoryCache::Get()->Remove(this);
bool failed_during_start = status_ == ResourceStatus::kNotStarted;
if (!ErrorOccurred()) {
SetStatus(ResourceStatus::kLoadError);
// If the response type has not been set, set it to "error". This is
// important because in some cases we arrive here after setting the response
// type (e.g., while downloading payload), and that shouldn't change the
// response type.
if (response_.GetType() == network::mojom::FetchResponseType::kDefault)
response_.SetType(network::mojom::FetchResponseType::kError);
}
DCHECK(ErrorOccurred());
ClearData();
CheckResourceIntegrity();
loader_ = nullptr;
TriggerNotificationForFinishObservers(task_runner);
// Most resource types don't expect to succeed or fail inside
// ResourceFetcher::RequestResource(). If the request does complete
// immediately, the convention is to notify the client asynchronously
// unless the type is exempted for historical reasons (mostly due to
// performance implications to making those notifications asynchronous).
// So if this is an immediate failure (i.e., before NotifyStartLoad()),
// post a task if the Resource::Type supports it.
if (failed_during_start && !NeedsSynchronousCacheHit(GetType(), options_)) {
task_runner->PostTask(FROM_HERE, BindOnce(&Resource::NotifyFinished,
WrapWeakPersistent(this)));
} else {
NotifyFinished();
}
}
void Resource::Finish(base::TimeTicks load_response_end,
base::SingleThreadTaskRunner* task_runner) {
DCHECK(!IsCacheValidator());
load_response_end_ = load_response_end;
if (!ErrorOccurred())
status_ = ResourceStatus::kCached;
CheckResourceIntegrity();
loader_ = nullptr;
TriggerNotificationForFinishObservers(task_runner);
NotifyFinished();
}
AtomicString Resource::HttpContentType() const {
return GetResponse().HttpContentType();
}
bool Resource::ForceIntegrityChecks() const {
return IsLinkPreload() || !GetResponse().GetUnencodedDigests().empty();
}
bool Resource::MustRefetchDueToIntegrityMetadata(
const FetchParameters& params) const {
if (params.IntegrityMetadata().empty())
return false;
return IntegrityMetadata() != params.IntegrityMetadata();
}
const scoped_refptr<const SecurityOrigin>& Resource::GetOrigin() const {
return LastResourceRequest().RequestorOrigin();
}
void Resource::DidDownloadToBlob(scoped_refptr<BlobDataHandle>) {}
static base::TimeDelta CurrentAge(const ResourceResponse& response,
base::Time response_timestamp) {
// RFC2616 13.2.3
// No compensation for latency as that is not terribly important in practice
std::optional<base::Time> date_value = response.Date();
base::TimeDelta apparent_age;
if (date_value && response_timestamp >= date_value.value())
apparent_age = response_timestamp - date_value.value();
std::optional<base::TimeDelta> age_value = response.Age();
base::TimeDelta corrected_received_age =
age_value ? std::max(apparent_age, age_value.value()) : apparent_age;
base::TimeDelta resident_time = Now() - response_timestamp;
return corrected_received_age + resident_time;
}
static base::TimeDelta FreshnessLifetime(const ResourceResponse& response,
base::Time response_timestamp) {
#if !BUILDFLAG(IS_ANDROID)
// On desktop, local files should be reloaded in case they change.
if (response.CurrentRequestUrl().IsLocalFile())
return base::TimeDelta();
#endif
// Cache other non-http / non-filesystem resources liberally.
if (!response.CurrentRequestUrl().ProtocolIsInHTTPFamily() &&
!response.CurrentRequestUrl().ProtocolIs("filesystem"))
return base::TimeDelta::Max();
// RFC2616 13.2.4
std::optional<base::TimeDelta> max_age_value = response.CacheControlMaxAge();
if (max_age_value)
return max_age_value.value();
std::optional<base::Time> expires = response.Expires();
std::optional<base::Time> date = response.Date();
base::Time creation_time = date ? date.value() : response_timestamp;
if (expires)
return expires.value() - creation_time;
std::optional<base::Time> last_modified = response.LastModified();
if (last_modified)
return (creation_time - last_modified.value()) * 0.1;
// If no cache headers are present, the specification leaves the decision to
// the UA. Other browsers seem to opt for 0.
return base::TimeDelta();
}
base::TimeDelta Resource::FreshnessLifetime() const {
base::TimeDelta lifetime =
blink::FreshnessLifetime(GetResponse(), response_timestamp_);
for (const auto& redirect : redirect_chain_) {
base::TimeDelta redirect_lifetime = blink::FreshnessLifetime(
redirect.redirect_response_, response_timestamp_);
lifetime = std::min(lifetime, redirect_lifetime);
}
return lifetime;
}
static bool CanUseResponse(const ResourceResponse& response,
bool allow_stale,
base::Time response_timestamp) {
if (response.IsNull())
return false;
if (response.CacheControlContainsNoCache() ||
response.CacheControlContainsNoStore())
return false;
if (response.HttpStatusCode() == 303) {
// Must not be cached.
return false;
}
if (response.HttpStatusCode() == 302 || response.HttpStatusCode() == 307) {
// Default to not cacheable unless explicitly allowed.
bool has_max_age = response.CacheControlMaxAge() != std::nullopt;
bool has_expires = response.Expires() != std::nullopt;
// TODO: consider catching Cache-Control "private" and "public" here.
if (!has_max_age && !has_expires)
return false;
}
base::TimeDelta max_life = FreshnessLifetime(response, response_timestamp);
if (allow_stale)
max_life += response.CacheControlStaleWhileRevalidate();
return CurrentAge(response, response_timestamp) <= max_life;
}
const ResourceRequestHead& Resource::LastResourceRequest() const {
if (!redirect_chain_.size())
return GetResourceRequest();
return redirect_chain_.back().request_;
}
const ResourceResponse& Resource::LastResourceResponse() const {
if (!redirect_chain_.size())
return GetResponse();
return redirect_chain_.back().redirect_response_;
}
size_t Resource::RedirectChainSize() const {
return redirect_chain_.size();
}
void Resource::SetRevalidatingRequest(const ResourceRequestHead& request) {
SECURITY_CHECK(redirect_chain_.empty());
SECURITY_CHECK(!is_unused_preload_);
DCHECK(!request.IsNull());
CHECK(!is_revalidation_start_forbidden_);
revalidation_status_ = RevalidationStatus::kRevalidating;
resource_request_ = request;
status_ = ResourceStatus::kNotStarted;
}
bool Resource::WillFollowRedirect(const ResourceRequest& new_request,
const ResourceResponse& redirect_response) {
if (IsCacheValidator()) {
RevalidationFailed();
}
redirect_chain_.push_back(RedirectPair(new_request, redirect_response));
return true;
}
void Resource::SetResponse(const ResourceResponse& response) {
response_ = response;
}
void Resource::ResponseReceived(const ResourceResponse& response) {
response_timestamp_ = Now();
if (IsCacheValidator()) {
if (IsSuccessfulRevalidationResponse(response)) {
RevalidationSucceeded(response);
return;
}
RevalidationFailed();
}
SetResponse(response);
String encoding = response.TextEncodingName();
if (!encoding.IsNull())
SetEncoding(encoding);
}
void Resource::SetSerializedCachedMetadata(mojo_base::BigBuffer data) {
DCHECK(!IsCacheValidator());
}
String Resource::ReasonNotDeletable() const {
StringBuilder builder;
if (HasClientsOrObservers()) {
builder.Append("hasClients(");
builder.AppendNumber(clients_.size());
if (!clients_awaiting_callback_.empty()) {
builder.Append(", AwaitingCallback=");
builder.AppendNumber(clients_awaiting_callback_.size());
}
if (!finished_clients_.empty()) {
builder.Append(", Finished=");
builder.AppendNumber(finished_clients_.size());
}
builder.Append(')');
}
if (loader_) {
if (!builder.empty())
builder.Append(' ');
builder.Append("loader_");
}
if (IsMainThread() && MemoryCache::Get()->Contains(this)) {
if (!builder.empty())
builder.Append(' ');
builder.Append("in_memory_cache");
}
return builder.ToString();
}
void Resource::DidAddClient(ResourceClient* client) {
if (scoped_refptr<SharedBuffer> data = Data()) {
for (const auto& span : *data) {
client->DataReceived(this, span);
// Stop pushing data if the client removed itself.
if (!HasClient(client))
break;
}
}
if (!HasClient(client))
return;
if (IsLoaded()) {
client->SetHasFinishedFromMemoryCache();
client->NotifyFinished(this);
if (clients_.Contains(client)) {
finished_clients_.insert(client);
clients_.erase(client);
}
}
}
void Resource::WillAddClientOrObserver() {
if (!HasClientsOrObservers()) {
is_alive_ = true;
}
}
void Resource::AddClient(ResourceClient* client,
base::SingleThreadTaskRunner* task_runner) {
CHECK(!is_add_remove_client_prohibited_);
WillAddClientOrObserver();
if (IsCacheValidator()) {
clients_.insert(client);
return;
}
// If an error has occurred or we have existing data to send to the new client
// and the resource type supports it, send it asynchronously.
if ((ErrorOccurred() || !GetResponse().IsNull()) &&
!NeedsSynchronousCacheHit(GetType(), options_)) {
clients_awaiting_callback_.insert(client);
if (!async_finish_pending_clients_task_.IsActive()) {
async_finish_pending_clients_task_ = PostCancellableTask(
*task_runner, FROM_HERE,
BindOnce(&Resource::FinishPendingClients, WrapWeakPersistent(this)));
}
return;
}
clients_.insert(client);
DidAddClient(client);
return;
}
void Resource::RemoveClient(ResourceClient* client) {
CHECK(!is_add_remove_client_prohibited_);
if (finished_clients_.Contains(client))
finished_clients_.erase(client);
else if (clients_awaiting_callback_.Contains(client))
clients_awaiting_callback_.erase(client);
else
clients_.erase(client);
if (clients_awaiting_callback_.empty() &&
async_finish_pending_clients_task_.IsActive()) {
async_finish_pending_clients_task_.Cancel();
}
DidRemoveClientOrObserver();
}
void Resource::AddFinishObserver(ResourceFinishObserver* client,
base::SingleThreadTaskRunner* task_runner) {
CHECK(!is_add_remove_client_prohibited_);
DCHECK(!finish_observers_.Contains(client));
WillAddClientOrObserver();
finish_observers_.insert(client);
if (IsLoaded())
TriggerNotificationForFinishObservers(task_runner);
}
void Resource::RemoveFinishObserver(ResourceFinishObserver* client) {
CHECK(!is_add_remove_client_prohibited_);
finish_observers_.erase(client);
DidRemoveClientOrObserver();
}
void Resource::DidRemoveClientOrObserver() {
if (!HasClientsOrObservers() && is_alive_) {
is_alive_ = false;
AllClientsAndObserversRemoved();
// RFC2616 14.9.2:
// "no-store: ... MUST make a best-effort attempt to remove the information
// from volatile storage as promptly as possible"
// "... History buffers MAY store such responses as part of their normal
// operation."
if (HasCacheControlNoStoreHeader() && IsMainThread()) {
MemoryCache::Get()->Remove(this);
}
}
}
void Resource::AllClientsAndObserversRemoved() {
if (loader_)
loader_->ScheduleCancel();
}
void Resource::SetDecodedSize(size_t decoded_size) {
if (decoded_size == decoded_size_)
return;
size_t old_size = size();
decoded_size_ = decoded_size;
if (IsMainThread())
MemoryCache::Get()->Update(this, old_size, size());
}
void Resource::SetEncodedSize(size_t encoded_size) {
if (encoded_size == encoded_size_) {
return;
}
size_t old_size = size();
encoded_size_ = encoded_size;
if (IsMainThread())
MemoryCache::Get()->Update(this, old_size, size());
}
void Resource::FinishPendingClients() {
// We're going to notify clients one by one. It is simple if the client does
// nothing. However there are a couple other things that can happen.
//
// 1. Clients can be added during the loop. Make sure they are not processed.
// 2. Clients can be removed during the loop. Make sure they are always
// available to be removed. Also don't call removed clients or add them
// back.
//
// Handle case (1) by saving a list of clients to notify. A separate list also
// ensure a client is either in cliens_ or clients_awaiting_callback_.
HeapVector<Member<ResourceClient>> clients_to_notify(
clients_awaiting_callback_.Values());
for (const auto& client : clients_to_notify) {
// Handle case (2) to skip removed clients.
if (!clients_awaiting_callback_.erase(client))
continue;
clients_.insert(client);
// When revalidation starts after waiting clients are scheduled and
// before they are added here. In such cases, we just add the clients
// to |clients_| without DidAddClient(), as in Resource::AddClient().
if (!IsCacheValidator()) {
DidAddClient(client);
}
}
// It is still possible for the above loop to finish a new client
// synchronously. If there's no client waiting we should deschedule.
bool scheduled = async_finish_pending_clients_task_.IsActive();
if (scheduled && clients_awaiting_callback_.empty())
async_finish_pending_clients_task_.Cancel();
// Prevent the case when there are clients waiting but no callback scheduled.
DCHECK(clients_awaiting_callback_.empty() || scheduled);
}
Resource::MatchStatus Resource::CanReuse(const FetchParameters& params) const {
const ResourceRequest& new_request = params.GetResourceRequest();
const ResourceLoaderOptions& new_options = params.Options();
scoped_refptr<const SecurityOrigin> existing_origin =
GetResourceRequest().RequestorOrigin();
scoped_refptr<const SecurityOrigin> new_origin =
new_request.RequestorOrigin();
DCHECK(existing_origin);
DCHECK(new_origin);
// Never reuse opaque responses from a service worker for requests that are
// not no-cors. https://crbug.com/625575
// TODO(yhirano): Remove this.
if (GetResponse().WasFetchedViaServiceWorker() &&
GetResponse().GetType() == network::mojom::FetchResponseType::kOpaque &&
new_request.GetMode() != network::mojom::RequestMode::kNoCors) {
return MatchStatus::kUnknownFailure;
}
// Use GetResourceRequest to get the const resource_request_.
const ResourceRequestHead& current_request = GetResourceRequest();
// If credentials mode is defferent from the the previous request, re-fetch
// the resource.
//
// This helps with the case where the server sends back
// "Access-Control-Allow-Origin: *" all the time, but some of the client's
// requests are made without CORS and some with.
if (current_request.GetCredentialsMode() !=
new_request.GetCredentialsMode()) {
return MatchStatus::kRequestCredentialsModeDoesNotMatch;
}
// Certain requests (e.g., XHRs) might have manually set headers that require
// revalidation. In theory, this should be a Revalidate case. In practice, the
// MemoryCache revalidation path assumes a whole bunch of things about how
// revalidation works that manual headers violate, so punt to Reload instead.
//
// Similarly, a request with manually added revalidation headers can lead to a
// 304 response for a request that wasn't flagged as a revalidation attempt.
// Normally, successful revalidation will maintain the original response's
// status code, but for a manual revalidation the response code remains 304.
// In this case, the Resource likely has insufficient context to provide a
// useful cache hit or revalidation. See http://crbug.com/643659
if (new_request.IsConditional() || response_.HttpStatusCode() == 304) {
return MatchStatus::kUnknownFailure;
}
// Answers the question "can a separate request with different options be
// re-used" (e.g. preload request). The safe (but possibly slow) answer is
// always false.
//
// Data buffering policy differences are believed to be safe for re-use.
//
// TODO: Check content_security_policy_option.
//
// initiator_info is purely informational and should be benign for re-use.
//
// request_initiator_context is benign (indicates document vs. worker).
// Reuse only if both the existing Resource and the new request are
// asynchronous. Particularly,
// 1. Sync and async Resource/requests shouldn't be mixed (crbug.com/652172),
// 2. Sync existing Resources shouldn't be revalidated, and
// 3. Sync new requests shouldn't revalidate existing Resources.
//
// 2. and 3. are because SyncResourceHandler handles redirects without
// calling WillFollowRedirect, and causes response URL mismatch
// (crbug.com/618967) and bypassing redirect restriction around revalidation
// (crbug.com/613971 for 2. and crbug.com/614989 for 3.).
if (new_options.synchronous_policy == kRequestSynchronously ||
options_.synchronous_policy == kRequestSynchronously) {
return MatchStatus::kSynchronousFlagDoesNotMatch;
}
if (current_request.GetKeepalive() || new_request.GetKeepalive())
return MatchStatus::kKeepaliveSet;
if (current_request.HttpMethod() != http_names::kGET ||
new_request.HttpMethod() != http_names::kGET) {
return MatchStatus::kRequestMethodDoesNotMatch;
}
// A GET request doesn't have a request body.
DCHECK(!new_request.HttpBody());
// Don't reuse an existing resource when the source origin is different.
if (!existing_origin->IsSameOriginWith(new_origin.get()))
return MatchStatus::kUnknownFailure;
if (new_request.GetCredentialsMode() !=
current_request.GetCredentialsMode()) {
return MatchStatus::kRequestCredentialsModeDoesNotMatch;
}
const auto new_mode = new_request.GetMode();
const auto existing_mode = current_request.GetMode();
if (new_mode != existing_mode)
return MatchStatus::kRequestModeDoesNotMatch;
return MatchStatus::kOk;
}
void Resource::Prune() {
DestroyDecodedDataIfPossible();
}
void Resource::OnMemoryPressure(
base::MemoryPressureLevel memory_pressure_level) {
if (memory_pressure_level == base::MEMORY_PRESSURE_LEVEL_CRITICAL &&
base::FeatureList::IsEnabled(
features::kReleaseResourceDecodedDataOnMemoryPressure)) {
Prune();
}
}
void Resource::OnMemoryDump(WebMemoryDumpLevelOfDetail level_of_detail,
WebProcessMemoryDump* memory_dump) const {
static const size_t kMaxURLReportLength = 128;
static const int kMaxResourceClientToShowInMemoryInfra = 10;
const String dump_name = GetMemoryDumpName();
WebMemoryAllocatorDump* dump =
memory_dump->CreateMemoryAllocatorDump(dump_name);
if (data_)
GetSharedBufferMemoryDump(Data(), dump_name, memory_dump);
if (level_of_detail == WebMemoryDumpLevelOfDetail::kDetailed) {
String url_to_report = Url().GetString();
if (url_to_report.length() > kMaxURLReportLength) {
url_to_report.Truncate(kMaxURLReportLength);
url_to_report = StrCat({url_to_report, "..."});
}
dump->AddString("url", "", url_to_report);
dump->AddString("reason_not_deletable", "", ReasonNotDeletable());
Vector<String> client_names;
ResourceClientWalker<ResourceClient> walker(clients_);
while (ResourceClient* client = walker.Next())
client_names.push_back(client->DebugName());
ResourceClientWalker<ResourceClient> walker2(clients_awaiting_callback_);
while (ResourceClient* client = walker2.Next())
client_names.push_back(StrCat({"(awaiting) ", client->DebugName()}));
ResourceClientWalker<ResourceClient> walker3(finished_clients_);
while (ResourceClient* client = walker3.Next())
client_names.push_back(StrCat({"(finished) ", client->DebugName()}));
std::sort(client_names.begin(), client_names.end(),
CodeUnitCompareLessThan);
StringBuilder builder;
for (wtf_size_t i = 0;
i < client_names.size() && i < kMaxResourceClientToShowInMemoryInfra;
++i) {
if (i > 0)
builder.Append(" / ");
builder.Append(client_names[i]);
}
if (client_names.size() > kMaxResourceClientToShowInMemoryInfra) {
builder.Append(" / and ");
builder.AppendNumber(client_names.size() -
kMaxResourceClientToShowInMemoryInfra);
builder.Append(" more");
}
dump->AddString("ResourceClient", "", builder.ToString());
}
const String overhead_name = StrCat({dump_name, "/metadata"});
WebMemoryAllocatorDump* overhead_dump =
memory_dump->CreateMemoryAllocatorDump(overhead_name);
overhead_dump->AddScalar("size", "bytes", OverheadSize());
memory_dump->AddSuballocation(overhead_dump->Guid(),
String(Partitions::kAllocatedObjectPoolName));
}
String Resource::GetMemoryDumpName() const {
return StrCat({"web_cache/",
ResourceTypeToString(GetType(), Options().initiator_info.name),
"_resources/", String::Number(InspectorId())});
}
void Resource::SetCachePolicyBypassingCache() {
resource_request_.SetCacheMode(mojom::FetchCacheMode::kBypassCache);
}
void Resource::ClearRangeRequestHeader() {
resource_request_.ClearHttpHeaderField(http_names::kLowerRange);
}
void Resource::RevalidationSucceeded(
const ResourceResponse& validating_response) {
SECURITY_CHECK(redirect_chain_.empty());
SECURITY_CHECK(
EqualIgnoringFragmentIdentifier(validating_response.CurrentRequestUrl(),
GetResponse().CurrentRequestUrl()));
response_.SetResourceLoadTiming(validating_response.GetResourceLoadTiming());
// RFC2616 10.3.5
// Update cached headers from the 304 response
const HTTPHeaderMap& new_headers = validating_response.HttpHeaderFields();
for (const auto& header : new_headers) {
// Entity headers should not be sent by servers when generating a 304
// response; misconfigured servers send them anyway. We shouldn't allow such
// headers to update the original request. We'll base this on the list
// defined by RFC2616 7.1, with a few additions for extension headers we
// care about.
if (!ShouldUpdateHeaderAfterRevalidation(header.key))
continue;
response_.SetHttpHeaderField(header.key, header.value);
}
revalidation_status_ = RevalidationStatus::kRevalidated;
}
void Resource::RevalidationFailed() {
SECURITY_CHECK(redirect_chain_.empty());
ClearData();
integrity_disposition_ = ResourceIntegrityDisposition::kNotChecked;
integrity_report_.Clear();
DestroyDecodedDataForFailedRevalidation();
revalidation_status_ = RevalidationStatus::kNoRevalidatingOrFailed;
memory_cache_hit_count_ = 0;
}
void Resource::MarkAsPreload() {
DCHECK(!is_unused_preload_);
is_unused_preload_ = true;
}
void Resource::MatchPreload(const FetchParameters& params) {
DCHECK(is_unused_preload_);
is_unused_preload_ = false;
}
bool Resource::CanReuseRedirectChain() const {
for (auto& redirect : redirect_chain_) {
if (!CanUseResponse(redirect.redirect_response_, false /*allow_stale*/,
response_timestamp_)) {
return false;
}
if (redirect.request_.CacheControlContainsNoCache() ||
redirect.request_.CacheControlContainsNoStore())
return false;
}
return true;
}
bool Resource::HasCacheControlNoStoreHeader() const {
return GetResponse().CacheControlContainsNoStore() ||
GetResourceRequest().CacheControlContainsNoStore();
}
bool Resource::MustReloadDueToVaryHeader(
const ResourceRequest& new_request) const {
const AtomicString& vary = GetResponse().HttpHeaderField(http_names::kVary);
if (vary.IsNull())
return false;
if (vary == "*")
return true;
CommaDelimitedHeaderSet vary_headers;
ParseCommaDelimitedHeader(vary, vary_headers);
for (const String& header : vary_headers) {
AtomicString atomic_header(header);
if (GetResourceRequest().HttpHeaderField(atomic_header) !=
new_request.HttpHeaderField(atomic_header)) {
return true;
}
}
return false;
}
bool Resource::MustRevalidateDueToCacheHeaders(bool allow_stale) const {
return !CanUseResponse(GetResponse(), allow_stale, response_timestamp_) ||
GetResourceRequest().CacheControlContainsNoCache() ||
GetResourceRequest().CacheControlContainsNoStore();
}
static bool ShouldRevalidateStaleResponse(const ResourceResponse& response,
base::Time response_timestamp) {
base::TimeDelta staleness = response.CacheControlStaleWhileRevalidate();
if (staleness.is_zero())
return false;
return CurrentAge(response, response_timestamp) >
FreshnessLifetime(response, response_timestamp);
}
bool Resource::ShouldRevalidateStaleResponse() const {
for (auto& redirect : redirect_chain_) {
// Use |response_timestamp_| since we don't store the timestamp
// of each redirect response.
if (blink::ShouldRevalidateStaleResponse(redirect.redirect_response_,
response_timestamp_)) {
return true;
}
}
return blink::ShouldRevalidateStaleResponse(GetResponse(),
response_timestamp_);
}
bool Resource::StaleRevalidationRequested() const {
if (GetResponse().AsyncRevalidationRequested())
return true;
for (auto& redirect : redirect_chain_) {
if (redirect.redirect_response_.AsyncRevalidationRequested())
return true;
}
return false;
}
bool Resource::NetworkAccessed() const {
if (GetResponse().NetworkAccessed())
return true;
for (auto& redirect : redirect_chain_) {
if (redirect.redirect_response_.NetworkAccessed())
return true;
}
return false;
}
bool Resource::CanUseCacheValidator() const {
if (IsLoading() || ErrorOccurred())
return false;
if (HasCacheControlNoStoreHeader())
return false;
// Do not revalidate Resource with redirects. https://crbug.com/613971
if (!RedirectChain().empty())
return false;
return GetResponse().HasCacheValidatorFields() ||
GetResourceRequest().HasCacheValidatorFields();
}
size_t Resource::CalculateOverheadSize() const {
static const int kAverageClientsHashMapSize = 384;
return sizeof(Resource) + GetResponse().MemoryUsage() +
kAverageClientsHashMapSize +
GetResourceRequest().Url().GetString().length() * 2;
}
void Resource::DidChangePriority(ResourceLoadPriority load_priority,
int intra_priority_value) {
resource_request_.SetPriority(load_priority, intra_priority_value);
if (loader_)
loader_->DidChangePriority(load_priority, intra_priority_value);
}
void Resource::UpdateResourceWidth(const AtomicString& resource_width) {
if (resource_width) {
resource_request_.SetHttpHeaderField(AtomicString("sec-ch-width"),
resource_width);
} else {
resource_request_.ClearHttpHeaderField(AtomicString("sec-ch-width"));
}
}
// TODO(toyoshim): Consider to generate automatically. https://crbug.com/675515.
static const char* InitiatorTypeNameToString(
const AtomicString& initiator_type_name) {
if (initiator_type_name == fetch_initiator_type_names::kAudio) {
return "Audio";
}
if (initiator_type_name == fetch_initiator_type_names::kAttributionsrc) {
return "Attribution resource";
}
if (initiator_type_name == fetch_initiator_type_names::kCSS) {
return "CSS resource";
}
if (initiator_type_name == fetch_initiator_type_names::kDocument) {
return "Document";
}
if (initiator_type_name == fetch_initiator_type_names::kIcon) {
return "Icon";
}
if (initiator_type_name == fetch_initiator_type_names::kInternal) {
return "Internal resource";
}
if (initiator_type_name == fetch_initiator_type_names::kFetch) {
return "Fetch";
}
if (initiator_type_name == fetch_initiator_type_names::kLink) {
return "Link element resource";
}
if (initiator_type_name == fetch_initiator_type_names::kOther) {
return "Other resource";
}
if (initiator_type_name ==
fetch_initiator_type_names::kProcessinginstruction) {
return "Processing instruction";
}
if (initiator_type_name == fetch_initiator_type_names::kScript) {
return "Script";
}
if (initiator_type_name == fetch_initiator_type_names::kTrack) {
return "Track";
}
if (initiator_type_name == fetch_initiator_type_names::kUacss) {
return "User Agent CSS resource";
}
if (initiator_type_name == fetch_initiator_type_names::kUse) {
return "SVG Use element resource";
}
if (initiator_type_name == fetch_initiator_type_names::kVideo) {
return "Video";
}
if (initiator_type_name == fetch_initiator_type_names::kXml) {
return "XML resource";
}
if (initiator_type_name == fetch_initiator_type_names::kXmlhttprequest) {
return "XMLHttpRequest";
}
static_assert(
fetch_initiator_type_names::kNamesCount == 20,
"New FetchInitiatorTypeNames should be handled correctly here.");
return "Resource";
}
const char* Resource::ResourceTypeToString(
ResourceType type,
const AtomicString& fetch_initiator_name) {
switch (type) {
case ResourceType::kImage:
return "Image";
case ResourceType::kCSSStyleSheet:
return "CSS stylesheet";
case ResourceType::kScript:
return "Script";
case ResourceType::kFont:
return "Font";
case ResourceType::kRaw:
return InitiatorTypeNameToString(fetch_initiator_name);
case ResourceType::kSVGDocument:
return "SVG document";
case ResourceType::kXSLStyleSheet:
return "XSL stylesheet";
case ResourceType::kLinkPrefetch:
return "Link prefetch resource";
case ResourceType::kTextTrack:
return "Text track";
case ResourceType::kAudio:
return "Audio";
case ResourceType::kVideo:
return "Video";
case ResourceType::kManifest:
return "Manifest";
case ResourceType::kSpeculationRules:
return "SpeculationRule";
case ResourceType::kMock:
return "Mock";
case ResourceType::kDictionary:
return "Dictionary";
}
NOTREACHED();
}
bool Resource::IsLoadEventBlockingResourceType() const {
switch (type_) {
case ResourceType::kImage:
case ResourceType::kCSSStyleSheet:
case ResourceType::kFont:
case ResourceType::kSVGDocument:
case ResourceType::kXSLStyleSheet:
return true;
case ResourceType::kScript:
// <script> elements delay the load event in core/script (e.g. in
// ScriptRunner) and no longer need the delaying in platform/loader side.
case ResourceType::kRaw:
case ResourceType::kLinkPrefetch:
case ResourceType::kTextTrack:
case ResourceType::kAudio:
case ResourceType::kVideo:
case ResourceType::kManifest:
case ResourceType::kMock:
case ResourceType::kSpeculationRules:
case ResourceType::kDictionary:
return false;
}
NOTREACHED();
}
// static
void Resource::SetClockForTesting(const base::Clock* clock) {
g_clock_for_testing = clock;
}
void Resource::SetIsAdResource() {
resource_request_.SetIsAdResource();
}
void Resource::UpdateMemoryCacheLastAccessedTime() {
memory_cache_last_accessed_ = base::TimeTicks::Now();
IncrementMemoryCacheHitCount();
}
std::unique_ptr<BackgroundResponseProcessorFactory>
Resource::MaybeCreateBackgroundResponseProcessorFactory() {
return nullptr;
}
} // namespace blink