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 "CacheValidation.h"
28
29#include "CookieJar.h"
30#include "HTTPHeaderMap.h"
31#include "NetworkStorageSession.h"
32#include "ResourceRequest.h"
33#include "ResourceResponse.h"
34#include "SameSiteInfo.h"
35#include <wtf/Optional.h>
36#include <wtf/Vector.h>
37#include <wtf/text/StringView.h>
38#include <wtf/text/WTFString.h>
39
40namespace WebCore {
41
42// These response headers are not copied from a revalidated response to the
43// cached response headers. For compatibility, this list is based on Chromium's
44// net/http/http_response_headers.cc.
45static const char* const headersToIgnoreAfterRevalidation[] = {
46 "allow",
47 "connection",
48 "etag",
49 "keep-alive",
50 "last-modified",
51 "proxy-authenticate",
52 "proxy-connection",
53 "trailer",
54 "transfer-encoding",
55 "upgrade",
56 "www-authenticate",
57 "x-frame-options",
58 "x-xss-protection",
59};
60
61// Some header prefixes mean "Don't copy this header from a 304 response.".
62// Rather than listing all the relevant headers, we can consolidate them into
63// this list, also grabbed from Chromium's net/http/http_response_headers.cc.
64static const char* const headerPrefixesToIgnoreAfterRevalidation[] = {
65 "content-",
66 "x-content-",
67 "x-webkit-"
68};
69
70static inline bool shouldUpdateHeaderAfterRevalidation(const String& header)
71{
72 for (auto& headerToIgnore : headersToIgnoreAfterRevalidation) {
73 if (equalIgnoringASCIICase(header, headerToIgnore))
74 return false;
75 }
76 for (auto& prefixToIgnore : headerPrefixesToIgnoreAfterRevalidation) {
77 // FIXME: Would be more efficient if we added an overload of
78 // startsWithIgnoringASCIICase that takes a const char*.
79 if (header.startsWithIgnoringASCIICase(prefixToIgnore))
80 return false;
81 }
82 return true;
83}
84
85void updateResponseHeadersAfterRevalidation(ResourceResponse& response, const ResourceResponse& validatingResponse)
86{
87 // Freshening stored response upon validation:
88 // http://tools.ietf.org/html/rfc7234#section-4.3.4
89 for (const auto& header : validatingResponse.httpHeaderFields()) {
90 // Entity headers should not be sent by servers when generating a 304
91 // response; misconfigured servers send them anyway. We shouldn't allow
92 // such headers to update the original request. We'll base this on the
93 // list defined by RFC2616 7.1, with a few additions for extension headers
94 // we care about.
95 if (!shouldUpdateHeaderAfterRevalidation(header.key))
96 continue;
97 response.setHTTPHeaderField(header.key, header.value);
98 }
99}
100
101Seconds computeCurrentAge(const ResourceResponse& response, WallTime responseTime)
102{
103 // Age calculation:
104 // http://tools.ietf.org/html/rfc7234#section-4.2.3
105 // No compensation for latency as that is not terribly important in practice.
106 auto dateValue = response.date();
107 auto apparentAge = dateValue ? std::max(0_us, responseTime - *dateValue) : 0_us;
108 auto ageValue = response.age().valueOr(0_us);
109 auto correctedInitialAge = std::max(apparentAge, ageValue);
110 auto residentTime = WallTime::now() - responseTime;
111 return correctedInitialAge + residentTime;
112}
113
114Seconds computeFreshnessLifetimeForHTTPFamily(const ResourceResponse& response, WallTime responseTime)
115{
116 if (!response.url().protocolIsInHTTPFamily())
117 return 0_us;
118
119 // Freshness Lifetime:
120 // http://tools.ietf.org/html/rfc7234#section-4.2.1
121 auto maxAge = response.cacheControlMaxAge();
122 if (maxAge)
123 return *maxAge;
124
125 auto date = response.date();
126 auto effectiveDate = date.valueOr(responseTime);
127 if (auto expires = response.expires())
128 return *expires - effectiveDate;
129
130 // Implicit lifetime.
131 switch (response.httpStatusCode()) {
132 case 301: // Moved Permanently
133 case 410: // Gone
134 // These are semantically permanent and so get long implicit lifetime.
135 return 24_h * 365;
136 default:
137 // Heuristic Freshness:
138 // http://tools.ietf.org/html/rfc7234#section-4.2.2
139 if (auto lastModified = response.lastModified())
140 return (effectiveDate - *lastModified) * 0.1;
141 return 0_us;
142 }
143}
144
145void updateRedirectChainStatus(RedirectChainCacheStatus& redirectChainCacheStatus, const ResourceResponse& response)
146{
147 if (redirectChainCacheStatus.status == RedirectChainCacheStatus::Status::NotCachedRedirection)
148 return;
149 if (response.cacheControlContainsNoStore() || response.cacheControlContainsNoCache() || response.cacheControlContainsMustRevalidate()) {
150 redirectChainCacheStatus.status = RedirectChainCacheStatus::Status::NotCachedRedirection;
151 return;
152 }
153
154 redirectChainCacheStatus.status = RedirectChainCacheStatus::Status::CachedRedirection;
155 auto responseTimestamp = WallTime::now();
156 // Store the nearest end of cache validity date
157 auto endOfValidity = responseTimestamp + computeFreshnessLifetimeForHTTPFamily(response, responseTimestamp) - computeCurrentAge(response, responseTimestamp);
158 redirectChainCacheStatus.endOfValidity = std::min(redirectChainCacheStatus.endOfValidity, endOfValidity);
159}
160
161bool redirectChainAllowsReuse(RedirectChainCacheStatus redirectChainCacheStatus, ReuseExpiredRedirectionOrNot reuseExpiredRedirection)
162{
163 switch (redirectChainCacheStatus.status) {
164 case RedirectChainCacheStatus::Status::NoRedirection:
165 return true;
166 case RedirectChainCacheStatus::Status::NotCachedRedirection:
167 return false;
168 case RedirectChainCacheStatus::Status::CachedRedirection:
169 return reuseExpiredRedirection || WallTime::now() <= redirectChainCacheStatus.endOfValidity;
170 }
171 ASSERT_NOT_REACHED();
172 return false;
173}
174
175inline bool isCacheHeaderSeparator(UChar c)
176{
177 // http://tools.ietf.org/html/rfc7230#section-3.2.6
178 switch (c) {
179 case '(':
180 case ')':
181 case '<':
182 case '>':
183 case '@':
184 case ',':
185 case ';':
186 case ':':
187 case '\\':
188 case '"':
189 case '/':
190 case '[':
191 case ']':
192 case '?':
193 case '=':
194 case '{':
195 case '}':
196 case ' ':
197 case '\t':
198 return true;
199 default:
200 return false;
201 }
202}
203
204inline bool isControlCharacterOrSpace(UChar character)
205{
206 return character <= ' ' || character == 127;
207}
208
209inline StringView trimToNextSeparator(StringView string)
210{
211 return string.substring(0, string.find(isCacheHeaderSeparator));
212}
213
214static Vector<std::pair<String, String>> parseCacheHeader(const String& header)
215{
216 Vector<std::pair<String, String>> result;
217
218 String safeHeaderString = header.removeCharacters(isControlCharacterOrSpace);
219 StringView safeHeader = safeHeaderString;
220 unsigned max = safeHeader.length();
221 unsigned pos = 0;
222 while (pos < max) {
223 size_t nextCommaPosition = safeHeader.find(',', pos);
224 size_t nextEqualSignPosition = safeHeader.find('=', pos);
225 if (nextEqualSignPosition == notFound && nextCommaPosition == notFound) {
226 // Add last directive to map with empty string as value
227 result.append({ trimToNextSeparator(safeHeader.substring(pos, max - pos)).toString(), emptyString() });
228 return result;
229 }
230 if (nextCommaPosition != notFound && (nextCommaPosition < nextEqualSignPosition || nextEqualSignPosition == notFound)) {
231 // Add directive to map with empty string as value
232 result.append({ trimToNextSeparator(safeHeader.substring(pos, nextCommaPosition - pos)).toString(), emptyString() });
233 pos += nextCommaPosition - pos + 1;
234 continue;
235 }
236 // Get directive name, parse right hand side of equal sign, then add to map
237 String directive = trimToNextSeparator(safeHeader.substring(pos, nextEqualSignPosition - pos)).toString();
238 pos += nextEqualSignPosition - pos + 1;
239
240 StringView value = safeHeader.substring(pos, max - pos);
241 if (value[0] == '"') {
242 // The value is a quoted string
243 size_t nextDoubleQuotePosition = value.find('"', 1);
244 if (nextDoubleQuotePosition == notFound) {
245 // Parse error; just use the rest as the value
246 result.append({ directive, trimToNextSeparator(value.substring(1)).toString() });
247 return result;
248 }
249 // Store the value as a quoted string without quotes
250 result.append({ directive, value.substring(1, nextDoubleQuotePosition - 1).toString() });
251 pos += (safeHeader.find('"', pos) - pos) + nextDoubleQuotePosition + 1;
252 // Move past next comma, if there is one
253 size_t nextCommaPosition2 = safeHeader.find(',', pos);
254 if (nextCommaPosition2 == notFound)
255 return result; // Parse error if there is anything left with no comma
256 pos += nextCommaPosition2 - pos + 1;
257 continue;
258 }
259 // The value is a token until the next comma
260 size_t nextCommaPosition2 = value.find(',');
261 if (nextCommaPosition2 == notFound) {
262 // The rest is the value; no change to value needed
263 result.append({ directive, trimToNextSeparator(value).toString() });
264 return result;
265 }
266 // The value is delimited by the next comma
267 result.append({ directive, trimToNextSeparator(value.substring(0, nextCommaPosition2)).toString() });
268 pos += (safeHeader.find(',', pos) - pos) + 1;
269 }
270 return result;
271}
272
273CacheControlDirectives parseCacheControlDirectives(const HTTPHeaderMap& headers)
274{
275 CacheControlDirectives result;
276
277 String cacheControlValue = headers.get(HTTPHeaderName::CacheControl);
278 if (!cacheControlValue.isEmpty()) {
279 auto directives = parseCacheHeader(cacheControlValue);
280
281 size_t directivesSize = directives.size();
282 for (size_t i = 0; i < directivesSize; ++i) {
283 // A no-cache directive with a value is only meaningful for proxy caches.
284 // It should be ignored by a browser level cache.
285 // http://tools.ietf.org/html/rfc7234#section-5.2.2.2
286 if (equalLettersIgnoringASCIICase(directives[i].first, "no-cache") && directives[i].second.isEmpty())
287 result.noCache = true;
288 else if (equalLettersIgnoringASCIICase(directives[i].first, "no-store"))
289 result.noStore = true;
290 else if (equalLettersIgnoringASCIICase(directives[i].first, "must-revalidate"))
291 result.mustRevalidate = true;
292 else if (equalLettersIgnoringASCIICase(directives[i].first, "max-age")) {
293 if (result.maxAge) {
294 // First max-age directive wins if there are multiple ones.
295 continue;
296 }
297 bool ok;
298 double maxAge = directives[i].second.toDouble(&ok);
299 if (ok)
300 result.maxAge = Seconds { maxAge };
301 } else if (equalLettersIgnoringASCIICase(directives[i].first, "max-stale")) {
302 // https://tools.ietf.org/html/rfc7234#section-5.2.1.2
303 if (result.maxStale) {
304 // First max-stale directive wins if there are multiple ones.
305 continue;
306 }
307 if (directives[i].second.isEmpty()) {
308 // if no value is assigned to max-stale, then the client is willing to accept a stale response of any age.
309 result.maxStale = Seconds::infinity();
310 continue;
311 }
312 bool ok;
313 double maxStale = directives[i].second.toDouble(&ok);
314 if (ok)
315 result.maxStale = Seconds { maxStale };
316 } else if (equalLettersIgnoringASCIICase(directives[i].first, "immutable"))
317 result.immutable = true;
318 }
319 }
320
321 if (!result.noCache) {
322 // Handle Pragma: no-cache
323 // This is deprecated and equivalent to Cache-control: no-cache
324 // Don't bother tokenizing the value; handling that exactly right is not important.
325 result.noCache = headers.get(HTTPHeaderName::Pragma).containsIgnoringASCIICase("no-cache");
326 }
327
328 return result;
329}
330
331static String cookieRequestHeaderFieldValue(const NetworkStorageSession& session, const ResourceRequest& request)
332{
333 return session.cookieRequestHeaderFieldValue(request.firstPartyForCookies(), SameSiteInfo::create(request), request.url(), WTF::nullopt, WTF::nullopt, request.url().protocolIs("https") ? IncludeSecureCookies::Yes : IncludeSecureCookies::No).first;
334}
335
336static String cookieRequestHeaderFieldValue(const CookieJar* cookieJar, const PAL::SessionID& sessionID, const ResourceRequest& request)
337{
338 if (!cookieJar)
339 return { };
340
341 return cookieJar->cookieRequestHeaderFieldValue(sessionID, request.firstPartyForCookies(), SameSiteInfo::create(request), request.url(), WTF::nullopt, WTF::nullopt, request.url().protocolIs("https") ? IncludeSecureCookies::Yes : IncludeSecureCookies::No).first;
342}
343
344static String headerValueForVary(const ResourceRequest& request, const String& headerName, Function<String()>&& cookieRequestHeaderFieldValueFunction)
345{
346 // Explicit handling for cookies is needed because they are added magically by the networking layer.
347 // FIXME: The value might have changed between making the request and retrieving the cookie here.
348 // We could fetch the cookie when making the request but that seems overkill as the case is very rare and it
349 // is a blocking operation. This should be sufficient to cover reasonable cases.
350 if (headerName == httpHeaderNameString(HTTPHeaderName::Cookie))
351 return cookieRequestHeaderFieldValueFunction();
352 return request.httpHeaderField(headerName);
353}
354
355static Vector<std::pair<String, String>> collectVaryingRequestHeadersInternal(const ResourceResponse& response, Function<String(const String& headerName)>&& headerValueForVaryFunction)
356{
357 String varyValue = response.httpHeaderField(HTTPHeaderName::Vary);
358 if (varyValue.isEmpty())
359 return { };
360 Vector<String> varyingHeaderNames = varyValue.split(',');
361 Vector<std::pair<String, String>> varyingRequestHeaders;
362 varyingRequestHeaders.reserveCapacity(varyingHeaderNames.size());
363 for (auto& varyHeaderName : varyingHeaderNames) {
364 String headerName = varyHeaderName.stripWhiteSpace();
365 String headerValue = headerValueForVaryFunction(headerName);
366 varyingRequestHeaders.append(std::make_pair(headerName, headerValue));
367 }
368 return varyingRequestHeaders;
369}
370
371Vector<std::pair<String, String>> collectVaryingRequestHeaders(NetworkStorageSession& storageSession, const ResourceRequest& request, const ResourceResponse& response)
372{
373 return collectVaryingRequestHeadersInternal(response, [&] (const String& headerName) {
374 return headerValueForVary(request, headerName, [&] {
375 return cookieRequestHeaderFieldValue(storageSession, request);
376 });
377 });
378}
379
380Vector<std::pair<String, String>> collectVaryingRequestHeaders(const CookieJar* cookieJar, const ResourceRequest& request, const ResourceResponse& response, const PAL::SessionID& sessionID)
381{
382 return collectVaryingRequestHeadersInternal(response, [&] (const String& headerName) {
383 return headerValueForVary(request, headerName, [&] {
384 return cookieRequestHeaderFieldValue(cookieJar, sessionID, request);
385 });
386 });
387}
388
389static bool verifyVaryingRequestHeadersInternal(const Vector<std::pair<String, String>>& varyingRequestHeaders, Function<String(const String&)>&& headerValueForVary)
390{
391 for (auto& varyingRequestHeader : varyingRequestHeaders) {
392 // FIXME: Vary: * in response would ideally trigger a cache delete instead of a store.
393 if (varyingRequestHeader.first == "*")
394 return false;
395 if (headerValueForVary(varyingRequestHeader.first) != varyingRequestHeader.second)
396 return false;
397 }
398 return true;
399}
400
401bool verifyVaryingRequestHeaders(NetworkStorageSession& storageSession, const Vector<std::pair<String, String>>& varyingRequestHeaders, const ResourceRequest& request)
402{
403 return verifyVaryingRequestHeadersInternal(varyingRequestHeaders, [&] (const String& headerName) {
404 return headerValueForVary(request, headerName, [&] {
405 return cookieRequestHeaderFieldValue(storageSession, request);
406 });
407 });
408}
409
410bool verifyVaryingRequestHeaders(const CookieJar* cookieJar, const Vector<std::pair<String, String>>& varyingRequestHeaders, const ResourceRequest& request, const PAL::SessionID& sessionID)
411{
412 return verifyVaryingRequestHeadersInternal(varyingRequestHeaders, [&] (const String& headerName) {
413 return headerValueForVary(request, headerName, [&] {
414 return cookieRequestHeaderFieldValue(cookieJar, sessionID, request);
415 });
416 });
417}
418
419// http://tools.ietf.org/html/rfc7231#page-48
420bool isStatusCodeCacheableByDefault(int statusCode)
421{
422 switch (statusCode) {
423 case 200: // OK
424 case 203: // Non-Authoritative Information
425 case 204: // No Content
426 case 206: // Partial Content
427 case 300: // Multiple Choices
428 case 301: // Moved Permanently
429 case 404: // Not Found
430 case 405: // Method Not Allowed
431 case 410: // Gone
432 case 414: // URI Too Long
433 case 501: // Not Implemented
434 return true;
435 default:
436 return false;
437 }
438}
439
440bool isStatusCodePotentiallyCacheable(int statusCode)
441{
442 switch (statusCode) {
443 case 201: // Created
444 case 202: // Accepted
445 case 205: // Reset Content
446 case 302: // Found
447 case 303: // See Other
448 case 307: // Temporary redirect
449 case 403: // Forbidden
450 case 406: // Not Acceptable
451 case 415: // Unsupported Media Type
452 return true;
453 default:
454 return false;
455 }
456}
457
458}
459