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