utils.sub.js (10067B)
1 /** 2 * Utilities for initiating prefetch via speculation rules. 3 */ 4 5 // Resolved URL to find this script. 6 const SR_PREFETCH_UTILS_URL = new URL(document.currentScript.src, document.baseURI); 7 8 // If (and only if) you are writing a test that depends on 9 // `requires: ["anonymous-client-ip-when-cross-origin"]`, then you must use this 10 // host as the cross-origin host. (If you need a generic cross-origin host, use 11 // `get_host_info().NOTSAMESITE_HOST` or similar instead.) 12 // 13 // TODO(domenic): document in the web platform tests server infrastructure that 14 // such a host must exist, and possibly separate it from `{{hosts[alt][]}}`. 15 const CROSS_ORIGIN_HOST_THAT_WORKS_WITH_ACIWCO = "{{hosts[alt][]}}"; 16 17 class PrefetchAgent extends RemoteContext { 18 constructor(uuid, t) { 19 super(uuid); 20 this.t = t; 21 } 22 23 getExecutorURL(options = {}) { 24 let {hostname, username, password, protocol, executor, ...extra} = options; 25 let params = new URLSearchParams({uuid: this.context_id, ...extra}); 26 if(executor === undefined) { 27 executor = "executor.sub.html"; 28 } 29 let url = new URL(`${executor}?${params}`, SR_PREFETCH_UTILS_URL); 30 if(hostname !== undefined) { 31 url.hostname = hostname; 32 } 33 if(username !== undefined) { 34 url.username = username; 35 } 36 if(password !== undefined) { 37 url.password = password; 38 } 39 if(protocol !== undefined) { 40 url.protocol = protocol; 41 url.port = protocol === "https" ? "{{ports[https][0]}}" : "{{ports[http][0]}}"; 42 } 43 return url; 44 } 45 46 // Requests prefetch via speculation rules. 47 // 48 // In the future, this should also use browser hooks to force the prefetch to 49 // occur despite heuristic matching, etc., and await the completion of the 50 // prefetch. 51 async forceSinglePrefetch(url, extra = {}, wait_for_completion = true) { 52 return this.forceSpeculationRules( 53 { 54 prefetch: [{source: 'list', urls: [url], ...extra}] 55 }, wait_for_completion); 56 } 57 58 async forceSpeculationRules(rules, wait_for_completion = true) { 59 await this.execute_script((rules) => { 60 insertSpeculationRules(rules); 61 }, [rules]); 62 if (!wait_for_completion) { 63 return Promise.resolve(); 64 } 65 return new Promise(resolve => this.t.step_timeout(resolve, 2000)); 66 } 67 68 // `url` is the URL to navigate. 69 // 70 // `expectedDestinationUrl` is the expected URL after navigation. 71 // When omitted, `url` is used. When explicitly null, the destination URL is 72 // not validated. 73 async navigate(url, {expectedDestinationUrl} = {}) { 74 await this.execute_script((url) => { 75 window.executor.suspend(() => { 76 location.href = url; 77 }); 78 }, [url]); 79 if (expectedDestinationUrl === undefined) { 80 expectedDestinationUrl = url; 81 } 82 if (expectedDestinationUrl) { 83 expectedDestinationUrl.username = ''; 84 expectedDestinationUrl.password = ''; 85 assert_equals( 86 await this.execute_script(() => location.href), 87 expectedDestinationUrl.toString(), 88 "expected navigation to reach destination URL"); 89 } 90 await this.execute_script(() => {}); 91 } 92 93 async getRequestHeaders() { 94 return this.execute_script(() => requestHeaders); 95 } 96 97 async getResponseCookies() { 98 return this.execute_script(() => { 99 let cookie = {}; 100 document.cookie.split(/\s*;\s*/).forEach((kv)=>{ 101 let [key, value] = kv.split(/\s*=\s*/); 102 cookie[key] = value; 103 }); 104 return cookie; 105 }); 106 } 107 108 async getRequestCookies() { 109 return this.execute_script(() => window.requestCookies); 110 } 111 112 async getRequestCredentials() { 113 return this.execute_script(() => window.requestCredentials); 114 } 115 116 async setReferrerPolicy(referrerPolicy) { 117 return this.execute_script(referrerPolicy => { 118 const meta = document.createElement("meta"); 119 meta.name = "referrer"; 120 meta.content = referrerPolicy; 121 document.head.append(meta); 122 }, [referrerPolicy]); 123 } 124 125 async getDeliveryType(){ 126 return this.execute_script(() => { 127 return performance.getEntriesByType("navigation")[0].deliveryType; 128 }); 129 } 130 } 131 132 // Produces a URL with a UUID which will record when it's prefetched. 133 // |extra_params| can be specified to add extra search params to the generated 134 // URL. 135 function getPrefetchUrl(extra_params={}) { 136 let params = new URLSearchParams({ uuid: token(), ...extra_params }); 137 return new URL(`prefetch.py?${params}`, SR_PREFETCH_UTILS_URL); 138 } 139 140 // Produces n URLs with unique UUIDs which will record when they are prefetched. 141 function getPrefetchUrlList(n) { 142 return Array.from({ length: n }, () => getPrefetchUrl()); 143 } 144 145 async function isUrlPrefetched(url) { 146 let response = await fetch(url, {redirect: 'follow'}); 147 assert_true(response.ok); 148 return response.json(); 149 } 150 151 // Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this. 152 async function spawnWindowWithReference(t, options = {}, uuid = token()) { 153 let agent = new PrefetchAgent(uuid, t); 154 let w = window.open(agent.getExecutorURL(options), '_blank', options); 155 t.add_cleanup(() => w.close()); 156 return {"agent":agent, "window":w}; 157 } 158 159 // Must also include /common/utils.js and /common/dispatcher/dispatcher.js to use this. 160 async function spawnWindow(t, options = {}, uuid = token()) { 161 let agent_window_pair = await spawnWindowWithReference(t, options, uuid); 162 return agent_window_pair.agent; 163 } 164 165 function insertSpeculationRules(body) { 166 let script = document.createElement('script'); 167 script.type = 'speculationrules'; 168 script.textContent = JSON.stringify(body); 169 document.head.appendChild(script); 170 } 171 172 // Creates and appends <a href=|href|> to |insertion point|. If 173 // |insertion_point| is not specified, document.body is used. 174 function addLink(href, insertion_point=document.body) { 175 const a = document.createElement('a'); 176 a.href = href; 177 insertion_point.appendChild(a); 178 return a; 179 } 180 181 // Inserts a prefetch document rule with |predicate|. |predicate| can be 182 // undefined, in which case the default predicate will be used (i.e. all links 183 // in document will match). 184 function insertDocumentRule(predicate, extra_options={}) { 185 insertSpeculationRules({ 186 prefetch: [{ 187 source: 'document', 188 eagerness: 'immediate', 189 where: predicate, 190 ...extra_options 191 }] 192 }); 193 } 194 195 function assert_prefetched (requestHeaders, description) { 196 assert_in_array(requestHeaders.purpose, [undefined, "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'."); 197 assert_in_array(requestHeaders['sec-purpose'], 198 ["prefetch", "prefetch;anonymous-client-ip"], description); 199 } 200 201 function assert_prefetched_anonymous_client_ip(requestHeaders, description) { 202 assert_in_array(requestHeaders.purpose, [undefined, "prefetch"], "The vendor-specific header Purpose, if present, must be 'prefetch'."); 203 assert_equals(requestHeaders['sec-purpose'], 204 "prefetch;anonymous-client-ip", 205 description); 206 } 207 208 function assert_not_prefetched (requestHeaders, description){ 209 assert_equals(requestHeaders.purpose, undefined, description); 210 assert_equals(requestHeaders['sec-purpose'], undefined, description); 211 } 212 213 // If the prefetch request is intercepted and modified by ServiceWorker, 214 // - "Sec-Purpose: prefetch" header is dropped in Step 33 of 215 // https://fetch.spec.whatwg.org/#dom-request 216 // because it's a https://fetch.spec.whatwg.org/#forbidden-request-header. 217 // - "Purpose: prefetch" can still be sent. 218 // Note that this check passes also for non-prefetch requests, so additional 219 // checks are needed to distinguish from non-prefetch requests. 220 function assert_prefetched_without_sec_purpose(requestHeaders, description) { 221 assert_in_array(requestHeaders.purpose, [undefined, "prefetch"], 222 "The vendor-specific header Purpose, if present, must be 'prefetch'."); 223 assert_equals(requestHeaders['sec-purpose'], undefined, description); 224 } 225 226 // For ServiceWorker tests. 227 // `interceptedRequest` is an element of `interceptedRequests` in 228 // `resources/basic-service-worker.js`. 229 230 // The ServiceWorker fetch handler intercepted a prefetching request. 231 function assert_intercept_prefetch(interceptedRequest, expectedUrl) { 232 assert_equals(interceptedRequest.request.url, expectedUrl.toString(), 233 "intercepted request URL."); 234 235 assert_prefetched(interceptedRequest.request.headers, 236 "Prefetch request should be intercepted."); 237 238 if (new URL(location.href).searchParams.has('clientId')) { 239 // https://github.com/WICG/nav-speculation/issues/346 240 // https://crbug.com/404294123 241 assert_equals(interceptedRequest.resultingClientId, "", 242 "resultingClientId shouldn't be exposed."); 243 244 // https://crbug.com/404286918 245 // `assert_not_equals()` isn't used for now to create stable failure diffs. 246 assert_false(interceptedRequest.clientId === "", 247 "clientId should be initiator."); 248 } 249 } 250 251 // The ServiceWorker fetch handler intercepted a non-prefetching request. 252 function assert_intercept_non_prefetch(interceptedRequest, expectedUrl) { 253 assert_equals(interceptedRequest.request.url, expectedUrl.toString(), 254 "intercepted request URL."); 255 256 assert_not_prefetched(interceptedRequest.request.headers, 257 "Non-prefetch request should be intercepted."); 258 259 if (new URL(location.href).searchParams.has('clientId')) { 260 // Because this is an ordinal non-prefetch request, `resultingClientId` 261 // can be set as normal. 262 assert_not_equals(interceptedRequest.resultingClientId, "", 263 "resultingClientId can be exposed."); 264 265 assert_not_equals(interceptedRequest.clientId, "", 266 "clientId should be initiator."); 267 } 268 } 269 270 function assert_served_by_navigation_preload(requestHeaders) { 271 assert_equals( 272 requestHeaders['service-worker-navigation-preload'], 273 'true', 274 'Service-Worker-Navigation-Preload'); 275 } 276 277 // Use nvs_header query parameter to ask the wpt server 278 // to populate No-Vary-Search response header. 279 function addNoVarySearchHeaderUsingQueryParam(url, value){ 280 if(value){ 281 url.searchParams.append("nvs_header", value); 282 } 283 }