diff options
Diffstat (limited to 'testing/web-platform/tests/speculation-rules/prefetch/resources')
11 files changed, 416 insertions, 0 deletions
diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py new file mode 100644 index 0000000000..8820781709 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/authenticate.py @@ -0,0 +1,36 @@ + +def main(request, response): + def fmt(x): + return f'"{x.decode("utf-8")}"' if x is not None else "undefined" + + purpose = request.headers.get("Purpose", b"").decode("utf-8") + sec_purpose = request.headers.get("Sec-Purpose", b"").decode("utf-8") + + headers = [ + (b"Content-Type", b"text/html"), + (b'WWW-Authenticate', b'Basic'), + (b'Cache-Control', b'no-store') + ] + status = 200 if request.auth.username is not None or sec_purpose.startswith( + "prefetch") else 401 + + content = f''' + <!DOCTYPE html> + <script src="/common/dispatcher/dispatcher.js"></script> + <script src="utils.sub.js"></script> + <script> + window.requestHeaders = {{ + purpose: "{purpose}", + sec_purpose: "{sec_purpose}" + }}; + + window.requestCredentials = {{ + username: {fmt(request.auth.username)}, + password: {fmt(request.auth.password)} + }}; + + const uuid = new URLSearchParams(location.search).get('uuid'); + window.executor = new Executor(uuid); + </script> + ''' + return status, headers, content diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html b/testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html new file mode 100644 index 0000000000..ba1b3acb0c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="utils.sub.js"></script> +<script> +window.requestHeaders = { + purpose: "{{header_or_default(Purpose, )}}", + sec_purpose: "{{header_or_default(Sec-Purpose, )}}", + referer: "{{header_or_default(Referer, )}}", +}; + +const uuid = new URLSearchParams(location.search).get('uuid'); +window.executor = new Executor(uuid); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html.headers b/testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html.headers new file mode 100644 index 0000000000..0ee6ec2ab1 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/cacheable-executor.sub.html.headers @@ -0,0 +1 @@ +Cache-Control: private, max-age=604800 diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py new file mode 100644 index 0000000000..3ba9cd9270 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/cookies.py @@ -0,0 +1,41 @@ + +def main(request, response): + def get_cookie(key): + key = key.encode("utf-8") + if key in request.cookies: + return f'"{request.cookies[key].value.decode("utf-8")}"' + else: + return "undefined" + + purpose = request.headers.get("Purpose", b"").decode("utf-8") + sec_purpose = request.headers.get("Sec-Purpose", b"").decode("utf-8") + + cookie_count = int( + request.cookies[b"count"].value) if b"count" in request.cookies else 0 + response.set_cookie("count", f"{cookie_count+1}", + secure=True, samesite="None") + response.set_cookie( + "type", "prefetch" if sec_purpose.startswith("prefetch") else "navigate") + + headers = [(b"Content-Type", b"text/html"), (b"Cache-Control", b"no-store")] + + content = f''' + <!DOCTYPE html> + <script src="/common/dispatcher/dispatcher.js"></script> + <script src="utils.sub.js"></script> + <script> + window.requestHeaders = {{ + purpose: "{purpose}", + sec_purpose: "{sec_purpose}" + }}; + + window.requestCookies = {{ + count: {get_cookie("count")}, + type: {get_cookie("type")} + }}; + + const uuid = new URLSearchParams(location.search).get('uuid'); + window.executor = new Executor(uuid); + </script> + ''' + return headers, content diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html b/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html new file mode 100644 index 0000000000..ba1b3acb0c --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<script src="/common/dispatcher/dispatcher.js"></script> +<script src="utils.sub.js"></script> +<script> +window.requestHeaders = { + purpose: "{{header_or_default(Purpose, )}}", + sec_purpose: "{{header_or_default(Sec-Purpose, )}}", + referer: "{{header_or_default(Referer, )}}", +}; + +const uuid = new URLSearchParams(location.search).get('uuid'); +window.executor = new Executor(uuid); +</script> diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html.headers b/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html.headers new file mode 100644 index 0000000000..4030ea1d3d --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/executor.sub.html.headers @@ -0,0 +1 @@ +Cache-Control: no-store diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py new file mode 100644 index 0000000000..14ac4d1699 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch.py @@ -0,0 +1,17 @@ +from wptserve.handlers import json_handler + +@json_handler +def main(request, response): + uuid = request.GET[b"uuid"] + prefetch = request.headers.get( + "Sec-Purpose", b"").decode("utf-8").startswith("prefetch") + response.headers.set("Cache-Control", "no-store") + + n = request.server.stash.take(uuid) + if n is None: + n = 0 + if prefetch: + n += 1 + request.server.stash.put(uuid, n) + + return n diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py new file mode 100644 index 0000000000..d912eff90a --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/prefetch_nvs_hint.py @@ -0,0 +1,49 @@ +import time + +def main(request, response): + response.headers.set("Cache-Control", "no-store") + uuid = request.GET[b"uuid"] + wait_for_prefetch_start_uuid = None + if b"wait_for_prefetch_uuid" in request.GET: + wait_for_prefetch_start_uuid = request.GET[b"wait_for_prefetch_uuid"] + prefetch = request.headers.get( + "Sec-Purpose", b"").decode("utf-8").startswith("prefetch") + if b"unblock" in request.GET: + request.server.stash.put(uuid, 0) + return '' + + if b"wait_for_prefetch" in request.GET: + if wait_for_prefetch_start_uuid is None: + return '' + wait_for_prefetch = None + while wait_for_prefetch is None: + time.sleep(0.1) + wait_for_prefetch = request.server.stash.take(wait_for_prefetch_start_uuid) + return '' + + if b"nvs_header" in request.GET: + nvs_header = request.GET[b"nvs_header"] + response.headers.set("No-Vary-Search", nvs_header) + + if prefetch: + if wait_for_prefetch_start_uuid is not None: + request.server.stash.put(wait_for_prefetch_start_uuid, 0) + nvswait = None + while nvswait is None: + time.sleep(0.1) + nvswait = request.server.stash.take(uuid) + + content = (f'<!DOCTYPE html>\n' + f'<script src="/common/dispatcher/dispatcher.js"></script>\n' + f'<script src="utils.sub.js"></script>\n' + f'<script>\n' + f' window.requestHeaders = {{\n' + f' purpose: "{request.headers.get("Purpose", b"").decode("utf-8")}",\n' + f' sec_purpose: "{request.headers.get("Sec-Purpose", b"").decode("utf-8")}",\n' + f' referer: "{request.headers.get("Referer", b"").decode("utf-8")}",\n' + f' }};\n' + f' const uuid = new URLSearchParams(location.search).get("uuid");\n' + f' window.executor = new Executor(uuid);\n' + f'</script>\n') + + return content diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py b/testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py new file mode 100644 index 0000000000..97de1cc1a0 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/ruleset.py @@ -0,0 +1,49 @@ +def main(request, response): + url = request.GET[b"url"].decode("utf-8") + uuid = request.GET[b"uuid"].decode("utf-8") + page = request.GET[b"page"].decode("utf-8") + valid_json = request.GET[b"valid_json"].decode("utf-8") + empty_json = request.GET[b"empty_json"].decode("utf-8") + fail_cors = request.GET[b"fail_cors"].decode("utf-8") + valid_encoding = request.GET[b"valid_encoding"].decode("utf-8") + redirect = request.GET[b"redirect"].decode("utf-8") + sec_fetch_dest = request.headers[b"Sec-Fetch-Dest"].decode( + "utf-8").lower() if b"Sec-Fetch-Dest" in request.headers else None + content_type = b"application/speculationrules+json" if request.GET[ + b"valid_mime"].decode("utf-8") == "true" else b"application/json" + status = int(request.GET[b"status"]) + + if redirect == "true": + new_url = request.url.replace("redirect=true", + "redirect=false").encode("utf-8") + return 301, [(b"Location", new_url), + (b'Access-Control-Allow-Origin', b'*')], b"" + + encoding = "utf-8" if valid_encoding == "true" else "windows-1250" + content_type += f'; charset={encoding}'.encode('utf-8') + strparam = b'\xc3\xb7'.decode('utf-8') + + content = f''' + {{ + "prefetch": [ + {{ + "source":"list", + "urls":["{url}?uuid={uuid}&page={page}&str={strparam}"], + "requires":["anonymous-client-ip-when-cross-origin"] + }} + ] + }} + ''' + if empty_json == "true": + content = "" + elif valid_json != "true": + content = "invalid json" + elif sec_fetch_dest is None or sec_fetch_dest != "script": + content = "normal document" + + headers = [(b"Content-Type", content_type)] + if fail_cors != "true": + origin = request.headers[ + b"Origin"] if b"Origin" in request.headers else b'*' + headers.append((b'Access-Control-Allow-Origin', origin)) + return status, headers, content.encode(encoding) diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js b/testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js new file mode 100644 index 0000000000..dd8a9631b4 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/sw.js @@ -0,0 +1 @@ +self.addEventListener('fetch', event => event.respondWith(fetch(event.request))); diff --git a/testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js b/testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js new file mode 100644 index 0000000000..73624c0c25 --- /dev/null +++ b/testing/web-platform/tests/speculation-rules/prefetch/resources/utils.sub.js @@ -0,0 +1,195 @@ +/** + * Utilities for initiating prefetch via speculation rules. + */ + +// Resolved URL to find this script. +const SR_PREFETCH_UTILS_URL = new URL(document.currentScript.src, document.baseURI); +// Hostname for cross origin urls. +const PREFETCH_PROXY_BYPASS_HOST = "{{hosts[alt][]}}"; + +class PrefetchAgent extends RemoteContext { + constructor(uuid, t) { + super(uuid); + this.t = t; + } + + getExecutorURL(options = {}) { + let {hostname, username, password, protocol, executor, ...extra} = options; + let params = new URLSearchParams({uuid: this.context_id, ...extra}); + if(executor === undefined) { + executor = "executor.sub.html"; + } + let url = new URL(`${executor}?${params}`, SR_PREFETCH_UTILS_URL); + if(hostname !== undefined) { + url.hostname = hostname; + } + if(username !== undefined) { + url.username = username; + } + if(password !== undefined) { + url.password = password; + } + if(protocol !== undefined) { + url.protocol = protocol; + url.port = protocol === "https" ? "{{ports[https][0]}}" : "{{ports[http][0]}}"; + } + return url; + } + + // Requests prefetch via speculation rules. + // + // In the future, this should also use browser hooks to force the prefetch to + // occur despite heuristic matching, etc., and await the completion of the + // prefetch. + async forceSinglePrefetch(url, extra = {}, wait_for_completion = true) { + await this.execute_script((url, extra) => { + insertSpeculationRules({ prefetch: [{source: 'list', urls: [url], ...extra}] }); + }, [url, extra]); + if (!wait_for_completion) { + return Promise.resolve(); + } + return new Promise(resolve => this.t.step_timeout(resolve, 2000)); + } + + // `url` is the URL to navigate. + // + // `expectedDestinationUrl` is the expected URL after navigation. + // When omitted, `url` is used. + async navigate(url, {expectedDestinationUrl} = {}) { + await this.execute_script((url) => { + window.executor.suspend(() => { + location.href = url; + }); + }, [url]); + if (!expectedDestinationUrl) { + expectedDestinationUrl = url; + } + expectedDestinationUrl.username = ''; + expectedDestinationUrl.password = ''; + assert_equals( + await this.execute_script(() => location.href), + expectedDestinationUrl.toString(), + "expected navigation to reach destination URL"); + await this.execute_script(() => {}); + } + + async getRequestHeaders() { + return this.execute_script(() => requestHeaders); + } + + async getResponseCookies() { + return this.execute_script(() => { + let cookie = {}; + document.cookie.split(/\s*;\s*/).forEach((kv)=>{ + let [key, value] = kv.split(/\s*=\s*/); + cookie[key] = value; + }); + return cookie; + }); + } + + async getRequestCookies() { + return this.execute_script(() => window.requestCookies); + } + + async getRequestCredentials() { + return this.execute_script(() => window.requestCredentials); + } + + async setReferrerPolicy(referrerPolicy) { + return this.execute_script(referrerPolicy => { + const meta = document.createElement("meta"); + meta.name = "referrer"; + meta.content = referrerPolicy; + document.head.append(meta); + }, [referrerPolicy]); + } + + async getDeliveryType(){ + return this.execute_script(() => { + return performance.getEntriesByType("navigation")[0].deliveryType; + }); + } +} + +// Produces a URL with a UUID which will record when it's prefetched. +// |extra_params| can be specified to add extra search params to the generated +// URL. +function getPrefetchUrl(extra_params={}) { + let params = new URLSearchParams({ uuid: token(), ...extra_params }); + return new URL(`prefetch.py?${params}`, SR_PREFETCH_UTILS_URL); +} + +// Produces n URLs with unique UUIDs which will record when they are prefetched. +function getPrefetchUrlList(n) { + return Array.from({ length: n }, () => getPrefetchUrl()); +} + +async function isUrlPrefetched(url) { + let response = await fetch(url, {redirect: 'follow'}); + assert_true(response.ok); + return response.json(); +} + +// Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this. +async function spawnWindowWithReference(t, options = {}, uuid = token()) { + let agent = new PrefetchAgent(uuid, t); + let w = window.open(agent.getExecutorURL(options), '_blank', options); + t.add_cleanup(() => w.close()); + return {"agent":agent, "window":w}; +} + +// Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this. +async function spawnWindow(t, options = {}, uuid = token()) { + let agent_window_pair = await spawnWindowWithReference(t, options, uuid); + return agent_window_pair.agent; +} + +function insertSpeculationRules(body) { + let script = document.createElement('script'); + script.type = 'speculationrules'; + script.textContent = JSON.stringify(body); + document.head.appendChild(script); +} + +// Creates and appends <a href=|href|> to |insertion point|. If +// |insertion_point| is not specified, document.body is used. +function addLink(href, insertion_point=document.body) { + const a = document.createElement('a'); + a.href = href; + insertion_point.appendChild(a); + return a; +} + +// Inserts a prefetch document rule with |predicate|. |predicate| can be +// undefined, in which case the default predicate will be used (i.e. all links +// in document will match). +function insertDocumentRule(predicate, extra_options={}) { + insertSpeculationRules({ + prefetch: [{ + source: 'document', + eagerness: 'eager', + where: predicate, + ...extra_options + }] + }); +} + +function assert_prefetched (requestHeaders, description) { + assert_in_array(requestHeaders.purpose, ["", "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'."); + assert_in_array(requestHeaders.sec_purpose, + ["prefetch", "prefetch;anonymous-client-ip"], description); +} + +function assert_not_prefetched (requestHeaders, description){ + assert_equals(requestHeaders.purpose, "", description); + assert_equals(requestHeaders.sec_purpose, "", description); +} + +// Use nvs_header query parameter to ask the wpt server +// to populate No-Vary-Search response header. +function addNoVarySearchHeaderUsingQueryParam(url, value){ + if(value){ + url.searchParams.append("nvs_header", value); + } +} |