Unreviewed, rolling out r220268.
[WebKit-https.git] / Source / WebCore / loader / ResourceLoadObserver.cpp
1 /*
2  * Copyright (C) 2016-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 "ResourceLoadObserver.h"
28
29 #include "Document.h"
30 #include "Frame.h"
31 #include "Logging.h"
32 #include "MainFrame.h"
33 #include "Page.h"
34 #include "ResourceLoadStatistics.h"
35 #include "ResourceRequest.h"
36 #include "ResourceResponse.h"
37 #include "SecurityOrigin.h"
38 #include "Settings.h"
39 #include "URL.h"
40
41 namespace WebCore {
42
43 template<typename T> static inline String primaryDomain(const T& value)
44 {
45     return ResourceLoadStatistics::primaryDomain(value);
46 }
47
48 static Seconds timestampResolution { 1_h };
49 static const Seconds minimumNotificationInterval { 5_s };
50
51 ResourceLoadObserver& ResourceLoadObserver::shared()
52 {
53     static NeverDestroyed<ResourceLoadObserver> resourceLoadObserver;
54     return resourceLoadObserver;
55 }
56
57 static bool shouldEnableSiteSpecificQuirks(Page* page)
58 {
59 #if PLATFORM(IOS)
60     UNUSED_PARAM(page);
61
62     // There is currently no way to toggle the needsSiteSpecificQuirks setting on iOS so we always enable
63     // the site-specific quirks on iOS.
64     return true;
65 #else
66     return page && page->settings().needsSiteSpecificQuirks();
67 #endif
68 }
69
70 // FIXME: Temporary fix for <rdar://problem/32343256> until content can be updated.
71 static bool areDomainsAssociated(Page* page, const String& firstDomain, const String& secondDomain)
72 {
73     static NeverDestroyed<HashMap<String, unsigned>> metaDomainIdentifiers = [] {
74         HashMap<String, unsigned> map;
75
76         // Domains owned by Dow Jones & Company, Inc.
77         const unsigned dowJonesIdentifier = 1;
78         map.add(ASCIILiteral("dowjones.com"), dowJonesIdentifier);
79         map.add(ASCIILiteral("wsj.com"), dowJonesIdentifier);
80         map.add(ASCIILiteral("barrons.com"), dowJonesIdentifier);
81         map.add(ASCIILiteral("marketwatch.com"), dowJonesIdentifier);
82         map.add(ASCIILiteral("wsjplus.com"), dowJonesIdentifier);
83
84         return map;
85     }();
86
87     if (firstDomain == secondDomain)
88         return true;
89
90     ASSERT(!equalIgnoringASCIICase(firstDomain, secondDomain));
91
92     if (!shouldEnableSiteSpecificQuirks(page))
93         return false;
94
95     unsigned firstMetaDomainIdentifier = metaDomainIdentifiers.get().get(firstDomain);
96     if (!firstMetaDomainIdentifier)
97         return false;
98
99     return firstMetaDomainIdentifier == metaDomainIdentifiers.get().get(secondDomain);
100 }
101
102 void ResourceLoadObserver::setShouldThrottleObserverNotifications(bool shouldThrottle)
103 {
104     m_shouldThrottleNotifications = shouldThrottle;
105
106     if (!m_notificationTimer.isActive())
107         return;
108
109     // If we change the notification state, we need to restart any notifications
110     // so they will be on the right schedule.
111     m_notificationTimer.stop();
112     scheduleNotificationIfNeeded();
113 }
114
115 void ResourceLoadObserver::setNotificationCallback(WTF::Function<void (Vector<ResourceLoadStatistics>&&)>&& notificationCallback)
116 {
117     ASSERT(!m_notificationCallback);
118     m_notificationCallback = WTFMove(notificationCallback);
119 }
120
121 ResourceLoadObserver::ResourceLoadObserver()
122     : m_notificationTimer(*this, &ResourceLoadObserver::notificationTimerFired)
123 {
124 }
125
126 static inline bool is3xxRedirect(const ResourceResponse& response)
127 {
128     return response.httpStatusCode() >= 300 && response.httpStatusCode() <= 399;
129 }
130
131 bool ResourceLoadObserver::shouldLog(Page* page) const
132 {
133     // FIXME: Err on the safe side until we have sorted out what to do in worker contexts
134     if (!page)
135         return false;
136
137     return Settings::resourceLoadStatisticsEnabled() && !page->usesEphemeralSession() && m_notificationCallback;
138 }
139
140 static WallTime reduceToHourlyTimeResolution(WallTime time)
141 {
142     return WallTime::fromRawSeconds(std::floor(time.secondsSinceEpoch() / timestampResolution) * timestampResolution.seconds());
143 }
144
145 void ResourceLoadObserver::logFrameNavigation(const Frame& frame, const Frame& topFrame, const ResourceRequest& newRequest)
146 {
147     ASSERT(frame.document());
148     ASSERT(topFrame.document());
149     ASSERT(topFrame.page());
150
151     if (frame.isMainFrame())
152         return;
153     
154     auto* page = topFrame.page();
155     if (!shouldLog(page))
156         return;
157
158     auto& sourceURL = frame.document()->url();
159     auto& targetURL = newRequest.url();
160     auto& mainFrameURL = topFrame.document()->url();
161     
162     if (!targetURL.isValid() || !mainFrameURL.isValid())
163         return;
164
165     auto targetHost = targetURL.host();
166     auto mainFrameHost = mainFrameURL.host();
167
168     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || targetHost == sourceURL.host())
169         return;
170
171     auto targetPrimaryDomain = primaryDomain(targetURL);
172     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
173     auto sourcePrimaryDomain = primaryDomain(sourceURL);
174     
175     if (areDomainsAssociated(page, targetPrimaryDomain, mainFramePrimaryDomain) || areDomainsAssociated(page, targetPrimaryDomain, sourcePrimaryDomain))
176         return;
177
178     auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
179     targetStatistics.lastSeen = reduceToHourlyTimeResolution(WallTime::now());
180     auto subframeUnderTopFrameOriginsResult = targetStatistics.subframeUnderTopFrameOrigins.add(mainFramePrimaryDomain);
181     if (subframeUnderTopFrameOriginsResult.isNewEntry)
182         scheduleNotificationIfNeeded();
183 }
184
185 // FIXME: This quirk was added to address <rdar://problem/33325881> and should be removed once content is fixed.
186 static bool resourceNeedsSSOQuirk(Page* page, const URL& url)
187 {
188     if (!shouldEnableSiteSpecificQuirks(page))
189         return false;
190
191     return equalIgnoringASCIICase(url.host(), "sp.auth.adobe.com");
192 }
193
194 void ResourceLoadObserver::logSubresourceLoading(const Frame* frame, const ResourceRequest& newRequest, const ResourceResponse& redirectResponse)
195 {
196     ASSERT(frame->page());
197
198     auto* page = frame->page();
199     if (!shouldLog(page))
200         return;
201
202     bool isRedirect = is3xxRedirect(redirectResponse);
203     const URL& sourceURL = redirectResponse.url();
204     const URL& targetURL = newRequest.url();
205     const URL& mainFrameURL = frame ? frame->mainFrame().document()->url() : URL();
206     
207     auto targetHost = targetURL.host();
208     auto mainFrameHost = mainFrameURL.host();
209
210     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || (isRedirect && targetHost == sourceURL.host()))
211         return;
212
213     auto targetPrimaryDomain = primaryDomain(targetURL);
214     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
215     auto sourcePrimaryDomain = primaryDomain(sourceURL);
216     
217     if (areDomainsAssociated(page, targetPrimaryDomain, mainFramePrimaryDomain) || (isRedirect && areDomainsAssociated(page, targetPrimaryDomain, sourcePrimaryDomain)))
218         return;
219
220     if (resourceNeedsSSOQuirk(page, targetURL))
221         return;
222
223     bool shouldCallNotificationCallback = false;
224     {
225         auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
226         targetStatistics.lastSeen = reduceToHourlyTimeResolution(WallTime::now());
227         if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
228             shouldCallNotificationCallback = true;
229     }
230
231     if (isRedirect) {
232         auto& redirectingOriginStatistics = ensureResourceStatisticsForPrimaryDomain(sourcePrimaryDomain);
233         if (redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetPrimaryDomain).isNewEntry)
234             shouldCallNotificationCallback = true;
235     }
236
237     if (shouldCallNotificationCallback)
238         scheduleNotificationIfNeeded();
239 }
240
241 void ResourceLoadObserver::logWebSocketLoading(const Frame* frame, const URL& targetURL)
242 {
243     // FIXME: Web sockets can run in detached frames. Decide how to count such connections.
244     // See LayoutTests/http/tests/websocket/construct-in-detached-frame.html
245     if (!frame)
246         return;
247
248     auto* page = frame->page();
249     if (!shouldLog(page))
250         return;
251
252     auto& mainFrameURL = frame->mainFrame().document()->url();
253
254     auto targetHost = targetURL.host();
255     auto mainFrameHost = mainFrameURL.host();
256     
257     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost)
258         return;
259     
260     auto targetPrimaryDomain = primaryDomain(targetURL);
261     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
262     
263     if (areDomainsAssociated(page, targetPrimaryDomain, mainFramePrimaryDomain))
264         return;
265
266     auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
267     targetStatistics.lastSeen = reduceToHourlyTimeResolution(WallTime::now());
268     if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
269         scheduleNotificationIfNeeded();
270 }
271
272 void ResourceLoadObserver::logUserInteractionWithReducedTimeResolution(const Document& document)
273 {
274     ASSERT(document.page());
275
276     if (!shouldLog(document.page()))
277         return;
278
279     auto& url = document.url();
280     if (url.isBlankURL() || url.isEmpty())
281         return;
282
283     auto& statistics = ensureResourceStatisticsForPrimaryDomain(primaryDomain(url));
284     auto newTime = reduceToHourlyTimeResolution(WallTime::now());
285     if (newTime == statistics.mostRecentUserInteractionTime)
286         return;
287
288     statistics.hadUserInteraction = true;
289     statistics.lastSeen = newTime;
290     statistics.mostRecentUserInteractionTime = newTime;
291
292     scheduleNotificationIfNeeded();
293 }
294
295 ResourceLoadStatistics& ResourceLoadObserver::ensureResourceStatisticsForPrimaryDomain(const String& primaryDomain)
296 {
297     auto addResult = m_resourceStatisticsMap.ensure(primaryDomain, [&primaryDomain] {
298         return ResourceLoadStatistics(primaryDomain);
299     });
300     return addResult.iterator->value;
301 }
302
303 void ResourceLoadObserver::scheduleNotificationIfNeeded()
304 {
305     ASSERT(m_notificationCallback);
306     if (m_resourceStatisticsMap.isEmpty()) {
307         m_notificationTimer.stop();
308         return;
309     }
310
311     if (!m_notificationTimer.isActive())
312         m_notificationTimer.startOneShot(m_shouldThrottleNotifications ? minimumNotificationInterval : 0_s);
313 }
314
315 void ResourceLoadObserver::notificationTimerFired()
316 {
317     ASSERT(m_notificationCallback);
318     m_notificationCallback(takeStatistics());
319 }
320
321 String ResourceLoadObserver::statisticsForOrigin(const String& origin)
322 {
323     auto iter = m_resourceStatisticsMap.find(origin);
324     if (iter == m_resourceStatisticsMap.end())
325         return emptyString();
326
327     return "Statistics for " + origin + ":\n" + iter->value.toString();
328 }
329
330 Vector<ResourceLoadStatistics> ResourceLoadObserver::takeStatistics()
331 {
332     Vector<ResourceLoadStatistics> statistics;
333     statistics.reserveInitialCapacity(m_resourceStatisticsMap.size());
334     for (auto& statistic : m_resourceStatisticsMap.values())
335         statistics.uncheckedAppend(WTFMove(statistic));
336
337     m_resourceStatisticsMap.clear();
338
339     return statistics;
340 }
341
342 } // namespace WebCore