4d1479fb9a79bf99510c4a91a340afbb5f3e34f2
[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::setNotificationCallback(WTF::Function<void (Vector<ResourceLoadStatistics>&&)>&& notificationCallback)
103 {
104     ASSERT(!m_notificationCallback);
105     m_notificationCallback = WTFMove(notificationCallback);
106 }
107
108 ResourceLoadObserver::ResourceLoadObserver()
109     : m_notificationTimer(*this, &ResourceLoadObserver::notifyObserver)
110 {
111 }
112
113 static inline bool is3xxRedirect(const ResourceResponse& response)
114 {
115     return response.httpStatusCode() >= 300 && response.httpStatusCode() <= 399;
116 }
117
118 bool ResourceLoadObserver::shouldLog(Page* page) const
119 {
120     // FIXME: Err on the safe side until we have sorted out what to do in worker contexts
121     if (!page)
122         return false;
123
124     return Settings::resourceLoadStatisticsEnabled() && !page->usesEphemeralSession() && m_notificationCallback;
125 }
126
127 static WallTime reduceToHourlyTimeResolution(WallTime time)
128 {
129     return WallTime::fromRawSeconds(std::floor(time.secondsSinceEpoch() / timestampResolution) * timestampResolution.seconds());
130 }
131
132 void ResourceLoadObserver::logFrameNavigation(const Frame& frame, const Frame& topFrame, const ResourceRequest& newRequest)
133 {
134     ASSERT(frame.document());
135     ASSERT(topFrame.document());
136     ASSERT(topFrame.page());
137
138     if (frame.isMainFrame())
139         return;
140     
141     auto* page = topFrame.page();
142     if (!shouldLog(page))
143         return;
144
145     auto& sourceURL = frame.document()->url();
146     auto& targetURL = newRequest.url();
147     auto& mainFrameURL = topFrame.document()->url();
148     
149     if (!targetURL.isValid() || !mainFrameURL.isValid())
150         return;
151
152     auto targetHost = targetURL.host();
153     auto mainFrameHost = mainFrameURL.host();
154
155     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || targetHost == sourceURL.host())
156         return;
157
158     auto targetPrimaryDomain = primaryDomain(targetURL);
159     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
160     auto sourcePrimaryDomain = primaryDomain(sourceURL);
161     
162     if (areDomainsAssociated(page, targetPrimaryDomain, mainFramePrimaryDomain) || areDomainsAssociated(page, targetPrimaryDomain, sourcePrimaryDomain))
163         return;
164
165     auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
166     targetStatistics.lastSeen = reduceToHourlyTimeResolution(WallTime::now());
167     auto subframeUnderTopFrameOriginsResult = targetStatistics.subframeUnderTopFrameOrigins.add(mainFramePrimaryDomain);
168     if (subframeUnderTopFrameOriginsResult.isNewEntry)
169         scheduleNotificationIfNeeded();
170 }
171
172 // FIXME: This quirk was added to address <rdar://problem/33325881> and should be removed once content is fixed.
173 static bool resourceNeedsSSOQuirk(Page* page, const URL& url)
174 {
175     if (!shouldEnableSiteSpecificQuirks(page))
176         return false;
177
178     return equalIgnoringASCIICase(url.host(), "sp.auth.adobe.com");
179 }
180
181 void ResourceLoadObserver::logSubresourceLoading(const Frame* frame, const ResourceRequest& newRequest, const ResourceResponse& redirectResponse)
182 {
183     ASSERT(frame->page());
184
185     auto* page = frame->page();
186     if (!shouldLog(page))
187         return;
188
189     bool isRedirect = is3xxRedirect(redirectResponse);
190     const URL& sourceURL = redirectResponse.url();
191     const URL& targetURL = newRequest.url();
192     const URL& mainFrameURL = frame ? frame->mainFrame().document()->url() : URL();
193     
194     auto targetHost = targetURL.host();
195     auto mainFrameHost = mainFrameURL.host();
196
197     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost || (isRedirect && targetHost == sourceURL.host()))
198         return;
199
200     auto targetPrimaryDomain = primaryDomain(targetURL);
201     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
202     auto sourcePrimaryDomain = primaryDomain(sourceURL);
203     
204     if (areDomainsAssociated(page, targetPrimaryDomain, mainFramePrimaryDomain) || (isRedirect && areDomainsAssociated(page, targetPrimaryDomain, sourcePrimaryDomain)))
205         return;
206
207     if (resourceNeedsSSOQuirk(page, targetURL))
208         return;
209
210     bool shouldCallNotificationCallback = false;
211     {
212         auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
213         targetStatistics.lastSeen = reduceToHourlyTimeResolution(WallTime::now());
214         if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
215             shouldCallNotificationCallback = true;
216     }
217
218     if (isRedirect) {
219         auto& redirectingOriginStatistics = ensureResourceStatisticsForPrimaryDomain(sourcePrimaryDomain);
220         if (redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetPrimaryDomain).isNewEntry)
221             shouldCallNotificationCallback = true;
222     }
223
224     if (shouldCallNotificationCallback)
225         scheduleNotificationIfNeeded();
226 }
227
228 void ResourceLoadObserver::logWebSocketLoading(const Frame* frame, const URL& targetURL)
229 {
230     // FIXME: Web sockets can run in detached frames. Decide how to count such connections.
231     // See LayoutTests/http/tests/websocket/construct-in-detached-frame.html
232     if (!frame)
233         return;
234
235     auto* page = frame->page();
236     if (!shouldLog(page))
237         return;
238
239     auto& mainFrameURL = frame->mainFrame().document()->url();
240
241     auto targetHost = targetURL.host();
242     auto mainFrameHost = mainFrameURL.host();
243     
244     if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost)
245         return;
246     
247     auto targetPrimaryDomain = primaryDomain(targetURL);
248     auto mainFramePrimaryDomain = primaryDomain(mainFrameURL);
249     
250     if (areDomainsAssociated(page, targetPrimaryDomain, mainFramePrimaryDomain))
251         return;
252
253     auto& targetStatistics = ensureResourceStatisticsForPrimaryDomain(targetPrimaryDomain);
254     targetStatistics.lastSeen = reduceToHourlyTimeResolution(WallTime::now());
255     if (targetStatistics.subresourceUnderTopFrameOrigins.add(mainFramePrimaryDomain).isNewEntry)
256         scheduleNotificationIfNeeded();
257 }
258
259 void ResourceLoadObserver::logUserInteractionWithReducedTimeResolution(const Document& document)
260 {
261     ASSERT(document.page());
262
263     if (!shouldLog(document.page()))
264         return;
265
266     auto& url = document.url();
267     if (url.isBlankURL() || url.isEmpty())
268         return;
269
270     auto domain = primaryDomain(url);
271     auto newTime = reduceToHourlyTimeResolution(WallTime::now());
272     auto lastReportedUserInteraction = m_lastReportedUserInteractionMap.get(domain);
273     if (newTime == lastReportedUserInteraction)
274         return;
275
276     m_lastReportedUserInteractionMap.set(domain, newTime);
277
278     auto& statistics = ensureResourceStatisticsForPrimaryDomain(domain);
279     statistics.hadUserInteraction = true;
280     statistics.lastSeen = newTime;
281     statistics.mostRecentUserInteractionTime = newTime;
282
283     notifyObserver();
284 }
285
286 ResourceLoadStatistics& ResourceLoadObserver::ensureResourceStatisticsForPrimaryDomain(const String& primaryDomain)
287 {
288     auto addResult = m_resourceStatisticsMap.ensure(primaryDomain, [&primaryDomain] {
289         return ResourceLoadStatistics(primaryDomain);
290     });
291     return addResult.iterator->value;
292 }
293
294 void ResourceLoadObserver::scheduleNotificationIfNeeded()
295 {
296     ASSERT(m_notificationCallback);
297     if (m_resourceStatisticsMap.isEmpty()) {
298         m_notificationTimer.stop();
299         return;
300     }
301
302     if (!m_notificationTimer.isActive())
303         m_notificationTimer.startOneShot(minimumNotificationInterval);
304 }
305
306 void ResourceLoadObserver::notifyObserver()
307 {
308     ASSERT(m_notificationCallback);
309     m_notificationTimer.stop();
310     m_notificationCallback(takeStatistics());
311 }
312
313 String ResourceLoadObserver::statisticsForOrigin(const String& origin)
314 {
315     auto iter = m_resourceStatisticsMap.find(origin);
316     if (iter == m_resourceStatisticsMap.end())
317         return emptyString();
318
319     return "Statistics for " + origin + ":\n" + iter->value.toString();
320 }
321
322 Vector<ResourceLoadStatistics> ResourceLoadObserver::takeStatistics()
323 {
324     Vector<ResourceLoadStatistics> statistics;
325     statistics.reserveInitialCapacity(m_resourceStatisticsMap.size());
326     for (auto& statistic : m_resourceStatisticsMap.values())
327         statistics.uncheckedAppend(WTFMove(statistic));
328
329     m_resourceStatisticsMap.clear();
330
331     return statistics;
332 }
333
334 void ResourceLoadObserver::clearState()
335 {
336     m_resourceStatisticsMap.clear();
337     m_lastReportedUserInteractionMap.clear();
338 }
339
340 } // namespace WebCore