Remove experimental affiliated domain code now that StorageAccess API is available
[WebKit-https.git] / Source / WebCore / loader / ResourceLoadObserver.cpp
1 /*
2  * Copyright (C) 2016-2018 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 "ResourceLoadObserver.h"
28
29 #include "DeprecatedGlobalSettings.h"
30 #include "Document.h"
31 #include "Frame.h"
32 #include "FrameLoader.h"
33 #include "HTMLFrameOwnerElement.h"
34 #include "Logging.h"
35 #include "Page.h"
36 #include "ResourceLoadStatistics.h"
37 #include "ResourceRequest.h"
38 #include "ResourceResponse.h"
39 #include "ScriptExecutionContext.h"
40 #include "SecurityOrigin.h"
41 #include "Settings.h"
42 #include "URL.h"
43
44 namespace WebCore {
45
46 template<typename T> static inline String primaryDomain(const T& value)
47 {
48     return ResourceLoadStatistics::primaryDomain(value);
49 }
50
51 static const Seconds minimumNotificationInterval { 5_s };
52
53 ResourceLoadObserver& ResourceLoadObserver::shared()
54 {
55     static NeverDestroyed<ResourceLoadObserver> resourceLoadObserver;
56     return resourceLoadObserver;
57 }
58
59 void ResourceLoadObserver::setNotificationCallback(WTF::Function<void (Vector<ResourceLoadStatistics>&&)>&& notificationCallback)
60 {
61     ASSERT(!m_notificationCallback);
62     m_notificationCallback = WTFMove(notificationCallback);
63 }
64
65 void ResourceLoadObserver::setRequestStorageAccessUnderOpenerCallback(WTF::Function<void(const String& domainInNeedOfStorageAccess, uint64_t openerPageID, const String& openerDomain, bool isTriggeredByUserGesture)>&& callback)
66 {
67     ASSERT(!m_requestStorageAccessUnderOpenerCallback);
68     m_requestStorageAccessUnderOpenerCallback = WTFMove(callback);
69 }
70
71 ResourceLoadObserver::ResourceLoadObserver()
72     : m_notificationTimer(*this, &ResourceLoadObserver::notifyObserver)
73 {
74 }
75
76 static inline bool is3xxRedirect(const ResourceResponse& response)
77 {
78     return response.httpStatusCode() >= 300 && response.httpStatusCode() <= 399;
79 }
80
81 bool ResourceLoadObserver::shouldLog(bool usesEphemeralSession) const
82 {
83     return DeprecatedGlobalSettings::resourceLoadStatisticsEnabled() && !usesEphemeralSession && m_notificationCallback;
84 }
85
86 void ResourceLoadObserver::logSubresourceLoading(const Frame* frame, const ResourceRequest& newRequest, const ResourceResponse& redirectResponse)
87 {
88     ASSERT(frame->page());
89
90     auto* page = frame->page();
91     if (!shouldLog(page->usesEphemeralSession()))
92         return;
93
94     bool isRedirect = is3xxRedirect(redirectResponse);
95     const URL& sourceURL = redirectResponse.url();
96     const URL& targetURL = newRequest.url();
97     const URL& mainFrameURL = frame ? frame->mainFrame().document()->url() : URL();
98     
99     auto targetHost = targetURL.host();
100     auto mainFrameHost = mainFrameURL.host();
101
102     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || (isRedirect && targetHost == sourceURL.host()))
103         return;
104
105     auto targetPrimaryDomain = primaryDomain(targetURL);
106     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
107     auto sourcePrimaryDomain = primaryDomain(sourceURL);
108
109     if (targetPrimaryDomain == mainFramePrimaryDomain || (isRedirect && targetPrimaryDomain == sourcePrimaryDomain))
110         return;
111
112     bool shouldCallNotificationCallback = false;
113     {
114         auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
115         targetStatistics.lastSeen = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
116         if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
117             shouldCallNotificationCallback = true;
118     }
119
120     if (isRedirect) {
121         auto& redirectingOriginStatistics = ensureResourceStatisticsForPrimaryDomain(sourcePrimaryDomain);
122         bool isNewRedirectToEntry = redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetPrimaryDomain).isNewEntry;
123         auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
124         bool isNewRedirectFromEntry = targetStatistics.subresourceUniqueRedirectsFrom.add(sourcePrimaryDomain).isNewEntry;
125
126         if (isNewRedirectToEntry || isNewRedirectFromEntry)
127             shouldCallNotificationCallback = true;
128     }
129
130     if (shouldCallNotificationCallback)
131         scheduleNotificationIfNeeded();
132 }
133
134 void ResourceLoadObserver::logWebSocketLoading(const URL& targetURL, const URL& mainFrameURL, bool usesEphemeralSession)
135 {
136     if (!shouldLog(usesEphemeralSession))
137         return;
138
139     auto targetHost = targetURL.host();
140     auto mainFrameHost = mainFrameURL.host();
141     
142     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost)
143         return;
144     
145     auto targetPrimaryDomain = primaryDomain(targetURL);
146     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
147
148     if (targetPrimaryDomain == mainFramePrimaryDomain)
149         return;
150
151     auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
152     targetStatistics.lastSeen = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
153     if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
154         scheduleNotificationIfNeeded();
155 }
156
157 void ResourceLoadObserver::logUserInteractionWithReducedTimeResolution(const Document& document)
158 {
159     ASSERT(document.page());
160
161     if (!shouldLog(document.page()->usesEphemeralSession()))
162         return;
163
164     auto& url = document.url();
165     if (url.isBlankURL() || url.isEmpty())
166         return;
167
168     auto domain = primaryDomain(url);
169     auto newTime = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
170     auto lastReportedUserInteraction = m_lastReportedUserInteractionMap.get(domain);
171     if (newTime == lastReportedUserInteraction)
172         return;
173
174     m_lastReportedUserInteractionMap.set(domain, newTime);
175
176     auto& statistics = ensureResourceStatisticsForPrimaryDomain(domain);
177     statistics.hadUserInteraction = true;
178     statistics.lastSeen = newTime;
179     statistics.mostRecentUserInteractionTime = newTime;
180
181 #if HAVE(CFNETWORK_STORAGE_PARTITIONING)
182     if (auto* opener = document.frame()->loader().opener()) {
183         if (auto* openerDocument = opener->document()) {
184             if (auto* openerFrame = openerDocument->frame()) {
185                 if (auto openerPageID = openerFrame->loader().client().pageID()) {
186                     requestStorageAccessUnderOpener(domain, openerPageID.value(), *openerDocument, true);
187                 }
188             }
189         }
190     }
191 #endif
192
193     m_notificationTimer.stop();
194     notifyObserver();
195
196 #if HAVE(CFNETWORK_STORAGE_PARTITIONING) && !RELEASE_LOG_DISABLED
197     if (shouldLogUserInteraction()) {
198         auto counter = ++m_loggingCounter;
199 #define LOCAL_LOG(str, ...) \
200         RELEASE_LOG(ResourceLoadStatistics, "ResourceLoadObserver::logUserInteraction: counter = %" PRIu64 ": " str, counter, ##__VA_ARGS__)
201
202         auto escapeForJSON = [](String s) {
203             s.replace('\\', "\\\\").replace('"', "\\\"");
204             return s;
205         };
206         auto escapedURL = escapeForJSON(url.string());
207         auto escapedDomain = escapeForJSON(domain);
208
209         LOCAL_LOG(R"({ "url": "%{public}s",)", escapedURL.utf8().data());
210         LOCAL_LOG(R"(  "domain" : "%{public}s",)", escapedDomain.utf8().data());
211         LOCAL_LOG(R"(  "until" : %f })", newTime.secondsSinceEpoch().seconds());
212
213 #undef LOCAL_LOG
214     }
215 #endif
216 }
217
218 void ResourceLoadObserver::logWindowCreation(const URL& popupUrl, uint64_t openerPageID, Document& openerDocument)
219 {
220 #if HAVE(CFNETWORK_STORAGE_PARTITIONING)
221     requestStorageAccessUnderOpener(primaryDomain(popupUrl), openerPageID, openerDocument, false);
222 #else
223     UNUSED_PARAM(popupUrl);
224     UNUSED_PARAM(openerPageID);
225     UNUSED_PARAM(openerDocument);
226 #endif
227 }
228
229 #if HAVE(CFNETWORK_STORAGE_PARTITIONING)
230 void ResourceLoadObserver::requestStorageAccessUnderOpener(const String& domainInNeedOfStorageAccess, uint64_t openerPageID, Document& openerDocument, bool isTriggeredByUserGesture)
231 {
232     auto openerUrl = openerDocument.url();
233     auto openerPrimaryDomain = primaryDomain(openerUrl);
234     if (domainInNeedOfStorageAccess != openerPrimaryDomain
235         && !openerDocument.hasRequestedPageSpecificStorageAccessWithUserInteraction(domainInNeedOfStorageAccess)
236         && !equalIgnoringASCIICase(openerUrl.string(), blankURL())) {
237         m_requestStorageAccessUnderOpenerCallback(domainInNeedOfStorageAccess, openerPageID, openerPrimaryDomain, isTriggeredByUserGesture);
238         // Remember user interaction-based requests since they don't need to be repeated.
239         if (isTriggeredByUserGesture)
240             openerDocument.setHasRequestedPageSpecificStorageAccessWithUserInteraction(domainInNeedOfStorageAccess);
241     }
242 }
243 #endif
244
245 ResourceLoadStatistics& ResourceLoadObserver::ensureResourceStatisticsForPrimaryDomain(const String& primaryDomain)
246 {
247     auto addResult = m_resourceStatisticsMap.ensure(primaryDomain, [&primaryDomain] {
248         return ResourceLoadStatistics(primaryDomain);
249     });
250     return addResult.iterator->value;
251 }
252
253 void ResourceLoadObserver::scheduleNotificationIfNeeded()
254 {
255     ASSERT(m_notificationCallback);
256     if (m_resourceStatisticsMap.isEmpty()) {
257         m_notificationTimer.stop();
258         return;
259     }
260
261     if (!m_notificationTimer.isActive())
262         m_notificationTimer.startOneShot(minimumNotificationInterval);
263 }
264
265 void ResourceLoadObserver::notifyObserver()
266 {
267     ASSERT(m_notificationCallback);
268     m_notificationTimer.stop();
269     m_notificationCallback(takeStatistics());
270 }
271
272 String ResourceLoadObserver::statisticsForOrigin(const String& origin)
273 {
274     auto iter = m_resourceStatisticsMap.find(origin);
275     if (iter == m_resourceStatisticsMap.end())
276         return emptyString();
277
278     return "Statistics for " + origin + ":\n" + iter->value.toString();
279 }
280
281 Vector<ResourceLoadStatistics> ResourceLoadObserver::takeStatistics()
282 {
283     Vector<ResourceLoadStatistics> statistics;
284     statistics.reserveInitialCapacity(m_resourceStatisticsMap.size());
285     for (auto& statistic : m_resourceStatisticsMap.values())
286         statistics.uncheckedAppend(WTFMove(statistic));
287
288     m_resourceStatisticsMap.clear();
289
290     return statistics;
291 }
292
293 void ResourceLoadObserver::clearState()
294 {
295     m_notificationTimer.stop();
296     m_resourceStatisticsMap.clear();
297     m_lastReportedUserInteractionMap.clear();
298 }
299
300 URL ResourceLoadObserver::nonNullOwnerURL(const Document& document) const
301 {
302     auto url = document.url();
303     auto* frame = document.frame();
304     auto host = document.url().host();
305
306     while ((host.isNull() || host.isEmpty()) && frame && !frame->isMainFrame()) {
307         auto* ownerElement = frame->ownerElement();
308
309         ASSERT(ownerElement != nullptr);
310         
311         auto& doc = ownerElement->document();
312         frame = doc.frame();
313         url = doc.url();
314         host = url.host();
315     }
316
317     return url;
318 }
319
320 } // namespace WebCore