1/*
2 * Copyright (C) 2016-2019 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 "FrameLoaderClient.h"
34#include "HTMLFrameOwnerElement.h"
35#include "Logging.h"
36#include "Page.h"
37#include "RegistrableDomain.h"
38#include "ResourceLoadStatistics.h"
39#include "ResourceRequest.h"
40#include "ResourceResponse.h"
41#include "RuntimeEnabledFeatures.h"
42#include "ScriptExecutionContext.h"
43#include "SecurityOrigin.h"
44#include "Settings.h"
45#include <wtf/URL.h>
46
47namespace WebCore {
48
49static const Seconds minimumNotificationInterval { 5_s };
50
51ResourceLoadObserver& ResourceLoadObserver::shared()
52{
53 static NeverDestroyed<ResourceLoadObserver> resourceLoadObserver;
54 return resourceLoadObserver;
55}
56
57void ResourceLoadObserver::setStatisticsUpdatedCallback(WTF::Function<void(Vector<ResourceLoadStatistics>&&)>&& notificationCallback)
58{
59 ASSERT(!m_notificationCallback);
60 m_notificationCallback = WTFMove(notificationCallback);
61}
62
63void ResourceLoadObserver::setRequestStorageAccessUnderOpenerCallback(WTF::Function<void(PAL::SessionID sessionID, const RegistrableDomain& domainInNeedOfStorageAccess, uint64_t openerPageID, const RegistrableDomain& openerDomain)>&& callback)
64{
65 ASSERT(!m_requestStorageAccessUnderOpenerCallback);
66 m_requestStorageAccessUnderOpenerCallback = WTFMove(callback);
67}
68
69void ResourceLoadObserver::setLogUserInteractionNotificationCallback(Function<void(PAL::SessionID, const RegistrableDomain&)>&& callback)
70{
71 ASSERT(!m_logUserInteractionNotificationCallback);
72 m_logUserInteractionNotificationCallback = WTFMove(callback);
73}
74
75void ResourceLoadObserver::setLogWebSocketLoadingNotificationCallback(Function<void(PAL::SessionID, const RegistrableDomain&, const RegistrableDomain&, WallTime)>&& callback)
76{
77 ASSERT(!m_logWebSocketLoadingNotificationCallback);
78 m_logWebSocketLoadingNotificationCallback = WTFMove(callback);
79}
80
81void ResourceLoadObserver::setLogSubresourceLoadingNotificationCallback(Function<void(PAL::SessionID, const RegistrableDomain&, const RegistrableDomain&, WallTime)>&& callback)
82{
83 ASSERT(!m_logSubresourceLoadingNotificationCallback);
84 m_logSubresourceLoadingNotificationCallback = WTFMove(callback);
85}
86
87void ResourceLoadObserver::setLogSubresourceRedirectNotificationCallback(Function<void(PAL::SessionID, const RegistrableDomain&, const RegistrableDomain&)>&& callback)
88{
89 ASSERT(!m_logSubresourceRedirectNotificationCallback);
90 m_logSubresourceRedirectNotificationCallback = WTFMove(callback);
91}
92
93static inline bool is3xxRedirect(const ResourceResponse& response)
94{
95 return response.httpStatusCode() >= 300 && response.httpStatusCode() <= 399;
96}
97
98bool ResourceLoadObserver::shouldLog(bool usesEphemeralSession) const
99{
100 return DeprecatedGlobalSettings::resourceLoadStatisticsEnabled() && !usesEphemeralSession && m_notificationCallback;
101}
102
103void ResourceLoadObserver::logSubresourceLoading(const Frame* frame, const ResourceRequest& newRequest, const ResourceResponse& redirectResponse)
104{
105 ASSERT(frame->page());
106
107 if (!frame)
108 return;
109
110 auto* page = frame->page();
111 if (!page || !shouldLog(page->usesEphemeralSession()))
112 return;
113
114 bool isRedirect = is3xxRedirect(redirectResponse);
115 const URL& redirectedFromURL = redirectResponse.url();
116 const URL& targetURL = newRequest.url();
117 const URL& topFrameURL = frame ? frame->mainFrame().document()->url() : URL();
118
119 auto targetHost = targetURL.host();
120 auto topFrameHost = topFrameURL.host();
121
122 if (targetHost.isEmpty() || topFrameHost.isEmpty() || targetHost == topFrameHost || (isRedirect && targetHost == redirectedFromURL.host()))
123 return;
124
125 RegistrableDomain targetDomain { targetURL };
126 RegistrableDomain topFrameDomain { topFrameURL };
127 RegistrableDomain redirectedFromDomain { redirectedFromURL };
128
129 if (targetDomain == topFrameDomain || (isRedirect && targetDomain == redirectedFromDomain))
130 return;
131
132 {
133 auto& targetStatistics = ensureResourceStatisticsForRegistrableDomain(targetDomain);
134 auto lastSeen = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
135 targetStatistics.lastSeen = lastSeen;
136 targetStatistics.subresourceUnderTopFrameDomains.add(topFrameDomain);
137
138 m_logSubresourceLoadingNotificationCallback(page->sessionID(), targetDomain, topFrameDomain, lastSeen);
139 }
140
141 if (isRedirect) {
142 auto& redirectingOriginStatistics = ensureResourceStatisticsForRegistrableDomain(redirectedFromDomain);
143 redirectingOriginStatistics.subresourceUniqueRedirectsTo.add(targetDomain);
144 auto& targetStatistics = ensureResourceStatisticsForRegistrableDomain(targetDomain);
145 targetStatistics.subresourceUniqueRedirectsFrom.add(redirectedFromDomain);
146
147 m_logSubresourceRedirectNotificationCallback(page->sessionID(), redirectedFromDomain, targetDomain);
148 }
149}
150
151void ResourceLoadObserver::logWebSocketLoading(const URL& targetURL, const URL& mainFrameURL, PAL::SessionID sessionID)
152{
153 if (!shouldLog(sessionID.isEphemeral()))
154 return;
155
156 auto targetHost = targetURL.host();
157 auto mainFrameHost = mainFrameURL.host();
158
159 if (targetHost.isEmpty() || mainFrameHost.isEmpty() || targetHost == mainFrameHost)
160 return;
161
162 RegistrableDomain targetDomain { targetURL };
163 RegistrableDomain topFrameDomain { mainFrameURL };
164
165 if (targetDomain == topFrameDomain)
166 return;
167
168 auto lastSeen = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
169
170 auto& targetStatistics = ensureResourceStatisticsForRegistrableDomain(targetDomain);
171 targetStatistics.lastSeen = lastSeen;
172 targetStatistics.subresourceUnderTopFrameDomains.add(topFrameDomain);
173
174 m_logWebSocketLoadingNotificationCallback(sessionID, targetDomain, topFrameDomain, lastSeen);
175}
176
177void ResourceLoadObserver::logUserInteractionWithReducedTimeResolution(const Document& document)
178{
179 if (!document.sessionID().isValid() || !shouldLog(document.sessionID().isEphemeral()))
180 return;
181
182 auto& url = document.url();
183 if (url.protocolIsAbout() || url.isLocalFile() || url.isEmpty())
184 return;
185
186 RegistrableDomain topFrameDomain { url };
187 auto newTime = ResourceLoadStatistics::reduceTimeResolution(WallTime::now());
188 auto lastReportedUserInteraction = m_lastReportedUserInteractionMap.get(topFrameDomain);
189 if (newTime == lastReportedUserInteraction)
190 return;
191
192 m_lastReportedUserInteractionMap.set(topFrameDomain, newTime);
193
194 auto& statistics = ensureResourceStatisticsForRegistrableDomain(topFrameDomain);
195 statistics.hadUserInteraction = true;
196 statistics.lastSeen = newTime;
197 statistics.mostRecentUserInteractionTime = newTime;
198
199#if ENABLE(RESOURCE_LOAD_STATISTICS)
200 if (auto* frame = document.frame()) {
201 if (auto* opener = frame->loader().opener()) {
202 if (auto* openerDocument = opener->document()) {
203 if (auto* openerFrame = openerDocument->frame()) {
204 if (auto openerPageID = openerFrame->loader().client().pageID())
205 requestStorageAccessUnderOpener(document.sessionID(), topFrameDomain, openerPageID.value(), *openerDocument);
206 }
207 }
208 }
209 }
210
211 m_logUserInteractionNotificationCallback(document.sessionID(), topFrameDomain);
212#endif
213
214#if ENABLE(RESOURCE_LOAD_STATISTICS) && !RELEASE_LOG_DISABLED
215 if (shouldLogUserInteraction()) {
216 auto counter = ++m_loggingCounter;
217#define LOCAL_LOG(str, ...) \
218 RELEASE_LOG(ResourceLoadStatistics, "ResourceLoadObserver::logUserInteraction: counter = %" PRIu64 ": " str, counter, ##__VA_ARGS__)
219
220 auto escapeForJSON = [](String s) {
221 s.replace('\\', "\\\\").replace('"', "\\\"");
222 return s;
223 };
224 auto escapedURL = escapeForJSON(url.string());
225 auto escapedDomain = escapeForJSON(topFrameDomain.string());
226
227 LOCAL_LOG(R"({ "url": "%{public}s",)", escapedURL.utf8().data());
228 LOCAL_LOG(R"( "domain" : "%{public}s",)", escapedDomain.utf8().data());
229 LOCAL_LOG(R"( "until" : %f })", newTime.secondsSinceEpoch().seconds());
230
231#undef LOCAL_LOG
232 }
233#endif
234}
235
236#if ENABLE(RESOURCE_LOAD_STATISTICS)
237void ResourceLoadObserver::requestStorageAccessUnderOpener(PAL::SessionID sessionID, const RegistrableDomain& domainInNeedOfStorageAccess, uint64_t openerPageID, Document& openerDocument)
238{
239 auto openerUrl = openerDocument.url();
240 RegistrableDomain openerDomain { openerUrl };
241 if (domainInNeedOfStorageAccess != openerDomain
242 && !openerDocument.hasRequestedPageSpecificStorageAccessWithUserInteraction(domainInNeedOfStorageAccess)
243 && !equalIgnoringASCIICase(openerUrl.string(), WTF::blankURL())) {
244 m_requestStorageAccessUnderOpenerCallback(sessionID, domainInNeedOfStorageAccess, openerPageID, openerDomain);
245 // Remember user interaction-based requests since they don't need to be repeated.
246 openerDocument.setHasRequestedPageSpecificStorageAccessWithUserInteraction(domainInNeedOfStorageAccess);
247 }
248}
249#endif
250
251void ResourceLoadObserver::logFontLoad(const Document& document, const String& familyName, bool loadStatus)
252{
253#if ENABLE(WEB_API_STATISTICS)
254 if (!shouldLog(document.sessionID().isEphemeral()))
255 return;
256 RegistrableDomain registrableDomain { document.url() };
257 auto& statistics = ensureResourceStatisticsForRegistrableDomain(registrableDomain);
258 bool shouldCallNotificationCallback = false;
259 if (!loadStatus) {
260 if (statistics.fontsFailedToLoad.add(familyName).isNewEntry)
261 shouldCallNotificationCallback = true;
262 } else {
263 if (statistics.fontsSuccessfullyLoaded.add(familyName).isNewEntry)
264 shouldCallNotificationCallback = true;
265 }
266 RegistrableDomain mainFrameRegistrableDomain { document.topDocument().url() };
267 if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain.string()).isNewEntry)
268 shouldCallNotificationCallback = true;
269 if (shouldCallNotificationCallback)
270 scheduleNotificationIfNeeded();
271#else
272 UNUSED_PARAM(document);
273 UNUSED_PARAM(familyName);
274 UNUSED_PARAM(loadStatus);
275#endif
276}
277
278void ResourceLoadObserver::logCanvasRead(const Document& document)
279{
280#if ENABLE(WEB_API_STATISTICS)
281 if (!shouldLog(document.sessionID().isEphemeral()))
282 return;
283 RegistrableDomain registrableDomain { document.url() };
284 auto& statistics = ensureResourceStatisticsForRegistrableDomain(registrableDomain);
285 RegistrableDomain mainFrameRegistrableDomain { document.topDocument().url() };
286 statistics.canvasActivityRecord.wasDataRead = true;
287 if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain.string()).isNewEntry)
288 scheduleNotificationIfNeeded();
289#else
290 UNUSED_PARAM(document);
291#endif
292}
293
294void ResourceLoadObserver::logCanvasWriteOrMeasure(const Document& document, const String& textWritten)
295{
296#if ENABLE(WEB_API_STATISTICS)
297 if (!shouldLog(document.sessionID().isEphemeral()))
298 return;
299 RegistrableDomain registrableDomain { document.url() };
300 auto& statistics = ensureResourceStatisticsForRegistrableDomain(registrableDomain);
301 bool shouldCallNotificationCallback = false;
302 RegistrableDomain mainFrameRegistrableDomain { document.topDocument().url() };
303 if (statistics.canvasActivityRecord.recordWrittenOrMeasuredText(textWritten))
304 shouldCallNotificationCallback = true;
305 if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain.string()).isNewEntry)
306 shouldCallNotificationCallback = true;
307 if (shouldCallNotificationCallback)
308 scheduleNotificationIfNeeded();
309#else
310 UNUSED_PARAM(document);
311 UNUSED_PARAM(textWritten);
312#endif
313}
314
315void ResourceLoadObserver::logNavigatorAPIAccessed(const Document& document, const ResourceLoadStatistics::NavigatorAPI functionName)
316{
317#if ENABLE(WEB_API_STATISTICS)
318 if (!shouldLog(document.sessionID().isEphemeral()))
319 return;
320 RegistrableDomain registrableDomain { document.url() };
321 auto& statistics = ensureResourceStatisticsForRegistrableDomain(registrableDomain);
322 bool shouldCallNotificationCallback = false;
323 if (!statistics.navigatorFunctionsAccessed.contains(functionName)) {
324 statistics.navigatorFunctionsAccessed.add(functionName);
325 shouldCallNotificationCallback = true;
326 }
327 RegistrableDomain mainFrameRegistrableDomain { document.topDocument().url() };
328 if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain.string()).isNewEntry)
329 shouldCallNotificationCallback = true;
330 if (shouldCallNotificationCallback)
331 scheduleNotificationIfNeeded();
332#else
333 UNUSED_PARAM(document);
334 UNUSED_PARAM(functionName);
335#endif
336}
337
338void ResourceLoadObserver::logScreenAPIAccessed(const Document& document, const ResourceLoadStatistics::ScreenAPI functionName)
339{
340#if ENABLE(WEB_API_STATISTICS)
341 if (!shouldLog(document.sessionID().isEphemeral()))
342 return;
343 RegistrableDomain registrableDomain { document.url() };
344 auto& statistics = ensureResourceStatisticsForRegistrableDomain(registrableDomain);
345 bool shouldCallNotificationCallback = false;
346 if (!statistics.screenFunctionsAccessed.contains(functionName)) {
347 statistics.screenFunctionsAccessed.add(functionName);
348 shouldCallNotificationCallback = true;
349 }
350 RegistrableDomain mainFrameRegistrableDomain { document.topDocument().url() };
351 if (statistics.topFrameRegistrableDomainsWhichAccessedWebAPIs.add(mainFrameRegistrableDomain.string()).isNewEntry)
352 shouldCallNotificationCallback = true;
353 if (shouldCallNotificationCallback)
354 scheduleNotificationIfNeeded();
355#else
356 UNUSED_PARAM(document);
357 UNUSED_PARAM(functionName);
358#endif
359}
360
361ResourceLoadStatistics& ResourceLoadObserver::ensureResourceStatisticsForRegistrableDomain(const RegistrableDomain& domain)
362{
363 auto addResult = m_resourceStatisticsMap.ensure(domain, [&domain] {
364 return ResourceLoadStatistics(domain);
365 });
366 return addResult.iterator->value;
367}
368
369void ResourceLoadObserver::updateCentralStatisticsStore()
370{
371 m_notificationCallback(takeStatistics());
372}
373
374String ResourceLoadObserver::statisticsForURL(const URL& url)
375{
376 auto iter = m_resourceStatisticsMap.find(RegistrableDomain { url });
377 if (iter == m_resourceStatisticsMap.end())
378 return emptyString();
379
380 return makeString("Statistics for ", url.host().toString(), ":\n", iter->value.toString());
381}
382
383Vector<ResourceLoadStatistics> ResourceLoadObserver::takeStatistics()
384{
385 Vector<ResourceLoadStatistics> statistics;
386 statistics.reserveInitialCapacity(m_resourceStatisticsMap.size());
387 for (auto& statistic : m_resourceStatisticsMap.values())
388 statistics.uncheckedAppend(WTFMove(statistic));
389
390 m_resourceStatisticsMap.clear();
391
392 return statistics;
393}
394
395void ResourceLoadObserver::clearState()
396{
397 m_resourceStatisticsMap.clear();
398 m_lastReportedUserInteractionMap.clear();
399}
400
401URL ResourceLoadObserver::nonNullOwnerURL(const Document& document) const
402{
403 auto url = document.url();
404 auto* frame = document.frame();
405 auto host = document.url().host();
406
407 while ((host.isNull() || host.isEmpty()) && frame && !frame->isMainFrame()) {
408 auto* ownerElement = frame->ownerElement();
409
410 ASSERT(ownerElement != nullptr);
411
412 auto& doc = ownerElement->document();
413 frame = doc.frame();
414 url = doc.url();
415 host = url.host();
416 }
417
418 return url;
419}
420
421} // namespace WebCore
422