diff --git a/navigation-timing/redirect-tao.tentative.html b/navigation-timing/redirect-tao.tentative.html new file mode 100644 index 00000000000000..d33f5af83e9f0b --- /dev/null +++ b/navigation-timing/redirect-tao.tentative.html @@ -0,0 +1,50 @@ + + +Navigation Timing: redirect timing exposure for cross-origin redirect chains based on Timing-Allow-Origin and the destination origin + + + + + + + + + + diff --git a/navigation-timing/resources/redirect-tao-helper.js b/navigation-timing/resources/redirect-tao-helper.js new file mode 100644 index 00000000000000..642ca72ff11de1 --- /dev/null +++ b/navigation-timing/resources/redirect-tao-helper.js @@ -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"); +} diff --git a/navigation-timing/resources/redirect-tao.py b/navigation-timing/resources/redirect-tao.py new file mode 100644 index 00000000000000..021acfc7c3261d --- /dev/null +++ b/navigation-timing/resources/redirect-tao.py @@ -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")) diff --git a/navigation-timing/unload-event-same-origin-check.html b/navigation-timing/unload-event-same-origin-check.html index 319d04462deba8..9814b582320e1d 100644 --- a/navigation-timing/unload-event-same-origin-check.html +++ b/navigation-timing/unload-event-same-origin-check.html @@ -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" }, @@ -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" }, ];