Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions navigation-timing/redirect-tao.tentative.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Navigation Timing: redirect timing exposure for cross-origin redirect chains based on Timing-Allow-Origin and the destination origin</title>
<link rel="author" title="Yoav Weiss" href="mailto:yoav@yoav.ws">
<link rel="help" href="https://github.com/whatwg/fetch/pull/1931">
<link rel="help" href="https://github.com/whatwg/html/pull/12513">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/utils.js"></script>
<script src="resources/redirect-tao-helper.js"></script>
<body>
<script>
const dest = DESTINATION_ORIGIN;

promise_test(async () => {
const entry = await navigation_entry_after_redirects([dest]);
assert_redirect_timing_exposed(entry, 1);
}, "Exposed when a cross-origin redirect opts in to the destination origin");

promise_test(async () => {
const entry = await navigation_entry_after_redirects(["*"]);
assert_redirect_timing_exposed(entry, 1);
}, "Exposed when a cross-origin redirect opts in with a wildcard");

promise_test(async () => {
const entry = await navigation_entry_after_redirects([dest, dest]);
assert_redirect_timing_exposed(entry, 2);
}, "Exposed when every redirect in the chain opts in to the destination origin");

promise_test(async () => {
const entry = await navigation_entry_after_redirects([
"https://not-the-destination.example",
]);
assert_redirect_timing_hidden(entry);
}, "Hidden when the redirect opts in to a non-destination origin");

promise_test(async () => {
// First hop sends no Timing-Allow-Origin header; second hop opts in. Every
// redirect in the chain must opt in for redirect timing to be exposed.
const entry = await navigation_entry_after_redirects([null, dest]);
assert_redirect_timing_hidden(entry);
}, "Hidden when only part of the chain opts in to the destination origin");

promise_test(async () => {
const entry = await navigation_entry_after_redirects([dest],
{referrerPolicy: "no-referrer"});
assert_redirect_timing_hidden(entry);
}, "Hidden for a no-referrer navigation even when the redirect opts in");
</script>
</body>
71 changes: 71 additions & 0 deletions navigation-timing/resources/redirect-tao-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Helpers for navigation redirect-timing TAO tests.
//
// These build a cross-origin server-side redirect chain (served from the "www"
// subdomain, which is cross-origin to the test page) that finally lands back on
// the test page's own origin -- the navigation's "destination origin". Each hop
// is handled by resources/redirect-tao.py, which optionally emits a
// Timing-Allow-Origin header.
//
// See:
// https://github.com/whatwg/fetch/pull/1931
// https://github.com/whatwg/html/pull/12513

// The navigation's destination origin (where the chain lands).
const DESTINATION_ORIGIN = location.origin;

// The final, non-redirect document the chain resolves to (same-origin with the
// test page, so its navigation timing entry is readable).
const FINAL_URL =
make_absolute_url({path: "/navigation-timing/resources/blank-page-green.html"});

// Builds a redirect-chain URL from `hops`, an array with one entry per redirect.
// Each entry is the value to send in that redirect's Timing-Allow-Origin header,
// or null to send no header (i.e. that hop does not opt in).
function redirect_chain_url(hops) {
let url = FINAL_URL;
// Build from the last hop backwards, so each redirect points at the next one.
for (let i = hops.length - 1; i >= 0; i--) {
const tao = hops[i] === null ? "" : "tao=" + encodeURIComponent(hops[i]) + "&";
url = make_absolute_url({
subdomain: "www",
path: "/navigation-timing/resources/redirect-tao.py",
query: tao + "location=" + encodeURIComponent(url),
});
}
return url;
}

// Navigates an iframe through the redirect chain described by `hops` and resolves
// with the iframe's PerformanceNavigationTiming entry. `referrerPolicy` is an
// optional referrer policy to apply to the iframe (e.g. "no-referrer").
function navigation_entry_after_redirects(hops, {referrerPolicy} = {}) {
return new Promise(resolve => {
const frame = document.createElement("iframe");
frame.style.cssText = "width: 250px; height: 250px;";
if (referrerPolicy) {
frame.referrerPolicy = referrerPolicy;
}
frame.onload = () => {
resolve(frame.contentWindow.performance.getEntriesByType("navigation")[0]);
};
frame.src = redirect_chain_url(hops);
document.body.appendChild(frame);
});
}

// Asserts that redirect timing is exposed, with `expectedCount` redirects.
function assert_redirect_timing_exposed(entry, expectedCount) {
assert_equals(entry.type, "navigate", "navigation type");
assert_equals(entry.redirectCount, expectedCount, "redirectCount");
assert_greater_than(entry.redirectStart, 0, "redirectStart is exposed");
assert_greater_than_equal(entry.redirectEnd, entry.redirectStart,
"redirectEnd is greater than or equal to redirectStart");
}

// Asserts that redirect timing is fully hidden (zeroed out).
function assert_redirect_timing_hidden(entry) {
assert_equals(entry.type, "navigate", "navigation type");
assert_equals(entry.redirectCount, 0, "redirectCount is hidden");
assert_equals(entry.redirectStart, 0, "redirectStart is hidden");
assert_equals(entry.redirectEnd, 0, "redirectEnd is hidden");
}
22 changes: 22 additions & 0 deletions navigation-timing/resources/redirect-tao.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
def main(request, response):
"""Redirect handler that optionally sets a Timing-Allow-Origin header.

Query parameters:
status - The status code to use for the redirection. Defaults to 302.
location - The (percent-encoded) resource to redirect to.
tao - The value to send in the Timing-Allow-Origin response header. If
absent, no Timing-Allow-Origin header is sent (i.e. the redirect
does not opt in).
"""
status = 302
if b"status" in request.GET:
try:
status = int(request.GET.first(b"status"))
except ValueError:
pass

response.status = status
location = request.GET.first(b"location")
response.headers.set(b"Location", location)
if b"tao" in request.GET:
response.headers.set(b"Timing-Allow-Origin", request.GET.first(b"tao"))
4 changes: 2 additions & 2 deletions navigation-timing/unload-event-same-origin-check.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
const cross_origin_start = host_info["HTTP_REMOTE_ORIGIN"] + start_page;
const test_cases = [
{ start_url : start_page, end_url: redirect_chain_partial_tao(), unload_exposed: false, redirects: 0, name: "Redirect chain with a partial TAO opt-in" },
{ start_url : start_page, end_url: redirect_chain_full_tao(), unload_exposed: false, redirects: 0, name: "Redirect chain with full TAO opt-in" },
{ start_url : start_page, end_url: redirect_chain_full_tao(), unload_exposed: false, redirects: 1, name: "Redirect chain with full TAO opt-in" },
{ start_url : start_page, end_url: redirect_chain_no_tao(), unload_exposed: false, redirects: 0, name: "Same-cross-same redirect chain with no TAO opt-in" },
{ start_url : cross_origin_start, end_url: redirect_chain_no_tao(), unload_exposed: false, redirects: 0, name: "cross-cross-same Redirect chain with no TAO opt-in" },
{ start_url : cross_origin_start, end_url: end_page, unload_exposed: false, redirects: 0, name: "Previous document cross origin" },
Expand All @@ -62,7 +62,7 @@
{ start_url : start_page, end_url: same_origin_redirect_chain(), unload_exposed: true, redirects: 1, name: "Same origin previous document with same origin redirect" },
{ start_url : same_origin_redirect_chain(), end_url: null, unload_exposed: false, redirects: 1, name: "No previous document with same origin redirect" },
{ start_url : redirect_chain_no_tao(), end_url: null, unload_exposed: false, redirects: 0, name: "No previous document with cross origin redirect" },
{ start_url : redirect_chain_full_tao(), end_url: null, unload_exposed: false, redirects: 0, name: "No previous document with cross origin redirect with partial TAO" },
{ start_url : redirect_chain_full_tao(), end_url: null, unload_exposed: false, redirects: 1, name: "No previous document with cross origin redirect with partial TAO" },
{ start_url : redirect_chain_partial_tao(), end_url: null, unload_exposed: false, redirects: 0, name: "No previous document with cross origin redirect with TAO" },
];

Expand Down
Loading