blob: d8a6cf83acdbf3d675b78532428b935cb4ca1a2a [file] [log] [blame]
/*
* Copyright (C) 2014-2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "NetworkCache.h"
#if ENABLE(NETWORK_CACHE)
#include "Logging.h"
#include "NetworkCacheStatistics.h"
#include "NetworkCacheStorage.h"
#include <WebCore/CacheValidation.h>
#include <WebCore/FileSystem.h>
#include <WebCore/HTTPHeaderNames.h>
#include <WebCore/NetworkStorageSession.h>
#include <WebCore/PlatformCookieJar.h>
#include <WebCore/ResourceRequest.h>
#include <WebCore/ResourceResponse.h>
#include <WebCore/SharedBuffer.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/text/StringBuilder.h>
#if PLATFORM(COCOA)
#include <notify.h>
#endif
namespace WebKit {
namespace NetworkCache {
Cache& singleton()
{
static NeverDestroyed<Cache> instance;
return instance;
}
bool Cache::initialize(const String& cachePath, bool enableEfficacyLogging)
{
m_storage = Storage::open(cachePath);
if (enableEfficacyLogging)
m_statistics = Statistics::open(cachePath);
#if PLATFORM(COCOA)
// Triggers with "notifyutil -p com.apple.WebKit.Cache.dump".
if (m_storage) {
int token;
notify_register_dispatch("com.apple.WebKit.Cache.dump", &token, dispatch_get_main_queue(), ^(int) {
dumpContentsToFile();
});
}
#endif
LOG(NetworkCache, "(NetworkProcess) opened cache storage, success %d", !!m_storage);
return !!m_storage;
}
void Cache::setMaximumSize(size_t maximumSize)
{
if (!m_storage)
return;
m_storage->setMaximumSize(maximumSize);
}
static Key makeCacheKey(const WebCore::ResourceRequest& request)
{
#if ENABLE(CACHE_PARTITIONING)
String partition = request.cachePartition();
#else
String partition;
#endif
if (partition.isEmpty())
partition = ASCIILiteral("No partition");
return { request.httpMethod(), partition, request.url().string() };
}
static String headerValueForVary(const WebCore::ResourceRequest& request, const String& headerName)
{
// Explicit handling for cookies is needed because they are added magically by the networking layer.
// FIXME: The value might have changed between making the request and retrieving the cookie here.
// We could fetch the cookie when making the request but that seems overkill as the case is very rare and it
// is a blocking operation. This should be sufficient to cover reasonable cases.
if (headerName == httpHeaderNameString(WebCore::HTTPHeaderName::Cookie))
return WebCore::cookieRequestHeaderFieldValue(WebCore::NetworkStorageSession::defaultStorageSession(), request.firstPartyForCookies(), request.url());
return request.httpHeaderField(headerName);
}
static Vector<std::pair<String, String>> collectVaryingRequestHeaders(const WebCore::ResourceRequest& request, const WebCore::ResourceResponse& response)
{
String varyValue = response.httpHeaderField(WebCore::HTTPHeaderName::Vary);
if (varyValue.isEmpty())
return { };
Vector<String> varyingHeaderNames;
varyValue.split(',', /*allowEmptyEntries*/ false, varyingHeaderNames);
Vector<std::pair<String, String>> varyingRequestHeaders;
varyingRequestHeaders.reserveCapacity(varyingHeaderNames.size());
for (auto& varyHeaderName : varyingHeaderNames) {
String headerName = varyHeaderName.stripWhiteSpace();
String headerValue = headerValueForVary(request, headerName);
varyingRequestHeaders.append(std::make_pair(headerName, headerValue));
}
return varyingRequestHeaders;
}
static bool verifyVaryingRequestHeaders(const Vector<std::pair<String, String>>& varyingRequestHeaders, const WebCore::ResourceRequest& request)
{
for (auto& varyingRequestHeader : varyingRequestHeaders) {
// FIXME: Vary: * in response would ideally trigger a cache delete instead of a store.
if (varyingRequestHeader.first == "*")
return false;
String headerValue = headerValueForVary(request, varyingRequestHeader.first);
if (headerValue != varyingRequestHeader.second)
return false;
}
return true;
}
static bool cachePolicyAllowsExpired(WebCore::ResourceRequestCachePolicy policy)
{
switch (policy) {
case WebCore::ReturnCacheDataElseLoad:
case WebCore::ReturnCacheDataDontLoad:
return true;
case WebCore::UseProtocolCachePolicy:
case WebCore::ReloadIgnoringCacheData:
return false;
}
ASSERT_NOT_REACHED();
return false;
}
static bool responseHasExpired(const WebCore::ResourceResponse& response, std::chrono::milliseconds timestamp, double maxStale)
{
if (response.cacheControlContainsNoCache())
return true;
auto doubleTimeStamp = std::chrono::duration<double>(timestamp);
double age = WebCore::computeCurrentAge(response, doubleTimeStamp.count());
double lifetime = WebCore::computeFreshnessLifetimeForHTTPFamily(response, doubleTimeStamp.count());
maxStale = std::isnan(maxStale) ? 0 : maxStale;
bool hasExpired = age - lifetime > maxStale;
#ifndef LOG_DISABLED
if (hasExpired)
LOG(NetworkCache, "(NetworkProcess) needsRevalidation hasExpired age=%f lifetime=%f max-stale=%g", age, lifetime, maxStale);
#endif
return hasExpired;
}
static bool responseNeedsRevalidation(const WebCore::ResourceResponse& response, const WebCore::ResourceRequest& request, std::chrono::milliseconds timestamp)
{
auto requestDirectives = WebCore::parseCacheControlDirectives(request.httpHeaderFields());
if (requestDirectives.noCache)
return true;
// For requests we ignore max-age values other than zero.
if (requestDirectives.maxAge == 0)
return true;
return responseHasExpired(response, timestamp, requestDirectives.maxStale);
}
static UseDecision makeUseDecision(const Entry& entry, const WebCore::ResourceRequest& request)
{
if (!verifyVaryingRequestHeaders(entry.varyingRequestHeaders(), request))
return UseDecision::NoDueToVaryingHeaderMismatch;
// We never revalidate in the case of a history navigation.
if (cachePolicyAllowsExpired(request.cachePolicy()))
return UseDecision::Use;
if (!responseNeedsRevalidation(entry.response(), request, entry.timeStamp()))
return UseDecision::Use;
if (!entry.response().hasCacheValidatorFields())
return UseDecision::NoDueToMissingValidatorFields;
return UseDecision::Validate;
}
static RetrieveDecision makeRetrieveDecision(const WebCore::ResourceRequest& request)
{
// FIXME: Support HEAD requests.
if (request.httpMethod() != "GET")
return RetrieveDecision::NoDueToHTTPMethod;
// FIXME: We should be able to validate conditional requests using cache.
if (request.isConditional())
return RetrieveDecision::NoDueToConditionalRequest;
if (request.cachePolicy() == WebCore::ReloadIgnoringCacheData)
return RetrieveDecision::NoDueToReloadIgnoringCache;
return RetrieveDecision::Yes;
}
// http://tools.ietf.org/html/rfc7231#page-48
static bool isStatusCodeCacheableByDefault(int statusCode)
{
switch (statusCode) {
case 200: // OK
case 203: // Non-Authoritative Information
case 204: // No Content
case 300: // Multiple Choices
case 301: // Moved Permanently
case 404: // Not Found
case 405: // Method Not Allowed
case 410: // Gone
case 414: // URI Too Long
case 501: // Not Implemented
return true;
default:
return false;
}
}
static bool isStatusCodePotentiallyCacheable(int statusCode)
{
switch (statusCode) {
case 201: // Created
case 202: // Accepted
case 205: // Reset Content
case 302: // Found
case 303: // See Other
case 307: // Temporary redirect
case 403: // Forbidden
case 406: // Not Acceptable
case 415: // Unsupported Media Type
return true;
default:
return false;
}
}
static StoreDecision makeStoreDecision(const WebCore::ResourceRequest& originalRequest, const WebCore::ResourceResponse& response)
{
if (!originalRequest.url().protocolIsInHTTPFamily() || !response.isHTTP())
return StoreDecision::NoDueToProtocol;
if (originalRequest.httpMethod() != "GET")
return StoreDecision::NoDueToHTTPMethod;
auto requestDirectives = WebCore::parseCacheControlDirectives(originalRequest.httpHeaderFields());
if (requestDirectives.noStore)
return StoreDecision::NoDueToNoStoreRequest;
if (response.cacheControlContainsNoStore())
return StoreDecision::NoDueToNoStoreResponse;
if (!isStatusCodeCacheableByDefault(response.httpStatusCode())) {
// http://tools.ietf.org/html/rfc7234#section-4.3.2
bool hasExpirationHeaders = std::isfinite(response.expires()) || std::isfinite(response.cacheControlMaxAge());
bool expirationHeadersAllowCaching = isStatusCodePotentiallyCacheable(response.httpStatusCode()) && hasExpirationHeaders;
if (!expirationHeadersAllowCaching)
return StoreDecision::NoDueToHTTPStatusCode;
}
// Main resource has ResourceLoadPriorityVeryHigh.
bool storeUnconditionallyForHistoryNavigation = originalRequest.priority() == WebCore::ResourceLoadPriorityVeryHigh;
if (!storeUnconditionallyForHistoryNavigation) {
auto currentTime = std::chrono::duration<double>(std::chrono::system_clock::now().time_since_epoch());
bool hasNonZeroLifetime = WebCore::computeFreshnessLifetimeForHTTPFamily(response, currentTime.count()) > 0;
bool possiblyReusable = response.hasCacheValidatorFields() || hasNonZeroLifetime;
if (!possiblyReusable)
return StoreDecision::NoDueToUnlikelyToReuse;
}
return StoreDecision::Yes;
}
void Cache::retrieve(const WebCore::ResourceRequest& originalRequest, uint64_t webPageID, std::function<void (std::unique_ptr<Entry>)> completionHandler)
{
ASSERT(isEnabled());
ASSERT(originalRequest.url().protocolIsInHTTPFamily());
LOG(NetworkCache, "(NetworkProcess) retrieving %s priority %u", originalRequest.url().string().ascii().data(), originalRequest.priority());
if (m_statistics)
m_statistics->recordRetrievalRequest(webPageID);
Key storageKey = makeCacheKey(originalRequest);
auto retrieveDecision = makeRetrieveDecision(originalRequest);
if (retrieveDecision != RetrieveDecision::Yes) {
if (m_statistics)
m_statistics->recordNotUsingCacheForRequest(webPageID, storageKey, originalRequest, retrieveDecision);
completionHandler(nullptr);
return;
}
auto startTime = std::chrono::system_clock::now();
unsigned priority = originalRequest.priority();
m_storage->retrieve(storageKey, priority, [this, originalRequest, completionHandler, startTime, storageKey, webPageID](std::unique_ptr<Storage::Record> record) {
if (!record) {
LOG(NetworkCache, "(NetworkProcess) not found in storage");
if (m_statistics)
m_statistics->recordRetrievalFailure(webPageID, storageKey, originalRequest);
completionHandler(nullptr);
return false;
}
ASSERT(record->key == storageKey);
auto entry = Entry::decodeStorageRecord(*record);
auto useDecision = entry ? makeUseDecision(*entry, originalRequest) : UseDecision::NoDueToDecodeFailure;
switch (useDecision) {
case UseDecision::Use:
break;
case UseDecision::Validate:
entry->setNeedsValidation();
break;
default:
entry = nullptr;
};
#if !LOG_DISABLED
auto elapsedMS = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - startTime).count();
LOG(NetworkCache, "(NetworkProcess) retrieve complete useDecision=%d priority=%u time=%lldms", useDecision, originalRequest.priority(), elapsedMS);
#endif
completionHandler(WTF::move(entry));
if (m_statistics)
m_statistics->recordRetrievedCachedEntry(webPageID, storageKey, originalRequest, useDecision);
return useDecision != UseDecision::NoDueToDecodeFailure;
});
}
void Cache::store(const WebCore::ResourceRequest& originalRequest, const WebCore::ResourceResponse& response, RefPtr<WebCore::SharedBuffer>&& responseData, std::function<void (MappedBody&)> completionHandler)
{
ASSERT(isEnabled());
ASSERT(responseData);
LOG(NetworkCache, "(NetworkProcess) storing %s, partition %s", originalRequest.url().string().latin1().data(), originalRequest.cachePartition().latin1().data());
StoreDecision storeDecision = makeStoreDecision(originalRequest, response);
if (storeDecision != StoreDecision::Yes) {
LOG(NetworkCache, "(NetworkProcess) didn't store, storeDecision=%d", storeDecision);
if (m_statistics) {
auto key = makeCacheKey(originalRequest);
m_statistics->recordNotCachingResponse(key, storeDecision);
}
return;
}
Entry cacheEntry(makeCacheKey(originalRequest), response, WTF::move(responseData), collectVaryingRequestHeaders(originalRequest, response));
auto record = cacheEntry.encodeAsStorageRecord();
m_storage->store(record, [completionHandler](bool success, const Data& bodyData) {
MappedBody mappedBody;
#if ENABLE(SHAREABLE_RESOURCE)
if (bodyData.isMap()) {
RefPtr<SharedMemory> sharedMemory = SharedMemory::createFromVMBuffer(const_cast<uint8_t*>(bodyData.data()), bodyData.size());
mappedBody.shareableResource = sharedMemory ? ShareableResource::create(WTF::move(sharedMemory), 0, bodyData.size()) : nullptr;
if (mappedBody.shareableResource)
mappedBody.shareableResource->createHandle(mappedBody.shareableResourceHandle);
}
#endif
completionHandler(mappedBody);
LOG(NetworkCache, "(NetworkProcess) store success=%d", success);
});
}
void Cache::update(const WebCore::ResourceRequest& originalRequest, const Entry& existingEntry, const WebCore::ResourceResponse& validatingResponse)
{
LOG(NetworkCache, "(NetworkProcess) updating %s", originalRequest.url().string().latin1().data());
WebCore::ResourceResponse response = existingEntry.response();
WebCore::updateResponseHeadersAfterRevalidation(response, validatingResponse);
Entry updateEntry(existingEntry.key(), response, existingEntry.buffer(), collectVaryingRequestHeaders(originalRequest, response));
auto updateRecord = updateEntry.encodeAsStorageRecord();
m_storage->update(updateRecord, existingEntry.sourceStorageRecord(), [](bool success, const Data&) {
LOG(NetworkCache, "(NetworkProcess) updated, success=%d", success);
});
}
void Cache::remove(const Key& key)
{
ASSERT(isEnabled());
m_storage->remove(key);
}
void Cache::traverse(std::function<void (const Entry*)>&& traverseHandler)
{
ASSERT(isEnabled());
m_storage->traverse(0, [traverseHandler](const Storage::Record* record, const Storage::RecordInfo&) {
if (!record) {
traverseHandler(nullptr);
return;
}
auto entry = Entry::decodeStorageRecord(*record);
if (!entry)
return;
traverseHandler(entry.get());
});
}
String Cache::dumpFilePath() const
{
return WebCore::pathByAppendingComponent(m_storage->baseDirectoryPath(), "dump.json");
}
void Cache::dumpContentsToFile()
{
if (!m_storage)
return;
auto fd = WebCore::openFile(dumpFilePath(), WebCore::OpenForWrite);
if (!fd)
return;
auto prologue = String("{\n\"entries\": [\n").utf8();
WebCore::writeToFile(fd, prologue.data(), prologue.length());
struct Totals {
unsigned count { 0 };
double worth { 0 };
size_t bodySize { 0 };
};
Totals totals;
m_storage->traverse(Storage::TraverseFlag::ComputeWorth, [fd, totals](const Storage::Record* record, const Storage::RecordInfo& info) mutable {
if (!record) {
StringBuilder epilogue;
epilogue.appendLiteral("{}\n],\n");
epilogue.appendLiteral("\"totals\": {\n");
epilogue.appendLiteral("\"count\": ");
epilogue.appendNumber(totals.count);
epilogue.appendLiteral(",\n");
epilogue.appendLiteral("\"bodySize\": ");
epilogue.appendNumber(totals.bodySize);
epilogue.appendLiteral(",\n");
epilogue.appendLiteral("\"averageWorth\": ");
epilogue.appendNumber(totals.count ? totals.worth / totals.count : 0);
epilogue.appendLiteral("\n");
epilogue.appendLiteral("}\n}\n");
auto writeData = epilogue.toString().utf8();
WebCore::writeToFile(fd, writeData.data(), writeData.length());
WebCore::closeFile(fd);
return;
}
auto entry = Entry::decodeStorageRecord(*record);
if (!entry)
return;
++totals.count;
totals.worth += info.worth;
totals.bodySize += info.bodySize;
StringBuilder json;
entry->asJSON(json, info);
json.appendLiteral(",\n");
auto writeData = json.toString().utf8();
WebCore::writeToFile(fd, writeData.data(), writeData.length());
});
}
void Cache::clear()
{
LOG(NetworkCache, "(NetworkProcess) clearing cache");
if (m_storage) {
m_storage->clear();
auto queue = WorkQueue::create("com.apple.WebKit.Cache.delete");
StringCapture dumpFilePathCapture(dumpFilePath());
queue->dispatch([dumpFilePathCapture] {
WebCore::deleteFile(dumpFilePathCapture.string());
});
}
if (m_statistics)
m_statistics->clear();
}
String Cache::storagePath() const
{
return m_storage ? m_storage->directoryPath() : String();
}
}
}
#endif