| 1 | /* |
| 2 | * Copyright (C) 2014-2017 Apple Inc. All rights reserved. |
| 3 | * |
| 4 | * Redistribution and use in source and binary forms, with or without |
| 5 | * modification, are permitted provided that the following conditions |
| 6 | * are met: |
| 7 | * 1. Redistributions of source code must retain the above copyright |
| 8 | * notice, this list of conditions and the following disclaimer. |
| 9 | * 2. Redistributions in binary form must reproduce the above copyright |
| 10 | * notice, this list of conditions and the following disclaimer in the |
| 11 | * documentation and/or other materials provided with the distribution. |
| 12 | * |
| 13 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| 14 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| 15 | * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 16 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| 17 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 18 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 19 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 20 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| 21 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 22 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| 23 | * THE POSSIBILITY OF SUCH DAMAGE. |
| 24 | */ |
| 25 | |
| 26 | #include "config.h" |
| 27 | #include "NetworkCache.h" |
| 28 | |
| 29 | #include "Logging.h" |
| 30 | #include "NetworkCacheSpeculativeLoadManager.h" |
| 31 | #include "NetworkCacheStorage.h" |
| 32 | #include "NetworkProcess.h" |
| 33 | #include <WebCore/CacheValidation.h> |
| 34 | #include <WebCore/HTTPHeaderNames.h> |
| 35 | #include <WebCore/LowPowerModeNotifier.h> |
| 36 | #include <WebCore/NetworkStorageSession.h> |
| 37 | #include <WebCore/ResourceRequest.h> |
| 38 | #include <WebCore/ResourceResponse.h> |
| 39 | #include <WebCore/SharedBuffer.h> |
| 40 | #include <wtf/FileSystem.h> |
| 41 | #include <wtf/MainThread.h> |
| 42 | #include <wtf/NeverDestroyed.h> |
| 43 | #include <wtf/RunLoop.h> |
| 44 | #include <wtf/text/StringBuilder.h> |
| 45 | |
| 46 | #if PLATFORM(COCOA) |
| 47 | #include <notify.h> |
| 48 | #endif |
| 49 | |
| 50 | namespace WebKit { |
| 51 | namespace NetworkCache { |
| 52 | |
| 53 | using namespace FileSystem; |
| 54 | |
| 55 | static const AtomicString& resourceType() |
| 56 | { |
| 57 | ASSERT(WTF::RunLoop::isMain()); |
| 58 | static NeverDestroyed<const AtomicString> resource("Resource" , AtomicString::ConstructFromLiteral); |
| 59 | return resource; |
| 60 | } |
| 61 | |
| 62 | RefPtr<Cache> Cache::open(NetworkProcess& networkProcess, const String& cachePath, OptionSet<Option> options) |
| 63 | { |
| 64 | auto storage = Storage::open(cachePath, options.contains(Option::TestingMode) ? Storage::Mode::AvoidRandomness : Storage::Mode::Normal); |
| 65 | |
| 66 | LOG(NetworkCache, "(NetworkProcess) opened cache storage, success %d" , !!storage); |
| 67 | |
| 68 | if (!storage) |
| 69 | return nullptr; |
| 70 | |
| 71 | return adoptRef(*new Cache(networkProcess, storage.releaseNonNull(), options)); |
| 72 | } |
| 73 | |
| 74 | #if PLATFORM(GTK) |
| 75 | static void dumpFileChanged(Cache* cache) |
| 76 | { |
| 77 | cache->dumpContentsToFile(); |
| 78 | } |
| 79 | #endif |
| 80 | |
| 81 | Cache::Cache(NetworkProcess& networkProcess, Ref<Storage>&& storage, OptionSet<Option> options) |
| 82 | : m_storage(WTFMove(storage)) |
| 83 | , m_networkProcess(networkProcess) |
| 84 | { |
| 85 | #if ENABLE(NETWORK_CACHE_SPECULATIVE_REVALIDATION) |
| 86 | if (options.contains(Option::SpeculativeRevalidation)) { |
| 87 | m_lowPowerModeNotifier = std::make_unique<WebCore::LowPowerModeNotifier>([this](bool isLowPowerModeEnabled) { |
| 88 | ASSERT(WTF::RunLoop::isMain()); |
| 89 | if (isLowPowerModeEnabled) |
| 90 | m_speculativeLoadManager = nullptr; |
| 91 | else { |
| 92 | ASSERT(!m_speculativeLoadManager); |
| 93 | m_speculativeLoadManager = std::make_unique<SpeculativeLoadManager>(*this, m_storage.get()); |
| 94 | } |
| 95 | }); |
| 96 | if (!m_lowPowerModeNotifier->isLowPowerModeEnabled()) |
| 97 | m_speculativeLoadManager = std::make_unique<SpeculativeLoadManager>(*this, m_storage.get()); |
| 98 | } |
| 99 | #endif |
| 100 | |
| 101 | if (options.contains(Option::RegisterNotify)) { |
| 102 | #if PLATFORM(COCOA) |
| 103 | // Triggers with "notifyutil -p com.apple.WebKit.Cache.dump". |
| 104 | int token; |
| 105 | notify_register_dispatch("com.apple.WebKit.Cache.dump" , &token, dispatch_get_main_queue(), ^(int) { |
| 106 | dumpContentsToFile(); |
| 107 | }); |
| 108 | #endif |
| 109 | #if PLATFORM(GTK) |
| 110 | // Triggers with "touch $cachePath/dump". |
| 111 | CString dumpFilePath = fileSystemRepresentation(pathByAppendingComponent(m_storage->basePath(), "dump" )); |
| 112 | GRefPtr<GFile> dumpFile = adoptGRef(g_file_new_for_path(dumpFilePath.data())); |
| 113 | GFileMonitor* monitor = g_file_monitor_file(dumpFile.get(), G_FILE_MONITOR_NONE, nullptr, nullptr); |
| 114 | g_signal_connect_swapped(monitor, "changed" , G_CALLBACK(dumpFileChanged), this); |
| 115 | #endif |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | Cache::~Cache() |
| 120 | { |
| 121 | } |
| 122 | |
| 123 | void Cache::setCapacity(size_t maximumSize) |
| 124 | { |
| 125 | m_storage->setCapacity(maximumSize); |
| 126 | } |
| 127 | |
| 128 | Key Cache::makeCacheKey(const WebCore::ResourceRequest& request) |
| 129 | { |
| 130 | // FIXME: This implements minimal Range header disk cache support. We don't parse |
| 131 | // ranges so only the same exact range request will be served from the cache. |
| 132 | String range = request.httpHeaderField(WebCore::HTTPHeaderName::Range); |
| 133 | return { request.cachePartition(), resourceType(), range, request.url().string(), m_storage->salt() }; |
| 134 | } |
| 135 | |
| 136 | static bool cachePolicyAllowsExpired(WebCore::ResourceRequestCachePolicy policy) |
| 137 | { |
| 138 | switch (policy) { |
| 139 | case WebCore::ResourceRequestCachePolicy::ReturnCacheDataElseLoad: |
| 140 | case WebCore::ResourceRequestCachePolicy::ReturnCacheDataDontLoad: |
| 141 | return true; |
| 142 | case WebCore::ResourceRequestCachePolicy::UseProtocolCachePolicy: |
| 143 | case WebCore::ResourceRequestCachePolicy::ReloadIgnoringCacheData: |
| 144 | case WebCore::ResourceRequestCachePolicy::RefreshAnyCacheData: |
| 145 | return false; |
| 146 | case WebCore::ResourceRequestCachePolicy::DoNotUseAnyCache: |
| 147 | ASSERT_NOT_REACHED(); |
| 148 | return false; |
| 149 | } |
| 150 | return false; |
| 151 | } |
| 152 | |
| 153 | static bool responseHasExpired(const WebCore::ResourceResponse& response, WallTime timestamp, Optional<Seconds> maxStale) |
| 154 | { |
| 155 | if (response.cacheControlContainsNoCache()) |
| 156 | return true; |
| 157 | |
| 158 | auto age = WebCore::computeCurrentAge(response, timestamp); |
| 159 | auto lifetime = WebCore::computeFreshnessLifetimeForHTTPFamily(response, timestamp); |
| 160 | |
| 161 | auto maximumStaleness = maxStale ? maxStale.value() : 0_ms; |
| 162 | bool hasExpired = age - lifetime > maximumStaleness; |
| 163 | |
| 164 | #ifndef LOG_DISABLED |
| 165 | if (hasExpired) |
| 166 | LOG(NetworkCache, "(NetworkProcess) needsRevalidation hasExpired age=%f lifetime=%f max-stale=%g" , age, lifetime, maxStale); |
| 167 | #endif |
| 168 | |
| 169 | return hasExpired; |
| 170 | } |
| 171 | |
| 172 | static bool responseNeedsRevalidation(const WebCore::ResourceResponse& response, const WebCore::ResourceRequest& request, WallTime timestamp) |
| 173 | { |
| 174 | auto requestDirectives = WebCore::parseCacheControlDirectives(request.httpHeaderFields()); |
| 175 | if (requestDirectives.noCache) |
| 176 | return true; |
| 177 | // For requests we ignore max-age values other than zero. |
| 178 | if (requestDirectives.maxAge && requestDirectives.maxAge.value() == 0_ms) |
| 179 | return true; |
| 180 | |
| 181 | return responseHasExpired(response, timestamp, requestDirectives.maxStale); |
| 182 | } |
| 183 | |
| 184 | static UseDecision makeUseDecision(NetworkProcess& networkProcess, const Entry& entry, const WebCore::ResourceRequest& request) |
| 185 | { |
| 186 | // The request is conditional so we force revalidation from the network. We merely check the disk cache |
| 187 | // so we can update the cache entry. |
| 188 | if (request.isConditional() && !entry.redirectRequest()) |
| 189 | return UseDecision::Validate; |
| 190 | |
| 191 | if (!WebCore::verifyVaryingRequestHeaders(networkProcess.defaultStorageSession(), entry.varyingRequestHeaders(), request)) |
| 192 | return UseDecision::NoDueToVaryingHeaderMismatch; |
| 193 | |
| 194 | // We never revalidate in the case of a history navigation. |
| 195 | if (cachePolicyAllowsExpired(request.cachePolicy())) |
| 196 | return UseDecision::Use; |
| 197 | |
| 198 | if (!responseNeedsRevalidation(entry.response(), request, entry.timeStamp())) |
| 199 | return UseDecision::Use; |
| 200 | |
| 201 | if (!entry.response().hasCacheValidatorFields()) |
| 202 | return UseDecision::NoDueToMissingValidatorFields; |
| 203 | |
| 204 | return entry.redirectRequest() ? UseDecision::NoDueToExpiredRedirect : UseDecision::Validate; |
| 205 | } |
| 206 | |
| 207 | static RetrieveDecision makeRetrieveDecision(const WebCore::ResourceRequest& request) |
| 208 | { |
| 209 | ASSERT(request.cachePolicy() != WebCore::ResourceRequestCachePolicy::DoNotUseAnyCache); |
| 210 | |
| 211 | // FIXME: Support HEAD requests. |
| 212 | if (request.httpMethod() != "GET" ) |
| 213 | return RetrieveDecision::NoDueToHTTPMethod; |
| 214 | if (request.cachePolicy() == WebCore::ResourceRequestCachePolicy::ReloadIgnoringCacheData && !request.isConditional()) |
| 215 | return RetrieveDecision::NoDueToReloadIgnoringCache; |
| 216 | |
| 217 | return RetrieveDecision::Yes; |
| 218 | } |
| 219 | |
| 220 | static bool isMediaMIMEType(const String& type) |
| 221 | { |
| 222 | return startsWithLettersIgnoringASCIICase(type, "video/" ) || startsWithLettersIgnoringASCIICase(type, "audio/" ); |
| 223 | } |
| 224 | |
| 225 | static StoreDecision makeStoreDecision(const WebCore::ResourceRequest& originalRequest, const WebCore::ResourceResponse& response, size_t bodySize) |
| 226 | { |
| 227 | if (!originalRequest.url().protocolIsInHTTPFamily() || !response.isHTTP()) |
| 228 | return StoreDecision::NoDueToProtocol; |
| 229 | |
| 230 | if (originalRequest.httpMethod() != "GET" ) |
| 231 | return StoreDecision::NoDueToHTTPMethod; |
| 232 | |
| 233 | auto requestDirectives = WebCore::parseCacheControlDirectives(originalRequest.httpHeaderFields()); |
| 234 | if (requestDirectives.noStore) |
| 235 | return StoreDecision::NoDueToNoStoreRequest; |
| 236 | |
| 237 | if (response.cacheControlContainsNoStore()) |
| 238 | return StoreDecision::NoDueToNoStoreResponse; |
| 239 | |
| 240 | if (!WebCore::isStatusCodeCacheableByDefault(response.httpStatusCode())) { |
| 241 | // http://tools.ietf.org/html/rfc7234#section-4.3.2 |
| 242 | bool = response.expires() || response.cacheControlMaxAge(); |
| 243 | bool = WebCore::isStatusCodePotentiallyCacheable(response.httpStatusCode()) && hasExpirationHeaders; |
| 244 | if (!expirationHeadersAllowCaching) |
| 245 | return StoreDecision::NoDueToHTTPStatusCode; |
| 246 | } |
| 247 | |
| 248 | bool isMainResource = originalRequest.requester() == WebCore::ResourceRequest::Requester::Main; |
| 249 | bool storeUnconditionallyForHistoryNavigation = isMainResource || originalRequest.priority() == WebCore::ResourceLoadPriority::VeryHigh; |
| 250 | if (!storeUnconditionallyForHistoryNavigation) { |
| 251 | auto now = WallTime::now(); |
| 252 | bool hasNonZeroLifetime = !response.cacheControlContainsNoCache() && WebCore::computeFreshnessLifetimeForHTTPFamily(response, now) > 0_ms; |
| 253 | |
| 254 | bool possiblyReusable = response.hasCacheValidatorFields() || hasNonZeroLifetime; |
| 255 | if (!possiblyReusable) |
| 256 | return StoreDecision::NoDueToUnlikelyToReuse; |
| 257 | } |
| 258 | |
| 259 | // Media loaded via XHR is likely being used for MSE streaming (YouTube and Netflix for example). |
| 260 | // Streaming media fills the cache quickly and is unlikely to be reused. |
| 261 | // FIXME: We should introduce a separate media cache partition that doesn't affect other resources. |
| 262 | // FIXME: We should also make sure make the MSE paths are copy-free so we can use mapped buffers from disk effectively. |
| 263 | auto requester = originalRequest.requester(); |
| 264 | bool isDefinitelyStreamingMedia = requester == WebCore::ResourceRequest::Requester::Media; |
| 265 | bool isLikelyStreamingMedia = requester == WebCore::ResourceRequest::Requester::XHR && isMediaMIMEType(response.mimeType()); |
| 266 | if (isLikelyStreamingMedia || isDefinitelyStreamingMedia) |
| 267 | return StoreDecision::NoDueToStreamingMedia; |
| 268 | |
| 269 | return StoreDecision::Yes; |
| 270 | } |
| 271 | |
| 272 | void Cache::retrieve(const WebCore::ResourceRequest& request, const GlobalFrameID& frameID, RetrieveCompletionHandler&& completionHandler) |
| 273 | { |
| 274 | ASSERT(request.url().protocolIsInHTTPFamily()); |
| 275 | |
| 276 | LOG(NetworkCache, "(NetworkProcess) retrieving %s priority %d" , request.url().string().ascii().data(), static_cast<int>(request.priority())); |
| 277 | |
| 278 | Key storageKey = makeCacheKey(request); |
| 279 | auto priority = static_cast<unsigned>(request.priority()); |
| 280 | |
| 281 | RetrieveInfo info; |
| 282 | info.startTime = MonotonicTime::now(); |
| 283 | info.priority = priority; |
| 284 | |
| 285 | #if ENABLE(NETWORK_CACHE_SPECULATIVE_REVALIDATION) |
| 286 | bool canUseSpeculativeRevalidation = m_speculativeLoadManager && !request.isConditional() && !cachePolicyAllowsExpired(request.cachePolicy()); |
| 287 | if (canUseSpeculativeRevalidation) |
| 288 | m_speculativeLoadManager->registerLoad(frameID, request, storageKey); |
| 289 | #endif |
| 290 | |
| 291 | auto retrieveDecision = makeRetrieveDecision(request); |
| 292 | if (retrieveDecision != RetrieveDecision::Yes) { |
| 293 | completeRetrieve(WTFMove(completionHandler), nullptr, info); |
| 294 | return; |
| 295 | } |
| 296 | |
| 297 | #if ENABLE(NETWORK_CACHE_SPECULATIVE_REVALIDATION) |
| 298 | if (canUseSpeculativeRevalidation && m_speculativeLoadManager->canRetrieve(storageKey, request, frameID)) { |
| 299 | m_speculativeLoadManager->retrieve(storageKey, [networkProcess = makeRef(networkProcess()), request, completionHandler = WTFMove(completionHandler), info = WTFMove(info)](std::unique_ptr<Entry> entry) mutable { |
| 300 | info.wasSpeculativeLoad = true; |
| 301 | if (entry && WebCore::verifyVaryingRequestHeaders(networkProcess->defaultStorageSession(), entry->varyingRequestHeaders(), request)) |
| 302 | completeRetrieve(WTFMove(completionHandler), WTFMove(entry), info); |
| 303 | else |
| 304 | completeRetrieve(WTFMove(completionHandler), nullptr, info); |
| 305 | }); |
| 306 | return; |
| 307 | } |
| 308 | #endif |
| 309 | |
| 310 | m_storage->retrieve(storageKey, priority, [request, completionHandler = WTFMove(completionHandler), info = WTFMove(info), storageKey, networkProcess = makeRef(networkProcess())](auto record, auto timings) mutable { |
| 311 | info.storageTimings = timings; |
| 312 | |
| 313 | if (!record) { |
| 314 | LOG(NetworkCache, "(NetworkProcess) not found in storage" ); |
| 315 | |
| 316 | completeRetrieve(WTFMove(completionHandler), nullptr, info); |
| 317 | return false; |
| 318 | } |
| 319 | |
| 320 | ASSERT(record->key == storageKey); |
| 321 | |
| 322 | auto entry = Entry::decodeStorageRecord(*record); |
| 323 | |
| 324 | auto useDecision = entry ? makeUseDecision(networkProcess, *entry, request) : UseDecision::NoDueToDecodeFailure; |
| 325 | switch (useDecision) { |
| 326 | case UseDecision::Use: |
| 327 | break; |
| 328 | case UseDecision::Validate: |
| 329 | entry->setNeedsValidation(true); |
| 330 | break; |
| 331 | default: |
| 332 | entry = nullptr; |
| 333 | }; |
| 334 | |
| 335 | #if !LOG_DISABLED |
| 336 | auto elapsed = MonotonicTime::now() - info.startTime; |
| 337 | LOG(NetworkCache, "(NetworkProcess) retrieve complete useDecision=%d priority=%d time=%" PRIi64 "ms" , static_cast<int>(useDecision), static_cast<int>(request.priority()), elapsed.millisecondsAs<int64_t>()); |
| 338 | #endif |
| 339 | completeRetrieve(WTFMove(completionHandler), WTFMove(entry), info); |
| 340 | |
| 341 | return useDecision != UseDecision::NoDueToDecodeFailure; |
| 342 | }); |
| 343 | } |
| 344 | |
| 345 | void Cache::completeRetrieve(RetrieveCompletionHandler&& handler, std::unique_ptr<Entry> entry, RetrieveInfo& info) |
| 346 | { |
| 347 | info.completionTime = MonotonicTime::now(); |
| 348 | handler(WTFMove(entry), info); |
| 349 | } |
| 350 | |
| 351 | std::unique_ptr<Entry> Cache::makeEntry(const WebCore::ResourceRequest& request, const WebCore::ResourceResponse& response, RefPtr<WebCore::SharedBuffer>&& responseData) |
| 352 | { |
| 353 | return std::make_unique<Entry>(makeCacheKey(request), response, WTFMove(responseData), WebCore::collectVaryingRequestHeaders(networkProcess().defaultStorageSession(), request, response)); |
| 354 | } |
| 355 | |
| 356 | std::unique_ptr<Entry> Cache::makeRedirectEntry(const WebCore::ResourceRequest& request, const WebCore::ResourceResponse& response, const WebCore::ResourceRequest& redirectRequest) |
| 357 | { |
| 358 | return std::make_unique<Entry>(makeCacheKey(request), response, redirectRequest, WebCore::collectVaryingRequestHeaders(networkProcess().defaultStorageSession(), request, response)); |
| 359 | } |
| 360 | |
| 361 | std::unique_ptr<Entry> Cache::store(const WebCore::ResourceRequest& request, const WebCore::ResourceResponse& response, RefPtr<WebCore::SharedBuffer>&& responseData, Function<void(MappedBody&)>&& completionHandler) |
| 362 | { |
| 363 | ASSERT(responseData); |
| 364 | |
| 365 | LOG(NetworkCache, "(NetworkProcess) storing %s, partition %s" , request.url().string().latin1().data(), makeCacheKey(request).partition().latin1().data()); |
| 366 | |
| 367 | StoreDecision storeDecision = makeStoreDecision(request, response, responseData ? responseData->size() : 0); |
| 368 | if (storeDecision != StoreDecision::Yes) { |
| 369 | LOG(NetworkCache, "(NetworkProcess) didn't store, storeDecision=%d" , static_cast<int>(storeDecision)); |
| 370 | auto key = makeCacheKey(request); |
| 371 | |
| 372 | auto isSuccessfulRevalidation = response.httpStatusCode() == 304; |
| 373 | if (!isSuccessfulRevalidation) { |
| 374 | // Make sure we don't keep a stale entry in the cache. |
| 375 | remove(key); |
| 376 | } |
| 377 | |
| 378 | return nullptr; |
| 379 | } |
| 380 | |
| 381 | auto cacheEntry = makeEntry(request, response, WTFMove(responseData)); |
| 382 | auto record = cacheEntry->encodeAsStorageRecord(); |
| 383 | |
| 384 | m_storage->store(record, [protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)](const Data& bodyData) mutable { |
| 385 | MappedBody mappedBody; |
| 386 | #if ENABLE(SHAREABLE_RESOURCE) |
| 387 | if (auto sharedMemory = bodyData.tryCreateSharedMemory()) { |
| 388 | mappedBody.shareableResource = ShareableResource::create(sharedMemory.releaseNonNull(), 0, bodyData.size()); |
| 389 | ASSERT(mappedBody.shareableResource); |
| 390 | mappedBody.shareableResource->createHandle(mappedBody.shareableResourceHandle); |
| 391 | } |
| 392 | #endif |
| 393 | completionHandler(mappedBody); |
| 394 | LOG(NetworkCache, "(NetworkProcess) stored" ); |
| 395 | }); |
| 396 | |
| 397 | return cacheEntry; |
| 398 | } |
| 399 | |
| 400 | std::unique_ptr<Entry> Cache::storeRedirect(const WebCore::ResourceRequest& request, const WebCore::ResourceResponse& response, const WebCore::ResourceRequest& redirectRequest, Optional<Seconds> maxAgeCap) |
| 401 | { |
| 402 | LOG(NetworkCache, "(NetworkProcess) storing redirect %s -> %s" , request.url().string().latin1().data(), redirectRequest.url().string().latin1().data()); |
| 403 | |
| 404 | StoreDecision storeDecision = makeStoreDecision(request, response, 0); |
| 405 | if (storeDecision != StoreDecision::Yes) { |
| 406 | LOG(NetworkCache, "(NetworkProcess) didn't store redirect, storeDecision=%d" , static_cast<int>(storeDecision)); |
| 407 | return nullptr; |
| 408 | } |
| 409 | |
| 410 | auto cacheEntry = makeRedirectEntry(request, response, redirectRequest); |
| 411 | |
| 412 | #if ENABLE(RESOURCE_LOAD_STATISTICS) |
| 413 | if (maxAgeCap) { |
| 414 | LOG(NetworkCache, "(NetworkProcess) capping max age for redirect %s -> %s" , request.url().string().latin1().data(), redirectRequest.url().string().latin1().data()); |
| 415 | cacheEntry->capMaxAge(maxAgeCap.value()); |
| 416 | } |
| 417 | #else |
| 418 | UNUSED_PARAM(maxAgeCap); |
| 419 | #endif |
| 420 | |
| 421 | auto record = cacheEntry->encodeAsStorageRecord(); |
| 422 | |
| 423 | m_storage->store(record, nullptr); |
| 424 | |
| 425 | return cacheEntry; |
| 426 | } |
| 427 | |
| 428 | std::unique_ptr<Entry> Cache::update(const WebCore::ResourceRequest& originalRequest, const GlobalFrameID& frameID, const Entry& existingEntry, const WebCore::ResourceResponse& validatingResponse) |
| 429 | { |
| 430 | LOG(NetworkCache, "(NetworkProcess) updating %s" , originalRequest.url().string().latin1().data()); |
| 431 | |
| 432 | WebCore::ResourceResponse response = existingEntry.response(); |
| 433 | WebCore::updateResponseHeadersAfterRevalidation(response, validatingResponse); |
| 434 | |
| 435 | auto updateEntry = std::make_unique<Entry>(existingEntry.key(), response, existingEntry.buffer(), WebCore::collectVaryingRequestHeaders(networkProcess().defaultStorageSession(), originalRequest, response)); |
| 436 | auto updateRecord = updateEntry->encodeAsStorageRecord(); |
| 437 | |
| 438 | m_storage->store(updateRecord, { }); |
| 439 | |
| 440 | return updateEntry; |
| 441 | } |
| 442 | |
| 443 | void Cache::remove(const Key& key) |
| 444 | { |
| 445 | m_storage->remove(key); |
| 446 | } |
| 447 | |
| 448 | void Cache::remove(const WebCore::ResourceRequest& request) |
| 449 | { |
| 450 | remove(makeCacheKey(request)); |
| 451 | } |
| 452 | |
| 453 | void Cache::remove(const Vector<Key>& keys, Function<void()>&& completionHandler) |
| 454 | { |
| 455 | m_storage->remove(keys, WTFMove(completionHandler)); |
| 456 | } |
| 457 | |
| 458 | void Cache::traverse(Function<void(const TraversalEntry*)>&& traverseHandler) |
| 459 | { |
| 460 | // Protect against clients making excessive traversal requests. |
| 461 | const unsigned maximumTraverseCount = 3; |
| 462 | if (m_traverseCount >= maximumTraverseCount) { |
| 463 | WTFLogAlways("Maximum parallel cache traverse count exceeded. Ignoring traversal request." ); |
| 464 | |
| 465 | RunLoop::main().dispatch([traverseHandler = WTFMove(traverseHandler)] () mutable { |
| 466 | traverseHandler(nullptr); |
| 467 | }); |
| 468 | return; |
| 469 | } |
| 470 | |
| 471 | ++m_traverseCount; |
| 472 | |
| 473 | m_storage->traverse(resourceType(), { }, [this, protectedThis = makeRef(*this), traverseHandler = WTFMove(traverseHandler)] (const Storage::Record* record, const Storage::RecordInfo& recordInfo) mutable { |
| 474 | if (!record) { |
| 475 | --m_traverseCount; |
| 476 | traverseHandler(nullptr); |
| 477 | return; |
| 478 | } |
| 479 | |
| 480 | auto entry = Entry::decodeStorageRecord(*record); |
| 481 | if (!entry) |
| 482 | return; |
| 483 | |
| 484 | TraversalEntry traversalEntry { *entry, recordInfo }; |
| 485 | traverseHandler(&traversalEntry); |
| 486 | }); |
| 487 | } |
| 488 | |
| 489 | String Cache::dumpFilePath() const |
| 490 | { |
| 491 | return pathByAppendingComponent(m_storage->versionPath(), "dump.json" ); |
| 492 | } |
| 493 | |
| 494 | void Cache::dumpContentsToFile() |
| 495 | { |
| 496 | auto fd = openFile(dumpFilePath(), FileOpenMode::Write); |
| 497 | if (!isHandleValid(fd)) |
| 498 | return; |
| 499 | auto prologue = String("{\n\"entries\": [\n" ).utf8(); |
| 500 | writeToFile(fd, prologue.data(), prologue.length()); |
| 501 | |
| 502 | struct Totals { |
| 503 | unsigned count { 0 }; |
| 504 | double worth { 0 }; |
| 505 | size_t bodySize { 0 }; |
| 506 | }; |
| 507 | Totals totals; |
| 508 | auto flags = { Storage::TraverseFlag::ComputeWorth, Storage::TraverseFlag::ShareCount }; |
| 509 | size_t capacity = m_storage->capacity(); |
| 510 | m_storage->traverse(resourceType(), flags, [fd, totals, capacity](const Storage::Record* record, const Storage::RecordInfo& info) mutable { |
| 511 | if (!record) { |
| 512 | StringBuilder epilogue; |
| 513 | epilogue.appendLiteral("{}\n],\n" ); |
| 514 | epilogue.appendLiteral("\"totals\": {\n" ); |
| 515 | epilogue.appendLiteral("\"capacity\": " ); |
| 516 | epilogue.appendNumber(capacity); |
| 517 | epilogue.appendLiteral(",\n" ); |
| 518 | epilogue.appendLiteral("\"count\": " ); |
| 519 | epilogue.appendNumber(totals.count); |
| 520 | epilogue.appendLiteral(",\n" ); |
| 521 | epilogue.appendLiteral("\"bodySize\": " ); |
| 522 | epilogue.appendNumber(totals.bodySize); |
| 523 | epilogue.appendLiteral(",\n" ); |
| 524 | epilogue.appendLiteral("\"averageWorth\": " ); |
| 525 | epilogue.appendFixedPrecisionNumber(totals.count ? totals.worth / totals.count : 0); |
| 526 | epilogue.appendLiteral("\n" ); |
| 527 | epilogue.appendLiteral("}\n}\n" ); |
| 528 | auto writeData = epilogue.toString().utf8(); |
| 529 | writeToFile(fd, writeData.data(), writeData.length()); |
| 530 | closeFile(fd); |
| 531 | return; |
| 532 | } |
| 533 | auto entry = Entry::decodeStorageRecord(*record); |
| 534 | if (!entry) |
| 535 | return; |
| 536 | ++totals.count; |
| 537 | totals.worth += info.worth; |
| 538 | totals.bodySize += info.bodySize; |
| 539 | |
| 540 | StringBuilder json; |
| 541 | entry->asJSON(json, info); |
| 542 | json.appendLiteral(",\n" ); |
| 543 | auto writeData = json.toString().utf8(); |
| 544 | writeToFile(fd, writeData.data(), writeData.length()); |
| 545 | }); |
| 546 | } |
| 547 | |
| 548 | void Cache::deleteDumpFile() |
| 549 | { |
| 550 | WorkQueue::create("com.apple.WebKit.Cache.delete" )->dispatch([path = dumpFilePath().isolatedCopy()] { |
| 551 | deleteFile(path); |
| 552 | }); |
| 553 | } |
| 554 | |
| 555 | void Cache::clear(WallTime modifiedSince, Function<void()>&& completionHandler) |
| 556 | { |
| 557 | LOG(NetworkCache, "(NetworkProcess) clearing cache" ); |
| 558 | |
| 559 | String anyType; |
| 560 | m_storage->clear(anyType, modifiedSince, WTFMove(completionHandler)); |
| 561 | |
| 562 | deleteDumpFile(); |
| 563 | } |
| 564 | |
| 565 | void Cache::clear() |
| 566 | { |
| 567 | clear(-WallTime::infinity(), nullptr); |
| 568 | } |
| 569 | |
| 570 | String Cache::recordsPath() const |
| 571 | { |
| 572 | return m_storage->recordsPath(); |
| 573 | } |
| 574 | |
| 575 | void Cache::retrieveData(const DataKey& dataKey, Function<void(const uint8_t*, size_t)> completionHandler) |
| 576 | { |
| 577 | Key key { dataKey, m_storage->salt() }; |
| 578 | m_storage->retrieve(key, 4, [completionHandler = WTFMove(completionHandler)] (auto record, auto) mutable { |
| 579 | if (!record || !record->body.size()) { |
| 580 | completionHandler(nullptr, 0); |
| 581 | return true; |
| 582 | } |
| 583 | completionHandler(record->body.data(), record->body.size()); |
| 584 | return true; |
| 585 | }); |
| 586 | } |
| 587 | |
| 588 | void Cache::storeData(const DataKey& dataKey, const uint8_t* data, size_t size) |
| 589 | { |
| 590 | Key key { dataKey, m_storage->salt() }; |
| 591 | Storage::Record record { key, WallTime::now(), { }, Data { data, size }, { } }; |
| 592 | m_storage->store(record, { }); |
| 593 | } |
| 594 | |
| 595 | } |
| 596 | } |
| 597 | |