1/* 2 * Copyright (C) 2008, 2009 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. ``AS IS'' AND ANY 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 * 25 */ 26 27#include "config.h" 28#include "CrossOriginPreflightResultCache.h" 29 30#include "CrossOriginAccessControl.h" 31#include "HTTPHeaderNames.h" 32#include "ResourceResponse.h" 33#include <wtf/MainThread.h> 34#include <wtf/NeverDestroyed.h> 35#include <wtf/StdLibExtras.h> 36 37namespace WebCore { 38 39// These values are at the discretion of the user agent. 40static const auto defaultPreflightCacheTimeout = std::chrono::seconds(5); 41static const auto maxPreflightCacheTimeout = std::chrono::seconds(600); // Should be short enough to minimize the risk of using a poisoned cache after switching to a secure network. 42 43CrossOriginPreflightResultCache::CrossOriginPreflightResultCache() 44{ 45} 46 47static bool parseAccessControlMaxAge(const String& string, std::chrono::seconds& expiryDelta) 48{ 49 // FIXME: this will not do the correct thing for a number starting with a '+' 50 bool ok = false; 51 expiryDelta = std::chrono::seconds(string.toUIntStrict(&ok)); 52 return ok; 53} 54 55template<class HashType> 56static void addToAccessControlAllowList(const String& string, unsigned start, unsigned end, HashSet<String, HashType>& set) 57{ 58 StringImpl* stringImpl = string.impl(); 59 if (!stringImpl) 60 return; 61 62 // Skip white space from start. 63 while (start <= end && isSpaceOrNewline((*stringImpl)[start])) 64 ++start; 65 66 // only white space 67 if (start > end) 68 return; 69 70 // Skip white space from end. 71 while (end && isSpaceOrNewline((*stringImpl)[end])) 72 --end; 73 74 set.add(string.substring(start, end - start + 1)); 75} 76 77template<class HashType> 78static bool parseAccessControlAllowList(const String& string, HashSet<String, HashType>& set) 79{ 80 unsigned start = 0; 81 size_t end; 82 while ((end = string.find(',', start)) != notFound) { 83 if (start != end) 84 addToAccessControlAllowList(string, start, end - 1, set); 85 start = end + 1; 86 } 87 if (start != string.length()) 88 addToAccessControlAllowList(string, start, string.length() - 1, set); 89 90 return true; 91} 92 93bool CrossOriginPreflightResultCacheItem::parse(const ResourceResponse& response, String& errorDescription) 94{ 95 m_methods.clear(); 96 if (!parseAccessControlAllowList(response.httpHeaderField(HTTPHeaderName::AccessControlAllowMethods), m_methods)) { 97 errorDescription = "Cannot parse Access-Control-Allow-Methods response header field."; 98 return false; 99 } 100 101 m_headers.clear(); 102 if (!parseAccessControlAllowList(response.httpHeaderField(HTTPHeaderName::AccessControlAllowHeaders), m_headers)) { 103 errorDescription = "Cannot parse Access-Control-Allow-Headers response header field."; 104 return false; 105 } 106 107 std::chrono::seconds expiryDelta; 108 if (parseAccessControlMaxAge(response.httpHeaderField(HTTPHeaderName::AccessControlMaxAge), expiryDelta)) { 109 if (expiryDelta > maxPreflightCacheTimeout) 110 expiryDelta = maxPreflightCacheTimeout; 111 } else 112 expiryDelta = defaultPreflightCacheTimeout; 113 114 m_absoluteExpiryTime = std::chrono::steady_clock::now() + expiryDelta; 115 return true; 116} 117 118bool CrossOriginPreflightResultCacheItem::allowsCrossOriginMethod(const String& method, String& errorDescription) const 119{ 120 if (m_methods.contains(method) || isOnAccessControlSimpleRequestMethodWhitelist(method)) 121 return true; 122 123 errorDescription = "Method " + method + " is not allowed by Access-Control-Allow-Methods."; 124 return false; 125} 126 127bool CrossOriginPreflightResultCacheItem::allowsCrossOriginHeaders(const HTTPHeaderMap& requestHeaders, String& errorDescription) const 128{ 129 for (const auto& header : requestHeaders) { 130 if (!m_headers.contains(header.key) && !isOnAccessControlSimpleRequestHeaderWhitelist(header.key, header.value)) { 131 errorDescription = "Request header field " + header.key + " is not allowed by Access-Control-Allow-Headers."; 132 return false; 133 } 134 } 135 return true; 136} 137 138bool CrossOriginPreflightResultCacheItem::allowsRequest(StoredCredentials includeCredentials, const String& method, const HTTPHeaderMap& requestHeaders) const 139{ 140 String ignoredExplanation; 141 if (m_absoluteExpiryTime < std::chrono::steady_clock::now()) 142 return false; 143 if (includeCredentials == AllowStoredCredentials && m_credentials == DoNotAllowStoredCredentials) 144 return false; 145 if (!allowsCrossOriginMethod(method, ignoredExplanation)) 146 return false; 147 if (!allowsCrossOriginHeaders(requestHeaders, ignoredExplanation)) 148 return false; 149 return true; 150} 151 152CrossOriginPreflightResultCache& CrossOriginPreflightResultCache::shared() 153{ 154 ASSERT(isMainThread()); 155 156 static NeverDestroyed<CrossOriginPreflightResultCache> cache; 157 return cache; 158} 159 160void CrossOriginPreflightResultCache::appendEntry(const String& origin, const URL& url, std::unique_ptr<CrossOriginPreflightResultCacheItem> preflightResult) 161{ 162 ASSERT(isMainThread()); 163 m_preflightHashMap.set(std::make_pair(origin, url), WTF::move(preflightResult)); 164} 165 166bool CrossOriginPreflightResultCache::canSkipPreflight(const String& origin, const URL& url, StoredCredentials includeCredentials, const String& method, const HTTPHeaderMap& requestHeaders) 167{ 168 ASSERT(isMainThread()); 169 auto it = m_preflightHashMap.find(std::make_pair(origin, url)); 170 if (it == m_preflightHashMap.end()) 171 return false; 172 173 if (it->value->allowsRequest(includeCredentials, method, requestHeaders)) 174 return true; 175 176 m_preflightHashMap.remove(it); 177 return false; 178} 179 180void CrossOriginPreflightResultCache::empty() 181{ 182 ASSERT(isMainThread()); 183 m_preflightHashMap.clear(); 184} 185 186} // namespace WebCore 187