Skip to content

Commit 08a9b91

Browse files
committed
Navigation Timing: cross-origin redirect opt-in should be based on destination origin.
https://bugs.webkit.org/show_bug.cgi?id=316647 Reviewed by Alex Christensen. This PR aligns the WebKit implementation with whatwg/fetch#1931 and whatwg/html#12513, and ensures that TAO opt-ins for navigation timing take the destination origin into account. New iframe tests, plus test progressions. * LayoutTests/imported/w3c/web-platform-tests/navigation-timing/redirect-tao-expected.txt: Progression. * LayoutTests/imported/w3c/web-platform-tests/navigation-timing/redirect-tao.html: * LayoutTests/imported/w3c/web-platform-tests/navigation-timing/resources/redirect-tao-helper.js: * LayoutTests/imported/w3c/web-platform-tests/navigation-timing/resources/report-navigation-redirect-timing.html: Added. * LayoutTests/imported/w3c/web-platform-tests/navigation-timing/response-start-after-coop-bcg-switch.https-expected.txt: Expectation change. * LayoutTests/imported/w3c/web-platform-tests/navigation-timing/unload-event-same-origin-check-expected.txt: Progression. * Source/WebCore/loader/DocumentLoader.cpp: (WebCore::hideRedirectTimingForNoReferrerNavigation): Resets redirect count if noreferrer. (WebCore::DocumentLoader::notifyFinished): Calls hideRedirectTimingForNoReferrerNavigation. (WebCore::DocumentLoader::commitData): Calls hideRedirectTimingForNoReferrerNavigation. * Source/WebCore/page/PerformanceNavigationTiming.cpp: (WebCore::PerformanceNavigationTiming::shouldExposeRedirectTiming const): Only exposes redirect timing if redirectCount is not zero. (WebCore::PerformanceNavigationTiming::redirectCount const): TAO check. * Source/WebCore/page/PerformanceNavigationTiming.h: * Source/WebCore/page/PerformanceResourceTiming.cpp: (WebCore::fetchStart): Pass exposeRedirectTiming. (WebCore::entryStartTime): Use shouldExposeRedirectTiming(). (WebCore::PerformanceResourceTiming::shouldExposeRedirectTiming const): A virtual fun that enables NavigationTiming to override the default RT behavior. (WebCore::PerformanceResourceTiming::redirectStart const): Use shouldExposeRedirectTiming(). (WebCore::PerformanceResourceTiming::redirectEnd const): Use shouldExposeRedirectTiming(). (WebCore::PerformanceResourceTiming::fetchStart const): Use shouldExposeRedirectTiming(). * Source/WebCore/page/PerformanceResourceTiming.h: * Source/WebCore/platform/network/TimingAllowOrigin.cpp: (WebCore::passesNavigationTAOCheck): * Source/WebCore/platform/network/TimingAllowOrigin.h: * Source/WebKit/NetworkProcess/NetworkLoadChecker.cpp: (WebKit::NetworkLoadChecker::checkRedirection): Append TAO values. (WebKit::NetworkLoadChecker::validateResponse): Set TAO values on the response. (WebKit::NetworkLoadChecker::appendToNavigationTimingAllowValuesList): Accumulate TAO values. * Source/WebKit/NetworkProcess/NetworkLoadChecker.h: (WebKit::NetworkLoadChecker::navigationTAOCheckPassed const): Getter. * Source/WebKit/NetworkProcess/NetworkResourceLoader.cpp: (WebKit::NetworkResourceLoader::didFinishLoading): Canonical link: https://commits.webkit.org/317060@main
1 parent a5831b7 commit 08a9b91

16 files changed

Lines changed: 172 additions & 16 deletions
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11

22

3-
FAIL Exposed when a cross-origin redirect opts in to the destination origin assert_equals: redirectCount expected 1 but got 0
4-
FAIL Exposed when a cross-origin redirect opts in with a wildcard assert_equals: redirectCount expected 1 but got 0
5-
FAIL Exposed when every redirect in the chain opts in to the destination origin assert_equals: redirectCount expected 2 but got 0
3+
PASS Exposed when a cross-origin redirect opts in to the destination origin
4+
PASS Exposed when a cross-origin redirect opts in with a wildcard
5+
PASS Exposed when every redirect in the chain opts in to the destination origin
66
PASS Hidden when the redirect opts in to a non-destination origin
77
PASS Hidden when only part of the chain opts in to the destination origin
88
PASS Hidden for a no-referrer navigation even when the redirect opts in
9+
PASS Exposed when the redirect opts in to a cross-origin destination origin
10+
PASS Hidden when the redirect opts in to the main frame origin but not the destination origin
911

LayoutTests/imported/w3c/web-platform-tests/navigation-timing/redirect-tao.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,19 @@
4646
{referrerPolicy: "no-referrer"});
4747
assert_redirect_timing_hidden(entry);
4848
}, "Hidden for a no-referrer navigation even when the redirect opts in");
49+
50+
// The following two navigations land on a cross-origin destination (distinct
51+
// from the main frame origin), so they distinguish "opt in to the destination"
52+
// from "opt in to the main frame origin".
53+
54+
promise_test(async () => {
55+
const entry = await cross_origin_destination_navigation_entry(CROSS_ORIGIN_DESTINATION);
56+
assert_redirect_timing_exposed(entry, 1);
57+
}, "Exposed when the redirect opts in to a cross-origin destination origin");
58+
59+
promise_test(async () => {
60+
const entry = await cross_origin_destination_navigation_entry(dest);
61+
assert_redirect_timing_hidden(entry);
62+
}, "Hidden when the redirect opts in to the main frame origin but not the destination origin");
4963
</script>
5064
</body>

LayoutTests/imported/w3c/web-platform-tests/navigation-timing/resources/redirect-tao-helper.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,43 @@ function navigation_entry_after_redirects(hops, {referrerPolicy} = {}) {
5353
});
5454
}
5555

56+
// The cross-origin origin a chain can land on, used to verify the navigation TAO
57+
// check keys off the navigation's destination origin rather than the main
58+
// frame's. Served from the "www" subdomain, cross-origin to the test page.
59+
const CROSS_ORIGIN_DESTINATION =
60+
new URL(make_absolute_url({subdomain: "www", path: "/"})).origin;
61+
62+
// A landing page served from the cross-origin destination that reports its own
63+
// navigation timing back to the (cross-origin) parent via postMessage.
64+
const CROSS_ORIGIN_FINAL_URL = make_absolute_url({
65+
subdomain: "www",
66+
path: "/navigation-timing/resources/report-navigation-redirect-timing.html",
67+
});
68+
69+
// Navigates an iframe through a single cross-origin redirect that lands on the
70+
// cross-origin destination, and resolves with the destination document's
71+
// reported navigation timing. `tao` is the Timing-Allow-Origin value the redirect
72+
// sends (or null for none).
73+
function cross_origin_destination_navigation_entry(tao) {
74+
return new Promise(resolve => {
75+
const frame = document.createElement("iframe");
76+
frame.style.cssText = "width: 250px; height: 250px;";
77+
const onMessage = event => {
78+
if (event.source !== frame.contentWindow)
79+
return;
80+
window.removeEventListener("message", onMessage);
81+
resolve(event.data);
82+
};
83+
window.addEventListener("message", onMessage);
84+
const tao_query = tao === null ? "" : "tao=" + encodeURIComponent(tao) + "&";
85+
frame.src = make_absolute_url({
86+
path: "/navigation-timing/resources/redirect-tao.py",
87+
query: tao_query + "location=" + encodeURIComponent(CROSS_ORIGIN_FINAL_URL),
88+
});
89+
document.body.appendChild(frame);
90+
});
91+
}
92+
5693
// Asserts that redirect timing is exposed, with `expectedCount` redirects.
5794
function assert_redirect_timing_exposed(entry, expectedCount) {
5895
assert_equals(entry.type, "navigate", "navigation type");
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>Reports its navigation redirect timing to the parent</title>
4+
<script>
5+
// Landing page for a redirected cross-origin iframe navigation. It reads its own
6+
// PerformanceNavigationTiming entry and posts the redirect-timing fields to the
7+
// parent, which is cross-origin and so cannot read them directly.
8+
addEventListener("load", () => {
9+
const entry = performance.getEntriesByType("navigation")[0];
10+
parent.postMessage({
11+
type: entry.type,
12+
redirectCount: entry.redirectCount,
13+
redirectStart: entry.redirectStart,
14+
redirectEnd: entry.redirectEnd,
15+
}, "*");
16+
});
17+
</script>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11

2-
FAIL Navigation responseStart is not clamped to zero after a cross-origin COOP browsing context group switch assert_greater_than: responseStart should not be clamped to zero after the COOP browsing context group switch expected a number greater than 0 but got 0
2+
FAIL Navigation responseStart is not clamped to zero after a cross-origin COOP browsing context group switch assert_equals: The cross-origin navigation should not expose the redirect count expected 0 but got 2
33

LayoutTests/imported/w3c/web-platform-tests/navigation-timing/unload-event-same-origin-check-expected.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This test validates that the values of window.performance.getEntriesByType("navi
55

66

77
PASS Redirect chain with a partial TAO opt-in
8-
FAIL Redirect chain with full TAO opt-in assert_equals: Expected redirectCount to be 1 expected 1 but got 0
8+
PASS Redirect chain with full TAO opt-in
99
PASS Same-cross-same redirect chain with no TAO opt-in
1010
PASS cross-cross-same Redirect chain with no TAO opt-in
1111
PASS Previous document cross origin
@@ -14,6 +14,6 @@ PASS No previous document
1414
PASS Same origin previous document with same origin redirect
1515
PASS No previous document with same origin redirect
1616
PASS No previous document with cross origin redirect
17-
FAIL No previous document with cross origin redirect with partial TAO assert_equals: Expected redirectCount to be 1 expected 1 but got 0
17+
PASS No previous document with cross origin redirect with partial TAO
1818
PASS No previous document with cross origin redirect with TAO
1919

Source/WebCore/loader/DocumentLoader.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,13 @@ bool DocumentLoader::isLoading() const
430430
return isLoadingMainResource() || !m_subresourceLoaders.isEmpty() || !m_plugInStreamLoaders.isEmpty();
431431
}
432432

433+
static void hideRedirectTimingForNoReferrerNavigation(const DocumentLoader& loader, NetworkLoadMetrics& metrics)
434+
{
435+
// https://html.spec.whatwg.org/C#initialise-the-document-object step 15.3 resets redirectCount in case of "no-referrer".
436+
if (loader.triggeringAction().requester() && loader.request().httpReferrer().isEmpty())
437+
metrics.redirectCount = 0;
438+
}
439+
433440
void DocumentLoader::notifyFinished(CachedResource& resource, const NetworkLoadMetrics& fetchMetrics, LoadWillContinueInAnotherProcess loadWillContinueInAnotherProcess)
434441
{
435442
ASSERT(isMainThread());
@@ -448,6 +455,8 @@ void DocumentLoader::notifyFinished(CachedResource& resource, const NetworkLoadM
448455
if (!metrics)
449456
metrics = Box<NetworkLoadMetrics>::create(fetchMetrics);
450457

458+
hideRedirectTimingForNoReferrerNavigation(*this, *metrics);
459+
451460
if (RefPtr document = this->document()) {
452461
if (RefPtr window = document->window())
453462
protect(window->performance())->documentLoadFinished(*metrics);
@@ -1389,6 +1398,7 @@ void DocumentLoader::commitData(const SharedBuffer& data)
13891398
|| source == ResourceResponse::Source::MemoryCacheAfterValidation;
13901399
if (RefPtr frameLoader = this->frameLoader())
13911400
finalMetrics.fromPrefetch = frameLoader->documentPrefetcher().wasPrefetched(url());
1401+
hideRedirectTimingForNoReferrerNavigation(*this, finalMetrics);
13921402
protect(window->performance())->addNavigationTiming(*this, document, protect(*m_mainResource), timing(), finalMetrics);
13931403
}
13941404
}

Source/WebCore/page/PerformanceNavigationTiming.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,17 @@ PerformanceNavigationTiming::NavigationType PerformanceNavigationTiming::type()
121121
return m_navigationType;
122122
}
123123

124-
unsigned short PerformanceNavigationTiming::redirectCount() const
124+
bool PerformanceNavigationTiming::shouldExposeRedirectTiming() const
125125
{
126-
if (m_resourceTiming.networkLoadMetrics().hasCrossOriginRedirect)
127-
return 0;
126+
// https://html.spec.whatwg.org/C#initialise-the-document-object step 15 zeroes redirectCount when a
127+
// cross-origin redirect chain does not opt in, so hide the redirect gap only then; a navigation with
128+
// no such redirect (e.g. served by a service worker) must still expose its real fetch start.
129+
auto& metrics = m_resourceTiming.networkLoadMetrics();
130+
return metrics.redirectCount || !metrics.hasCrossOriginRedirect;
131+
}
128132

133+
unsigned short PerformanceNavigationTiming::redirectCount() const
134+
{
129135
return m_resourceTiming.networkLoadMetrics().redirectCount;
130136
}
131137

Source/WebCore/page/PerformanceNavigationTiming.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class PerformanceNavigationTiming final : public PerformanceResourceTiming {
7777

7878
double millisecondsSinceOrigin(MonotonicTime) const;
7979
bool NODELETE sameOriginCheckFails() const;
80+
bool NODELETE shouldExposeRedirectTiming() const final;
8081

8182
DocumentEventTiming m_documentEventTiming;
8283
DocumentLoadTiming m_documentLoadTiming;

Source/WebCore/page/PerformanceResourceTiming.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@ static double networkLoadTimeToDOMHighResTimeStamp(MonotonicTime timeOrigin, Mon
5656
return result.milliseconds();
5757
}
5858

59-
static double fetchStart(MonotonicTime timeOrigin, const ResourceTiming& resourceTiming)
59+
static double fetchStart(MonotonicTime timeOrigin, const ResourceTiming& resourceTiming, bool exposeRedirectTiming)
6060
{
61-
if (auto fetchStart = resourceTiming.networkLoadMetrics().fetchStart; fetchStart && !resourceTiming.networkLoadMetrics().failsTAOCheck)
61+
// The real fetch start reveals how long the redirects took, so only use it when redirect timing
62+
// is exposed; otherwise fall back to the start time, which collapses that gap.
63+
if (auto fetchStart = resourceTiming.networkLoadMetrics().fetchStart; fetchStart && exposeRedirectTiming)
6264
return networkLoadTimeToDOMHighResTimeStamp(timeOrigin, fetchStart);
6365

6466
// fetchStart is a required property.
@@ -71,7 +73,7 @@ static double entryStartTime(MonotonicTime timeOrigin, const ResourceTiming& res
7173
{
7274
if (resourceTiming.networkLoadMetrics().failsTAOCheck
7375
|| !resourceTiming.networkLoadMetrics().redirectCount)
74-
return fetchStart(timeOrigin, resourceTiming);
76+
return fetchStart(timeOrigin, resourceTiming, !resourceTiming.networkLoadMetrics().failsTAOCheck);
7577

7678
if (resourceTiming.networkLoadMetrics().redirectStart)
7779
return networkLoadTimeToDOMHighResTimeStamp(timeOrigin, resourceTiming.networkLoadMetrics().redirectStart);
@@ -118,9 +120,14 @@ double PerformanceResourceTiming::workerStart() const
118120
return networkLoadTimeToDOMHighResTimeStamp(m_timeOrigin, m_resourceTiming.networkLoadMetrics().workerStart);
119121
}
120122

123+
bool PerformanceResourceTiming::shouldExposeRedirectTiming() const
124+
{
125+
return !m_resourceTiming.networkLoadMetrics().failsTAOCheck;
126+
}
127+
121128
double PerformanceResourceTiming::redirectStart() const
122129
{
123-
if (m_resourceTiming.networkLoadMetrics().failsTAOCheck)
130+
if (!shouldExposeRedirectTiming())
124131
return 0.0;
125132

126133
if (m_resourceTiming.isLoadedFromServiceWorker())
@@ -134,7 +141,7 @@ double PerformanceResourceTiming::redirectStart() const
134141

135142
double PerformanceResourceTiming::redirectEnd() const
136143
{
137-
if (m_resourceTiming.networkLoadMetrics().failsTAOCheck)
144+
if (!shouldExposeRedirectTiming())
138145
return 0.0;
139146

140147
if (m_resourceTiming.isLoadedFromServiceWorker())
@@ -143,14 +150,15 @@ double PerformanceResourceTiming::redirectEnd() const
143150
if (!m_resourceTiming.networkLoadMetrics().redirectCount)
144151
return 0.0;
145152

153+
// redirectEnd is when the last redirect finished, i.e. when the final request's fetch started.
146154
// These two times are so close to each other that we don't record two timestamps.
147155
// See https://www.w3.org/TR/resource-timing-2/#attribute-descriptions
148156
return fetchStart();
149157
}
150158

151159
double PerformanceResourceTiming::fetchStart() const
152160
{
153-
return WebCore::fetchStart(m_timeOrigin, m_resourceTiming);
161+
return WebCore::fetchStart(m_timeOrigin, m_resourceTiming, shouldExposeRedirectTiming());
154162
}
155163

156164
double PerformanceResourceTiming::domainLookupStart() const

0 commit comments

Comments
 (0)