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" },
];